写在前面

最近写了些东西,但是不想直接放在公网上给所有人看。

提供权限控制的类似 wiki 的应用,虽然也有开源的 bookstack 和 wiki.js 之类,但都有我不怎么喜欢的 anti-feature.

结果沦落到用静态框架(docsify/mkdocs-material)和 basic auth, 比用 hexo 还要丢人。

然后想到自己有个私有的 gitea 啊,可以充当身份服务器。虽然权限管理确实非常粗放,但是我想没人会真的用我的 gitea 存东西吧,就直接拿来用也可以。

于是找到了 vouch-proxy 和 oauth2-proxy 这两个项目(后来折腾 code-server 的时候还发现了 pomerium, 但那是后话了)。这两个都是利用 nginx 的 auth_request 模块来鉴权的,都很轻(毕竟都是 go 写的),比较了一番以后,感觉还是 vouch 更容易上手。

安装方式

编译安装

vouch 支持很多安装方式。

俺一开始用的是 docker, 但后来发现 vouch 所在的服务器在初次授权时要访问 gitea, 而它的特征被互联网皇帝认成 bot, 就会吃 Bot Fight Mode, 于是 vouch 日志疯狂报 400, 就用 wireguard 打隧道,从内网走。

也就换成编译安装了。

$ git clone https://github.com/vouch/vouch-proxy.git
$ cd vouch-proxy
$ ./do.sh goget
$ ./do.sh build
$ doas mkdir -p /opt/vouch-proxy/config
$ doas cp vouch-proxy /opt/vouch-proxy/

部署在 Docker 容器内

