把Docker直接部署在跳板机或者堡垒机上,很多人第一反应是方便:运维工具打包成镜像,新机器拉下来就能用,不用再折腾依赖库。但堡垒机作为所有运维操作的唯一入口,一旦Docker的隔离性被攻破,攻击者就能从容器逃逸到宿主机,拿到整个内网的跳板权限。因此,在堡垒机上使用Docker必须采取与普通开发机完全不同的安全策略。

本文会结合真实的部署经验,从安全边界、镜像管理、网络访问和审计日志四个维度,说明如何在堡垒机上既能享受Docker带来的工具版本管理便利,又不给整体安全体系留下缺口。
为什么堡垒机上的Docker需要单独加固
普通服务器上跑Docker,大家关注的是容器之间的网络隔离和资源限制。而堡垒机最核心的资产是宿主机本身,因为它保存了所有运维人员的SSH密钥、操作记录和授权策略。Docker daemon以root权限运行,容器内的root用户默认与宿主机的root在用户命名空间未开启时是同一个UID 0。这意味着只要容器内存在任意一个内核漏洞或者配置不当的挂载,攻击者就能直接读写宿主机文件系统。
更隐蔽的风险来自Docker socket的暴露。很多团队为了在容器内执行docker命令,会把/var/run/docker.sock挂载进容器。在堡垒机上这是绝对禁止的,因为挂载了socket的容器等同于拥有了对宿主机的完全控制权,哪怕容器本身只是一个简单的web终端。攻击者只需要在容器内执行docker run -v /:/host alpine chroot /host,就能拿到宿主机root shell。
所以,堡垒机上的Docker首要原则是:不允许任何容器访问宿主机的Docker daemon,同时必须开启用户命名空间隔离,确保容器内的root映射到宿主机上的非特权用户。另外,容器运行时要使用--read-only和--tmpfs限制可写层,防止攻击者通过写入计划任务或修改系统配置实现持久化。
构建隔离的运维工具镜像
堡垒机最常见的用途是给运维人员提供统一的操作入口,而不同岗位需要的工具不同:DBA需要MySQL客户端,K8s管理员需要kubectl,应用运维需要Ansible和Terraform。如果把这些工具都装在宿主机上,版本冲突和权限管理会非常混乱。用Docker把每个工具集打包成独立镜像,再通过受限的容器运行,可以大幅降低维护成本。
下面是一个最小化Ansible运行环境的Dockerfile示例。它基于Alpine构建,只安装ansible-core和必要的Python库,不包含任何shell之外的交互式工具,减少攻击面。
FROM alpine:3.19
RUN apk add --no-cache python3 py3-pip openssh-client sshpass \
&& pip3 install --no-cache-dir ansible-core==2.16.4
RUN adduser -D -u 10001 ansible
USER ansible
WORKDIR /home/ansible
ENTRYPOINT ["ansible-playbook"]
构建完成后,在堡垒机上用下面的命令启动容器。关键参数包括--user 10001:10001强制使用非root用户、--read-only让根文件系统只读、--tmpfs /tmp提供临时可写空间、--network none禁止容器主动发起网络连接,只能使用宿主机映射的SSH隧道。
docker run --rm -it \ --user 10001:10001 \ --read-only \ --tmpfs /tmp:rw,size=64m \ --network none \ --cap-drop ALL \ --security-opt no-new-privileges \ -v /data/ansible/playbooks:/home/ansible/playbooks:ro \ ansible-runner:2.16.4 \ playbooks/site.yml
注意--network none会阻止Ansible通过SSH连接目标服务器。实际使用中可以通过在宿主机建立SSH隧道,再用-p 127.0.0.1:2222:22把目标端口映射进来,或者改用--network host但配合防火墙规则限制出站。对于堡垒机场景,推荐使用--network none加显式端口映射,这样容器只能访问管理员明确允许的目标。
不同角色的运维人员登录堡垒机后,通过一个受控的wrapper脚本选择自己要用的工具镜像。脚本会检查当前用户的组信息,比如dba组只能启动mysql-client镜像,k8s组只能启动kubectl镜像。这样就把宿主机上的工具管理彻底解耦,而且每个工具的环境变量和配置文件都固化在镜像里,不会互相污染。
网络隔离与访问控制
堡垒机本身通常位于内网的核心区域,能够直接访问数据库、K8s API和各类管理后台。如果容器默认使用bridge网络,它就能通过NAT访问整个内网,这等于把堡垒机的网络权限完整复制给了容器。因此,必须让容器使用--network none或者自定义的macvlan网络,并对出站流量做精细化控制。
最稳妥的做法是结合宿主机iptables。在宿主机上创建一条链,只允许来自容器网段的流量访问指定的IP和端口。例如,只允许容器访问数据库服务器10.0.1.5的3306端口,其余全部DROP。
iptables -N DOCKER_BASTION iptables -A FORWARD -s 172.17.0.0/16 -j DOCKER_BASTION iptables -A DOCKER_BASTION -d 10.0.1.5 -p tcp --dport 3306 -j ACCEPT iptables -A DOCKER_BASTION -j DROP
如果堡垒机上运行了多种工具容器,建议为每个工具创建独立的Docker网络,并分配不同的子网。例如mysql-client容器使用10.100.1.0/24,kubectl容器使用10.100.2.0/24,然后分别编写iptables规则。这样即使某个容器被攻破,攻击者也只能访问该工具所允许的目标网络,无法横向探测其他服务。
对于需要访问外部API的工具(比如调用云厂商的OpenAPI),可以使用宿主机的HTTP代理,并在容器启动时设置--env HTTP_PROXY=http://172.17.0.1:3128。代理服务器本身配置白名单,只放行工具所必需的域名。这比直接在容器内使用--network host安全得多,因为host模式会让容器共享宿主机的网络栈,任何监听在localhost的服务都可能被容器访问到。
日志审计与容器生命周期管理
堡垒机的核心价值之一是可审计性,所有操作都必须有完整记录。Docker容器运行时的标准输出和标准错误默认会写入宿主机日志文件,但默认的json-file驱动不会持久化到远程,而且容器删除后日志会丢失。对于堡垒机场景,应该使用--log-driver=journald或者syslog,把容器日志统一发送到中心日志服务器。
docker run --rm -it \
--log-driver=journald \
--log-opt tag="bastion-ansible-{{.Name}}" \
--read-only \
--network none \
ansible-runner:2.16.4 \
playbooks/site.yml
除了容器日志,还需要记录谁在什么时候启动了哪个镜像、传递了什么参数。可以在堡垒机上封装一个docker-exec-wrapper脚本,在执行docker run之前把当前登录用户、时间、镜像名、参数完整写入审计日志,再通过rsyslog或Filebeat发送到SIEM。脚本中可以使用logger命令写入系统日志,同时把命令结束后的退出码也记录下来,方便事后追溯操作结果。
容器生命周期管理也不能马虎。堡垒机上不应该允许用户直接执行docker run,而应该把常用工具封装成只读的别名或脚本。脚本内部固定好安全参数(--read-only、--cap-drop ALL、--network none等),用户无法覆盖这些选项。对于临时需要自定义参数的情况,走审批流程后由管理员生成一次性token,用户通过受控的API提交任务。这样既保证了灵活性,又避免了用户误操作或恶意提权。
最后建议定期清理堡垒机上的悬空镜像和已停止容器,避免磁盘被占满影响正常运维。可以在宿主机配置cron任务,每天凌晨执行docker system prune -f --filter "until=24h",但必须确保不会删除正在使用的镜像。清理操作本身也要写日志,作为运维操作的一部分归档。