OpenShiftクラスター認証とIBM Cloud認証とoauth-proxyを使って開発中のコンテナアプリケーションへのアクセスを保護する

# シナリオ(実際に手元で必要になった状況)
Red Hat OpenShift on IBM Cloud (クラシック・インフラストラクチャー、OpenShift 4.5) を使用して開発・テスト環境を構築している。
このクラスターには開発途中のテスト用のアプリケーション・コンテナーを配置して稼働させる予定であるが、開発中のコンテナアプリケーションへのアクセスは開発チームメンバーに限りたい。
ただ、VPN接続などを新規に構築してプライベート・ネットワーク接続を実現するといったことは構成が複雑となるためできれば避けたい。またアプリ自体に独自の認証を組み込む方法もあるがアプリケーションの構造に開発環境独自の構造は含めないようにしたい。

OpenShiftクラスターのコンソールへのアクセスにはIBM CloudのIAM認証(Identity and Access Management)がかけられていて、IBM Cloudのアカウントに登録されクラスターへのアクセス権が与えられたユーザーだけがアクセスできるようになっている。この時の認証はIBM Cloudの認証の設定が使われていて2要素認証など高度な認証設定を行うこともできる。
何とか自作のアプリにも同じ認証を仕掛けて保護することはできないものか？

# 環境
- OpenShift 4.3以降 (記事作成時点での検証環境は OpenShift 4.5)

# 方法
oauth-proxyのOpenShift版(https://github.com/openshift/oauth-proxy) を使用するといい具合に実現できる。

コンテナイメージ: OpenShift Oauth Proxy
https://hub.docker.com/r/openshift/oauth-proxy

OpenShiftのドキュメンテーション
https://docs.openshift.com/container-platform/4.1/authentication/using-service-accounts-as-oauth-client.html

oauth-proxyはGo言語で実装されたproxyでOAuthIDプロバイダーによる認証機能を容易に組み込むことを可能にするリバース・プロキシー実装である。
OpenShiftクラスターはOAuth認証サーバー機能を提供しており、OpenShift oauth-proxy はOpenShiftの認証を利用する場合により簡易に利用できるように特化された実装となっている。

oauth-proxyを利用すると以下のようなワークフローで認証がかけられるようになる。
1. ブラウザーでパブリックアドレスに公開されたテスト用のアプリケーションのURLを開く。
2. OpenShiftによる認証画面が表示される（以前に認証していた場合はOAuth認証トークンの有効期限内であればこの画面はスキップされる)
3. OpenShift on IBM CloudのOpenShiftクラスターの場合は、IBM Cloudの認証にリダイレクトされてIBM Cloudの認証を行う
4. IBM Cloudのアカウント(IAMアカウント)に基づいてOpenShiftクラスターに登録されているかどうか確認される
5. 認証ができて、OpenShiftクラスターへのアクセス権限を持っていることが確認できたらテスト用アプリケーションが開く

この記事ではクラシック・インフラストラクチャー構成のOpenShiftクラスターでの検証を行った手順で記載する。
原理上はVPCクラスターでも同様に利用可能だと思われるが、VPCクラスターの場合はLoadBalancerサービスへのIPアドレスのアサイン方法が異なる可能性がある。


# アーキテクチャー
oauth-proxyを差し込む場所は大きく2通りある。

