🐳

[Kuberntes] 汎用OAuth2 Proxyをサービスの手前に置く:認証認可編

2022/02/15に公開

前回の記事を読んでない人は

先にこちらをご覧下さい
https://zenn.dev/takitake/articles/23fb840c925475

本日のゴール

  1. oauth2-proxyがOpenID Connectプロバイダー兼認可サーバー(Keycloak)からアクセストークンを受け取れるようにする
  2. Upstreamのサービスが、X-Auth-Request-XXXを用いて認証を行う

システムアーキテクチャ図

Auth System architecture

OIDCのフローはだいぶ端折っているので、詳しくは下記の記事が分かりやすいと思います。
https://qiita.com/TakahikoKawasaki/items/498ca08bbfcc341691fe

今回は、既存のKeycloadサーバーと連携してますが、時間とPCリソースに余裕のある方はKeycloakもKubernetes上にデプロイすると、より理解が深まるかと。

oauth2-proxy用のNamespaceを作成

テナントのNamespaceには、OIDC Client情報を公開しないコンセプトですので、oauth2-proxyは専用のNamespaceで運用します。

必要なリソースを作成

Namespaceは cicd-system としていますが、こちらも好みで変更してください。

CLIENT_ID, CLIENT_SECRETおよびOIDC_ISSUERは、Keycloakの設定によって読み替えてください。

COOKIE_SECRETは、oauth2-proxyが発行するCookieのシードに使われる値で、ランダムな文字列の生成方法が紹介されています。
https://oauth2-proxy.github.io/oauth2-proxy/docs/configuration/overview#generating-a-cookie-secret

sessionの値をCookieに持たせるとヘッダーが大きすぎてエラーになったりするので、Redisに保存するためにsidecar方式でRedis containerも追加しています。本番運用する場合は、スケーラビリティや可用性の観点から別途Redis Clusterを作成した方が良いです。

cicd-system.yaml
apiVersion: v1
kind: Namespace
metadata:
  labels:
    istio-injection: enabled
  name: cicd-system
---
apiVersion: v1
kind: Service
metadata:
  labels:
    app: oauth2-proxy
  name: oauth2-proxy
  namespace: cicd-system
spec:
  ports:
  - name: http
    port: 80
    protocol: TCP
    targetPort: 4180
  selector:
    app: oauth2-proxy
  type: ClusterIP
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: oauth2-proxy
  name: oauth2-proxy
  namespace: cicd-system
spec:
  replicas: 1
  selector:
    matchLabels:
      app: oauth2-proxy
  template:
    metadata:
      labels:
        app: oauth2-proxy
    spec:
      containers:
      - args:
        - --client-id={{ CLIENT_ID }}
        - --client-secret={{ CLIENT_SECRET }}
        - --whitelist-domain=.svc.macbook.local
        - --cookie-domain=.svc.macbook.local
        - --cookie-secret={{ COOKIE_SECRET }}
        - --cookie-secure=false
        - --email-domain=*
        - --http-address=0.0.0.0:4180
        - --oidc-issuer-url={{ OIDC_ISSUER }}
        - --pass-access-token=false
        - --pass-authorization-header=true
        - --pass-host-header=true
        - --pass-user-headers=true
        - --provider=oidc
        - --redirect-url=http://oauth2-proxy.cicd-system.svc.macbook.local/oauth2/callback
        - --reverse-proxy=true
        - --set-xauthrequest=true
        - --upstream=static://200
	- --skip-provider-button=true
        - --session-store-type=redis
        - --redis-connection-url=redis://localhost:6379
        image: quay.io/oauth2-proxy/oauth2-proxy:v7.2.1
        name: oauth2-proxy
        ports:
        - containerPort: 4180
          protocol: TCP
      - image: redis:6.2.6-alpine
        name: redis
---
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: cicd-system-gateway
  namespace: cicd-system
spec:
  selector:
    istio: ingressgateway
  servers:
  - hosts:
    - '*.cicd-system.svc.macbook.local'
    port:
      name: http
      number: 80
      protocol: HTTP
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: oauth2-proxy
  namespace: cicd-system
spec:
  gateways:
  - cicd-system-gateway
  hosts:
  - oauth2-proxy.cicd-system.svc.macbook.local
  http:
  - route:
    - destination:
        host: oauth2-proxy.cicd-system.svc.cluster.local
        port:
          number: 80
