点击展开更新日志

2026

2026-02-02

只是创建了这篇帖子

2026-03-20

  1. 【新增】系统部署、使用、备份、实践&疑问章节内容

nexttime

会有些什么呢(❁´◡`❁)

源起

承接上文,终于来到了 Authelia 的回合。其实最初我在找SSO的时候,找到的就是 Authelia,甚至都开始看文档准备配置文件了,然后回家之后,搜着搜着就变成了 Authentik,想着干脆就先看看这个吧,于是今天才有时间整理 Authelia 相关内容。

介绍

authelia

Authelia:是一个支持双因素认证的单点登录认证服务器,可以和已有反向代理(Nginx,traefik等)集成添加认证。

Authentik 提供类似的基本认证功能(OIDC、MFA),相较 authentik 最大优势在于极其轻量,几乎没有管理Web后端,但功能性上略显欠缺。

系统功能

  1. 异地组网: 默认添加为专用网络,支持 Windows RDP 访问,几乎没有防火墙限制,之前使用 OpenVPN 默认公用网络改不了,只得弃用;
  2. headscale 后端 + headplane 前端 + traefik 反代 + authelia认证:
    1. headscale 与 headplane 共用域名,//admin 转发至 headplane,其它 API 请求转发给 headscale
    2. GUI 管理
    3. authelia 统一认证,支持 账密 + OTP + Passkey

效果

我的核心需求是办公,因此只要求原画质+60帧,接受偶尔卡顿,在用过的几套方案(todesk、frp、UU、皎月连)中,个人认为完胜。

部署环境

  • 腾讯云轻量:
    • 系统规格:2C2G50G
    • 系统:Ubuntu 24.04 LTS
    • Docker Compose v5.0.2
  • 软件环境
    • 反向代理:traefik
    • headscale、headplane
  • 公网域名:
    • headscale.domain.top:headscale 服务地址及headplane管理地址
    • sso.domain.top:authelia 认证域名

🤔在部署 Authelia 过程中涉及多次对 traefikheadplane配置的修改,本文的目的是为已经正式使用的 headscale+headplane+traefik 接入 OIDC 认证, 因此本文中贴出的是集成后的配置,可能与原配置差异较大,已在实际环境中运行过一段时间,与另一篇 《headscale搭建部署》只是有无SSO的差异,根据需要选用即可。

在继续阅读之前,假设已完成公网域名DNS解析。

然后是一点碎碎念,这套折腾完前前后后大概1-2个月,总算是基本符合预期了,于是匆匆写下这篇文档,但这样的后果就是配置存在很多繁复/不必要的内容,没有经过更进一步的检查测试,在编写的过程中也发现了很多不必要的配置,暂时也不想直接改,也担心随手一删反倒弄巧成拙,因此本文仅起一个抛砖引玉之效,在实际部署过程中有任何疑问/质疑都是正常的,本文只是提供了一个可以满足需求正常运行的配置,能用但不完美。

系统部署

官网:Authelia | Free Open-Source Software Modern IAM Solution

官方的示例配置包含了详细的注释解释,好像是接近2k行,我的建议是依照本文例子先做基本配置,再查阅官网文档了解更详细的配置及含义,根据需要增改。

创建数据目录

我的部署偏好是 /data/docker ,因此以这个目录为例:

1
2
3
4
5
6
7
8
9
10
11
# authelia
mkdir -p /data/docker/authelia/{config,redis}

# redis
mkdir -p /data/docker/authelia/redis/{backups,conf,data,scripts}
chown -R 999:1000 redis/{backups,data}

# headscale 项目目录
mkdir -p /data/docker/headscale/{headscale,headplane,traefik}
mkdir /data/docker/headscale/headscale/config

准备配置

DOCKER_ROOT = /data/docker

config.yaml

<DOCKER_ROOT>/headscale/headscale/config/config.yaml

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
---
server_url: https://headscale.domain.top
listen_addr: 0.0.0.0:8080
metrics_listen_addr: 0.0.0.0:9090
grpc_listen_addr: 127.0.0.1:50443
grpc_allow_insecure: false

noise:
private_key_path: /var/lib/headscale/noise_private.key

prefixes:
v4: 100.64.0.0/10
v6: fd7a:115c:a1e0::/48
allocation: sequential

derp:
server:
enabled: true
region_id: 999
region_code: "headscale"
region_name: "Headscale Embedded DERP"
verify_clients: true
stun_listen_addr: "0.0.0.0:3478"
private_key_path: /var/lib/headscale/derp_server_private.key
automatically_add_embedded_derp_region: true
ipv4: <SERVER_IPV4>
ipv6: <SERVER_IPV6>
urls:
- https://controlplane.tailscale.com/derpmap/default
paths: []
auto_update_enabled: true
update_frequency: 3h

disable_check_updates: false

ephemeral_node_inactivity_timeout: 30m

database:
type: sqlite
debug: false
gorm:
prepare_stmt: true
parameterized_queries: true
skip_err_record_not_found: true
slow_threshold: 1000

# SQLite config
sqlite:
path: /var/lib/headscale/db.sqlite
write_ahead_log: true
wal_autocheckpoint: 1000

acme_url: https://acme-v02.api.letsencrypt.org/directory

acme_email: ""

tls_letsencrypt_hostname: ""

tls_letsencrypt_cache_dir: /var/lib/headscale/cache

tls_letsencrypt_challenge_type: HTTP-01

tls_letsencrypt_listen: ":http"

tls_cert_path: ""
tls_key_path: ""

log:
level: info
format: text

policy:
mode: database

dns:
magic_dns: true
# 客户端域名路径,启用 magicDNS 之后会为每个客户端分配域名:hostname.clients.domain.top,可以直接域名访问 client,不需要记 ip
base_domain: clients.domain.top
override_local_dns: true
nameservers:
global:
- 8.8.8.8
- 8.8.4.4
- 2001:4860:4860::8888
- 2001:4860:4860::8844
# 设置 splitDNS 可以为指定域名设置 DNS 服务器,适用于内网组网后使用内网 DNS 解析内网域名。
# 192.168.1.26 是我的内网 DNS 地址
split: { domain.top: [ 192.168.1.26 ] }
search_domains:
- domain.top
extra_records: []

unix_socket: /var/run/headscale/headscale.sock
unix_socket_permission: "0770"

oidc:
only_start_if_oidc_is_available: true
issuer: "https://sso.domain.top"
client_id: "headplane"
client_secret: "<CLIENT_SECRET>"
scope: ["openid", "profile", "email", "groups"]


pkce:
enabled: true
method: 'S256'
logtail:.
enabled: false
randomize_client_port: false

taildrop:
enabled: true

device_expiry: 90d
  • 关于几个 listen_addr 是否建议设置为 127.0.0.1 :不建议,因为网络方式全部为桥接且不对外暴露端口,因此设置 0.0.0.0 只是方便其它容器访问,并不会对互联网开放,况且我们防火墙也没有开放。
  • derp.server.ipv4/6 :设置为公网 IPv4/6地址,中继连接地址(不过目前还没遇到这种情况所以也不知道到底有用没)
  • dns.nameservers.split :如果不需要可以直接删除,可以从 Web UI 进行添加
  • client_secret :这里的值是下面 authelia client_secret 生成时的明文密码。

config.yaml

<DOCKER_ROOT>/headscale/headplane/config.yaml

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
server:
host: "0.0.0.0"
port: 3000
base_url: "https://headscale.domain.top"
cookie_secret: "<COOKIE_SECRET>"
cookie_secure: true
cookie_max_age: 86400 # 1 day in seconds

cookie_domain: "headscale.domain.top"
data_path: "/var/lib/headplane"


headscale:
url: "http://headscale:8080"
public_url: "https://headscale.domain.top"
config_path: "/etc/headscale/config.yaml"
config_strict: true

integration:
agent:
enabled: false
pre_authkey: "<your-preauth-key>"

docker:
enabled: false
container_label: "me.tale.headplane.target=headscale"
socket: "unix:///var/run/docker.sock"

kubernetes:
enabled: false
validate_manifest: true
pod_name: "headscale"

proc:
enabled: false

oidc:
enabled: true
issuer: "https://sso.domain.top"
headscale_api_key: "<API Key>"
token_endpoint_auth_method: "client_secret_basic"
client_id: "headplane"
client_secret: "<CLIENT_SECRET>"
use_pkce: true
  • cookie_secret :32位随机字符串,随便找个工具生成:

    1
    openssl rand -hex 16
  • oidc.headscale_api_key :headscale 容器生成:

    1
    2
    3
    4
    docker exec headscale headscale apikeys create

    # 查看已生成key
    docker exec headscale headscale apikeys list
  • oidc.client_secret :和 headscale 用一样的 client_id 及 client_secret 以保证认证通过。

traefik.yml

<DOCKER_ROOT>/headscale/traefik/traefik.yml

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
global:
checkNewVersion: false
sendAnonymousUsage: false


# Traefik 静态配置
api:
dashboard: true
debug: false

entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
scheme: https
permanent: true
websecure:
address: ":443"
http:
tls: {}

certificatesResolvers:
letsencrypt:
acme:
email: violetsiki@163.com # 替换为你的邮箱
storage: /etc/traefik/acme/acme.json
httpChallenge:
entryPoint: web
caServer: https://acme-staging-v02.api.letsencrypt.org/directory

log:
level: INFO
format: common

accessLog:
format: common # 可选: common, json

providers:
docker:
endpoint: "unix:///var/run/docker.sock"
exposedByDefault: false
network: headscale
file:
directory: /etc/traefik/dynamic
watch: true

authelia.yml

<DOCKER_ROOT>/headscale/traefik/dynamic/authelia.yml

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
http:
# ==========================
# Routers & Services(Authelia 自身)
# ==========================
routers:
# Authelia 登录门户
authelia:
rule: "Host(`sso.domain.top`)"
entryPoints:
- websecure
service: authelia-service
tls:
certResolver: letsencrypt # 或你的解析器名称
# Authelia 自身不需要 Authelia 认证(防止循环)
middlewares:
- security-headers


services:
authelia-service:
loadBalancer:
servers:
- url: "http://authelia:9091"
healthCheck:
path: /api/health
interval: 10s
timeout: 5s

certificates.yml

<DOCKER_ROOT>/headscale/traefik/dynamic/certificates.yml

1
2
3
4
5
6
7
8
9
10
11
12
tls:
certificates:
- certFile: /certs/domain.top.crt
keyFile: /certs/domain.top.key
stores:
- default

stores:
default:
defaultCertificate:
certFile: /certs/domain.top.crt
keyFile: /certs/domain.top.key

不知道配置的 acme 生效没,之前在其它的地方申请了泛域名证书,直接就用了还没过期。

middlewares.yml

<DOCKER_ROOT>/headscale/traefik/dynamic/middlewares.yml

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
http:
# ==========================
# Middlewares(通用)
# ==========================
middlewares:
# Authelia 标准认证(浏览器重定向)
authelia:
forwardAuth:
address: "http://authelia:9091/api/authz/forward-auth"
trustForwardHeader: true
authRequestHeaders:
- "Accept"
- "Authorization"
- "Cookie" # 关键:会话 Cookie
- "X-Forwarded-For" # 原始 IP
- "X-Forwarded-Host" # 关键:原始域名(traefik.evergardenviolet.top)
- "X-Forwarded-Method" # 关键:原始方法(GET/HEAD/POST)
- "X-Forwarded-Proto" # 关键:原始协议(https)
- "X-Forwarded-Uri" # 关键:原始路径(/)
authResponseHeaders:
- "LOCATION"
- Remote-User
- Remote-Groups
- Remote-Name
- Remote-Email
# 超时设置(Authelia 查询数据库可能较慢)
forwardBody: false
maxBodySize: 1048576 # 1MB

# Authelia 基础认证(API/返回 401 而非 302)
authelia-basic:
forwardAuth:
address: "http://authelia:9091/api/authz/forward-auth?auth=basic"
trustForwardHeader: true
authResponseHeaders:
- Remote-User
- Remote-Groups
- Remote-Name
- Remote-Email
# 安全头部(最常用)
security-headers:
headers:
browserXSSFilter: true
contentTypeNosniff: true
forceSTSHeader: true
stsIncludeSubdomains: true
stsPreload: true
stsSeconds: 31536000
frameDeny: true
sslRedirect: true
contentSecurityPolicy: "default-src 'self'; img-src 'self' data:; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"

# CORS 跨域
cors:
headers:
accessControlAllowMethods:
- HEAD
- GET
- OPTIONS
- PUT
- POST
- DELETE
accessControlAllowHeaders:
- Authorization
- Content-Type
- Accept
- Origin
- X-Requested-With
accessControlAllowOriginList:
- "https://headscale.domain.top"
- "https://*.domain.top"
accessControlMaxAge: 86400
addVaryHeader: true

# WebSocket 支持
websocket:
headers:
customRequestHeaders:
X-Forwarded-Proto: "https"

# 组合中间件:Authelia + 安全头部
secured:
chain:
middlewares:
- authelia
- security-headers

# 仅安全头部(用于 Authelia 自身)
secured-basic:
chain:
middlewares:
- security-headers

# 重定向
headscale-redirect:
redirectRegex:
regex: "^/$"
replacement: "/admin"
permanent: true

redis.conf

<DOCKER_ROOT>/authelia/redis/conf/redis.conf

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# 网络绑定(监听所有接口,但在 Docker 网络内隔离)
bind 0.0.0.0
port 6379
tcp-backlog 511

# 超时设置(防止空闲连接占用)
timeout 300
tcp-keepalive 300

# 守护进程(前台运行配合 Docker)
daemonize no
supervised no

# 进程文件(不使用,避免权限问题)
pidfile ""

# 日志配置(审计)
loglevel notice
logfile ""

# 数据库数量(通常 1 个足够)
databases 1

# 持久化配置(AOF 优先,RDB 备用)
save 900 1
save 300 10
save 60 10000

stop-writes-on-bgsave-error yes
rdbcompression yes
rdbchecksum yes
dbfilename dump.rdb
dir /data

# AOF 配置(高安全性推荐)
appendonly yes
appendfilename "appendonly.aof"
appendfsync everysec
no-appendfsync-on-rewrite no
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

# 内存管理(关键:防止内存耗尽)
maxmemory 400mb
maxmemory-policy allkeys-lru
maxmemory-samples 5

# 客户端连接限制
maxclients 10000

# 安全:禁用危险命令(重命名为空即禁用)
rename-command FLUSHALL ""
rename-command FLUSHDB ""
rename-command KEYS ""
rename-command DEBUG ""
rename-command CONFIG "CONFIG_9f8s7d9f8s7d"
rename-command SHUTDOWN "SHUTDOWN_9f8s7d9f8s7d"

# 安全:仅允许本地连接(Docker 网络内)
protected-mode yes

# 慢查询日志(安全审计)
slowlog-log-slower-than 10000
slowlog-max-len 128

# 延迟监控
latency-monitor-threshold 100

# 事件通知(可选)
notify-keyspace-events ""

# 哈希优化
hash-max-ziplist-entries 512
hash-max-ziplist-value 64

# 列表优化
list-max-ziplist-size -2
list-compress-depth 0

# 集合优化
set-max-intset-entries 512

# 有序集合优化
zset-max-ziplist-entries 128
zset-max-ziplist-value 64

# HyperLogLog 优化
hll-sparse-max-bytes 3000

# 流优化
stream-node-max-bytes 4096
stream-node-max-entries 100

# 主动重哈希
activerehashing yes

# 客户端输出缓冲区限制
client-output-buffer-limit normal 0 0 0
client-output-buffer-limit replica 256mb 64mb 60
client-output-buffer-limit pubsub 32mb 8mb 60

# 内部配置
hz 10
dynamic-hz yes

# 重写时是否启用 AOF
aof-rewrite-incremental-fsync yes
rdb-save-incremental-fsync yes

configuration.yml

<DOCKER_ROOT>/authelia/config/configuration.yml

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
---
theme: dark
server:
address: 'tcp://0.0.0.0:9091'

log:
level: info
format: json

identity_validation:
reset_password:
jwt_secret: <JWT_SECRET>

# 用户认证后端,使用本地文件 users_database.yml
authentication_backend:
password_change:
disable: false
password_reset:
disable: false
file:
path: /config/users_database.yml
watch: true
search:
email: true
password:
algorithm: argon2
argon2:
variant: argon2id

# 访问控制
access_control:
default_policy: deny
rules:
- domain: traefik.domain.top
policy: two_factor
subject:
- group:admins
- domain: headscale.domain.top
policy: bypass
resources:
- '^/admin/oidc/callback.*$' # OIDC 回调路径必须免认证
- '/ts2021'
- '/derp'
- '/health'
- '/key'
- '/register' # 设备认证需要调用这个接口
- domain: headscale.domain.top
policy: two_factor
subject:
- group:admins
# ============================================
# 1. Authelia 自身的公开访问路径
# ============================================
- domain: sso.domain.top
policy: bypass
resources:
# 登录页面 - 必须公开
- '^/$' # 根路径(登录页)
- '^/login($|/.*$)' # 登录页面
- '^/logout($|/.*$)' # 登出页面

# 认证 API - 必须公开(供其他服务验证)
- '^/api/verify($|/.*$)' # Traefik forwardAuth 端点
- '^/api/health($|/.*$)' # 健康检查

# OAuth/OIDC 端点 - 必须公开
- '^/api/oidc($|/.*$)' # OIDC 发现端点
- '^/api/oidc/authorization($|/.*$)' # 授权端点
- '^/api/oidc/token($|/.*$)' # Token 端点
- '^/api/oidc/userinfo($|/.*$)' # 用户信息端点
- '^/api/oidc/jwks($|/.*$)' # JWKS 端点
- '^/api/oidc/revocation($|/.*$)' # Token 撤销端点

# 静态资源
- '^/static($|/.*$)' # 静态文件
- '^/favicon.ico$' # 图标
- '^/manifest.json$' # PWA 清单
- '^/robots.txt$' # 爬虫协议

# WebAuthn 相关(如果启用)
- '^/api/webauthn($|/.*$)' # WebAuthn 注册/验证

# 密码重置/双因素设置(根据需求选择是否公开)
# - '^/api/reset-password($|/.*$)' # 密码重置
# - '^/api/secondfactor($|/.*$)' # 双因素设置

# ============================================
# 2. Authelia 管理界面 - 需要高级别认证
# ============================================
- domain: sso.domain.top
policy: two_factor # 建议双因素认证
resources:
- '^/admin($|/.*$)' # 管理界面
- '^/settings($|/.*$)' # 用户设置页面
- '^/api/user/info($|/.*$)' # 用户信息API
- '^/api/user/session($|/.*$)' # 会话管理API
subject:
- group:admins # 限制管理员组

# ============================================
# 3. 用户设置和配置文件 - 需要单因素认证
# ============================================
- domain: sso.domain.top
policy: two_factor
resources:
- '^/profile($|/.*$)' # 用户资料页面
- '^/api/user/profile($|/.*$)' # 用户资料API
- '^/api/user/preferences($|/.*$)' # 用户偏好设置

# ============================================
# 4. 兜底规则 - 其他所有路径需要认证
# ============================================
- domain: sso.domain.top
policy: two_factor
resources:
- '^/.*$' # 所有其他路径

session:
secret: <SESSION_SECRET>
name: 'authelia_session'
same_site: 'lax'
inactivity: '5m'
expiration: '1h'
remember_me: '1M'
cookies:
- name: 'authelia_session'
domain: 'domain.top'
authelia_url: 'https://sso.domain.top'
same_site: 'lax'
inactivity: '5m'
expiration: '1h'
remember_me: '1h'

# redis 配置
redis:
host: secure-redis # redis 容器名
port: 6379
timeout: 5 seconds
max_retries: 3
password: <REDIS_PASSWORD>
database_index: 0

# 数据库存储及加密
storage:
encryption_key: <ENCRYPTION_KEY>
local:
path: /config/db.sqlite3

identity_providers:
oidc:
hmac_secret: <HMAC_SECRET>
jwks:
- key_id: authelia_rs256
algorithm: RS256
use: sig
# 删除所有注释和多余引号,保持严格的 YAML 多行格式
key: |
-----BEGIN PRIVATE KEY-----
MIIE...
-----END PRIVATE KEY-----

clients:
- client_id: 'headplane'
claims_policy: 'headplane'
client_name: 'Headscale Admin Panel'
client_secret: '$pbkdf2-sha512xxx'
public: false
redirect_uris:
# headscale callback
- "https://headscale.domain.top/oidc/callback"
# headplane callback
- "https://headscale.domain.top/admin/oidc/callback"
scopes:
- 'openid'
- 'email'
- 'profile'
- 'groups'
grant_types:
- 'authorization_code'
response_types:
- 'code'
authorization_policy: 'two_factor'
require_pkce: true
pkce_challenge_method: 'S256'
access_token_signed_response_alg: 'none'
userinfo_signed_response_alg: 'none'
token_endpoint_auth_method: 'client_secret_basic'
claims_policies:
headplane:
id_token:
- email
- groups
- preferred_username



totp:
disable: false
issuer: 'sso.domain.top'
algorithm: 'sha512'
digits: 6
period: 30
skew: 1
secret_size: 32
allowed_algorithms:
- 'SHA256'
- 'SHA512'
allowed_digits:
- 6
allowed_periods:
- 30
disable_reuse_security_policy: false

webauthn:
disable: false
enable_passkey_login: false
display_name: 'Authelia'
attestation_conveyance_preference: 'indirect'
timeout: '60 seconds'
filtering:
permitted_aaguids: []
prohibited_aaguids: []
prohibit_backup_eligibility: false
selection_criteria:
attachment: ''
discoverability: 'preferred'
user_verification: 'preferred'
metadata:
enabled: false
validate_trust_anchor: true
validate_entry: true
validate_entry_permit_zero_aaguid: false
validate_status: true
validate_status_permitted: []
validate_status_prohibited:
- 'REVOKED'
- 'USER_KEY_PHYSICAL_COMPROMISE'
- 'USER_KEY_REMOTE_COMPROMISE'
- 'USER_VERIFICATION_BYPASS'
- 'ATTESTATION_KEY_COMPROMISE'


notifier:
disable_startup_check: false
template_path: ''
# filesystem: {}
smtp:
address: 'submissions://smtp.qiye.aliyun.com:465'
timeout: '5s'
username: 'notice@domain.top'
password: '<PASSWORD>'
sender: "Authelia <Notice@domain.top>"
identifier: 'authelia'
subject: "[Authelia] {title}"
startup_check_address: '12345678@qq.com'
disable_require_tls: false
disable_starttls: false
disable_html_emails: false

需要更新的内容:

users_database.yml

authelia 用户管理,有条件更建议使用数据库,只是我懒得弄,就我一个用户专门再部署一套没太大必要。

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
---
###############################################################
# Users Database #
###############################################################

# This file can be used if you do not have an LDAP set up.

users:
authelia:
disabled: true
displayname: "Test User"
password: "$argon2id$v=19$m=32768,t=1,p=8$eUhVT1dQa082YVk2VUhDMQ$E8QI4jHbUBt3EdsU1NFDu4Bq5jObKNx7nBKSn1EYQxk" # Password is 'authelia'
email: authelia@authelia.com
groups:
- admins
- dev
violet: # 登录用户名
disabled: false
displayname: "Violet" # 显示用户名
password: "$argon2idxxx" #
email: 'violet@domain.top' # 邮箱
groups: # 管理员组
- admins

...
  • 密码生成:
    1
    2
    # docker
    docker run --rm authelia/authelia:latest authelia crypto hash generate argon2 --password 'password'

如果需要额外的用户,参考以上格式新增重启即可。

.env

<DOCKER_ROOT>/authelia/redis/.env

1
2
3
4
5
6
7
8
9
10
# Redis 安全配置
REDIS_PASSWORD=<REDIS_PASSWORD>

# 时区
TZ=Asia/Shanghai

# 备份保留天数
BACKUP_RETENTION_DAYS=7
BACKUP_INTERVAL=3600
BACKUP_RETENTION_DAYS=7

scripts

<DOCKER_ROOT>/authelia/redis/scripts

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
# scripts/backup.sh
#!/bin/sh
set -e

BACKUP_DIR="/backups"
DATE=$(date +%Y%m%d_%H%M%S)
RETENTION_DAYS="${BACKUP_RETENTION_DAYS:-7}"

echo "[$(date)] Starting Redis backup..."

# 触发 BGSAVE(异步保存)
redis-cli -h "${REDIS_HOST}" -a "${REDIS_PASSWORD}" BGSAVE

# 等待保存完成(最多等待 30 秒)
sleep 2
for i in $(seq 1 30); do
if redis-cli -h "${REDIS_HOST}" -a "${REDIS_PASSWORD}" INFO Persistence | grep -q "rdb_bgsave_in_progress:0"; then
break
fi
echo "[$(date)] Waiting for BGSAVE to complete..."
sleep 1
done

# 复制 RDB 文件
if [ -f /data/dump.rdb ]; then
cp /data/dump.rdb "${BACKUP_DIR}/redis_backup_${DATE}.rdb"
gzip -f "${BACKUP_DIR}/redis_backup_${DATE}.rdb"
echo "[$(date)] Backup created: redis_backup_${DATE}.rdb.gz"
else
# 如果数据目录挂载方式不同,使用 redis-cli 导出
redis-cli -h "${REDIS_HOST}" -a "${REDIS_PASSWORD}" --rdb "${BACKUP_DIR}/redis_backup_${DATE}.rdb"
gzip -f "${BACKUP_DIR}/redis_backup_${DATE}.rdb"
echo "[$(date)] Backup created via CLI: redis_backup_${DATE}.rdb.gz"
fi

# 清理旧备份
find "${BACKUP_DIR}" -name "redis_backup_*.rdb.gz" -mtime +${RETENTION_DAYS} -delete

echo "[$(date)] Backup completed"
1
2
3
4
5
# scripts/entrypoint.sh
#!/bin/sh
apk add --no-cache dcron
echo "0 * * * * sh /scripts/backup.sh" | crontab -
crond -f

compose 文件

Redis

(先叠个甲)redis直接随便跑一个就行,和 authelia 一个虚拟网能够访问即可,以下仅供参考。

至于为什么一个 redis 搞这么复杂,只是因为多问了 Kimi 一句,然后看了眼也懒得改,干脆就用了,原本打算和 authelia 整合一起的,但看到这个就放弃了,就这样吧,等服务器到期迁移了再做打算。

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
networks:
# 内部网络,完全隔离(关键安全特性)
cache_internal:
name: redis_internal
driver: bridge
internal: true
ipam:
config:
- subnet: 172.21.0.0/16

# 允许其他服务通过此网络连接(如 Authelia)
backend:
name: "redis_net"
driver: bridge

services:
redis:
image: redis:7-alpine
container_name: secure-redis
restart: unless-stopped

# 安全:以非 root 运行(redis UID 999)
user: "999:1000"

# 安全:禁用特权提升
security_opt:
- no-new-privileges:true

# 安全:能力降权(仅需网络绑定能力)
cap_drop:
- ALL
cap_add:
- SETGID
- SETUID

# 关键:仅 expose 给容器网络,不映射宿主机端口
expose:
- "6379"

command: >
redis-server /usr/local/etc/redis/redis.conf
--requirepass ${REDIS_PASSWORD}
--masterauth ${REDIS_PASSWORD}

volumes:
# 数据持久化
- ./data:/data

# 只读配置文件
- ./conf/redis.conf:/usr/local/etc/redis/redis.conf:ro

# 备份目录(可选)
- ./backups:/backups

# 自定义脚本(如备份脚本)
- ./scripts:/scripts:ro

environment:
- TZ=Asia/Shanghai
- REDIS_PASSWORD=${REDIS_PASSWORD}

networks:
- cache_internal
- backend # 其他服务通过此网络访问

healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
interval: 10s
timeout: 3s
retries: 5
start_period: 30s

# 资源限制(防止缓存穿透导致内存耗尽)
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M
reservations:
cpus: '0.1'
memory: 64M

sysctls:
# 优化 TCP 连接
- net.core.somaxconn=65535

ulimits:
# 文件描述符限制
nofile:
soft: 65536
hard: 65536

# 可选:Redis 备份服务(定期 RDB 备份)
redis-backup:
image: redis:7-alpine
container_name: redis-backup
restart: unless-stopped
user: "999:1000"
environment:
- REDIS_HOST=secure-redis
- REDIS_PASSWORD=${REDIS_PASSWORD}
- BACKUP_INTERVAL=${BACKUP_INTERVAL:-3600}
- BACKUP_RETENTION_DAYS=${BACKUP_RETENTION_DAYS:-7}
volumes:
- ./backups:/backups
- ./scripts/backup.sh:/backup.sh:ro
command: >
sh -c "
echo '[Backup] Service started, interval: ${BACKUP_INTERVAL}s'
while true; do
echo '[Backup] Starting backup at $$(date)'
sh /backup.sh
echo '[Backup] Next backup in ${BACKUP_INTERVAL}s'
sleep ${BACKUP_INTERVAL}
done
"
networks:
- cache_internal

Authelia

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
# 这部分没用上,可以删,直接写到配置文件了
secrets:
JWT_SECRET:
file: './secrets/JWT_SECRET'
SESSION_SECRET:
file: './secrets/SESSION_SECRET'
STORAGE_PASSWORD:
file: './secrets/STORAGE_PASSWORD'
STORAGE_ENCRYPTION_KEY:
file: './secrets/STORAGE_ENCRYPTION_KEY'

services:
authelia:
container_name: 'authelia'
image: 'authelia/authelia:latest'
restart: 'unless-stopped'
ports:
- 9091:9091 # 仅内部访问,可以选择删掉,当时为便于调试没删,不过防火墙没开倒也无伤大雅
security_opt:
- no-new-privileges:true
environment:
TZ: 'Asia/Shanghai'
X_AUTHELIA_CONFIG_FILTERS: template
networks:
- net
- redis
volumes:
- './config:/config' # 要写数据库,不能ro
healthcheck:
test: ["CMD", "authelia", "healthcheck"]
interval: 30s
timeout: 3s
retries: 3
start_period: 60s
labels:
- "traefik.enable=true"
# 路由规则(对应你之前的 authelia router)
- "traefik.http.routers.authelia.rule=Host(`sso.domain.top`)"
- "traefik.http.routers.authelia.entrypoints=websecure"
- "traefik.http.routers.authelia.tls.certresolver=letsencrypt"

# 引用 file 中定义的中间件(关键语法:@file)
- "traefik.http.routers.authelia.middlewares=security-headers@file"

# 服务端口(Authelia 内部端口)
- "traefik.http.services.authelia.loadbalancer.server.port=9091"
- "traefik.http.middlewares.authelia-headers.headers.browserXSSFilter=true"
- "traefik.http.middlewares.authelia-headers.headers.contentTypeNosniff=true"
- "traefik.http.middlewares.authelia-headers.headers.frameDeny=true"
- "traefik.http.middlewares.authelia-headers.headers.referrerPolicy=strict-origin-when-cross-origin"
- "traefik.http.routers.authelia.middlewares=authelia-headers"

networks:
net:
name: 'authelia'
redis:
name: 'redis_net'
external: true

headscale+headplane+traefik

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
services:
headplane:
image: ghcr.io/tale/headplane:latest
container_name: headplane
restart: unless-stopped
environment:
- TZ=Asia/Shanghai
# - HEADPLANE_DEBUG_LOG=true
volumes:
- './headplane/config.yaml:/etc/headplane/config.yaml'
- './headplane/data:/var/lib/headplane'
- './headscale/config/config.yaml:/etc/headscale/config.yaml'
- '/var/run/docker.sock:/var/run/docker.sock:ro'
labels:
- 'traefik.enable=true'
- 'traefik.http.routers.headplane.rule=Host(`headscale.domain.top`) && PathPrefix(`/admin`)'
- 'traefik.http.routers.headplane.entrypoints=websecure'
- 'traefik.http.routers.headplane.tls=true'
- 'traefik.http.routers.headplane.priority=100'
- "traefik.http.routers.headplane.tls.certresolver=letsencrypt"
- "traefik.http.routers.headplane.middlewares=secured@file"
- 'traefik.http.routers.headplane.service=headplane-service'
- 'traefik.http.services.headplane-service.loadbalancer.server.port=3000'

- 'traefik.http.routers.headplane-root.rule=Host(`headscale.domain.top`) && Path(`/`)'
- 'traefik.http.routers.headplane-root.entrypoints=websecure'
- 'traefik.http.routers.headplane-root.tls=true'
- 'traefik.http.routers.headplane-root.priority=110' # 最高优先级
- "traefik.http.routers.headplane-root.tls.certresolver=letsencrypt"
- "traefik.http.routers.headplane-root.middlewares=secured@file,redirect-root"
- 'traefik.http.middlewares.redirect-root.redirectregex.regex=^https://headscale.domain.top/?$$'
- 'traefik.http.middlewares.redirect-root.redirectregex.replacement=https://headscale.domain.top/admin'
- 'traefik.http.middlewares.redirect-root.redirectregex.permanent=true'
networks:
- headscale

headscale:
image: headscale/headscale:latest
container_name: headscale
restart: unless-stopped
command: serve
environment:
- TZ=Asia/Shanghai
ports:
# - '8080:8080' # HTTP API和 gRPC API 端口
- '3478:3478/udp' # DERP 辅助打洞
# - '9090:9090' # Prometheus 指标监控
volumes:
- './headscale/config:/etc/headscale'
- './headscale/lib:/var/lib/headscale'
- './headscale/run:/var/run/headscale'
healthcheck:
test: ["CMD", "headscale", "health"]
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
labels:
# headplane 集成
- 'me.tale.headplane.target=headscale'

- 'traefik.enable=true'
- 'traefik.http.routers.headscale.rule=Host(`headscale.domain.top`) && !PathPrefix(`/admin`)'
- 'traefik.http.routers.headscale.entrypoints=websecure'
- 'traefik.http.routers.headscale.tls=true'
- 'traefik.http.routers.headscale.priority=50'
- "traefik.http.routers.headscale.tls.certresolver=letsencrypt"
- "traefik.http.routers.headscale.middlewares=secured@file,cors@file"
# 服务定义
- 'traefik.http.routers.headscale.service=headscale-service'
- 'traefik.http.services.headscale-service.loadbalancer.server.port=8080'

- 'traefik.http.middlewares.cors.headers.accesscontrolallowheaders=*'
- 'traefik.http.middlewares.cors.headers.accesscontrolallowmethods=GET,POST,PUT'
- 'traefik.http.middlewares.cors.headers.accesscontrolalloworiginlist=https://headscale.domain.top'
- 'traefik.http.middlewares.cors.headers.accesscontrolmaxage=100'
- 'traefik.http.middlewares.cors.headers.addvaryheader=true'

networks:
- headscale

traefik:
image: traefik:latest
container_name: traefik
restart: unless-stopped
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
ports:
- '80:80'
- '443:443'
# - '8080'
volumes:
- './traefik/traefik.yml:/etc/traefik/traefik.yml'
- './traefik/dynamic:/etc/traefik/dynamic:ro'
- '/var/run/docker.sock:/var/run/docker.sock:ro'
- './traefik/certs:/certs'
- './traefik/acme:/etc/traefik/acme'
- '/etc/localtime:/etc/localtime:ro'
- './logs:/logs'
command:
- '--configFile=/etc/traefik/traefik.yml'
labels:
- "traefik.enable=true"
- "traefik.http.routers.traefik.rule=Host(`traefik.domain.top`)"
- "traefik.http.routers.traefik.entrypoints=websecure"
- "traefik.http.routers.traefik.service=api@internal"
- "traefik.http.routers.traefik.tls=true"
- "traefik.http.routers.traefik.tls.certresolver=letsencrypt"
- "traefik.http.routers.traefik.middlewares=secured@file"
networks:
- headscale
- authelia
networks:
headscale:
name: headscale
authelia:
name: authelia
external: true

启动服务

  1. 启动 Redis
  2. 启动 authelia
  3. 启动 headscale 服务组
1
2
3
4
5

docker compose up -d

# 检查启动日志:请务必检查!
docker compose logs -f

使用

新增客户端

Windows

  1. 从官方 下载 客户端安装并启动;

  2. 打开【终端】,输入命令进行认证:

    1
    2
    3
    4
    > tailscale up --login-server=https://headscale.domain.top --accept-dns=false --accept-routes=false
    To authenticate, visit:

    https://headscale.domain.top/register/abcX_defS-hwx
    • --login-server :连接自建 headscale 服务器地址
    • -accept-dns=false :不使用 headscale 的magicDNS 覆盖本地 DNS。这玩意看情况:
      • 内网设备:不建议开,使用内网 DNS 即可
      • 外网设备:建议打开
    • --accept-routes=false :不使用其它客户端声明的子网路由,同上:
      • 内网设备:不建议开,不然能直连的地址也要走 tailscale 网卡绕一圈,容易出问题
      • 外网设备:建议,不就是为了访问内网么
  3. 打开以上认证地址,输入 authelia 用户进行认证,认证成功如下:

    image-20260320210915871

Linux

  1. 使用官方一键脚本安装,会自动根据系统版本安装仓库源,并配置 systemd 服务:

    1
    curl -fsSL https://tailscale.com/install.sh | sh
  2. (可选)开启IP转发(如果需要配置子网路由才需要):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 临时开启
    sysctl -w net.ipv4.ip_forward=1

    # 永久
    echo "net.ipv4.ip_forward=1" >> /etc/sysctl.conf

    # 如果需要开启 IPv6
    sysctl -w net.ipv6.conf.all.forwarding=1
    echo "net.ipv6.conf.all.forwarding=1" >> /etc/sysctl.conf
  3. 认证客户端:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    > tailscale up --login-server=https://headscale.domain.top --accept-dns=false --accept-routes=false
    To authenticate, visit:

    https://headscale.domain.top/register/abcX_defS-hwx

    # 如果需要开启子网路由,网段改成你的内网地址:
    > tailscale up --login-server=https://headscale.domain.top --accept-dns=false --advertise-routes=192.168.1.0/24
    To authenticate, visit:

    https://headscale.domain.top/register/abcX_defS-hwx
  4. 打开以上认证地址,输入 authelia 用户进行认证。

  5. (可选)如果开启了子网路由,需要到管理页面手动允许才能使用,开启后就可以在 tailnet 客户端 ping 通内网地址了。

Android

从 Google Play 下载客户端,右上角用户进入,点击右上角三个点点展开,选择【Use an alternate server】输入自定义服务器地址:https://headscale.domain.top ,然后选择登录,会弹出浏览器登录。

开启 TOTP

  1. 打开 authelia 登录地址:https://sso.domain.top 登录;

  2. 登录之后从右上角进入设置选择【两步验证】:

    image-20260320220513121

    算法选择 SHA256 及以上(不过我的配置最低就是 SHA256,想选弱算法也没有机会呢),位数 6 位。

  3. 开启 OTP,如果手机支持,建议开启 WebAuthn (Passkey)直接不用记密码了(笨蛋Flyme不支持)。

使用 splitDNS

功能:使用指定的 DNS 解析指定的域名。

场景:加入 tailsnet 的设备可以直接使用内网域名访问系统,最主要的需求是 immich 备份和 vaultwarden 。其实在上面的 headscale 配置中已经加了,如果要从 headplane Web 页面添加,需要到 【DNS】页面下,添加 nameservers 的时候勾选【splitDNS】。

备份

请务必做好定期备份,我的备份方案是:

  1. 轻量服务器本地使用 restic 每周备份,开启 sftp;
  2. 通过 sftp 每周定期将 restic 仓库使用 rclone 备份到本地主机。

实践&疑问

实践

如果需要使用内网 AdguardHome 为子网路由提供 DNS,建议在单独的 Linux 服务器(可以是虚拟机)部署一套 tailscale 客户端,1c512M5G就够了,补充说明我的环境需求:

  1. 计算服务器底层系统是 PVE,专门启了个 LXC 容器跑 Docker,当时试过容器部署 tailscale 客户端,但是需要开启特权容器/挂载 tun 设备,权衡之后还是放弃了容器部署方式;
  2. AdgurdHome 部署方式为 Docker 容器,网络模式为桥接,内网DNS,子网路由组网之后需要广播给客户端解析内网域名,加上使用 openwrt + openclash 组合,且几乎没有关过,tailscale 安装到 openwrt 上会导致 openclash 劫持 DNS;
  3. 子网路由其它部署在哪里都可以满足,只是因为以上原因最终单独启了一台部署。

调试尽量每次开一个无痕模式测试,因为缓存、cookie会有影响,产生奇奇怪怪的bug而且难以觉察。

疑问

Q:为什么不在 openwrt 上安装 tailscale?

A:开启 openclash 后,以我当前配置会劫持网卡 DNS 查询请求,根本到不了 AdGuardHome。

Q:Linux 安装 tailscale 之后启动显示 running,但是命令无法使用?
A:使用 systemctl status headscaled.service 查看报错为 TUN 设备不可用,可能是部署在了 PVE LXC 容器,默认不提供 TUN 设备,解决:

  • 宿主机开启 /dev/net/tun 设备挂载(未测试)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    vim /etc/pve/lxc/<LXC_ID>.conf

    # 新增以下配置
    lxc.cgroup2.devices.allow: c 10:200 rwm
    lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file

    # 重启容器
    pct restart <LXC_ID>

    # 进入容器验证是否存在 /dev/net/tun 设备
  • 使用 privileged 容器(未测试)

    1
    2
    3
    4
    vim /etc/pve/lxc/<LXC_ID>.conf

    # 修改
    privileged: 1
  • 使用 VM 替代 LXC 部署(推荐)