小Cの已经记不起来的博客

用 Docker 搭建鉴权体系的完整流程

家里服务器上跑的自托管服务越来越多,Gitea、Vaultwarden、qBittorrent,之前每个都是各管各的账号密码,有两个甚至直接裸奔在公网上,每次想起这事心里都有点发毛。本来想图省事,在每个应用前面套一层 Nginx Basic Auth,用了一阵发现体验稀碎,qb 的本地客户端压根不认这套东西。后来研究了一圈,决定上 Authelia 配合 Nginx 做统一鉴权,全部用 Docker 跑。Authelia 的文档其实挺全,就是散得到处都是,一个配置项能让你在官网、GitHub 示例和 FAQ 之间来回跳,所以干脆把整个过程记成一篇,下次重装直接照着抄。

为什么选 Authelia 而不是 Keycloak

一开始其实想用 Keycloak,毕竟名气大,教程也多。结果拉下来一看,这玩意儿自己跑不起来,还得再拖一个 PostgreSQL,就为了给几个个人服务加个登录,养一个数据库属实有点重了。Authelia 是单个容器,配置全是 yaml,存储直接用 sqlite 就够了,对个人自托管这个量级来说刚刚好,那就它了。

目录结构先定下来

/opt/auth/
├── docker-compose.yml
├── nginx/
│   ├── conf.d/default.conf
│   └── certs/                # 证书怎么签随意,wildcard 一张就够
└── authelia/
    ├── configuration.yml
    ├── users.yml
    ├── db.sqlite3            # 跑起来之后自动生成
    └── secrets/
        ├── jwt_secret
        └── storage_encryption_key

用 docker-compose 把服务拉起来

Authelia 官方镜像在 Docker Hub 上,国内拉取经常超时,我这里配了镜像加速域名,拉的时候报错的话可以把 docker.1ms.run/ 前缀删掉,没准儿你用的时候这个加速域名已经不可用了。

services:
  authelia:
    image: docker.1ms.run/authelia/authelia:4.38
    container_name: authelia
    restart: unless-stopped
    volumes:
      - ./authelia:/config
    networks:
      - auth

  nginx:
    image: docker.1ms.run/library/nginx:alpine
    container_name: nginx
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d
      - ./nginx/certs:/etc/nginx/certs
    networks:
      - auth

networks:
  auth:
    external: true

注意这个 auth 网络是先手动 docker network create auth 建出来的,然后在所有 compose 文件里声明成 external。这样写的好处是业务容器不管在哪个 compose 文件里,只要挂上这个网络,Nginx 就能通过容器名直接反代到它。Authelia 不需要映射任何端口到宿主机,反正只有 Nginx 找它。

Authelia 的最小配置

configuration.yml,我把用不上的全删了,只留能跑通的部分(把 example.com 换成你自己的域名,下同):

server:
  port: 9091

authentication_backend:
  file:
    path: /config/users.yml

storage:
  encryption_key: {{ secret "/config/secrets/storage_encryption_key" }}
  local:
    path: /config/db.sqlite3

session:
  secret: {{ secret "/config/secrets/jwt_secret" }}
  cookies:
    - domain: example.com          # 注意是父域,写错了 cookie 不会跨子域
      authelia_url: https://auth.example.com

access_control:
  default_policy: deny
  rules:
    - domain: qb.example.com
      policy: two_factor
    - domain: git.example.com
      policy: one_factor

regulation:
  max_retries: 3
  find_time: 2m
  ban_time: 5m

两个密钥文件用 openssl 随便生成一下就行:

openssl rand -hex 32 > authelia/secrets/jwt_secret
openssl rand -hex 32 > authelia/secrets/storage_encryption_key

users.yml 里放用户和密码哈希,格式很简单:

users:
  me:
    displayname: Me
    password: $argon2id$v=19$m=65536,t=3,p=4$...

这个哈希也用 docker 生成,不然你就得在宿主机上折腾 argon2 的工具链,没必要:

docker run --rm docker.1ms.run/authelia/authelia:4.38 authelia crypto hash generate argon2 --password '你的密码'

由于加了 --rm,哈希生成完容器就自动删了。注意密码参数一定要用单引号包起来,不然输出里 $argon2id$ 开头那串 $ 会被 shell 当变量吃掉,生成的哈希直接作废,之后登录永远提示账号密码错误,别问我怎么知道的。

Nginx 侧接入 forward auth

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

# 登录门户本身,这个域名千万不能套鉴权
server {
    listen 443 ssl;
    server_name auth.example.com;

    ssl_certificate     /etc/nginx/certs/example.com.crt;
    ssl_certificate_key /etc/nginx/certs/example.com.key;

    location / {
        proxy_pass http://authelia:9091;
        proxy_set_header X-Original-URL $scheme://$host$request_uri;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

# 被保护的业务,以 qb 为例
server {
    listen 443 ssl;
    server_name qb.example.com;

    ssl_certificate     /etc/nginx/certs/example.com.crt;
    ssl_certificate_key /etc/nginx/certs/example.com.key;

    location /internal/authelia/authverify {
        internal;
        proxy_pass http://authelia:9091/api/verify;
        proxy_set_header X-Original-Method $request_method;
        proxy_set_header X-Original-URL $scheme://$host$request_uri;
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header Content-Length "";
        proxy_set_header Connection "";
    }

    location / {
        auth_request /internal/authelia/authverify;
        auth_request_set $user $upstream_http_remote_user;
        auth_request_set $redirect $upstream_http_location;
        error_page 401 =302 $redirect;

        proxy_set_header Remote-User $user;
        # 下面三行是给 websocket 用的,qb 和 Gitea 都需要
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;

        proxy_pass http://qbit:8080;
    }
}

另外再加一个 80 端口的 server 块做 301 跳转,这里就不贴了。然后 docker compose up -d,接着 docker compose logs -f authelia 看到 Startup complete 就算起来了。不出问题的话,打开 https://qb.example.com 会先被踢到登录页,第一次登录会让你绑 TOTP,随便找个验证器 App 扫一下就行。

踩过的坑

第一个坑是无限重定向。我一开始图省事把 auth.example.com 也挂上了 auth_request,登录页自己去校验自己,浏览器一路 302 到天荒地老。鉴权门户必须裸奔,切记。

第二个坑还是 cookie。session 里的 domain 写成了 auth.example.com,结果 cookie 不会带到别的子域,每访问一个服务都被踢回登录页,看起来像登录失效,其实就是域写小了。

第三个坑是 502。新反代一个容器,页面直接 502,查了半天发现是新容器忘了挂 auth 网络,Nginx 解析不到容器名。

最后一个坑是客户端类应用。浏览器里一切都好,但 qb 的本地客户端和一些命令行工具不走 cookie,会被 forward auth 直接挡掉。这类场景要么在 access_control 里给对应路径加 bypass 规则、靠应用自己的 API key 兜底,要么就老老实实浏览器里用。

之后加新服务怎么办

基本就是三步:DNS 加一条子域解析、Nginx 复制一个 server 块改两处域名、configuration.yml 里加一条规则,然后 docker compose exec nginx nginx -s reload。改完发现 502,八成是新容器忘了挂网络;发现无限跳登录,八成是 cookie 的 domain 写错了。整套跑下来其实没几行配置,但初次搭的时候文档翻得人头晕,希望能省下你一个晚上。

评论

还没有评论。

发表评论

提交后评论将经过自动审核,审核通过后公开展示。

未在播放