$ kubectl apply -f cicd-system.yaml

KeycloadにリダイレクトURLを登録

http://oauth2-proxy.cicd-system.svc.macbook.local/oauth2/callback

/etc/hostsを更新

oauth2-proxy用のドメインも追加してください。

/etc/hosts
127.0.0.1 oauth2-proxy.cicd-system.svc.macbook.local

Istioに外部認可サーバーを登録

要はoauth2-proxyサービスのエンドポイントと、Upstreamのサービスに送るヘッダー等を設定します。

既存のConfigMapを取得して

$ kubectl get cm istio -n istio-system -o yaml > istio.cm.yaml

extensionProvidersとして、oauth2-proxyを登録します。

istio.cm.yaml
    extensionProviders:
    - name: oauth2-proxy
      envoyExtAuthzHttp:
        service: oauth2-proxy.cicd-system.svc.cluster.local
        port: 80
        timeout: 10s
        includeHeadersInCheck:
          - cookie
          - authorization
	includeAdditionalHeadersInCheck:
          X-Auth-Request-Redirect: 'http://%REQ(Host)%'
        headersToUpstreamOnAllow:
          - authorization
          - path
          - x-auth-request-user
          - x-auth-request-email
          - x-auth-request-groups
          - x-auth-request-preferred-username
        headersToDownstreamOnDeny:
          - content-type
          - set-cookie
$ kubectl apply -f istio.cm.yaml

Upstreamサービスとして、Jenkinsをデプロイする

上記のステップまで設定が完了していれば、あとはどんなサービスでもUpstreamサービスとして置くことが可能です。今回は、みんな大好きJenkinsと連携させてみます。

あくまでテスト用なので、Podリスタートしたら設定は消し飛びますのであしからず。永続化したい場合は、PersistentVolumeを作成してJENKINS_HOMEにマウントしてください。

必要なリソースを作成

default Namespaceは既に存在しますが、Istio sidecarのインジェクションを有効にしたいのでNamespaceの再定義しています。

肝はAuthorizationPolicyリソースで、CUSTOM actionとしてoauth2-proxy(IstioにextensionProvidersとして登録した名前)を指定しています。そして、ラベルセレクター(任意のセレクターを設定可能)にマッチしたPodへのアクセスは、自動的にoauth2-proxyにプロキシされ200 OKが返ってくればそのままUpstreamへリクエストが送られますが、それ以外の場合はKeycloakのログインフォームに遷移するので、ユーザー名とパスワードを入力してログインすると再びアプリのトップページに戻ってこれます。
この時、アプリはx-auth-request-xxxヘッダーを受け取ることができるので、ユーザー名やグループ名での認証認可が可能になります。言い換えると、それらの値を利用しないとアプリにとってはanonymousユーザーになってしまうので、注意が必要です。

default.yaml
apiVersion: v1
kind: Namespace
metadata:
  labels:
    istio-injection: enabled
  name: default
---
apiVersion: v1
kind: Service
metadata:
  labels:
    app: jenkins
  name: jenkins
  namespace: default
spec:
  ports:
  - name: http
    port: 8080
    protocol: TCP
    targetPort: 8080
  selector:
    app: jenkins
  type: ClusterIP
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: jenkins
  name: jenkins
  namespace: default
spec:
  replicas: 1
  selector:
    matchLabels:
      app: jenkins
  template:
    metadata:
      labels:
        app: jenkins
        oauth: enabled
    spec:
      containers:
      - image: jenkins/jenkins:2.333-alpine-jdk8
        name: jenkins
---
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: default-gateway
  namespace: default
spec:
  selector:
    istio: ingressgateway
  servers:
  - hosts:
    - '*.default.svc.macbook.local'
    port:
      name: http
      number: 80
      protocol: HTTP
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: jenkins
  namespace: default
spec:
  gateways:
  - default-gateway
  hosts:
  - jenkins.default.svc.macbook.local
  http:
  - route:
    - destination:
        host: jenkins
        port:
          number: 8080
---
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  name: ext-authz-oauth2-proxy
  namespace: default
spec:
  action: CUSTOM
  provider:
    name: oauth2-proxy
  rules:
  - to:
    - operation:
        hosts:
        - jenkins.default.svc.macbook.local
  selector:
    matchLabels:
      oauth: enabled
