tech.guitarrapc.cóm

Technical updates

DevtronにみるDexのサブパス統合がOIDC連携に生む制約

Kubernetesにおいて、Dexを使ってOIDC連携しているアプリケーションは多くあります。 今回はアプリケーションのDex組み込み方法によってOIDC連携に制約が生まれることをDevtronを例に見てみましょう。

Dexとは

DexはオープンソースのOIDCプロバイダーであり、IdP(Identity Provider)と連携してOIDC認証を提供します。Kubernetesで動作するアプリケーションのOIDCプロバイダーとしてよく使われ、OIDC認証を用いたシングルサインオン(SSO)を提供しているアプリケーションのhelmチャートで見かけたことがある人も多いでしょう。

ただ近年はDex離れも見られ、当初Dexを使ってOIDC認証を提供していたものの、独自OIDC認証実装へ移行しているものもあります。例えばArgoCDは現在、Bundled Dexを使ったSSO認証と、外部OIDCプロバイダーへの直接連携の両方をサポートしています。個人的には、Dexを使っているアプリケーションのOIDC実装次第でOIDC連携を思ったようにできないケースがあり、Dexを使ったOIDC設定は避けたい気持ちがあります。

ということで、Devtronを例にDexを使っているアプリケーションのOIDC連携の制約を紹介します。

Dexの組み込み方法

アプリケーションにDexを組み込む方法は大きく分けて2つあります。

  1. 独立ホスト: Dexが独自のHost URL(例: dex.example.com)を持ち、アプリケーションとは別のエンドポイントとして公開
  2. リバースプロキシ方式(サブパス統合): アプリケーションが単一のHost URLを持ち、その配下に/api/dexのようなパスでDexを統合。同じURLが「外部からのブラウザアクセス」と「Pod内部からのDex API呼び出し」の両方に使われる

ほとんどのアプリケーションは1の方法を採用しており、利用者としてもスムーズなOIDC設定が可能です。しかし、Devtronは2の方法を採用しておりOIDC連携に制約を生んでいます。

DevtronとOIDC認証

Devtronは、OIDC認証を設定することで任意のIdPでSSOログインできます。DevtronのOIDC認証設定はDexであり、OIDCに関する設定要素が並んでいます。以下は、CognitoをIdPとして利用しつつ、DevtronをIngressで公開している場合の設定例です。

要素 説明 設定例
URL Dexの.well-known/openid-configurationエンドポイントURLのベースURL https://devtron.example.com/orchestrator
name Dexの任意の構成名 cognito
id Dexの任意の構成ID cognito
clientID OIDCクライアントID。IdPのクライアントIDを用いる ••••••••
clientSecret OIDCクライアントシークレット。IdPのクライアントシークレットを用いる ••••••••
issuer IdPのIssuer URL https://cognito-idp.REGION.amazonaws.com/REGION_USER-POOL-ID
redirectURI DevtronのOIDCコールバックURL。ブラウザアクセス時のURLホストに相当する https://devtron.example.com/orchestrator/api/dex/callback

Devtron上では、以下yamlのように設定します。

# URL: https://devtron.example.com/orchestrator
name: cognito
id: cognito
config:
 claimMapping:
  groups: cognito:groups
 clientID: ••••••••
 clientSecret: ••••••••
 issuer: https://cognito-idp.REGION.amazonaws.com/USER-POOL-ID
 redirectURI: https://devtron.example.com/orchestrator/api/dex/callback
 scopes:
  - openid
  - email
  - profile
 userNameKey: cognito:username

外部からOIDC認証を試みると、IdPのログイン画面が表示され、ログイン後にRedirectURIへリダイレクトされます。

先ほどのDevtronのOIDC認証設定には、DexのHost URLを指定する項目がありません。なぜなら、DevtronはDexを独自のHost URLで提供しておらず、ブラウザアクセスURLの一部として提供しているからです。代わりにURLの値に.well-known/openid-configurationパスを追加して、Devtron内部のOIDCクライアントがDexのOIDCエンドポイントにアクセスします。上記の例なら、URLに設定したhttp://localhost:8082/orchestratorをベースにしたhttps://devtron.example.com/orchestrator/api/dex/.well-known/openid-configurationを使ってDexエンドポイントにアクセスを試みるということです。

ちなみに、.well-known/openid-configurationエンドポイントはOIDCプロバイダーのメタデータを提供するための標準エンドポイントです。Devtronに組み込まれているDexもこのエンドポイントを提供しており、例えばhttps://devtron.example.com/orchestrator/api/dex/.well-known/openid-configurationエンドポイントにアクセスすると、次のようなOIDCプロバイダーJSONレスポンスが返ってきます。

