Keycloakでクライアント証明書認証を試してみた


# やること
[NRI OpenStandia Advent Calendar 2020](https://qiita.com/advent-calendar/2020/nri-openstandia)の13日目は、Keycloakでクライアント証明書を使った認証を試してみます。
本記事で扱う証明書はSSLサーバ証明書とクライアント証明書があり、それぞれ次のような特徴があります。

- SSLサーバ証明書はサーバにインストールされ、サイトの実在性を認証します。
- クライアント証明書は、個人や組織を認証し発行される証明書のことで、システムやサービス等を利用するユーザのデバイスに証明書をインストールし、そのユーザが正規の利用者であることを認証します。

いずれの証明書もCA(Certificate Authrority)によって署名されることで、真正性が保証された証明書として利用できるようになります。
本記事での証明書と認証局の関係は下図のとおりで、ルート認証局A、中間認証局AはKeycloakサーバのSSLサーバ証明書に信頼性を与えるための認証局で、ルート認証局B、中間認証局Bはクライアント証明書に真正性を与えるための認証局です。

![cert1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/857744/a2daf0ca-9c74-3de9-98a9-92247a8faa25.png)

今回は、クライアント証明書のSubjectのe-mail属性をアイデンティティソースとして、keycloakのユーザとマッピングさせ認証を行います。システム構成としては、Keycloakの前段にApacheを配置してSSLの紐解きはApacheで行う構成としています。

## 証明書作成
それでは証明書の作成を行っていきます。今回必要となる証明書はルート認証局の証明書、中間認証局の証明書、クライアントとKeycloakサーバの証明書となっています。ルート認証局、中間認証局については上図のとおりA,Bそれぞれ作成していきます。
手順の概要は以下のとおりです。

1. ルート認証局の秘密鍵を作成（A,Bそれぞれ作成）
2. ルート認証局の証明書を作成（A,Bそれぞれ作成）
3. 中間認証局の秘密鍵を作成（A,Bそれぞれ作成）
4. 中間認証局の証明書署名要求を作成（A,Bそれぞれ作成）
5. 中間認証局の証明書を作成（A,Bそれぞれ作成）
6. SSLサーバ証明書の秘密鍵を作成
7. SSLサーバ証明書の証明書署名要求を作成
8. SSLサーバ証明書を作成
9. クライアント証明書の秘密鍵を作成
10. クライアント証明書の証明書署名要求を作成
11. クライアント証明書を作成

証明書の作成には[openssl](https://www.openssl.org/)を使用しています。opensslのコマンドの詳細については割愛しています。興味がある方は参考文献のリンクをご確認ください。

## ルート認証局の証明書作成
はじめに下図の赤丸の部分にあたるルート認証局の秘密鍵を作成します。コマンド実行時に秘密鍵のパスワードを何にするかの入力を求められるので、適当に入力してください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/857744/e78843bb-cc78-f119-900c-94622c057d6c.png)

```:ルート認証局の秘密鍵作成
# openssl genrsa -out rootCA1.key -des3 2048
```
作成したルート認証局の秘密鍵を使ってルート認証局の証明書を作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/857744/cd5d297b-05e4-ac91-708e-644add0fffc7.png)

```:ルート認証局の証明書作成
# openssl req -new -x509 -key rootCA1.key -sha256 -days 3650 -extensions v3_ca -out rootCA1.pem -subj "/C=JP/ST=Tokyo/O=rootCA1/CN=rooCA1"
```

以上でルート認証局の秘密鍵と証明書の作成が完了です。

## 中間認証局の証明書作成
ルート認証局と同様に中間認証局の秘密鍵も作成していきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/857744/8fb8e0e5-ddbd-7f18-1d6f-c9beef4769d9.png)


```:中間認証局の秘密鍵作成
# openssl genrsa -out inCA1.key -des3 2048
```

ルート認証局の証明書は、いきなり証明書を発行しましたが、中間認証局の証明書は、ルート認証局の証明書が署名し作成するので、中間認証局では、CSR(Certificate Signing Request)と呼ばれる証明書署名要求を作成します。 中間認証局が自身の秘密鍵を使って証明書を発行しないのは、中間認証局単独で真正性を証明できないためです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/857744/430da431-c244-b8cb-c8c7-e091b23ec4e1.png)

```:中間認証局の証明書署名要求作成
# openssl req -new -key inCA1.key -sha256 -outform PEM -keyform PEM -out inCA1.csr -subj="/C=JP/ST=Tokyo/O=inCA1/CN=inCA1"
```

中間認証局の証明書署名要求から中間認証局の証明書を作成します。中間認証局Aで作成したCSRとルート認証局Aの秘密鍵と証明書から中間認証局Aの証明書を作成しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/857744/1d3970e8-eec9-06a6-ad85-80e581128606.png)


```:中間認証局の証明書作成
# openssl x509 -extfile openssl_sign_inca.cnf -req -in inCA1.csr -sha256 -CA rootCA1.pem -CAkey rootCA1.key -set_serial 01 -extensions v3_ca -days 3650 -out inCA1.pem
```

