用 1Panel + Authelia 给多个站点加一道统一登录墙

本文记录我在 1Panel(OpenResty + 反向代理)环境下部署 Authelia,并把 OpenList、Typecho 后台接入同一套登录体系的完整过程。
文中统一以 example.com 作为示例域名,请全部替换成你自己的域名。
环境:Debian/Ubuntu + 1Panel v2 + OpenResty(host 网络模式)+ Authelia 4.39.28。

一、先搞清楚 Authelia 是什么

很多人第一次接触会误以为它是「网站插件」,其实不是。

Authelia 是反向代理前面的一道鉴权前置层,它不修改你的站点代码,也不接管你的业务逻辑。工作方式是:nginx 每收到一个请求,先内部问一句 Authelia「这个人登录了吗」,Authelia 说 OK 才把请求转给后端。

1
2
3
4
5
6
7
外网用户
│
▼
OpenResty (80/443)
│ ├─► 内部子请求 ──► Authelia :9091 ──「放行 / 拒绝」
│
└─► 放行后 proxy_pass ──► OpenList / Typecho / 静态站

所以部署它实际要做三件事:

  1. 启动一个 Authelia 容器
  2. 给它一个域名和证书(必须 HTTPS)
  3. 在站点 nginx 配置里挂一段 auth_request

二、前置条件

条件说明
一个域名Authelia 不能用 IP 访问,登录态存在 cookie 里,cookie 绑域名
一个泛域名证书(推荐)*.example.com,省得每个子域单独申请
DNS 控制权要能加 auth 的 A 记录
nginx 带 http_auth_request_module1Panel 的 OpenResty 一般都有,见下一步验证
国内服务器还要注意:域名必须完成备案,否则 80/443 无法正常解析访问。

三、开工前先探测环境

这一步能省掉后面大量的试错。全部在「主机终端」里执行(1Panel 左侧菜单的「终端」,提示符形如 root@iZbp...),不要在应用卡片的「容器终端」里跑——容器里没有 docker 命令,文件也是临时的。

3.1 查真实容器名

1
docker ps --format '{{.Names}}\t{{.Ports}}' | grep -Ei 'openresty|typecho|openlist'

注意:1Panel 界面上显示的容器名末尾常被缩略,实际名字可能是 1Panel-openresty-cGcF 而不是界面上看到的 1Panel-openresty-6d4f。以命令输出为准。

3.2 查 OpenResty 网络模式

1
docker inspect <openresty容器名> --format '{{.HostConfig.NetworkMode}}'
  • 输出 host → Authelia 用 127.0.0.1:9091 访问
  • 输出某个网络名(如 1panel-network)→ Authelia 用容器名访问,需要把两者放进同一网络

本文按 host 模式 写(1Panel 默认安装的 OpenResty 就是 host)。

3.3 查 auth_request 模块

1
docker exec <openresty容器名> openresty -V 2>&1 | tr ' ' '\n' | grep -E 'auth|lua'

必须看到 --with-http_auth_request_module。没有这个模块,整套方案走不通(那就得改用 Lua 的 ngx.location.capture 方案,复杂不少)。

小坑:别写成 openresty -V | grep auth_request。如果容器名不对,No such container 的报错会被 grep 过滤掉,屏幕上一片空白,看起来像「命令成功但没输出」。

四、部署 Authelia

4.1 建目录、写主配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
mkdir -p /opt/authelia/config && cd /opt/authelia

cat > config/configuration.yml <<EOF
theme: 'light'
server:
address: 'tcp://:9091/'
log:
level: 'info'
totp:
issuer: 'example.com'
webauthn:
disable: false
display_name: 'example.com'
attestation_conveyance_preference: 'none'
identity_validation:
reset_password:
jwt_secret: '$(openssl rand -hex 32)'
authentication_backend:
file:
path: '/config/users_database.yml'
password:
algorithm: 'argon2'
argon2:
variant: 'argon2id'
iterations: 3
memory: 65536
parallelism: 4
key_length: 32
salt_length: 16
access_control:
default_policy: 'deny'
rules:
- domain: 'auth.example.com'
policy: 'bypass'
- domain: 'pan.example.com'
policy: 'one_factor'
- domain: 'blog.example.com'
policy: 'one_factor'
session:
name: 'authelia_session'
same_site: 'lax'
expiration: '1h'
inactivity: '1h'
remember_me: '1M'
cookies:
- domain: 'example.com'
authelia_url: 'https://auth.example.com'
default_redirection_url: 'https://example.com'
regulation:
max_retries: 3
find_time: '2m'
ban_time: '5m'
storage:
encryption_key: '$(openssl rand -hex 32)'
local:
path: '/config/db.sqlite3'
notifier:
filesystem:
filename: '/config/notification.txt'
EOF