{
  "issuer": "https://devtron.example.com/orchestrator/api/dex",
  "authorization_endpoint": "https://devtron.example.com/orchestrator/api/dex/auth",
  "token_endpoint": "https://devtron.example.com/orchestrator/api/dex/token",
  "jwks_uri": "https://devtron.example.com/orchestrator/api/dex/keys",
  "userinfo_endpoint": "https://devtron.example.com/orchestrator/api/dex/userinfo",
  "device_authorization_endpoint": "https://devtron.example.com/orchestrator/api/dex/device/code",
  "grant_types_supported": [
    "authorization_code",
    "refresh_token",
    "urn:ietf:params:oauth:grant-type:device_code"
  ],
  "response_types_supported": [
    "code"
  ],
  "subject_types_supported": [
    "public"
  ],
  "id_token_signing_alg_values_supported": [
    "RS256"
  ],
  "code_challenge_methods_supported": [
    "S256",
    "plain"
  ],
  "scopes_supported": [
    "openid",
    "email",
    "groups",
    "profile",
    "offline_access"
  ],
  "token_endpoint_auth_methods_supported": [
    "client_secret_basic",
    "client_secret_post"
  ],
  "claims_supported": [
    "iss",
    "sub",
    "aud",
    "iat",
    "exp",
    "email",
    "email_verified",
    "locale",
    "name",
    "preferred_username",
    "at_hash"
  ]
}

先のOIDC設定はDevtronのアプリエンドポイント(=Dexのエンドポイント) https://devtron.example.com はグローバルに公開されているので、Devtronをホストしているpod -> グローバルなdevtronエンドポイントという経路でDexの.well-known/openid-configurationエンドポイントにアクセスできます。

このことがDevtronをポートフォワードで接続しつつ、OIDC認証することに制約を生んでいます。

ローカルマシンからポートフォワードでDevtronにアクセスしつつOIDC認証を動かせるのか

結論から言うと、ポートフォワードだけで完結させたままDevtronのOIDC認証は動作しません。これは、devtronは単一URLという前提で設計されており、その単一URLを使って2つの役割を果たしているからです。

  • ブラウザからアクセス元として信頼する(redirectURIの検証)
  • Dexをたたく時のベースURLにも使う(/api/dex/.well-known/openid-configurationエンドポイントの構築)

ポートフォワードとしてlocalhost:8082でDevtronへアクセスする場合と、クラスター内部のサービス名でDevtronにアクセスする場合の2つのケースを考えてみましょう。先に、設定例と結果をまとめた表を示します。

設定項目 localhost:8082の例 cluster.localの例
URL (ベースURL) http://localhost:8082/orchestrator http://devtron-service.devtroncd.svc.cluster.local/orchestrator
redirectURI http://localhost:8082/orchestrator/api/dex/callback http://localhost:8082/orchestrator/api/dex/callback
ブラウザアクセス ✅ 成功 ✅ 成功
Pod内部アクセス ❌ 失敗(Pod内localhostに到達不可) ✅ 成功
RedirectURIの検証 ✅ 成功 ❌ 失敗(URLとRedirectURIでホスト&ポートが不一致)
結果 connection refused Invalid redirect URL

localhost:8082の例

よくあるポートフォワードでDevtronにアクセスする場合どうなるか考えてみましょう。今回はブラウザからはhttp://localhost:8082のようなURLでポートフォワードを介してアクセスします。

設定は以下のようになりますが、これではOIDC認証できません。

要素 説明 設定例
URL Dexの.well-known/openid-configurationエンドポイントURLのベースURL http://localhost:8082/orchestrator
name Dexの任意の構成名 cognito
id Dexの任意の構成ID cognito
clientID OIDCクライアントID。IdPのクライアントIDを用いる ••••••••
clientSecret OIDCクライアントシークレット。IdPのクライアントシークレットを用いる ••••••••
issuer IdPのIssuer URL https://cognito-idp.REGION.amazonaws.com/REGION_USER-POOL-ID
redirectURI DevtronのOIDCコールバックURL。ブラウザアクセス時のURLホストに相当する http://localhost:8082/orchestrator/api/dex/callback

Devtron上では、以下yamlのように設定します。

# URL: http://localhost:8082/orchestrator
name: cognito
id: cognito
config:
 claimMapping:
  groups: cognito:groups
 clientID: ••••••••
 clientSecret: ••••••••
 issuer: https://cognito-idp.REGION.amazonaws.com/USER-POOL-ID
 redirectURI: http://localhost:8082/orchestrator/api/dex/callback
 scopes:
  - openid
  - email
  - profile
 userNameKey: cognito:username

この設定でOIDC認証を試みると次のようなエラーが発生します。

Failed to query provider "http://localhost:8082/orchestrator/api/dex": Get "http://localhost:8082/orchestrator/api/dex/.well-known/openid-configuration": dial tcp [::1]:8082: connect: connection refused

エラーは、DevtronのpodからdexのOIDCエンドポイントhttp://localhost:8082/orchestrator/api/dex/.well-known/openid-configurationへアクセスするときに発生しています。なぜなら、localhost:8082はブラウザから見たDevtronのエンドポイントですが、Devtronのpodから見たlocalhost:8082はDevtronのpod自身を指しており、Devtronのpod内で動作しているDexエンドポイントにはアクセスできないからです。 具体的には、Devtronの初期時にUserAuthOidcHelper.go/NewUserAuthOidcHelperImplにてDexConfigのURL設定を使ってclient.GetOidcClient()を呼び出し、その中で{URL}/.well-known/openid-configurationにアクセスしてOIDCプロバイダー情報を取得しようとします。URLに入るlocalhost:8082はポートフォワードで作られたローカルマシン上のトンネルであり、Kubernetes Pod内から見ると存在しないアドレスなので接続が拒否される、それはそう。

