【Keycloak】Apache の VirtualHost で分けられた複数のサイトをまとめてシングルサインオンしよう

関連記事
Keycloak コンテナがめっちゃ便利だったのでついでに https 化もした
https://qiita.com/thirdpenguin/items/9729738289bc0809cdbf

## はじめに
シングルサインオンを実現する認証系 OSS の開発動向が近年活発になってきています。

OpenAM は 初の OpenAM コンソーシアム発バージョンとなる 14 がリリースされました。
Keycloak は Keycloak.X と銘打たれた 10.0.X 系列がリリースされ、早くも2回のマイナーアップデートが行われています。

コンテナ推しの自分としては公式からのイメージ配布のある Keycloak に特に興味を寄せています。
pull コマンド 1 個で前提パッケージを含む環境構築が済み、入れ替えも楽々なのはとても良いですね。

というわけで今回は Apache の SAML 認証用モジュール mod_auth_mellon を使って、1 回のログインで複数のサイトの利用を認可させる構成の構築手順をまとめてみました。

## やりたいこと
- 単一の Apache サーバ上に、名前ベースの VirtualHost によって分かれた 2 つのテストページを稼働させ、1 回ユーザ/パスワード認証を行うとどちらのページも見られるようにする。
- 別サーバで稼働する Keycloak の導入には podman-compose を利用し、 keycloak コンテナと postgres コンテナを一括で作成する。

## バージョン情報
- Keycloak サーバ (IdP)
  - OS: CentOS8 (8.1.1911)
  - podman: 1.6.4
  - podman-compose: 0.1.7.dev0
- Apache サーバ (SP)
  - OS: CentOS7 (7.7.1908)
  - Apache: 2.4.6

## 前提条件

2台のサーバにOSをインストールして以下の設定を行っておきます。

- Keycloak サーバ (IdP)
  - ホスト名: keycloak.example.com
  - firewall: ssh, 8080/tcpのみ通信許可設定
- Apache サーバ (SP)
  - ホスト名: vhost1.example.com
  - SELinux: Disabled
  - firewall:ssh, httpsのみ通信許可設定

また、どちらも IPv4 の固定アドレスを設定し、ssh接続にて作業を行います。

## 構築手順
### 1. Apache のインストールと設定
VirtualHost で 2 つのサイトを立て、SAML 認証に必要なメタデータと証明書の発行を 2 つのサイトに対して行います。

1. Apache と 必要モジュールのインストール

```
# yum install -y httpd mod_auth_mellon mod_ssl
```
　
2. VirtualHost の設定

/etc/httpd/conf.d/ 下に 2 つのファイル (virtualhost1.conf, virtualhost2.conf) を作成し、VirtualHost の設定を書いていきます。

Apache の VirtualHost 機能について特に言及無く進めてきましたが、ざっくり言うとクライアントがサイトへの接続に使った名前に応じて、異なるコンテンツにアクセスさせるための機能です。
外からは見かけ上、複数のサイトがそれぞれ異なるコンテンツを提供しているように見えますが実際はどちらも同じホスト上で稼働しています。

以下の設定ファイル例では、vhost1.examlple.com に接続した場合は /var/www/html を DocumentRoot とし、
vhost2.examlple.com に接続した場合は /var/www/html2 を DocumentRoot としています。
加えて、```<Location />```　タグ内の設定によって / ディレクトリ以下の階層全てを SAML 認証の保護下として指定しています。
 
```vim:/etc/httpd/conf.d/virtualhost1.conf
<VirtualHost 192.168.180.198:80>
    ServerAdmin webadmin@www.example.com
    ServerName vhost1.example.com
    DocumentRoot /var/www/html
  <Location />
    MellonEndpointPath "/mellon"
    MellonIdPMetadataFile /etc/httpd/saml/idp_metadata.xml
    MellonSPPrivateKeyFile /etc/httpd/saml/https_vhost1.example.com_sp.key
    MellonSPCertFile /etc/httpd/saml/https_vhost1.example.com_sp.cert
    MellonSPMetadataFile /etc/httpd/saml/https_vhost1.example.com_sp.xml

    AuthType "Mellon"
    Require valid-user
    MellonEnable "auth"
  </Location>
</VirtualHost>
 
<VirtualHost 192.168.180.198:443>
    ServerAdmin webadmin@www.example.com
    ServerName vhost1.example.com
    DocumentRoot /var/www/html

    SSLEngine on
    SSLCertificateKeyFile /etc/httpd/saml/https_vhost1.example.com_sp.key
    SSLCertificateFile /etc/httpd/saml/https_vhost1.example.com_sp.cert
  <Location />
    MellonEndpointPath "/mellon"
    MellonIdPMetadataFile /etc/httpd/saml/idp_metadata.xml
    MellonSPPrivateKeyFile /etc/httpd/saml/https_vhost1.example.com_sp.key
    MellonSPCertFile /etc/httpd/saml/https_vhost1.example.com_sp.cert
    MellonSPMetadataFile /etc/httpd/saml/https_vhost1.example.com_sp.xml

    AuthType "Mellon"
    Require valid-user
    MellonEnable "auth"
  </Location>
</VirtualHost>
```
 