```:openssl_sign_inca.cnf
[ v3_ca ]
basicConstraints = CA:true,pathlen:0
keyUsage = cRLSign,keyCertSign
nsCertType = sslCA,emailCA
```

作成した中間認証局の証明書の内容を確認すると、Issuerにルート認証局のSubjectが設定されていることがわかります。

```
# openssl x509 -text -noout -in inCA1.pem 
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number: 1 (0x1)
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: C = JP, ST = Tokyo, O = rootCA1, CN = rooCA1
        Validity
            Not Before: Dec 12 12:54:05 2020 GMT
            Not After : Dec 10 12:54:05 2030 GMT
        Subject: C = JP, ST = Tokyo, O = inCA1, CN = inCA1
        Subject Public Key Info:
以下略
```
## KeycloakサーバのSSLサーバ証明書作成
次に、KeycloakサーバのSSLサーバ証明書作成を行います。秘密鍵とCSRを順に作成しても良いのですが、同じ手順の繰り返しになるため、opensslの設定ファイルを使って、一度にKeycloakサーバの秘密鍵とKeycloakサーバの証明書署名要求を作成しています。
設定ファイルの内容は割と適当ですが、作成する証明書のSANs(Subject Alternate Names)に設定される`alt_names`には証明書のCN(Common Name)を含むように設定してください。

```conf:openssl_sign_server.cnf
[ ca ]
default_ca = CA_default

[ CA_default ]
x509_extensions = user_cert
private_key = inCA1.key
new_certs_dir = ./

[ v3_ca ]
basicConstraints = CA:true,pathlen:0
keyUsage = cRLSign,keyCertSign
nsCertType = sslCA,emailCA

[ req ]
default_bits = 2048
default_keyfile = server.key
distinguished_name = req_distinguished_name
attributes = req_attributes
x509_extensions = v3_ca
req_extensions = v3_req

[ req_distinguished_name ]
countryName = JP
countryName_default = JP
countryName_min = 2
countryName_max = 2

stateOrProvinceName = Tokyo
0.organizationName = Keycloak
commonName = example.jp

emailAddress = server@example.jp

[ req_attributes ]

[ usr_cert ]
basicConstraints=CA:FALSE
nsComment = "OpenSSL Generated Certificate"
subjectKeyIdentifier = hash
authorityKeyIdentifier=keyid,issuer:always
subjectAltName = @alt_names

[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation,digitalSignature,keyEncipherment
subjectAltName = @alt_names

[ alt_names ]
DNS.1 = DNS:example.jp
```

```:秘密鍵と証明書署名要求を作成
# openssl req -new -newkey rsa:2048 -nodes -out server.csr -keyout server.key -sha256 -config openssl_sign_server.cnf -subj "/C=JP/ST=Tokyo/O=Keycloak/CN=example.jp"
```

Keycloakサーバの証明書署名要求からKeycloakサーバのSSLサーバ証明書を作成します。

```:Keycloakサーバの証明書作成
# openssl x509 -req -in server.csr -sha256 -CA inCA1.pem -CAkey inCA1.key -set_serial 01 -days 3650 -extfile san.txt -out server.pem
```

```:san.txt
subjectAltName = DNS:example.jp
```

## クライアント証明書作成
クライアント証明書を作成します。クライアント証明書に署名する認証局は中間認証局Bなので、クライアント証明書を作成する前に、ルート認証局Bと中間認証局Bを作っておく必要があります。認証局作成の手順については、前述したルート認証局の証明書作成と同様の手順のため割愛します。
他の証明書と同様、秘密鍵を作成します。

```:クライアントの秘密鍵
openssl genrsa -out client.key 2048
```

クライアント証明書の署名要求を作成します。

```:クライアント証明書署名要求作成
openssl req -new -key client.key -subj "/C=jp/ST=Tokyo/O=Client/CN=example.jp/emailAddress=client_jiro@example.jp" -out client.pem
```

クライアント証明書を作成します。

```:クライアント証明書作成
openssl x509 -req -days 3650 -in client.csr -CA inCA2.pem -CAkey inCA2.key -CAcreateserial -extfile san.txt -out client.pem
```
以上でクライアント証明書の作成は完了です。

### クライアント証明書のインストール
クライアント証明書をWindowsPCにインストールするので、P12形式に変換します。

```:P12形式への変換
openssl pkcs12 -export -in client.pem -inkey client.key -certfile inCA2.pem -out client.p12 -passin pass:password01 -passout pass:password01
```

作成したP12ファイルをダブルクリックすると、証明書のインポートウィザードが起動しますので、「現在のユーザー」を選択して「次へ」ボタンをクリックしてください。

![cert2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/857744/7af574e9-4467-49ac-70f0-a5a12a13116a.png)

インポートする証明書を入力し、「次へ」ボタンをクリックしてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/857744/d3406ba6-3b47-7148-b9c0-225ef301e671.png)

クライアント証明書を作成した際のパスワード(本記事の場合は、password01)を入力して「次へ」ボタンをクリックしてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/857744/2d1724df-0621-d025-62a6-8ed84c0eeb2a.png)

