Last Updated on 2022年9月16日 by かんりにん
AWS EC2でセットアップし、自社内で利用しているredashを、社内の担当者と外注先とで共用しつつ、セキュリティのためSSLクライアント認証を導入したいとの依頼を受け対応。
ただし従業員のほうが利用者が多いので、ちょっと融通を利かせる必要が。
具体的には
- グローバルに公開する
ほんとはVPNなりセキュリティグループでの制限をしたかったが、外注先が固定IPではないので( ^ω^)・・・ - 会社からアクセスする場合も、VPNを経由せずグローバルからアクセスさせる。外注先からレポートをもらうとき、同じドメインを見るようにしたい、とのこと(DNSの設定で制約があったため、インターナルゾーンが使えなかった…)
- 指定したIPアドレス(会社のゲートウェイIP)からは、証明書なしでアクセスを許可
- 指定したIPアドレス以外からは、証明書が必要(外注さんに使ってもらう)
- 証明書がない場合はエラーを返す
といった感じで実装してみる。
▼構成
- webアプリはredashを使う
- サーバはAWS EC2インスタンス(redashが使えるやつ)
- EIPあり
- httpサーバはnginx
- サイトアクセスはSSLのみにする
- 証明書は自己署名認証局を立てる(いわゆるオレオレ)
ルート証明書は利用者全員に配布する。
▼情報収集
apacheでは変数を設定することで許可させることが可能。
参考:お世話になっております!
Apacheのクライアント認証をIPアドレスによっては不要にしてみる
ただし、nginxではssl moduleの埋め込み変数でIPアドレスを定義する変数が無い様子。
- nginx ssl moduleの埋め込み変数で、IPアドレスを定義する変数が無い
http://nginx.org/en/docs/http/ngx_http_ssl_module.html (英語)
http://mogile.web.fc2.com/nginx/http/ngx_http_ssl_module.html#ssl_client_certificate (日本語)
と同時に、apacheのSSLRequire に該当する条件式をif文で定義することは可能。
ただしnginx公式サイトではif文での条件式を推奨していない)
参考:NGINX:If Is Evil
https://www.nginx.com/resources/wiki/start/topics/depth/ifisevil/ (英文)
http://mogile.web.fc2.com/nginx_wiki/nginx_wiki201701/start/topics/depth/ifisevil/ (日本語)
今回はlocationでなくserverでif文を使うようにしたので、変な動きはしないと思う…(という願い)
結局ほかにうまい方法が見当たらなかったので、IPアドレスを指定した条件式をif文で書くことに。
参考:お世話になっております!
- IPアドレスによる条件式作成のオプション
Nginx でIPアドレスを使って国を判断する(geo を使ってみる)、おまけで携帯キャリアを判定して携帯サイトへ転送させてみる - sslクライアント認証のオプション
CN-based client authentification with nginx. This emulates Apache’s SSLRequire (%{SSL_CLIENT_S_DN_CN} in {“Really Me”})
Nginx verifying client certs only on a particular location
▼動作確認が出来た、認証時のIP振り分け設定
結局、(やりたくなかったが)ifで条件式を設定する形で何とか対応した。
やったことまとめ:
- apacheの埋め込み変数でのIP指定にあたる部分は、nginxのgeoモジュールを使って許可するIPを変数に指定(if文の条件式に使う)。
ngx_http_geo_module - if文での条件式に対応させるためssl_verify_client の指定を“optional”にする
- 条件式にてgeo変数で指定したIPアドレスを評価し、OKの場合は認証時の埋め込み変数”ssl_client_verify”にSUCSESSを入れて認証成功状態にして処理を通す。
http://nginx.org/en/docs/http/ngx_http_ssl_module.html#var_ssl_client_verify - 指定したIP以外は通常に認証を行う。ssl_client_verifyにてSUCSESS出なかった場合は403を返す
ssl_verify_client で”optional”を指定した場合、はクライアント側の証明書の有無を判別するもののリクエストの拒否はしないので、if文で何らかの形で拒否をする設定が必要となる。ちなみにssl_verify_client をyesで設定した場合の認証エラー時はステータスコード400(Bad request)になる。
■設定抜粋:
最終的にはapache httpdと同じような感じで実装が出来た。
http { <--snip--> # geoディレクティブで許可するIPを指定し、ステータスOKとする。 geo $geo { default disable; 203.0.113.254 allow; } <--snip--> server { # if文での条件式に対応させるため、optionalを指定する。 ssl_verify_client optional; # クライアントとの接続元IPが変数"$geo"に合致した場合、"ssl_client_verify"の値に"SUCCESS"をsetして認証成功扱いにする。 if ($geo = 'allow') { set ssl_client_verify "SUCCESS"; } # 変数"$geo"に合致しないIPからのアクセスの条件式はこちら。 # "ssl_verify_client optional"の補完として、認証に失敗したクライアントには403を返す。 if ($ssl_client_verify != SUCCESS) { return 403; } <--snip--> } }
■上記を反映させた、実際の設定(IPとかホスト名は変えてあります):
upstream rd_servers { server 127.0.0.1:5000; } # 後述する"ssl_client_verify"での判定時に、自社のIPを許可するためgeoモジュールを使用して # 自分の会社のゲートウェイIPアドレスを許可する。 geo $geo { default disable; 203.0.113.254 allow; } # 80 -> 443 http -> https リダイレクト server { listen 80; server_name host.example.com; location / { return 301 https://$host$request_uri; } } server { server_tokens off; listen 443 ssl; server_name host.example.com; # サーバ側SSL設定 ssl_certificate /etc/ssl/CA/certs/host.example.com.crt; ssl_certificate_key /etc/ssl/CA/private/host.example.com.key; # クライアント証明書を要求する設定 # if文での条件式に対応させるため、optionalを指定する。 ssl_verify_client optional; ssl_client_certificate /etc/ssl/CA/cacert.pem; access_log /var/log/nginx/rd.access.log; gzip on; gzip_types *; gzip_proxied any; ## IP access limitation 2018-03-23 # 自社GWからアクセスする場合は許可する if ($geo = 'allow') { set $ssl_client_verify "SUCCESS"; } # 社外からのアクセスには証明書が必要。認証が通らない場合は403を返す。 if ($ssl_client_verify != SUCCESS) { return 403; } location / { proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_pass http://rd_servers; } }
ひとまず上記で要件はクリア。ラーメン食べに行ってきます!