共计 12363 个字符,预计需要花费 31 分钟才能阅读完成。
导读 | 开发人员需要了解如何在容器化应用程序中嵌入控件,以及如何启用运行时保护机制以阻止黑客访问容器化系统。 |
Kubernetes 是当前流行和常用的容器编排工具之一。Kubernetes 工作负载就像简单的 nginx 服务器或 cron 作业一样执行的应用程序。Kubernetes 部署成为了一种最常用的工作负载,因为它可以轻松更新、扩展和管理。
最近发布的 Kubernetes Hardening 指南是一个很好的资源,它提供了有关如何有效保护 Kubernetes 的指导。该指南提供的信息清楚地表明,保护和强化 Kubernetes 不仅是 Kubernetes 管理员的工作,也是在集群上部署工作负载的开发人员的工作。
本文讨论部署 Kubernetes 工作负载的开发人员如何通过应用“Kubernetes 强化指南”提供的一些指南来引导安全性。
这是一个实用的指南,将采用一个简单的 Dockerfile,然后逐步添加安全优秀实践来创建模板部署清单文件,开发人员可以快速重用该文件。
- Docker 是必需的,因为将从头开始构建。
- 像 minikube 这样的单节点 Kubernetes 集群应该遵循这一指南以及 kubectl 实用程序。开发人员可以使用官方 minikube 文档在其环境中进行设置。
使用由与 WSL2 绑定的 Docker Desktop 创建的独立集群作为后端。
本指南假设有一个可通过 kubectl 实用程序访问的正在运行的集群,如下面的代码所示。
Shell | |
git clone git@github.com:salecharohit/bootstrapsecurityinkubernetesdeployment.git | |
cd springbootmaven | |
docker build . -f Dockerfile.basic -t springbootmaven | |
docker run --name springboot -d -p 8080:8080 springbootmaven | |
curl http://localhost:8080 | |
Expected Response: | |
Hello World From Spring Boot Build Using Maven on Alpine OS! |
保护 Kubernetes 工作负载可以有效地划分为“构建时”的安全性和“运行时”的安全性。为了运行这些示例,将使用这个简单的 Spring Boot Hello World 应用程序并将其部署在 Kubernetes 中,并应用构建时安全性和运行时安全性。相关网址如下:https://github.com/salecharohit/bootstrapsecurityinkubernetesdeployment
而在开始之前,先要克隆这个存储库,构建 Docker 容器,并在本地运行应用程序。
构建时的安全性更多地关注如何以减少的占用空间构建底层容器,并编程以尽可能少的权限执行。
以下将使用问题的解决方法讨论这两种方法:
在容器中构建应用程序时,主要目标是让应用程序在不考虑运行环境的情况下始终独立运行,无论其运行环境是数据中心、云平台还是内部部署设施。然而,在构建这些应用程序时有一条不成文的规则:它应该是一个独立的应用程序,并且没有很多依赖项。
以 SpringBoot 应用程序为例。这个应用程序运行的唯一依赖是它需要一个 JVM 或 Java 运行时。任何其他在容器中的东西实际上都是无用的。
例如,在基于 AlpineOS 构建的 SpringBoot 容器中,没有任何特定需要安装 apk 包管理器。
Shell | |
docker exec -it springboot /bin/sh | |
apk add curl |
因此,可以尝试删除 apk 二进制文件并重建或 Docker 映像。
此时将使用 Dockerfile.asr 来重建 Docker 容器,其共享如下:
Dockerfile | |
FROM maven:3.8.1-openjdk-17-slim AS MAVEN_BUILD | |
WORKDIR /build/ | |
COPY pom.xml /build/ | |
COPY src /build/src/ | |
RUN mvn package | |
FROM openjdk:17-alpine | |
RUN rm -f /sbin/apk && \ | |
rm -rf /etc/apk && \ | |
rm -rf /lib/apk && \ | |
rm -rf /usr/share/apk && \ | |
rm -rf rm -rf /var/lib/apk | |
COPY --from=MAVEN_BUILD /build/target/springbootmaven.jar /springbootmaven.jar | |
EXPOSE 8080 | |
CMD java -jar /springbootmaven.jar |
在此重建并重新运行:
Shell | |
# First let's stop the previously running container | |
docker stop springboot | |
# Next let's re-build and re-run | |
docker build . -f Dockerfile.asr -t springbootmaven | |
docker run --name springboot -p 8080:8080 springbootmaven | |
docker run --name springboot -d -p 8080:8080 springbootmaven | |
curl http://localhost:8080 |
现在尝试再次运行 apk add curl 命令。
Shell | |
docker exec -it springboot /bin/sh | |
apk add curl |
因此成功摆脱了 apk 依赖,并且应用程序运行成功!
下面是一些专门为强化 Alpine OS 编写的优秀脚本。根据编程语言进行挑选,并相应地强化基本 alpine 图像。以下是一些参考的网址:
https://gist.github.com/kost/017e95aa24f454f77a37
https://github.com/ironpeakservices/iron-alpine/blob/master/Dockerfile
另一方面,还可以查看由谷歌公司创建的 distroless 容器,这也是非常值得推荐的。
https://github.com/GoogleContainerTools/distroless/tree/main/examples
有人可能会争辩说,如果网络攻击者在容器内获得 RCE,他们可能无法安装 curl、wget 等包来建立持久性。
但是,仍然以“root”用户身份运行,从技术上讲,仍然可以重新安装 apk。
在此重新运行 Docker 容器并检查它当前运行的权限。
Shell | |
docker exec -it springboot /bin/sh | |
whoami | |
ping rohitsalecha.com |
因此,重要的是不要以 root 身份运行容器,而是以只有有限权限的用户身份运行容器。
Dockerfile.lpr 显示了添加更多命令,这些命令添加了一个名为“boot”的用户和组,并为其分配一个工作目录(这是它的主目录)。还为用户和组分配了数值,以下将在 Pod 安全场景部分详细讨论。
Dockerfile | |
FROM maven:3.8.1-openjdk-17-slim AS MAVEN_BUILD | |
WORKDIR /build/ | |
COPY pom.xml /build/ | |
COPY src /build/src/ | |
RUN mvn package | |
FROM openjdk:17-alpine | |
# Removing apk package manager | |
RUN rm -f /sbin/apk && \ | |
rm -rf /etc/apk && \ | |
rm -rf /lib/apk && \ | |
rm -rf /usr/share/apk && \ | |
rm -rf rm -rf /var/lib/apk | |
# Adding a user and group called "boot" | |
RUN addgroup boot -g 1337 && \ | |
adduser -D -h /home/boot -u 1337 -s /bin/ash boot -G boot | |
# Changing the context that shall run the below commands with User "boot" instead of root | |
USER boot | |
WORKDIR /home/boot | |
# By default even in a non-root context, Docker copies the file as root. Hence its best practice to chown | |
# the files being copied as the user. https://stackoverflow.com/a/44766666/1679541 | |
COPY --chown=boot:boot --from=MAVEN_BUILD /build/target/springbootmaven.jar /home/boot/springbootmaven.jar | |
EXPOSE 8080 | |
CMD java -jar /home/boot/springbootmaven.jar |
重建并重新运行:
# First let's stop the previously running container | |
docker stop springboot | |
# Next let's re-build and re-run | |
docker build . -f Dockerfile.lpr -t springbootmaven docker run --name springboot -d -p 8080:8080 springbootmaven curl http://localhost:8080 |
现在尝试运行 whoami 命令,并检查现在正在运行哪个容器的哪些权限。
Shell | |
docker exec -it springboot /bin/sh | |
whoami | |
ping rohitsalecha.com |
现在人们对构建时安全性有了很大的信心,其中已经学会了删除包并更新用户场景,以使用有限的权限运行容器。这些安全特性是在构建 Docker 容器时应用的; 但是,还需要关注容器在 Kubernetes 环境中运行时的安全状况,这将在下面进行探讨。
在开始保护 Kubernetes 部署之前,首先将 Docker 容器推送到 hub.docker.com,在 Kubernetes 集群上运行的应用程序。可以使用这一指南开始相同的操作。
Shell | |
docker build . -f Dockerfile.lpr -t springbootmaven | |
docker tag springbootmaven salecharohit/springbootmaven | |
docker push salecharohit/springbootmaven | |
docker run -d -p 8080:8080 --name springboot salecharohit/springbootmaven | |
curl http://localhost:8080 |
现在 Docker 镜像已经准备好了,应用 kubernetes-basic.yaml 文件来部署这个应用程序以及一个可以帮助连接到它的服务。
YAML | |
# Create Namespace | |
apiVersion: v1 | |
kind: Namespace | |
metadata: | |
name: boot | |
# Create SpringBoot Deployment | |
apiVersion: apps/v1 | |
kind: Deployment | |
metadata: | |
labels: | |
app: springbootmaven | |
name: springbootmaven | |
namespace: boot | |
spec: | |
replicas: 1 | |
selector: | |
matchLabels: | |
app: springbootmaven | |
template: | |
metadata: | |
labels: | |
app: springbootmaven | |
spec: | |
containers: | |
- image: salecharohit/springbootmaven | |
name: springbootmaven | |
ports: | |
- containerPort: 8080 | |
# Create Service for SpringBoot Deployment | |
apiVersion: v1 | |
kind: Service | |
metadata: | |
labels: | |
app: springbootmaven | |
name: springbootmaven | |
namespace: boot | |
spec: | |
ports: | |
- name: "http" | |
port: 8080 | |
targetPort: 8080 | |
selector: | |
app: springbootmaven |
如果 Pod 需要与 Kubernetes API-Server 通信,则需要服务帐户令牌进行身份验证。
Shell | |
kubectl apply -f kubernetes-basic.yaml | |
kubectl get deploy -n boot | |
# Run a temporary container that will only curl our bootservice | |
kubectl run -it testpod --image=radial/busyboxplus:curl --restart=Never --rm -- curl http://springbootmaven.boot.svc.cluster.local:8080 | |
Expected Output: | |
Hello World From Spring Boot Build Using Maven on Alpine OS!pod "testpod" deleted |
如果 Pod 需要与 Kubernetes API 服务器通信,则需要服务帐户令牌进行身份验证。
在默认情况下,每个 Pod 都会分配一个服务帐户令牌,该令牌安装在 /var/run/secrets/kubernetes.io/serviceaccount/token 上。可以通过部署 SpringBoot 应用程序在实践中查看这一点。
Shell | |
kubectl get pods -n boot | |
kubectl exec -it springbootmaven-7d7c5c8597-mndv9 -n boot -- /bin/sh | |
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token) | |
curl -k -H "Authorization:Bearer $TOKEN" https://kubernetes.docker.internal:6443/version |
应用程序上的 RCE 漏洞可以将此访问令牌泄露给网络攻击者,他们可以滥用该令牌来读写同一命名空间中的资源,甚至具有全局读取权限。
解决这一问题有两个解决方案,具体取决于具体情况:一是 Pod 不需要访问 API-Server。二是 Pod 需要访问 API-Server。
- 不需要访问 API-Server 的 Pod
这种情况很容易解决,只需在 Kubernetes 清单文件中添加两行,如下所示:
YAML | |
1 serviceAccountName: "" | |
2 automountServiceAccountToken: false |
完整的部署文件 kubernetes-nosa.yaml 如下:
YAML | |
apiVersion: apps/v1 | |
kind: Deployment | |
metadata: | |
labels: | |
app: springbootmaven | |
name: springbootmaven | |
namespace: boot | |
spec: | |
eplicas: 1 | |
selector: | |
matchLabels: | |
app: springbootmaven | |
template: | |
metadata: | |
labels: | |
app: springbootmaven | |
spec: | |
containers: | |
image: salecharohit/springbootmaven | |
name: springbootmaven | |
ports: | |
- containerPort: 8080 | |
serviceAccountName: "" | |
automountServiceAccountToken: false |
然后检查服务帐户令牌现在是否已安装。
Shell | |
# Ensure our previous deploy is deleted. | |
kubectl delete ns boot | |
# Apply with no service account token | |
kubectl apply -f kubernetes-nosa.yaml | |
kubectl get pods -n boot | |
kubectl exec -it springbootmaven-5568b9874f-8nml8 -n boot -- /bin/sh | |
cat /var/run/secrets/kubernetes.io/serviceaccount/token |
如上所示,不再挂载默认服务帐户令牌。
- 需要访问 API-Server 的 Pod
在这种情况下,需要创建将 ServiceAccount 映射到角色的 ServiceAccount、Role 和 RoleBinding。以下是 Kubernetes 清单:
- 为特定命名空间 (即 boot) 创建一个名为 bootserviceaccount 的 ServiceAccount。
- 创建一个名为 bootservicerole 的角色,该角色只有查看正在运行的 Pod 的权限。
- 创建一个名为 bootservicerolebinding 的 RoleBinding。
- 挂载 ServiceAccount,从而在部署中使用以下几行进行创建。
YAML | |
spec: | |
containers: | |
- image: salecharohit/springbootmaven | |
name: springbootmaven | |
ports: | |
- containerPort: 8080 | |
serviceAccountName: bootserviceaccount | |
这将允许仅读取“boot”命名空间中的 Pod。
完整的部署文件 kubernetes-withsa.yaml 如下:
YAML | |
# Create Namespace | |
apiVersion: v1 | |
kind: Namespace | |
metadata: | |
name: boot | |
apiVersion: v1 | |
kind: ServiceAccount | |
metadata: | |
name: bootserviceaccount | |
namespace: boot | |
kind: Role | |
apiVersion: rbac.authorization.k8s.io/v1 | |
metadata: | |
name: bootservicerole | |
namespace: boot | |
rules: | |
- apiGroups: [""] | |
resources: ["pods"] | |
verbs: ["get", "list", "watch"] | |
kind: RoleBinding | |
apiVersion: rbac.authorization.k8s.io/v1 | |
metadata: | |
name: bootservicerolebinding | |
namespace: boot | |
subjects: | |
- kind: ServiceAccount | |
name: bootserviceaccount | |
namespace: boot | |
roleRef: | |
kind: Role | |
name: bootservicerole | |
apiGroup: rbac.authorization.k8s.io | |
# Create SpringBoot Deployment | |
apiVersion: apps/v1 | |
kind: Deployment | |
metadata: | |
labels: | |
app: springbootmaven | |
name: springbootmaven | |
namespace: boot | |
spec: | |
replicas: 1 | |
selector: | |
matchLabels: | |
app: springbootmaven | |
template: | |
metadata: | |
labels: | |
app: springbootmaven | |
spec: | |
containers: | |
- image: salecharohit/springbootmaven | |
name: springbootmaven | |
ports: | |
- containerPort: 8080 | |
serviceAccountName: bootserviceaccount | |
# Create Service for SpringBoot Deployment | |
apiVersion: v1 | |
kind: Service | |
metadata: | |
labels: | |
app: springbootmaven | |
name: springbootmaven | |
namespace: boot | |
spec: | |
ports: | |
- name: "http" | |
port: 8080 | |
targetPort: 8080 | |
selector: | |
app: springbootmaven |
现在运行并检查的应用程序是否运行良好。
# Ensure our previous deploy is deleted. | |
kubectl delete ns boot | |
kubectl apply -f kubernetes-withsa.yaml | |
kubectl run -it testpod --image=radial/busyboxplus:curl --restart=Never --rm -- curl http://springbootmaven.boot.svc.cluster.local:8080 |
# 确保删除以前的部署。
尽管已将基本 Docker 映像配置为以非 root 权限运行,但仍需要添加少量配置作为安全优秀实践。因此需要:
- ①限制容器和 Pod 的能力。
- ②禁用权限提升。
- ③将容器配置为使用先前在 Dockerfile.lpr 中创建的特定 uid/gid 运行。
在 Kubernetes 清单文件中,定义了两种类型的“安全场景(SecurityContexts)”。
- 在 Pod 级别运行,这将应用到在这个 Pod 中运行的所有容器
YAML | |
--- | |
securityContext: | |
fsGroup: 1337 | |
runAsNonRoot: true | |
runAsUser: 1337 | |
containers: | |
- 在容器级别运行
YAML | |
securityContext: | |
allowPrivilegeEscalation: false | |
privileged: false | |
runAsUser: 1337 | |
capabilities: | |
drop: ["SETUID", "SETGID"] | |
serviceAccountName: "" | |
automountServiceAccountToken: false | |
嵌入 PodSecurity 场景的完整部署文件 kubernetes-ps.yaml 如下:
YAML | |
# Create Namespace | |
apiVersion: v1 | |
kind: Namespace | |
metadata: | |
name: boot | |
# Create SpringBoot Deployment | |
apiVersion: apps/v1 | |
kind: Deployment | |
metadata: | |
labels: | |
app: springbootmaven | |
name: springbootmaven | |
namespace: boot | |
spec: | |
replicas: 1 | |
selector: | |
matchLabels: | |
app: springbootmaven | |
template: | |
metadata: | |
labels: | |
app: springbootmaven | |
spec: | |
securityContext: | |
fsGroup: 1337 | |
runAsNonRoot: true | |
runAsUser: 1337 | |
containers: | |
- image: salecharohit/springbootmaven | |
name: springbootmaven | |
ports: | |
- containerPort: 8080 | |
securityContext: | |
allowPrivilegeEscalation: false | |
privileged: false | |
runAsUser: 1337 | |
capabilities: | |
drop: ["SETUID", "SETGID"] | |
erviceAccountName: "" | |
automountServiceAccountToken: false | |
# Create Service for SpringBoot Deployment | |
apiVersion: v1 | |
kind: Service | |
metadata: | |
labels: | |
app: springbootmaven | |
name: springbootmaven | |
namespace: boot | |
spec: | |
ports: | |
- name: "http" | |
port: 8080 | |
targetPort: 8080 | |
selector: | |
app: springbootmaven |
运行并测试应用程序是否正在运行。
Shell | |
# Ensure our previous apply is deleted | |
kubectl delete ns boot | |
kubectl apply -f kubernetes-ps.yaml | |
kubectl run -it testpod --image=radial/busyboxplus:curl --restart=Never --rm -- curl http://springbootmaven.boot.svc.cluster.local:8080 | |
kubectl get pods -n boot | |
kubectl exec -it springbootmaven-56c64ff85-mqz2z -n boot -- /bin/sh | |
whoami | |
id | |
ping google.com |
开发人员可以根据自己的需求删除更多功能。
AppArmor 或 SecComp 等功能需要控制平面组件的附加配置。因此,我的讨论仅限于开箱即用的功能,这些功能可以轻松激活并确保良好的安全保证水平。
在容器化环境中运行的应用程序很少写入数据,因为这实际上违背了拥有不可变系统的逻辑。但是,有时可能需要它来缓存或临时交换 / 处理文件。因此,为了向开发人员提供此功能,可以将 emptyDir 装载为临时卷,一旦容器被终止,该临时卷就会丢失。
有了它,还可以添加另一个名为“readOnlyRootFilesystem”的安全场景属性,并将其设置为 true,因为在容器中运行的应用程序不再需要在文件系统上除“tmp”目录以外的任何位置写入。
可以按如下所示配置上述要求。
YAML | |
containers: | |
- image: salecharohit/springbootmaven | |
name: springbootmaven | |
ports: | |
- containerPort: 8080 | |
securityContext: | |
readOnlyRootFilesystem: true | |
volumeMounts: | |
- mountPath: /tmp | |
name: tmp | |
volumes: | |
- emptyDir: {} | |
name: tmp | |
--- |
完整的部署文件 kubernetes-rofs.yaml 如下面的代码所示:
YAML | |
# Create Namespace | |
apiVersion: v1 | |
kind: Namespace | |
metadata: | |
name: boot | |
# Create SpringBoot Deployment | |
apiVersion: apps/v1 | |
kind: Deployment | |
metadata: | |
labels: | |
app: springbootmaven | |
name: springbootmaven | |
namespace: boot | |
spec: | |
replicas: 1 | |
selector: | |
matchLabels: | |
app: springbootmaven | |
template: | |
metadata: | |
labels: | |
app: springbootmaven | |
spec: | |
securityContext: | |
fsGroup: 1337 | |
runAsNonRoot: true | |
runAsUser: 1337 | |
containers: | |
- image: salecharohit/springbootmaven | |
name: springbootmaven | |
ports: | |
- containerPort: 8080 | |
securityContext: | |
allowPrivilegeEscalation: false | |
readOnlyRootFilesystem: true | |
privileged: false | |
runAsUser: 1337 | |
capabilities: | |
drop: ["SETUID", "SETGID"] | |
volumeMounts: | |
- mountPath: /tmp | |
name: tmp | |
serviceAccountName: "" | |
automountServiceAccountToken: false | |
volumes: | |
- emptyDir: {} | |
name: tmp | |
# Create Service for SpringBoot Deployment | |
apiVersion: v1 | |
kind: Service | |
metadata: | |
labels: | |
app: springbootmaven | |
name: springbootmaven | |
namespace: boot | |
spec: | |
ports: | |
- name: "http" | |
port: 8080 | |
targetPort: 8080 | |
selector: | |
app: springbootmaven |
开始应用并测试应用程序是否正在运行。
Shell | |
# Ensure our previous apply is deleted | |
kubectl delete ns boot | |
kubectl apply -f kubernetes-rofs.yaml | |
kubectl run -it testpod --image=radial/busyboxplus:curl --restart=Never --rm -- curl http://springbootmaven.boot.svc.cluster.local:8080 | |
kubectl get pods -n boot | |
kubectl exec -it springbootmaven-56c64ff85-mqz2z -n boot -- /bin/sh | |
pwd | |
touch test.txt |
如今已经了解了可以在容器化应用程序中嵌入哪些不同的控件,还了解了如何启用运行时保护机制,这些机制可以使网络攻击者难以在容器化系统中站稳脚跟。
kubernetes-rofs.yaml 可以作为一个很好的模板,供开发人员在 kubernetes 环境中部署时使用默认的安全功能对其应用程序进行容器化。
当然,企业需要为特定的应用程序创建 Dockerfile。
