KeycloakとGitBucketとRocketChatをリバースプロクシ以下に置いて認証させる

ちょっと諸般の事情で製作中のゲーム(teslawire)で使うゲームサーバのコンポーネントを可能な限り既製品で構成することになった。
元々はゲームAPIサーバと自前のOpenID Connect認証ブローカーで済ませるつもりだったのが、既存の企画を生かしたまま既製品をあてはめるとすごい事に:

... 実際のゲームサーバがどうなるかはまだ検討しきれていないが、とにかく、大量のWebAPIサーバを抱えるシステムになってしまう。
一番重要なポイントは、1アプリ1コンテナを徹底してDocker前提の計画としたこと。世間的によく使われる普通のワークフローを可能な限り採用する方向にした。その他製品の選択としては:

  • HTTP(S)リバースプロキシは当初計画ではnghttp2を使った自前のWebサーバだったが、VarnishHitchの組み合せにした。nginxとかに比べて個人的に(多少)使い慣れているので。。Varnishは次のバージョンでUNIX Domain Socketを使ったbackendが使えるようになることと、HTTP/2の終端も一応できるようになったりと機能性としては十分と考えている。
  • IAM(IdP)実装としてはKeycloakを選定。Gluu ( https://gluu.org/ )のような他の競合製品と比べて一番将来性が有るんじゃないかと思っている。また、内部APIが比較的しっかりしていてゲーム側の仕様に合わせた拡張が比較的容易そうなのもポイント。
    • ゲームのアカウントには"匿名アカウント(ゲームプレイ用)"と"記名アカウント(ゲーム制作参加用)"の2種類があり、記名アカウントをKeycloakで管理する。記名アカウントはtwitterGitHubのような既存のOAuth実装で処理し、通常のパスワードは要求しない。
  • リポジトリサーバは Sonatype Nexus3。これは以前から採用。
  • コントリビューション用にNextCloudGitBucketRocketChatを採用。NextCloudは、いわゆるアップローダやベータビルドの配布場所として使用する。将来的にはゲーム側にクラッシュリポータを用意したいが。。GitBucketはソースコードビューアとしての使用(ネタバレ防止のためシナリオデータはGitHubではなくプライベートリポジトリに置く)、RocketChatは2Pプレイのデバッグ時など細かいコミュニケーションが必要な場合に使用。世間的にはRocketChatのようなSlack cloneとしてはMattermostが有名で、実際Mattermostの方が完成度が高いと思うが、いわゆるSSOがEnterprise版専用で今回の目的に合わないので不採用。同様の理由でownCloudでなくNextCloudにした。

最大のポイントは自前のIdPから既製品のKeycloakへの乗り換えで、アプリケーション間の接続をOpenID Connectに統一する等アーキテクチャ上の大きな変更が必要になる。

上手くいっていないこと

  • https://github.com/okuoku/tew-serv/issues/1
    • Keycloakはユーザのプロファイルピクチャを直接的にはサポートしていない。個人的には超重要な機能だと思っているのでどうにかしたいが良いアイデアがない。

Keycloak自体は、各アカウントにメタデータを付けることができ、そのメタデータAPIを通じてアクセスできるし、OpenID Connectのclaimとして送出することもできる。また、そもそも、GitBucketはOpenID Connectのpictureに現状対応していない等対応状況もマチマチになっている。

これに対する解決策としては単にダミーのe-mailアドレスを生成して埋めてしまう手があるが、そもそも半匿名ユーザを大量に抱えること自体があんまり目指すモデルでは無いような気もしている。よくあるやりかたとしては、誰でも名乗ることができる"mob"ユーザを用意し、それをGitBucket上での唯一のユーザとして共有することが考えられるが。。

各Webアプリをリバースプロキシ配下に配置する

これ自体は散々他所でも繰り返されている話題だが、RocketChat以外は結構面倒だった。。
KeycloakはXMLで設定する必要があった。というわけで、Dockerコンテナからデフォルト設定を抜き出し、手でweb-contextを書き換えている。ちなみに、ここを書き換えても既に登録されたアプリケーションは自動的に追従しないので、設定はインストール時(= 最初のサーバ起動よりも前)に行う必要がある。
Keycloakはデフォルトでは "/auth" 以下のURLで操作するが、ここでは "/kc/auth" 以下に変更している。

<web-context>kc/auth</web-context>

(個人的には伝統的にアプリケーションに2、3文字のプレフィックスを付け、ポート番号でのマルチプレクスは極力行わないようにしている。今回はKeycloakなので"kc"。)
Keycloakは自分に設定されたURLスキームを推論せず、常に X-Forwarded-Proto に従うため、オプション PROXY_ADDRESS_FORWARDING をKeycloak側に設定した上で、Varnishからは

 set req.http.X-Forwarded-Proto = "https";

のようにして、X-Forwarded-Proto を設定してやる必要がある。
GitBucketは設定にアプリケーションURLの設定が有るが、何故かリバースプロクシ側でもURLをリライトしないと上手く動かなかった。たぶんプロクシ側が正常に設定できていないが、ログを見ても判然としなかったので後回し。

 set req.url = regsub(req.url, "^/gb/", "/");

この手のURL書き換えはVarnishの得意分野。ここでは、先頭の /gb/ を除いている。こういうURL変換は、Webアプリ側で /gb/ が付くようなURLを使われるとダメになるため良くない。

KeycloakにTwitter認証ボタンを付けて、生成されるユーザ名に"twitter"をプレフィックスする

単純にアカウントの認証をTwitterで行うだけならば、Qiitaに書いてある通りにすればできる。

ただ、これだと"Twitterでサインイン"ボタンを押した後に(Keycloak上の)アカウントの登録フローに飛ばされてしまう。この登録フローは不要なので無効にする。サインインフローをカスタマイズするためには、login flowを修正する必要がある。デフォルトでは

のように、"first broker login"が割りあてられているが、このフローは左の"Authentication"メニューから別のフローを作成して置き換えることができる。
というわけで、最初の"Review Account"がDISABLEDになっているフローを作成して、割り当てることでほぼ期待通りのUIフローになった。

最後に付与されている"Set UUID username"はユーザ名をtwitterから指定されたものではなくUUIDに置き換えるもので、

// import enum for error lookup
AuthenticationFlowError = Java.type("org.keycloak.authentication.AuthenticationFlowError");

function authenticate(context) {
    user.setUsername("twitter" + user.getId());
    context.success();
}

のようなスクリプトを与えている。スクリプト内では、KeycloakのJavaDocに有るような各種オブジェクトとメソッドがそのまま使用できる。例えば変数userには、認証中のユーザを表現するUserModelオブジェクトが渡ってくるので、そのメソッドsetUsername()を呼べばユーザ名をオーバーライドできる。

作成されたアカウントが twitter + UUID の形式になっていることが確認できる。

GitBucketの認証をKeycloakの提供するOpenID Connectで行う

実際に接続方法はGitBucketのWikiに有り https://github.com/gitbucket/gitbucket/wiki/OpenID-Connect-Settings keycloakでの設定サンプルも有るのでコレに従えばOK。今のところ、GitBucketが公式で配布しているDockerイメージは4.20で、OpenID Connectに対応していない。というわけで、4.21を使ったDockerfileを公式のDockerfileを元に用意した。

重要なポイントは、OpenID Connectの接続時にGitBucket → Keycloakの接続が行われる点で、このために、テスト環境で使用している自己署名証明書をGitBucket側のTruststoreに追加する必要があった。これをやっておかないと単に"internal server error"となって認証に失敗する。

COPY out.cer /gitbucket/
# keytool is an interactive tool...
RUN echo yes | keytool -keystore /etc/ssl/certs/java/cacerts -storepass changeit -importcert -file /gitbucket/out.cer 

("changeit"がデフォルトのキーストアパスワードになっている。)
更に、テスト環境ではmDNSを使ってホストのIPアドレスをlookupしているが、Dockerコンテナ内部からは基本的にmDNS lookupはできないため、docker-compose.yamlにextra_hostsを追加して静的に/etc/hostsに追加してやる必要がある。

extra_hosts:
 - ubuntults.local:${MYIP4}

ここでは、mDNSホスト名としてubuntults.local、実際のIPv4アドレスは環境変数MYIP4に格納しているものと仮定している。

RocketChatの認証をOAuth2で行う

RocketChat自体はSAMLに対応しているのでそちらを使うのがスジな気もするが、どうもKeycloakが生成する生のRSA証明書が扱えないようなので一旦OAuth2で試してみた。
OpenID ConnectはOAuth2上に実現されているため、KeycloakのOpenID Connect実装をOAuth2実装と見做して認証に使用できる。Keycloakに作成したRealmの名前が"Check"の場合、

で、GitBucket同様、自己署名証明書の対策も必要になる。RocketChatはnode.jsで書かれているため、単に NODE_TLS_REJECT_UNAUTHORIZED=0 を環境変数に設定すればチェック自体を無効化できる。

# Allow self-signed certificate for now (for OAuth2)
- NODE_TLS_REJECT_UNAUTHORIZED=0

... たぶんちゃんと証明書をインストールした方が良いが、どうせ本番環境ではLet's encryptするのでとりあえず逃げておく。