点击展开更新日志
源起
承接上文,终于来到了 Authelia 的回合。其实最初我在找SSO的时候,找到的就是 Authelia,甚至都开始看文档准备配置文件了,然后回家之后,搜着搜着就变成了 Authentik,想着干脆就先看看这个吧,于是今天才有时间整理 Authelia 相关内容。
介绍
authelia
Authelia:是一个支持双因素认证的单点登录认证服务器,可以和已有反向代理(Nginx,traefik等)集成添加认证。
与 Authentik 提供类似的基本认证功能(OIDC、MFA),相较 authentik 最大优势在于极其轻量,几乎没有管理Web后端,但功能性上略显欠缺。
系统功能
异地组网: 默认添加为专用网络,支持 Windows RDP 访问,几乎没有防火墙限制,之前使用 OpenVPN 默认公用网络改不了,只得弃用;
headscale 后端 + headplane 前端 + traefik 反代 + authelia认证:
headscale 与 headplane 共用域名,/ 及 /admin 转发至 headplane,其它 API 请求转发给 headscale
GUI 管理
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 过程中涉及多次对 traefik、headplane配置的修改,本文的目的是为已经正式使用的 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 mkdir -p /data/docker/authelia/{config,redis}mkdir -p /data/docker/authelia/redis/{backups,conf,data,scripts}chown -R 999:1000 redis/{backups,data}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: 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 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 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 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位随机字符串,随便找个工具生成:
oidc.headscale_api_key :headscale 容器生成:
1 2 3 4 docker exec headscale headscale apikeys create 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 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 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: authelia: rule: "Host(`sso.domain.top`)" entryPoints: - websecure service: authelia-service tls: certResolver: letsencrypt 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: authelia: forwardAuth: address: "http://authelia:9091/api/authz/forward-auth" trustForwardHeader: true authRequestHeaders: - "Accept" - "Authorization" - "Cookie" - "X-Forwarded-For" - "X-Forwarded-Host" - "X-Forwarded-Method" - "X-Forwarded-Proto" - "X-Forwarded-Uri" authResponseHeaders: - "LOCATION" - Remote-User - Remote-Groups - Remote-Name - Remote-Email forwardBody: false maxBodySize: 1048576 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: 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: headers: customRequestHeaders: X-Forwarded-Proto: "https" secured: chain: middlewares: - authelia - security-headers 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 bind 0.0.0.0port 6379 tcp-backlog 511 timeout 300tcp-keepalive 300 daemonize no supervised no pidfile "" loglevel notice logfile "" databases 1 save 900 1 save 300 10 save 60 10000 stop-writes-on-bgsave-error yes rdbcompression yes rdbchecksum yes dbfilename dump.rdb dir /dataappendonly 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" 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 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-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> 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.*$' - '/ts2021' - '/derp' - '/health' - '/key' - '/register' - domain: headscale.domain.top policy: two_factor subject: - group:admins - domain: sso.domain.top policy: bypass resources: - '^/$' - '^/login($|/.*$)' - '^/logout($|/.*$)' - '^/api/verify($|/.*$)' - '^/api/health($|/.*$)' - '^/api/oidc($|/.*$)' - '^/api/oidc/authorization($|/.*$)' - '^/api/oidc/token($|/.*$)' - '^/api/oidc/userinfo($|/.*$)' - '^/api/oidc/jwks($|/.*$)' - '^/api/oidc/revocation($|/.*$)' - '^/static($|/.*$)' - '^/favicon.ico$' - '^/manifest.json$' - '^/robots.txt$' - '^/api/webauthn($|/.*$)' - domain: sso.domain.top policy: two_factor resources: - '^/admin($|/.*$)' - '^/settings($|/.*$)' - '^/api/user/info($|/.*$)' - '^/api/user/session($|/.*$)' subject: - group:admins - domain: sso.domain.top policy: two_factor resources: - '^/profile($|/.*$)' - '^/api/user/profile($|/.*$)' - '^/api/user/preferences($|/.*$)' - 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: host: secure-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 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: - "https://headscale.domain.top/oidc/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: '' 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: authelia: disabled: true displayname: "Test User" password: "$argon2id$v=19$m=32768,t=1,p=8$eUhVT1dQa082YVk2VUhDMQ$E8QI4jHbUBt3EdsU1NFDu4Bq5jObKNx7nBKSn1EYQxk" email: authelia@authelia.com groups: - admins - dev violet: disabled: false displayname: "Violet" password: "$argon2idxxx" email: 'violet@domain.top' groups: - admins ...
密码生成:1 2 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_PASSWORD =<REDIS_PASSWORD>TZ =Asia/ShanghaiBACKUP_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 backend: name: "redis_net" driver: bridge services: redis: image: redis:7-alpine container_name: secure-redis restart: unless-stopped user: "999:1000" security_opt: - no -new-privileges:true cap_drop: - ALL cap_add: - SETGID - SETUID 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: - net.core.somaxconn=65535 ulimits: nofile: soft: 65536 hard: 65536 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' healthcheck: test: ["CMD" , "authelia" , "healthcheck" ] interval: 30s timeout: 3s retries: 3 start_period: 60s labels: - "traefik.enable=true" - "traefik.http.routers.authelia.rule=Host(`sso.domain.top`)" - "traefik.http.routers.authelia.entrypoints=websecure" - "traefik.http.routers.authelia.tls.certresolver=letsencrypt" - "traefik.http.routers.authelia.middlewares=security-headers@file" - "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 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: - '3478:3478/udp' 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: - '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' 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
启动服务
启动 Redis
启动 authelia
启动 headscale 服务组
1 2 3 4 5 docker compose up -d docker compose logs -f
使用
新增客户端
Windows
从官方 下载 客户端安装并启动;
打开【终端】,输入命令进行认证:
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 网卡绕一圈,容易出问题
外网设备:建议,不就是为了访问内网么
打开以上认证地址,输入 authelia 用户进行认证,认证成功如下:
Linux
使用官方一键脚本安装,会自动根据系统版本安装仓库源,并配置 systemd 服务:
1 curl -fsSL https://tailscale.com/install.sh | sh
(可选)开启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.confsysctl -w net.ipv6.conf.all.forwarding=1 echo "net.ipv6.conf.all.forwarding=1" >> /etc/sysctl.conf
认证客户端:
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
打开以上认证地址,输入 authelia 用户进行认证。
(可选)如果开启了子网路由,需要到管理页面手动允许才能使用,开启后就可以在 tailnet 客户端 ping 通内网地址了。
Android
从 Google Play 下载客户端,右上角用户进入,点击右上角三个点点展开,选择【Use an alternate server】输入自定义服务器地址:https://headscale.domain.top ,然后选择登录,会弹出浏览器登录。
开启 TOTP
打开 authelia 登录地址:https://sso.domain.top 登录;
登录之后从右上角进入设置选择【两步验证】:
算法选择 SHA256 及以上(不过我的配置最低就是 SHA256,想选弱算法也没有机会呢),位数 6 位。
开启 OTP,如果手机支持,建议开启 WebAuthn (Passkey)直接不用记密码了(笨蛋Flyme不支持)。
使用 splitDNS
功能:使用指定的 DNS 解析指定的域名。
场景:加入 tailsnet 的设备可以直接使用内网域名访问系统,最主要的需求是 immich 备份和 vaultwarden 。其实在上面的 headscale 配置中已经加了,如果要从 headplane Web 页面添加,需要到 【DNS】页面下,添加 nameservers 的时候勾选【splitDNS】。
备份
请务必做好定期备份,我的备份方案是:
轻量服务器本地使用 restic 每周备份,开启 sftp;
通过 sftp 每周定期将 restic 仓库使用 rclone 备份到本地主机。
实践&疑问
实践
如果需要使用内网 AdguardHome 为子网路由提供 DNS,建议在单独的 Linux 服务器(可以是虚拟机)部署一套 tailscale 客户端,1c512M5G就够了,补充说明我的环境需求:
计算服务器底层系统是 PVE,专门启了个 LXC 容器跑 Docker,当时试过容器部署 tailscale 客户端,但是需要开启特权容器/挂载 tun 设备,权衡之后还是放弃了容器部署方式;
AdgurdHome 部署方式为 Docker 容器,网络模式为桥接,内网DNS,子网路由组网之后需要广播给客户端解析内网域名,加上使用 openwrt + openclash 组合,且几乎没有关过,tailscale 安装到 openwrt 上会导致 openclash 劫持 DNS;
子网路由其它部署在哪里都可以满足,只是因为以上原因最终单独启了一台部署。
调试尽量每次开一个无痕模式测试,因为缓存、cookie会有影响,产生奇奇怪怪的bug而且难以觉察。
疑问
Q:为什么不在 openwrt 上安装 tailscale?
A:开启 openclash 后,以我当前配置会劫持网卡 DNS 查询请求,根本到不了 AdGuardHome。
Q:Linux 安装 tailscale 之后启动显示 running,但是命令无法使用?
A:使用 systemctl status headscaled.service 查看报错为 TUN 设备不可用,可能是部署在了 PVE LXC 容器,默认不提供 TUN 设备,解决: