Once a system is constructed on solid foundations, it must be used correctly to maintain its integrity. Building a sea-fort to defend an island from pirates is half the battle, followed by posting guards to the watchtower and being prepared for defense at any time.
Like the orders to the fort’s guards, the policies applied to a cluster define the range of behaviors allowed. For example, what security configuration options a pod must use, storage and network options, container images, and any other feature of the workloads.
Policies must be synchronized across clusters and cloud (admission controllers,
IAM policy, security sidecars, service mesh, seccomp
and AppArmor profiles)
and enforced. And policies must target workloads, which raises a question of identity.
Can we prove the identity of a workload before giving it privileges?
In this chapter we look at what happens when policies are not enforced, how identity for workloads and operators should be managed, and how the Captain would try to engage with potential holes in our defensive walls.
We will first review different types of policies and discuss the out-of-the-box (OOTB) features of Kubernetes in this area. Then we move on to threat models and common expectations concerning policies such as auditing. The bulk of the chapter we spend with the access control topic, specifically around role-based access control (RBAC) and further on we investigate the generic handling of policies for Kubernetes, based on projects such as the Open Policy Agent (OPA) and Kyverno.
In real-world scenarios—that is, when you’re running workloads in production—in the context of a business, you have to consider different types of policies:
These are usually well understood and straightforward to implement (for example, runtime or network communication policies).
Arriving at these policies can, depending on the organization, be challenging (for example, “developers only deploy to test and dev environments”).
These policies are dependent on the vertical your workload is operating in and can, depending on the level of compliance, take a lot of time and energy to implement (for example, the Payment Card Industry Data Security Standards [PCI DSS] policy that cardholder data transmitted across open, public networks must be encrypted).
In the context of this chapter, we focus mainly on how to define and enforce policies that can be explicitly stated. In Chapter 10 we go further and look at the organizational context and how usually, despite all policies in place, the human user (as the weakest link in the chain), provides the Captain with a welcome entry angle.
Let’s first have a look at what Kubernetes brings, by default, to the table.
Policy is essential to keeping Kubernetes secure, but by default little is enabled. Configuration mutates with time for most software as new features come out; misconfiguration is a common attack vector, and Kubernetes is no different.
Reusing and extending open source policy configuration for your needs is often safer than rolling out your own, and to protect against regressions you must test your infrastructure and security code with tools like conftest before you deploy it; in “Open Policy Agent” we will dive deeper into this topic.
Figure 8-1, sums up the sentiment nicely. In it, Kubernetes security practitioner Brad Geesaman points out the dangers of not having admission control enabled by default; see also the respective TGIK episdode.
Now, what are the defaults that the Captain might be able to exploit, if you’re asleep at the helm?
Kubernetes offers out-of-the-box support for some policies, including for controlling network traffic, limiting resource usage, runtime behavior, and most prominently for access control, which we will dive deeper into in “Authentication and Authorization” and “Role-Based Access Control (RBAC)” before we shift our attention to generic policies in “Generic Policy Engines”.
Let’s have a closer look now at the defaults and see what challenges we face.
The NetworkPolicy
resource, in conjunction with a CNI plug-in that
enforces it, allow us to put policies constraining network traffic in place (also see Chapter 5).
In Kubernetes, by default, containers in pods are not restricted concerning compute resource consumption. Since Kubernetes 1.10 you can use LimitRanges to constrain container and pod resource allocations on a per-namespace basis. This policy type is enforced via an admission controller, with the implication that it doesn’t apply to running pods.
To see how LimitRanges work in action, assume you want to limit the memory
containers can use in the dev
namespace to 2 GB of RAM. You would define the
policy like so:
apiVersion
:
v1
kind
:
LimitRange
metadata
:
name
:
dev-mem-limits
spec
:
limits
:
-
type
:
Container
max
:
memory
:
2Gi
Assuming you stored the preceding YAML snippet in a file called dev-mem-limits.yaml you would then, in order to enforce the limit range, execute the following command:
kubectl -n dev apply -f dev-mem-limits.yaml
If you now tried to create a pod with a container that attempts to use more memory, you’d get an error message of type 403 Forbidden.
In a multitenant environment, where a cluster is shared among multiple teams, a particular team could potentially use more than its fair share of the available resources as provided by the worker nodes (CPU, RAM, etc.). Resource quotas are a policy type allowing you to control these quotas.
Certain Kubernetes distributions, such as OpenShift, for example, extend namespaces in a way (there it’s called “project”) that things like resource quotas are available and enforced out of the box.
For concrete usages, peruse the in-depth article “How to Use Kubernetes Resource Quotas” and also check out the Google Cloud blog post on the topic, “Kubernetes Best Practices: Resource Requests and Limits”.
In addition, since Kubernetes v1.20 there is also a possibility to limit the number of process IDs a pod may use on a per-node basis.
Pod Security Policies (PSPs) allow you to define fine-grained authorization of pod creation and updates.
Let’s say you want to set default seccomp
and AppArmor profiles with PSPs, as also shown in the canonical docs example:
apiVersion
:
policy/v1beta1
kind
:
PodSecurityPolicy
metadata
:
name
:
restricted
annotations
:
seccomp.security.alpha.kubernetes.io/allowedProfileNames
:
'docker/default,runtime/default'
apparmor.security.beta.kubernetes.io/allowedProfileNames
:
'runtime/default'
seccomp.security.alpha.kubernetes.io/defaultProfileName
:
'runtime/default'
apparmor.security.beta.kubernetes.io/defaultProfileName
:
'runtime/default'
spec
:
# ...
There is an issue with PSPs, though. They are at time of writing of the book in the process of being deprecated.
Increasingly, organizations are looking into the OPA Constraints Framework as a replacement for PSPs, so maybe this is something you want to consider as well.
The good news is that replacements for PSPs exist: upstream, they are replaced by Pod Security Standards (PSS) (the Aqua Security blog post “Kubernetes Pod Security Policy Deprecation: All You Need to Know” goes into further detail here), and alternatively you can use frameworks discussed in “Generic Policy Engines” to cover runtime policies.
Kubernetes is, concerning authentication and authorization, flexible and extensible. We discuss the details of access control policies in “Authentication and Authorization” and specifically role-based access control (RBAC) in “Role-Based Access Control (RBAC)”.
Now, with the overview on built-in policies in Kubernetes out of the way, what does the threat modeling in the policies space look like? Let’s find out.
The threat model relevant in the context of policies is broad, however sometimes they may subtly be hidden within other topics and/or not explicitly called out. Let’s have a look at some scenarios of past attacks pertinent to the policy space using examples from the 2016 to 2019 time frame:
CVE-2016-5392 describes an attack where the API server (in a multitenant environment) allowed remote authenticated users with knowledge of other project names to obtain sensitive project and user information via vectors related to the watch-cache list.
Certain versions of CoreOS Tectonic mount a direct proxy to the cluster at /api/kubernetes/, accessible without authentication to and allowing an attacker to directly connect to the API server, as observed in CVE-2018-5256.
In CVE-2019-3818, the kube-rbac-proxy
container did not honor TLS configurations, allowing for use of insecure
ciphers and TLS 1.0. An attacker could target traffic sent over a TLS
connection with a weak configuration and potentially break the encryption.
In CVE-2019-11245 we see
how an attacker could exploit the fact that certain kubelet
versions
did not specify an explicit runAsUser
attempt to run as UID 0 (root)
on container restart, or if the image was previously pulled to the node.
As per CVE-2019-11247 the Kubernetes API server mistakenly allowed access to a cluster-scoped custom resource if the request was made as if the resource were namespaced. Authorizations for the resource accessed in this manner are enforced using roles and role bindings within the namespace, meaning that a user with access only to a resource in one namespace could create, view, update, or delete the cluster-scoped resource.
In CVE-2020-8554 it’s possible for an attack to person-in-the-middle traffic, which in multitenant environments may intercept traffic to other tenants. The new DenyServiceExternalIPs admission controller was added as there is currently no patch for this issue.
In the following sections, we review some common expectations—that is, policy-related situations and methods that are well-established—and how they are addressed by defaults in Kubernetes and, in case there are no OOTB functions available, point to examples that work on top of Kubernetes.
When we say breakglass scenario we routinely think of a process to bypass the default access control regime, in case of an emergency. The emergency could be an external event like a natural disaster or an attacker trying to mess with your cluster. If such a functionality is provided, the breakglass accounts offered are usually highly privileged (so to stop the bleeding) and oftentimes time-boxed. As breakglass access is granted, what happens in the background is that owners are notified and the account is recorded for auditing.
While Kubernetes does not ship with breakglass features by default, there are examples, such as GKE’s binary authorization breakglass capability, that show how this might work in practice.
Kubernetes comes with auditing built in. In the API server, each request generates an audit event, which is preprocessed according to a policy that states what is recorded and then written to a backend; currently logfiles and webhooks (sends events to an external HTTP API) are supported.
The configurable audit levels range from None
(do not record event) to
RequestResponse
(record event metadata, request and response bodies).
An example policy to capture events on ConfigMaps may look as follows:
apiVersion
:
audit.k8s.io/v1
kind
:
Policy
rules
:
-
level
:
Request
resources
:
-
group
:
""
resources
:
[
"configmaps"
]
The OOTB auditing features of Kubernetes are a good starting point and many security and observability vendors offer, based on it, additional functionality, be it a more convenient interface or integrations with destinations, including but not limited to the following:
Sysdig “Kubernetes Audit Logging”
Datadog “Kubernetes Audit Logs”
Splunk/Outcold “Monitoring Kubernetes: Metrics and Log Forwarding”
As a good practice, enable auditing and try to find the right balance between verbosity (audit level) and retention period.
If you consider a Kubernetes cluster, there are different types of resources, both in-cluster (such as a pod or a namespace) as well as out-of-cluster (for example, the load balancer of your cloud provider), that a service may provision. In this section we will dive into the topic of defining and checking the access a person or a program requires to access resources necessary to carry out a task.
In the context of access control, when we say authorization we mean the process of checking the permissions concerning a certain action, for example to create or delete a resource, for a given identity. This identity can represent a human user or a program, which we usually refer to as workload identity. Verifying the identity of a subject, human or machine, is called authentication.
Figure 8-2 shows, on a high level, how the access to resources works in a Kubernetes cluster, covering the authentication and authorization steps.
The first step in the API server is the authentication of the request via one or more of the configured authentication modules such as client certificates, passwords, or JSON Web Tokens (JWT). If an API server cannot authenticate the request, it rejects it with a 401 HTTP status. However, if the authentication succeeds, the API server moves on to the authorization step.
In this step the API server uses one of the configured authorization modules to determine if the access is allowed; it takes the credentials along with the requested path, resource (pod, service, etc.) and verb (create, get, etc.), and if at least one module grants access, the request is allowed. If the authorization fails, an 403 HTTP status code is returned. The most widely used authorization module nowadays is RBAC (see “Role-Based Access Control (RBAC)”).
In the following sections, we will first review the defaults Kubernetes has, show how those can be attacked, and subsequently discuss how to monitor and defend against attacks in the access control space.
Kubernetes does not consider human users as first-class citizens, in contrast to machines (or applications), which are represented by so-called service accounts (see “Service accounts”). In other words, there are no core Kubernetes resources representing human users in Kubernetes proper.
In practice, organizations oftentimes want to map Kubernetes cluster users to existing user directories such as LDAP servers like Azure Directory and ideally provide single sign-on (SSO).
As usual, there are the two options available: buy or build. If you’re using the Kubernetes distribution of your cloud provider, check the integrations there. If you’re looking into building out SSO yourself, there are a number of open source tools available that allow you to do this:
OpenID Connect (OIDC)/OAuth 2.0–based solutions, such as available via Dex.
Security Assertion Markup Language (SAML)–based solutions, such as offered by Teleport.
In addition, there are more complete open source offerings such as Keycloak, supporting a range of use cases from SSO to policy enforcement.
While humans don’t have a native representation in Kubernetes, your workload does.
In contrast to human users, workloads such as a deployment owning pods are indeed first-class citizens in Kubernetes.
By default, a service account represents the identity of an app in Kubernetes. A service account is a namespaced resource that can be used in the context of a pod to authenticate your app against the API server. Its canonical form is as follows:
system:serviceaccount:NAMESPACE:NAME
As part of the control plane, three controllers jointly implement the service account automation, that is, managing Secrets and tokens:
The ServiceAccount admission controller, part of the API server, acts on pod
creation and update. The controller checks if service account used by the
pod exists, and in case it does not, rejects the pod (or, if no service
account is specified, uses the default
service account). In addition, it
manages a volume, making the service account available via a well-known location:
/var/run/secrets/kubernetes.io/serviceaccount.
The TokenController, part of the control plane component called controller manager, watches service accounts and creates or deletes the respective tokens. These are JSON Web Tokens (JWT) as defined in RFC 7519.
The ServiceAccount controller, also part of the controller manager,
ensures that in every namespace a service account default
exists.
For example, the default
service account in the kube-system
namespace
would be referred to as system:serviceaccount:kube-system:default
and would
look something like the following:
apiVersion
:
v1
kind
:
ServiceAccount
metadata
:
name
:
default
namespace
:
kube-system
secrets
:
-
name
:
default-token-v9vsm
The default
service account
In the kube-system
namespace
Using the Secret with the name default-token-v9vsm
We saw that the default
service account uses a Secret called
default-token-v9vsm
, so let have a look at it with
kubectl -n kube-system get secret default-token-v9vsm -o yaml
, which yields
the following YAML doc (edited to fit):
apiVersion
:
v1
kind
:
Secret
metadata
:
annotations
:
kubernetes.io/service-account.name
:
default
name
:
default-token-v9vsm
namespace
:
kube-system
type
:
kubernetes.io/service-account-token
data
:
ca.crt
:
LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tL...==
namespace
:
a3ViZS1zeXN0ZW0=
token
:
ZXlKaGJHY2lPaUpTVXpJMU5pSXNJbXRwWk...==
Your application can use the data managed by the control plane components as described previously from within the pod. For example, from inside a container, the volume is available at:
~$
ls -al /var/run/secrets/kubernetes.io/serviceaccount/ total 4 drwxrwxrwt3
root root140
Jun16
11:31 . drwxr-xr-x3
root root4096
Jun16
11:31 .. drwxr-xr-x2
root root100
Jun16
11:31 ..2021_06_16_11_31_31.83035518 lrwxrwxrwx1
root root31
Jun16
11:31 ..data -> ..2021_06_16_11_31_31.83035518 lrwxrwxrwx1
root root13
Jun16
11:31 ca.crt -> ..data/ca.crt lrwxrwxrwx1
root root16
Jun16
11:31 namespace -> ..data/namespace lrwxrwxrwx1
root root12
Jun16
11:31 token -> ..data/token
The JWT token that the TokenController created is readily available for you:
~ $
cat /var/run/secrets/kubernetes.io/serviceaccount/token
eyJhbGciOiJSUzI1NiIsImtpZCI6InJTT1E1VDlUX1ROZEpRMmZSWi1aVW0yNWVocEh.
...
Service accounts are regularly used as building blocks and can be combined
with other mechanisms such as projected volumes
(discussed in Chapter 6, and the kubelet
for workload identity management.
For example, the EKS feature IAM roles for service accounts demonstrates such a combination in action.
While handy, the service account does not provide for a cryptographically strong workload identity out-of-the-box and hence may be not sufficient for certain use cases.
Secure Production Identity Framework for Everyone (SPIFFE) is a Cloud Native Computing Foundation (CNCF) project that establishes identities for your workloads. SPIRE is a production-ready reference implementation of the SPIFFE APIs allowing performance of node and workload attestation; that is, you can automatically assign cryptographically strong identities to resources like pods.
In SPIFFE, a workload is a program deployed using a specific configuration, defined in the context of a trust domain, such as a Kubernetes cluster. The identity of the workload is in the form of a so-called SPIFFE ID, which comes in the general schema shown as follows:
spiffe://trust-domain/workload-identifier
An SVID (short for SPIFFE Verifiable Identity Document) is the document, for example a X.509 certificate JWT token, a workload proves its identity toward a caller. The SVID is valid if it has been signed by an authority in the trust domain.
If you are not familiar with SPIFFE and want to read up on it, we recommend having a look at the terminology section of the SPIFFE docs.
With this we’ve reached the end of the general authentication and authorization discussion and focus now on a central topic in Kubernetes security: role-based access control.
Nowadays, the default mechanism for granting humans and workloads access to resources in Kubernetes is role-based access control (RBAC).
We will first review the defaults, then discuss how to understand RBAC using tools to analyze and visualize the relations, and finally we review attacks in this space.
In the context of RBAC we use the following terminology:
A resource is something (like a namespace or deployment) we want to provide access to.
A role is used to define conditions for actions on resources.
A role binding attaches a role to an identity, effectively representing the permissions of a set of actions concerning specified resources.
Allowed actions of an identity on a given resource are called verbs that come
in two flavors: read-only ones (get
and list
) and read-write ones (create
,
update
, patch
, delete
, and deletecollection
). Further, the scope of
a role can be cluster-wide or in the context of a Kubernetes namespace.
By default, Kubernetes comes with privilege escalation prevention. That is, users can create or update a role only if they already have all the permissions contained in the role.
There are two types of roles in Kubernetes: roles and cluster roles. The difference is the scope: the former is only relevant and valid in the context of a namespace, whereas the latter works cluster-wide. The same is true for the respective bindings.
Last but not least, Kubernetes defines a number of default roles you might want to review before defining your own roles (or use them as starting points).
For example, there’s a default cluster role called edit
predefined (note that
the output has been cut down to fit):
$
kubectl describe clusterrole edit Name: edit Labels: kubernetes.io/bootstrapping=
rbac-defaults rbac.authorization.k8s.io/aggregate-to-admin=
true
Annotations: rbac.authorization.kubernetes.io/autoupdate:true
PolicyRule: Resources Non-Resource URLs Resource Names Verbs --------- ----------------- -------------- ----- configmaps[]
[]
[
create delete ... watch]
...
In this section, we have a look at a simple RBAC example: assume you want to give a
developer joey
the permission to view resources of type deployments
in the yolo
namespace.
Let’s first create a cluster role called view-deploys
that defines the
actions allowed for the targeted resources with the following command:
$
kubectl create clusterrole view-deploys\
--verb=
get --verb=
list\
--resource=
deployments
The preceding command creates a resource with a YAML representation as shown in the following:
apiVersion
:
rbac.authorization.k8s.io/v1
kind
:
ClusterRole
metadata
:
name
:
view-deploys
rules
:
-
apiGroups
:
-
apps
resources
:
-
deployments
verbs
:
-
get
-
list
Next, we equip the targeted principal with the cluster role we created in the
previous step. This is achieved by the following command that binds the
view-deploys
cluster role to the user joey
:
$
kubectl create rolebinding assign-perm-view-deploys\
--role=
view-deploys\
--user=
joey\
--namespace=
yolo
When you execute this command you create a resource with a YAML representation like so:
apiVersion
:
rbac.authorization.k8s.io/v1
kind
:
RoleBinding
metadata
:
name
:
assign-perm-view-deploys
namespace
:
yolo
roleRef
:
apiGroup
:
rbac.authorization.k8s.io
kind
:
Role
name
:
view-deploys
subjects
:
-
apiGroup
:
rbac.authorization.k8s.io
kind
:
User
name
:
joey
The scope of the role binding
The cluster role we want to use (bind)
The targeted principal (subject) to bind the cluster role to
Now, looking at a bunch of YAML code to determine what the permissions are is usually not the way you want to go. Given its graph nature, usually you want some visual representation, something akin to what is depicted in Figure 8-3.
For this case it looks pretty straightforward, but alas the reality is much more complicated and messy. Expect to deal with hundreds of roles, bindings, and subjects and actions across core Kubernetes resources as well as custom resource definitions (CRDs).
So, how can you figure out what’s going on, how can you truly understand the RBAC setup in your cluster? As usual, the answer is: additional software.
According to the least privileges principle, you should only grant exactly the permissions necessary to carry out a specific task. But how do you arrive at the exact permissions? Too few means the task will fail, but too much power can yield a field day for attackers. A good way to go about this is to automate it: let’s have a look at a small but powerful tool called audit2rbac
that can generate Kubernetes RBAC roles and role bindings covering API requests made by a user.
As a concrete example we’ll use an EKS cluster running in AWS. First, install awslogs and also audit2rbac for your platform.
For the following you need two terminal sessions as we use the first
command (awslogs
) in a blocking mode.
First, in one terminal session, create the audit log by tailing the CloudWatch
output as follows (note, you can also directly pipe into audit2rbac
):
$
awslogs get /aws/eks/example/cluster\
"kube-apiserver-audit*"
\
--no-stream --no-group --watch\
>> audit-log.json
While the awslogs
snippet shown here uses an AWS-specific method to grab the logs, the principle stays the same. For example, to
view GKE logs you could use gcloud logging read
and AKS offers a similar way to access logs.
Now, in another terminal session, execute the kubectl
command with the user you want to create the RBAC setting for. In the case shown we’re already logged in as said user, otherwise you can impersonate them with --as
.
Let’s say you want to generate the necessary role and binding for listing all the default resources (such as pods, services, etc.) across all namespaces. You would use the following command (note that the output is not shown):
$
kubectl get all -A
...
At this point we should have the audit log in audit-log.json and can use it
as an input for audit2rbac
as shown in the following. Let’s
consume the audit log and create RBAC roles and bindings for a specific user:
$
audit2rbac
--user
kubernetes-admin
\
--filename
audit-log.json
\
>
list-all.yaml
Opening
audit
source...
Loading
events....
Evaluating
API
calls...
Generating
roles...
Complete!
After running the preceding command, the resulting RBAC resources, comprising a cluster role and a cluster
role binding that permit the user kubernetes-admin
to successfully execute
kubectl get all -A
, is now available in list-all.yaml (note that the output has been trimmed):
apiVersion
:
rbac.authorization.k8s.io/v1
kind
:
ClusterRole
metadata
:
annotations
:
audit2rbac.liggitt.net/version
:
v0.8.0
labels
:
audit2rbac.liggitt.net/generated
:
"
true
"
audit2rbac.liggitt.net/user
:
kubernetes-admin
name
:
audit2rbac:kubernetes-admin
rules
:
-
apiGroups
:
-
"
"
resources
:
-
pods
-
replicationcontrollers
-
services
verbs
:
-
get
-
list
-
watch
...
---
apiVersion
:
rbac.authorization.k8s.io/v1
kind
:
ClusterRoleBinding
metadata
:
annotations
:
audit2rbac.liggitt.net/version
:
v0.8.0
labels
:
audit2rbac.liggitt.net/generated
:
"
true
"
audit2rbac.liggitt.net/user
:
kubernetes-admin
name
:
audit2rbac:kubernetes-admin
roleRef
:
apiGroup
:
rbac.authorization.k8s.io
kind
:
ClusterRole
name
:
audit2rbac:kubernetes-admin
subjects
:
-
apiGroup
:
rbac.authorization.k8s.io
kind
:
User
name
:
kubernetes-admin
The generated cluster role allowing you to list the default resources across all namespaces
The binding, giving the user kubernetes-admin
the permissions
There’s also a krew
plug-in called who-can
allowing you to gather the same information, quickly.
That was some (automagic) entertainment, was it not? Automating the creation of the roles helps you in enforcing least privileges as otherwise the temptation to simply “give access to everything to make it work” is indeed a big one, playing into the hands of the Captain and their greedy crew.
Next up: how to read and understand RBAC in a scalable manner.
Given their nature, with RBAC you end up with a huge forest of directed acyclic graph (DAGs), including the subjects, roles, their bindings, and actions. Trying to manually comprehend the connections is almost impossible, so you want to either visualize the graphs and/or use tooling to query for specific paths.
To address the challenge of discovering RBAC tooling and good practices, we maintain rbac.dev, open to suggestions for additions via issues and pull requests.
For example, let’s assume you would like to perform a static analysis on your RBAC setup. You could consider using krane, a tool that identifies potential security risks and also makes suggestions on how to mitigate those.
To demonstrate RBAC visualization in action, let’s walk through two examples.
The first example to visualize RBAC is a krew plug-in
called rbac-view
(Figure 8-4) that you can run as follows:
$
kubectl rbac-view INFO[
0000]
Getting K8s client INFO[
0000]
serving RBAC View and http://localhost:8800 INFO[
0010]
Building full matrixfor
json INFO[
0010]
Building Matrixfor
Roles INFO[
0010]
Retrieving RoleBindings INFO[
0010]
Building Matrixfor
ClusterRoles ...
Then you open the link provided, here http://localhost:8800
, in a browser and
can interactively view and query roles.
The second example is a CLI tool called rback,
invented and codeveloped by one of the authors. rback
queries
RBAC-related information and generates a graph representation of service accounts,
(cluster) roles, and the access rules in dot
format:
$
kubectl
get
sa,roles,rolebindings,clusterroles,clusterrolebindings
\
--all-namespaces
\
-o
json
|
rback
|
dot
-Tpng
>
rback-output.png
List the resources to include in the graph.
Set the scope (in our case: cluster-wide).
Feed the resulting JSON into rback
via stdin
.
Feed the rback
output in dot
format to the dot
program to generate the
image rback-output.png
.
If you do have dot installed you would find the output in the file called rback-output.png, which would look something like shown in Figure 8-5.
There are not that many RBAC-related attacks found in the wild, indicated by CVEs. The basic patterns include:
Too-loose permissions. Oftentimes, due to time constraints or not being aware of the issue, more permissions than actually needed to carry out a task are granted. For example, you want to state people are allowed to manage deployments and really all they need is to list and describe them, but you also give edit rights to them. This is violating the least privileges principle and a skilled attacker can misuse this setting.
Demarcation line blurry. The shared responsibilities model in the context of running containers in a cloud environment might not always be super clear. For example, while it’s usually clear who is responsible for patching the worker nodes, it’s not always explicit who maintains application packages and their dependencies. Too liberal RBAC settings suggested as defaults can, if not properly reviewed, lead to an attack vector both subtle—as in: “ah, I thought you are taking care of it”—and potentially with an unwelcome outcome when the T&C of the service have not been carefully perused.
Prior to Helm 3, there was an overly privileged component present that caused all sorts of security concerns, especially confused deputy situations. While this is less and less of an issue, you might want to double check if there’s still some Helm 2 used in your clusters.
With the RBAC fun wrapped up, let’s now move on to the topic of generic policy handling and engines for said purpose. The basic idea being that, rather than hardcode certain policy types, making them part of Kubernetes proper, one has a generic way to define policies and enforce it using one of the many Kubernetes extension mechanisms.
Let’s discuss generic policy engines that can be used in the context of Kubernetes to define and enforce any kind of policy, from organizational to regulatory ones.
Open Policy Agent (OPA) is a graduated CNCF project that provides a general-purpose policy engine that unifies policy enforcement. The policies in OPA are represented in a high-level declarative language called Rego. It lets you specify policy as code and simple APIs to externalize policy decision-making, that is, moving it out of your own software. As you can see in Figure 8-6, OPA decouples policy decision-making from policy enforcement.
When you need to make a policy decision somewhere in your code (service
), you’d use the OPA API to query the policy in question. As an input the OPA server takes the current request data (in JSON format) as well as a policy (in Rego format) as input and computes an answer such as “access allowed” or “here is a list of relevant locations.” Note that the answer is not a binary one and entirely depends on the rules and data provided, computed in a deterministic manner.
Let’s look at a concrete example (one of
the examples from the Rego online playground). Imagine you
want to make sure that every resource has a costcenter
label that starts with
cccode-
, and if that’s not the case the user receives a message that this is
missing and cannot proceed (for example, cannot deploy an app).
In Rego, the rule would look something like the following (we will get back to this example in “Gatekeeper” in greater detail):
package
prod.k8s.acme.org
deny
[
msg
]
{
not
input.request.object.metadata.labels.costcenter
msg
:
=
"Every resource must have a costcenter label"
}
deny
[
msg
]
{
value
:
=
input.request.object.metadata.labels.costcenter
not
startswith
(
value,
"cccode-"
)
msg
:
=
sprintf
(
"Costcenter code must start with `cccode-`; found `%v`"
,
[
value
]
)
}
Now, let’s assume someone does a kubectl apply
that causes a pod to be created
that does not have a label.
The way OPA rather literally hooks into the API server is achieved via one
of the many Kubernetes extension mechanisms. In this case it uses the
Dynamic Admission Control; to be more precise, it registers a webhook that
the API server calls before the respective resource is persisted in etcd
.
In other words, the AdmissionReview
resource shown in the example is what the
API server sends to the OPA server, registered as a webhook.
As a result of the kubectl
command the API server generates an AdmissionReview
resource, in the following shown as a JSON document:
{
"kind"
:
"AdmissionReview"
,
"request"
:
{
"kind"
:
{
"kind"
:
"Pod"
,
"version"
:
"v1"
},
"object"
:
{
"metadata"
:
{
"name"
:
"myapp"
},
"spec"
:
{
"containers"
:
[
{
"image"
:
"nginx"
,
"name"
:
"nginx-frontend"
},
{
"image"
:
"mysql"
,
"name"
:
"mysql-backend"
}
]
}
}
}
}
With the preceding input, the OPA engine would compute the following output, which in
turn would be, for example, fed back by the API server to kubectl
and shown to the user on the command line:
{
"deny"
:[
"Every resource must have a costcenter label"
]
}
Now, how to rectify the situation and make it work? Simply add a label:
"metadata"
:
{
"name"
:
"myapp"
,
"labels"
:
{
"costcenter"
:
"cccode-HQ"
}
}
,
This should go without saying, but it is always a good idea to test your policies before you deploy them.
Rego is a little different than what you might be used to and the best analogue we could come up with is XSLT. If you do decide to adopt Rego, consider internalizing some tips.
To use OPA on the command line directly or in the context of an editor is fairly straightforward.
First, let’s see how to evaluate a given input and a policy. You start, as usual, with installing OPA. Given that it’s written in Go, this means a single, self-contained binary.
Next, let’s say we want to use the costcenter
example and evaluate it on the
command line, assuming you have stored the AdmissionReview
resource in
a file called input.json and the Rego rules in cc-policy.rego:
$
opa
eval
\
--input
input.json
\
--data
cc-policy.rego
\
--package
prod.k8s.acme.org
\
--format
pretty
'deny'
[
"Every resource must have a costcenter label"
]
Specify the input OPA should use (an AdmissionReview
resource).
Specify what rules to use (in Rego format).
Set the evaluation context.
Specify output.
That was easy enough! But we can go a step further: how about using OPA/Rego in an editor, for developing new policies?
Interestingly enough, a range of
IDEs and editors, from VSCode to vim
, are supported (see Figure 8-7).
In the context of managing OPA policies across a fleet of clusters, you may want to consider evaluating Styra’s Declarative Authorization Service (DAS) offering, an enterprise OPA solution coming with some useful features such as centralized policy management and logging, as well as impact analysis.
You can type-check Rego policies in OPA with JSON schema. This adds another layer of validation and can help policy developers to catch bugs. Learn more about this topic via “Type Checking Your Rego Policies with JSON Schema in OPA”.
Do you have to use Rego directly, though? No you do not have to, really. Let’s discuss alternatives in the context of Kubernetes, next.
Given that Rego is a DSL and has a learning curve, folks oftentimes wonder if they should use it directly or if there are more Kubernetes-native ways to use OPA. In fact the Gatekeeper project allows exactly for this.
If you’re unsure if you should be using Gatekeeper over OPA directly, there are plenty of nice articles available that discuss the topic in greater detail; for example, “Differences Between OPA and Gatekeeper for Kubernetes Admission Control” and “Integrating Open Policy Agent (OPA) With Kubernetes”.
What Gatekeeper does is essentially introduce a separation of concerns: so-called templates represent the policies (encoding Rego) and as an end user you would interface with CRDs that use said templates. An admission controller configured in the API server takes care of the enforcement of the policies, then.
Let’s have a look at how the previous example concerning costcenter
labels being required could look with Gatekeeper. We assume that you have
installed Gatekeeper already.
First, you define the template, defining a new CRD called K8sCostcenterLabels
in a file called costcenter_template.yaml:
apiVersion
:
templates.gatekeeper.sh/v1beta1
kind
:
ConstraintTemplate
metadata
:
name
:
costcenterlabels
spec
:
crd
:
spec
:
names
:
kind
:
K8sCostcenterLabels
validation
:
openAPIV3Schema
:
properties
:
labels
:
type
:
array
items
:
string
targets
:
-
target
:
admission.k8s.gatekeeper.sh
rego
:
|
package
prod.k8s.acme.org
deny[msg]
{
not
input.request.object.metadata.labels.costcenter
msg
:=
"Every
resource
must
have
a
costcenter
label"
}
deny[msg]
{
value
:=
input.request.object.metadata.labels.costcenter
not
startswith(value,
"cccode-")
msg
:=
sprintf("Costcenter
code
must
start
with
`cccode-`;
found
`%v`",
[value])
}
This defines the schema for the parameters
field.
This definition checks if the costcenter
label is provided or not. Note that
each rule contributes individually to the resulting (error) messages.
The not
keyword in this rule turns an undefined statement into a truthy
statement. That is, if any of the keys are missing, this statement is true.
In this rule we check if the costcenter
label is formatted appropriately.
In other words, we require that it must start with cccode-
.
When you have the CRD defined, you then can install it as follows:
$
kubectl apply -f costcenter_template.yaml
To use the costcenter
template CRD, you have to define a concrete
instance (a custom resource, or CR for short), so put the following
in a file called req_cc.yaml:
apiVersion
:
constraints.gatekeeper.sh/v1beta1
kind
:
K8sCostcenterLabels
metadata
:
name
:
ns-must-have-cc
spec
:
match
:
kinds
:
-
apiGroups
:
[
""
]
kinds
:
[
"Namespace"
]
Which you then create using the following command:
$
kubectl apply -f req_cc.yaml
After this command, the Gatekeeper controller knows about the policy and enforces it.
To check if the preceding policy works, you can create a namespace
that doesn’t have a label and if you then tried to create the namespace, for
example using kubectl apply
, you would see an error message containing
“Every resource must have a costcenter label” along with the resource creation
denial.
With this you have a basic idea of how Gatekeeper works. Now let’s move on to an alternative way to effectively achieve the same: the CNCF Kyverno project.
Another way to go about managing and enforcing policies is a CNCF project by the name of Kyverno. This project, initiated by Nirmata, is conceptually similar to Gatekeeper. Kyverno works as shown in Figure 8-8: it runs as a dynamic admission controller, supporting both validating and mutating admission webhooks.
So, what’s the difference between using Gatekeeper or plain OPA, then? Well, rather than directly or indirectly using Rego, with Kyverno you can do the following:
apiVersion
:
kyverno.io/v1
kind
:
ClusterPolicy
metadata
:
name
:
costcenterlabels
spec
:
validationFailureAction
:
enforce
rules
:
-
name
:
check-for-labels
match
:
resources
:
kinds
:
-
Namespace
validate
:
message
:
"
label
'app.kubernetes.io/name'
is
required
"
pattern
:
metadata
:
labels
:
app.kubernetes.io/name
:
"
?cccode-*
"
Defines what resources to target, in this case namespaces.
Defines the expected pattern; in case this is not achieved, the preceding error message is returned via webhook to client.
Does the preceding YAML look familiar? This is our costcenter-labels-are-required example from earlier on.
Learn more about getting started from Gaurav Agarwal’s article “Policy as Code on Kubernetes with Kyverno” and watch “Introduction to Kyverno” from David McKay’s excellent Rawkode Live series on YouTube.
Both OPA/Gatekeeper and Kyverno fail open, meaning that if the policy engine service called by the API server webhook is down and hence unable to validate an inbound change, they will proceed unvalidated. Depending on your requirements this may not be what you want, but the reasoning behind this is to prevent DOSing your cluster and subsequently slowing it down or potentially bringing down the control plane at all.
Both have auditing functionalities as well as a scanning mode that addresses this situation. For a more fine-grained comparison, we recommend you peruse Chip Zoller’s blog post “Kubernetes Policy Comparison: OPA/Gatekeeper vs. Kyverno”.
Let’s now have a further look at other options in this space.
In this last section on handling policies for and in Kubernetes we review some projects and offerings that you may want to consider using in addition to or as an alternative to the previously discussed ones.
Given that a Kubernetes cluster doesn’t operate in a vacuum, but in a certain environment such as the case with managed offerings that would be the cloud provider of your choice, you may indeed already be using some of the following:
This is a library for building authorization in your application. It comes with a set of APIs built on top of a declarative policy language called Polar, as well as a CLI/REPL and a debugger and REPL. With OSO you can express policies like “these types of users can see these sorts of information,” as well as implement role-based access control in your app.
These extend the functionalities of Kubernetes network policies.
This has a range of policies, from identity-based to resource-based to organization-level policies. There are also more specialized offerings; for example, in the context of Amazon EKS, you can define security groups for pods.
This has a rich and powerful policy model, similar to Kubernetes.
This allows stating business-level policies and they in addition offer Azure RBAC for access control purposes.
By Pulumi, this is described as “Policy as Code,” offering to define and enforce guardrails across cloud providers.
Policy is essential to securing your clusters, and thought is required to map your teams to their groups and roles. Roles that allow transitive access to other service accounts may offer a path to privilege escalation. Also, don’t forget to threat model the impact of credential compromise, and always use 2FA for humans. Last but not least, as usual, automating as much as possible, including policy testing and validation, pays off in the long run.
The wonderful Kubernetes and wider CNCF ecosystem has already provided a wealth of open source solutions, so in our experience it’s usually not a problem to find a tool but to figure out which out of the, say, ten tools available is the best and will still be supported when the Captain’s grandchildren have taken over.
With this we’ve reached the end of the policy chapter and will now turn our attention to the question of what happens if the Captain somehow, despite of all our controls put in place, manages to break in. In other words, we discuss intrusion detection systems (IDS) to detect unexpected activity. Arrrrrrr!