A Guide to Kubernetes Admission Controllers
Author: Malte Isberner (StackRox)
Kubernetes has greatly improved the speed and manageability of backend clusters in production today. Kubernetes has emerged as the de facto standard in container orchestrators thanks to its flexibility, scalability, and ease of use. Kubernetes also provides a range of features that secure production workloads. A more recent introduction in security features is a set of plugins called “
In a nutshell, Kubernetes admission controllers are plugins that govern and enforce how the cluster is used. They can be thought of as a gatekeeper that intercept (authenticated) API requests and may change the request object or deny the request altogether. The admission control process has two phases: the mutating phase is executed first, followed by the validating phase. Consequently, admission controllers can act as mutating or validating controllers or as a combination of both. For example, the LimitRanger admission controller can augment pods with default resource requests and limits (mutating phase), as well as verify that pods with explicitly set resource requirements do not exceed the per-namespace limits specified in the LimitRange object (validating phase). It is worth noting that some aspects of Kubernetes’ operation that many users would consider built-in are in fact governed by admission controllers. For example, when a namespace is deleted and subsequently enters the Among the more than 30 admission controllers shipped with Kubernetes, two take a special role because of their nearly limitless flexibility - The difference between the two kinds of admission controller webhooks is pretty much self-explanatory: mutating admission webhooks may mutate the objects, while validating admission webhooks may not. However, even a mutating admission webhook can reject requests and thus act in a validating fashion. Validating admission webhooks have two main advantages over mutating ones: first, for security reasons it might be desirable to disable the The set of enabled admission controllers is configured by passing a flag to the Kubernetes API server. Note that the old --admission-control flag was deprecated in 1.10 and replaced with --enable-admission-plugins. Kubernetes recommends the following admission controllers to be enabled by default. The complete list of admission controllers with their descriptions can be found
In this way, admission controllers and policy management help make sure that applications stay in compliance within an ever-changing landscape of controls. To illustrate how admission controller webhooks can be leveraged to establish custom security policies, let’s consider an example that addresses one of the shortcomings of Kubernetes: a lot of its defaults are optimized for ease of use and reducing friction, sometimes at the expense of security. One of these settings is that containers are by default allowed to run as root (and, without further configuration and no You can use a custom mutating admission controller webhook to apply more secure defaults: unless explicitly requested, our webhook will ensure that pods run as a non-root user (we assign the user ID 1234 if no explicit assignment has been made). Note that this setup does not prevent you from deploying any workloads in your cluster, including those that legitimately require running as root. It only requires you to explicitly enable this riskier mode of operation in the deployment configuration, while defaulting to non-root mode for all other workloads. The full code along with deployment instructions can be found in our accompanying
A mutating admission controller webhook is defined by creating a This configuration defines a The Kubernetes API server makes an HTTPS POST request to the given service and URL path, with a JSON-encoded
Our demo repository contains a
Note that for the server to run without elevated privileges, we have the HTTP server listen on port 8443. Kubernetes does not allow specifying a port in the webhook configuration; it always assumes the HTTPS port 443. However, since a service object is required anyway, we can easily map port 443 of the service to port 8443 on the container: In a mutating admission controller webhook, mutations are performed via
For setting the field Since a webhook must be served via HTTPS, we need proper certificates for the server. These certificates can be self-signed (rather: signed by a self-signed CA), but we need Kubernetes to instruct the respective CA certificate when talking to the webhook server. In addition, the common name (CN) of the certificate must match the server name used by the Kubernetes API server, which for internal services is The webhook configuration shown previously contains a placeholder After deploying the webhook server and configuring it, which can be done by invoking the ./deploy.sh script from the repository, it is time to test and verify that the webhook indeed does its job. The repository contains : Create one of these pods by running In the third example, the object creation should be rejected with an appropriate error message: Feel free to test this with your own workloads as well. Of course, you can also experiment a little bit further by changing the logic of the webhook and see how the changes affect object creation. More information on how to do experiment with such changes can be found in the . Kubernetes admission controllers offer significant advantages for security. Digging into two powerful examples, with accompanying available code, will help you get started on leveraging these powerful capabilities.What are Kubernetes admission controllers?
Terminating
state, the
ValidatingAdmissionWebhooks
and MutatingAdmissionWebhooks
, both of which are in beta status as of Kubernetes 1.13. We will examine these two admission controllers closely, as they do not implement any policy decision logic themselves. Instead, the respective action is obtained from a REST endpoint (a webhook) of a service running inside the cluster. This approach decouples the admission controller logic from the Kubernetes API server, thus allowing users to implement custom logic to be executed whenever resources are created, updated, or deleted in a Kubernetes cluster.MutatingAdmissionWebhook
admission controller (or apply stricter RBAC restrictions as to who may create MutatingWebhookConfiguration
objects) because of its potentially confusing or even dangerous side effects. Second, as shown in the previous diagram, validating admission controllers (and thus webhooks) are run after any mutating ones. As a result, whatever request object a validating webhook sees is the final version that would be persisted to etcd
.--enable-admission-plugins=ValidatingAdmissionWebhook,MutatingAdmissionWebhook
--enable-admission-plugins=NamespaceLifecycle,LimitRanger,ServiceAccount,DefaultStorageClass,DefaultTolerationSeconds,MutatingAdmissionWebhook,ValidatingAdmissionWebhook,Priority,ResourceQuota,PodSecurityPolicy
Why do I need admission controllers?
PodSecurityPolicy
admission controller is perhaps the most prominent example; it can be used for disallowing containers from running as root or making sure the container’s root filesystem is always mounted read-only, for example. Further use cases that can be realized by custom, webhook-based admission controllers include:privileged
flag can circumvent a lot of security checks. This risk could be mitigated by a webhook-based admission controller that either rejects such deployments (validating) or overrides the privileged
flag, setting it to false
.latest
tags, or tags with a -dev
suffix.Example: Writing and Deploying an Admission Controller Webhook
USER
directive in the Dockerfile, will also do so). Even though containers are isolated from the underlying host to a certain extent, running containers as root does increase the risk profile of your deployment— and should be avoided as one of many
Mutating Webhook Configuration
MutatingWebhookConfiguration
object in Kubernetes. In our example, we use the following configuration:apiVersion: admissionregistration.k8s.io/v1beta1
kind: MutatingWebhookConfiguration
metadata:
name: demo-webhook
webhooks:
- name: webhook-server.webhook-demo.svc
clientConfig:
service:
name: webhook-server
namespace: webhook-demo
path: "/mutate"
caBundle: ${CA_PEM_B64}
rules:
- operations: [ "CREATE" ]
apiGroups: [""]
apiVersions: ["v1"]
resources: ["pods"]
webhook webhook-server.webhook-demo.svc
, and instructs the Kubernetes API server to consult the service webhook-server
in namespace webhook-demo
whenever a pod is created by making a HTTP POST request to the /mutate
URL. For this configuration to work, several prerequisites have to be met.Webhook REST API
mux := http.NewServeMux()
mux.Handle("/mutate", admitFuncHandler(applySecurityDefaults))
server := &http.Server{
Addr: ":8443",
Handler: mux,
}
log.Fatal(server.ListenAndServeTLS(certPath, keyPath))
apiVersion: v1
kind: Service
metadata:
name: webhook-server
namespace: webhook-demo
spec:
selector:
app: webhook-server # specified by the deployment/pod
ports:
- port: 443
targetPort: webhook-api # name of port 8443 of the container
Object Modification Logic
type patchOperation struct {
Op string `json:"op"`
Path string `json:"path"`
Value interface{} `json:"value,omitempty"`
}
.spec.securityContext.runAsNonRoot
of a pod to true, we construct the following patchOperation
object:patches = append(patches, patchOperation{
Op: "add",
Path: "/spec/securityContext/runAsNonRoot",
Value: true,
})
TLS Certificates
<service-name>
.<namespace>.svc
, i.e., webhook-server.webhook-demo.svc
in our case. Since the generation of self-signed TLS certificates is well documented across the Internet, we simply refer to the respective
${CA_PEM_B64}
. Before we can create this configuration, we need to replace this portion with the Base64-encoded PEM certificate of the CA. The openssl base64 -A
command can be used for this purpose.Testing the Webhook
pod-with-defaults
). We expect this pod to be run as non-root with user id 1234.pod-with-override
).pod-with-conflict
). To showcase the rejection of object creation requests, we have augmented our admission controller logic to reject such obvious misconfigurations.kubectl create -f examples/<name>.yaml
. In the first two examples, you can verify the user id under which the pod ran by inspecting the logs, for example:$ kubectl create -f examples/pod-with-defaults.yaml
$ kubectl logs pod-with-defaults
I am running as user 1234
$ kubectl create -f examples/pod-with-conflict.yaml
Error from server (InternalError): error when creating "examples/pod-with-conflict.yaml": Internal error occurred: admission webhook "webhook-server.webhook-demo.svc" denied the request: runAsNonRoot specified, but runAsUser set to 0 (the root user)
Summary
References: