DD-WRT路由下Docker+Traefik+Let's Encrypt暴露服务遇TLS握手错误
背景
我打算把Tiny Tiny RSS、ownCloud这类自托管服务暴露到公网,所以选了Traefik做反向代理,搭配Let's Encrypt来获取HTTPS证书。在部署正式服务前,先在Odroid XU-4的Debian系统上搭了个测试栈,包含Traefik、Let's Encrypt和whoami容器。路由器刷了DD-WRT,已经把80、8080、443端口转发到Debian主机,DuckDNS的动态域名更新也由路由器搞定了。
用docker-compose build --no-cache && docker-compose up --build启动容器后,从外网访问http://my_duck_dns.duckdns.org/whoami/时,80端口能正常重定向到443,但一直报TLS握手错误。
Docker Compose配置
version: '3.6' services: traefik: container_name: traefik image: traefik:1.6.1-alpine ports: - 80:80 - 443:443 - 8080:8080 networks: - proxy environment: - DUCKDNS_TOKEN=my_duck_dns_token volumes: - /var/run/docker.sock:/var/run/docker.sock - ./traefik/traefik.toml:/traefik.toml - ./traefik/acme/acme.json:/etc/traefik/acme.json - ./log:/var/log/traefik labels: - traefik.enable=true - traefik.port=8080 - traefik.frontend.rule=Host:my_duck_dns.duckdns.org restart: always whoami: container_name: whoami image: hypriot/rpi-whoami ports: - 8000 networks: - proxy labels: - traefik.frontend.rule=Host:my_duck_dns.duckdns.org;PathPrefixStrip:/whoami/ - traefik.frontend.entryPoints=https - traefik.docker.network=proxy - traefik.protocol=http - traefik.enable=true - traefik.port=8000 restart: always networks: proxy: name: proxy
Traefik.toml配置
debug = true logLevel = "DEBUG" checkNewVersion = true defaultEntryPoints = ["http", "https"] [proxy] address = ":8080" [entryPoints] [entryPoints.http] address = ":80" [entryPoints.http.redirect] entryPoint = "https" [entryPoints.https] address = ":443" [entryPoints.https.tls] #[traefikLog] # filePath = "/var/log/traefik/traefik.log" # format = "json" # logLevel = "DEBUG" #[accessLog] # filePath = "/var/log/traefik/access.log" # format = "json" # logLevel = "DEBUG" [docker] endpoint = "unix:///var/run/docker.sock" domain = "my_duck_dns.duckdns.org" exposedbydefault = false watch = true [acme] caServer = "https://acme-staging-v02.api.letsencrypt.org/directory" email = "my_email_address@gmail.com" storage = "/etc/traefik/acme.json" entryPoint = "https" acmeLogging = false [acme.httpChallenge] entryPoint = "http" [acme.dnsChallenge] provider = "duckdns" delayBeforeCheck = 0 [[acme.domains]] main = "my_duck_dns.duckdns.org" sans = ["my_duck_dns.duckdns.org"]
错误日志
traefik | time="2018-05-18T16:15:05Z" level=debug msg="http: TLS handshake error from 151.58.32.33:65175: read tcp 172.27.0.3:443->154.47.32.66:64175: read: connection reset by peer"
完整DEBUG日志片段:
whoami | Listening on :8000 traefik | time="2018-05-18T18:30:55Z" level=info msg="Using TOML configuration file /traefik.toml" traefik | time="2018-05-18T18:30:55Z" level=info msg="Traefik version v1.6.1 built on 2018-05-14_07:16:56PM" traefik | time="2018-05-18T18:30:55Z" level=info msg=" Stats collection is disabled. Help us improve Traefik by turning this feature on :) More details on: https://docs.traefik.io/basics/#collected-data " traefik | time="2018-05-18T18:30:55Z" level=debug msg="Global configuration loaded {\"LifeCycle\":{\"RequestAcceptGraceTimeout\":0,\"GraceTimeOut\":10000000000},\"GraceTimeOut\":0,\"Debug\":true,\"CheckNewVersion\":true,\"SendAnonymousUsage\":false,\"AccessLogsFile\":\"\",\"AccessLog\":null,\"TraefikLogsFile\":\"\",\"TraefikLog\":null,\"Tracing\":null,\"LogLevel\":\"DEBUG\",\"EntryPoints\":{\"http\":{\"Address\":\":80\",\"TLS\":null,\"Redirect\":{\"entryPoint\":\"https\"},\"Auth\":null,\"WhitelistSourceRange\":null,\"WhiteList\":null,\"Compress\":false,\"ProxyProtocol\":null,\"ForwardedHeaders\":{\"Insecure\":true,\"TrustedIPs\":null}},\"https\":{\"Address\":\":443\",\"TLS\":{\"MinVersion\":\"\",\"CipherSuites\":null,\"Certificates\":null,\"ClientCAFiles\":null,\"ClientCA\":{\"Files\":null,\"Optional\":false}},\"Redirect\":null,\"Auth\":null,\"WhitelistSourceRange\":null,\"WhiteList\":null,\"Compress\":false,\"ProxyProtocol\":null,\"ForwardedHeaders\":{\"Insecure\":true,\"TrustedIPs\":null}}},\"Cluster\":null,\"Constraints\":[],\"ACME\":null,\"DefaultEntryPoints\":[\"http\",\"https\"],\"ProvidersThrottleDuration\":2000000000,\"MaxIdleConnsPerHost\":200,\"IdleTimeout\":0,\"InsecureSkipVerify\":false,\"RootCAs\":null,\"Retry\":null,\"HealthCheck\":{\"Interval\":30000000000},\"RespondingTimeouts\":null,\"ForwardingTimeouts\":null,\"AllowMinWeightZero\":false,\"Web\":null,\"Docker\":
排查与解决方案
这个TLS握手错误我碰到过好几次,大概率和证书生成失败、网络连通性或者配置冲突有关,咱们一步步来定位:
1. 先确认ACME证书是否成功生成
Traefik没法完成TLS握手,最常见的原因就是没拿到有效的证书。
- 先检查
acme.json的权限:Traefik需要读写这个文件,而且必须是600权限(因为Alpine镜像里Traefik以非root用户运行,权限太宽会报错),执行chmod 600 ./traefik/acme/acme.json。 - 打开这个文件看看,如果是空的或者没有包含
Certificates字段的有效JSON,说明证书获取失败了。
另外,你的ACME配置同时启用了httpChallenge和dnsChallenge,这两种方式会冲突,Traefik可能不知道用哪种来验证域名。建议只保留一种:
- 既然你已经转发了80端口,优先用
httpChallenge,把[acme.dnsChallenge]整个区块注释掉; - 如果http挑战有问题(比如ISP封了80端口),就保留
dnsChallenge,注释掉[acme.httpChallenge]。
还有你用的是Let's Encrypt的staging服务器,虽然 staging证书浏览器不认可,但至少能完成TLS握手,如果连这个都失败,肯定是证书没生成的问题。
2. 验证端口转发与网络连通性
- 确认路由器的端口转发是否真的生效:可以在Debian主机上用
nc -l 443监听端口,然后从外网用telnet my_duck_dns.duckdns.org 443测试,如果能连通说明转发没问题,否则检查路由器配置或者主机防火墙(比如ufw是否允许443端口入站)。 - 检查Traefik容器是否在监听443端口:执行
docker exec traefik netstat -tulpn,看看:443是不是处于LISTEN状态。
3. 优化Traefik TLS配置
你的[entryPoints.https.tls]是空的,虽然Traefik会自动加载ACME证书,但可以添加TLS最小版本配置,避免和老客户端的兼容性问题:
[entryPoints.https.tls] MinVersion = "VersionTLS12"
4. 清理Docker网络冗余配置
- whoami容器的
ports: - 8000完全没必要,Traefik是通过Docker内部网络直接访问容器的,映射到主机端口反而可能带来安全隐患,直接删掉这一行。 - 确认两个容器都在
proxy网络里:执行docker network inspect proxy,查看容器列表,确保traefik和whoami都在其中。
5. 打开ACME日志排查证书问题
把acmeLogging = true加到[acme]区块里,重启Traefik后看日志,里面会有证书获取的详细错误信息,比如域名验证失败、网络超时之类的,这能帮你快速定位问题。
6. 局域网内测试排除外网因素
先在局域网里直接访问https://<Debian主机局域网IP>/whoami/,如果能正常握手,说明问题出在路由器转发或者ISP限制(比如有些ISP会封锁443端口);如果局域网内也失败,那就是本地配置的问题,回到前面的步骤继续排查。
内容的提问来源于stack exchange,提问作者William Nameless