几个关键点,都是踩过坑才明白的:

① jwt_secret 不能省。
Authelia 4.39 起这是强制项,缺了会一直重启,日志里刷:

1
Configuration: identity_validation.reset_password: option 'jwt_secret' is required

② cookies.domain 必须写父域(example.com,不带 www、不带子域前缀)。
写错了会出现「登录成功了,但换个站点又让你登录」的诡异现象。登录态靠这个 cookie 在各子域间共享,SSO 能不能成立全看这一行。

③ inactivity 别用默认值。
默认很短,5 分钟不操作就掉登录,开了两步验证之后每次重登都很折磨。设成 1h 和 expiration 对齐比较舒服。

④ heredoc 用的是 <<EOF(不加引号),这样 $(openssl rand -hex 32) 会被 shell 展开,生成真正的随机密钥。如果写成 <<'EOF',密钥会变成这行字符串本身,容器起不来。

4.2 生成用户

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
cd /opt/authelia
read -rp "登录密码: " PWD1

HASH=$(docker run --rm authelia/authelia:4.39.28 \
authelia crypto hash generate argon2 --password "$PWD1" \
| grep -o '\$argon2id[^ ]*')
echo "生成的哈希: $HASH"

cat > config/users_database.yml <<EOF
users:
admin:
disabled: false
displayname: 'Admin'
password: '$HASH'
email: 'admin@example.com'
groups:
- admins
EOF

看到 生成的哈希: $argon2id$v=19$m=65536,t=3,p=4$... 才算成功。如果是空的,说明密码里有 $、!、\ 之类被 shell 吃掉了——换纯字母数字密码重试。这是「明明密码对却登不进去」最常见的原因。

用户名 admin: 那一行不能加引号,且缩进 2 空格,下面的字段缩进 4 空格。YAML 缩进错一个字符,Authelia 直接起不来。

4.3 启动容器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
cat > docker-compose.yml <<'EOF'
services:
authelia:
image: authelia/authelia:4.39.28
container_name: authelia
restart: unless-stopped
volumes:
- ./config:/config
ports:
- '127.0.0.1:9091:9091'
environment:
TZ: Asia/Shanghai
EOF

docker compose up -d
sleep 6
docker logs --tail 15 authelia
curl -s http://127.0.0.1:9091/api/health

端口绑在 127.0.0.1 上是故意的:公网无法直连 Authelia,只能通过 nginx 访问,少一个暴露面。

成功标志:

1
2
Authelia is listening for non-TLS connections on [::]:9091
{"status":"OK"}

如果 curl 返回空,一定是配置问题,先看 docker logs authelia 的 level=error 那几行,基本都是缩进或字段名写错。


五、给 Authelia 一个域名

5.1 加 DNS

类型主机记录记录值
Aauth你的服务器公网 IP
1
ping -c 2 auth.example.com

能解析到你的 IP 就 OK。

5.2 1Panel 建反向代理站点

网站 → 创建网站 → 反向代理

字段值
域名auth.example.com
代理地址http://127.0.0.1:9091
证书选泛域名证书,或用 DNS 验证申请

HTTPS 必须开。 Authelia 的 session cookie 带 Secure 标记,走 http 根本种不上,登录会一直失败。

5.3 验证

1
curl -sk -o /dev/null -w "%{http_code}\n" https://auth.example.com

返回 200 就通了。浏览器打开应该能看到登录页,用 admin + 密码能登进去。

这一步必须先成功再往下做,否则后面挂站点会变成「登不进去又退不出来」的死循环。


六、把站点接入鉴权(核心步骤)

6.1 先看清 1Panel 的配置结构

1
2
cp /opt/1panel/www/conf.d/pan.example.com.conf /root/pan.conf.bak
cat /opt/1panel/www/conf.d/pan.example.com.conf

