NGINX Ingress ControllerとOauth2でJenkinsにSSO

CI/CDでJenkinsを使用している方も多いと思います。社内のプロジェクトなんかで使うと、SSOを使ってユーザー認証およびユーザー権限の付与もできたりすると運用が楽になるのでそのあたりをまとめてみました。
SSOの認証は別のIdpを使い、ユーザーのグループ情報はLDAPを利用して取得します。
IdpやActive Directorなどへのユーザー情報は別プロセスで自動的におかなわれているのが前提で、たとえばユーザーがプロジェクトから抜けた場合は自動的にIdpからも削除されActive Directoryのグループからも削除されることとしています。


#使用するもの
* NGINX Ingress Controller - https://www.nginx.co.jp/products/products-nginx/kubernetes-ingress-controller/
* Kubernetes - https://kubernetes.io/ja/
* OAuth2 Proxy - https://github.com/oauth2-proxy/oauth2-proxy, https://oauth2-proxy.github.io/oauth2-proxy/docs/
* Jenkins - https://www.jenkins.io/
* Jenkins Helm Chart - https://github.com/jenkinsci/helm-charts
* Jenkins plugin 
    * Reverse Proxy Auth - https://plugins.jenkins.io/reverse-proxy-auth-plugin/
    * Configuration as Code - https://plugins.jenkins.io/configuration-as-code/
    * Matrix Authorization Strategy - https://plugins.jenkins.io/matrix-auth/

#OAuth2とIdpを使ってSSOを実装
### Idp
まずはIdpの設定が必要です。これはプロバイダーで異なったりすると思いますが、主に重要な設定は以下の通りです。

```
Redirect URIs = https://jenkins_url.name.company/oauth2/callback
Logout URIs = https://jenkins_url.name.company/oauth2/sign_out
```
`Redirect URIs`は認証が成功した後にリダイレクトされるアプリケーションのURLで、このURLにアクセスしてセッションを開始しクッキーが付与されます。

###OAuth2の設定
Ingressを2つ用意します。
* Oauth2 - Jenkins用のトラフィック。/oauth2
* Nginx - 通常のアプリケーションのトラフィック。/oauth2のパス以外のすべて
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/799224/d11fa3b3-c54d-b6df-b725-34534fed156f.png)

詳細は、設定次第で変わりますが全体の流れはこのような感じです。

1. Jenkinsにアクセス。
2. TLSオフロードを行った後に、Oauth2のサービスに転送して認証確認。認証していないので401を返します。図では書いていませんが正確に言うとK8Sのサービスが認証用とJenkins用と2つあります。
3. セッションクッキーがないので、認証するためにIdpへリダイレクト。
4. Idpで認証。
5. Idpで認証された後、`Redirect URIs`へリダイレクト。
6. 図には書いていませんが、`Redirect URIs`へリダイレクトされセッションクッキーを付与された後、Jenkinsへアクセス。今回はセッションクッキーを持ってアクセス。
7. TLSオフロードを行った後に、Oauth2のサービスに転送して認証確認。今度は認証済みなので202が返ってきます。
8. セッションクッキーがあるので、Jenkinsに転送。

やはり重要なのは、Jenkinsから見て外部のIdpサービスで認証を行うところで、Jenkinsの運用は格段に楽になると思います。また、一度このように設定すると他のアプリケーションも同様に認証サービスを設定でき、Idpで認証・ユーザー管理などを一元化することも可能です。