```vim:/etc/httpd/conf.d/virtualhost2.conf
<VirtualHost 192.168.180.198:80>
    ServerAdmin webadmin@www.example.com
    ServerName vhost2.example.com
    DocumentRoot /var/www/html2
    ErrorLog logs/www2-error_log
    CustomLog logs/www2-access_log common
  <Location />
    MellonEndpointPath "/mellon"
    MellonIdPMetadataFile /etc/httpd/saml/idp_metadata.xml
    MellonSPPrivateKeyFile /etc/httpd/saml/https_vhost2.example.com_sp.key
    MellonSPCertFile /etc/httpd/saml/https_vhost2.example.com_sp.cert
    MellonSPMetadataFile /etc/httpd/saml/https_vhost2.example.com_sp.xml

    AuthType "Mellon"
    Require valid-user
    MellonEnable "auth"
  </Location>
</VirtualHost>
 
<VirtualHost 192.168.180.198:443>
    ServerAdmin webadmin@www.example.com
    ServerName vhost2.example.com
    DocumentRoot /var/www/html2
    ErrorLog logs/www2-error_log
    CustomLog logs/www2-access_log common

    SSLEngine on
    SSLCertificateKeyFile /etc/httpd/saml/https_vhost2.example.com_sp.key
    SSLCertificateFile /etc/httpd/saml/https_vhost2.example.com_sp.cert
  <Location />
    MellonEndpointPath "/mellon"
    MellonIdPMetadataFile /etc/httpd/saml/idp_metadata.xml
    MellonSPPrivateKeyFile /etc/httpd/saml/https_vhost2.example.com_sp.key
    MellonSPCertFile /etc/httpd/saml/https_vhost2.example.com_sp.cert
    MellonSPMetadataFile /etc/httpd/saml/https_vhost2.example.com_sp.xml

    AuthType "Mellon"
    Require valid-user
    MellonEnable "auth"
  </Location>
</VirtualHost>
```
　
3. SP メタデータの作成
mod_auth_mellon に用意されているメタデータ作成スクリプトを使って、IdP との紐づけと真正性の担保に必要なメタデータと証明書を作成していきます。
SAML SP の識別子である Entity_ID と、mod_auth_mellon の動作 URL となるベース URL をスクリプトに渡して実行します。
 
```:1つ目のサイト用
# mkdir /etc/httpd/saml
# cd /etc/httpd/saml
# ENTITY_ID1=https://vhost1.example.com/sp
# BASE_URL1=https://vhost1.example.com/mellon
# /usr/libexec/mod_auth_mellon/mellon_create_metadata.sh ${ENTITY_ID1} ${BASE_URL1}
Output files:
Private key:                              https_vhost1.example.com_sp.key
Certificate:                              https_vhost1.example.com_sp.cert
Metadata:                                 https_vhost1.example.com_sp.xml
Host:                                     vhost1.example.com

Endpoints:
SingleLogoutService (SOAP):               https://vhost1.example.com/mellon/logout
SingleLogoutService (HTTP-Redirect):      https://vhost1.example.com/mellon/logout
AssertionConsumerService (HTTP-POST):     https://vhost1.example.com/mellon/postResponse
AssertionConsumerService (HTTP-Artifact): https://vhost1.example.com/mellon/artifactResponse
AssertionConsumerService (PAOS):          https://vhost1.example.com/mellon/paosResponse
```
 
```:2つ目のサイト用
# mkdir /etc/httpd/saml
# cd /etc/httpd/saml
# ENTITY_ID2=https://vhost2.example.com/sp
# BASE_URL2=https://vhost2.example.com/mellon
# /usr/libexec/mod_auth_mellon/mellon_create_metadata.sh ${ENTITY_ID2} ${BASE_URL2}
Output files:
Private key:                              https_vhost2.example.com_sp.key
Certificate:                              https_vhost2.example.com_sp.cert
Metadata:                                 https_vhost2.example.com_sp.xml
Host:                                     vhost2.example.com

Endpoints:
SingleLogoutService (SOAP):               https://vhost2.example.com/mellon/logout
SingleLogoutService (HTTP-Redirect):      https://vhost2.example.com/mellon/logout
AssertionConsumerService (HTTP-POST):     https://vhost2.example.com/mellon/postResponse
AssertionConsumerService (HTTP-Artifact): https://vhost2.example.com/mellon/artifactResponse
AssertionConsumerService (PAOS):          https://vhost2.example.com/mellon/paosResponse
```
 
