我在 https://home.yaoyun.xyz 的自建服务的安全认证程度参差不齐:
- dufs / miniflux 只有 HTTP 认证
- Portainer / Photoprism 有完整的用户系统,前者支持内部的用户名 - 密码认证、OAuth 和 LDAP,后者支持 OIDC 和 OAuth
- Traefik 的 Dashboard、Netdata 的监控无认证,还会暴露敏感信息。
因此,管理多个自建服务时,自建/使用第三方 SSO 就能提供同一水平、集中控制的访问控制。Authentik 是一个开源 SSO,采用免费/开源自建 + 付费支持的商业模式,自称提供最丰富的接口支持,同类替代还有:
- Keycloak:红帽开发。半年前试用过、对于个人服务太重了,难以维护。开源 + 卖商业支持和进阶 feature。
- Authenlia:强调轻量,正在筹款做安全审计。
- GitHub - casdoor/casdoor,国人开发。
使用 CF Tunnel + Traefik 反向代理 Authentik
在使用 Authentik 之前,我的自建服务架构是:公网用户 -(https, yaoyun.xyz)-> Cloudflare 反代 -(https, *.cfargotunnel.com)-> CF Tunnel -(…)-[LAN[-> Cloudflared -(http)-> traefik at 80 -(http)-> services ]]
。由于 CF Tunnel 访问 Traefik 的 HTTP Entrypoint,送往 Authentik 的 HTTP 请求头中会包含 X-Forwarded-Proto: http
。Authentik 网页中的 URL 并非协议相对 URL(protocol-relative URLs),而是根据请求头中的(X-Forwarded-Proto
)决定,此处即 HTTP。同时我启用了 CF 的 Always Use HTTPS 和 Edge Certificate,因此公网用户始终使用 HTTPS 访问。此外,CF 的 Automatic HTTPS Rewrites 对 Authentik 还不起效。这一切就成了 Mixed Content Error 的典型例子……
为了使 Traefik 转发的请求头中 X-Forwarded-Proto
为 https
,有两种方案:
- 粗暴的使用
headers
中间件,覆写之 - 配置 Traefik 使用自签的
localhost
TLS 证书和443
Entrypoint,使 Cloudflared 转发 Tunnel 流量至https://localhost:443
。
为了一劳永逸,这里我使用了第二种方案。注意在 Cloudflare Tunnel 设置中启用 No TLS Verify 以信任自签的 localhost 证书。由于使用该证书加密的流量只通过本地回环,可以不担心中间人攻击。
此外,Authentik 的推荐 Compose File 中包含四个容器:作为数据库存储状态的 postgresql
和 redis
,和无状态的 server
与 worker
;server
对外提供服务,也是唯一需要连接至 traefik
Docker 网络的。
配置 Traefik 转发认证(Forward Authentication)
缺乏认证支持的服务可通过反代 + 转发认证来接入 Authentik。由于 Traefik 是我这台 All-in-boom 的主要路由反代,我使用它的转发认证,而非 Authentik 自己的反代。
Authentik 对转发认证的支持实现如下:假设需要认证的服务位于 SERVICE_HOST = srv.example.com
。
services:
whoami:
image: traefik/whoami
restart: unless-stopped
networks:
- traefik
labels:
- traefik.enable=true
- traefik.http.routers.whoami.rule= Host(`${SERVICE_HOST}`)
networks:
traefik:
external: true
一个额外的 Proxy 服务需要监听 srv.example.com/outpost.goauthentik.io/
目录。这一 Proxy 服务作为一个哨站(Outpost)与 Authentik 主站连接,交换具体的用户、访问控制和授权信息。这是为了将 Authentik 提供的会话 Token 存储在浏览器 srv.example.com
的 Cookies 中、以在会话生命周期内实现无感认证。给这个路由设置较高的优先级、以覆盖需认证服务的路由。
services:
whoami: ...
auth-whoami:
image: ghcr.io/goauthentik/proxy
networks:
- traefik
environment: ...
labels:
traefik.enable: true
traefik.http.services.auth-whoami.loadbalancer.server.port: 9000
traefik.http.routers.auth-whoami.rule: Host(`${SERVICE_HOST}`) && PathPrefix(`/outpost.goauthentik.io/`)
traefik.http.routers.auth-whoami.tls: true
traefik.http.routers.auth-whoami.priority: 15
...
networks: ...
在 Authentik 主站管理面板中新建一个 Proxy Outpost,复制供 Outpost 访问 Authrntik 的 Token ${AUTHENTIK_TOKEN}
。为了提升性能和安全性,令这一 Proxy 服务使用 Docker 内部网络来访问 Authentik 主站 ${AUTHENTIK_INTERNAL_URL}
,并无条件信任主站的 SSL 证书。此外配置用户访问主站时的外部 URL ${AUTHENTIK_URL}
。
services:
whoami: ...
auth-whoami:
image: ...
networks: ...
environment:
AUTHENTIK_HOST: ${AUTHENTIK_INTERNAL_URL}
AUTHENTIK_INSECURE: "true"
AUTHENTIK_TOKEN: ${AUTHENTIK_TOKEN}
AUTHENTIK_HOST_BROWSER: ${AUTHENTIK_URL}
labels: ...
networks: ...
然后,声明一个 Traefik 的转发认证中间件,指向 Outpost 监听的 srv.example.com/outpost.goauthentik.io/
、转发返回的 Header。最后,令需认证服务使用该中间件。
services:
whoami:
...
labels:
- ...
- traefik.http.routers.whoami.middlewares=auth-whoami@docker
auth-whoami:
image: ...
networks: ...
environment: ...
labels:
...
traefik.http.middlewares.auth-whoami.forwardauth.address: http://auth-whoami:9000/outpost.goauthentik.io/auth/traefik
traefik.http.middlewares.auth-whoami.forwardauth.trustForwardHeader: true
traefik.http.middlewares.auth-whoami.forwardauth.authResponseHeaders: X-authentik-username,X-authentik-groups,X-authentik-email,X-authentik-name,X-authentik-uid,X-authentik-jwt,X-authentik-meta-jwks,X-authentik-meta-outpost,X-authentik-meta-provider,X-authentik-meta-app,X-authentik-meta-version
networks: ...
完整 Compose file 示例见 附录。
配置 OAuth 及其他协议
OAuth 和其他认证协议的支持几乎开箱即用,在 Authentik 面板中新建一对绑定的 Application - Provider,选择对应协议即可。Authentik 也提供了 分应用的具体指导,故不详述。
其他
配 Traefik TLS 和面板的时候花了一天半排查这个问题:Traefik 的 api@internal
需要一个假端口配置才能正常 routing,不然就会报 port is missing
并且拒绝创建 router…… Seriously, WTF?
- traefik.http.services.justAdummyService.loadbalancer.server.port=1337
这是一个有五年多历史的 Open Issue,而且在我看过的所有博客和文档里都没有提及…… Docker swarm configuration ignored with “port is missing” · Issue #5732 · traefik/traefik · GitHub
此外,静态配置(启动时的变量,如 --entrypoints=…
,和 /etc/traefik/traefik.yaml
等标准位置的配置文件)应仅包含 Entrypoints、Provider 等项;而路由、服务、中间件和证书应当由一个动态配置 Provider 提供,比如 Docker 标签或指定的动态配置目录(文件)。
附录
Forward Auth 示例
services:
whoami:
image: traefik/whoami
restart: unless-stopped
networks:
- traefik
labels:
- traefik.enable=true
- traefik.http.routers.whoami.rule= Host(`${SERVICE_HOST}`)
- traefik.http.routers.whoami.tls=true
- traefik.http.routers.whoami.middlewares=auth-whoami@docker
auth-whoami:
image: ghcr.io/goauthentik/proxy
networks:
- traefik
environment:
AUTHENTIK_HOST: ${AUTHENTIK_INTERNAL_URL}
AUTHENTIK_INSECURE: "true"
AUTHENTIK_TOKEN: ${AUTHENTIK_TOKEN}
AUTHENTIK_HOST_BROWSER: ${AUTHENTIK_URL}
labels:
traefik.enable: true
traefik.http.services.auth-whoami.loadbalancer.server.port: 9000
traefik.http.routers.auth-whoami.rule: Host(`${SERVICE_HOST}`) && PathPrefix(`/outpost.goauthentik.io/`)
traefik.http.routers.auth-whoami.tls: true
traefik.http.routers.auth-whoami.priority: 15
traefik.http.middlewares.auth-whoami.forwardauth.address: http://auth-whoami:9000/outpost.goauthentik.io/auth/traefik
traefik.http.middlewares.auth-whoami.forwardauth.trustForwardHeader: true
traefik.http.middlewares.auth-whoami.forwardauth.authResponseHeaders: X-authentik-username,X-authentik-groups,X-authentik-email,X-authentik-name,X-authentik-uid,X-authentik-jwt,X-authentik-meta-jwks,X-authentik-meta-outpost,X-authentik-meta-provider,X-authentik-meta-app,X-authentik-meta-version
networks:
traefik:
external: true