使用 seccomp 限制容器的系统调用

    Seccomp 代表安全计算(Secure Computing)模式,自 2.6.12 版本以来,一直是 Linux 内核的一个特性。 它可以用来沙箱化进程的权限,限制进程从用户态到内核态的调用。 Kubernetes 能使你自动将加载到 节点上的 seccomp 配置文件应用到你的 Pod 和容器。

    识别你的工作负载所需要的权限是很困难的。在本篇教程中, 你将了解如何将 seccomp 配置文件加载到本地的 Kubernetes 集群中, 如何将它们应用到 Pod,以及如何开始制作只为容器进程提供必要的权限的配置文件。

    • 了解如何在节点上加载 seccomp 配置文件
    • 了解如何将 seccomp 配置文件应用到容器上
    • 观察容器进程对系统调用的审计
    • 观察指定的配置文件缺失时的行为
    • 观察违反 seccomp 配置文件的行为
    • 了解如何创建细粒度的 seccomp 配置文件
    • 了解如何应用容器运行时所默认的 seccomp 配置文件

    准备开始

    为了完成本篇教程中的所有步骤,你必须安装 kind 和 。

    本篇教程演示的某些示例仍然是 alpha 状态(自 v1.22 起),另一些示例则仅使用 seccomp 正式发布的功能。 你应该确保,针对你使用的版本, 正确配置了集群。

    本篇教程也使用了 curl 工具来下载示例到你的计算机上。 你可以使用其他自己偏好的工具来自适应这些步骤。

    说明:

    无法将 seccomp 配置文件应用于在容器的 securityContext 中设置了 privileged: true 的容器。 特权容器始终以 Unconfined 的方式运行。

    下载示例 seccomp 配置文件

    这些配置文件的内容将在稍后进行分析, 现在先将它们下载到名为 profiles/ 的目录中,以便将它们加载到集群中。

    pods/security/seccomp/profiles/violation.json

    1. {
    2. "defaultAction": "SCMP_ACT_ERRNO"
    3. }

    1. {
    2. "defaultAction": "SCMP_ACT_ERRNO",
    3. "architectures": [
    4. "SCMP_ARCH_X86_64",
    5. "SCMP_ARCH_X86",
    6. "SCMP_ARCH_X32"
    7. ],
    8. "syscalls": [
    9. {
    10. "names": [
    11. "accept4",
    12. "epoll_wait",
    13. "pselect6",
    14. "futex",
    15. "madvise",
    16. "epoll_ctl",
    17. "getsockname",
    18. "setsockopt",
    19. "vfork",
    20. "mmap",
    21. "read",
    22. "write",
    23. "close",
    24. "arch_prctl",
    25. "sched_getaffinity",
    26. "munmap",
    27. "brk",
    28. "rt_sigaction",
    29. "rt_sigprocmask",
    30. "sigaltstack",
    31. "gettid",
    32. "clone",
    33. "bind",
    34. "socket",
    35. "openat",
    36. "readlinkat",
    37. "exit_group",
    38. "epoll_create1",
    39. "listen",
    40. "rt_sigreturn",
    41. "sched_yield",
    42. "clock_gettime",
    43. "connect",
    44. "dup2",
    45. "epoll_pwait",
    46. "execve",
    47. "exit",
    48. "fcntl",
    49. "getpid",
    50. "getuid",
    51. "ioctl",
    52. "mprotect",
    53. "nanosleep",
    54. "open",
    55. "poll",
    56. "recvfrom",
    57. "sendto",
    58. "set_tid_address",
    59. "writev"
    60. ],
    61. "action": "SCMP_ACT_ALLOW"
    62. }
    63. ]
    64. }

    执行这些命令:

    1. mkdir ./profiles
    2. curl -L -o profiles/audit.json https://k8s.io/examples/pods/security/seccomp/profiles/audit.json
    3. curl -L -o profiles/violation.json https://k8s.io/examples/pods/security/seccomp/profiles/violation.json
    4. curl -L -o profiles/fine-grained.json https://k8s.io/examples/pods/security/seccomp/profiles/fine-grained.json
    5. ls profiles

    你应该看到在最后一步的末尾列出有三个配置文件:

    1. audit.json fine-grained.json violation.json

    为简单起见,kind 可用来创建加载了 seccomp 配置文件的单节点集群。 Kind 在 Docker 中运行 Kubernetes,因此集群的每个节点都是一个容器。 这允许将文件挂载到每个容器的文件系统中,类似于将文件加载到节点上。

    1. apiVersion: kind.x-k8s.io/v1alpha4
    2. kind: Cluster
    3. nodes:
    4. - role: control-plane
    5. extraMounts:
    6. - hostPath: "./profiles"
    7. containerPath: "/var/lib/kubelet/seccomp/profiles"

    下载该示例 kind 配置,并将其保存到名为 kind.yaml 的文件中:

    1. curl -L -O https://k8s.io/examples/pods/security/seccomp/kind.yaml

    你可以通过设置节点的容器镜像来设置特定的 Kubernetes 版本。 有关此类配置的更多信息, 参阅 kind 文档中节点小节。 本篇教程假定你正在使用 Kubernetes v1.24。

    作为 alpha 特性,你可以将 Kubernetes 配置为使用 默认首选的配置文件,而不是回退到 Unconfined。 如果你想尝试,请在继续之前参阅 启用使用 RuntimeDefault 作为所有工作负载的默认 seccomp 配置文件

    有了 kind 配置后,使用该配置创建 kind 集群:

    1. kind create cluster --config=kind.yaml

    新的 Kubernetes 集群准备就绪后,找出作为单节点集群运行的 Docker 容器:

    你应该看到输出中名为 kind-control-plane 的容器正在运行。 输出类似于:

    1. CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
    2. 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

    如果观察该容器的文件系统, 你应该会看到 profiles/ 目录已成功加载到 kubelet 的默认 seccomp 路径中。 使用 docker exec 在 Pod 中运行命令:

    1. # 将 6a96207fed4b 更改为你从 “docker ps” 看到的容器 ID
    2. docker exec -it 6a96207fed4b ls /var/lib/kubelet/seccomp/profiles
    1. audit.json fine-grained.json violation.json

    你已验证这些 seccomp 配置文件可用于在 kind 中运行的 kubelet。

    启用使用 RuntimeDefault 作为所有工作负载的默认 seccomp 配置文件

    特性状态: Kubernetes v1.22 [alpha]

    SeccompDefault 是一个可选的 kubelet 特性门控 以及相应的 --seccomp-default 。 两者必须同时启用才能使用该功能。

    说明:

    启用该功能既不会更改 Kubernetes securityContext.seccompProfile API 字段, 也不会添加已弃用的工作负载注解。 这为用户提供了随时回滚的可能性,而且无需实际更改工作负载配置。 crictl inspect 之类的工具可用于验证容器正在使用哪个 seccomp 配置文件。

    与其他工作负载相比,某些工作负载可能需要更少的系统调用限制。 这意味着即使使用 RuntimeDefault 配置文件,它们也可能在运行时失败。 要应对此类故障,你可以:

    • 将工作负载显式运行为 Unconfined
    • 禁用节点的 SeccompDefault 功能。还要确保工作负载被调度到禁用该功能的节点上。
    • 为工作负载创建自定义 seccomp 配置文件。

    如果你将此功能引入到类似生产的集群中, Kubernetes 项目建议你在部分节点上启用此特性门控, 然后在整个集群范围内推出更改之前,测试工作负载执行情况。

    有关可能的升级和降级策略的更多详细信息, 请参阅。

    由于此特性处于 alpha 阶段,默认是被禁用的。 要启用它,传递标志 --feature-gates=SeccompDefault=true --seccomp-default 到 kubelet CLI 或者通过 kubelet 配置文件启用。 要在 启用特性门控, 请确保 kind 提供所需的最低 Kubernetes 版本, 并在 kind 配置中 启用了 SeccompDefault 特性:

    1. kind: Cluster
    2. apiVersion: kind.x-k8s.io/v1alpha4
    3. featureGates:
    4. SeccompDefault: true
    5. nodes:
    6. - role: control-plane
    7. image: kindest/node:v1.23.0@sha256:49824ab1727c04e56a21a5d8372a402fcd32ea51ac96a2706a12af38934f81ac
    8. kubeadmConfigPatches:
    9. - |
    10. kind: JoinConfiguration
    11. nodeRegistration:
    12. kubeletExtraArgs:
    13. seccomp-default: "true"
    14. - role: worker
    15. image: kindest/node:v1.23.0@sha256:49824ab1727c04e56a21a5d8372a402fcd32ea51ac96a2706a12af38934f81ac
    16. kubeadmConfigPatches:
    17. - |
    18. kind: JoinConfiguration
    19. nodeRegistration:
    20. kubeletExtraArgs:
    21. feature-gates: SeccompDefault=true
    22. seccomp-default: "true"

    如果集群已就绪,则运行一个 Pod:

    1. kubectl run --rm -it --restart=Never --image=alpine alpine -- sh

    现在应该附加了默认的 seccomp 配置文件。 这可以通过使用 docker exec 为 kind 上的容器运行 crictl inspect 来验证:

    1. docker exec -it kind-worker bash -c \
    2. 'crictl inspect $(crictl ps --name=alpine -q) | jq .info.runtimeSpec.linux.seccomp'
    1. {
    2. "defaultAction": "SCMP_ACT_ERRNO",
    3. "architectures": ["SCMP_ARCH_X86_64", "SCMP_ARCH_X86", "SCMP_ARCH_X32"],
    4. "syscalls": [
    5. {
    6. "names": ["..."]
    7. }
    8. ]
    9. }

    使用 seccomp 配置文件创建 Pod 以进行系统调用审计

    首先,将 audit.json 配置文件应用到新的 Pod 上,该配置文件将记录进程的所有系统调用。

    这是该 Pod 的清单:

    pods/security/seccomp/ga/audit-pod.yaml

    说明:

    已弃用的 seccomp 注解 seccomp.security.alpha.kubernetes.io/pod(针对整个 Pod)和 container.seccomp.security.alpha.kubernetes.io/[name](针对单个容器) 将随着 Kubernetes v1.25 的发布而被删除。 请在可能的情况下使用原生 API 字段而不是注解。

    在集群中创建 Pod:

    1. kubectl apply -f https://k8s.io/examples/pods/security/seccomp/ga/audit-pod.yaml

    此配置文件不限制任何系统调用,因此 Pod 应该成功启动。

    1. kubectl get pod/audit-pod
    1. NAME READY STATUS RESTARTS AGE
    2. audit-pod 1/1 Running 0 30s

    为了能够与容器暴露的端点交互, 创建一个 NodePort 类型的 , 允许从 kind 控制平面容器内部访问端点。

    1. kubectl expose pod audit-pod --type NodePort --port 5678

    检查 Service 在节点上分配的端口。

    1. kubectl get service audit-pod

    输出类似于:

    1. NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
    2. audit-pod NodePort 10.111.36.142 <none> 5678:32373/TCP 72s

    现在,你可以使用 curl 从 kind 控制平面容器内部访问该端点,位于该服务所公开的端口上。 使用 docker exec 在属于该控制平面容器的容器中运行 curl 命令:

    1. # 将 6a96207fed4b 更改为你从 “docker ps” 看到的控制平面容器 ID
    2. docker exec -it 6a96207fed4b curl localhost:32373
    1. just made some syscalls!

    你可以看到该进程正在运行,但它实际上进行了哪些系统调用? 因为这个 Pod 在本地集群中运行,你应该能够在 /var/log/syslog 中看到它们。 打开一个新的终端窗口并 tail 来自 http-echo 的调用的输出:

    1. tail -f /var/log/syslog | grep 'http-echo'

    你应该已经看到了一些由 http-echo 进行的系统调用的日志, 如果你在控制平面容器中 curl 端点,你会看到更多的写入。

    例如:

    1. 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
    2. 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
    3. 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
    4. 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
    5. 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
    6. 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
    7. 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

    通过查看每一行的 syscall= 条目,你可以开始了解 http-echo 进程所需的系统调用。 虽然这些不太可能包含它使用的所有系统调用,但它可以作为此容器的 seccomp 配置文件的基础。

    在转到下一部分之前清理该 Pod 和 Service:

    1. kubectl delete service audit-pod --wait

    出于演示目的,将配置文件应用于不允许任何系统调用的 Pod 上。

    pods/security/seccomp/ga/violation-pod.yaml

    1. apiVersion: v1
    2. kind: Pod
    3. metadata:
    4. name: violation-pod
    5. labels:
    6. app: violation-pod
    7. spec:
    8. securityContext:
    9. seccompProfile:
    10. type: Localhost
    11. localhostProfile: profiles/violation.json
    12. containers:
    13. - name: test-container
    14. image: hashicorp/http-echo:0.2.3
    15. args:
    16. - "-text=just made some syscalls!"
    17. securityContext:
    18. allowPrivilegeEscalation: false

    尝试在集群中创建 Pod:

    1. kubectl apply -f https://k8s.io/examples/pods/security/seccomp/ga/violation-pod.yaml

    Pod 创建,但存在问题。 如果你检查 Pod 状态,你应该看到它没有启动。

    1. kubectl get pod/violation-pod
    1. NAME READY STATUS RESTARTS AGE
    2. violation-pod 0/1 CrashLoopBackOff 1 6s

    如上例所示,http-echo 进程需要相当多的系统调用。 这里 seccomp 已通过设置 "defaultAction": "SCMP_ACT_ERRNO" 被指示为在发生任何系统调用时报错。 这是非常安全的,但消除了做任何有意义的事情的能力。 你真正想要的是只给工作负载它们所需要的权限。

    在转到下一部分之前清理该 Pod:

    使用只允许必要的系统调用的 seccomp 配置文件创建 Pod

    如果你看一看 fine-grained.json 配置文件, 你会注意到第一个示例的 syslog 中看到的一些系统调用, 其中配置文件设置为 "defaultAction": "SCMP_ACT_LOG"。 现在的配置文件设置 "defaultAction": "SCMP_ACT_ERRNO", 但在 "action": "SCMP_ACT_ALLOW" 块中明确允许一组系统调用。 理想情况下,容器将成功运行,并且你看到没有消息发送到 syslog

    此示例的清单是:

    pods/security/seccomp/ga/fine-pod.yaml

    1. apiVersion: v1
    2. kind: Pod
    3. metadata:
    4. name: fine-pod
    5. labels:
    6. app: fine-pod
    7. spec:
    8. securityContext:
    9. seccompProfile:
    10. type: Localhost
    11. localhostProfile: profiles/fine-grained.json
    12. containers:
    13. - name: test-container
    14. image: hashicorp/http-echo:0.2.3
    15. args:
    16. - "-text=just made some syscalls!"
    17. securityContext:
    18. allowPrivilegeEscalation: false

    在你的集群中创建 Pod:

    1. kubectl apply -f https://k8s.io/examples/pods/security/seccomp/ga/fine-pod.yaml
    1. kubectl get pod fine-pod

    此 Pod 应该显示为已成功启动:

    1. NAME READY STATUS RESTARTS AGE
    2. fine-pod 1/1 Running 0 30s

    打开一个新的终端窗口并使用 tail 来监视提到来自 http-echo 的调用的日志条目:

    1. # 你计算机上的日志路径可能与 “/var/log/syslog” 不同
    2. tail -f /var/log/syslog | grep 'http-echo'

    接着,使用 NodePort Service 公开 Pod:

    1. kubectl expose pod fine-pod --type NodePort --port 5678

    检查节点上的 Service 分配了什么端口:

    1. kubectl get service fine-pod

    输出类似于:

    1. NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
    2. fine-pod NodePort 10.111.36.142 <none> 5678:32373/TCP 72s

    使用 curl 从 kind 控制平面容器内部访问端点:

    1. # 将 6a96207fed4b 更改为你从 “docker ps” 看到的控制平面容器 ID
    2. docker exec -it 6a96207fed4b curl localhost:32373
    1. just made some syscalls!

    你应该在 syslog 中看不到任何输出。 这是因为配置文件允许所有必要的系统调用,并指定如果调用列表之外的系统调用应发生错误。 从安全角度来看,这是一种理想的情况,但需要在分析程序时付出一些努力。 如果有一种简单的方法可以在不需要太多努力的情况下更接近这种安全性,那就太好了。

    在转到下一部分之前清理该 Pod 和服务:

    1. kubectl delete service fine-pod --wait
    2. kubectl delete pod fine-pod --wait --now

    创建使用容器运行时默认 seccomp 配置文件的 Pod

    大多数容器运行时都提供了一组合理的默认系统调用,以及是否允许执行这些系统调用。 你可以通过将 Pod 或容器的安全上下文中的 seccomp 类型设置为 RuntimeDefault 来为你的工作负载采用这些默认值。

    说明:

    如果你已经启用了 SeccompDefault 特性门控, 只要没有指定其他 seccomp 配置文件,那么 Pod 就会使用 SeccompDefault 的 seccomp 配置文件。 否则,默认值为 Unconfined

    这是一个 Pod 的清单,它要求其所有容器使用 RuntimeDefault seccomp 配置文件:

    1. apiVersion: v1
    2. kind: Pod
    3. metadata:
    4. name: default-pod
    5. labels:
    6. app: default-pod
    7. spec:
    8. securityContext:
    9. seccompProfile:
    10. type: RuntimeDefault
    11. containers:
    12. - name: test-container
    13. image: hashicorp/http-echo:0.2.3
    14. args:
    15. - "-text=just made some more syscalls!"
    16. securityContext:
    17. allowPrivilegeEscalation: false

    创建此 Pod:

    1. kubectl apply -f https://k8s.io/examples/pods/security/seccomp/ga/default-pod.yaml
    1. kubectl get pod default-pod

    此 Pod 应该显示为成功启动:

    1. NAME READY STATUS RESTARTS AGE

    最后,你看到一切正常之后,请清理: