本文提供了在Kubernetes上部署安全,可伸缩和弹性服务的最佳实践。内容开源在github仓库。如果有缺少或不足之处,欢迎提issue。
Part1 应用开发
健康检测
- 为容器配置Readiness探针
- 如果未设置readiness探针,则kubelet会假定该应用程序已准备就绪,可以在容器启动后立即接收流量。
- 如果容器需要2分钟才能启动,则这2分钟内对容器的所有请求将失败。
- 发生致命错误时允许容器崩溃
如果应用程序遇到不可恢复的错误,则应使其崩溃。
此类不可恢复的错误的示例是:
- 未捕获的异常
- 代码中的错字(动态语言)
- 无法加载标头或依赖项
上述错误不应发信号通知Liveness探针失败。相反,应该立即退出该进程,并让kubelet重新启动容器。
- 配置被动的Liveness探针
- Liveness探针旨在容器卡住时重新启动容器。
- 考虑以下情形:如果应用程序正在处理无限循环,则无法退出。当该进程消耗100%的CPU时,将没有时间回复(其他)Readiness探针检查,并且最终将其从服务中删除。但是,该Pod仍被注册为当前Deployment的活动副本。如果没有Liveness探针,它将保持运行状态,但与服务分离。换句话说,该进程不仅不处理任何请求,而且也在消耗资源。
- 请注意,不应该使用Liveness探针来处理应用程序中的致命错误,并要求Kubernetes重新启动应用程序。相反,应该让应用程序崩溃。仅在过程无响应的情况下,才应将“liveness”探针用作恢复机制。
- 两个探针的值不同
- 当“liveness”和“readiness”探针指向相同的端点时,探针的作用会合并在一起。当应用程序发出信号表明尚未准备就绪或尚待运行时,kubelet会将容器与服务分离并同时将其删除。这时可能会注意到连接断开,因为容器没有足够的时间耗尽当前连接或处理传入的连接。
- 更多信息参考:handling-client-requests-properly-with-kubernetes/
请注意,readiness和liveness没有默认值。
应用独立
Readiness探针是独立的
- Readiness不包括对服务的依赖性,例如:数据库、数据库的迁移、API、第三方服务(反例)
应用重试连接到依赖服务
- 应用启动时,它不应该因为数据库等依赖项尚未就绪而崩溃。相反,应用程序应继续尝试重新连接数据库,直到成功为止。
- Kubernetes希望可以以任何顺序启动应用程序。当确保应用程序可以重新连接到诸如数据库之类的依赖项时,便知道可以提供更强大,更灵活的服务。
友好关闭
- 应用程序未通过SIGTERM关闭,但可以正常终止连接
- 可能需要一些时间才能感知到诸如kube-proxy或Ingress控制器之类的组件endpoint更改。因此,尽管标记为已终止,流量仍可能流向Pod。
- 应用程序应停止在所有剩余连接上接受新请求,并在耗尽传出队列后将其关闭。
- 如果想回顾endpoint在群集中的传播方式,请参考:handling-client-requests-properly-with-kubernetes/
- 应用程序仍在宽限期内处理传入的请求
- 可能要考虑使用容器生命周期事件(例如preStop处理程序)来自定义Pod删除之前发生的情况。
- Dockerfile中的CMD将SIGTERM转发到进程
- 通过在应用中捕获SIGTERM信号,可以在Pod即将终止时收到通知。
- 更多信息参考:graceful-shutdown-of-kubernetes-pods
- 关闭所有空闲的keep-alive套接字
- 如果应用程序调用未关闭TCP连接(例如使用TCP保持活动状态或连接池),它将连接到一个Pod,而不使用该服务中的其他Pod。
- 不应该突然终止长期存在的连接。相反,应该在关闭应用程序之前终止它们。
- 更多信息参考:gracefully-shutting-down-a-nodejs-http-server
失败容忍
- 为Deployment部署运行多个副本
- 切勿单独运行一个Pod类型的资源,而是考虑将Pod作为Deployment,DaemonSet,ReplicaSet或StatefulSet的一部分进行部署。
- 示例参考:Node-Management-In-GKE
- 避免将Pod放置在单个节点中
- 即使运行Pod的多个副本,也无法保证丢失节点不会影响服务。
- 应该将反关联性规则应用于部署,以便Pod分布在群集的所有节点中。
- 更多信息参考:inter-pod-affinity-and-anti-affinit
- 设定Pod中断预算
- drain节点后,该节点上的所有Pod都将被删除并重新安排。
- 为了保护Deployment免受可能同时摧毁多个Pod的意外事件的影响,可以定义Pod中断预算。
- 更多信息参考:pod-disruptions
资源使用
- 为所有容器设置内存限制和请求
- 资源限制用于限制容器可以使用多少CPU和内存,并使用containerSpec的resources属性设置。
- 调度程序将这些用作度量标准之一,以确定哪个节点最适合当前Pod。
- 根据调度程序,没有内存限制的容器的内存利用率为零。
- 如果可调度在任何节点上的Pod数量不受限制,则会导致资源超负荷使用并可能导致节点(和kubelet)崩溃。
- 如果容器进程超出内存限制,则该进程将终止。由于CPU是可压缩的资源,因此如果容器超出限制,则将限制该过程。即使它可以使用当时可用的某些CPU。
- 更多信息参考:understanding-resource-limits-in-kubernetes-memory,understanding-resource-limits-in-kubernetes-cpu
- 将CPU请求设置为1个CPU或以下
- 除非有计算密集型作业,否则建议将请求设置为1个CPU或更低.
- 更多信息参考:YouTube视频
- 禁用CPU限制—除非有很好的用例
- CPU 资源以 CPU 单位度量。
- cpu:1表示每秒1个CPU单位。如果有1个线程,则每秒消耗的CPU时间不能超过1秒。如果有2个线程,则可以在0.5秒内消耗1个CPU单位。8个线程可以在0.125秒内消耗1个CPU单位。此后,请求将受到限制。
- 如果不确定最佳应用设置,最好不要设置CPU限制。
- 更多信息参考:understanding-resource-limits-in-kubernetes-cpu
- 命名空间具有LimitRange
- 如果我们认为可能忘记设置内存和CPU限制,则应考虑使用LimitRange对象为当前名称空间中部署的容器定义标准大小。
- 设置方法参考:limit-range
- 为Pod设置适当的服务质量(QoS)
- 当节点进入过量使用状态(即使用过多资源)时,Kubernetes会尝试驱逐该节点中的某些Pod。
- Kubernetes根据定义明确的逻辑对Pod进行排名和逐出。
- 设置方法参考:quality-service-pod
请注意,如果不确定如何配置正确的CPU或内存限制,则可以使用Kubernetes中的Vertical Pod Autoscaler。自动缩放器会分析应用并给出建议的值。
标签资源
- 定义技术标签
- 定义业务标签
- 定义安全标签
日志配置
- 将应用程序日志记录到stdout和stderr
- 有两种日志记录策略:被动和主动。使用被动日志记录的应用程序不了解日志记录基础结构,而是将消息记录到标准输出中。
- 在主动日志记录中,该应用程序与中间聚合器建立网络连接,将数据发送到第三方日志记录服务,或直接写入数据库或索引。主动日志记录被视为反模式,应避免使用它。
- 最佳实践参考:logs
- 避免使用sidecar记录日志(如果可以的话)
- 如果希望将日志转换应用于具有非标准日志事件模型的应用程序,则可能需要使用sidecar容器。
- 使用Sidecar容器,可以在将日志条目运送到其他地方之前对其进行规范化。例如,先将Apache日志转换为Logstash JSON格式,然后再将其发送到日志基础结构。但是,如果可以控制应用程序,则可以从一开始就输出正确的格式。这样可以节省为集群中的每个Pod运行额外的容器的时间。
Pod扩缩容
- 容器在其本地文件系统中不存储任何状态
- 容器可以访问本地文件系统,用户可能会想使用它来持久化数据。
- 但是,将持久性数据存储在容器的本地文件系统中会阻止Pod进行水平缩放(即通过添加或删除Pod的副本)。
- 这是因为,通过使用本地文件系统,每个容器都维护自己的“状态”,这意味着Pod副本的状态可能会随时间而变化。从用户的角度来看,这会导致行为不一致(例如,当请求命中一个Pod时,一条特定的用户信息可用,但当请求命中另一个Pod时,则不可用)。
- 相反,任何持久性信息都应保存在Pod外部的集中位置。例如,在集群中的PersistentVolume中,或者在集群外部的某些存储服务中甚至更好。
- 对具有可变使用模式的应用程序使用HPA
- HPA是内置的Kubernetes功能,可监视应用程序并根据当前使用情况自动添加或删除Pod副本。
- 配置HPA可使应用在任何流量情况下(包括意外的高峰)保持可用并响应。
- 配置HPA时必须创建一个HorizontalPodAutoscaler资源,该资源定义要监视的应用程序的度量。
- HPA可以监视内置资源指标(Pod的CPU和内存使用情况)或自定义指标。对于自定义指标,还负责收集和公开这些指标,例如,可以使用Prometheus和Prometheus Adapter进行此操作。
- Vertical Pod Autoscaler仍处于Beta版,请勿使用
- 类似于HPA,还有VPA。
- VPA可以自动调整Pod的资源请求和限制,以便当Pod需要更多资源时可以获取它们(增加/减少单个Pod的资源称为垂直缩放,与水平缩放相对)。
- 这对于缩放无法水平缩放的应用程序很有用。
- 但是,HPA当前处于beta版本,它具有一些已知的局限性(例如,通过更改其资源要求来扩展Pod,要求终止Pod并重新启动它)。
- 考虑到这些限制以及Kubernetes上大多数应用程序都可以水平扩展的事实,建议不要在生产环境中使用VPA(至少要等到稳定的版本才能使用)。
- 如果工作负载差异很大,请使用群集自动伸缩放器
- 群集自动缩放器是“自动缩放器”的另一种类型(HAP和VPA除外)。
- 群集自动缩放器可以通过添加或删除工作节点来自动缩放群集的大小。
- 当由于现有工作节点上的资源不足而无法调度Pod时,会进行放大操作。在这种情况下,Cluster Autoscaler将创建一个新的工作节点,以便可以调度Pod。同样,当现有工作节点的利用率较低时,群集自动伸缩程序可以通过从一个工作节点中逐出所有工作负载并将其删除来进行缩减。
- 对于高度可变的工作负载,例如当Pods的数量可能在短时间内成倍增长然后返回到先前的值时,使用Cluster Autoscaler是有意义的。在这种情况下,群集自动伸缩器可以满足需求高峰,而不会通过过度配置工作节点来浪费资源。
- 但是,如果工作负载变化不大,则可能不值得设置Cluster Autoscaler,因为它可能永远不会触发。如果工作负载缓慢且单调地增长,则足以监视现有工作节点的利用率并在达到临界值时手动添加其他工作节点。
配置原则
- 外部化所有配置
- 配置应在应用程序代码之外进行维护。
- 这有几个好处。首先,更改配置不需要重新编译应用程序。其次,可以在应用程序运行时更新配置。第三,相同的代码可以在不同的环境中使用。
- 在Kubernetes中,可以将配置保存在ConfigMaps中,然后可以在将卷作为环境变量传入时将其安装到容器中。
- 在ConfigMap中仅保存非敏感配置。对于敏感信息(例如凭据),请使用Secret资源。
- 将Secrets作为卷而不是环境变量安装
- Secret资源的内容应作为卷装入容器中,而不应作为环境变量传递。
- 这是为了防止秘密值出现在用于启动容器的命令中,该命令可能由不应该访问秘密值的人员看到。
Part2 集群管理
命名空间限制
- 命名空间具有LimitRange
- 没有限制的容器可能导致与其他容器的资源争用以及计算资源的消耗。
- Kubernetes具有两个限制资源利用的功能:ResourceQuota和LimitRange。
- 使用LimitRange对象,可以定义资源请求的默认值以及名称空间内单个容器的限制。
- 在该命名空间内创建的,未明确指定请求和限制值的任何容器都将分配为默认值。
- 更多信息参考:resource-quotas
- 命名空间具有ResourceQuotas
- 使用ResourceQuotas,可以限制命名空间内所有容器的总资源消耗。
- 定义命名空间的资源配额会限制属于该名称空间的所有容器可以消耗的CPU,内存或存储资源的总量。
- 还可以为其他Kubernetes对象设置配额,例如当前名称空间中的Pod数量。
- 如果存在他人使用群集并创建20000 ConfigMap,则可以使用LimitRange来防止这种情况。
Pod安全策略
启用Pod安全策略
- 例如,可以使用Kubernetes Pod安全策略来限制:
- 访问主机进程或网络名称空间;
- 运行特权容器容器;
- 运行的用户;
- 访问主机文件系统;
- Linux功能,Seccomp或SELinux配置文件
- 选择正确的策略取决于集群的性质。
- 更多信息参考:kubernetes-pod-security-policy
- 例如,可以使用Kubernetes Pod安全策略来限制:
禁用特权容器
- 在Pod中,容器可以以“特权”模式运行,并且对主机系统上的资源的访问几乎不受限制。
- 尽管在某些特定的用例中,必须具有这种级别的访问权限,但总的来说,让容器执行此操作存在安全风险。
- 特权Pod的有效使用案例包括在节点上使用硬件,例如GPU。
- 更多信息参考:security-context
在容器中使用只读文件系统
- 在容器中运行只读文件系统会强制容器不可变。
- 这不仅减轻了一些旧的(且有风险的)做法(例如热修补),而且还帮助防止了恶意进程在容器内存储或操作数据的风险。
- 使用只读文件系统运行容器听起来可能很简单,但是可能会带来一些复杂性。
- 如果需要写日志或将文件存储在临时文件夹中怎么办?
- 更多信息参考:running-docker-containers-securely-in-production
防止容器以root身份运行
- 在容器中运行的进程与主机上的任何其他进程没有什么不同,只不过它有一小部分元数据声明它在容器中。
- 因此,容器中的根与主机上的根(uid 0)相同。
- 如果用户设法脱离了以root用户身份在容器中运行的应用程序,则他们可能能够使用同一root用户获得对主机的访问权限。
- 配置容器以使用非特权用户是防止特权升级攻击的最佳方法。
- 更多信息参考:processes-in-containers-should-not-run-as-root
限制capabilities
Linux capabilities使进程能够执行许多特权操作,其中只有root用户默认可以执行。
例如,CAP_CHOWN允许进程“对文件UID和GID进行任意更改”。
即使进程不是以root身份运行,进程也有可能通过提升特权来使用那些类似root的功能。
换句话说,如果不想受到损害,则应仅启用所需的功能。
但是应该启用什么功能?为什么?以下两篇文章探讨了有关Linux内核功能的理论和最佳实践:
防止特权升级
- 应该在关闭特权升级的情况下运行容器,以防止使用setuid或setgid二进制文件提升特权。
网络策略
- 启用网络策略
- Kubernetes网络策略指定Pod组的访问权限,就像云中的安全组用于控制对VM实例的访问一样。
- 换句话说,它在Kubernetes集群上运行的Pod之间创建了防火墙。
- 更多信息参考:Securing Kubernetes Cluster Networking
- 每个命名空间中都有一个保守的NetworkPolicy
- 存储库包含Kubernetes网络策略的各种用例和示例YAML文件。
- 如果想知道如何丢弃/限制在Kubernetes上运行的应用程序的流量,请阅读how to drop/restrict traffic to applications running on Kubernetes。
RBAC策略
- 禁用默认服务帐户的自动挂载RBAC策略
- 请注意,默认的ServiceAccount将自动安装到所有Pod的文件系统中,详见use-the-default-service-account-to-access-the-api-server
- 可能要禁用它并提供更详细的策略。
- 设置为所需的最少特权
- 寻找有关如何设置RBAC规则的好的建议是一项挑战。
- 在Kubernetes RBAC的3种现实方法中,可以找到三种实用场景和有关如何入门的实用建议。
- RBAC策略是精细的,不能共享
- Zalando有一个简洁的策略来定义角色和ServiceAccounts。
- 首先,他们描述他们的要求:
- 用户应该能够部署,但不应允许他们查看如“secret”这类资源
- 管理员应拥有对所有资源的完全访问权限
- 默认情况下,应用程序不应获得对Kubernetes API的写访问权限
- 对于某些用途,可以有Kubernetes API写权限。
- 四个要求转化为五个单独的角色:
- ReadOnly
- PowerUser
- Operator
- Controller
- Admin
- 更多信息参考:access-control-roles-and-service-accounts
自定义策略
- 只允许从已知registry部署容器
- 可能要考虑的最常见的自定义策略之一是限制可以在群集中部署的镜像。
- 参考文档说明了如何使用开放策略代理来限制未批准的镜像。
- 强制Ingress主机名唯一
- 用户创建Ingress清单时,可以使用其中的任何主机名。
- 但是,可能希望阻止用户多次使用相同的主机名并互相覆盖。
- Open Policy Agent的官方文档包含有关如何在validation Webhook中检查Ingress资源的教程。
- 仅在Ingress主机名中使用批准的域名
- 用户创建Ingress清单时,可以使用其中的任何主机名。
- 但是,可能希望阻止用户使用无效的主机名。
- Open Policy Agent的官方文档包含有关如何在validation Webhook中检查Ingress资源的教程。
Part3 集群配置
该部分还在进行中。如果对这部分内容有意见,欢迎提issue。
集群要求
集群通过CIS基准测试
- 互联网安全中心提供了一些准则和基准测试,以确保代码安全的最佳做法
- 他们还维护了Kubernetes的基准,可以从官方网站上下载该基准。
- 虽然可以阅读冗长的指南并手动检查集群是否符合要求,但更简单的方法是下载并执行kube-bench。
- kube-bench是一个工具,用于自动执行CIS Kubernetes基准测试并报告集群中的错误配置。
请注意,无法使用kube-bench检查托管集群(例如GKE,EKS和AKS)的主节点。主节点由云提供商控制。
禁用云提供商的元数据API
- 云平台(AWS,Azure,GCE等)通常将本地元数据服务公开给实例。
- 默认情况下,实例上运行的Pod可以访问这些API,并且可以包含该节点的云凭据或诸如kubelet凭据之类的置备数据。
- 这些凭据可用于在群集内升级或升级到同一帐户下的其他云服务。
限制对Alpha或Beta功能的访问
- Alpha和Beta Kubernetes功能正在积极开发中,可能会存在限制或错误,从而导致安全漏洞。
- 始终评估Alpha或Beta功能可能提供的价值,以防对安全状况造成潜在风险。
- 如有疑问,请禁用不使用的功能。
身份认证
使用OpenID(OIDC)令牌作为用户身份验证策略
- Kubernetes支持各种身份验证方法,包括OpenID Connect(OIDC)。
- OpenID Connect允许单点登录(SSO)(例如Google身份)连接到Kubernetes集群和其他开发工具。
- 无需单独记住或管理凭据。
- 可能有多个群集连接到同一OpenID提供程序。
- 更多信息参考:kubernetes-single-sign-one-less-identity
ServiceAccount令牌仅适用于应用程序和控制器
- ServiceAccount不应用于尝试与Kubernetes群集进行交互的最终用户,但对于在Kubernetes上运行的应用程序和工作负载,它们是首选的身份验证策略。
日志设置
- 有一个日志保留和归档策略
- 应该保留30-45天的历史日志。
- 从节点,控制平面,审计中收集日志
- 从哪些地方收集日志:
- 节点 (kubelet, container runtime)
- 控制平面 (API server, scheduler, controller manager)
- Kubernetes审计 (all requests to the API server)
- 应该收集什么:
- 应用名称。从元数据标签中检索。
- 应用程序实例。从元数据标签中检索。
- 应用程序版本。从元数据标签中检索。
- 集群ID。从Kubernetes集群检索。
- 容器名称。从Kubernetes API检索。
- 运行此容器的群集节点。从Kubernetes集群检索。
- 运行容器的Pod名称。从Kubernetes集群检索。
- 命名空间。从Kubernetes集群检索。
- 从哪些地方收集日志:
- 在每个节点上最好有一个守护程序来收集日志,而不是sidecar
- 应用程序日志应输出到标准输出,而不是文件。
- 每个节点上的守护程序可以从容器运行时收集日志(如果记录到文件,则可能需要每个pod的sidecar容器)。
- 提供日志聚合工具
- 使用日志聚合工具,例如EFK技术栈(Elasticsearch,Fluentd,Kibana),DataDog,Sumo Logic,Sysdig,GCP Stackdriver,Azure Monitor,AWS CloudWatch。