Basic認証でBalancing
同じURIにアクセスしても、Basic認証で使用したユーザ名によって、管理用のアプリケーションにProxyしたり、ユーザ用のアプリケーションにProxyしたりする方法を考える。
具体的な例としては、mongo-expressでWeb UIを通してMongoDBのデータを確認したいものの、ログインするBasic認証アカウントによって、書き込み権限を持ったMongoDBユーザを使用して起動しているmongo-expressにつなぐのか、読み込み権限だけを持ったMongoDBを使用して起動しているmongo-expressにつなぐのかを分けたい。
今回使用したミドルウェアのversion
- mongo-express: 0.47.0
- Nginx: 1.11.12
mongo-express
Basic認証によるBalancingの話の前に、権限別で複数アカウントのmongo-expressをどのように動かしているのかを説明する。
mongo-expressにはinitスクリプトがないため、簡易的に以下のように作成した。
$ cat /etc/init.d/mongo-express
#!/bin/bash
#
# chkconfig: - 85 15
# description: mongo-express.
case "$1" in
start)
source /etc/profile.d/nvm.sh
nohup mongo-express -d db_express_test -u r_user -p PASSWORD1 --port 8081 >/dev/null 2>&1 &
nohup mongo-express -d db_express_test -u rw_user -p PASSWORD2 --port 8082 >/dev/null 2>&1 &
;;
*)
echo $"Usage: $0 start"
RETVAL=3
esac
exit $RETVAL
読み込み権限を持ったMongoDBユーザ(r_user)でMongoDBに接続するmongo-expressを8081ポートで起動し、読み書き両方の権限を持ったMongoDBユーザ(rw_user)でMongoDBに接続するmongo-expressを8082ポートで起動している。
直接http://FQDN:8081/
とhttp://FQDN:8082/
にアクセスすれば、それぞれで権限を分けることができるが、同じFQDNと同じポートでアクセスさせたい場合、Webサーバでリバースプロキシしてリクエストを割り振る必要がある。
Nginxと$remote_user
Basic認証を行うと、$remote_user
にBasic認証で使用したユーザ名が入る。ApacheのREMOTE_USER
と同じ。
そのためlocation /
内でBasic認証をした後に$remote_user
を参照してProxy先を分けるロジックを記載することができる。
location / {
auth_basic "Restricted";
auth_basic_user_file /etc/nginx/.htpasswd;
if ($remote_user = "admin") {
proxy_pass http://localhost:8081;
}
if ($remote_user != "admin") {
proxy_pass http://localhost:8082;
}
}
Nginxのifをlocation内で書くことはIf Is Evilと言われている。
Directive if has problems when used in location context, in some cases it doesn’t do what you expect but something completely different instead.
今回の記述では問題なく動いているが、何かカスタマイズする際はNginxのlocation内でのifの動きに注意することに加えて、Luaスクリプトで代替するなども検討した方がいい。
2021/02/02 追記: nginx if
If Is Evilではifを使っても問題ないパターンとして以下の2点が挙げられている。
- return ...;
- rewrite ... last;
今回のケースで動作確認はしていないが、別件でifを使った際にはこの問題ないパターンで実装した。今回のケースでは以下のように記載すれば動くはず。
location / {
auth_basic "Restricted";
auth_basic_user_file /etc/nginx/.htpasswd;
if ($remote_user = "admin") {
rewrite ^/(.*) /admin_location_not_exist_in_app/$1 last;
}
if ($remote_user != "admin") {
rewrite ^/(.*) /user_location_not_exist_in_app/$1 last;
}
}
location /admin_location_not_exist_in_app/ {
proxy_pass http://localhost:8081/;
}
location /user_location_not_exist_in_app/ {
proxy_pass http://localhost:8082/;
}
ポイントは以下の点
rewrite ... last;
を使用rewrite ... last;
でURIを書き換えて再度locationマッチを行い、適切なproxy先へ繋ぐproxy_pass
の末尾に/
を入れることで、自分で付与した余分な部分のURIを削除してproxy先へ繋ぐ
※最後のproxy_passの/
有無については公式ドキュメントを参照
- If the proxy_pass directive is specified with a URI, then when a request is passed to the server, the part of a normalized request URI matching the location is replaced by a URI specified in the directive:
- If proxy_pass is specified without a URI, the request URI is passed to the server in the same form as sent by a client when the original request is processed, or the full normalized request URI is passed when processing the changed URI:
Nginxと$remote_userとifの動作確認
mongo-expressを実際に動かして確認するのはWebブラウザで行うとして、Nginxだけでうまくlocation内のifが動作しているか簡単に確認してみる。Nginxのconfに以下を記載する。
server {
listen localhost:8081;
server_name localhost;
location / {
return 200 "admin app is running on port 8081\n";
}
}
server {
listen localhost:8082;
server_name localhost;
location / {
return 200 "user app is running on port 8082\n";
}
}
server {
listen localhost:443;
server_name localhost;
ssl on;
ssl_certificate /etc/pki/tls/certs/server.crt;
ssl_certificate_key /etc/pki/tls/private/server.key;
ssl_session_timeout 5m;
ssl_protocols TLSv1.2;
ssl_ciphers ALL:!ADH:!EXPORT:!SSLv2:RC4+RSA:+HIGH:+MEDIUM:+LOW;
ssl_prefer_server_ciphers on;
location / {
auth_basic "Restricted";
auth_basic_user_file /etc/nginx/.htpasswd;
if ($remote_user = "admin") {
proxy_pass http://localhost:8081;
}
if ($remote_user != "admin") {
proxy_pass http://localhost:8082;
}
proxy_redirect http:// https://;
}
}
この設定でNginxを再起動した後、curlでBasic認証を突破させる。
$ curl -u user:user -k https://localhost/
user app is running on port 8082
$ curl -u admin:admin -k https://localhost/
admin app is running on port 8081