【Kubernetes】サイドカー方式でOAtuh2-proxyの認証処理を追加する

f:id:tm200:20210523114141p:plain f:id:tm200:20210423222508p:plain  

kubernetes上でのOAuth2 proxyコンテナの利用方法をメモ。
nginxの使い方について詳しく知りたい方は、この本がお勧めです。
手元にあれば、実装時にいちいち検索しなくて済むかもしれません。

OAuth2 proxy

ドキュメントはこちら。

oauth2-proxy.github.io

構成

リバースプロキシにnginxを使用する方法です。
nginxのauth_requestでOAuth2 proxyに認証を依頼し、認証成功時にアプリケーションにプロキシされます。
認証方法はOIDC。

openid.net

config

各configはkubernetesのconfigMapに登録します。

nginx.conf
user nginx;
worker_processes 1;
pid /var/run/nginx.pid
daemon off; # dockerで起動する場合、フォアグラウンドで起動する必要がある

events {
    worker_connections 2048;
}

http {
    include               /etc/nginx/mime.types;
    default_type      application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                                  '$status $body_bytes_sent "$http_referer" '
                                  '"$http_user_agent" "$http_x_forwarded_for"';

    access_log   /dev/stdout main;
    error_log       /dev/stderr error;

    sendfile                      on;
    # tcp_nopush            on;
    # gzip                         on;
    keepalive_timeout    65;

    
    # oauth2 proxyの為に、HTTPヘッダー用のバッファ領域を増やす
    proxy_buffers           8 32k;
    proxy_buffer_size        32k;

    # "Request Header Or Cookie Too Large"の対応
    large_client_header_buffers    8 32k;
    client_header_buffer_size           32k;

    server_token off;

    upstream backend {
        server 127.0.0.1:8080;             # アプリケーションコンテナ
    }

    upstream oauth-proxy {
        server 127.0.0.1:4180;             # OAuth2 proxyコンテナ
    }

    server {
        listen 80;
        return 301 https://$host$request_uri;
    }

    server {
        listen 403 ssl;
        ssl_certificate                         /etc/nginx/ssl/tls.crt;                     # 中間証明書がある場合は連結する
        ssl_certificate_key                 /etc/nginx/ssl/tls.key;
        ssl_protocols TLSv1.2;
        ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:EC
DHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';
        ssl_session_cache builtin:1000 shared:SSL:10m;

        client_max_body_size       32m;
        client_body_buffer_size    64k;

        location /oauth2/ {
            proxy_pass                http://oauth-proxy;
            proxy_set_header     Host                                          $host;
            proxy_set_header     X-Real_IP                                  $remote_addr;
            proxy_set_header     X-Sheme                                  $sheme;
            proxy_set_header     X-Auth-Request-Redirect      $request_uri;
            # 他ドメインに転送する場合
            # proxy_set_header     X-Auth-Request-Redirect      $sheme://$host$request_uri;
        }

        # 認証用エンドポイント、「=」なことに注意
        location = /oauth2/auth {
            proxy_pass                http://oauth-proxy;
            proxy_set_header     Host                                          $host;
            proxy_set_header     X-Real_IP                                  $remote_addr;
            proxy_set_header     X-Sheme                                  $sheme;
            # nginxのauth_requestにbodyは不要
            proxy_set_header     Content-Length                       "";
            proxy_pass_request_body                                         off;
        }

        location / {
            satisfy     any;
            auth_request           /oauth2/auth;
            error_page 401       /oauth2/sign_in;

            proxy_set_header     Host                                          $host;
            proxy_set_header     X-Real_IP                                  $remote_addr;
            proxy_set_header     X-Forwarded-For                    $remote_addr;
            proxy_set_header     X-Forwarded-Proto                 https;

            auth_request_set     X-User      $user;
            auth_request_set     X-Email     $email;
            auth_request_set     X-ID          $empid;
            auth_request_set     X-Roles     $roles;

            auth_request_set     $token                      $upstream_http_x_auth_request_access_token;
            proxy_set_header     X-Access-Token     $token;

            auth_request_set     $auth_cookie_name_upstream_1 $upstream_cookie_auth_cookie_name_1;

            proxy_pass http://backend/;
        }

        location /proxy/ {
            proxy_set_header     Host                                          $host;
            proxy_set_header     X-Real_IP                                  $remote_addr;
            proxy_set_header     X-Forwarded-For                    $remote_addr;
            proxy_set_header     X-Forwarded-Proto                 https;
            proxy_pass                http://oauth-proxy;
        }
    }
}
oauth2_proxy.cfg
http_address = "0.0.0.0:4180"

upstream = ["http://127.0.0.1:8080/"]

# メールアドレスのドメインを指定、アスタリスクで全て許容
email_domain = ["*"]

# 認可するロール
permission_policies = []

oidc_issuer_url = "https://xxx.xxx.xxx/dex"

cookie_secure = true

# 認証方法を指定
provider = "oidc"

# X-Forwarded-Access-Token
pass_access_token = true

# Authorization Bearer header
pass_authorization_header = true

reverse_proxy = true
set_xauthrequest = true
set_authorization_header = true

skip_provider_button = true

# ヘルスチェック用エンドポイントもこちらに記載する
skip_auth_regex = [
    "^(/|/forbidden|/health|/favicon.ico)$",
    "^(/|/forbidden|/health|/favicon.ico)$",
    "^(js|image|css|stylesheets)/.*$",
    ".(png|css|ico|js)$"
]

configMapとSecret