你会看到一个大致这样的结构:

1
2
3
4
5
6
server {
listen 443 ssl ;
server_name pan.example.com;
...
include /www/sites/pan.example.com/proxy/*.conf;
}

关键点:location / 和 proxy_pass 不在主配置里,被拆到了 sites/xxx/proxy/*.conf。

这解释了为什么「往主配置里塞 location」会失败。也决定了我们的插入策略:写在主配置的 server 级,不动 proxy/ 目录。

原因是 1Panel 在修改站点设置(换域名、改端口、开关 HTTPS)时会重新生成 proxy/*.conf,手工改动会被覆盖;而 conf.d/xxx.conf 基本不会被动。

6.2 准备配置片段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
cat > /tmp/ah.conf <<'EOF'
auth_request /authelia-verify;
auth_request_set $target_url $scheme://$http_host$request_uri;
error_page 401 =302 https://auth.example.com/?rd=$target_url;

location = /authelia-verify {
internal;
proxy_pass http://127.0.0.1:9091/api/authz/auth-request;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header Host $http_host;
proxy_set_header Authorization "";
proxy_set_header Cookie $http_cookie;
proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
proxy_set_header X-Original-Method $request_method;
proxy_set_header X-Forwarded-Method $request_method;
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-For $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
}
EOF

这几行里有三个「不写就会出怪问题」的 header:

配置作用不写的后果
Host $http_host让 Authelia 知道真实域名,按域名匹配 cookie 规则判定失败,永远匿名
Cookie $http_cookie显式把 cookie 传给子请求主页面能过,API 全部 302
Authorization ""抹掉客户端自带的 Authorization日志刷 failed to parse content of Authorization header

注意 auth_request 写在 server 级、location 外面,不能写进 location / 里面——否则 location = /authelia-verify 自己也会被要求认证,形成递归死循环。

6.3 插入配置

1
2
3
4
cd /opt/1panel/www/conf.d
cp pan.example.com.conf pan.example.com.conf.bak
sed -i '/server_name pan.example.com;/r /tmp/ah.conf' pan.example.com.conf
grep -n "server_name\|auth_request /authelia-verify" pan.example.com.conf

期望输出:

1
2
4:    server_name pan.example.com;
5: auth_request /authelia-verify;

auth_request 紧跟在 server_name 后面,位置就对了。

sed 的 r 命令是「匹配行之后追加文件内容」,比手写正则插入稳得多,也不用你去找大括号在哪。

6.4 检查语法 → 重载 → 验证

1
docker exec <openresty容器名> openresty -t

必须看到 test is successful。不通过就别 reload,先回滚:

1
cp pan.example.com.conf.bak pan.example.com.conf

通过后:

1
2
3
docker exec <openresty容器名> openresty -s reload
sleep 2
curl -sk -o /dev/null -w "%{http_code} %{redirect_url}\n" https://pan.example.com/

期望:

1
302 https://auth.example.com/?rd=https://pan.example.com/

看到这个 302,就说明鉴权链路通了。


七、两个必踩的坑(重点看)

7.1 -t 不会让配置生效

这是最容易浪费时间的一条:

命令作用
openresty -t只检查语法对不对
openresty -s reload真正加载新配置

只跑 -t 会让你以为改好了,实际运行的还是旧配置。我第一次就是改完 -t 一通过就去 curl,结果首页仍然 302,找了一小时才发现少了一条 reload。

7.2 reload 之后要等一下再验证

1
docker exec <openresty容器名> openresty -s reload; sleep 2

reload 是平滑重载,旧 worker 会继续处理完手头的请求,新 worker 才接管。reload 完立刻 curl,很可能打到旧 worker 上,拿到的是旧结果。

同样的道理:openresty -T(打印运行中的完整配置)读的是磁盘文件,只能证明「文件写对了」,不能证明「已经生效」。


八、进阶:只保护后台,前台保持公开

网盘这类「本来就是私密」的站点适合整站保护,但博客不行——整站加锁等于访客看不到内容、搜索引擎也抓不到。

思路:auth_request 照样挂在 server 级,但在校验入口里加一个判断,只有后台路径才去问 Authelia,其他路径直接返回 204 放行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
cat > /tmp/ah-admin.conf <<'EOF'
auth_request /authelia-verify;
auth_request_set $target_url $scheme://$http_host$request_uri;
error_page 401 =302 https://auth.example.com/?rd=$target_url;

location = /authelia-verify {
internal;

# 只对后台和登录接口做鉴权,其余路径直接放行
if ($request_uri !~ "^/(admin|action/login)") {
return 204;
}

proxy_pass http://127.0.0.1:9091/api/authz/auth-request;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header Host $http_host;
proxy_set_header Authorization "";
proxy_set_header Cookie $http_cookie;
proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
proxy_set_header X-Original-Method $request_method;
proxy_set_header X-Forwarded-Method $request_method;
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-For $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
}
EOF

为什么 /action/login 也要保护?因为 Typecho 的登录表单提交到这个路径。只护 /admin 而放行 /action/login,等于留了一个绕过外层鉴权直接登后台的后门。 两者一起护才严密,而评论走的 /action/comment 不在名单里,读者照常评论。

验证:

1
2
curl -sk -o /dev/null -w "首页 %{http_code} -> %{redirect_url}\n" https://blog.example.com/
curl -sk -o /dev/null -w "后台 %{http_code} -> %{redirect_url}\n" https://blog.example.com/admin/
1
2
首页 200 -> 
后台 302 -> https://auth.example.com/?rd=https://blog.example.com/admin/

首页那行 -> 后面是空的,才说明真的放行了。


九、进阶:开启两步验证(TOTP / WebAuthn)

9.1 开启模块

webauthn 段必须在配置最外层(顶格),加在 totp 之后:

1
2
3
4
webauthn:
disable: false
display_name: 'example.com'
attestation_conveyance_preference: 'none'
1
docker restart authelia; sleep 4; docker logs --tail 6 authelia

9.2 注册设备

  1. 打开 https://auth.example.com,用密码登录
  2. 访问 https://auth.example.com/settings
  3. 左侧点「安全」——注意不是「两步验证」
页面作用
安全注册/删除验证方式(TOTP、WebAuthn)
两步验证列出哪些站点要求二因素,还没改策略时这里是空的

「一次性密码」点添加 → 出二维码 → 手机验证器扫码 → 填 6 位数字。
「WebAuthn 凭据」点添加 → 给设备起名 → 按指纹 / Windows Hello。

建议两种都注册:TOTP 当保底(不绑域名,换域名也能用),WebAuthn 当日常(更快、抗钓鱼)。

9.3 改成强制二因素

先确认设备注册成功并能通过验证,再改策略。 顺序反了会把自己锁在门外。

1
2
3
sed -i "s/policy: 'one_factor'/policy: 'two_factor'/" /opt/authelia/config/configuration.yml
sed -n '/^access_control:/,/^session:/p' /opt/authelia/config/configuration.yml
docker restart authelia; sleep 4

十、踩坑清单

现象原因处理
容器反复重启,日志报 jwt_secret is required4.39 起强制项补 identity_validation.reset_password.jwt_secret
生成的哈希是空密码含特殊字符被 shell 吃掉换纯字母数字密码
登录成功但换站又要登录cookies.domain 写成了子域改成父域 example.com
主页面能进,API 全 302 / Network Error子请求没带 cookie加 proxy_set_header Cookie $http_cookie;
日志刷 Invalid scheme客户端 Authorization 被透传加 proxy_set_header Authorization "";
改了配置但没效果只跑了 -t 没 reload补 openresty -s reload
reload 后仍 200旧 worker 还在处理请求加 sleep 2 再验证
站点改设置后保护失效1Panel 重写了配置重新执行 6.3,并确认改的是 conf.d
验证码弹窗总失败提交的码长度不对 / 触发了限流见下方第十一节
一直显示「获取设置失败」浏览器缓存了旧页面Ctrl+Shift+R 强刷
登出后所有站点一起登出登录态共享在父域 cookie设计如此,不是 bug

十一、关于「身份验证」弹窗和限流

Authelia 在执行敏感操作(改邮箱、改密码、注册新的两步验证设备)前,会要求一次身份确认,弹窗里写着「已向您的电子邮件发送了一次性密码」。

但如果你用的是 notifier: filesystem(本文配置就是这样),它不会真的发邮件,而是写到服务器上的一个文本文件里:

1
tail -n 60 /opt/authelia/config/notification.txt

验证码在两行 ==== 之间,8 位、区分大小写。想只取码不看废话:

1
tail -n 60 /opt/authelia/config/notification.txt | grep -E "^[A-Z0-9]{8}$" | tail -1

两个高频错误:

  1. 粘贴了一整段文字进去。 日志会明说 the user provided code was 65 characters in length——正确长度是 8。粘贴前瞄一眼输入框长度。
  2. 连续重试触发限流。 配置里 regulation 的「2 分钟内失败 3 次封 5 分钟」对验证码生成本身也生效,日志会刷 Rate Limit Exceeded bucket=2 delay=245s。

被限流的解法:手动重启容器(限流计数在内存里,重启即清零):

1
docker restart authelia

代价是所有登录会话作废,需要重新登录。

操作纪律:一次只点一次「添加」,弹窗出来就去拿码,拿到就填。 任何一步不顺,先停下看日志,不要连点。


十二、收尾工作

12.1 关掉绕过鉴权的直连端口

这条最容易被忽略,也最要紧。

假设你的站点用 IP:8089 也能打开,那这个入口完全绕过了 OpenResty 和 Authelia——知道 IP 和端口的人可以直接打开站点登录页,Authelia 的防爆破规则根本不参与。

在云厂商安全组里删掉 8080/8089 之类的入方向规则,只保留 22、80、443 和管理面板端口。

不用改容器、不用重启应用。因为安全组只管「从外网进来」的流量,反向代理走的是 127.0.0.1 本机回环,不经过安全组:

1
2
外网访客 ──► 公网网卡:8089 ──► 容器      ← 安全组拦截(要堵的就是这条)
OpenResty ──► 127.0.0.1:8089 ──► 容器 ← 回环,安全组管不着

动手前先确认代理用的是回环地址:

1
grep -rn "proxy_pass" /opt/1panel/www/sites/<站点>/proxy/ 2>/dev/null

只要写的是 127.0.0.1:xxxx,删安全组规则就绝对安全。

12.2 备份

1
2
tar czf /root/authelia-$(date +%F).tar.gz -C /opt authelia/config
ls -lh /root/authelia-*.tar.gz

config/ 里存着 encryption_key 和用户哈希,丢了等于所有会话失效、所有两步验证设备要重新注册。

需要长期保存的三个文件:

文件内容
configuration.yml主配置
users_database.yml用户名 + 密码哈希
db.sqlite3TOTP 密钥、WebAuthn 凭据、登录会话(一次性密码就存在这里,不是一个单独的文件)

十三、设备全丢了的应急复位

密码和账号不受影响,只清两步验证:

1
2
3
4
docker stop authelia
mv /opt/authelia/config/db.sqlite3 /opt/authelia/config/db.sqlite3.bak
docker start authelia; sleep 4
docker logs --tail 5 authelia

Authelia 会重建一个空库。用户名密码在 users_database.yml 里,不会丢,但所有 2FA 设备要重新注册,所有会话要重新登录。


十四、已知的局限

诚实地说,这套方案有两个不足:

① 重启即掉线。 会话存在 Authelia 自己的本地存储里,docker restart 之后所有人都要重新登录。想解决就接 Redis 存 session(内存开销约 10MB),配置里加一个 session.redis 段即可,属于优化项。

② 1Panel 改站点设置会覆盖配置。 每次在 1Panel 里换域名、改端口、开关 HTTPS 之后,跑一遍确认保护还在:

1
curl -sk -o /dev/null -w "%{http_code}\n" https://pan.example.com/

302 正常,200 说明被覆盖了,重新执行第六节。


十五、最终检查清单

  • [ ] https://auth.example.com 能打开登录页并登录成功
  • [ ] 各站点未登录时返回 302 → auth.example.com/?rd=...
  • [ ] 登录后自动跳回原站点,刷新不掉线
  • [ ] 登录一个站点后,其他站点无需再登(SSO 生效)
  • [ ] 8080/8089 等直连端口从外网已无法访问
  • [ ] /admin 等后台路径被拦、前台正常
  • [ ] 两步验证能通过,且至少注册了 2 个设备
  • [ ] 备份已生成

整件事说到底就三块:一个容器 + 一段 nginx 钩子 + 一份用户库。以后再加站点,复制