Jaybanuan's Blog

どうせまた調べるハメになることをメモしていくブログ

Keycloak 16 + OAuth2 ProxyをDocker Composeでお試し環境を構築

はじめに

Keycloak 16を使う必要に迫られたので、Docker Composeでお試し環境を作ることにした。 OpenID Connect用のリバースプロキシとしてOAuth2 Proxyがお手軽に使えそうなので、これも試用してみた。

なお、現時点でのKeycloakの最新版はKeycloak 17で、一部非互換がありそう。 特にレルムのインポート/エクスポート等の管理系が大幅に変わっているように見受けられる。

環境

$ cat /etc/os-release | grep PRETTY_NAME
PRETTY_NAME="Ubuntu 20.04.4 LTS"

$ uname -srvm
Linux 5.13.0-35-generic #40~20.04.1-Ubuntu SMP Mon Mar 7 09:18:32 UTC 2022 x86_64

$ docker version
Client: Docker Engine - Community
 Version:           20.10.12
(略)
Server: Docker Engine - Community
 Engine:
  Version:          20.10.12
(略)

$ docker compose version
Docker Compose version v2.2.3

今回の構成

Docker Composeを利用して、Keycloak 16とOAuth2 ProxyとNginxを立ち上げる。 以下が構成の概要図で、図中の水色の箱はDockerのネットワークを示している。

f:id:redj:20220319080639p:plain

ここで、auth-demo:80がブラウザから見た場合のWebアプリになる。 実際のWebアプリはOAuth2 Proxyの後ろに控えているwebapp:80で、今回は素のNginxである。

また、KeycloakについてはDockerのネットワークの内と外で同じ名前とポート番号keycloak:8080でアクセスできるようにしている。 その理由は、認証画面やOpenID ConnectのエンドポイントのURLが内と外で変わると、うまく動作しなかった*1からである。

これらを踏まえて、ホストマシンの/etc/hostsのローカルホストのエントリを以下のように修正している。

127.0.0.1 localhost keycloak auth-demo

もし、他のマシンでWebブラウザを起動するなら、そのマシンの/etc/hostsを適切に編集する必要がある。

docker-compose.ymlの作成

以下の内容のdocker-compose.ymlを作成する。 細かい説明は後回しにして、まずは内容全体を示す。

version: '3.9'

services:
  keycloak:
    image: jboss/keycloak:16.1.1
    ports:
      - 8080:8080
      - 9990:9990
    environment:
      KEYCLOAK_USER: kc
      KEYCLOAK_PASSWORD: kc
      KEYCLOAK_IMPORT:


  webapp:
    image: nginx:1.21.6


  oauth2-proxy:
    image: quay.io/oauth2-proxy/oauth2-proxy:v7.2.1-amd64
    ports:
      - 80:4180
    environment:
      OAUTH2_PROXY_PROVIDER: oidc
      OAUTH2_PROXY_CLIENT_ID: auth-demo
      OAUTH2_PROXY_CLIENT_SECRET:
      OAUTH2_PROXY_REDIRECT_URL: http://auth-demo/oauth2/callback
      OAUTH2_PROXY_OIDC_ISSUER_URL: http://keycloak:8080/auth/realms/demo
      OAUTH2_PROXY_COOKIE_SECRET: "01234567890123456789012345678901"
      OAUTH2_PROXY_COOKIE_SECURE: "false"
      OAUTH2_PROXY_COOKIE_NAME: "auth_demo"
      OAUTH2_PROXY_EMAIL_DOMAINS: "*"
      OAUTH2_PROXY_HTTP_ADDRESS: 0.0.0.0:4180
      OAUTH2_PROXY_UPSTREAMS: http://webapp/
      OAUTH2_PROXY_PASS_AUTHORIZATION_HEADER: "true"

一部の環境変数で値が未指定のものがあるが、その場合はDocker Compose起動時に利用可能な環境変数の値が、そのままコンテナに引き渡される動きになる。 詳細は以下を参照。

docs.docker.com

Makefileの作成

Makefileは必須ではないが、今回はKeycloakのレルムのデータのインポートとエクスポートを行うためのヘルパとして利用する。 こちらも、細かい説明は後回しにして、まずは内容全体を示す。

##############################################################################
# Variables

REALM_NAME := demo
REALM_FILE_TO_EXPORT := $(REALM_NAME)-realm-$(shell date +%s).json


##############################################################################
# Targets