$ kubectl apply -f default.yaml

Jenkinsの初期設定

Jenkins PodのログからinitialAdminPasswordを取得

$ kubectl logs $(kubectl get po -l app=jenkins --no-headers -o name | cut -f2 -d/)
*************************************************************
*************************************************************
*************************************************************

Jenkins initial setup is required. An admin user has been created and a password generated.
Please use the following password to proceed to installation:

{{ initialAdminPassword }}

This may also be found at: /var/jenkins_home/secrets/initialAdminPassword

*************************************************************
*************************************************************
*************************************************************

認証回避のためport-forwardでJenkins Podへ直接アクセス

これができちゃうので、テナントにはNamespaceへアクセスする権限を与えない方が良いです。

$ kubectl port-forward svc/jenkins 8080:8080

http://localhost:8080

初期設定

Unlock Jenkins

先程の initialAdminPassword を入力して、Continueをクリック。
Install Suggested Pluginsも入れておきましょう。

Admin Userは適当に設定しておいてください。x-auth-requestでのログインを有効にした後は、adminユーザとしてログインはできなくなります。

Jenkins URLは、localhostではなく jenkins.default.svc.macbook.local を設定してください。hostsファイルの更新もお忘れなく。

/etc/hosts
127.0.0.1 jenkins.default.svc.macbook.local

x-auth-request-xxxで認証できるようにする

http://localhost:8080/pluginManager/
Manage Jenkins => Manage Plugins => Availableで
"reverse proxy auth"と検索すると、同名のプラグインがヒットするはずです。
プラグイン名にチェックをつけてインストールしましょう。

http://localhost:8080/configureSecurity/
Manage Jenkins => Configure Global Security
Security Realmとして、"HTTP Header by reverse proxy"を選択してデフォルト値を下記の値で上書きした後に保存してください。

Header User Name: X-Auth-Request-User もしくは X-Auth-Request-Preferred-Username
Header Groups Name: X-Auth-Request-Groups
Header Groups Delimiter Name: ,

Config Security

動作確認

全部の設定がうまくいっていれば、下記のページにアクセスすると
http://jenkins.default.svc.macbook.local/

  1. Keycloakのログインページにリダイレクトされる
  2. ユーザー名、パスワードを入力してログインボタンをクリックすると、Jenkinsのトップページに戻ってくる

右上のユーザー名にはHeader User Nameに指定したヘッダーの値が表示されているはずです。

Groupsは http://jenkins.default.svc.macbook.local/whoAmI/ から確認可能です。区切り文字が間違っていると、Groupがリスト表示されないのでその場合は http://localhost:8080/configureSecurity/ からDelimiter Nameを変更してください。

応用

ユーザー名やグループ名を利用して、Jenkins UIレベルのRoll Base Access Control(RBAC)を設定したい場合は、追加でRole-based Authorization Strategyプラグインをインストールしてください。

http://jenkins.default.svc.macbook.local/configureSecurity/
Manage Jenkins => Configure Global Security
AuthorizationをRole-Based Strategyに変更

Authorization

http://jenkins.default.svc.macbook.local/manage
Manage Jenkinsに戻ると、Manage and Assign Rolesという項目が増えているので、こちらからRoleとAssignmentを設定可能です。

Manage and Assign Roles

フォルダーのパターンは再起的にマッチするようにしないと、親フォルダへのアクセス権限が無いために子フォルダにアクセスできないことになります。

Foo/Bar/Bazフォルダ以下全部のジョブ or フォルダに権限を与えたい場合は

Job Readの権限を以下のフォルダに対して与えるRoleを定義して

  • Foo
  • Foo/Bar
  • Foo/Bar/Baz
  • Foo/Bar/Baz/.*
^Foo(|/Bar(|/Baz(|/.*)))

全権限を以下のフォルダに与えるRoleを別途定義する必要があります。

  • Foo/Bar/Baz
  • Foo/Bar/Baz/.*
^Foo/Bar/Baz(|/.*)

そして、対象のUser/Groupに上記2つのRoleをアサインすれば、Foo/Barフォルダへはアクセス権限のみで、Foo/Bar/Baz以下のフォルダでは自由にジョブの作成が可能になる権限を付与することができます。

Discussion