Cloud Armor + Cloud IAPでKubernetes Podへセキュアにアクセスする


機械学習モデルの開発やデータ解析において、Jupyterはデファクトツールと言えるでしょう。また、分析結果をRedash などのBIツールを通してチームメンバーにシェアすることは日常茶飯事だと思います。

そういったWebツールを構築する場合には、少なからず実業務のデータを取り扱うという性質上、常にセキュリティの問題がつきまといます。

  • IP制限
  • 個人単位での認証管理
  • SSL通信

etc…

こういったセキュリティ要件を、ツールが導入されるたびに設定するのはとてもめんどうですし、また設定漏れにも注意が必要です。

今回、GCPの機能を使って、Kubernetes(k8s)のPodを立てたら、上記セキュリティ対策が(なるべく)自動で設定されるような仕組みを作ってみました。

全体構成 :

それぞれ、下記の機能で実現します。

  • IP制限… Cloud Armor
  • 個人単位の認証 … Cloud IAP
  • SSL通信 … Cloud Load Balancer

事前構築

以下はすでに構築 or インストール済みとします。

  • GKEクラスタ
  • kubectlコマンド
  • jqコマンド

Pod ~ Service ~ Ingressによる外部公開まで

今回はnginxをウェブアプリとして、80番ポートに対して外部からアクセスできるよう、ServiceとIngressで外部と通信できるようにします。

まずはPodを作ります。

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: sample-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: sample-app
  template:
    metadata:
      labels:
        app: sample-app
    spec:
      containers:
        - name: nginx-container
          image: nginx:1.15.7

マニフェストを作成したら、クラスタにデプロイします。

$ kubectl apply -f deployment.yaml
deployment.apps "sample-app" created

​​​​$ kubectl get pods                                                                                                                                                     
NAME                          READY     STATUS    RESTARTS   AGE
sample-app-647897db5d-r2467   1/1       Running   0          19s

k8s Ingressの追加

続いて、Ingressを構築していきます。

PodへのアクセスはService単体でできますが、そのルーティングはL4になります。
一方、KubernetesのIngressはL7のロードバランサを実現するもので、後述するCloud ArmorやCloud IAPと組み合わせるために必要です。
L7のため、パスに応じたルーティングを設定することもできます。

NodePort Serviceのデプロイ

Ingressは直接Podにつながるわけではなく、Serviceへの通信をルーティングするものであるため、先にIngress用のServiceを type: Nodeport で作ります。

# serivce.yaml
apiVersion: v1
kind: Service
metadata:
  name: sample-app-service

spec:
  type: NodePort
  ports:
  - name: "http-port"
    protocol: "TCP"
    port: 8080
    targetPort: 80
  selector:
    app: sample-app

Nginxの80ポートにNodePort:8080をつなげています。

$ kubectl apply -f service.yaml                                                       
service "sample-app-service" created

デプロイできたら、接続を確認します。kubectl port-forward でlocalの8000番と、Serviceの8080番をつなげます。

$ kubectl port-forward  service/sample-app-service 8000:8080
Forwarding from 127.0.0.1:8000 -> 80
Forwarding from [::1]:8000 -> 80
Handling connection for 8000
Handling connection for 8000

http://localhost:8000 にアクセスすると、正常に表示されることが確認できます。

Ingressのデプロイ

続いてIngressでHTTP Load Balancerを構築します。

今回はあらかじめStatic IPを作成しておいてから、それをIngressのLBに割り当てます。

# LBに割り当てる場合、Static IPはGlobal IPである必要があるみたいです。
$ gcloud compute addresses create  --global sample-app-ip
  Created
$ IP_ADDR=$(gcloud --format=json compute addresses describe sample-app-ip  --global | jq -r ".address")

次に、SSL通信用の(オレオレ)シークレットを作成します。

今回ドメインを取るのは面倒なので、xip.io を利用します。
xip.ioは任意のIPアドレスに対してドメイン名によるアクセスを可能にしてくれるので、「ドメイン取るほどではなけいどSSLにしたい」、という用途で重宝します。

$ openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout ./tls.key -out ./tls.crt -subj "/CN=${IP_ADDRESS}.xip.io"

$ kubectl create secret tls --save-config sample-app-tls --key tls.key --cert tls.crt

作成したシークレットとIPアドレスを使って、Ingressを作ります。

#ingress.yaml
echo """apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: sample-app-ingress
  annotations:
    kubernetes.io/ingress.allow-http: \"false\"
    kubernetes.io/ingress.global-static-ip-name: "sample-app-ip"
spec:
  rules:
    - host: ${IP_ADDR}.xip.io
      http:
        paths:
          - path: /*
            backend:
              serviceName: sample-app-service
              servicePort: 8080
  tls:
    - hosts:
      - ${IP_ADDR}.xip.io
      secretName: sample-app-tls
""" > ingress.yaml

Ingressをデプロイします。

$ kubectl apply -f ingress.yaml                                                                                                                                          
ingress.extensions "sample-app-ingress" created

作成したサービスに対してもport-forwardして接続に問題がないことを確認します。

特に認証が必要なければここまででもよいのですが、サービスが全世界に公開されてしまっていますので、さらにアクセス制限をかけていきます。

Cloud IAPの設定