.PHONY: up-keycloak
up-keycloak:
  if [ "$(REALM_FILE_TO_IMPORT)" ]; then \
      echo "up keycloak with realm file to import"; \
      KEYCLOAK_IMPORT="/$(REALM_NAME)-realm.json" docker compose up --no-start keycloak; \
      docker compose cp "$(REALM_FILE_TO_IMPORT)" "keycloak:/$(REALM_NAME)-realm.json"; \
      docker compose start keycloak; \
  else \
      echo "up keycloak without realm file to import"; \
      docker compose up --detach keycloak; \
  fi


.PHONY: down
down:
   @docker compose down


.PHONY: up
up: up-keycloak
  sleep 30
  docker compose up --detach webapp oauth2-proxy


# see
#   https://hub.docker.com/r/jboss/keycloak/
#   https://www.keycloak.org/docs/16.1/server_admin/#assembly-exporting-importing_server_administration_guide
.PHONY: export-realm
export-realm:
  docker compose exec keycloak /opt/jboss/keycloak/bin/standalone.sh \
      -Djboss.socket.binding.port-offset=100 \
      -Dkeycloak.migration.action=export \
      -Dkeycloak.migration.provider=singleFile \
      -Dkeycloak.migration.realmName=$(REALM_NAME) \
      -Dkeycloak.migration.usersExportStrategy=REALM_FILE \
      -Dkeycloak.migration.file=/tmp/$(REALM_FILE_TO_EXPORT)

    docker compose cp keycloak:/tmp/$(REALM_FILE_TO_EXPORT) $(REALM_FILE_TO_EXPORT)

Keycloakでレルムを設定

(1) Keycloakを立ち上げる

$ make up-keycloak

この時、まだ設定は何も実施していないので、以下の部分のelse側が実行される。 処理内容的には、単にdocker compose upを実行してKeycloakだけを起動している。

up-keycloak:
  if [ "$(REALM_FILE_TO_IMPORT)" ]; then \
      (略)
  else \
      echo "up keycloak without realm file to import"; \
      docker compose up --detach keycloak; \
  fi

(2) WebブラウザでKeycloakにログインする

Webブラウザhttp://keycloak:8080/auth/にアクセスする。

f:id:redj:20220319080657p:plain

表示されたログイン画面で以下を入力して、ボタン「Sign in」を押下する。

項目
Username or email kc
Password kc

ここで入力するユーザ名とパスワードは、docker-compose.ymlの以下の部分で指定している。

services:
  keycloak:
    environment:
      KEYCLOAK_USER: kc
      KEYCLOAK_PASSWORD: kc

(3) レルムを作成する

画面左上の「Master」というレルム名が表示されている部分にマウスカーソルを合わせると、「Add realm」というボタンが現れるので、そのボタンを押下する。

f:id:redj:20220319080704p:plain

以下のようなレルムの作成画面が表示されるので、

f:id:redj:20220319080728p:plain

以下を入力して、ボタン「Create」を押下する。

項目
Name demo

(4) issuerのURLを確認する

画面左のメニューから「Realm Settings」を選択する。 タブ「General」の中にある項目「Endpoints」で、「OpenID Endpoint Configuration」がリンクになっているので、それをクリックする。

f:id:redj:20220319080737p:plain

以下の画面のようにJSON形式の構成情報が表示されるので、その中のキーissuerの値を確認する。

f:id:redj:20220319080744p:plain

この値はOAuth2 Proxyに設定する必要があり、docker-compose.ymlの以下の場所で設定している。

services:
  oauth2-proxy:
    environment:
      OAUTH2_PROXY_OIDC_ISSUER_URL: http://keycloak:8080/auth/realms/demo

(5) クライアントを作成する

画面左のメニューから「Clients」を選択すると、以下のような画面が表示される。

f:id:redj:20220319080758p:plain

この画面のタブ「Lookup」の画面の右上にあるボタン「Create」を押下すると、以下のようなクライアントを追加する画面が表示される。

f:id:redj:20220319083653p:plain

この画面で以下を入力して、ボタン「Save」を押下する。

項目
Clinet ID auth-demo
Client Protocol openid-connect

ここで入力したClient IDはOAuth2 Proxyに設定する必要があり、docker-compose.ymlの以下の場所で設定している。

services:
  oauth2-proxy:
    environment:
      OAUTH2_PROXY_CLIENT_ID: auth-demo

(6) クライアントの設定を行う

クライアントを作成すると画面左のメニューの「Clients」のタブ「Settings」が選択された状態になっている。 画面は以下のようになっており、

f:id:redj:20220319083658p:plain

f:id:redj:20220319083703p:plain

f:id:redj:20220319083708p:plain

以下を入力して、ボタン「Save」を押下する。

項目
Access Type confidential
Valid Redirect URIs http://auth-demo/*

(7) クライアントシークレットを確認する

