Nginx + oauth2-proxy + Rundeck の検証を手元で行う
はじめに
Rundeck を使うとき、ユーザー認証と権限管理をどうやるかは割と悩みどころです。
Enterprise 版を使うと SSO もサポートされている のですが、結構いいお値段しますので、円安でコスト削減圧の強い昨今、札束で殴って解決するのは難しいところです。
OSS 版でも LDAP 等いくつかサポートされている方法はありますが、ユーザー管理のために別途サービスを管理するのも気が重いです。
そこで使えるのが oauth2-proxy/oauth2-proxy です。
Nginx + oauth2-proxy + Rundeck の基本的な設定
Rundeck の Preauthenticated Mode using headers にある通り、いくつかのヘッダーを Rundeck がリクエストを処理する前に設定してやれば、その情報を使って Rundeck 上のユーザーとロールを設定してくれるというものです。
oauth2_proxyでRundeckにGitHub認証でログインする - Qiita にある通り、素直に設定するとユーザー名などに応じてロールを動的に設定することができません。
その問題を解決するために、いくつか調べた結果をまとめました。
構成はこんな感じです。
ローカルで試したいので docker-compose.yml を用意します。
version: "3.1"
services:
nginx:
image: nginx
ports:
- 8000:80
volumes:
- ./image-files/etc/nginx/conf.d/default.conf:/etc/nginx/conf.d/default.conf
depends_on:
- oauth2-proxy
- rundeck
rundeck:
image: rundeck/rundeck:4.8.0
environment:
RUNDECK_SERVER_UUID: <your server uuid>
RUNDECK_GRAILS_URL: http://localhost:8000
RUNDECK_SERVER_CONTEXT_PATH: /
RUNDECK_SERVER_FORWARDED: "true"
RUNDECK_SERVER_ADDRESS: 0.0.0.0
RUNDECK_PREAUTH_ENABLED: "true"
RUNDECK_PREAUTH_ATTRIBUTE_NAME: REMOTE_USER_GROUPS
RUNDECK_PREAUTH_DELIMITER: ","
RUNDECK_PREAUTH_USERNAME_HEADER: X-Forwarded-User
RUNDECK_PREAUTH_ROLES_HEADER: X-Forwarded-Roles
RUNDECK_PREAUTH_REDIRECT_URL: /oauth2/sign_in
RUNDECK_PREAUTH_REDIRECT_LOGOUT: "true"
oauth2-proxy:
image: quay.io/oauth2-proxy/oauth2-proxy:v7.4.0
environment:
OAUTH2_PROXY_HTTP_ADDRESS: 0.0.0.0:4180
OAUTH2_PROXY_PROVIDER: github
OAUTH2_PROXY_CLIENT_ID: <your client id>
OAUTH2_PROXY_CLIENT_SECRET: <your client secret>
OAUTH2_PROXY_COOKIE_SECRET: <your cookie secret>
OAUTH2_PROXY_SESSION_STORE_TYPE: redis
OAUTH2_PROXY_REDIS_CONNECTION_URL: redis://redis/
# メールアドレスが複数ある場合、プライマリのものが使われるので個人用と会社用など、複数のメールアドレスを登録している場合は
# organization や team などメールアドレス以外の条件を使う方がよい
OAUTH2_PROXY_EMAIL_DOMAINS: "*"
# 7.3.0 以降はこれを設定しないと動かない。スコープを必ず指定する
OAUTH2_PROXY_SCOPE: user:email
# 特定の organization に所属するアカウントのみ認証したい場合はここに organization をコンマ区切りで書く。
# OAUTH2_PROXY_GITHUB_ORG: org1,org2
# 特定の team に所属するアカウントのみ認証したい場合はここに team をコンマ区切りで書く。
# OAUTH2_PROXY_GITHUB_TEAM: team1,team2
# HTTPS ではなく HTTP で動かすための設定
OAUTH2_PROXY_COOKIE_SECURE: 'false'
OAUTH2_PROXY_REDIRECT_URL: http://localhost:8000/oauth2/callback
depends_on:
- redis
redis:
image: redis
volumes:
- redis:/data
volumes:
redis:
oauth2-proxy, Rundeck の設定については以下を参照してください。
OAUTH2_PROXY_GITHUB_ORG
と OAUTH2_PROXY_GITHUB_TEAM
を両方設定することか可能で、両方とも設定した場合は設定した org に所属し、かつ、設定した team にも所属していると認証が通る。
Nginx の設定は以下の通りです。
upstream oauth2 {
server oauth2-proxy:4180;
}
upstream rundeck {
server rundeck:4440;
}
server {
listen 80;
server_name localhost;
access_log /dev/stdout;
error_log /dev/stderr debug;
rewrite_log on;
root /usr/local/openresty/nginx/html;
index index.html index.htm;
location / {
auth_request /oauth2/auth;
error_page 401 = /oauth2/sign_in;
auth_request_set $x_github_user $upstream_http_x_auth_request_user;
auth_request_set $x_github_email $upstream_http_x_auth_request_email;
proxy_set_header X-Forwarded-User $x_github_user;
proxy_set_header X-Forwarded-Roles admin;
proxy_pass http://rundeck;
}
location /oauth2/ {
proxy_pass http://oauth2;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Scheme $scheme;
}
location /oauth2/auth {
proxy_pass http://oauth2;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Scheme $scheme;
proxy_set_header Content-Length "";
proxy_pass_request_body off;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
この設定で Rundeck に admin としてアクセスできるようになりますが、認証したユーザーのロールを動的に変更し権限を絞ることはできていません。
参考にした oauth2_proxyでRundeckにGitHub認証でログインする - Qiita ではここまでしかできていませんした。
$upstream_http_x_auth_request_user
には認証した GiiHub user の login が入っています。
最初は $upstream_http_x_auth_request_user
に値が入っていることがわからなくてとても困りました。[1]
X-Forwarded-Roles
を動的に変更する
Nginx はいくつかの言語で様々なフックを仕掛けてヘッダーを書き換えたり Nginx 内部で使用する変数を追加したりできます。
この仕組みを使って X-Forwarded-Roles
を動的に書き換えることにします。
最初は matsumotory/ngx_mruby を使ってやろうと思ったのですが、色々ハマりポイントがあって簡単には解決できそうになかったので断念しました。ハマったポイントは以下のような感じでした。
- 存在しない var にアクセスしようとしたら SEGV してレスポンスが返ってこない(せめて 500 error 返して欲しい)
- 曖昧な知識でいい加減にビルドしようとしたら永遠にビルドできない(自業自得)
- 使いたい mrbgems が使おうとしている mruby に対応しているかどうか動かしてみないとわからない
- mrbgems にバージョンという概念がなさそう
かわりに何回か使ったことのある Lua を使うことにしました。そんなに Lua 力は高くないので書く度に文法から調べてます。
version: "3.1"
services:
nginx:
# image: openresty/openresty:bullseye-fat
build: .
ports:
- 8000:80
volumes:
- ./image-files/etc/nginx/conf.d/default.conf:/etc/nginx/conf.d/default.conf
- ./image-files/etc/nginx/lua:/etc/nginx/lua
depends_on:
- oauth2-proxy
- rundeck
rundeck:
image: rundeck/rundeck:4.8.0
environment:
RUNDECK_SERVER_UUID: <your rundeck server uuid>
RUNDECK_GRAILS_URL: http://localhost:8000
RUNDECK_SERVER_CONTEXT_PATH: /
RUNDECK_SERVER_FORWARDED: "true"
RUNDECK_SERVER_ADDRESS: 0.0.0.0
# RUNDECK_DATABASE_URL:
# RUNDECK_DATABASE_DRIVER:
# RUNDECK_DATABASE_USER:
# RUNDECK_DATABASE_PASSWORD:
RUNDECK_PREAUTH_ENABLED: "true"
RUNDECK_PREAUTH_ATTRIBUTE_NAME: REMOTE_USER_GROUPS
RUNDECK_PREAUTH_DELIMITER: ","
RUNDECK_PREAUTH_USERNAME_HEADER: X-Forwarded-User
RUNDECK_PREAUTH_ROLES_HEADER: X-Forwarded-Roles
RUNDECK_PREAUTH_REDIRECT_URL: /oauth2/sign_in
RUNDECK_PREAUTH_REDIRECT_LOGOUT: "true"
oauth2-proxy:
image: quay.io/oauth2-proxy/oauth2-proxy:v7.4.0
environment:
OAUTH2_PROXY_HTTP_ADDRESS: 0.0.0.0:4180
OAUTH2_PROXY_PROVIDER: github
OAUTH2_PROXY_CLIENT_ID: <your client id>
OAUTH2_PROXY_CLIENT_SECRET: <your client secret>
OAUTH2_PROXY_COOKIE_SECRET: <your cookie secret>
OAUTH2_PROXY_SESSION_STORE_TYPE: redis
OAUTH2_PROXY_REDIS_CONNECTION_URL: redis://redis/
OAUTH2_PROXY_EMAIL_DOMAINS: "*"
OAUTH2_PROXY_SCOPE: user:email
OAUTH2_PROXY_GITHUB_ORG: org1,org2
OAUTH2_PROXY_GITHUB_TEAM: team1,team2
# OAUTH2_PROXY_PASS_USER_HEADERS: "true"
# 以下の2行で GitHub の access token を $upstream_http_x_auth_request_access_token にセットする
OAUTH2_PROXY_SET_XAUTHREQUEST: "true"
OAUTH2_PROXY_PASS_ACCESS_TOKEN: "true"
# OAUTH2_PROXY_PASS_AUTHORIZATION_HEADER: "true"
# 以下、HTTPS ではなく HTTP で動かすための設定
OAUTH2_PROXY_COOKIE_SECURE: 'false'
OAUTH2_PROXY_REDIRECT_URL: http://localhost:8000/oauth2/callback
depends_on:
- redis
redis:
image: redis
volumes:
- redis:/data
volumes:
redis:
oauth2-proxy から nginx の var に認証したユーザーの GitHub のアクセストークンを設定するために設定を追加しています。
GitHub のアクセストークンは Lua から認証したユーザーの所属するチームを取得するために使います。
FROM openresty/openresty:bullseye-fat
RUN apt-get update && apt-get upgrade -y -q
RUN opm get ledgetech/lua-resty-http
GitHub の API を叩くために ledgetech/lua-resty-http を使います。
Lua のパッケージマネージャといえば LuaRocks が定番だったと思うのですが OpenResty が出している OPM - OpenResty Package Manager を使いました。
OpenResty の Docker image を使うかどうかに関わらず LuaRocks は別途インストールしないといけないのですが OPM だと最初からインストールされているので簡単でした。
Nginx の設定はこうなります。
upstream oauth2 {
server oauth2-proxy:4180;
}
upstream rundeck {
server rundeck:4440;
}
server {
listen 80;
server_name localhost;
access_log /dev/stdout;
error_log /dev/stderr debug;
rewrite_log on;
root /usr/local/openresty/nginx/html;
index index.html index.htm;
lua_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;
lua_ssl_verify_depth 2;
resolver local=on;
resolver_timeout 5s;
# Rundeck の API は oauth2-proxy を通さない
location /api {
proxy_pass http://rundeck;
}
location / {
auth_request /oauth2/auth;
error_page 401 = /oauth2/sign_in;
auth_request_set $x_github_user $upstream_http_x_auth_request_user;
# email はいらない
# auth_request_set $x_github_email $upstream_http_x_auth_request_email;
auth_request_set $x_github_token $upstream_http_x_auth_request_access_token;
# Set $x_roles by Lua
set $x_roles "";
access_by_lua_file /etc/nginx/lua/role.lua;
proxy_set_header X-Forwarded-User $x_github_user;
proxy_set_header X-Forwarded-Roles $x_roles;
proxy_pass http://rundeck;
}
location /oauth2/ {
proxy_pass http://oauth2;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Scheme $scheme;
}
location /oauth2/auth {
proxy_pass http://oauth2;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Scheme $scheme;
proxy_set_header Content-Length "";
proxy_pass_request_body off;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
ポイントは4つです。
- 認証したユーザーの GitHub アクセストークンを Nginx の変数にセットする
-
access_by_lua_file
の前にset $x_roles "";
で変数を定義する- 空の値をセットしておかないと Lua スクリプト内で変数が見えない
-
rewrite_by_lua_file
だとauth_request_set
でセットした値が見えないのでrewrite_by_lua_file
ではなくaccess_by_lua_file
を使う - API アクセスについては oauth2-proxy を通さないようにした
- oauth2-proxy のオプションにも
--skip-auth-route
があるけど手元では動かなかった
- oauth2-proxy のオプションにも
local json = require("cjson.safe")
local http = require("resty.http").new()
-- TODO cache?
local fetch_teams = function (github_token)
local page = 1
local per_page = 100
local teams = {}
-- 認証されたユーザーが 100 を越えるチームに所属することってあんまりない気がするが念のため。
while true do
local url = string.format("https://api.github.com/user/teams?per_page=%d&page=%d", per_page, page)
local res, err = http:request_uri(
url,
{
method = "GET",
headers = {
["Content-Type"] = "application/json",
["Accept"] = "application/vnd.github+json",
["Authorization"] = "Bearer " .. github_token,
}
}
)
if not res then
ngx.log(ngx.ERR, "request failed: ", err)
return { { name = "no-team", organization = "no-organization" } }
end
local tmp_teams = json.decode(res.body)
ngx.log(ngx.DEBUG, string.format("%s %d", url, table.getn(tmp_teams)))
if table.getn(tmp_teams) == 0 then
break
end
for _, v in ipairs(tmp_teams) do
table.insert(teams, { name = v.name, organization = v.organization.login })
end
page = page + 1
end
return teams
end
if ngx.var.x_github_user and ngx.var.x_github_token then
local teams = fetch_teams(ngx.var.x_github_token)
local roles = {}
for i, team in ipairs(teams) do
ngx.log(ngx.DEBUG, "Organization: " .. team.organization)
if team.organization ~= "org1" then
goto continue
end
ngx.log(ngx.DEBUG, "Team: " .. team.name)
if team.name == "rundeck-admin" then
table.insert(roles, "admin")
end
if team.name == "rundeck-user" then
table.insert(roles, "user")
end
::continue::
end
ngx.var.x_roles = table.concat(roles, ",")
else
ngx.log(ngx.WARN, "missing user/token")
ngx.var.x_roles = "nogroup"
end
検証用の実装なので非効率な部分がいくつかあります。
- cjson がリクエストごとに初期化される
-
init_by_lua*
で Nginx の初期化時に読み込むこともできる
-
- 認証されたユーザーが所属するチームを全て取得するためにリクエストごとに2回 API を叩いている
- Redis などを使って結果をキャッシュすれば API を叩く回数は減らすことができるはず
- Rundeck 用のロールに関してハードコーディングしている
- 頻繁に変わりそうなら GitHub のチームと Rundeck のロールの関係を外部で定義できるようにした方がよさそう
まとめ
少し設定を工夫することで Rundeck に手を加えずに oauth2-proxy で認証しつつ Rundeck のロールを設定することができました。
oauth2-proxy は OIDC をサポートしているので OpenID Connect に対応している任意の IDaaS も使うことができそうです。そういった IDaaS でちょっと機能が足りないなどあれば、この仕組みを使ってその IDaaS の API を叩けばちょっとユーザー名を変換したり、ちょっとロールをカスタマイズしたり簡単にできると思います。
参考
- Docker で OAuth2 Proxy を動かすサンプル - Qiita
- oauth2_proxyでRundeckにGitHub認証でログインする - Qiita
- 逆引きlua-nginx-module
- Lua | NGINX
- openresty/lua-nginx-module
-
Nginx で簡単に var の一覧を取得する方法はないのでしょうか? ↩︎
Discussion