「証明書をすべて次のストアに配置する」を選択し、「参照」ボタンをクリックし、表示された証明書ストアの選択ウィンドウから「信頼されたルート証明機関」を選択し「OK」ボタンをクリックしてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/857744/7de31155-8386-6ec8-a6c7-c3316d486c65.png)

証明書ストアが「信頼されたルート証明機関」になっていることを確認して「次へ」ボタンをクリックしてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/857744/4b04479f-1d8b-d398-4033-b4fe8b591926.png)

「完了」をクリックしてください。　以上でクライアント証明書のインストールは完了です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/857744/112376f7-cc39-de4f-ca6b-b49e12c4c622.png)

# Apache設定
Keycloakの前段に配置したApacheは、SSLを紐解いて後段のKeycloakへクライアント証明書を連携するために、ssl.confを以下のように設定します。

```conf:/etc/httpd/conf.d/ssl.conf　必要箇所のみ抜粋
SSLCertificateFile server.pem
SSLCertificateKeyFile server.key
SSLCACertificateFile inCA2.pem

SSLVerifyClient require
SSLVerifyDepth  1

RequestHeader set SSL_CLIENT_CERT "%{SSL_CLIENT_CERT}e"
```

設定値の詳細は下表のとおりです。

|設定項目|設定値|意味|
|---|---|---|
|SSLCertificateFile |server.pem|SSLサーバ証明書|
|SSLCertificateKeyFile |server.key|Keycloakサーバの秘密鍵|
|SSLCACertificateFile  |inCA2.pem|中間認証局の証明書。ここで指定する証明書はクライアント証明書に署名した証明書です。|
|SSLVerifyClient |require|クライアント証明書による認証のレベル指定。クライアント証明書による認証を行いたいので、requireを指定しています。|
|SSLVerifyDepth  |1|クライアント証明書を認証するのに必要なCA証明書の最大数。1を指定すると、自己署名されたクライアント証明書もしくは SSLCACertificateFileで指定した証明書のCAによって署名されたクライアント証明書が許可されます。|
|RequestHeader|set SSL_CLIENT_CERT "%{SSL_CLIENT_CERT}e"|HTTPリクエストヘッダの設定。|

# Keycloak設定
クライアント証明書がApacheからKeycloakへ連携されるので、KeycloakにApache証明書検索プロバイダーを設定します。具体的には、standalone.xmlの<subsystem xmlns="urn:jboss:domain:keycloak-server:1.1">内に以下のとおりにx509cert-lookupを追加します。
既に、standalone.xmlにx509cert-lookupが定義済みであればコメントアウトしてください。

```xml:/opt/keycloak/standalone/configuration/standalone.xml
<spi name="x509cert-lookup">
    <default-provider>apache</default-provider>
    <provider name="apache" enabled="true">
        <properties>
            <property name="sslClientCert" value="SSL_CLIENT_CERT"/>
            <property name="sslCertChainPrefix" value="SSL_CLIENT_CERT_CHAIN"/>
            <property name="certificateChainLength" value="1"/>
        </properties>
   </provider>
</spi>
```

動作確認用に認証タイプに「X509/Validate Username」を含む適当な認証フローを作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/857744/4016cf60-7c29-eb38-11de-80caddca2929.png)


「X509/Validate Username」の設定内容は以下のとおりで、今回はクライアント証明書のSubjectに含まれるメールアドレスがKeycloakに登録されたユーザのメールアドレスに一致した場合に認証されるように設定したいので、赤枠で囲った部分のとおり「Subject's e-mail」を設定しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/857744/e17c322d-186f-a0f2-1ccc-10d991280069.png)


# 動作確認
それでは動作確認してみます。
作成した認証フローを設定したクライアントにアクセスすると、下図のような証明書選択ウィンドウが表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/857744/91928c10-a5a9-beb6-1b9e-c932e2f8303a.png)

ここでキャンセルボタンをクリックすると、認証に失敗するためエラー画面へ遷移します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/857744/cc5f44fe-b08a-b2f2-fc20-51e8257de734.png)

クライアント証明書を選択し、「OK」ボタンをクリックすると、Keycloakの認証画面が表示されるので、IDとパスワードを入力し、「Log In」ボタンをクリックしてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/857744/d4c5252c-93f5-2f99-0b62-1e2e9877fb17.png)

無事ログインできました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/857744/93d98941-aac3-2178-389a-d138d760ddf0.png)

# まとめ
「Keyclaokでクライアント証明書認証を試してみた」というタイトルの割に、証明書の作り方が大部分を占めてKeycloakに関する話が殆ど出てこず「Keyclaokの設定ってシンプルなんだね」という結果になってしまいました。
本当は、ここからクライアント証明書の失効リストの話なんかを書きたかったのですが、（時間切れのため）またの機会にしたいと思います。

# 参考文献
[Keycloak Server Administration Guide](https://keycloak-documentation.openstandia.jp/master/ja_JP/server_admin/index.html)
[https://www.openssl.org/](https://www.openssl.org/)