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.remoteauth.nat)
  • 通配/对应证书:remote.vanjay.cnnat.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.ymlpassword


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_nameauth.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 对外端口不固定。关键在两点:

  1. 回跳目标 rd:用 $http_host(= 客户端请求的 Host,自带端口),所以 rd 永远是用户实际访问的 host:port
  2. 门户地址:由两个 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 新增一个被保护站点(同一父域)

  1. 在该站点的 nginx vhost 里加 §6.6 的两处 include
  2. configuration.ymlaccess_control 规则 domain: 列表里加上它。
  3. docker compose restart authelia(改了 Authelia 配置)+ docker exec nginx nginx -s reload

10.2 新增一条访问路径(新的父域,如再来个 lan.vanjay.cn

  1. session.cookies 增加一项:domain: 'lan.vanjay.cn'authelia_url: 'https://auth.lan.vanjay.cn'
  2. access_control 加该父域下的站点 → two_factor
  3. 新建门户 vhost auth.lan.vanjay.cn(参考 §6.5)。
  4. $authelia_portal_host 的 map 已通用(auth.<父域>),无需改。
  5. 重启 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/