この場合、このような検証が行われています。

  • OK: ブラウザ側のURLと一致するので、RedirectURIの検証は通る
  • NG: Devtronのpodからhttp://localhost:8082/orchestrator/api/dex/.well-known/openid-configurationにアクセスできないので、OIDCプロバイダー情報の取得に失敗する

devtron-service.devtroncd.svc.cluster.localの例

先のlocalhost:8082でアクセスできなかった原因は、Devtronのpodから見たlocalhost:8082がDevtronのpod自身を指してしまうことでした。では、Devtronのpodから見たDevtronのサービス名でアクセスできるように設定すればどうでしょうか?Devtronのサービス名エンドポイントdevtron-service.devtroncd.svc.cluster.localであればクラスター内部のサービスアドレスなので、Devtronのpodからもアクセスできるはずです。

設定は以下のようになりますが、これではOIDC認証できません。なお、リダイレクトURIはブラウザからアクセスするURLホストに相当するので、localhost:8082のままとなります。

要素 説明 設定例
URL Dexの.well-known/openid-configurationエンドポイントURLのベースURL http://devtron-service.devtroncd.svc.cluster.local/orchestrator
name Dexの任意の構成名 cognito
id Dexの任意の構成ID cognito
clientID OIDCクライアントID。IdPのクライアントIDを用いる ••••••••
clientSecret OIDCクライアントシークレット。IdPのクライアントシークレットを用いる ••••••••
issuer IdPのIssuer URL https://cognito-idp.REGION.amazonaws.com/REGION_USER-POOL-ID
redirectURI DevtronのOIDCコールバックURL。ブラウザアクセス時のURLホストに相当する http://localhost:8082/orchestrator/api/dex/callback

Devtron上では、以下yamlのように設定します。

# URL: http://devtron-service.devtroncd.svc.cluster.local/orchestrator
name: cognito
id: cognito
config:
 claimMapping:
  groups: cognito:groups
 clientID: ••••••••
 clientSecret: ••••••••
 issuer: https://cognito-idp.REGION.amazonaws.com/USER-POOL-ID
 redirectURI: http://localhost:8082/orchestrator/api/dex/callback
 scopes:
  - openid
  - email
  - profile
 userNameKey: cognito:username

この設定でOIDC認証を試みると次のようなエラーが発生します。

Invalid redirect URL: the protocol and host (including port) must match and the path must be within allowed URLs if provided

エラーは、リダイレクトURIの検証時に発生しています。クラスター内部のSVC URLを用いることでDevtronのPodからDexエンドポイントに疎通できるのですが、リダイレクトURIhttp://localhost:8082とDex URLhttp://devtron-service.devtroncd.svc.cluster.local/orchestratorのhost+portが違うので検証エラーになっています。

この場合、このような検証が行われています。

  • NG: リダイレクトURLとBaseURLのhost+portが一致しないので、RedirectURIの検証に失敗する
  • OK: Devtronのpodからhttp://devtron-service.devtroncd.svc.cluster.local/orchestrator/api/dex/.well-known/openid-configurationはアクセスできる

なぜDevtronはポートフォワードだけでOIDC認証が動かせないのか

DevtronのOIDC認証はDexを使って提供されていますが、「DevtronのHost URLは単一」で「そのURLを経路の異なるアクセスの両方に使っている」という設計です。経路の異なるアクセスとは、DevtronのPodからDexの.well-known/openid-configurationエンドポイントにアクセスする経路と、ブラウザからDevtronのOIDCコールバックURLにアクセスする経路を指します。Devtronは内部にDexを持ちつつ、この2つの経路で同じURLが到達可能であることを前提としているため、ポートフォワードのみでOIDC認証を完全に動作させることはできません。

  • クライアント側(ブラウザ): OIDC認証フローのリダイレクトURL生成とコールバック検証
  • サーバー側(Pod内部): Dexサービスへの直接接続と.well-known/openid-configurationの取得

まとめ

DevtronのOIDC認証を使う場合は、IngressやLoadBalancerを使って内外両方からアクセスを担保できる統一されたエンドポイントを用意しましょう。ただし、このエンドポイントはDevtronのPodが起動しているNodeからもアクセス可能である必要があるため、IP制限でNodeを考慮する必要がありこれはこれで厄介です。NAT GWを使っているならばNAT GWのIPを許可するなどの対応でよいのですが、そうでない場合はDevtronのPodが起動しているNodeのIPを特定して許可する必要があります。

Dexが悪いわけじゃないのですが、Dexをアプリケーションの裏に組み込む時、単一URLでのアクセスを前提とするのはOIDC連携に制約を生むことがあるので注意しましょう。

参考