自托管必备 Docker 安全技巧
保护您的自托管服务,从防御到监控!
目录
🧗♀️ 勇者无畏
如果你在自建 Docker 服务,安全责任完全在你身上——没有云提供商帮你挡掉端口扫描或配置错误。无论是在家庭网络里部署应用,还是从 Vultr、DigitalOcean、Linode、AWS、Azure、Google Cloud 等厂商租用 VPS,你都必须加固系统——并验证加固是否到位。
本指南将带你梳理 Docker 安全——从一些“少为人知”的技巧,到另一些“难以搞对”的实践;我们会探讨金丝雀令牌、只读卷、防火墙规则、网络隔离与加固、添加认证代理等等。
我们还会对比家庭网络与公有云部署的差异,并展示如何用 Nginx 搭建基础认证代理。读完本指南,你将拥有多种手段来挡住那些不速之客(朋友、家人,有时候甚至是你自己……)。
内容确实不少!但大部分互有关联,你可以根据自身情况挑选最相关的部分。🍀
🔄 :latest之舞
保持镜像更新对安全至关重要。但依赖 :latest 可能在未经审查的情况下引入破坏性变更或有漏洞的构建。
安全的更新方式
将更新命令与 pull 或 build 结合使用,这样你会有意识地刷新镜像,并在一个能注意到问题的时间窗口内重启。
#!/bin/bashdocker compose pull && \ docker compose up -d版本固定 vs 使用 Latest
选择合适的版本来固定,是在稳定性与安全性之间的权衡。以下是一些常见策略:
# ... # 精确版本固定,最适合关键服务 image: postgres:17.2
# 补丁版本固定,适合非关键服务 image: postgres:17.2
# 主版本固定,适合个人项目 image: postgres:17
# 裸奔,尽量避免 image: postgres:latest使用 Dependabot 或 Renovate 来生成可审查的更新 PR。对于那些你不想凌晨 2 点爬起来重建的服务,固定到具体版本或摘要,让自动化通知你何时该升级。
如果你有维护 Docker 镜像更新的趁手工具,欢迎告诉我!
🔐 密钥管理
管理密钥的方法有很多,但最重要的原则之一是:切勿将密钥硬编码到Docker镜像中,或提交到Git仓库。 这是最常见的安全错误之一,会带来长期风险,事后修复也很麻烦。
安全存储密钥是一个大话题,选项众多,包括 .env 文件、Docker secrets、1Password/Bitwarden,或者像 HashiCorp Vault 或 AWS Secrets Manager 这样的密钥管理服务。
你需要根据自身用例选择“合适”的投入程度与安全级别。
生成强密钥
以下是一个为 .env 文件生成新密钥的小脚本:
#!/bin/bashgenerate_secret() { local length=${1:-30} local generate_length=$((length + 4)) openssl rand -base64 "$generate_length" | tr -d '+=/\n' | cut -c1-"$length"}
[ -f .env ] && { echo ".env 文件已存在!"; exit 1; }
cat > .env << EOLPOSTGRES_PASSWORD=$(generate_secret)JWT_SECRET=$(generate_secret 64)SESSION_KEY=$(generate_secret 24)REDIS_PASSWORD=$(generate_secret 20)UNSAFE_PLACEHOLDER=__WARNING_REPLACE_RANDOM_TEXT__EOL
echo "已生成新的 .env 文件,包含安全的随机值!"金丝雀令牌
金丝雀令牌 是一种很好的方式,可以检测你的密钥是否被入侵(并被使用)。它们就像绊网,你可以添加到任何敏感文件、URL和令牌中。
可以考虑将它们放在你真正担心的密钥旁边:.env 文件、CI 变量、密码管理器、备份文件夹和云凭据。别把它变成花架子;绊网要放在真实攻击者或未来的你可能会误触的位置。
金丝雀“令牌”有多种类型可选,从 AWS 令牌、伪造信用卡号码、Excel 和 Word 文件、Kubeconfig 文件、VPN 凭据,甚至 SQL 转储文件都可以设置绊网!
金丝雀令牌最佳实践
- 遍地布设:在你所能想到的每一个
.env文件、CI/CD 流水线和“密钥管理器”中都放上一个。- 在 home 目录里放一个
passwords.xlsx或passwords.docx文件。 - 添加一个名为
billing_prod的 AWS 配置文件,把金丝雀令牌当密钥用。 - 为你的
~/.ssh目录生成一个private.key文件。 - 在
~/backups目录里创建一个金丝雀 SQL 转储文件all_credit_cards.sql。
- 在 home 目录里放一个
- 监控:设置邮件规则或告警,以便在金丝雀令牌被触发时第一时间收到通知。
从 .env 升级到 MacOS 钥匙串
对于 Mac 用户来说,最简单的方案之一就是使用钥匙串(Keychain)。
下面是一种自动化从 OSX 钥匙串加载密钥的简单方式,支持 TouchID,并且比 .env 文件更安全。
原作者: Brian Hetfield 与 Jan Schaumann。
### Functions for setting and getting environment variables from the OSX keychain ###### Adapted from: https://www.netmeister.org/blog/keychain-passwords.html andOriginal credit: [Brian Hetfield](https://gist.github.com/bmhatfield/f613c10e360b4f27033761bbee4404fd) and [Jan Schaumann](https://www.netmeister.org/).
# Use: get-keychain-secret SECRET_ENV_VARfunction get-keychain-secret () { security find-generic-password -w -a ${USER} -D "environment variable" -s "${1}"}
# Use: set-keychain-secret SECRET_ENV_VAR# You will be prompted to enter the secret value!function set-keychain-secret () { [ -n "$1" ] || print "Missing environment variable name"
# prompt user for secret echo -n "Enter secret for ${1}" read secret [ -n "$secret" ] || return 1
( [ -n "$1" ] || [ -n "$secret" ] ) || return 1 security add-generic-password -U -a ${USER} -D "environment variable" -s "${1}" -w "${secret}"}source ~/keychain-secrets.sh
# Load Env vars into the current shellexport AWS_ACCESS_KEY_ID=$(get-keychain-secret AWS_ACCESS_KEY_ID);export AWS_SECRET_ACCESS_KEY=$(get-keychain-secret AWS_SECRET_ACCESS_KEY);# Note: If an attack can run `env` in your shell, then these secrets could be exposed!#!/usr/bin/env bashsource ~/keychain-secrets.sh
# Specify all secrets for this projectAWS_ACCESS_KEY_ID=$(get-keychain-secret AWS_ACCESS_KEY_ID) \AWS_SECRET_ACCESS_KEY=$(get-keychain-secret AWS_SECRET_ACCESS_KEY) \ "$@"
# Note: Using a shell wrapper helps prevent secrets from staying# around in the environment. And it's safe to commit.
# Usage:# ./scripts/env-run.sh docker compose up -d# ./scripts/env-run.sh docker run -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS ...🌐 网络风险
自定义网络与内部端口
使用 Docker 网络正确隔离服务,是减少攻击面的重要手段。
在网络中开洞时要格外小心!一个错误的端口转发就可能导致严重后果。
默认情况下,私有 LAN 上的服务并不会暴露到互联网——你需要显式地在路由器上做端口转发。
Docker 在局域网中的隐患
无论你是在本地运行开发服务器,还是从家庭网络自托管服务,对 Docker 网络模型的错误假设都可能带来麻烦。
开发者常常惊讶地发现,保护 Linux 服务器的“传统”方法(iptables、限制 TCP/IP sysctl 选项)在 Docker 主机上可能静默失效!这在自托管或在典型家庭网络上运行时尤为突出。(再说一遍:这意味着他人可能访问你 MacBook 上的开发容器!!!)
⚠️ 警告1: Docker 发布的端口可能绕过你认为正在保护主机的防火墙规则,尤其是 Ubuntu/Debian 上的 UFW。这并不意味着每条防火墙规则都无用,但它确实说明“UFW 说拒绝”并不等于真的拒绝了。参见 issue #690:Docker 绕过 UFW 防火墙规则。
⚠️ 警告2: 将端口绑定到本地 IP 地址(例如
-p 127.0.0.1:8080:80)是正确默认做法,但在 Docker Engine 28.0.0 之前的版本中,存在同一 L2 网络上的主机仍能访问 localhost 发布端口的情况。Docker 在其端口发布指南中记录了这一注意事项,因此下文提到的“用 nmap 验证”的习惯仍然很重要。
如果你对此感到惊讶,我当初也一样!
绑定到本地 IP 仍然是好习惯,并且在托管云环境和特殊配置的网络中有实际意义。
Docker Compose 示例
以下是一个 docker-compose.yml 示例文件,将 app 服务绑定到 127.0.0.1:8080,并将两个容器连接到 backend 自定义网络。
networks: backend:
services: app: networks: - backend ports: # 尽量绑定到本地回环地址 - "127.0.0.1:8080:8080" # ... 其他设置 database: image: postgres:17.1 # 无需暴露端口;通过 backend 网络内部访问。 networks: - backend网络最佳实践
- 🏆 不暴露任何端口 最近我才发现,这一做法比想象中更有用!当使用命名(桥接)网络时,容器之间可以无过滤地互相访问。它们的行为就像处于本地网络(NAT 网关)之后。
- 虽然并非所有用例都适用,但对于运行批处理任务或主要通过
attach/exec访问的容器,这可能很有用。
- 虽然并非所有用例都适用,但对于运行批处理任务或主要通过
- 🥇 使用 Docker 网络 来隔离和控制哪些容器可以相互通信。
- 🥉 使用本地回环地址绑定:虽然不完美,但将端口绑定到回环地址(例如
127.0.0.1:8080:80)通常是更好的做法。只要确保验证你的设置即可。
🛡️ 访问控制
访问控制是保护 Docker 服务安全的关键部分。这包括限制容器的能力与权限、限制对 Docker 套接字的访问等等。
限制容器能力
另一个扎实的访问控制实践是限制容器的能力。这可以缩小多种威胁的爆炸半径,从权限提升到流量劫持。它并不是一个力场,但移除了大多数容器从来不需要的权限。
什么是能力? Linux 内核定义的、命名的权限或能力。(capabilities 手册页有完整列表。)包括 CAP_CHOWN(更改文件所有权)、CAP_NET_ADMIN(配置网络接口)、CAP_KILL(终止任何进程)等等。
确定所需能力的两种方法:
- 试错法:这种较慢但有效的方法是先放弃所有能力,然后逐个添加回来,直到应用正常运行。
- 参考前人工作:搜索“
项目名cap_dropDockerfile”或“项目名cap_dropdocker-compose.yml”,看是否已经有人为你完成了这项工作。LLM 可以给出一个起点,但在测试容器并阅读镜像文档之前,仅将其视为猜测。
能力最佳实践
- 放弃所有能力:使用
cap_drop: [ ALL ]放弃容器的所有 Linux 能力。 - 不新增特权:使用
security_opt: [ no-new-privileges=true ]防止容器获得新特权。
services: database: image: postgres:17.1 networks: [ db-network ] security_opt: - no-new-privileges:true cap_drop: - ALL cap_add: - CHOWN - DAC_READ_SEARCH - FOWNER - SETGID - SETUID db-admin: image: dpage/pgadmin4:4.1 networks: [ db-network ] ports: - "8081:80" # ... 其他设置networks: db-network:现在你的服务可以通过 db-network 网络相互通信。Docker Compose 会自动创建该网络。
使用 --external/external: 选项加入一个预先存在的网络。省略此选项则会创建一个新网络。
Docker 套接字访问
⚠️ 警告:docker.sock 基本上就是主机管理员权限
⚠️ :ro 选项不影响通过套接字发送的 I/O!
它仅仅确保套接字路径本身以只读方式挂载。通过该套接字发送的 API 调用仍然可以创建容器、挂载主机路径,以及执行其他你可能并不想委托的非常危险的操作。
套接字最佳实践
- 🥇 避免挂载 Docker 套接字,通常会有更好的替代方案。
- 🫣 如果必须使用,请在它前面放置一个狭窄的代理,只允许应用实际需要的 API 端点。可以看看源自 Tecnativa 的
docker-socket-proxy项目:docker-socket-proxy。然后验证被拒绝的调用确实被拒绝了。 - 🤢 好吧,在高度信任、低风险的测试环境中,共享它可能还行。
封锁国家!
有时有用,但并非真正的安全边界。
这里谈论的是地缘政治实体,而不是音乐……
如果你主要托管给家人和朋友使用的应用,可以阻止来自你不希望收到流量的国家的流量。或者只允许来自你期望国家的流量。它可以减少噪音,但无法阻止 VPN、代理、僵尸网络或任何有耐心的人。
查看这个阻止所有来自中国的流量的脚本:
curl -fsSL https://www.ipdeny.com/ipblocks/data/countries/cn.zone | \ while read line; do ufw deny from $line to any; done同理,也可以只允许来自美国的流量:
curl -fsSL https://www.ipdeny.com/ipblocks/data/countries/us.zone | \ while read line; do ufw allow from $line to any; done加固 CloudFlare 代理主机
如果你的家庭服务器隐藏在 CloudFlare IP(代理)后面,可以将访问限制为仅来自 CloudFlare IP 和本地网络的流量。
这与上面的封锁国家有些类似,但控制更严格。
ufw default deny incoming # 阻止所有入站流量!!!ufw default allow outgoing # 允许所有出站流量ufw allow ssh # 允许 SSH
# 允许本地子网访问(最好为托管服务设置专用的 DMZ/VLAN)ufw allow from 10.0.0.0/8 to any port 443
# 允许 CloudFlare IPcurl -fsSL https://www.cloudflare.com/ips-v4 | \ while read line; do ufw allow from $line to any port 443; done# 添加 IPv6 支持# curl -fsSL https://www.cloudflare.com/ips-v6 | \# while read line; do ufw allow from $line to any port 443; done要测试基于地理位置的变更,可以使用目标国家/地区的 VPN。详见监控与验证部分。
应用层安全
网络和主机加固完成后,你会发现还有更多事情要做。
现在我们需要思考服务本身的“应用”层。
那个数据库有有效密码吗?这个容器会自动处理 HTTPS/证书吗?应用包含内置认证吗?对哪些邮箱可以注册有限制吗?是否存在默认凭据或需要通过环境变量修改的内容?
唯一的方法是去检查。这时,从 README 和其他关键文件(如 docker-compose.yml、Dockerfile、.env.*)开始。既要检查项目本身,也要检查其配套服务(如 Postgres、Redis 等)。
反向代理
另一层防御是基础认证。不要在没有 HTTPS 的情况下使用它。对于老旧服务,在管理路由前面加上基础认证,通常足以阻止随意请求和未认证的爬虫直接访问。
location /admin { auth_basic "受限制访问"; auth_basic_user_file /etc/nginx/.htpasswd; proxy_pass http://internal_admin:80; proxy_set_header X-Real-IP $remote_addr;}生成凭据:
htpasswd -c /etc/nginx/.htpasswd admin有了基础认证代理,攻击者在访问你的内部服务之前就多了一道障碍——用户名和密码。
另一种选择是使用像 Traefik 或 Caddy 这样的服务,它们可以自动处理 HTTPS 和基础认证。
如果你希望通过图形界面管理多个域名和服务,我推荐 Nginx Proxy Manager。
🔍 监控与验证
这是最重要也是最容易被忽视的步骤。你可能有最好的防火墙、最好的网络和最佳实践,但如果不进行验证,你完全不知道它是否真的在工作。
此外,哪怕只掌握少数几条命令(或者知道去哪里查),就可能成为阻止入侵的关键。感觉自己像个黑客只是额外的奖励。(详细内容与示例请跳到监控与验证部分。)
不要信任,验证两次
检查端口
⚠️ 重要提示:不要扫描不属于你的主机。
无论你是在家庭网络还是VPS上,你都需要知道哪些端口对外部世界是开放的。
有两种方法可以做到这一点:
- 检查网络(
nmap、masscan) - 询问操作系统(
lsof、netstat、ss)
测试网络外部
你需要知道当前的(公网)IP,可以使用像 ifconfig.me 这样的服务轻松获取:curl https://ifconfig.me。或者在你的托管服务商的控制面板中查找。
curl -fsSL https://ifconfig.me# --> 当前公网IP获取公网IP后,你需要连接到一个外部网络。你可以使用朋友的电脑、手机/5G热点,或者一个专用的服务器主机。
target_host="$(curl -fsSL https://ifconfig.me)"
# 注意:确保 `target_host` 是你想要扫描的目标IP
# 扫描特定端口:nmap -A -p 80,443,8080 --open --reason $target_host# 前100个端口:nmap -A --top-ports 100 --open --reason $target_host# 所有端口nmap -A -p1-65535 --open --reason $target_host测试网络内部
练习使用 nmap,扫描你的本地网络或其中一台服务器,检查你的路由器、打印机、智能冰箱。
扫描命令示例
# 扫描本地主机的所有开放端口nmap -sT localhost
# 扫描本机私有IP上的服务nmap -sV 192.168.1.10
# 查找你网络上的服务详情nmap -sn 192.168.0.0/24nmap -sn 10.0.0.0/24# 或者 docker 的 172.18.0.1/16nmap -sn 172.18.0.1/16% nmap -A --open --reason 192.168.0.87
Starting Nmap 7.95 ( https://nmap.org ) at 2025-01-06 13:51 MSTNmap scan report for dev02.local (192.168.0.87)Host is up, received syn-ack (0.0067s latency).Not shown: 995 closed tcp ports (conn-refused)PORT STATE SERVICE REASON VERSION22/tcp open ssh syn-ack OpenSSH 9.6p1 Ubuntu 3ubuntu13.5 (Ubuntu Linux; protocol 2.0)| ssh-hostkey:|_ 256 {FINGERPRINT} (ED25519)80/tcp open http syn-ack Caddy httpd|_http-server-header: Caddy|_http-title: Dev02.DanLevy.net443/tcp open ssl/https syn-ack|_http-title: Dev02.DanLevy.net1234/tcp open http syn-ack Node.js Express framework|_http-cors: GET POST PUT DELETE PATCH|_http-title: Dev02.DanLevy.net (application/json; charset=utf-8).Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .Nmap done: 1 IP address (1 host up) scanned in 13.36 seconds查看开放端口
熟悉 lsof——它在 macOS 和 Linux 上都可用。它可以显示精细的网络状态和磁盘活动。
# 监控特定端口sudo lsof -i:80 -Pn
# Monitor ESTABLISHED connectionssudo lsof -i -Pn | grep ESTABLISHED# View LISTENsudo lsof -i -Pn | grep LISTEN
# to see network names instead of IP addresses (can be very slow to do reverse DNS lookups)sudo lsof -i -P | grep LISTEN
# Monitor all network connectionssudo watch -n1 "lsof -i -Pn"示例输出

文件监控
若要识别哪些 进程 占用最多 硬盘带宽,可以使用 iotop:
sudo iotop如需查看单个文件变更,Linux 上可用 inotifywait,macOS 上可用 fswatch:
这有助于按文件夹或全系统检测未经授权的或异常行为。
# 监控目录中的所有文件变更sudo inotifywait -m /path/to/directory在 macOS 上,可以使用 fswatch:
使用 brew install fswatch 安装
fswatch -r /path/to/directory⏰ 常被忽略的技巧
-
对认证尝试及其他关键端点实施速率限制。无论是通过 Nginx 的
limit_req模块还是fail2ban针对 SSH 访问,限制暴力破解都是个好主意。我说“好主意”,是因为在 IPv6 和廉价僵尸网络时代,情况已经和过去不一样了。 -
尽可能使用只读卷:
services: webapp: volumes:
- ./config:/config:ro
结合其他最佳实践(非 root 用户、最小化文件夹权限),`:ro` 卷挂载选项能为意外变更和容器内的部分写入尝试提供额外防护。它并不能保护主机免受已拥有更高权限的进程影响。
3. **定期审计容器访问**。如果一个容器不需要某个密钥、端口或挂载,就移除它!
4. **当心 WiFi 上的闲杂人等**。 我相信你绝不会把 WiFi 密码告诉别人,尤其是那些怪人,对吧?好吧,也许有些朋友除外……嗯,可能家人也要用。你永远不知道他们装了哪些应用,这些应用可能把你的 SSID 和密码泄露出去。
### 家庭网络 vs. 公共提供商 vs. 隧道
1. **虚拟隔离/DMZ**:对于家庭服务器,尽可能将其放在单独的 VLAN 或 DMZ 中。这样可以使你的内部设备免受来自服务器一端的潜在威胁。 - 使用单独的路由器或 VLAN 放置家庭服务器。 - 使用独立的 WiFi 网络放置家庭服务器。 - 使用独立的子网放置家庭服务器。
2. **云提供商**:Hetzner、Vultr、DigitalOcean、Linode、AWS、Azure、Google Cloud 都提供了不同的防火墙功能。 - 一些提供商和服务会默认封锁端口。有些提供可选服务或附加组件。请查阅服务提供商的文档。 - 许多提供商提供高级监控和威胁检测服务。
3. **VPN 和隧道**:考虑使用 VPN 类选项或隧道服务,在不将服务暴露于公共互联网的情况下,跨互联网安全地连接服务。 - TailScale、ngrok、ZeroTier。 - WireGuard、OpenVPN。
{/* 3. **Hardening Against Internal/Lateral Attacks**: One infected device can compromise an entire network. Segmenting Docker services on custom networks, using hardware, UFW rules, and blocking unneeded ports can all help reduce risk (when properly configured.) */}
## 🚀 生产环境检查清单
- [ ] **密钥**:所有密钥均已随机生成并安全存储- [ ] **更新**:容器更新策略已记录并自动化(记录在文本文件中的几条命令也可以)- [ ] **网络**:仅暴露必要端口,已配置内部网络- [ ] **防火墙规则**:默认拒绝,明确允许,必要时按国家封锁- [ ] **反向代理**:Nginx、Caddy 或 Traefik 能增加一层基本认证- [ ] **蜜罐令牌**:将其放置在真正敏感的文件和凭据附近,一旦被触碰你会实际调查- [ ] **监控**:使用 `nmap`、`lsof`、`inotifywait`、`glances` 等工具了解你的系统- [ ] **备份策略**:经过测试,最好自动化且异地存储- [ ] **最小权限**:非 root 容器用户,只读卷
## 📚 延伸阅读
- [Docker 安全最佳实践](https://docs.docker.com/develop/security-best-practices/)- [OWASP Docker 安全速查表](https://cheatsheetseries.owasp.org/cheatsheets/Docker_Security_Cheat_Sheet.html)- [CIS Docker 基准](https://www.cisecurity.org/benchmark/docker)- [Canarytokens.org 蜜罐令牌](https://canarytokens.org/)
## 致谢
感谢几位敏锐的 Reddit 用户:
- <em className="cite">[u/JCBird1012](https://www.reddit.com/user/JCBird1012/) - [帖子](https://www.reddit.com/r/selfhosted/comments/1hv8jn6/comment/m5rvlzi/).</em>- <em className="cite">[u/Salzig](https://www.reddit.com/user/Salzig/)</em>- <em className="cite">[u/Myelrond](https://www.reddit.com/user/myelrond/)</em>- <em className="cite">[u/shrimpdiddle](https://www.reddit.com/user/shrimpdiddle/)</em>- <em className="cite">[u/troeberry](https://www.reddit.com/user/troeberry/)</em>
感谢阅读!希望这份指南对你有帮助。如果你有任何问题或建议,欢迎通过下方的社交媒体联系我,或者点击 `Edit on GitHub` 链接提交 PR!❤️