Nginx と自前の認証システムを組み合わせてセキュアなリソースを制限する

こんにちは、Pythonエンジニアの @kimihiro_n です。
ブログを書くとハイボールが飲め… 会社のエンジニアブログをはてなに作ってもらったので初投稿してみます。


Nginx で静的なファイルを配信する際に、認証をかませて配信対象を制限したいときってありますよね。 ページ自体のHTMLを表示する際にはログインが必須だけれども、そこで使っている画像やCSS, JSはそのまま見れてしまうという状況は場合によって好ましくありません。

静的ファイルに対する制限を手っ取り早く実現するには、 Basic 認証をかけてあげるのがシンプルです。ただ Basic 認証だとブラウザ側のダイアログが出てしまったりして、サービスとしてそのまま使いづらいです。DBとは別にユーザーの管理が必要になったりもしますし実用性に欠いてしまいます。

すでにログインのシステムが自前であるのであれば、システムと連携してよしなに出し分けできると嬉しいですね。Nginx には http_auth_request_module というモジュールが用意されており、これを用いることで任意の認証バックエンドと連携してリソースのアクセスを制限することが可能です。

http_auth_request_module とは

http://nginx.org/en/docs/http/ngx_http_auth_request_module.html

ngx_http_auth_request_module は Nginx 1.5.4 以降で導入されたカスタムモジュールです。リソースへアクセスする際に、プレリクエストとして認証サーバに問い合わせを行い、その結果に応じてアクセス制御を行います。 モジュールはオプションなので自分で Nginx をコンパイルする場合は、--with-http_auth_request_module というフラグを有効にしてコンパイルする必要があります。手元の Nginx に含まれているかどうかは以下のコマンドで確認が可能です。

nginx -V 2>&1 | grep -- 'http_auth_request_module'

Docker Hubにあがっている公式 Image の nginx:alpine にはすでに有効化された状態で入っていました。

$ docker run nginx:alpine nginx -V 2>&1 | grep -- http_auth_request_module
configure arguments: ...()... --with-http_auth_request_module ...()...

図: 連携イメージ f:id:nsmr_jx:20180822192711p:plain

ユーザーがプライベートなファイルにアクセスしようとすると、Nginx が 認証先として指定したサーバーへ確認のリクエストを飛ばしてくれます。認証側のサーバーは、リクエストを見て認証済かどうかを返します。この返り値に使われるのがステータスコードで、200番台のときは許可、401や403を返すと不許可となります。許可であれば Nginx はプライベートファイルをユーザーに返してくれます。 ステータスコードだけ通ればいいので、既存のログインシステムとかでも連携しやすそうですね。

実際に試してみる

https://github.com/pistatium/nginx_auth_sample

Docker-compose で簡単に試せる環境をつくってみました。

f:id:nsmr_jx:20180822191119p:plain:w200

Login ボタンを押すとログイン状態になり、/private/ 以下に置いた iframe 内のコンテンツを見ることができます。Logout ボタンを押せば再び見れなくなります。

server {
    server_name _;
    listen      80;
    access_log  /dev/stdout;
    error_log  /dev/stderr warn;
    root        /var/www/html;
    index  index.html;

    location /private/ {
        auth_request /auth/is_login;
    }
    
    location /auth {
        proxy_pass http://backend:8888;
        proxy_redirect off;
        proxy_set_header   Host $http_host;
    }
}

Nginx の設定はこんな感じです。 location /private/ のディレクティブで auth_request /auth/is_login; という指定をしています。これは /private/ 以下のファイルへアクセスする際に、/auth/is_login へ権限があるかを確認しにいくよう指定しています。/auth/is_login が 200 番台のステータスコードを返してくれればファイルを見ることができます。

設定の肝はこれだけなのですが、認証サーバーが別途ないことには試せないので、Python + Flask で擬似ログインシステムを作りました。/auth 以下のリクエストはすべてこのサーバーへ飛んでいきます。

@app.route('/auth/is_login')
def is_login():
    if not request.cookies.get(SESSION_KEY):
        abort(401)
    return ''  # 200 (204 を返してもいいかもしれない)

/auth/is_login のコード抜粋です。疑似ログインなので、特定の Cookie がセットされてればログインしたことにしています。実際は DB を見にいったりして正しいログイン状態であるかチェックする必要があります。 Nginxへ返すレスポンスは「ログインしていれば空のレスポンス(ステータスコード200)」 を、「ログインしていなければ401を返す」だけなので簡単ですね。 なお、プライベートなファイルにアクセスするたびに Nginx から認証のプレリクエストが飛ぶので、スループットを上げるには別途キャッシュなどの工夫が必要になりそうです。

これだけで Nginx のプロキシに対して独自の認証を組み込むことができました。静的なファイルの配信ではアクセス制御がおざなりになりがちですが、Nginx 上で弾けるのであれば積極的に使っていきたいですね。

参考