Nginxでクライアント認証したい
環境
uname -a
# Linux tmsc4zht-dev 5.4.0-1042-gcp #45-Ubuntu SMP Tue Apr 13 01:44:53 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux
nginx -v
# nginx version: nginx/1.18.0 (Ubuntu)
Nginxでクライアント認証したい。
サーバー側のCAはLet's Encryptで作った鍵じゃだめなのかな。
まずはよく紹介されている方法で動くか確認しよう。
PKI基盤から作らなきゃいけないのか。
PKIはPublic Key Infrastructureの略だからPKI基盤って言うと意味が被る…
デフォルトの?CA設定は /usr/lib/ssl
にある。
これとは別にPKI用のディレクトリを作成したほうが良さそう。
とりあえず /etc/opt/pki
を作成する。
後のコマンドは以下を参考した。
ただしクライアント証明書は /etc/opt/pki/client
に作成した。
PKCS#12形式への変換
openssl pkcs12 -export -inkey user_key.pem -in user_crt.pem -out user_crt.pfx
ブラウザからSSH接続してpfxをダウンロードしようとしたができない。原因はわからない。
GCPのCloud Storage経由でやることにする。
今は以下の条件が無料枠
- 5 GB-月の Standard Storage
- 5,000 回のクラス A オペレーション(1 か月あたり)
- 50,000 回のクラス B オペレーション(1 か月あたり)
- 1 GB の北米から全リージョン宛ての下り(外向き)ネットワーク(1 か月あたり、中国とオーストラリアを除く)
- 無料枠を利用できるのは、us-east1、us-west1、us-central1 のみです。使用量の計算は、これらのリージョンすべての合計が集計されます。
computeがus-westにあるのでそれに合わせてバケットを作った。
次のコマンドでクライアント証明書をバケットに置く。
gsutil cp ./user_crt.pfx gs://{backet_name}
403エラーが出た。権限が足りてないので追加。
だめだった。なんで?
→キャッシュのせいでした。多分。
gcloud auth list
で表示されるGCEにのアカウントをIAM管理から確認して、GCSオブジェクト作成のロールをつけたらアップロードできるようになった…
正しいやり方か不明…
Cloud API アクセス スコープではR/W許可を無効にしても動いた。
まあもとのサービスアカウントで許可しているから当たり前か。
これがうまく設定できていないようだ
ともかくクライアント認証を手元に持ってくることはできた。
→キャッシュのせいでした。多分。
この設定は不要になったので戻しました。
nginxをclient証明を要求する設定にする。
server {
listen 443 ssl default_server;
# 略
ssl_verify_client on;
ssl_client_certificate /etc/opt/pki/RootCA/RootCA_crt.pem;
}
クライアント証明書がチェーン形式じゃなかったので、こちらが証明書を提供できず。
400エラーになるようだ。
チェーン形式でPKCS#12形式のファイルに纏め直す。
openssl pkcs12 -export -in user_crt.pem -inkey user_key.pem -certfile ../RootCA/RootCA_crt.pem -out user_crt.pfx
作り直して、またCloud Storageにアップロードしようとしたらまた403エラー
どういうこと。
/ .gsutil
に認証情報のキャッシュが残ってるとうまく動かないことがあるらしい。
もういちどnoriko3さんの設定をしてから該当のフォルダを削除したらアップロードできるようになった。
つまり
- noriko3さんの設定で動くようにはなるはずだった
- 認証情報のキャッシュが働き接続できない状態が維持されていた
- IAMで設定してる間に前のキャッシュが消え、新たに認証情報を作り直したためアップロードできるようになった
- IAMのせいだと思い1の設定を戻した
- しばらくはキャッシュで動いたが、そのうち消えて動かなくなった
- もう一度1の設定をして、キャッシュを消したので新たに認証情報を作り直しアップロードできるようになった
ということ?
IAMの設定を消して、キャッシュを消してアップロードを試す。
動いた。
そんな…
普通にこれ見ればよかったですね。
chromeは再起動しないとクライアント証明書を認識しないみたい。
Let's Encryptで取得した証明書と秘密鍵で署名してみる。
cd /etc/opt/pki/client
openssl genrsa -des3 -out user.key 4096
openssl req -new -key user.key -out user.csr
openssl x509 -req -days 365 -in user.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out user.crt
クライアント証明書選択ダイアログが出ない。
よく考えたら、nginxのCAの設定も書き換えなきゃいけないのかもしれない。
この署名チェーンのルート証明書をダウンロードしてssl_client_certificate
のパスに設定。
クライアント証明書選択ダイアログが出ない。
このサイトにはssl_verify_depath
も必要との記載がある。
ssl_verify_depth 4;
クライアント証明書選択ダイアログが出ない。
どういう条件でクライアント証明書選択ダイアログが出るのかよくわからない。
→わかった(次)
原理的はできるようだが。
そもそもプライベートCA使ったほうがいいよとの記載があり、ガイドのリンクもある。
It’s much more better to generate a private CA (and intermediate CA as well as client certificate) to use with authentication.
For now, the best (and complete) guide to this is
https://jamielinux.com/docs/openssl-certificate-authority/
正統?にこちらの選択をとる。
こちらに記載の通りにすすめる。
変更点が少しある。
以下の値をoptionalにした。
これらは別になくていい。
[policy_strict]
stateOrProvinceName = optional
organizationName = optional
参考
このセクションではCAが発行する証明書のポリシーを記述する。
CAは依頼者からCSR(証明書要求)を受け取って証明書を発行するわけだが、全てのCSRに対して証明書を発行するべきではない。
全く知らない人から来たCSRを本人の身元を確認もせずに発行された証明書は信用度があまりないからである。
そこで、CAではどのようなCSR(証明書要求)に対して証明書を発行するかを決めた「ポリシー」を設定する。
CSRのSubjectの部分がどのような場合に証明書を発行するかについて以下のようにポリシーを設定できる。
- match
- CAの内容と一致しなければならない
- optional
- 無くてもよい
- supplied
- なければならない
基本的にはコマンドどおりに行えばOK。
Sign server and client certificatesではコマンド記述がサーバー用のものになっているのでクライアント用として少し書き換える必要がある。
ここはextensions
をusr_cert
にするとよい。
openssl ca -config intermediate/openssl.cnf \
-extensions server_cert -days 375 -notext -md sha256 \
-in intermediate/csr/www.example.com.csr.pem \
-out intermediate/certs/www.example.com.cert.pem
ファイル名やCNもサーバー用になっているので適当に変える。
ここではCNは(サーバー名) client
、ファイル名はclient
とした。
こうなる。
openssl ca -config intermediate/openssl.cnf \
-extensions usr_cert -days 375 -notext -md sha256 \
-in intermediate/csr/client.csr.pem \
-out intermediate/certs/client.cert.pem
PKCS#12でクライアント証明書・鍵ペアを作成
cd /etc/opt/pki/ca/intermediate/certs
openssl pkcs12 -export -in client.cert.pem -inkey ../private/client.key.pem -certfile ca-chain.cert.pem -out ../private/client.cert.pfx
よく読み直したら以下の記述があった。
ssl_verify_client on の場合, ssl_client_certificate ディレクティブが必須。ここで指定されたファイルの証明書 (複数可) がクライアントに送られ、クライアント側 (Webブラウザ) で, クライアント証明書の候補を表示する。
なるほど。そういうことか…
ssl_client_certificate
とssl_trusted_certificate
を混同していた。
つまり今の状態ではこう設定する。
ssl_client_certificate /etc/opt/pki/ca/intermediate/certs/intermediate.cert.pem;
ssl_trusted_certificate /etc/opt/pki/ca/certs/ca.cert.pem;
動いた!
ちなみにssl_trusted_certificate
をコメントアウトするとまた400エラーになった。
やはり正統な証明書として認められないようだ。
どの発行機関の証明書を要求するかと、送られてきた証明書を信用するかどうかは別の話だよな。そりゃそうだ。
技術的には可能と言われているところをできれば、考え方があっていたということになると思う。
Let's Encryptで取得した証明書によってクライアント証明書を署名しよう。
まず中間証明書用のディレクトリとかをつくる。
mkdir /etc/opt/pki/{ドメイン名}
cd /etc/opt/pki/{ドメイン名}
mkdir certs crl csr newcerts private
chmod 700 private
touch index.txt
echo 1000 > serial
Let's Encryptで証明書を取得したときに作った鍵とその証明書のリンクを作る・
ln -s (既存の鍵) private/intermediate.key.pem
ls -s (既存の証明書) certs/intermediate.crt.pem
# ls -s (既存の証明書チェーン) certs/ca-chain.cert.pem
中間証明書用のopenssl.cnf
をつくる。
vi openssl.cnf
クライアント用の秘密鍵を作る。
openssl genrsa -aes256 \
-out private/client.key.pem 2048
chmod 400 private/client.key.pem
署名要求をつくる。
openssl req -config openssl.cnf \
-key private/client.key.pem \
-new -sha256 -out csr/client.csr.pem
署名する。とりあえず期限は60日にする。(Let's Encryptで署名した証明書の期限が90日なのでそれより短いものにした)
openssl ca -config openssl.cnf \
-extensions usr_cert -days 60 -notext -md sha256 \
-in csr/client.csr.pem \
-out certs/client.cert.pem
ちゃんと出来てるか確認。
openssl x509 -noout -text \
-in certs/client.cert.pem
PKCS#12に変換する。
cd private
openssl pkcs12 -export -in ../certs/client.cert.pem -inkey client.key.pem -certfile ../certs/intermediate.crt.pem -out client.cert.pfx
クライアント証明書をインストールしたらnginxの設定を変える。
ssl_client_certificate /etc/opt/pki/{ドメイン名}/certs/intermediate.cert.pem;
ssl_trusted_certificate /etc/opt/pki/{ドメイン名}/certs/intermediate.cert.pem;
ssl_verify_depth 10;
とりあえずクライアント証明書の選択画面と選択はできるようになった。
が認証が通らないぞ。
デバッグログを出力する。(最初からしておけば…)
To enable writing the debugging log to a file:
Make sure your NGINX is configured with the --with-debug configuration option. Run the command and check if the output contains the --with-debug line:$ nginx -V 2>&1 | grep -- '--with-debug'
Open NGINX configuration file:
$ sudo vi /etc/nginx/nginx.conf
Find the error_log directive which is by default located in the main context, and change the logging level to debug. If necessary, change the path to the log file:
error_log /var/log/nginx/error.log debug;
Save the configuration and exit the configuration file.
設定を読み込み、アクセスし、エラーログを見るとこんな記載が。
*9 client SSL certificate verify error: (21:unable to verify the first certificate) while reading client request headers, client: ***.***.***.***, server: {domain}, request: "GET /favicon.ico HTTP/1.1", host: {domain}, referrer: {domain}
どうやら最初の証明書のverifyができないとの記載。なら最初の署名をssl_certificate
にルート証明書を指定すればいいのか?
エラー文面でググる。
スウェーデン語の情報があった。どうやらチェイン証明書にする必要がありそう。
ルート証明書を含むチェーン証明書にしたが同じエラーメッセージとなった。
# openssl verify -CAfile intermediate.cert.pem certs/client.cert.pem
error 20 at 0 depth lookup:unable to get local issuer certificate
そもそも証明書として正しいか不安になってきた。
Also seems the in ca extension you need set basicConstraints = CA:true otherwise I still encounter openssl verify report error.
多分これだと思う
前に作成したもののオプションを変えて同じエラーが出ればそれが原因と言えそうだ。
Let's Encryptで得られた証明書はCA用ではないので、そのあたりでエラーが出てそうだ。
証明書チェーンの中でpathlenが0のCA証明書の次はサーバー証明書じゃないといけない。この設定は知っておいた方がよさそうだ。
Let's Encryptの pathlen
は0だった。なるほどね...
つまりできないということだろう。
これで終わりとする。