さて、これで手順4. VirtualHost の設定の以下の部分で指定したメタデータファイルおよび証明書の作成が8割できました。
 
```
  <Location />
    MellonEndpointPath "/mellon"
    MellonIdPMetadataFile /etc/httpd/saml/idp_metadata.xml ←IdP から提供
    MellonSPPrivateKeyFile /etc/httpd/saml/https_vhost2.example.com_sp.key ←OK
    MellonSPCertFile /etc/httpd/saml/https_vhost2.example.com_sp.cert ←OK
    MellonSPMetadataFile /etc/httpd/saml/https_vhost2.example.com_sp.xml ←OK
```

残るのは MellonIdPMetadataFile で指定している IdP 側のメタデータです。IdP 側の作業で作成した後、このパスに設置していきます。
とりあえず一旦 Apache SP 側の作業は区切りです。おつかれさまでした。
 
### 2. podman-compose を使った KeyCloak のインストール

1. ホスト名の変更
KeyCloak サーバのホスト名を変更します。

```:/etc/hostname
keycloak.example.com
```
　
2. podman のインストール
podman のインストールついでに podman-docker も入れておきます。alias docker='podman' になるパッケージです。
podman のコマンドはほとんど docker と一緒なので、これがあると docker の使用感と何ら変わりない状態で podman が扱えます。

```
# dnf -y install podman podman-docker
```
　
3. podman-compose のインストール
 