前の手順のクライアントの設定で「Access Type」を「confidential」にしたことにより、タブ「Credentials」が増えているので、これを選択する。 画面は以下のようになっている。

f:id:redj:20220319083712p:plain

ここで、項目「Secret」の値を控えておく。 これはクライアントアプリケーション、すなわちコンテナoauth-proxyがKeycloakにアクセスする際に必要になるシークレットであり、docker-compose.ymlの以下の場所で設定している。

services:
  oauth2-proxy:
    environment:
      OAUTH2_PROXY_CLIENT_SECRET:

「設定している」と書きつつも値を書いていないが、これはクライアントシークレットが機微情報であるため、docker-compose.ymlに直書きはせずに、Docker Composeの起動時に環境変数で指定するようにしているため。 具体的な指定方法は後述する。

(8) ユーザを追加する

画面左のメニューから「Users」を選択すると、以下のような画面になる。

f:id:redj:20220319083717p:plain

この画面のタブ「Lookup」の中にあるボタン「Add user」を押下すると、以下のようなユーザを追加する画面が表示される。

f:id:redj:20220319083727p:plain

この画面で以下を入力して、ボタン「Save」を押下する。

項目
Username demo-user
Email demo-user@example.com
Email Verified ON

次に、追加したユーザのパスワードを設定する。 タブ「Credentials」を選択すると以下のような画面が表示される。

f:id:redj:20220319083734p:plain

この画面で以下を入力して、ボタン「Set Password」を押下する。

項目
Password (任意のパスワード)
Password Confirmation (任意のパスワード)
Temporary OFF

(9) レルムのデータをエクスポートする

ここまでで設定したレルムを別環境でも復元できるように、JSONファイル形式でエクスポートしておく。 以下のコマンドを実行する。

$ make export-realm

上記コマンドについて、Makefileの当該部分を以下に抜粋する。

.PHONY: export-realm
export-realm:
  docker compose exec keycloak /opt/jboss/keycloak/bin/standalone.sh \
      -Djboss.socket.binding.port-offset=100 \
      -Dkeycloak.migration.action=export \
      -Dkeycloak.migration.provider=singleFile \
      -Dkeycloak.migration.realmName=$(REALM_NAME) \
      -Dkeycloak.migration.usersExportStrategy=REALM_FILE \
      -Dkeycloak.migration.file=/tmp/$(REALM_FILE_TO_EXPORT)

    docker compose cp keycloak:/tmp/$(REALM_FILE_TO_EXPORT) $(REALM_FILE_TO_EXPORT)

Keycloakのコンテナに入って特別なオプションをつけてKeycloakを立ち上げると、レルムのデータをJSON形式でエクスポートできる。 データをエクスポートするためにKeycloakのインスタンスがもう一つ立ち上がるのはどうかと思うが、想像するに管理系の機能を後付けで入れたために、こうなってしまったのだろうと思う*2。 Keycloak 16のインポート/エクスポートに関するマニュアルはこちら。

www.keycloak.org

上記のとおりにmakeを実行すると、コンソールにKeycloakの起動時のログが出力される。

(略)
22:27:54,767 INFO  [org.jboss.as.server] (ServerService Thread Pool -- 42) WFLYSRV0010: Deployed "keycloak-server.war" (runtime-name : "keycloak-server.war")
22:27:54,820 INFO  [org.jboss.as.server] (Controller Boot Thread) WFLYSRV0212: Resuming server
22:27:54,823 INFO  [org.jboss.as] (Controller Boot Thread) WFLYSRV0025: Keycloak 16.1.1 (WildFly Core 18.0.4.Final) started in 13501ms - Started 573 of 851 services (576 services are lazy, passive or on-demand)
22:27:54,825 INFO  [org.jboss.as] (Controller Boot Thread) WFLYSRV0060: Http management interface listening on http://127.0.0.1:10090/management
22:27:54,825 INFO  [org.jboss.as] (Controller Boot Thread) WFLYSRV0051: Admin console listening on http://127.0.0.1:10090

最後のWFLYSRV0051: Admin console listening on http://127.0.0.1:10090で起動完了と思えばよい。 ここで[CTRL] + [C]でエクスポート用のKeycloakを停止すると、Makefileの中のdocker compose cpに処理が進みホスト側にJSONファイルがコピーされる*3

エクスポートされるJSONファイルは、Makefile内で以下のように定義しているので、

REALM_FILE_TO_EXPORT := $(REALM_NAME)-realm-$(shell date +%s).json

具体的にはdemo-realm-1647642460.jsonといったようなファイルがホスト側に生成される。

(10) Keycloakのコンテナの破棄

ここまでで設定してきたKeycloakのコンテナは、一旦破棄する。 動作確認では、エクスポートされたレルム(JSONファイル)をロードすることで初期データをセットアップする。