###Nginx Ingress Controllerの設定
```nginx.yaml
# ------------------- Nginx Ingress ------------------- #
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: jenkins-ingress
  namespace: jenkins
  annotations:
....
    kubernetes.io/ingress.class: "nginx"
    nginx.ingress.kubernetes.io/auth-signin: 'https://$host/oauth2/start?rd=$escaped_request_uri'
    nginx.ingress.kubernetes.io/auth-url: 'https://$host/oauth2/auth'
    nginx.ingress.kubernetes.io/configuration-snippet: |
      auth_request_set $user   $upstream_http_x_auth_request_user;
      proxy_set_header X-Forwarded-UserID  $user;
      proxy_set_header X-Forwarded-GroupID "dummy"; 
    nginx.ingress.kubernetes.io/ssl-redirect: 'true'
      #グループ情報はIdpより提供されないので適当にダミーの情報を設定
...
spec:
  tls:
    - secretName: jenkins_cert
  rules:
    - host: jenkins_url.name.company
      http:
        paths:
          - backend:
              serviceName: jenkins-service
              servicePort: 8080
            path: /
```
* Nginx ingressは Uri:/ のパスに設定
* 古いNginx ingress controllerのバージョンだとOAuth2とうまく動かないかもしれません。
* 認証されたユーザー情報を`upstream_http_x_auth_request_user`から取得し`X-Forwarded-UserID`ヘッダーに格納。ヘッダーはアプリケーションによって違なります。
* グループ情報はIdpより提供されない仮定なので適当にダミーの情報を設定。これにより、Jenkins側でグループ情報を確実に取得するようにするため。

###OAuth2 Ingressの設定
```oauth2-ingress.yaml
kind: Ingress
apiVersion: extensions/v1beta1
metadata:
  name: oauth2-proxy
  namespace: jenkins
  annotations:
    kubernetes.io/ingress.class: "nginx"
    nginx.ingress.kubernetes.io/ssl-redirect: 'true'
spec:
  tls:
    - secretName: jenkins_cert
  rules:
    - host: jenkins_url.name.company
      http:
        paths:
          - backend:
              serviceName: oauth2-proxy
              servicePort: 4180
            path: /oauth2
```
* OAuth2 ingressは Uri:/oauth2 のパスに設定。`App Namespace`の`OAuth2`サービスがアップストリームになります。
* Nginx Ingressと同じURLですがbackendは/oauth2のパスで転送するサービスも先ほどとは違いoauth2のサービスです。
* `secretName: jenkins_cert`は先ほどと同じです。Nginx Ingress Controllerを使うことによってパスごとに違うサービスに転送することができます。

###OAuth2サービスの設定
```oauth2-service.yaml
apiVersion: v1
kind: Service
metadata:
  labels:
    app: oauth2-proxy
  name: oauth2-proxy
  namespace: jenkins
spec:
  ports:
  - name: http
    port: 4180
    protocol: TCP
    targetPort: 4180
  selector:
    app: oauth2-proxy
```
* 上の図で２と７で示されているNginxからOAuth2へのトラフィックのために、OAuth2のPodへNginxのNamespaceからアクセスできるサービスの設定が必要です。
* この例ではTLSはすでにオフロードされているので、Portは任意のHTTP（今回はPodと同じ4180）を設定しす。
* 次に設定するPodをサービスで有効にするためにselector`app: oauth2-proxy`を追加します。

###OAuth2 Proxy Deploymentの設定
```oauth2-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: oauth2-proxy
  name: oauth2-proxy
  namespace: jenkins
spec:
  replicas: 1
  selector:
    matchLabels:
      app: oauth2-proxy
  template:
    metadata:
      labels:
        app: oauth2-proxy
    spec:
      containers:
      - args:
        - --provider=oidc
        - --provider-display-name=My Idp Provider
        - --client-id=my_openid_id
        - --client-secret=***********
        - --cookie-secure=true
        - --cookie-name=myjenkins
        - --cookie-secret=*************
        - --cookie-expire=1h0m0s
        - --redirect-url=https://jenkins_url.name.company/oauth2/callback
        - --oidc-issuer-url=https://openid.url.com # Idp URL
        - --email-domain=mycompany.com
        - --http-address=0.0.0.0:4180
        - --reverse-proxy=true
        - --upstream=http://127.0.0.1:8080 # Jenkinsのサービスのポート
        - --scope=openid
        - --set-xauthrequest=true
        - --skip-provider-button=true
        image: quay.io/oauth2-proxy/oauth2-proxy
        name: oauth2-proxy
        ports:
        - containerPort: 4180
          protocol: TCP
```
* `app: oauth2-proxy`を設定して先ほど設定したサービスを通してアクセスできるようにします。
* `--client-id`、`--client-secret`はIdp側で設定したものです。
* `--set-xauthrequest=true`で`X-Auth-Request-User`などのヘッダー情報を取得します。
* `--redirect-url`は先ほどIdpで設定したURLと同じです。
* `--cookie-expire=1h0m0s`で1時間セッションを維持できます。1時間後にIdpで再度認証が必要となります。 