Cloud IAPについてはGCPのドキュメントが詳しいです。
IAPを使えばGCPのLBにGoogleアカウントによる認証をかけることができます。LBの段階で認証をかけるので、その先に存在するサービスでは難しい認証機構を用意しなくとも済む、というのがメリットです。

またHTTPヘッダーにアカウント名がついてくるので、それを使えばアプリケーション側で独自に認可機構を加えることもできます。

このCloud IAP、エンタープライズでは非常に強力な機能で、例えば企業でGSuiteを導入していれば、従業員のアカウントをそのまま認証用アカウントに設定できます。
また、Google Groupも登録できるので、ロールごとにグループを作成して、そのロールに対してアクセス許可をする、ということもできます。ここらへんはGCPの IAMのメリットでもありますね。

今回はIngressで立てたLBにCloud IAPを設定します。
まずはCloud IAPをGCP Consoleからセットアップします。

初回の場合は、OAuthの同意画面設定が必要です。
「アプリケーション名」: sample-app
「ドメイン名」: xip.io

上記手順で作成した場合、IAPの設定画面にはすでにIngressで作成したLBが表示されているかと思います。

また、Cloud IAPでは、該当のServiceへのアクセス経路がIAP経由以外に存在する場合、「警告」を出してくれます。今回VPCネットワーク内の通信は許可しているので警告が出ていますが、そのまま続行します。

Cloud IAPを有効にすると、権限設定画面が表示されので、どのgmailアカウントに対してCloud IAP経由のアクセスを許可するか、アカウント単位で設定します。

以上で、Cloud IAPの設定は完了です。

Cloud Armor

続いて、Cloud ArmorによるIPアドレス制限を追加します。

GCPのLBは単体ではIP制限の機能はありませんが、Cloud Armorを設定すれば通信元IPアドレスのブラックリスト/ホワイトリスト管理ができます。

GCP Consoleから手動で設定してもいいのですが、GKEでは(ベータですが)IngressマニフェストのメタデータでCloud Armorの紐づけを記述できるので、これを利用するとよいでしょう。

まずはCloud Armorの設定を作成します。ここでは、作業中のマシンのIPアドレス以外のトラフィックを弾くようにします。

$ gcloud compute security-policies create sample-app-policy --description "sample app armor policy"
$ gcloud compute security-policies rules create 10000 \                 
    --security-policy sample-app-policy \
    --description "deny all" \
    --src-ip-ranges="*" \
    --action "deny-404"

# 作業PCのIPアドレス取得
$ MY_IP_ADDR=$(curl ifconfig.me)

$ gcloud compute security-policies rules create 1000 \                
    --security-policy sample-app-policy \
    --description "allow traffic from local machine" \
    --src-ip-ranges "$MY_IP_ADDR/32" \
    --action "allow"

Backend Configマニフェストに、作成したCloud Armorのポリシー名を spec.securityPolicy.name に指定してデプロイします。

# backendconfig.yaml
apiVersion: cloud.google.com/v1beta1
kind: BackendConfig
metadata:
  name: sample-app-backendconfig
spec:
  securityPolicy:
    name: "sample-app-policy"

(ちなみに、BackendConfigでは様々な設定を追加でき、先のCloud IAPもBackend Config経由で設定できるようです。)

つづいて、作成したBackend ConfigをServiceのポートに紐づけます。先程作成した service.yaml に、metadataを1つ追加します。

# service.yaml
apiVersion: v1
kind: Service
metadata:
  name: sample-app-service
  # 追加
  annotations:
    beta.cloud.google.com/backend-config: '{
      "ports": {
        "8080": "sample-app-backendconfig"
      }
    }'
spec:
  type: NodePort
  # <省略>

追加するのはbeta.cloud.google.com/backend-config annotationで、Serviceのポートと、作成したBackend Config名を1:1で記述します。
この設定により、8080ポートに対して、BackendConfig( sample-app-backendconfig )を紐づけました。

両者をクラスタにデプロイします。

$ kubectl apply -f backendconfig.yaml
$ kubectl apply -f service.yaml

アクセス確認

以上で構築は完了です。

ブラウザで 作成したIngress(LB)に対して、httpsでアクセスしてみます。設定がうまく行っていれば、GoogleのOAuth認証を挟んだあと、Podへのアクセスができるかと思います。

また、他のIPアドレスからアクセスすると「404」になることも確認できますし、Cloud IAPで許可したアカウント以外でアクセスしても弾かれることが確認できればOKです。

まとめ

今回、GCP + GKE (k8s)の機能を使って、Podへのアクセス認証とIPアドレス制限を実施しました。新しいWebアプリを追加したい場合には、

  1. WebアプリのPod のデプロイ
  2. Serviceの追加
  3. Ingressに2のServiceへのルーティングを追加

だけで、上記アクセス制限が再利用できるかと思います。

本格的な認証認可には及びませんが、この仕組であれば新しくWebアプリのPodを追加しても、最低限のアクセス制限をさくっと適用できるかと思います。

補足

ブラウザで「Error: redirect_uri_mismatch 」が表示される

OAuthでアクセスする場合に表示されることがあります。OAuthのリダイレクトURIがGoogle OAuthに登録されていないためで、表示される指示に従い、リダイレクト先URIを登録します。
リダレクトURIも登録先URLも画面に表示されますが、今回の例ではリダレクトURIは「https://「IPアドレス」.xip.io/_gcp_gatekeeper/authenticate」になります。

追加してしばらく経ったあとにアクセスすれば直ると思います。