pip3 で podman-compose をインストールするため、pip3 を同梱している python36 をインストールします。(python38 でも可。）
なお、podman-compose ver. 0.1.5 は ソースコード中のタイポにより、yaml ファイル中で定義した Volume が自動で作成されない不具合があるので、問題が修正されている 0.1.7.dev0 を開発者ブランチからインストールします。

```
# dnf install -y python36
# pip3 install https://github.com/containers/podman-compose/archive/devel.tar.gz
```
　
4. コンテナ作成

お待ちかねのコンテナ構築タイムです。
Keycloak の公式 git リポジトリをクローンして、docker-compose 用の yml ファイルを引っ張ります。
これを podman-compose に流用して keycloak コンテナと postgres コンテナを一気に構築します。

```
# mkdir localrepo
# cd local repo
# git init
# git clone https://github.com/keycloak/keycloak-containers.git
# cd docker-compose-examples/
# podman-compose -f keycloak-postgres.yml up -d
using podman version: podman version 1.6.4
podman pod create --name=docker-compose-examples --share net -p 8080:8080
96f023c108904557f7c953cc1896271bbfec1e9526583d9300d47cde3b1889d5
0
podman volume inspect docker-compose-examples_postgres_data || podman volume create docker-compose-examples_postgres_data
podman run --name=docker-compose-examples_postgres_1 -d --pod=docker-compose-examples --label io.podman.compose.config-hash=123 --label io.podman.compose.project=docker-compose-examples --label io.podman.compose.version=0.0.1 --label com.docker.compose.container-number=1 --label com.docker.compose.service=postgres -e POSTGRES_DB=keycloak -e POSTGRES_USER=keycloak -e POSTGRES_PASSWORD=password -v docker-compose-examples_postgres_data:/var/lib/postgresql/data --add-host postgres:127.0.0.1 --add-host docker-compose-examples_postgres_1:127.0.0.1 --add-host keycloak:127.0.0.1 --add-host docker-compose-examples_keycloak_1:127.0.0.1 postgres
046eec4c0a2619de953e35bd2940a00f43ac95ff87df955a55c07fe831492a23
0
podman run --name=docker-compose-examples_keycloak_1 -d --pod=docker-compose-examples --label io.podman.compose.config-hash=123 --label io.podman.compose.project=docker-compose-examples --label io.podman.compose.version=0.0.1 --label com.docker.compose.container-number=1 --label com.docker.compose.service=keycloak -e DB_VENDOR=POSTGRES -e DB_ADDR=postgres -e DB_DATABASE=keycloak -e DB_USER=keycloak -e DB_SCHEMA=public -e DB_PASSWORD=password -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=Pa55w0rd --add-host postgres:127.0.0.1 --add-host docker-compose-examples_postgres_1:127.0.0.1 --add-host keycloak:127.0.0.1 --add-host docker-compose-examples_keycloak_1:127.0.0.1 quay.io/keycloak/keycloak:latest
8241eb401a36880c0d3850a2230ae7c15510b9eedfe20478c6357972c597da6a
0
```

上記のように podman-compose によって実行されたコマンドが全て終了コード 0 で終わっていれば成功です。
失敗したら ```podman-compose -f keycloak-postgres.yml down``` で一度コンテナとポッドを全部削除してから上記 ``` podman-compose up ``` コマンドを再度実行します。

余談:
podman-compose は docker-compose 互換を指向していますが、内部の動作では「そのサービス専用のポッドをまず作成する」という点が docker と大きく異なります。そのため、podman-compose に頼らずこのステップを行う場合 keycloak コンテナと postgres コンテナが共に属するポッドを作成する必要があることに留意する必要があります。

### 3. KeyCloak IdP に Apache SP を紐付ける

1. クライアントの hosts ファイルを編集する。

今回は名前ベースの VirtualHost を利用しているので、ブラウザから SP および IdP へ接続するクライアントが名前解決できる状況にある必要があります。

DNS に登録するか、そうでなければクライアントの hosts ファイルを編集するのが手軽です。
Windows であれば ```C:\Windows\System32\drivers\etc\hosts```, Linux なら ```/etc/hosts``` にあります。
テキストエディタで開いて以下の内容を追記します。

```:hosts
<vhost1 の IP> vhost1.example.com
<vhost2 の IP> vhost2.example.com
<keycloak IdP の IP> keycloak.example.com
```
 
2. ブラウザから Keycloak の Administrator コンソールへ接続する。

http://keycloak.example.com:8080 に接続すると コンテナの 8080 ポートに転送され、keycloak の管理ページが表示されます。
![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320878/2c4b6c00-d75d-01d6-e88a-d3ebaf9bd41e.png)
 
Administration Console をクリックするとログイン画面が開きます。
![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320878/f77bb4c0-313e-a2b7-5f49-6d365b3c5b4a.png)
特に yml ファイルを変更していなければ初期管理者アカウントは ユーザ名 ```admin``` パスワード ```Pa55w0rd``` となっているのでこれでログインします。

3. レルムの作成
Select Realm の横にあるプルダウンメニューを開いて Add realm をクリックします。
レルムの名前を入力して Create をクリックすると レルムが作成されます。今回は Apache をレルム名にします。
![3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320878/317d9948-93e7-325b-211a-ceefcce066fd.png)
　
4. サービスプロバイダの登録
左側のメニューから [Clients] をクリックします。
"Import" 横にある [Select file] ボタンをクリックして、前章で作成した SP 側のメタデータ( https_vhost1.example.com_sp.xml または https_vhost1.example.com_sp.xml ) を指定します。

![4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320878/5ce68d8f-bace-6a4a-5e5d-c25484429f39.png)

[Save] を押すとメタデータファイルを元に、Apache SP の情報が登録されます。
この手順を 2 回繰り返し、 2 つのサイト両方をクライアントとして登録します。

### 4. Apache SP 内に KeyCloak IdP のメタデータを設置する

1. Apache SP 側で以下のコマンドを実行します。

```
# curl -k -o /etc/httpd/saml/idp_metadata.xml \
http://keycloak.example.com:8080/auth/realms/apache/protocol/saml/descriptor
```

IdP 側のメタデータが /etc/httpd/saml に設置されます。

……おつかれさまでした。これで構築作業終了です。
後は SSO が機能するか確認するだけですが、SELinux や httpd の設定の適用のために今一度再起動しておきましょう。

```
# systemctl enabled httpd
# reboot
```

うまくいけばブラウザから vhost1.example.com または vhost2.example.com に接続した際に keycloak.example.com にリダイレクトされ、ユーザ認証完了後にまた元の URL に戻ってくるはずです。

## トラブルシューティング

#### Apache が立ち上がらない。
まずログファイルを確認します。
``` /etc/httpd/logs ``` 内にある以下のファイルが見どころになると思います。

- error_log: Apache の起動などに関するエラーログ
- ssl_error_log: SSL に関連したエラーログ

#### keycloak.example.com にログインした後に SP へリダイレクトされるが、 接続が拒否される。
SSL 通信周りの設定を見直すことで改善するかもしれません。

- mod_ssl はインストールされているか。
- SSLEngine on の記述は、SSLCertificateKeyFile と SSLCertificateFile の記述があるファイルと同じファイルに書き込まれているか。
- SSLCertificateKeyFile 行で指定しているパスに秘密鍵が設置されているか。
- SSLCertificateFile 行で指定しているパスに証明書が設置されているか。
- SSLEngine on, SSLCertificateKeyFile, SSLCertificateFile の記述が他の設定ファイル（例えば /etc/httpd/ssl.conf ）にないか。

## 参考
執筆時の参考にさせていただきました。ありがとうございます。

- mod_auth_mellon を使ってみた(https://qiita.com/aimoto/items/89ba104db85a2b89fa67)