可以试试[用 docker 运行](https://github.com/vouch/vouch-proxy#installation-and-configuration),compose 挺好玩的。

可以给 vouch 容器单独开一个 bridge 网络,然后让 nginx 监听网关,就不需要在公网上绕路了。对于部署在同一台机器上的 CI/CD Runner 也是一样的。

如果 gitea 也是用 docker 部署的,那可以试试在 gitea 的 compose 文件里面开个网络:

networks:
  gitea-net:
    external: false
    name: gitea_network
    driver: bridge
    ipam: 
      config: 
        - subnet:  172.30.0.0/24
          gateway: 172.30.0.1

网段自己看着改,避免冲突就行。

在底下的 service 添加 network 键:

 services:
   gitea:
+    networks:
+      gitea-net:

确保两个 network 的 name 名字一样。vouch 的 compose file 里 external 属性为 true.

此外,用 extra_hosts 把 gitea 实例的 hostname 解析到网关(主机)上的 IP 地址,但同时需要在 nginx 中监听该地址

比如这样:

# docker-compose.yml
version: '3.0'

+networks:
+  gitea-net-vouch:
+    external: true
+    name: gitea_network

services: 
   vouch-proxy: 
    container_name: vouch
    image: quay.io/vouch/vouch-proxy:latest
    ports: 
      - "127.0.0.1:9090:9090"
    volumes: 
      - ./vouch/config:/config
    restart: unless-stopped
+    extra_hosts: 
+      - "gitea.yourdomain.com:172.30.0.1"
+    networks: 
+      gitea-net-vouch:

然后把 vouch 的容器起好,就可以了。

配置文件

路径

  • 编译安装,默认 /opt/vouch-proxy/config/config.yml , 但是命令行可以指定配置文件路径。

  • 对于 docker 安装,先启动一次容器再关掉,然后修改持久化文件路径里面的 config.yml 就可以了。

作者给出了很多示例,可以参考。

之前尝试了他们给的 gitea 配置示例,但是反复碰壁。想到 gitea 的身份服务是基于 OpenID Connect 的,那为什么不用呢?

前往 gitea 实例的 /.well-known/openid-configuration 就能看到要填的参数。

一些可能要踩的坑

  • 配置白名单太慢了,所以直接 allowAllUsers 也没关系。

  • vouch.jwt.secret 我直接加在配置里了,反正也没法加密存储。所以记得改权限到 600.

  • 另外还需要添加 code challenge method, 不然 PKCE 过不去。

  • maxAge 是很重要的参数。如果不设置,就会在 gitea 的数据库保存一个永不失效的 OAuth authorization code, 还是相当危险的。

  • scope 是 vouch 通过 OIDC 获取的 gitea 用户信息。此处的 group 即是 gitea 中的 organization.

  • client_idclient_secret 直接到 gitea 的 Settings/Applications 那里新建一个 OAuth Application 就行。
# Vouch Proxy configuration
# bare minimum to get Vouch Proxy running with Gitea

vouch:
  listen: 10.0.0.1 # 0.0.0.0
  port: 9090

  # domains:
  # - yourdomain.com

  # set allowAllUsers: true to use Vouch Proxy to just accept anyone who can authenticate at Gitea
  allowAllUsers: true

  cookie:
    # Set `secure: false` when protecting a non-https site such as http://app.yourdmain.com - VOUCH_COOKIE_SECURE
    secure: true
    # optionally force the domain of the cookie to set
    # vouch.cookie.domain must be set when enabling allowAllUsers
    domain: yourdomain.com
    # Number of minutes until session cookie expires - VOUCH_COOKIE_MAXAGE
    # Set cookie maxAge to 0 to delete the cookie every time the browser is closed.
    # Must not be longer than jwt.maxAge
    maxAge: 20160 

  jwt:
    # secret - VOUCH_JWT_SECRET
    # a random string used to cryptographically sign the jwt when signing_method is set to HS256, HS384 or HS512
    # Vouch Proxy complains if the string is less than 44 characters (256 bits as 32 base64 bytes)
    # if the secret is not set here then Vouch Proxy will..
    # - look for the secret in `./config/secret`
    # - if `./config/secret` doesn't exist then randomly generate a secret and store it there
    # in order to run multiple instances of vouch on multiple servers (perhaps purely for validating the jwt),
    # you'll want them all to have the same secret
    secret: <YOUR SECRET> # you can generate it by executing `openssl rand -base64 32`
    
    # number of minutes until jwt expires - VOUCH_JWT_MAXAGE
    maxAge: 20160 # 14d

      
oauth:
  # replace "gitea.yourdomain.com" with the domain your Gitea instance runs on
  # create a new OAuth application at:
  # https://gitea.yourdomain.com/user/settings/applications
  provider: oidc

  # PKCE method if enabled, S256 is currently supported (check https://www.oauth.com/oauth2-servers/pkce/)
  code_challenge_method: S256
  scopes:
    - openid
    - email
    - profile
    - groups
  client_id: <YOUR GITEA ID>
  client_secret: <YOUR GITEA SECRET>
  auth_url: https://gitea.yourdomain.com/login/oauth/authorize
  token_url: https://gitea.yourdomain.com/login/oauth/access_token
  user_info_url: https://gitea.yourdomain.com/login/oauth/userinfo
  callback_url: https://vouch.yourdomain.com/auth

(编译安装)启用进程守护

填好配置就可以启动进程守护了。作者已经给了完整的示例。如果工作目录和安装路径没有变化就不用改了。

[Unit]
Description=Vouch Proxy
After=network.target

[Service]
Type=simple
User=vouch-proxy
WorkingDirectory=/opt/vouch-proxy
ExecStart=/opt/vouch-proxy/vouch-proxy -config /opt/vouch-proxy/config/config.yml
Restart=on-failure
RestartSec=5
StartLimitInterval=60s
StartLimitBurst=3

[Install]
WantedBy=default.target

配置 nginx

接下来就是配置 auth_request 连接到 vouch.

作者也给出了很好的示例,基本不用改。

proxy_pass 直接指向 vouch 自身监听的 IP 和端口。

把这段配置夹在开头和真正的后端之间。单独写个文件然后 include 进来也可以。

# send all requests to the `/validate` endpoint for authorization
auth_request /validate;

location = /validate {
  # forward the /validate request to Vouch Proxy
  proxy_pass http://127.0.0.1:9090/validate;

  # be sure to pass the original host header
  proxy_set_header Host $http_host;

  # Vouch Proxy only acts on the request headers
  proxy_pass_request_body off;
  proxy_set_header Content-Length "";

  # optionally add X-Vouch-User as returned by Vouch Proxy along with the request
  auth_request_set $auth_resp_x_vouch_user $upstream_http_x_vouch_user;

  # these return values are used by the @error401 call
  auth_request_set $auth_resp_jwt $upstream_http_x_vouch_jwt;
  auth_request_set $auth_resp_err $upstream_http_x_vouch_err;
  auth_request_set $auth_resp_failcount $upstream_http_x_vouch_failcount;
}

# if validate returns `401 not authorized` then forward the request to the error401block
error_page 401 = @error401;

location @error401 {
    # redirect to Vouch Proxy for login
    return 302 https://vouch.yourdomain.com/login?url=$scheme://$http_host$request_uri&vouch-failcount=$auth_resp_failcount&X-Vouch-Token=$auth_resp_jwt&error=$auth_resp_err;
}

然后在后端配置里面加上 X-Vouch-User 就差不多了。

    # proxy pass authorized requests to your service
    location / {
        proxy_pass http://app2.yourdomain.com:8080;
+        # may need to set
+        # auth_request_set $auth_resp_x_vouch_user $upstream_http_x_vouch_user;
+        # in this bock as per https://github.com/vouch/vouch-proxy/issues/26#issuecomment-425215810
+        # set user header (usually an email)
+        proxy_set_header X-Vouch-User $auth_resp_x_vouch_user;
    }

执行 nginx -t 检查配置,然后 reload 一下就应该能用了。

最后

反正作者的文档写得详细,有什么问题直接去看项目主页就行了。