Traefik 自动 HTTPS 与 Authelia 统一登录门户:入口层认证实践
系列第二篇。承接上一篇的全容器化改造,这篇聚焦入口层:为什么选 Traefik、Let's Encrypt 证书怎么自动签发,以及如何从原始的 BasicAuth 弹窗升级成一个像样的统一登录门户 Authelia,中间常见的几个坑一并记下。文中域名、路径均为示例。
为什么是 Traefik
容器化之后,反向代理的选型可以优先考虑 Traefik,理由三条:
- 自动 HTTPS:内置 ACME 客户端,对接 Let's Encrypt,证书的申请、部署、续期全自动,不用像 Nginx 那样手动维护 certbot 的 cron 和证书路径。
- 原生理解 Docker:Docker Provider 能直接读容器标签生成路由;File Provider 又能用 YAML 静态描述路由,两种方式灵活组合。
- 动态配置热加载:改
services.yml不用重启代理,路由、中间件即时生效。
示例环境用的是 Traefik v3.7.12(从 v3.4.5 升上来的,正是这次升级解决了 Docker Provider 的 API 版本兼容问题——后面细说)。
Let's Encrypt 证书自动签发
Traefik 的 ACME 支持 HTTP-01 和 DNS-01 两种挑战。若域名为泛解析(*.example.com 指向服务器),则:
- 单域名证书走 HTTP-01 即可,Traefik 在 80 端口应答挑战,签发后自动跳转到 443。
- 如果要签泛域名证书(
*.example.com一张证书覆盖所有子域),必须走 DNS-01——因为 Let's Encrypt 规定通配符证书只能通过 DNS 挑战验证。这需要 DNS 服务商提供 API,Traefik 通过对应的 DNS provider 插件自动写入_acme-challengeTXT 记录完成验证。
证书统一存到 ACME 的 storage 文件(或存进 Traefik 挂载的卷),续期由 Traefik 在到期前自动完成,完全不用人管。关键前提:域名的 A/AAAA 记录(含泛解析)必须先正确指向服务器公网 IP,否则挑战失败、证书签不下来。
从 BasicAuth 弹窗到 Authelia 登录门户
最初给受保护的管理面板(Traefik Dashboard、日志查看器)加认证,用的是最省事的 Traefik basicAuth 中间件——就是浏览器弹一个原生用户名/密码框。能用,但体验差、也不安全:
- 原生弹窗丑且无法定制,不支持二次验证。
- 每个服务各配一份
htpasswd,凭据分散、难统一管理。 - 没有登出概念,会话管理原始。
更稳的做法是上 Authelia v4.39.22 作为统一登录门户,部署在 auth.example.com,用 Traefik 的 ForwardAuth 中间件把所有受保护服务的认证请求转发给它。架构变成:
浏览器 ──▶ Traefik ──(ForwardAuth)──▶ Authelia (auth.example.com)
│ │ 认证通过
└──────────────────────────┘
▼
后端服务(Dozzle / Dashboard ...)
并注入 Remote-User / Remote-Groups / Remote-Email
未登录访问受保护地址时,Traefik 返回 302 跳到 auth.example.com/?rd=<urlencoded 原地址>&rm=GET;用户在门户登录成功后,Authelia 回跳原地址,Traefik 放行并把用户身份注入后端。
密码哈希用 argon2id。这里有个坑值得单独讲。
坑一:users.yml 密码哈希与明文不匹配
Authelia 的用户配置写在 users.yml,密码字段存的是 argon2id 哈希。如果手写/复制的哈希和实际明文密码对不上,登录就会一直失败。正确做法是用官方 CLI 重新生成哈希:
# 注意子命令是 crypto hash generate argon2,变体用 --variant argon2id
authelia crypto hash generate argon2 --variant argon2id
# 校验某个明文是否匹配已有哈希(没有 hash check 这个子命令)
authelia crypto hash validate --password 'YOUR_PASSWORD' -- '$argon2id$...'
把生成的哈希填回 users.yml 即可。
这里还嵌套了上一篇提过的 sed 陷阱:users.yml 里的哈希以 $argon2id$ 开头,满是 $ 特殊字符。如果用 sed -i 's/旧哈希/新哈希/' 去替换,$ 会被 sed 当成正则锚点,替换静默失败或结果错乱。推荐的处理是改用 Python 正则替换(对特殊字符做 re.escape),避开 sed 的转义地狱:
import re
path = '/srv/stacks/authelia/config/users.yml'
old, new = OLD_DIGEST, NEW_DIGEST
text = open(path, encoding='utf-8').read()
text = re.sub(re.escape(old), new, text) # re.escape 处理 $ 等特殊字符
open(path, 'w', encoding='utf-8').write(text)
通用教训:凡是替换内容里含
$'\&这类正则/替换元字符的,别用sed s///;要么行号定位,要么换 Python 之类的工具做转义处理。
坑二:登录失败先分清「被封禁」还是「密码错」
调试时很容易误以为哈希又配错了——用正确密码也返回 401。查 Authelia 容器日志才会发现是 regulation(暴力破解防护) 触发了:默认 2 分钟内失败 3 次就临时封禁 5 分钟,封禁期内即使密码正确也返回 401 KO。短时间反复跑自检脚本,很容易把管理员账号临时锁死,然后误判成配置故障。
排查登录失败,第一步永远是看容器日志区分 banned(被封禁)和 do not match(密码不匹配)这两种截然不同的原因。
关键点:regulation 的封禁状态是内存态,重启 authelia 容器即可清除。 被自己锁住时不用干等 5 分钟:
docker compose restart authelia
坑三:session.inactivity 调优
Authelia 的会话有个 inactivity(不活动超时)参数,控制多久没操作就让会话过期。默认值偏短,会导致在受保护面板里看一会儿日志、填一会儿表单,回来就被踢去重新登录。根据实际使用节奏把它调长一些,体验明显改善。这属于纯配置调优,没有对错,按自己的使用习惯来。
ForwardAuth 端到端验证方法
配完之后怎么验证真的通了?可以固化一套 curl 流程(DNS 未生效时可用 --resolve 本机预演):
# 1. 未登录访问受保护地址,应 302 到门户
curl -s -o /dev/null -w '%{http_code}\n' https://logs.example.com/ # 期望 302/401,绝不能是 200
# 2. 打开门户拿会话 Cookie
curl -s -c jar 'https://auth.example.com/?rd=<rd>&rm=GET'
# 3. 登录:POST /api/firstfactor,body 必须带 targetURL
curl -s -b jar -c jar 'https://auth.example.com/api/firstfactor?rd=<rd>' \
-H 'Content-Type: application/json' \
-H 'X-Original-URL: https://logs.example.com/' \
-H 'Referer: https://auth.example.com/?rd=<rd>&rm=GET' \
--data '{"username":"u","password":"p","targetURL":"https://logs.example.com/"}'
# 4. 用同一 jar 重新请求原地址,应得 200
curl -s -b jar -o /dev/null -w '%{http_code}\n' https://logs.example.com/
这里有几个极易踩的细节:
- 登录后回跳地址由 body 里的
targetURL决定,URL query 上的rd根本不参与 firstfactor 逻辑。 只传rd不传targetURL,会回跳到配置里的default_redirection_url,让人误以为「ForwardAuth 回跳坏了」。浏览器 SPA 本身就发targetURL,所以真实用户无感知,只有自动化脚本会踩。用redirect_uri之类的字段名同样无效。 /api/verify在 v4.39 已废弃返回 404,Traefik 集成要用/api/authz/forward-auth。- 判断「是否彻底摆脱了 BasicAuth 弹窗」有个干净判据:不带
Authorization头访问被拒时,响应里不会出现WWW-Authenticate头——所以浏览器不会弹原生框。只有客户端主动发 Basic 头才会触发 Authelia 的一阶 basic 认证。
坑四:Traefik 双 Provider 路由竞争绕过认证
这是整个认证体系里最危险、最隐蔽的一个坑,单独强调。
现象:明明给受保护域名配了 forwardAuth 中间件,但公网无凭据访问却直接返回 200——认证形同虚设,服务在公网裸奔。
根因:Traefik 同时启用了 Docker Provider 和 File Provider。它会给同一个域名生成两条规则完全相同的路由——xxx@docker(来自 compose 里的 traefik.http.* 标签,通常不带任何认证中间件)和 xxx@file(来自动态配置文件,带 forwardAuth)。两条 rule 长度相同即优先级相同,Traefik 按 provider 加载顺序取其一,实测 @docker 会胜出,于是认证中间件被静默绕过。
这个坑是在 Traefik 从 v3.4.5 升到 v3.7.12、Docker Provider 因 API 版本不兼容失效的问题被修复后立刻暴露的——之前 Docker Provider 压根没生效,反而「歪打正着」没有竞争。
修复:每个受保护域名只保留一条路由来源。可以把路由统一收敛到 File Provider(services.yml),删掉服务 compose 里的全部 traefik.http.* 标签并在原处留警告注释,然后 docker compose up -d 重建容器让 @docker 幽灵路由消失。
验证:
# 无凭据必须 302/401,绝不能 200
curl -s -o /dev/null -w '%{http_code}\n' https://<受保护域名>/
# 检查是否还有 @docker 幽灵路由(示例:受保护域名同时存在 @docker 与 @file 两条路由)
docker exec traefik wget -qO- http://127.0.0.1:8080/api/http/routers
建议把这条检查固化成回归护栏脚本 authelia-check.sh,专门检测 logs@docker 这类幽灵路由。核心教训:混用多个 Traefik Provider 时,「配了认证中间件」绝不等于「认证生效」,必须用无凭据请求实测状态码来验证。
版本固定,别用 latest
最后一条通用经验:Authelia 镜像版本务必固定成 authelia/authelia:4.39.22 而不是 latest。它的配置 schema 会随 minor 版本演进,一次随手 docker compose pull 就可能让登录门户起不来——而门户一挂,你就会被锁在所有受保护服务之外,连进去看日志都费劲。同理适用于所有承载认证/入口的关键组件。
下一篇讲一个更烧脑的问题:Dozzle 的 Service Worker 是怎么和 ForwardAuth 撞车、把登录页搞成死循环的,以及为什么 curl 全绿却依然是坏的。
评论 (0)
暂无评论,来说点什么吧。
登录 后可以发表评论