Authelia 2FA 部署文档(nginx auth_request + haproxy)
用 Authelia 给若干站点加「用户名 + 密码 + TOTP」两步验证。
适配 Authelia v4.39.20。本文是按本机实际部署整理的可复用手册,在其他设备上照做即可。
1. 架构与原理
浏览器 ──TLS(SNI)──> haproxy(send-proxy-v2) ──proxy_protocol──> nginx(host 网络)
│
auth_request 子请求 ─────┤
▼
Authelia(127.0.0.1:9091)
- haproxy:TCP 模式按 SNI 分发,用
send-proxy-v2把真实客户端 IP 透传给 nginx;末尾default_backend nginx会兜住所有未显式列出的子域名(所以门户子域名无需单独加 haproxy 规则)。 - nginx:
network_mode: host,被保护站点的location里用auth_request向 Authelia 发子请求;返回 200 放行,401 则跳转到登录门户。因为前端走proxy_protocol,真实客户端 IP 在$proxy_protocol_addr。 - Authelia:只监听
127.0.0.1:9091(不直接对外),提供登录门户和鉴权 API。 - 会话 cookie 按父域划分:
*.remote.vanjay.cn与*.nat.vanjay.cn是两条独立访问路径,各自一个 cookie 域 + 一个门户(auth.remote/auth.nat),互不共享会话。同一父域下的子站点之间是 SSO(登录一次,其他子站点免登)。 - 对外端口:
remote固定4433;nat端口不固定,由 nginx 按客户端Host头动态求得(见 §8)。
受保护站点(本机实例)
| 站点 | 后端 | 路径 / cookie 域 | 门户 |
|---|---|---|---|
| code.remote.vanjay.cn | 192.168.18.28:8448 | remote.vanjay.cn | auth.remote.vanjay.cn:4433 |
| ikuai.remote.vanjay.cn | 192.168.18.1:80 | remote.vanjay.cn | auth.remote.vanjay.cn:4433 |
| tplink.remote.vanjay.cn | 192.168.18.10:80 | remote.vanjay.cn | auth.remote.vanjay.cn:4433 |
| code.nat.vanjay.cn | 192.168.18.28:8448 | nat.vanjay.cn | auth.nat.vanjay.cn:<动态> |
| ikuai.nat.vanjay.cn | 192.168.18.1:80 | nat.vanjay.cn | auth.nat.vanjay.cn:<动态> |
| tplink.nat.vanjay.cn | 192.168.18.10:80 | nat.vanjay.cn | auth.nat.vanjay.cn:<动态> |
2. 前置条件
- Docker / Docker Compose
- nginx(官方
nginx:latest自带ngx_http_auth_request_module,无需自行编译) - 前端 7 层/4 层入口(haproxy、lucky 等),能把流量带
proxy_protocol转发给 nginx - 通配 DNS:
*.remote.vanjay.cn、*.nat.vanjay.cn(含auth.remote、auth.nat) - 通配/对应证书:
remote.vanjay.cn、nat.vanjay.cn(需覆盖auth.*子域) - 主机系统时间准确(TOTP 依赖时间,误差应 < 30s)
3. 目录结构
authelia/
├── docker-compose.yml
├── .gitignore
└── config/
├── configuration.yml # 主配置
├── users_database.yml # 用户库(用户名 + argon2 密码哈希)
├── secrets/ # 三个密钥文件(不要提交 git)
│ ├── JWT_SECRET
│ ├── SESSION_SECRET
│ └── STORAGE_ENCRYPTION_KEY
├── db.sqlite3 # 运行时生成(存 TOTP 等)
└── notification.txt # 运行时生成(文件通知)
nginx/config/conf.d/
├── authelia-maps.conf # http 级:动态端口 + 动态门户域名
├── common/
│ ├── authelia-location.conf # 鉴权子请求端点(放 server 块)
│ ├── authelia-authrequest.conf # 鉴权 + 跳转(放 location 块,remote/nat 通用)
│ └── authelia-proxy.conf # 反代门户用的头
├── ipv6/
│ ├── auth.remote.vanjay.cn.conf
│ └── {code,ikuai,tplink}.remote.vanjay.cn.conf
└── ipv4/
├── auth.nat.vanjay.cn.conf
└── {code,ikuai,tplink}.nat.vanjay.cn.conf
4. Authelia 文件
4.1 authelia/docker-compose.yml
services:
authelia:
image: authelia/authelia:4.39.20
container_name: authelia
volumes:
- ./config:/config
environment:
- TZ=Asia/Shanghai
# 三个核心密钥通过文件注入,不写进 configuration.yml
- AUTHELIA_IDENTITY_VALIDATION_RESET_PASSWORD_JWT_SECRET_FILE=/config/secrets/JWT_SECRET
- AUTHELIA_SESSION_SECRET_FILE=/config/secrets/SESSION_SECRET
- AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE=/config/secrets/STORAGE_ENCRYPTION_KEY
ports:
# 只监听本机回环,由 host 网络的 nginx 通过 127.0.0.1:9091 访问
- 127.0.0.1:9091:9091
restart: always
4.2 authelia/config/configuration.yml
---
server:
address: 'tcp://0.0.0.0:9091'
log:
level: 'info'
file_path: '/config/authelia.log'
keep_stdout: true
theme: 'auto'
totp:
issuer: 'remote.vanjay.cn'
period: 30
skew: 1
# identity_validation.reset_password.jwt_secret 由环境变量
# AUTHELIA_IDENTITY_VALIDATION_RESET_PASSWORD_JWT_SECRET_FILE 注入
authentication_backend:
password_reset:
disable: false
file:
path: '/config/users_database.yml'
search:
case_insensitive: true # 用户名忽略大小写
password:
algorithm: 'argon2'
access_control:
default_policy: 'deny'
rules:
- domain:
- 'code.remote.vanjay.cn'
- 'ikuai.remote.vanjay.cn'
- 'tplink.remote.vanjay.cn'
- 'code.nat.vanjay.cn'
- 'ikuai.nat.vanjay.cn'
- 'tplink.nat.vanjay.cn'
policy: 'two_factor'
session:
# secret 来自环境变量 AUTHELIA_SESSION_SECRET_FILE
cookies:
# remote 路径:固定对外端口 4433
- name: 'authelia_session'
domain: 'remote.vanjay.cn'
authelia_url: 'https://auth.remote.vanjay.cn:4433'
default_redirection_url: 'https://remote.vanjay.cn:4433'
expiration: '4 hours'
inactivity: '30 minutes'
remember_me: '1 month'
# nat 路径:对外端口不固定,跳转端口由 nginx 按客户端 Host 动态拼接
- name: 'authelia_session'
domain: 'nat.vanjay.cn'
authelia_url: 'https://auth.nat.vanjay.cn'
expiration: '4 hours'
inactivity: '30 minutes'
remember_me: '1 month'
regulation:
# 防爆破:2 分钟内连续 3 次失败,封禁该用户 5 分钟
max_retries: 3
find_time: '2 minutes'
ban_time: '5 minutes'
storage:
# encryption_key 来自环境变量 AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE
local:
path: '/config/db.sqlite3'
notifier:
# 无邮件服务器时用文件通知:2FA 注册/改密的确认链接写入此文件
disable_startup_check: false
filesystem:
filename: '/config/notification.txt'
...
4.3 authelia/config/users_database.yml(模板)
---
users:
vanjay: # 登录用户名(配合 case_insensitive,大小写均可)
disabled: false
displayname: 'VanJay'
password: '<在下方用命令生成的 $argon2id$... 哈希>'
email: 'you@example.com'
groups:
- 'admins'
...
4.4 authelia/.gitignore
config/secrets/
config/db.sqlite3
config/notification.txt
config/authelia.log
5. 生成密钥与密码哈希
5.1 三个密钥
cd authelia
mkdir -p config/secrets
umask 077
openssl rand -base64 64 | tr -d '\n' > config/secrets/JWT_SECRET
openssl rand -base64 64 | tr -d '\n' > config/secrets/SESSION_SECRET
openssl rand -base64 64 | tr -d '\n' > config/secrets/STORAGE_ENCRYPTION_KEY
chmod 600 config/secrets/*
⚠️
STORAGE_ENCRYPTION_KEY一旦用于初始化db.sqlite3就不能更改(否则已存的 TOTP 等无法解密)。迁移设备时把整个config/一起搬,或重新注册 TOTP。
5.2 密码哈希(argon2id)
docker run --rm authelia/authelia:4.39.20 \
authelia crypto hash generate argon2 --password '你的强密码'
把输出的 $argon2id$... 整串填进 users_database.yml 的 password。
6. nginx 文件
6.1 conf.d/authelia-maps.conf(http 级,动态端口 + 动态门户域名)
# 1) 从客户端 Host 头提取端口(形如 :8443),无端口则为空
map $http_host $authelia_req_port {
default '';
'~^[^:]+:(?<p>[0-9]+)$' ':$p';
}
# 2) 由被访问主机推导对应门户域名:
# code.remote.vanjay.cn -> auth.remote.vanjay.cn
# code.nat.vanjay.cn -> auth.nat.vanjay.cn
map $host $authelia_portal_host {
default 'auth.remote.vanjay.cn';
'~^[^.]+\.(?<rest>.+)$' 'auth.$rest';
}
该文件必须在 http 上下文被加载。本机
nginx.conf里有include /etc/nginx/conf.d/*.conf;,放在conf.d/顶层即可(注意不要放进含server{}的子目录)。
6.2 conf.d/common/authelia-location.conf
set $upstream_authelia http://127.0.0.1:9091/api/authz/auth-request;
location /internal/authelia/authz {
internal;
proxy_pass $upstream_authelia;
proxy_set_header X-Original-Method $request_method;
# $http_host 含客户端实际端口,remote/nat 通用
proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
# 前端走 proxy_protocol,真实客户端 IP 在 $proxy_protocol_addr
proxy_set_header X-Forwarded-For $proxy_protocol_addr;
proxy_set_header Content-Length "";
proxy_set_header Connection "";
proxy_pass_request_body off;
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503;
proxy_redirect http:// $scheme://;
proxy_http_version 1.1;
proxy_cache_bypass $cookie_authelia_session;
proxy_no_cache $cookie_authelia_session;
proxy_buffers 4 32k;
client_body_buffer_size 128k;
send_timeout 5m;
proxy_read_timeout 240;
proxy_send_timeout 240;
proxy_connect_timeout 240;
}
6.3 conf.d/common/authelia-authrequest.conf(remote / nat 通用)
auth_request /internal/authelia/authz;
auth_request_set $user $upstream_http_remote_user;
auth_request_set $groups $upstream_http_remote_groups;
auth_request_set $name $upstream_http_remote_name;
auth_request_set $email $upstream_http_remote_email;
proxy_set_header Remote-User $user;
proxy_set_header Remote-Groups $groups;
proxy_set_header Remote-Email $email;
proxy_set_header Remote-Name $name;
# 回跳目标 = 客户端访问的原始 URL($http_host 已含动态端口)
set $target_url $scheme://$http_host$request_uri;
# 未认证(401)跳到对应门户:auth.<父域> + 客户端实际端口
error_page 401 =302 https://$authelia_portal_host$authelia_req_port/?rd=$target_url&rm=$request_method;
6.4 conf.d/common/authelia-proxy.conf(反代门户用)
proxy_set_header Host $host;
proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-URI $request_uri;
proxy_set_header X-Forwarded-Ssl on;
proxy_set_header X-Forwarded-For $proxy_protocol_addr;
proxy_set_header X-Real-IP $proxy_protocol_addr;
client_body_buffer_size 128k;
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503;
proxy_redirect http:// $scheme://;
proxy_http_version 1.1;
proxy_cache_bypass $cookie_authelia_session;
proxy_no_cache $cookie_authelia_session;
proxy_buffers 64 256k;
send_timeout 5m;
proxy_read_timeout 360;
proxy_send_timeout 360;
proxy_connect_timeout 360;
6.5 门户 vhost
conf.d/ipv6/auth.remote.vanjay.cn.conf:
server {
listen 127.0.0.2:8033;
server_name auth.remote.vanjay.cn;
return 301 https://$host$request_uri;
}
server {
listen 127.0.0.2:4435 ssl proxy_protocol;
server_name auth.remote.vanjay.cn;
ssl_certificate /etc/nginx/ssl/sync/remote.vanjay.cn/remote.vanjay.cn.crt;
ssl_certificate_key /etc/nginx/ssl/sync/remote.vanjay.cn/remote.vanjay.cn.key;
include /etc/nginx/conf.d/common/ssl_common.conf;
include /etc/nginx/conf.d/common/add_header_normal.conf;
include /etc/nginx/conf.d/common/custom_error_pages.conf;
location / {
proxy_pass http://127.0.0.1:9091;
include /etc/nginx/conf.d/common/authelia-proxy.conf;
}
}
conf.d/ipv4/auth.nat.vanjay.cn.conf 同上,把 server_name 改 auth.nat.vanjay.cn、证书改 nat.vanjay.cn。
6.6 给一个站点加保护(以 code.remote 为例)
在 server{} 块里(location / 之前)加鉴权端点,在 location / 内首行加鉴权:
server {
listen 127.0.0.2:4435 ssl proxy_protocol;
server_name code.remote.vanjay.cn;
ssl_certificate /etc/nginx/ssl/sync/remote.vanjay.cn/remote.vanjay.cn.crt;
ssl_certificate_key /etc/nginx/ssl/sync/remote.vanjay.cn/remote.vanjay.cn.key;
include /etc/nginx/conf.d/common/ssl_common.conf;
include /etc/nginx/conf.d/common/custom_error_pages.conf;
# ① 鉴权端点
include /etc/nginx/conf.d/common/authelia-location.conf;
location / {
# ② 鉴权 + 未登录跳转
include /etc/nginx/conf.d/common/authelia-authrequest.conf;
proxy_pass http://192.168.18.28:8448;
include /etc/nginx/conf.d/common/proxy_http.conf; # 你原有的反代头
}
}
7. 部署步骤(按顺序,避免把自己锁在外面)
# 1. 准备 authelia/ 下所有文件,生成密钥(§5.1)与密码哈希(§5.2),填好 users_database.yml
# 2. 离线校验配置(强烈建议,配错会导致容器起不来)
docker run --rm -v "$PWD/authelia/config:/config" \
-e AUTHELIA_IDENTITY_VALIDATION_RESET_PASSWORD_JWT_SECRET_FILE=/config/secrets/JWT_SECRET \
-e AUTHELIA_SESSION_SECRET_FILE=/config/secrets/SESSION_SECRET \
-e AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE=/config/secrets/STORAGE_ENCRYPTION_KEY \
authelia/authelia:4.39.20 authelia validate-config --config /config/configuration.yml
# 3. 启动 Authelia,看日志确认 "Startup complete" 且无 error
cd authelia && docker compose up -d && docker compose logs -f authelia
# 4. 校验并重载 nginx(此步同时启用门户 + 各站点鉴权)
docker exec nginx nginx -t && docker exec nginx nginx -s reload
# 5. 注册 TOTP(见 §9),然后浏览器实测
8. 动态端口是怎么实现的
nat 对外端口不固定。关键在两点:
- 回跳目标
rd:用$http_host(= 客户端请求的Host,自带端口),所以rd永远是用户实际访问的host:port。 - 门户地址:由两个 map 拼成 ——
$authelia_portal_host(auth.<父域>)+$authelia_req_port(从Host提取的端口)。
效果:
| 访问 | 跳转到 |
|---|---|
code.remote.vanjay.cn:4433 |
auth.remote.vanjay.cn:4433 |
code.nat.vanjay.cn:8443 |
auth.nat.vanjay.cn:8443 |
code.nat.vanjay.cn:12345 |
auth.nat.vanjay.cn:12345 |
因此 6 个站点共用同一个
authelia-authrequest.conf,固定端口与动态端口都适配。
9. 注册 / 管理 TOTP
方式 A — 命令行直接生成(推荐):
docker exec authelia authelia storage user totp generate vanjay --config /config/configuration.yml
用输出的 otpauth:// 链接 / 二维码加进 Authenticator(Google Authenticator、1Password、苹果密码等),再到门户网页输入 6 位码。
方式 B — 网页注册: 登录门户后按提示注册,确认链接在文件通知里:
docker exec authelia cat /config/notification.txt
其他常用命令:
# 删除某用户的 TOTP(重新绑定前)
docker exec authelia authelia storage user totp delete vanjay --config /config/configuration.yml
# 查看用户信息
docker exec authelia authelia storage user info vanjay --config /config/configuration.yml
10. 如何扩展
10.1 新增一个被保护站点(同一父域)
- 在该站点的 nginx vhost 里加 §6.6 的两处
include。 - 在
configuration.yml的access_control规则domain:列表里加上它。 docker compose restart authelia(改了 Authelia 配置)+docker exec nginx nginx -s reload。
10.2 新增一条访问路径(新的父域,如再来个 lan.vanjay.cn)
session.cookies增加一项:domain: 'lan.vanjay.cn'、authelia_url: 'https://auth.lan.vanjay.cn'。access_control加该父域下的站点 →two_factor。- 新建门户 vhost
auth.lan.vanjay.cn(参考 §6.5)。 $authelia_portal_host的 map 已通用(auth.<父域>),无需改。- 重启 Authelia + reload nginx。
11. 排错
| 现象 | 原因 / 处理 |
|---|---|
| 跳转地址丢端口 | nginx 用了 $host 而非 $http_host;门户端口靠 $authelia_req_port map,确认 authelia-maps.conf 在 http 级被加载 |
| 输用户名密码报 “user not found” | 用户名键大小写;已开 case_insensitive 时文件里的键必须小写 |
| 登录后 TOTP 总是错 | 主机系统时间不准(date 检查);TOTP 依赖时间 |
| 容器起不来 | 先跑 §7 第 2 步 validate-config 看报错 |
| 某站点没弹登录 | 多半是同父域 SSO 已登录(正常);用无痕窗口或登出验证 |
日志 NTP i/o timeout 警告 |
Authelia 自带校时探测被网络挡了,不影响功能,只要主机时间准即可 |
临时解除某站点保护(防锁死): 把该 vhost 里两行 include .../authelia-*.conf 注释掉,docker exec nginx nginx -s reload。
实时验证某域名是否受保护(无需浏览器):
curl -s -o /dev/null -w '%{http_code}\n' \
-H "X-Original-URL: https://code.remote.vanjay.cn:4433/" \
-H "X-Original-Method: GET" -H "X-Forwarded-For: 8.8.8.8" \
http://127.0.0.1:9091/api/authz/auth-request
# 401 = 受保护(要求登录)
12. 注意事项 / 已知取舍
- 会话存在内存(未接 Redis):重启 Authelia 会清空在线会话,需重新登录;TOTP 存在
db.sqlite3,不受影响。 - 旧式跳转的
rd未做 URL 编码:stock nginx 无set_escape_uri模块,若首次被拦截的 URL 查询串里带&,登录后回跳可能被截断。首次进站通常是根路径,影响很小;如需彻底解决,换带ngx_http_set_misc_module的 nginx 镜像并改用set_escape_uri。 - 密钥与
db.sqlite3要一起迁移,否则 TOTP 解不开,需重新注册。 config/secrets/不要提交到 git(已在.gitignore)。- 默认策略
deny:只有access_control里列出的域名才会被放行规则匹配;门户本身不走auth_request。
Authelia 版本:v4.39.20ㅤ|ㅤ参考官方文档:https://www.authelia.com/