Merge the tutorials under tutorials->clusters into tutorials->security
This commit is contained in:
@@ -0,0 +1,478 @@
|
||||
---
|
||||
reviewers:
|
||||
- stclair
|
||||
title: Restrict a Container's Access to Resources with AppArmor
|
||||
content_type: tutorial
|
||||
weight: 10
|
||||
---
|
||||
|
||||
<!-- overview -->
|
||||
|
||||
{{< feature-state for_k8s_version="v1.4" state="beta" >}}
|
||||
|
||||
|
||||
AppArmor is a Linux kernel security module that supplements the standard Linux user and group based
|
||||
permissions to confine programs to a limited set of resources. AppArmor can be configured for any
|
||||
application to reduce its potential attack surface and provide greater in-depth defense. It is
|
||||
configured through profiles tuned to allow the access needed by a specific program or container,
|
||||
such as Linux capabilities, network access, file permissions, etc. Each profile can be run in either
|
||||
*enforcing* mode, which blocks access to disallowed resources, or *complain* mode, which only reports
|
||||
violations.
|
||||
|
||||
AppArmor can help you to run a more secure deployment by restricting what containers are allowed to
|
||||
do, and/or provide better auditing through system logs. However, it is important to keep in mind
|
||||
that AppArmor is not a silver bullet and can only do so much to protect against exploits in your
|
||||
application code. It is important to provide good, restrictive profiles, and harden your
|
||||
applications and cluster from other angles as well.
|
||||
|
||||
|
||||
|
||||
## {{% heading "objectives" %}}
|
||||
|
||||
|
||||
* See an example of how to load a profile on a node
|
||||
* Learn how to enforce the profile on a Pod
|
||||
* Learn how to check that the profile is loaded
|
||||
* See what happens when a profile is violated
|
||||
* See what happens when a profile cannot be loaded
|
||||
|
||||
|
||||
|
||||
## {{% heading "prerequisites" %}}
|
||||
|
||||
|
||||
Make sure:
|
||||
|
||||
1. Kubernetes version is at least v1.4 -- Kubernetes support for AppArmor was added in
|
||||
v1.4. Kubernetes components older than v1.4 are not aware of the new AppArmor annotations, and
|
||||
will **silently ignore** any AppArmor settings that are provided. To ensure that your Pods are
|
||||
receiving the expected protections, it is important to verify the Kubelet version of your nodes:
|
||||
|
||||
```shell
|
||||
kubectl get nodes -o=jsonpath=$'{range .items[*]}{@.metadata.name}: {@.status.nodeInfo.kubeletVersion}\n{end}'
|
||||
```
|
||||
```
|
||||
gke-test-default-pool-239f5d02-gyn2: v1.4.0
|
||||
gke-test-default-pool-239f5d02-x1kf: v1.4.0
|
||||
gke-test-default-pool-239f5d02-xwux: v1.4.0
|
||||
```
|
||||
|
||||
2. AppArmor kernel module is enabled -- For the Linux kernel to enforce an AppArmor profile, the
|
||||
AppArmor kernel module must be installed and enabled. Several distributions enable the module by
|
||||
default, such as Ubuntu and SUSE, and many others provide optional support. To check whether the
|
||||
module is enabled, check the `/sys/module/apparmor/parameters/enabled` file:
|
||||
|
||||
```shell
|
||||
cat /sys/module/apparmor/parameters/enabled
|
||||
Y
|
||||
```
|
||||
|
||||
If the Kubelet contains AppArmor support (>= v1.4), it will refuse to run a Pod with AppArmor
|
||||
options if the kernel module is not enabled.
|
||||
|
||||
{{< note >}}
|
||||
Ubuntu carries many AppArmor patches that have not been merged into the upstream Linux
|
||||
kernel, including patches that add additional hooks and features. Kubernetes has only been
|
||||
tested with the upstream version, and does not promise support for other features.
|
||||
{{< /note >}}
|
||||
|
||||
3. Container runtime supports AppArmor -- Currently all common Kubernetes-supported container
|
||||
runtimes should support AppArmor, like {{< glossary_tooltip term_id="docker">}},
|
||||
{{< glossary_tooltip term_id="cri-o" >}} or {{< glossary_tooltip term_id="containerd" >}}.
|
||||
Please refer to the corresponding runtime documentation and verify that the cluster fulfills
|
||||
the requirements to use AppArmor.
|
||||
|
||||
4. Profile is loaded -- AppArmor is applied to a Pod by specifying an AppArmor profile that each
|
||||
container should be run with. If any of the specified profiles is not already loaded in the
|
||||
kernel, the Kubelet (>= v1.4) will reject the Pod. You can view which profiles are loaded on a
|
||||
node by checking the `/sys/kernel/security/apparmor/profiles` file. For example:
|
||||
|
||||
```shell
|
||||
ssh gke-test-default-pool-239f5d02-gyn2 "sudo cat /sys/kernel/security/apparmor/profiles | sort"
|
||||
```
|
||||
```
|
||||
apparmor-test-deny-write (enforce)
|
||||
apparmor-test-audit-write (enforce)
|
||||
docker-default (enforce)
|
||||
k8s-nginx (enforce)
|
||||
```
|
||||
|
||||
For more details on loading profiles on nodes, see
|
||||
[Setting up nodes with profiles](#setting-up-nodes-with-profiles).
|
||||
|
||||
As long as the Kubelet version includes AppArmor support (>= v1.4), the Kubelet will reject a Pod
|
||||
with AppArmor options if any of the prerequisites are not met. You can also verify AppArmor support
|
||||
on nodes by checking the node ready condition message (though this is likely to be removed in a
|
||||
later release):
|
||||
|
||||
```shell
|
||||
kubectl get nodes -o=jsonpath=$'{range .items[*]}{@.metadata.name}: {.status.conditions[?(@.reason=="KubeletReady")].message}\n{end}'
|
||||
```
|
||||
```
|
||||
gke-test-default-pool-239f5d02-gyn2: kubelet is posting ready status. AppArmor enabled
|
||||
gke-test-default-pool-239f5d02-x1kf: kubelet is posting ready status. AppArmor enabled
|
||||
gke-test-default-pool-239f5d02-xwux: kubelet is posting ready status. AppArmor enabled
|
||||
```
|
||||
|
||||
|
||||
|
||||
<!-- lessoncontent -->
|
||||
|
||||
## Securing a Pod
|
||||
|
||||
{{< note >}}
|
||||
AppArmor is currently in beta, so options are specified as annotations. Once support graduates to
|
||||
general availability, the annotations will be replaced with first-class fields (more details in
|
||||
[Upgrade path to GA](#upgrade-path-to-general-availability)).
|
||||
{{< /note >}}
|
||||
|
||||
AppArmor profiles are specified *per-container*. To specify the AppArmor profile to run a Pod
|
||||
container with, add an annotation to the Pod's metadata:
|
||||
|
||||
```yaml
|
||||
container.apparmor.security.beta.kubernetes.io/<container_name>: <profile_ref>
|
||||
```
|
||||
|
||||
Where `<container_name>` is the name of the container to apply the profile to, and `<profile_ref>`
|
||||
specifies the profile to apply. The `profile_ref` can be one of:
|
||||
|
||||
* `runtime/default` to apply the runtime's default profile
|
||||
* `localhost/<profile_name>` to apply the profile loaded on the host with the name `<profile_name>`
|
||||
* `unconfined` to indicate that no profiles will be loaded
|
||||
|
||||
See the [API Reference](#api-reference) for the full details on the annotation and profile name formats.
|
||||
|
||||
Kubernetes AppArmor enforcement works by first checking that all the prerequisites have been
|
||||
met, and then forwarding the profile selection to the container runtime for enforcement. If the
|
||||
prerequisites have not been met, the Pod will be rejected, and will not run.
|
||||
|
||||
To verify that the profile was applied, you can look for the AppArmor security option listed in the container created event:
|
||||
|
||||
```shell
|
||||
kubectl get events | grep Created
|
||||
```
|
||||
```
|
||||
22s 22s 1 hello-apparmor Pod spec.containers{hello} Normal Created {kubelet e2e-test-stclair-node-pool-31nt} Created container with docker id 269a53b202d3; Security:[seccomp=unconfined apparmor=k8s-apparmor-example-deny-write]
|
||||
```
|
||||
|
||||
You can also verify directly that the container's root process is running with the correct profile by checking its proc attr:
|
||||
|
||||
```shell
|
||||
kubectl exec <pod_name> cat /proc/1/attr/current
|
||||
```
|
||||
```
|
||||
k8s-apparmor-example-deny-write (enforce)
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
*This example assumes you have already set up a cluster with AppArmor support.*
|
||||
|
||||
First, we need to load the profile we want to use onto our nodes. This profile denies all file writes:
|
||||
|
||||
```shell
|
||||
#include <tunables/global>
|
||||
|
||||
profile k8s-apparmor-example-deny-write flags=(attach_disconnected) {
|
||||
#include <abstractions/base>
|
||||
|
||||
file,
|
||||
|
||||
# Deny all file writes.
|
||||
deny /** w,
|
||||
}
|
||||
```
|
||||
|
||||
Since we don't know where the Pod will be scheduled, we'll need to load the profile on all our
|
||||
nodes. For this example we'll use SSH to install the profiles, but other approaches are
|
||||
discussed in [Setting up nodes with profiles](#setting-up-nodes-with-profiles).
|
||||
|
||||
```shell
|
||||
NODES=(
|
||||
# The SSH-accessible domain names of your nodes
|
||||
gke-test-default-pool-239f5d02-gyn2.us-central1-a.my-k8s
|
||||
gke-test-default-pool-239f5d02-x1kf.us-central1-a.my-k8s
|
||||
gke-test-default-pool-239f5d02-xwux.us-central1-a.my-k8s)
|
||||
for NODE in ${NODES[*]}; do ssh $NODE 'sudo apparmor_parser -q <<EOF
|
||||
#include <tunables/global>
|
||||
|
||||
profile k8s-apparmor-example-deny-write flags=(attach_disconnected) {
|
||||
#include <abstractions/base>
|
||||
|
||||
file,
|
||||
|
||||
# Deny all file writes.
|
||||
deny /** w,
|
||||
}
|
||||
EOF'
|
||||
done
|
||||
```
|
||||
|
||||
Next, we'll run a simple "Hello AppArmor" pod with the deny-write profile:
|
||||
|
||||
{{< codenew file="pods/security/hello-apparmor.yaml" >}}
|
||||
|
||||
```shell
|
||||
kubectl create -f ./hello-apparmor.yaml
|
||||
```
|
||||
|
||||
If we look at the pod events, we can see that the Pod container was created with the AppArmor
|
||||
profile "k8s-apparmor-example-deny-write":
|
||||
|
||||
```shell
|
||||
kubectl get events | grep hello-apparmor
|
||||
```
|
||||
```
|
||||
14s 14s 1 hello-apparmor Pod Normal Scheduled {default-scheduler } Successfully assigned hello-apparmor to gke-test-default-pool-239f5d02-gyn2
|
||||
14s 14s 1 hello-apparmor Pod spec.containers{hello} Normal Pulling {kubelet gke-test-default-pool-239f5d02-gyn2} pulling image "busybox"
|
||||
13s 13s 1 hello-apparmor Pod spec.containers{hello} Normal Pulled {kubelet gke-test-default-pool-239f5d02-gyn2} Successfully pulled image "busybox"
|
||||
13s 13s 1 hello-apparmor Pod spec.containers{hello} Normal Created {kubelet gke-test-default-pool-239f5d02-gyn2} Created container with docker id 06b6cd1c0989; Security:[seccomp=unconfined apparmor=k8s-apparmor-example-deny-write]
|
||||
13s 13s 1 hello-apparmor Pod spec.containers{hello} Normal Started {kubelet gke-test-default-pool-239f5d02-gyn2} Started container with docker id 06b6cd1c0989
|
||||
```
|
||||
|
||||
We can verify that the container is actually running with that profile by checking its proc attr:
|
||||
|
||||
```shell
|
||||
kubectl exec hello-apparmor -- cat /proc/1/attr/current
|
||||
```
|
||||
```
|
||||
k8s-apparmor-example-deny-write (enforce)
|
||||
```
|
||||
|
||||
Finally, we can see what happens if we try to violate the profile by writing to a file:
|
||||
|
||||
```shell
|
||||
kubectl exec hello-apparmor -- touch /tmp/test
|
||||
```
|
||||
```
|
||||
touch: /tmp/test: Permission denied
|
||||
error: error executing remote command: command terminated with non-zero exit code: Error executing in Docker Container: 1
|
||||
```
|
||||
|
||||
To wrap up, let's look at what happens if we try to specify a profile that hasn't been loaded:
|
||||
|
||||
```shell
|
||||
kubectl create -f /dev/stdin <<EOF
|
||||
```
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: hello-apparmor-2
|
||||
annotations:
|
||||
container.apparmor.security.beta.kubernetes.io/hello: localhost/k8s-apparmor-example-allow-write
|
||||
spec:
|
||||
containers:
|
||||
- name: hello
|
||||
image: busybox
|
||||
command: [ "sh", "-c", "echo 'Hello AppArmor!' && sleep 1h" ]
|
||||
EOF
|
||||
pod/hello-apparmor-2 created
|
||||
```
|
||||
|
||||
```shell
|
||||
kubectl describe pod hello-apparmor-2
|
||||
```
|
||||
```
|
||||
Name: hello-apparmor-2
|
||||
Namespace: default
|
||||
Node: gke-test-default-pool-239f5d02-x1kf/
|
||||
Start Time: Tue, 30 Aug 2016 17:58:56 -0700
|
||||
Labels: <none>
|
||||
Annotations: container.apparmor.security.beta.kubernetes.io/hello=localhost/k8s-apparmor-example-allow-write
|
||||
Status: Pending
|
||||
Reason: AppArmor
|
||||
Message: Pod Cannot enforce AppArmor: profile "k8s-apparmor-example-allow-write" is not loaded
|
||||
IP:
|
||||
Controllers: <none>
|
||||
Containers:
|
||||
hello:
|
||||
Container ID:
|
||||
Image: busybox
|
||||
Image ID:
|
||||
Port:
|
||||
Command:
|
||||
sh
|
||||
-c
|
||||
echo 'Hello AppArmor!' && sleep 1h
|
||||
State: Waiting
|
||||
Reason: Blocked
|
||||
Ready: False
|
||||
Restart Count: 0
|
||||
Environment: <none>
|
||||
Mounts:
|
||||
/var/run/secrets/kubernetes.io/serviceaccount from default-token-dnz7v (ro)
|
||||
Conditions:
|
||||
Type Status
|
||||
Initialized True
|
||||
Ready False
|
||||
PodScheduled True
|
||||
Volumes:
|
||||
default-token-dnz7v:
|
||||
Type: Secret (a volume populated by a Secret)
|
||||
SecretName: default-token-dnz7v
|
||||
Optional: false
|
||||
QoS Class: BestEffort
|
||||
Node-Selectors: <none>
|
||||
Tolerations: <none>
|
||||
Events:
|
||||
FirstSeen LastSeen Count From SubobjectPath Type Reason Message
|
||||
--------- -------- ----- ---- ------------- -------- ------ -------
|
||||
23s 23s 1 {default-scheduler } Normal Scheduled Successfully assigned hello-apparmor-2 to e2e-test-stclair-node-pool-t1f5
|
||||
23s 23s 1 {kubelet e2e-test-stclair-node-pool-t1f5} Warning AppArmor Cannot enforce AppArmor: profile "k8s-apparmor-example-allow-write" is not loaded
|
||||
```
|
||||
|
||||
Note the pod status is Pending, with a helpful error message: `Pod Cannot enforce AppArmor: profile
|
||||
"k8s-apparmor-example-allow-write" is not loaded`. An event was also recorded with the same message.
|
||||
|
||||
## Administration
|
||||
|
||||
### Setting up nodes with profiles
|
||||
|
||||
Kubernetes does not currently provide any native mechanisms for loading AppArmor profiles onto
|
||||
nodes. There are lots of ways to setup the profiles though, such as:
|
||||
|
||||
* Through a [DaemonSet](/docs/concepts/workloads/controllers/daemonset/) that runs a Pod on each node to
|
||||
ensure the correct profiles are loaded. An example implementation can be found
|
||||
[here](https://git.k8s.io/kubernetes/test/images/apparmor-loader).
|
||||
* At node initialization time, using your node initialization scripts (e.g. Salt, Ansible, etc.) or
|
||||
image.
|
||||
* By copying the profiles to each node and loading them through SSH, as demonstrated in the
|
||||
[Example](#example).
|
||||
|
||||
The scheduler is not aware of which profiles are loaded onto which node, so the full set of profiles
|
||||
must be loaded onto every node. An alternative approach is to add a node label for each profile (or
|
||||
class of profiles) on the node, and use a
|
||||
[node selector](/docs/concepts/scheduling-eviction/assign-pod-node/) to ensure the Pod is run on a
|
||||
node with the required profile.
|
||||
|
||||
### Restricting profiles with the PodSecurityPolicy
|
||||
|
||||
{{< note >}}
|
||||
PodSecurityPolicy is deprecated in Kubernetes v1.21, and will be removed in v1.25.
|
||||
See [PodSecurityPolicy documentation](/docs/concepts/policy/pod-security-policy/) for more information.
|
||||
{{< /note >}}
|
||||
|
||||
If the PodSecurityPolicy extension is enabled, cluster-wide AppArmor restrictions can be applied. To
|
||||
enable the PodSecurityPolicy, the following flag must be set on the `apiserver`:
|
||||
|
||||
```
|
||||
--enable-admission-plugins=PodSecurityPolicy[,others...]
|
||||
```
|
||||
|
||||
The AppArmor options can be specified as annotations on the PodSecurityPolicy:
|
||||
|
||||
```yaml
|
||||
apparmor.security.beta.kubernetes.io/defaultProfileName: <profile_ref>
|
||||
apparmor.security.beta.kubernetes.io/allowedProfileNames: <profile_ref>[,others...]
|
||||
```
|
||||
|
||||
The default profile name option specifies the profile to apply to containers by default when none is
|
||||
specified. The allowed profile names option specifies a list of profiles that Pod containers are
|
||||
allowed to be run with. If both options are provided, the default must be allowed. The profiles are
|
||||
specified in the same format as on containers. See the [API Reference](#api-reference) for the full
|
||||
specification.
|
||||
|
||||
### Disabling AppArmor
|
||||
|
||||
If you do not want AppArmor to be available on your cluster, it can be disabled by a command-line flag:
|
||||
|
||||
```
|
||||
--feature-gates=AppArmor=false
|
||||
```
|
||||
|
||||
When disabled, any Pod that includes an AppArmor profile will fail validation with a "Forbidden"
|
||||
error. Note that by default docker always enables the "docker-default" profile on non-privileged
|
||||
pods (if the AppArmor kernel module is enabled), and will continue to do so even if the feature-gate
|
||||
is disabled. The option to disable AppArmor will be removed when AppArmor graduates to general
|
||||
availability (GA).
|
||||
|
||||
### Upgrading to Kubernetes v1.4 with AppArmor
|
||||
|
||||
No action is required with respect to AppArmor to upgrade your cluster to v1.4. However, if any
|
||||
existing pods had an AppArmor annotation, they will not go through validation (or PodSecurityPolicy
|
||||
admission). If permissive profiles are loaded on the nodes, a malicious user could pre-apply a
|
||||
permissive profile to escalate the pod privileges above the docker-default. If this is a concern, it
|
||||
is recommended to scrub the cluster of any pods containing an annotation with
|
||||
`apparmor.security.beta.kubernetes.io`.
|
||||
|
||||
### Upgrade path to General Availability
|
||||
|
||||
When AppArmor is ready to be graduated to general availability (GA), the options currently specified
|
||||
through annotations will be converted to fields. Supporting all the upgrade and downgrade paths
|
||||
through the transition is very nuanced, and will be explained in detail when the transition
|
||||
occurs. We will commit to supporting both fields and annotations for at least 2 releases, and will
|
||||
explicitly reject the annotations for at least 2 releases after that.
|
||||
|
||||
## Authoring Profiles
|
||||
|
||||
Getting AppArmor profiles specified correctly can be a tricky business. Fortunately there are some
|
||||
tools to help with that:
|
||||
|
||||
* `aa-genprof` and `aa-logprof` generate profile rules by monitoring an application's activity and
|
||||
logs, and admitting the actions it takes. Further instructions are provided by the
|
||||
[AppArmor documentation](https://gitlab.com/apparmor/apparmor/wikis/Profiling_with_tools).
|
||||
* [bane](https://github.com/jfrazelle/bane) is an AppArmor profile generator for Docker that uses a
|
||||
simplified profile language.
|
||||
|
||||
It is recommended to run your application through Docker on a development workstation to generate
|
||||
the profiles, but there is nothing preventing running the tools on the Kubernetes node where your
|
||||
Pod is running.
|
||||
|
||||
To debug problems with AppArmor, you can check the system logs to see what, specifically, was
|
||||
denied. AppArmor logs verbose messages to `dmesg`, and errors can usually be found in the system
|
||||
logs or through `journalctl`. More information is provided in
|
||||
[AppArmor failures](https://gitlab.com/apparmor/apparmor/wikis/AppArmor_Failures).
|
||||
|
||||
|
||||
## API Reference
|
||||
|
||||
### Pod Annotation
|
||||
|
||||
Specifying the profile a container will run with:
|
||||
|
||||
- **key**: `container.apparmor.security.beta.kubernetes.io/<container_name>`
|
||||
Where `<container_name>` matches the name of a container in the Pod.
|
||||
A separate profile can be specified for each container in the Pod.
|
||||
- **value**: a profile reference, described below
|
||||
|
||||
### Profile Reference
|
||||
|
||||
- `runtime/default`: Refers to the default runtime profile.
|
||||
- Equivalent to not specifying a profile (without a PodSecurityPolicy default), except it still
|
||||
requires AppArmor to be enabled.
|
||||
- For Docker, this resolves to the
|
||||
[`docker-default`](https://docs.docker.com/engine/security/apparmor/) profile for non-privileged
|
||||
containers, and unconfined (no profile) for privileged containers.
|
||||
- `localhost/<profile_name>`: Refers to a profile loaded on the node (localhost) by name.
|
||||
- The possible profile names are detailed in the
|
||||
[core policy reference](https://gitlab.com/apparmor/apparmor/wikis/AppArmor_Core_Policy_Reference#profile-names-and-attachment-specifications).
|
||||
- `unconfined`: This effectively disables AppArmor on the container.
|
||||
|
||||
Any other profile reference format is invalid.
|
||||
|
||||
### PodSecurityPolicy Annotations
|
||||
|
||||
Specifying the default profile to apply to containers when none is provided:
|
||||
|
||||
* **key**: `apparmor.security.beta.kubernetes.io/defaultProfileName`
|
||||
* **value**: a profile reference, described above
|
||||
|
||||
Specifying the list of profiles Pod containers is allowed to specify:
|
||||
|
||||
* **key**: `apparmor.security.beta.kubernetes.io/allowedProfileNames`
|
||||
* **value**: a comma-separated list of profile references (described above)
|
||||
- Although an escaped comma is a legal character in a profile name, it cannot be explicitly
|
||||
allowed here.
|
||||
|
||||
|
||||
|
||||
## {{% heading "whatsnext" %}}
|
||||
|
||||
|
||||
Additional resources:
|
||||
|
||||
* [Quick guide to the AppArmor profile language](https://gitlab.com/apparmor/apparmor/wikis/QuickProfileLanguage)
|
||||
* [AppArmor core policy reference](https://gitlab.com/apparmor/apparmor/wikis/Policy_Layout)
|
||||
|
||||
|
||||
@@ -0,0 +1,472 @@
|
||||
---
|
||||
reviewers:
|
||||
- hasheddan
|
||||
- pjbgf
|
||||
- saschagrunert
|
||||
title: Restrict a Container's Syscalls with seccomp
|
||||
content_type: tutorial
|
||||
weight: 20
|
||||
min-kubernetes-server-version: v1.22
|
||||
---
|
||||
|
||||
<!-- overview -->
|
||||
|
||||
{{< feature-state for_k8s_version="v1.19" state="stable" >}}
|
||||
|
||||
Seccomp stands for secure computing mode and has been a feature of the Linux
|
||||
kernel since version 2.6.12. It can be used to sandbox the privileges of a
|
||||
process, restricting the calls it is able to make from userspace into the
|
||||
kernel. Kubernetes lets you automatically apply seccomp profiles loaded onto a
|
||||
{{< glossary_tooltip text="node" term_id="node" >}} to your Pods and containers.
|
||||
|
||||
Identifying the privileges required for your workloads can be difficult. In this
|
||||
tutorial, you will go through how to load seccomp profiles into a local
|
||||
Kubernetes cluster, how to apply them to a Pod, and how you can begin to craft
|
||||
profiles that give only the necessary privileges to your container processes.
|
||||
|
||||
## {{% heading "objectives" %}}
|
||||
|
||||
* Learn how to load seccomp profiles on a node
|
||||
* Learn how to apply a seccomp profile to a container
|
||||
* Observe auditing of syscalls made by a container process
|
||||
* Observe behavior when a missing profile is specified
|
||||
* Observe a violation of a seccomp profile
|
||||
* Learn how to create fine-grained seccomp profiles
|
||||
* Learn how to apply a container runtime default seccomp profile
|
||||
|
||||
## {{% heading "prerequisites" %}}
|
||||
|
||||
In order to complete all steps in this tutorial, you must install
|
||||
[kind](/docs/tasks/tools/#kind) and [kubectl](/docs/tasks/tools/#kubectl).
|
||||
|
||||
This tutorial shows some examples that are still alpha (since v1.22) and
|
||||
others that use only generally available seccomp functionality. You should
|
||||
make sure that your cluster is
|
||||
[configured correctly](https://kind.sigs.k8s.io/docs/user/quick-start/#setting-kubernetes-version)
|
||||
for the version you are using.
|
||||
|
||||
The tutorial also uses the `curl` tool for downloading examples to your computer.
|
||||
You can adapt the steps to use a different tool if you prefer.
|
||||
|
||||
{{< note >}}
|
||||
It is not possible to apply a seccomp profile to a container running with
|
||||
`privileged: true` set in the container's `securityContext`. Privileged containers always
|
||||
run as `Unconfined`.
|
||||
{{< /note >}}
|
||||
|
||||
<!-- steps -->
|
||||
|
||||
|
||||
## Download example seccomp profiles {#download-profiles}
|
||||
|
||||
The contents of these profiles will be explored later on, but for now go ahead
|
||||
and download them into a directory named `profiles/` so that they can be loaded
|
||||
into the cluster.
|
||||
|
||||
{{< tabs name="tab_with_code" >}}
|
||||
{{{< tab name="audit.json" >}}
|
||||
{{< codenew file="pods/security/seccomp/profiles/audit.json" >}}
|
||||
{{< /tab >}}
|
||||
{{< tab name="violation.json" >}}
|
||||
{{< codenew file="pods/security/seccomp/profiles/violation.json" >}}
|
||||
{{< /tab >}}}
|
||||
{{< tab name="fine-grained.json" >}}
|
||||
{{< codenew file="pods/security/seccomp/profiles/fine-grained.json" >}}
|
||||
{{< /tab >}}}
|
||||
{{< /tabs >}}
|
||||
|
||||
Run these commands:
|
||||
|
||||
```shell
|
||||
mkdir ./profiles
|
||||
curl -L -o profiles/audit.json https://k8s.io/examples/pods/security/seccomp/profiles/audit.json
|
||||
curl -L -o profiles/violation.json https://k8s.io/examples/pods/security/seccomp/profiles/violation.json
|
||||
curl -L -o profiles/fine-grained.json https://k8s.io/examples/pods/security/seccomp/profiles/fine-grained.json
|
||||
ls profiles
|
||||
```
|
||||
|
||||
You should see three profiles listed at the end of the final step:
|
||||
```
|
||||
audit.json fine-grained.json violation.json
|
||||
```
|
||||
|
||||
|
||||
## Create a local Kubernetes cluster with kind
|
||||
|
||||
|
||||
For simplicity, [kind](https://kind.sigs.k8s.io/) can be used to create a single
|
||||
node cluster with the seccomp profiles loaded. Kind runs Kubernetes in Docker,
|
||||
so each node of the cluster is a container. This allows for files
|
||||
to be mounted in the filesystem of each container similar to loading files
|
||||
onto a node.
|
||||
|
||||
{{< codenew file="pods/security/seccomp/kind.yaml" >}}
|
||||
|
||||
Download that example kind configuration, and save it to a file named `kind.yaml`:
|
||||
```shell
|
||||
curl -L -O https://k8s.io/examples/pods/security/seccomp/kind.yaml
|
||||
```
|
||||
|
||||
You can set a specific Kubernetes version by setting the node's container image.
|
||||
See [Nodes](https://kind.sigs.k8s.io/docs/user/configuration/#nodes) within the
|
||||
kind documentation about configuration for more details on this.
|
||||
This tutorial assumes you are using Kubernetes {{< param "version" >}}.
|
||||
|
||||
As an alpha feature, you can configure Kubernetes to use the profile that the
|
||||
{{< glossary_tooltip text="container runtime" term_id="container-runtime" >}}
|
||||
prefers by default, rather than falling back to `Unconfined`.
|
||||
If you want to try that, see
|
||||
[enable the use of `RuntimeDefault` as the default seccomp profile for all workloads](#enable-the-use-of-runtimedefault-as-the-default-seccomp-profile-for-all-workloads)
|
||||
before you continue.
|
||||
|
||||
Once you have a kind configuration in place, create the kind cluster with
|
||||
that configuration:
|
||||
|
||||
```shell
|
||||
kind create cluster --config=kind.yaml
|
||||
```
|
||||
|
||||
After the new Kubernetes cluster is ready, identify the Docker container running
|
||||
as the single node cluster:
|
||||
|
||||
```shell
|
||||
docker ps
|
||||
```
|
||||
|
||||
You should see output indicating that a container is running with name
|
||||
`kind-control-plane`. The output is similar to:
|
||||
|
||||
```
|
||||
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
|
||||
6a96207fed4b kindest/node:v1.18.2 "/usr/local/bin/entr…" 27 seconds ago Up 24 seconds 127.0.0.1:42223->6443/tcp kind-control-plane
|
||||
```
|
||||
|
||||
If observing the filesystem of that container, you should see that the
|
||||
`profiles/` directory has been successfully loaded into the default seccomp path
|
||||
of the kubelet. Use `docker exec` to run a command in the Pod:
|
||||
|
||||
```shell
|
||||
# Change 6a96207fed4b to the container ID you saw from "docker ps"
|
||||
docker exec -it 6a96207fed4b ls /var/lib/kubelet/seccomp/profiles
|
||||
```
|
||||
|
||||
```
|
||||
audit.json fine-grained.json violation.json
|
||||
```
|
||||
|
||||
You have verified that these seccomp profiles are available to the kubelet
|
||||
running within kind.
|
||||
|
||||
## Enable the use of `RuntimeDefault` as the default seccomp profile for all workloads
|
||||
|
||||
{{< feature-state state="alpha" for_k8s_version="v1.22" >}}
|
||||
|
||||
`SeccompDefault` is an optional kubelet
|
||||
[feature gate](/docs/reference/command-line-tools-reference/feature-gates) as
|
||||
well as corresponding `--seccomp-default`
|
||||
[command line flag](/docs/reference/command-line-tools-reference/kubelet).
|
||||
Both have to be enabled simultaneously to use the feature.
|
||||
|
||||
If enabled, the kubelet will use the `RuntimeDefault` seccomp profile by default, which is
|
||||
defined by the container runtime, instead of using the `Unconfined` (seccomp disabled) mode.
|
||||
The default profiles aim to provide a strong set
|
||||
of security defaults while preserving the functionality of the workload. It is
|
||||
possible that the default profiles differ between container runtimes and their
|
||||
release versions, for example when comparing those from CRI-O and containerd.
|
||||
|
||||
Some workloads may require a lower amount of syscall restrictions than others.
|
||||
This means that they can fail during runtime even with the `RuntimeDefault`
|
||||
profile. To mitigate such a failure, you can:
|
||||
|
||||
- Run the workload explicitly as `Unconfined`.
|
||||
- Disable the `SeccompDefault` feature for the nodes. Also making sure that
|
||||
workloads get scheduled on nodes where the feature is disabled.
|
||||
- Create a custom seccomp profile for the workload.
|
||||
|
||||
If you were introducing this feature into production-like cluster, the Kubernetes project
|
||||
recommends that you enable this feature gate on a subset of your nodes and then
|
||||
test workload execution before rolling the change out cluster-wide.
|
||||
|
||||
More detailed information about a possible upgrade and downgrade strategy can be
|
||||
found in the [related Kubernetes Enhancement Proposal (KEP)](https://github.com/kubernetes/enhancements/tree/a70cc18/keps/sig-node/2413-seccomp-by-default#upgrade--downgrade-strategy).
|
||||
|
||||
Since the feature is in alpha state it is disabled per default. To enable it,
|
||||
pass the flags `--feature-gates=SeccompDefault=true --seccomp-default` to the
|
||||
`kubelet` CLI or enable it via the [kubelet configuration
|
||||
file](/docs/tasks/administer-cluster/kubelet-config-file/). To enable the
|
||||
feature gate in [kind](https://kind.sigs.k8s.io), ensure that `kind` provides
|
||||
the minimum required Kubernetes version and enables the `SeccompDefault` feature
|
||||
[in the kind configuration](https://kind.sigs.k8s.io/docs/user/quick-start/#enable-feature-gates-in-your-cluster):
|
||||
|
||||
```yaml
|
||||
kind: Cluster
|
||||
apiVersion: kind.x-k8s.io/v1alpha4
|
||||
featureGates:
|
||||
SeccompDefault: true
|
||||
```
|
||||
|
||||
## Create a Pod with a seccomp profile for syscall auditing
|
||||
|
||||
To start off, apply the `audit.json` profile, which will log all syscalls of the
|
||||
process, to a new Pod.
|
||||
|
||||
Here's a manifest for that Pod:
|
||||
|
||||
{{< codenew file="pods/security/seccomp/ga/audit-pod.yaml" >}}
|
||||
|
||||
{{< note >}}
|
||||
The functional support for the already deprecated seccomp annotations
|
||||
`seccomp.security.alpha.kubernetes.io/pod` (for the whole pod) and
|
||||
`container.seccomp.security.alpha.kubernetes.io/[name]` (for a single container)
|
||||
is going to be removed with the release of Kubernetes v1.25. Please always use
|
||||
the native API fields in favor of the annotations.
|
||||
{{< /note >}}
|
||||
|
||||
Create the Pod in the cluster:
|
||||
|
||||
```shell
|
||||
kubectl apply -f https://k8s.io/examples/pods/security/seccomp/ga/audit-pod.yaml
|
||||
```
|
||||
|
||||
This profile does not restrict any syscalls, so the Pod should start
|
||||
successfully.
|
||||
|
||||
```shell
|
||||
kubectl get pod/audit-pod
|
||||
```
|
||||
|
||||
```
|
||||
NAME READY STATUS RESTARTS AGE
|
||||
audit-pod 1/1 Running 0 30s
|
||||
```
|
||||
|
||||
In order to be able to interact with this endpoint exposed by this
|
||||
container, create a NodePort {{< glossary_tooltip text="Services" term_id="service" >}}
|
||||
that allows access to the endpoint from inside the kind control plane container.
|
||||
|
||||
```shell
|
||||
kubectl expose pod audit-pod --type NodePort --port 5678
|
||||
```
|
||||
|
||||
Check what port the Service has been assigned on the node.
|
||||
|
||||
```shell
|
||||
kubectl get service audit-pod
|
||||
```
|
||||
|
||||
The output is similar to:
|
||||
```
|
||||
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
|
||||
audit-pod NodePort 10.111.36.142 <none> 5678:32373/TCP 72s
|
||||
```
|
||||
|
||||
Now you can use `curl` to access that endpoint from inside the kind control plane container,
|
||||
at the port exposed by this Service. Use `docker exec` to run the `curl` command within the
|
||||
container belonging to that control plane container:
|
||||
|
||||
```shell
|
||||
# Change 6a96207fed4b to the control plane container ID you saw from "docker ps"
|
||||
docker exec -it 6a96207fed4b curl localhost:32373
|
||||
```
|
||||
|
||||
```
|
||||
just made some syscalls!
|
||||
```
|
||||
|
||||
You can see that the process is running, but what syscalls did it actually make?
|
||||
Because this Pod is running in a local cluster, you should be able to see those
|
||||
in `/var/log/syslog`. Open up a new terminal window and `tail` the output for
|
||||
calls from `http-echo`:
|
||||
|
||||
```shell
|
||||
tail -f /var/log/syslog | grep 'http-echo'
|
||||
```
|
||||
|
||||
You should already see some logs of syscalls made by `http-echo`, and if you
|
||||
`curl` the endpoint in the control plane container you will see more written.
|
||||
|
||||
For example:
|
||||
```
|
||||
Jul 6 15:37:40 my-machine kernel: [369128.669452] audit: type=1326 audit(1594067860.484:14536): auid=4294967295 uid=0 gid=0 ses=4294967295 pid=29064 comm="http-echo" exe="/http-echo" sig=0 arch=c000003e syscall=51 compat=0 ip=0x46fe1f code=0x7ffc0000
|
||||
Jul 6 15:37:40 my-machine kernel: [369128.669453] audit: type=1326 audit(1594067860.484:14537): auid=4294967295 uid=0 gid=0 ses=4294967295 pid=29064 comm="http-echo" exe="/http-echo" sig=0 arch=c000003e syscall=54 compat=0 ip=0x46fdba code=0x7ffc0000
|
||||
Jul 6 15:37:40 my-machine kernel: [369128.669455] audit: type=1326 audit(1594067860.484:14538): auid=4294967295 uid=0 gid=0 ses=4294967295 pid=29064 comm="http-echo" exe="/http-echo" sig=0 arch=c000003e syscall=202 compat=0 ip=0x455e53 code=0x7ffc0000
|
||||
Jul 6 15:37:40 my-machine kernel: [369128.669456] audit: type=1326 audit(1594067860.484:14539): auid=4294967295 uid=0 gid=0 ses=4294967295 pid=29064 comm="http-echo" exe="/http-echo" sig=0 arch=c000003e syscall=288 compat=0 ip=0x46fdba code=0x7ffc0000
|
||||
Jul 6 15:37:40 my-machine kernel: [369128.669517] audit: type=1326 audit(1594067860.484:14540): auid=4294967295 uid=0 gid=0 ses=4294967295 pid=29064 comm="http-echo" exe="/http-echo" sig=0 arch=c000003e syscall=0 compat=0 ip=0x46fd44 code=0x7ffc0000
|
||||
Jul 6 15:37:40 my-machine kernel: [369128.669519] audit: type=1326 audit(1594067860.484:14541): auid=4294967295 uid=0 gid=0 ses=4294967295 pid=29064 comm="http-echo" exe="/http-echo" sig=0 arch=c000003e syscall=270 compat=0 ip=0x4559b1 code=0x7ffc0000
|
||||
Jul 6 15:38:40 my-machine kernel: [369188.671648] audit: type=1326 audit(1594067920.488:14559): auid=4294967295 uid=0 gid=0 ses=4294967295 pid=29064 comm="http-echo" exe="/http-echo" sig=0 arch=c000003e syscall=270 compat=0 ip=0x4559b1 code=0x7ffc0000
|
||||
Jul 6 15:38:40 my-machine kernel: [369188.671726] audit: type=1326 audit(1594067920.488:14560): auid=4294967295 uid=0 gid=0 ses=4294967295 pid=29064 comm="http-echo" exe="/http-echo" sig=0 arch=c000003e syscall=202 compat=0 ip=0x455e53 code=0x7ffc0000
|
||||
```
|
||||
|
||||
You can begin to understand the syscalls required by the `http-echo` process by
|
||||
looking at the `syscall=` entry on each line. While these are unlikely to
|
||||
encompass all syscalls it uses, it can serve as a basis for a seccomp profile
|
||||
for this container.
|
||||
|
||||
Clean up that Pod and Service before moving to the next section:
|
||||
|
||||
```shell
|
||||
kubectl delete service audit-pod --wait
|
||||
kubectl delete pod audit-pod --wait --now
|
||||
```
|
||||
|
||||
## Create Pod with seccomp profile that causes violation
|
||||
|
||||
For demonstration, apply a profile to the Pod that does not allow for any
|
||||
syscalls.
|
||||
|
||||
The manifest for this demonstration is:
|
||||
|
||||
{{< codenew file="pods/security/seccomp/ga/violation-pod.yaml" >}}
|
||||
|
||||
Attempt to create the Pod in the cluster:
|
||||
|
||||
```shell
|
||||
kubectl apply -f https://k8s.io/examples/pods/security/seccomp/ga/violation-pod.yaml
|
||||
```
|
||||
|
||||
The Pod creates, but there is an issue.
|
||||
If you check the status of the Pod, you should see that it failed to start.
|
||||
|
||||
```shell
|
||||
kubectl get pod/violation-pod
|
||||
```
|
||||
|
||||
```
|
||||
NAME READY STATUS RESTARTS AGE
|
||||
violation-pod 0/1 CrashLoopBackOff 1 6s
|
||||
```
|
||||
|
||||
As seen in the previous example, the `http-echo` process requires quite a few
|
||||
syscalls. Here seccomp has been instructed to error on any syscall by setting
|
||||
`"defaultAction": "SCMP_ACT_ERRNO"`. This is extremely secure, but removes the
|
||||
ability to do anything meaningful. What you really want is to give workloads
|
||||
only the privileges they need.
|
||||
|
||||
Clean up that Pod before moving to the next section:
|
||||
|
||||
```shell
|
||||
kubectl delete pod violation-pod --wait --now
|
||||
```
|
||||
|
||||
## Create Pod with seccomp profile that only allows necessary syscalls
|
||||
|
||||
If you take a look at the `fine-grained.json` profile, you will notice some of the syscalls
|
||||
seen in syslog of the first example where the profile set `"defaultAction":
|
||||
"SCMP_ACT_LOG"`. Now the profile is setting `"defaultAction": "SCMP_ACT_ERRNO"`,
|
||||
but explicitly allowing a set of syscalls in the `"action": "SCMP_ACT_ALLOW"`
|
||||
block. Ideally, the container will run successfully and you will see no messages
|
||||
sent to `syslog`.
|
||||
|
||||
The manifest for this example is:
|
||||
|
||||
{{< codenew file="pods/security/seccomp/ga/fine-pod.yaml" >}}
|
||||
|
||||
Create the Pod in your cluster:
|
||||
|
||||
```shell
|
||||
kubectl apply -f https://k8s.io/examples/pods/security/seccomp/ga/fine-pod.yaml
|
||||
```
|
||||
|
||||
```shell
|
||||
kubectl get pod fine-pod
|
||||
```
|
||||
|
||||
The Pod should be showing as having started successfully:
|
||||
```
|
||||
NAME READY STATUS RESTARTS AGE
|
||||
fine-pod 1/1 Running 0 30s
|
||||
```
|
||||
|
||||
Open up a new terminal window and use `tail` to monitor for log entries that
|
||||
mention calls from `http-echo`:
|
||||
|
||||
```shell
|
||||
# The log path on your computer might be different from "/var/log/syslog"
|
||||
tail -f /var/log/syslog | grep 'http-echo'
|
||||
```
|
||||
|
||||
Next, expose the Pod with a NodePort Service:
|
||||
|
||||
```shell
|
||||
kubectl expose pod fine-pod --type NodePort --port 5678
|
||||
```
|
||||
|
||||
Check what port the Service has been assigned on the node:
|
||||
|
||||
```shell
|
||||
kubectl get service fine-pod
|
||||
```
|
||||
|
||||
The output is similar to:
|
||||
```
|
||||
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
|
||||
fine-pod NodePort 10.111.36.142 <none> 5678:32373/TCP 72s
|
||||
```
|
||||
|
||||
Use `curl` to access that endpoint from inside the kind control plane container:
|
||||
|
||||
```shell
|
||||
# Change 6a96207fed4b to the control plane container ID you saw from "docker ps"
|
||||
docker exec -it 6a96207fed4b curl localhost:32373
|
||||
```
|
||||
|
||||
```
|
||||
just made some syscalls!
|
||||
```
|
||||
|
||||
You should see no output in the `syslog`. This is because the profile allowed all
|
||||
necessary syscalls and specified that an error should occur if one outside of
|
||||
the list is invoked. This is an ideal situation from a security perspective, but
|
||||
required some effort in analyzing the program. It would be nice if there was a
|
||||
simple way to get closer to this security without requiring as much effort.
|
||||
|
||||
Clean up that Pod and Service before moving to the next section:
|
||||
|
||||
```shell
|
||||
kubectl delete service fine-pod --wait
|
||||
kubectl delete pod fine-pod --wait --now
|
||||
```
|
||||
|
||||
## Create Pod that uses the container runtime default seccomp profile
|
||||
|
||||
Most container runtimes provide a sane set of default syscalls that are allowed
|
||||
or not. You can adopt these defaults for your workload by setting the seccomp
|
||||
type in the security context of a pod or container to `RuntimeDefault`.
|
||||
|
||||
{{< note >}}
|
||||
If you have the `SeccompDefault` [feature gate](/docs/reference/command-line-tools-reference/feature-gates/) enabled, then Pods use the `RuntimeDefault` seccomp profile whenever
|
||||
no other seccomp profile is specified. Otherwise, the default is `Unconfined`.
|
||||
{{< /note >}}
|
||||
|
||||
Here's a manifest for a Pod that requests the `RuntimeDefault` seccomp profile
|
||||
for all its containers:
|
||||
|
||||
{{< codenew file="pods/security/seccomp/ga/default-pod.yaml" >}}
|
||||
|
||||
Create that Pod:
|
||||
```shell
|
||||
kubectl apply -f https://k8s.io/examples/pods/security/seccomp/ga/default-pod.yaml
|
||||
```
|
||||
|
||||
```shell
|
||||
kubectl get pod default-pod
|
||||
```
|
||||
|
||||
The Pod should be showing as having started successfully:
|
||||
```
|
||||
NAME READY STATUS RESTARTS AGE
|
||||
default-pod 1/1 Running 0 20s
|
||||
```
|
||||
|
||||
Finally, now that you saw that work OK, clean up:
|
||||
|
||||
```shell
|
||||
kubectl delete pod default-pod --wait --now
|
||||
```
|
||||
|
||||
## {{% heading "whatsnext" %}}
|
||||
|
||||
You can learn more about Linux seccomp:
|
||||
|
||||
* [A seccomp Overview](https://lwn.net/Articles/656307/)
|
||||
* [Seccomp Security Profiles for Docker](https://docs.docker.com/engine/security/seccomp/)
|
||||
Reference in New Issue
Block a user