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を利用したアライブチェック等にすべき。

Ubuntu 20.04LTSでウィンドウのスクリーンショットをとったら周辺に透明な領域があって邪魔だった

はじめに

ウィンドウのスクリーンショットを取ると、何故かウィンドウの周辺に透明な領域(drop shadow ?)がある。 邪魔なので消そうと思った。

f:id:redj:20220319013204p:plain

環境

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

やったこと

ウィンドウのエフェクトが関係していそうだが、色々調べても解決策が見当たらず。 仕方がないので、ウィンドウ部分を抜き出すシェルスクリプトを作成した。

#!/bin/bash

WIN_WIDTH=${WIN_WIDTH:-1280}
WIN_HEIGHT=${WIN_HEIGHT:-800}

if [ -v WIN_ID ]; then
    wmctrl -i -r ${WIN_ID} -e 0,100,100,$((WIN_WIDTH+44)),$((WIN_HEIGHT+44))
    wmctrl -i -a ${WIN_ID}
    gnome-screenshot --window --clipboard --delay 1 --file tmp.png; rm tmp.png
    xclip -selection clipboard -t image/png -o | convert - -crop ${WIN_WIDTH}x${WIN_HEIGHT}+22+19 "screenshot-$(date +%s)".png
else
    echo "export WIN_ID=..."
    wmctrl -l
fi

wmctrlでウィンドウを指定する際には、IDで指定するようにしている。 これで、以下のようにウィンドウの周辺の透明な領域がないスクリーンショットを取得できた。

f:id:redj:20220319013216p:plain

補足

gnome-screenshotでとったスクリーンショットクリップボードに転送したかったが、うまくいかず。 以下の情報より、ダミーの画像ファイルtmp.pngを作成することで、クリップボードへの転送ができた。

askubuntu.com

以前に「--clipboardと--fileを同時に使えない」というバグがあって修正されたらしいが、これがきっかけで別のバグを作ったのではないかと思う。。。

bugs.launchpad.net

Flaskアプリのデバッグではloggerを使う

Flaskでアプリを作成している時に、デバッグ情報をprintで出力しようとしたが、出力されなかった。 調べてみると、出力がバッファリングされるようなので、以下のような感じでloggerを使うのがよいとのこと。

from flask import Flask
import logging

app = Flask(__name__)
app.logger.setLevel(logging.INFO)

以下を参考にした。

hawksnowlog.blogspot.com

PythonにおけるMixin

PythonにおけるMixinについて、以下のブログの説明が分かりやすかった。

www.thedigitalcatonline.com

Mixinの説明を以下に引用する。

Python doesn't provide support for mixins with any dedicated language feature, so we use multiple inheritance to implement them. This clearly requires great discipline from the programmer, as it violates one of the main assumptions for mixins: their orthogonality to the inheritance tree. In Python, so-called mixins are classes that live in the normal inheritance tree, but they are kept small to avoid creating hierarchies that are too complicated for the programmer to grasp. In particular, mixins shouldn't have common ancestors other than object with the other parent classes.

一部補足して意訳すると、以下のようになる。

PythonはMixinに対応するための専用の言語機能を提供していないため、Mixinを実装するには多重継承を代用することになります。ところが、何の制約もなく自由に多重継承を利用すると、Mixinの根幹をなす前提である継承ツリーの直交性を乱すことになるため、プログラマによる優れた規律(コーディングルール)が明らかに必要になります。Pythonにおいては、Mixinと呼ばれるものは通常の継承ツリーに現れるクラスですが、プログラマが把握不能に陥るような複雑な階層にならないように、そのクラスは小さく保つ必要があります。特に、Mixinは(埋め込み先のクラスに対して)object以外の他の親クラスを共通の祖先に持つべきではありません。

要するに「PythonではMixinは言語機能じゃなくてコーディングルール」ということのよう。

authlib - PythonのOAuth2/OpenID Connectのライブラリ

authlibがよさげ。

Webサイト

docs.authlib.org

GitHub

github.com

ドキュメント

docs.authlib.org

メモ

デフォルトだと認証/認可の情報はSQLAlchemy + SQLite、すなわちRDBで取り扱うようだが、世の中の認証ミドルとの連携方法が分からなかった。 まだ試していないが、RDBの代わりにOAuth 2.0 Token Introspectionを利用できそうに見えた。

docs.authlib.org

Makefileを配置しているディレクトリ名を取得する

Makefileを配置しているディレクトリ名をビルドで使いたかったが、ディレクトリ名の取得がなかなか大変だった。 Makefileの例は以下。

# parent directory
PARENT_DIR_NAME := $(shell basename $(dir $(realpath $(firstword $(MAKEFILE_LIST)))))

# echo parent directory
.PHONY: echo
echo:
   @echo $(PARENT_DIR_NAME)

このMakefileディレクトmakefile-demoにある場合、実行結果は以下のようになる。

$ make
makefile-demo