上記configはconfigMapに、tlsの鍵データとoauth2proxy用各パラメータはSecretに登録します。
cookie secretをpythonで生成する際に、byte数が(16, 24, 32)のいずれかである必要があります。

$ NAMESPACE=xxxxx
$ alias k=kubectl

# tls作成
$ k -n=${NAMESPACE} create secret tls pki-tls --key=secret/server.key --cert=secret/server.crt
secret/pki-tls created

# OAuth2 proxy 各パラメータをbase64エンコードして、マニフェストに記載する
$ echo -n "<client-id>" | base64

$ echo -n "<client-secret>" | base64

# cookie secretの生成、byte型になっていたり改行が含まれているとエラーとなるので注意
$ python -c 'import os,base64; print(base64.urlsafe_b64encode(os.random(16)).decode(), end="")' | base64

$ cat secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: oauth2proxy
  namespace: xxx-xxx-xxx
type: Opaque
data:
  client-id: <base64 client-id>
  client-secret: <base64 client-secret>
  cookie-secret: <base64 cookie-secret>

$ k apply -f secret.yaml
secret/oauth2proxy created

# configMap
$ k create cm nginx-config --from-file=config/nginx.conf
$ k create cm proxy-config --from-file=config/oauth2_proxy.cfg

# 確認コマンド
$ k -n=${NAMESPACE} describe secret kpi-tls

$ k -n=${NAMESPACE} describe secret oauth2proxy

# 要jqコマンドのインストール
$ k -n=${NAMESPACE} get secret kpi-tls -o json | jq -r '.data."tls.crt"'

$ k -n=${NAMESPACE} get cm nginx-config -o json | jq -r '.data."nginx.conf"'

$ k -n=${NAMESPACE} get cm proxy-config -o json | jq -r '.data."oauth2_proxy.cfg"'

# 24byteであることの確認
$ k -n=${NAMESPACE} get secret oauth2proxy -o json | jq -r '.data."cookie-secret"' | base64 -d | wc -c
24

マニフェストファイル

特に変わった使い方はしていません。
deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: test-app
  namespace: xxx-xxx-xxx
spec:
  replicas: 2
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 1
  minReadySeconds: 10
  template:
    spec:
      volumes:
        - name: secret-volume
          secret:
            secretName: pki-tls
        - name: nginx-volume
          configMap:
            name: nginx-config
        - name: oidc-volume
          configMap:
            name: proxy-config
      containers:
      - image: nginx
        name: nginx
        ports:
          - containerPort: 443
        resources:
          limits:
            memory: 64Mi
          requests:
            cpu: 100m
            memory: 64Mi
        volumeMounts:
          - mountPath: /etc/nginx/ssl
             name: secret-volume
             readOnly: true
          - mountPath: /etc/nginx/nginx.conf
             subPath: nginx.conf
             name: nginx-volume
             readOnly: true
      - image: oauth2_proxy  # PJで管理しているイメージです、公開されているものと同じはず
         args: ["-config=/etc/oauth2/oauth2_proxy.cfg"]
         ports:
           - containerPort: 4180
        resources:
          limits:
            memory: 512Mi
          requests:
            cpu: 100m
            memory: 512Mi
        volumeMounts:
          - mountPath: /etc/oauth2/oauth2_proxy.cfg
             subPath: oauth2_proxy.cfg
             name: oidc-volume
             readOnly: true
        env:
          - name: OAUTH2_PROXY_CLIENT_ID
             valueFrom:
               secretKeyRef:
                 name: oauth2proxy
                 key: clinet-id
          - name: OAUTH2_PROXY_CLIENT_SECRET
             valueFrom:
               secretKeyRef:
                 name: oauth2proxy
                 key: cookie-secret
          - name: OAUTH2_PROXY_COOKIE_SECRET
             valueFrom:
               secretKeyRef:
                 name: oauth2proxy
                 key: clinet-id
          - name: OAUTH2_PROXY_HTTP_ADDRESS  # ここで指定しないと、127.0.0.1で起動してしまう?
             value: "0.0.0.0:4180"
          - name: OAUTH2_PROXY_REDIRECT_URL
             value: <登録したリダイレクトURLを指定>
      - image: python-app  # 詳細は割愛、実装したのはflaskアプリです
         name: python-app
         ports:
           - containerPort: 8080
         resources:
           limits:
             cpu: 1000m
             memory: 2G
         lifecyle:
           preStop:
             exec:
               command: ["/bin/sh", "-c", "sleep 20"]
         readinessProbe:
           httpGet:
             path: /health
             port: 8080
           initialDelaySeconds: 30
           periodSeconds: 5
           failureThreshold: 10

         livenessProbe:
           httpGet:
             path: /health
             port: 8080
           initialDelaySeconds: 60
           periodSeconds: 10
           timeoutSeconds: 10

ingress.yaml

apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: test-ingress
  namespace: test-namespace
  annotations:
    ingress.zlab.co.jp/backend-config: '{"xxx-xxx-xxx": {"443": {"tls": true, "sni": "<ドメイン名を記載>"}}}'
spec:
  tls:
    - secretName: pki-tls
  rules:
  - host: <ドメイン名を記載>
     http:
       paths:
       - backend:
           serviceName: testsvc
           servicePort: 443

 service.yaml

apiVersion: v1
kind: Service
metadata:
  name: testsvc
  labels:
    app: test-app
  namespace: xxx-xxx-xxx
spec:
  selector:
    app: test-app
  ports:
  - protocol: TCP
     port: 443
     targetPort: 443