$ make down

Makefileでは、以下のように単にdocker compose downを実行している。

.PHONY: down
down:
   @docker compose down

動作確認

(1) デモアプリを起動する

クライアントシークレットはdocker-compose.ymlの中で値を指定していない。 そのため、コンテナにクライアントシークレットを引き渡せるように、ターミナルで環境変数を設定しておく。

$ export OAUTH2_PROXY_CLIENT_SECRET=[クライアントシークレット]

次に、インポートするレルムのJSONファイルを環境変数で指定しておく。 このJSONファイルはmake export-realmを実行して作成されたファイルであり、具体的には以下のような感じで指定する。

$ export REALM_FILE_TO_IMPORT=./demo-realm-1647642460.json

そして、以下のコマンドを実行してKeycloak、OAuth2 Proxy、Nginxを起動する。

$ make up

この上記コマンドについて、Makefileの当該部分を抜粋しながら説明する。 まずはターゲットupについてだが、Makefileでは以下のようになっている。

.PHONY: up
up: up-keycloak
  sleep 30
  docker compose up --detach webapp oauth2-proxy

ターゲットupの実行の前に、ターゲットup-keycloakが実行される。 そしてKeycloakの起動完了の待ち合わせのために30秒スリープした後、OAuth2 ProxyとNginxを立ち上げている。 OAuth2 Proxyは起動処理の過程でKeycloakにアクセスしているため、起動完了の待ち合わせがないとOAuth2 Proxyは起動に失敗する*4

そして、ターゲットup-keycloakは以下のようになっており、

.PHONY: up-keycloak
up-keycloak:
  if [ "$(REALM_FILE_TO_IMPORT)" ]; then \
      echo "up keycloak with realm file to import"; \
      KEYCLOAK_IMPORT="/$(REALM_NAME)-realm.json" docker compose up --no-start keycloak; \
      docker compose cp "$(REALM_FILE_TO_IMPORT)" "keycloak:/$(REALM_NAME)-realm.json"; \
      docker compose start keycloak; \
  else \
      (略)
  fi

今回は変数(環境変数)REALM_FILE_TO_IMPORTが定義されているためthenの方が実行される。 ここでは、コンテナの作成(docker compose up --no-start)、コンテナ内にレルムのJSONファイルをコピー(docker compose cp)、コンテナの起動(docker compose start)を実施している。

インポートするレルムのJSONファイルを引き渡す方法としては、ボリュームを利用するやり方もあるが、これだとdocker-compose.ymlがベタに環境依存になるので避けた。

(2) デモアプリにWebブラウザでアクセスする

Webブラウザhttp://auth-demo/にアクセスすると、以下のような画面が表示される。

f:id:redj:20220319083742p:plain

この画面でボタン「Sign in with OpenID Connect」を押下すると、以下のログイン画面が表示される。

f:id:redj:20220319083746p:plain

この画面で以下を入力して、ボタン「Sign in」を押下する。

項目
Username or email demo-user
Password (demo-userのパスワード)

ログインに成功すると、以下のようなNginxのデフォルトのページが表示される。

f:id:redj:20220319083750p:plain

これにより、OAuth2 ProxyをリバースプロキシとしてKeycloakと連携し、KeyCloakによる認証を通過した後、OAuth2 Proxy経由でバックエンドのWebアプリケーション(Nginx)にアクセスできることを確認できた。

参考

Keycloakとは

Red HatのSSO (Single Sign On)のサーバソフトウェア。 今回はSSOというよりは、OpenID Connectに基づく認証サーバとして利用している。

www.keycloak.org

OAuth2 Proxyとは

アプリケーションサーバ等の前段にリバースプロキシとして立って、OAuth2/OpenID Connectのための処理を請け負ってくれるサーバソフトウェア。 アプリケーション内にOAuth2/OpenID Connectの処理を組み込まなくてもよいので、レガシーなサーバに簡易的に認証/認可の機能を付加する場合に便利。

oauth2-proxy.github.io

*1:これはKeycloakの設定次第かもしれないが、調べきれなかった。

*2:Keycloak 17から基盤がWildflyからQuarkusに変わったのでこの方法は使えない。管理系のコマンドが整備されているように見えるので、改善されているかもしれない

*3:[CTRL] + [C]でファイルがコピーされるとか不格好過ぎるが、Keycloak 17では改善しているかもしれないし、そもそもデモなのでプロダクションレベルの品質は不要だしで、このままでいいやという結論になった

*4:スリープによる待ち合わせは超簡易的な方法なので、プロダクション環境であればhttpを利用したアライブチェック等にすべき。