#Jenkinsの認可の設定
先ほどまではIdpを使って認証(Authentication)を設定しました。次はJenkins側で認可(Authorization)の設定をします。Idp側でユーザーのグループ情報は取り扱っていないという仮定です。ですのでJenkinsにアクセスする段階では、ユーザーが認証されて”誰か”と言うのはわかっていますが、”何ができるか”はまだわかっていません。

```jenkins-helm.yaml
InstallPlugins:
    - configuration-as-code:latest
    - matrix-auth:latest
    - reverse-proxy-auth-plugin:latest
```
```jenkins-jcasc.yaml
  JCasC:
    enabled: true
    PluginVersion: 1.5
    SupportPluginVersion: 1.5
    ConfigScripts:
      welcome-message: |
        jenkins:
          systemMessage: Jenkins 
          authorizationStrategy:
            projectMatrix:
              grantedPermissions:
                - "Overall/Administer:Jenkins_Admins"
                - "Job/Read:Jenkins_EndUsers"
          securityRealm:
            reverseProxy:
              disableLdapEmailResolver: false
              displayNameLdapAttribute: "displayname"
              emailAddressLdapAttribute: "email"
              forwardedUser: "X-Forwarded-UserID"
              groupNameAttribute: "memberOf"
              groupSearchBase: "OU=MSX,OU=Resources"
              groupSearchFilter: "(member={0})"
              headerGroups: "X-Forwarded-GroupID"
              headerGroupsDelimiter: "|"
              inhibitInferRootDN: false
              managerDN: "ldap_account"
              managerPassword: "********"
              rootDN: "DC=company,DC=private"
              server: "ldaps://ldap.name.company"
              updateInterval: 10
              userSearch: "sAMAccountName={0}"
              userSearchBase: "ou=userid"
```
* Configuration as Codeはすでに設定されているとします。
* JenkinsにReverse Proxy AuthとMatrix Authorization Strategyのプラグインをインストールします。
* `forwardedUser`に先ほど`X-Forwarded-UserID`に格納したユーザー情報を認証済されているものとして設定し、ユーザーが属しているグループ情報をLDAPで検索。
* グループごとにMatrix Authorization Strategyでパーミッションの設定。
* 上の例ですと、`Jenkins_Admins`は管理者権限があり、`Jenkins_EndUsers`はJobを見る権限が付きます。
* `X-Forwarded-UserID`のユーザーがLDAPで検索した`Jenkins_Admins`グループに所属していれば管理者としての権限が付与されます。

#まとめと課題
* Jenkinsから見て外部のIdpサービスで認証を行うところで、Jenkins側でユーザー情報を持つ必要がなくなります。
* 一度Idpを設定すると他のアプリケーションも同様に認証サービスを設定でき、Idpで認証・ユーザー管理などを一元化することも可能です。
* Idp側でグループ情報も管理できるとさらに楽な運用となり得ます。JenkinsでLDAPなどをつかってグループ情報を得る必要がありません。
* Idpは高可用性(High Availability)でなくてはなりません。他のアプリケーションなども同様に設定すると、Idpの可用性、セキュリティーはさらに重要になります。
* Jenkinsは本番環境を変更しうるシステムなので、IdpはMFA(多要素認証)をサポートしているサービスのほうがセキュリティー的には良いと考えられます。
* Idpが何かの理由でダウンしてしまうとすべてのアプリケーションが使えなくなります。ただそのリスクを加味しても、運用性の良さは検討の価値があると思われます。