使用nginx和vouch-proxy做OIDC登陆和权限控制
需求
我做了一个前后端分享的网页(前端使用vuejs3,后端使用 golang)
但我需要一个权限控制的功能,以保证某些“危险的”操作只能特定的人有权限操作。这些危险的操作我定义为POST PUT DELETE 等方法。
要做权限控制,首先需要有用户的信息,最好是和公司的 Ldap 结合的。
我不想从头实现这样的功能,想仅仅通过已有第三方的组件,简单的配置就可以实现。另外,我们公司有 OIDC 服务,所以优先选择能使用 OIDC 的组件。
以上就是我的需求。
OIDC
选择了使用 vouch-proxy 和 nginx 来做这个事情。(因为公司有OIDC服务了,如果没有,可能需要自己搭建)
申请 OIDC Client
申请之后,拿到 clientid 和 clientsecret,这两个需要配置在 vouch 的 config 里面(最后有示例)。
申请的时候,需要提交 callback 地址信息,这个地址需要配置在 vouch config 文件中,最好一点都不能错,包括 schema 和 最后的 /
。有些 OIDC 服务在这方便比较严格。
OIDC 服务提供方还会返回一个 odic 服务的地址,比如 https://oidc.corp.com。
但 vouch 需要同时配置 token_url
和 user_info_url
。 如果 OIDC 服务提供方没有给这两个地址,可以自己通过 https://oidc.corp.com/.well-known/openid-configuration
获取。(有些 OIDC 组件会自己去这个地址获取,不用用户自己配置)
域名准备
我们自己的页面叫 app.corp.com
再给 vouch 服务一个域名,vouch.app.corp.com
注意两点:
- 他们应该是同一个父域名,方便设置 cookie
- 只用一个域名可能也行,在 nginx 里面配置一下。我没有尝试,这里也不讨论了。
OIDC 原理
结合 vouch-proxy 说一下 OIDC 原理,知道了原理,以后有问题才好处理。
OIDC 并没有单点登陆的功能。所以原理里面并不会包括单点登陆的东西。
-
访问一个页面时,nginx 通过 auth_request 将请求交给 vouch 做权限控制。但注意这里的权限控制,我不知道应该叫什么更确切,不是看用户有没有特定的权限做特定的操作,在这个场景下,可以理解成只是看用户是不是登陆了。如果读者对 nginx auth_request 模块不了解,需要先去做个简单了解,这里不做解释了。
-
vouch 根据 cookie 来判断用户是不是已经登陆了,以及用户名等等信息。这里的 Cookie 怎么来的呢,我们后面会说到。
-
第一次访问的时候,肯定没有这个Cookie,vouch 根据 auth_request 的协议返回 401。nginx 通过 auth_request 模块,知道用户没有登陆,就会 302 跳转到一个登陆页面,在这里,我们是配置成 302 到 vouch.app.corp.com/login
-
vouch 的 login handler 会根据 OIDC 的配置,跳转到 OIDC 登陆页面。
-
OIDC 登陆之后,会根据 redirect_url 跳回 vouch 的 vouch.corp.com/auth,OIDC 跳回这里的时候,会带上一个 code 参数
-
vouch 拿 code 参数去请求 OIDC 服务,如果 code 合法,则返回用户信息。
-
auth handler 拿到用户信息之后,会set-cookie,将 token(是一串加密信息)设置到 app.corp.com domain 中。
-
用户再次打开 app.corp.com 时,同步骤1 一样将请求交给 vouch 做判断是否有权限。
-
vouch 通过 cookie 中的 token 拿到用户信息,直接返回 200,通过。
-
nginx 中通过 auth_request_set 配置,将用户名传递给 proxy_pass,以做其他用途。目前我这里还没有用到,后面的权限控制会用到。
OIDC 在这里就 OK 了,用户需要登陆才能使用 app.corp.com 了。
权限控制
这块是纯粹在 nginx 配置的,因为通过 auth_request_set,我们在 nginx 中已经可以获取用户名。但这块也非常折腾,主要还是 nginx 的指令实在是不符合代码的正常思维(个人感受)。可能用 OpenResty 更好。
这块不多说了,拿两个尝试的失败的方案说一下。最后给出目前的可以工作的配置。
-
最简单的想法是使用 if 指令,但是 if 指令不管写在配置的什么位置,都会在 auth_request 之前执行,导致拿不到用户信息。关于这个问题,有用户给出了详细的问题描述和尝试。https://serverfault.com/questions/1121736/nginx-is-not-handling-auth-request-before-if-statement。我也是在这个帖子的回复中找到进一步尝试的方法。
-
根据问题1 中下面某人的回复,我尝试使用 map 来做这件事。但还是失败了。失败的原因很奇怪,如果使用变量名做 proxy_pass 的后端,则url 会被全部截断传到后端,完全无法正常工作。我没有找到相关的官方文档,所以自己瞎尝试了几种配置,都失败了。最后还是放弃了这个看似美好而且配置相对简单的方案。
-
一度想在 vouch 里面来做这个事,后面发现不合适。因为 vouch 返回 401 的话,会再次去 OIDC 登陆,用户会迷茫,明明已经登陆过了啊,死循环了。所以在 vouch 中做这个事不合适。
配置
nginx 配置
upstream healer {
server 127.0.0.1:8001;
}
server {
access_log off;
listen 8001;
set $allow 0;
if ($request_method = GET) {
set $allow 1;
}
if ($request_method = HEAD) {
set $allow 1;
}
if ($request_method = OPTIONS) {
set $allow 1;
}
if ($http_x_vouch_user = "zhangsan" ) {
set $allow 1;
}
if ($http_x_vouch_user = "lisi" ) {
set $allow 1;
}
if ($allow = 0) {
return 403;
}
location / {
proxy_pass http://127.0.0.1:8000;
}
}
server {
log_format main '$remote_addr $auth_resp_x_vouch_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for" "$request_body"';
access_log /opt/logs/access.log main;
listen 8080 default_server;
server_name app.corp.com;
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;
}
error_page 401 = @error401;
location @error401 {
return 302 http://vouch.app.corp.com/login?url=$scheme://$http_host$request_uri&vouch-failcount=$auth_resp_failcount&X-Vouch-Token=$auth_resp_jwt&error=$auth_resp_err;
}
location / {
if ($http_user_agent ~* HealthChecker) {
return 200;
}
if ($http_user_agent ~* kube-probe) {
return 200;
}
root /opt/kafka-admin/;
auth_request_set $auth_resp_x_vouch_user $upstream_http_x_vouch_user;
auth_request_set $auth_resp_x_vouch_idp_claims_groups $upstream_http_x_vouch_idp_claims_groups;
auth_request_set $auth_resp_x_vouch_idp_claims_given_name $upstream_http_x_vouch_idp_claims_given_name;
}
location /api/ {
auth_request_set $auth_resp_x_vouch_user $upstream_http_x_vouch_user;
auth_request_set $auth_resp_x_vouch_idp_claims_groups $upstream_http_x_vouch_idp_claims_groups;
auth_request_set $auth_resp_x_vouch_idp_claims_given_name $upstream_http_x_vouch_idp_claims_given_name;
proxy_pass http://healer/;
proxy_set_header X-Vouch-User $auth_resp_x_vouch_user;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
server {
listen 8080 ;
server_name vouch.app.corp.com;
location / {
proxy_pass http://127.0.0.1:9090;
# be sure to pass the original host header
proxy_set_header Host vouch.app.corp.com;
}
}
vouch oidc
vouch:
session: # 如果是多机器部署在一个 LB 后面,session.key 需要一样。这里不配置就会随机生成,导致认证失败。
key: 4LjeTY9/E17RvQx1itF0p6CsfFuqhgQiNtVQQGh32is=
#whitelist:
#- rmself
#- childe
#allowAllUsers: true
domains:
- corp.com # 会用来验证 OIDC 返回的 email 是不是在此 domains 里面,如果不在会401。
cookie:
secure: false
# vouch.cookie.domain must be set when enabling allowAllUsers
domain: app.corp.com
oauth:
# Generic OpenID Connect
provider: oidc
client_id: xxxxxx
client_secret: xxxxxx
auth_url: https://oidc.corp.com/authorize
token_url: https://oidc.corp.com/token
user_info_url: https://oidc.corp.com/userinfo
scopes:
- openid
- email
- profile
callback_url: http://vouch.oidc.corp.com/auth