* Router の後ろ、アプリケーション・コンテナーの手前に oauth-proxy を入れる
https://github.com/openshift/oauth-proxy に記載された構成。
こちらのやり方については、oauth-proxy の [readme](https://github.com/openshift/oauth-proxy) を参照。

* Router の手前に oauth-proxy を入れる
クラスターの外部から、Routerにアクセスする手前にoauth-proxyを差し込む。Routerへのアクセスはすべてブロックされるようになる。
この記事ではこちらの方法を解説する。

# 構成のステップ
1. 保護アプリケーションにアクセスするアドレス(DNS名)を決める
1. oauth-proxyでアクセスを保護する専用のRouterを追加する
1. oauth-proxy コンテナをデプロイするためのnamespaceを準備する
1. oauth-proxyのコンテナをデプロイする
1. oauth-proxyをLoad Balancerに公開する
1. Routeを作成して保護対象アプリケーションを保護Routerに公開する
1. oauth-proxyの構成を更新して、公開した保護対象アプリケーションのURLをOAuthのリダイレクト先として登録する
1. 動作を確認する
 
## 1. 保護アプリケーションにアクセスするアドレス(DNS名)を決める。
独自DNSサーバーやSSL証明書を運用しているような場合であれば任意のアドレスでよい。
独自DNSサーバーを運用していない場合、IBM Cloud が割り当てるクラスターのサブドメイン(DNS名)を独自に追加する機能を利用することができる。
[クラシック: NLB の DNS サブドメインの登録](https://cloud.ibm.com/docs/containers?topic=containers-loadbalancer_hostname&locale=ja)

```shell:shell
ibmcloud ks nlb-dns create classic --cluster <cluster_name_or_id> --ip ...
```

この手順を実行することでパブリックDNSサーバーにワイルドカード・ドメイン名が登録されるとともに、後述の手順で利用するSSL証明書(Let's Encryptで認証されたもの)が生成され、クラスターに保管される。実際のIPアドレスはLoad Balancerにoauth-proxyを公開したときに決まるがこの時点ではIPアドレスは仮置きで作成する(たとえばデフォルトのDNSサブドメインに割り当てられたパブリックIPを利用するなど)。

ステップ5. でIPアドレスが確定したところで、[nlb-dns](https://cloud.ibm.com/docs/openshift?topic=openshift-kubernetes-service-cli&locale=ja#nlb-dns) コマンドの add と rm を使用して IPアドレスを変更する。

```shell:shell
$ ibmcloud oc nlb-dns classic add --cluster <cluster_name_or_id> --nlb-host ... --ip ...
$ ibmcloud oc nlb-dns classic rm --cluster <cluster_name_or_id> --nlb-host ... --ip ...
```

なお、クラスター作成時にデフォルトで割り当てられたサブドメイン名 ([クラスター名>-unique-id-0000.[region].containers.appdomain.cloud) をそのまま利用することも可能ではあるものの、デフォルトのサブドメインはOpenShiftコンソールのURL（こちらは作成後には基本的に変更は不可）にも使われているため、oauth-proxyによる追加の保護の対象にするものと、保護をしなくてよいものを区別できるようにするために、新しいサブドメインを割り振るほうが良い。


## 2. oauth-proxyでアクセスを保護する専用のRouterを追加する
OpenShiftクラスターを作成するとデフォルトでdefaultという名前のRouterが作成されて、パブリックIPアドレスに公開される。これがクラスターのデフォルトのRouterとなる。
このRouterは残したまま、OAuth認証によって保護したいアプリケーションを公開するための追加のRouterを定義しておく (以降ではデフォルトのRouter特別するために保護Routerと記載する)。

OpenShift 4.5 クラスターにて新しいRouterを作成するためには OpenShift Ingress Operator (IngressController CRD)を使用する。
defaultCertificate には接続に使用するtlsシークレットを指定する (tlsシークレットは nlb-dnsコマンドでサブドメインを作成した場合はdefaultネームスペースに登録されている)。

```shell:shell
$ oc create -f ic-router-protected.yaml
```

保護RouterのためのIngressController定義においてはnamespaceSelector(もしくはrouteSelector)を使用して保護対象のRouteを識別できるラベル条件を入れておく。
以下の例では、Routeを含むnamespaceのlabelとしてroute=router-protectedが定義されている場合のみ、そのRouteを保護Routerに登録するという条件となっている。

* なおデフォルトで作成されるdefault Routerにはこのような条件が何も指定されていないためすべてのRouteはdefault Routerにも公開されてしまう。保護したいRouteをdefault Routerに公開しないように構成するためにはdefault RouterのためのIngressController定義に同様を編集する必要がある。

```yaml:ic-router-protected.yaml
apiVersion: operator.openshift.io/v1
kind: IngressController
metadata:
  name: protected
  namespace: openshift-ingress-operator
spec:
  defaultCertificate:
    name: cluster1-xxxxxxxxxxxxxxxxxxxxxxxxx-0001
  domain: cluster1-xxxxxxxxxxxxxxxxxxxxxxxxx-0001.jp-tok.containers.appdomain.cloud
  endpointPublishingStrategy:
    type: Private
  nodePlacement:
    tolerations:
      - key: dedicated
        value: edge
  namespaceSelector:
    matchLabels:
      router: router-protected
  replicas: 2
```

IngressController リソースが作られると router の pod が openshift-ingress ネームスペース (openshift-ingress-operator ではないことに注意) に作成される。

```shell:shell
$ oc get deployment -n openshift-ingress
NAME             READY   UP-TO-DATE   AVAILABLE   AGE
router-default   2/2     2            2           10d
router-protected 2/2     1            1           9d
```

endpointPublishingStrategy をPrivateに指定しているため、LoadBalancerタイプのサービスはなく、routerはクラスターの外には公開されていない。
ただし、クラスター内でのアクセス用のClusterIPタイプのServiceが作成されていることが確認できる。
(router-defaultおよびrouter-internal-defaultはクラスター作成時にデフォルトで作成された router のためのService)

```shell:shell
$ oc get services -n openshift-ingress
NAME                      TYPE           CLUSTER-IP       EXTERNAL-IP     PORT(S)                      AGE
router-default            LoadBalancer   xxx.xxx.xxx.xxx  xxx.xxx.xxx.xxx 80:30122/TCP,443:31224/TCP   10d
router-internal-default   ClusterIP      172.xx.xx.xxx    <none>          80/TCP,443/TCP,1936/TCP      10d
router-internal-protected ClusterIP      172.xx.xx.xxx    <none>          80/TCP,443/TCP,1936/TCP      9d
```


## 3. oauth-proxy コンテナをデプロイするためのnamespaceを準備する
oauth-proxyのデプロイ先のnamespaceは任意ではあるが、OpenShiftの制約で"openshift"から始まる名前を付けることはできない。

```shell:shell
$ oc new-project oauth-proxy
```

このnamespaceにはoauth-proxyのTLS構成のためのtlsシークレットをあらかじめ配置しておく必要がある。
ibmcloud ks nlb-dns コマンドを使用してDNSサブドメインを作成した場合、defaultネームスペースにワイルドカードSSL証明書のtlsシークレットが配置されているため、そのシークレットを取り出して利用することができる。

```shell:shell
$ oc get secret -n default
NAME                                                  TYPE                                  DATA   AGE
....
cluster1-xxxxxxxxxxxxxxxxxxxxxxxxx-0001               kubernetes.io/tls                     2      9d
....

$ oc get secret <secret-name> -n default -o yaml | sed 's/default/oauth-proxy/g' | oc create -n oauth-proxy -f -
oauth-proxy/cluster1-xxxxxxxxxxxxxxxxxxxxxxxxx-0001 configured.
```


## 4. oauth-proxy コンテナをデプロイする
oauth-proxy のサンプルに従って、oauth-proxyのコンテナをデプロイする。

- oauth-proxy-sa: OAuth認証のためのリダイレクト先として許可するURLを保持するための特殊な Service Account。
- oauth-proxy: oauth-proxy そのもの。

```shell:shell
$ oc project oauth-proxy
Now using project "oauth-proxy" on server "https:...."
$ oc create -f oauth-proxy-sa.yaml
serviceaccount/oauth-proxy-sa created
$ oc create -f oauth-proxy.yaml
deployment.apps/oauth-proxy created
```

oauth-proxy-saは、OAuth認証のためのリダイレクト先として許可するURLを保持するための特殊な Service Accountである。
初期状態としてサンプルのアドレスを記載するが保護対象のアプリケーションが増えた場合にはこのエントリーを追加していく。

```yaml:oauth-proxy-sa.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: oauth-proxy-sa
  annotations:
    serviceaccounts.openshift.io/oauth-redirecturi.root: "https://cluster1-xxxx-0001.jp-tok.containers.appdomain.cloud"
```

oauth-proxy のデプロイ構成では、コンテナの--upstream オプションでrouterのClusterIPサービス (router-internal-[保護Router名]) を指定することで、oauth-proxyの認証を通した後のリクエストを保護Routerにリダイレクトするように構成する。 

```yaml:oauth-proxy.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: oauth-proxy
spec:
  replicas: 1
  selector:
    matchLabels:
      app: oauth-proxy
  template:
    metadata:
      labels:
        app: oauth-proxy
    spec:
      serviceAccountName: oauth-proxy-sa
      containers:
      - name: oauth-proxy
        image: openshift/oauth-proxy:latest
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 8443
          name: https
        - containerPort: 8080
          name: http
        args:
        - --https-address=:8443
        - --http-address=:8080
        - --provider=openshift
        - --openshift-service-account=oauth-proxy-sa
        - --upstream=http://router-internal-protected.openshift-ingress
        - --tls-cert=/etc/tls/private/tls.crt
        - --tls-key=/etc/tls/private/tls.key
        - --cookie-secret=SECRET
        volumeMounts:
        - mountPath: /etc/tls/private
          name: proxy-tls
      volumes:
      - name: proxy-tls
        secret:
          secretName: cluster1-xxxxxxxxxxxxxxxxxxxxxxxxx-0001
```


## 5. oauth-proxyをLoad Balancerに公開する
LoadBalancerタイプのServiceを作成して、oauth-proxyをパブリックIPアドレスをもつLoadBalancerに公開する。

```shell:shell
$ oc project oauth-proxy
$ oc create -f oauth-proxy-svc-publb.yaml
service/oauth-proxy-publb created
```

```yaml:oauth-proxy-svc-publb.yaml
apiVersion: v1
kind: Service
metadata:
  labels:
    app: oauth-proxy
  name: oauth-proxy-publb
spec:
  ports:
  - name: https
    port: 443
    protocol: TCP
    targetPort: 8443
  - name: http
    port: 80
    protocol: TCP
    targetPort: 8080
  selector:
    app: oauth-proxy
  sessionAffinity: None
  type: LoadBalancer
```

Step1.で DNSサブドメインを生成していた場合は、oc get services コマンドでPublic IPアドレス (External IP)を確認して、nlb-dnsのadd/removeコマンドを使用したDNSサブドメインのIPアドレスの変更を実施する。

```shell:shell
$ oc get services
NAME                 TYPE           CLUSTER-IP       EXTERNAL-IP     PORT(S)                      AGE
oauth-proxy-publb    LoadBalancer   172.xx.xxx.xxx   161.xxx.xxx.xxx 443:31661/TCP,80:31199/TCP   18s
```

## 6. Routeを作成して保護対象アプリケーションを保護Routerに公開する
任意のコンテナアプリケーションを作成して、Routeを使用して保護Routerに公開する。
Routeを作成する際には保護Routerによって公開されるようにlabel条件などを構成する。またRouteを作成する際にはtls属性などのhttps接続のための設定は構成せずhttp接続によるRouteとして構成する。

保護対象アプリケーションへの接続はすべてhttpsで行うが、oauth-proxyがhttpsでリクエストを受け付けた後で、保護Routerにhttpで転送するためである (oauth-proxyの設定の upstream の指定)。

```shell:shell
$ oc new project sampleapps
$ oc label namespace sampleapps router=router-protected
$ oc apply -f app1.yaml
deployment.apps/app1 created
service/app1 created
route.route.openshift.io/app1 created
$ oc get route app1
NAME      HOST/PORT                                                       PATH   SERVICES   PORT   TERMINATION   WILDCARD
testapp   app1.cluster1-xxxx-0001.jp-tok.containers.appdomain.cloud       /      app1       8080                 None
```

## 7. oauth-proxy-sa の構成を更新して、公開した保護対象アプリケーションのURLをOAuthのリダイレクト先として登録する
oauth-proxy-sa.yaml を編集して新しいアプリケーションのためのURLをOAuth認証のためのリダイレクト先として登録する。


```yaml:oauth-proxy-sa.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: oauth-proxy-sa
  annotations:
    serviceaccounts.openshift.io/oauth-redirecturi.root: "https://cluster1-xxxx-0001.jp-tok.containers.appdomain.cloud"
    # app1 のために追記。 redirecturi の後ろの suffix ".app1" は任意の名前でよい
    serviceaccounts.openshift.io/oauth-redirecturi.app1: "https://app1.cluster1-xxxx-0001.jp-tok.containers.appdomain.cloud"
```

```shell:shell
$ oc project oauth-proxy
$ oc apply -f oauth-proxy-sa.yaml
serviceaccount/oauth-proxy-sa configured
# oauth-proxy のpodを再起動させるためにenvを更新する (<yyyymmddhhmmss の部分はユニークになれば何でもよい)
$ oc set env deployment oauth-proxy OAUTH_PROXY_SA_UPDATE=<yyyymmddhhmmss>
deployment.apps/oauth-proxy updated
```

## 8. 動作を確認する

ブラウザーを開いて、アプリケーション・コンテナーのURLを開く
https://app1.cluster1-xxxx-0001.jp-tok.containers.appdomain.cloud

通常だとそのままコンテナへアクセスするところ、見慣れた OpenShift でのログイン画面が表示されるため、「Log in with OpenShift」をボタンを押す
![OpenShiftLogin.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252650/45af0727-f580-bede-19fa-61dbaf810d5a.png)

OpenShift on IBM Cloud を使用してる場合、そのままIBM Cloudのログインへとリダイレクトされるためログインを行う。
![LogInWithIBM.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252650/5ddafae4-ebab-6a38-8826-53add11990f4.png)

ログインが完了するとOAuth認証情報の取得の許可画面が表示されるので "Allow selected permissions" ボタンを押す
![OAuthAccessAuthorization.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252650/35a8a7b3-4595-b376-4727-b6efc42c75a0.png)

認証が完了してアプリケーションコンテナへアクセスできる。


###  権限がない場合
なお権限がないユーザーでアクセスすると以下のような画面が表示される。
![CouldNotFindUser.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252650/9d275612-fd87-7d00-cfe6-4d2c3ded6b90.png)

対象のユーザーがアカウントに登録されていて、OpenShiftクラスターへのアクセス権限 (Kubernetes Serviceへの参照権限)が設定されているにも関わらず、この画面が表示されてしまう場合は、IAM認証情報がOpenShiftクラスターに同期されていない可能性がある。

その場合は以下の手順で認証情報をOpenShiftクラスターに反映させることができる。

1. (ログインできない)対象のユーザーで、https://cloud.ibm.com にログインして、ダッシュボードを開く。
1. OpenShiftクラスターの画面を開く
1. 「OpenShift Web コンソール」ボタンを押してコンソールを開く (この時にユーザー情報の同期がバックグラウンドで行われる模様)
![OpenShiftWebConsole.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252650/045ae671-99f8-2c28-173e-d70fd9394b88.png)


もしくは

1. 対象のユーザーが ibmcloud コマンドラインクライアントを使用してを oc cluster config コマンドを実行する。

```shell:shell
$ ibmcloud login ....
$ ibmcloud target -g <resource group> -r <region>
$ ibmcloud oc cluster config --cluster <cluster name>
```



# 参考リンク
[OpenShift oauth-proxy (GitHub)](https://github.com/openshift/oauth-proxy)