oauth2-proxy 설정하기

kiyoung·2024년 2월 2일

istio

목록 보기
4/4

metric 애플리케이션의 데이터들은 관리자들이 확인하거나 수정할 만한 데이터이지만 퍼블릭에 열려 있습니다. istio에서 authorization provider를 활용하면 특정 호스트나 path로 오는 요청에 대해 인증과 권한을 요구하도록 설정할 수 있습니다.

istio에서 특정 호스트(admin.<<domain>>)으로 요청이 들어오면 oauth2-proxy를 통해 Oauth 2.0으로 인증을 하도록 구성하는 과정을 진행합니다.

metric 애플리케이션용 Ingress 만들기

metric 애플리케이션들을 위해 ingress 자원을 생성하도록 하겠습니다.

우선 ingress에서 사용할 고정 IP를 생성합니다.

gcloud compute addresses create metric-ingress-ip --global --ip-version IPV4

그리고 Cloud DNS에서 metric-ingress-ip 고정 IP 주소를 선택하여 admin 레코드를 생성합니다.

그리고 ingress 자원을 생성할 수 있도록 metric 차트의 설정 값과 templates 파일들을 추가합니다.

metric/values.yaml

...
ingress:
  domain: "admin.<<domain>>" # <<domain>>에는 발급받은 도메인 주소 입력
  staticIpName: "metric-ingress-ip"
  istio_gateway: "istio-ingressgateway"

BackendConfig 자원을 생성합니다.

metric/templates/backend.yaml

apiVersion: cloud.google.com/v1
kind: BackendConfig
metadata:
  name: {{ include "metric.name" .}}-backend-config
  namespace: {{ .Release.Namespace }}
spec:
  healthCheck:
    type: HTTP
    requestPath: /healthz/ready
    port: 15021

FrontendConfig 자원을 생성합니다.
metric/templates/frontend.yaml

apiVersion: networking.gke.io/v1beta1
kind: FrontendConfig
metadata:
  name: {{ include "metric.name" .}}-frontend-config
  namespace: {{ .Release.Namespace }}
spec:
  redirectToHttps:
    enabled: true

인증서를 생성할 수 있도록 ManagedCertificate 자원을 생성합니다.

metric/templates/managed-certificate.yaml

apiVersion: networking.gke.io/v1
kind: ManagedCertificate
metadata:
  name: {{ include "metric.name" .}}-managed-cert
  namespace: {{ .Release.Namespace }}
spec:
  domains:
    - {{ .Values.ingress.domain }}

ingress 자원을 생성합니다.

metric/templates/ingress.yaml

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: {{ include "metric.name" .}}
  namespace: {{ .Release.Namespace }}
  annotations:
    kubernetes.io/ingress.global-static-ip-name: {{ .Values.ingress.staticIpName }}
    networking.gke.io/managed-certificates: {{ include "metric.name" .}}-managed-cert
    networking.gke.io/v1beta1.FrontendConfig: {{ include "metric.name" .}}-frontend-config
    kubernetes.io/ingress.class: "gce"
spec:
  rules:
    - host: {{ .Values.ingress.domain }}
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: {{ .Values.ingress.istio_gateway }}
                port:
                  number: 80

그리고 admin.<<domain>> 경로로 접속할 수 있도록 Gateway와 VirtualService의 hosts 부분을 수정합니다.

metric/templates/metric-gateway.yaml

apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: metric-gateway
  namespace: {{ .Release.Namespace }}
spec:
  selector:
    istio: ingressgateway # use istio default controller
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
    - {{ .Values.ingress.domain }} # 수정
    tls:
      httpsRedirect: false

metric/templates/metric-vs.yaml

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: metric-vs
  namespace: {{ .Release.Namespace }}
spec:
  hosts:
  - {{ .Values.ingress.domain }} # 수정
  gateways:
  - metric-gateway
  http:
  ...

Sample 애플리케이션 Gateway 수정하기

default namespace에 있는 booking-gateway는 www 레코드의 주소로 접속할 수 있도록 hosts 값을 수정합니다.

kubectl patch gateway bookinfo-gateway --type='json' \
  -p='[{"op": "replace", "path": "/spec/servers/0/host/0", "value":"www.<<domain>>"}]'

Google Oauth 2.0 client 생성하기

google cloud console에서 Credentials로 검색하여 사용자 인증 정보 페이지로 접속합니다.

Oauth 클라이언트 ID를 생성합니다.

먼저 Oauth 동의 화면을 구성합니다.

조직이 없기 때문에 외부 타입으로 생성합니다.

앱 이름과 사용자 지원 이메일을 설정합니다.

admin 레코드와 동일한 이름으로 애플리케이션 홈페이지를 등록하고

도메인 이름으로 승인된 도메인을 추가합니다.

개발자 연락처 정보에 이메일을 추가합니다.

범위 추가 또는 삭제 버튼을 클릭한 다음 .../auth/userinfo.email을 추가한 다음 저장 후 계속 버튼을 클릭합니다.

테스트 사용자에는 원하는 계정을 추가하여 테스트 시 인증이 허용될 계정을 추가합니다.

검토 후 다시 사용자 인증 정보 페이지에서 Oauth 클라이언트 ID를 생성합니다.

애플리케이션 유형은 웹 애플리케이션, 이름은 istio-in-gcp로 입력합니다.

URI에는 도메인에 등록한 admin 레코드 주소를 입력하고,

리디렉션 URI에는 /oauth2/callback이 포함된 URI를 입력한 다음 만들기 버튼을 클릭하여 생성합니다.

클라이언트 정보는 메모장에 기록해 둡니다.


Oauth2-proxy subchart 생성하기

metric/charts 디렉토리에 oauth2-proxy 서브차트를 추가합니다.

cd ~/istio-in-gcp-gitops/metric/charts
helm repo add oauth2-proxy https://oauth2-proxy.github.io/manifests
helm pull oauth2-proxy/oauth2-proxy --untar

그리고 부모 차트인 metric 차트의 values.yaml에서 oauth2-proxy의 설정들을 추가합니다.

metric/values.yaml

...
oauth2-proxy:
  extraArgs:
    provider: google
    standard-logging: true
    auth-logging: true
    request-logging: true
    silence-ping-logging: true
    upstream: static://200
    # 로그인 버튼 화면을 생략할 수 있는 설정
    # skip-provider-button: true

Sealed secret 적용하기

google oauth 2.0 client의 보안 비밀번호는 노출된다면 누구나 클라이언트를 사용할 수 있게 되므로 노출되지 않아야 합니다.

특히 helm 차트들의 repository가 public으로 지정되어 있다면 client의 보안 비밀번호를 차트에 박아 놓는 다면 외부에 노출될 우려가 높습니다.

데이터가 들어 있는 kubernetes의 secret 오브젝트로 레포지토리에 저장해 두는 것도 좋은 방법은 아닌데, secret 오브젝트의 데이터는 base64로 인코딩되어 있을 뿐이므로 단순히 디코딩만 하여 데이터를 볼 수 있기 때문입니다.

실제 애플리케이션을 배포할 경우에는 helm 차트를 배포하는 시점에 client-secret 변수를 따로 설정하거나, secret을 암호화하는 것이 필요합니다.

secret을 보관하기 위해 다양한 서비스들이 있습니다. AWS의 경우에는 secret manager에 데이터를 저장하고 EKS의 플러그인인 Secrets Store CSI Driver를 통해 연결할 수도 있고,
HashCorp사의 Vault와 같은 서비스도 있습니다.

이번 과정에서는 kubeseal(sealed-secret)을 이용하여 비밀 정보를 github repository에 저장할 것입니다.
🔗 https://github.com/bitnami-labs/sealed-secrets

sealed-secret은 쿠버네티스 클러스터 내부에 배포되고, SealedSecret이라는 CRD를 통해 암호화 키를 이용하여 secret과 SealedSecret 오브젝트를 변환할 수 있습니다.

사용할 secret을 미리 정의한 다음 이를 토대로 비대칭 암호화하여 SealedSecret을 생성하고 이를 레포지토리에 저장합니다.

SealedSecret이 클러스터에 생성되면 sealed-secrets 컨트롤러가 이를 포착하여 kubernetes secret으로 변환합니다.

SealedSecret을 이용하기 위해서는 SealedSecret을 생성할 수 있는 kubeseal client를 로컬에 설치하고 sealed-secrets 컨트롤러를 클러스터에 배포합니다.

kubeseal(client) 설치를 먼저 수행합니다.

# Fetch the latest sealed-secrets version using GitHub API
KUBESEAL_VERSION=$(curl -s https://api.github.com/repos/bitnami-labs/sealed-secrets/tags | jq -r '.[0].name' | cut -c 2-)

# Check if the version was fetched successfully
if [ -z "$KUBESEAL_VERSION" ]; then
    echo "Failed to fetch the latest KUBESEAL_VERSION"
    exit 1보
fi

wget "https://github.com/bitnami-labs/sealed-secrets/releases/download/v${KUBESEAL_VERSION:?}/kubeseal-${KUBESEAL_VERSION:?}-linux-amd64.tar.gz"
tar -xvzf kubeseal-${KUBESEAL_VERSION:?}-linux-amd64.tar.gz kubeseal
sudo install -m 755 kubeseal /usr/local/bin/kubeseal

뒤이어 SealedSecret이 배포될 클러스터에 sealed-secrets 컨트롤러 helm 차트를 배포합니다.

kubeseal client에서 쉽게 컨트롤러를 인식하도록 하기 위해서는 kube-system 네임스페이스에 배포하고 fullnamesealed-secrets-controller로 설정해 두어야 합니다.

sealed-secrets(server) 설치

helm repo add sealed-secrets https://bitnami-labs.github.io/sealed-secrets
helm install sealed-secrets -n kube-system --set-string fullnameOverride=sealed-secrets-controller sealed-secrets/sealed-secrets

oauth2-proxy에서 사용할 Secret을 yaml 파일로 정의합니다.

oauth2-proxy-secret.yaml

apiVersion: v1
kind: Secret
metadata:
  labels:
    app: oauth2-proxy
  name: oauth2-proxy
  namespace: istio-system
type: Opaque
data:
  client-id: "xxxxx" # github oauth app의 client id를 base64로 인코딩한 값
  client-secret: "xxxxx" # github oauth app의 client secret를 base64로 인코딩한 값
  cookie-secret: "xxxxx" # 랜덤한 값 생성

client-idclient-secret은 다음과 같이 생성합니다.
client-secret은 클라이언트 보안 비밀번호입니다.

# client-id
printf "<<google-oauth-client-id>>" | base64

# client-secret
printf "<<google-oauth-client-secret>>" | base64

cookie-secret 값은 랜덤한 값을 생성합니다.

openssl rand -base64 32 | head -c 32 | base64

이렇게 생성한 yaml 파일을 이용해서 SealedSecret 오브젝트 파일을 생성합니다.

cat oauth2-proxy-secret.yaml | kubeseal -o yaml > sealed-oauth2-proxy-secret.yaml

이렇게 생성된 SealedSecret을 oauth2-proxy 서브차트의 templates에 옮겨 둡니다.

그리고 metric/values.yaml에 oauth2-proxy 설정에 config.existingSecret 값에 oauth2-proxy를 넣어서 그것을 사용하도록 합니다.

oauth2-proxy 서브차트가 배포되면 SealedSecret 오브젝트가 생성되는데, 이 때 sealed-secret controller에 의해 원래의 secret 오브젝트가 생성되게 됩니다. 이 secret을 사용하도록 설정하는 것입니다.

metric/values.yaml

...
oauth2-proxy:
  # 추가
  config:
    existingSecret: oauth2-proxy
  extraArgs:
    provider: google
    standard-logging: true
    auth-logging: true
    request-logging: true
    silence-ping-logging: true
    upstream: static://200

VirtualService 추가하기

metric/templates/metric-vs.yaml에 oauth2-proxy 서비스로 향하는 istio의 VirtualService 규칙을 추가합니다.

metric/templates/metric-vs.yaml

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: metric-vs
  namespace: {{ .Release.Namespace }}
spec:
  hosts:
  - {{ .Values.ingress.domain }}
  gateways:
  - metric-gateway
  http:
  - name: oauth2-proxy
    match:
    - uri:
        exact: /oauth2
    - uri:
        prefix: /oauth2
    route:
    - destination:
        host: {{ include "metric.name" . }}-oauth2-proxy.{{ .Release.Namespace }}.svc.cluster.local
        port:
          number: 80
   ...

Istio ExtensionProvider 정의하기

istio에서 인증을 요구하는 요청에 대해서 oauth2-proxy를 사용해서 인증하도록 처리하기 위해서는 ExtensionProvider를 추가하여 구현할 수 있습니다.

🔗 https://istio.io/latest/docs/reference/config/istio.mesh.v1alpha1/#MeshConfig-ExtensionProvider

AuthorizationPolicy에서 인증이 요구되는 요청이 들어온 경우 istiod의 MeshConfig에 정의된 ExtensionProvider를 사용하여 인증을 요청하도록 구성합니다.

istio 차트의 values.yaml에서 istiod의 meshConfigenvoyExtAuthzHttp 설정을 추가하도록 합니다.

istio/values.yaml

istiod:
  meshConfig:
    defaultConfig:
      tracing:
        zipkin:
          address: metric-jaeger-collector.istio-system.svc:9411
    # 추가
    extensionProviders:
    - name: "oauth2-proxy"
      envoyExtAuthzHttp:
        service: "metric-oauth2-proxy.istio-system.svc.cluster.local"
        port: "80" # The default port used by oauth2-proxy.
        includeHeadersInCheck: # headers sent to the oauth2-proxy in the check request.
            - "cookie"
            - "x-forwarded-access-token"
            - "x-forwarded-user"
            - "x-forwarded-email"
            - "authorization"
            - "x-forwarded-proto"
            - "proxy-authorization"
            - "user-agent"
            - "x-forwarded-host"
            - "from"
            - "x-forwarded-for"
            - "accept"
        headersToUpstreamOnAllow: ["authorization", "path", "x-auth-request-user", "x-auth-request-email", "x-auth-request-access-token", "x-auth-request-user-groups"] # headers sent to backend application when request is allowed.
        headersToDownstreamOnDeny: ["content-type", "set-cookie"] # headers sent back to the client when request is denied.

oauth2-proxy라는 이름으로 envoyExtAuthzHttp를 설정하고, 서비스는 oauth2-proxy를 향하도록 설정합니다.
includeHeadersInCheck에 요청이 들어왔을 때 인증을 체크할 헤더들을 추가하고,
headersToUpstreamOnAllow에는 허용된 요청이 들어가는 실제 컨테이너에 어떤 헤더들이 포함되는지를 추가합니다.
headersToDownStreamOnDeny에는 요청이 거부되었을 때 클라이언트로 보내질 헤더들을 추가합니다.


AuthorizationPolicy 생성하기

인증이 요구되는 요청들을 istiod에 설정했던 envoyExtAuthzHttp를 사용하여 인증을 하도록 설정하기 위해 AuthorizatinPolicy를 정의합니다.

metric/templates/metric-auth.yaml

apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  name: {{ include "metric.name" .}}-oauth-proxy
  namespace: {{ .Release.Namespace }}
spec:
  selector:
    matchLabels:
      app: istio-ingressgateway
  action: CUSTOM
  provider:
    name: "oauth2-proxy"
  rules:
  - to:
    - operation:
        hosts:
        - {{ .Values.ingress.domain }}
        notPaths:
        - /oauth2*
        paths: 
        - "/kiali*"
        - "/prometheus*"
        - "/grafana*"
        - "/jaeger*"

admin.<<domain>> 호스트에서 kiali, prometheus, grafana, jaeger 경로로 들어오는 요청들은 oauth2-proxy provider를 거칠 수 있도록 설정합니다.


oauth2-proxy subchart 배포하기

작업 내용은 github repository에 반영하고 argocd에서 metric 애플리케이션을 sync합니다.

이렇게 모두 생성한 다음 admin.<<domain>>의 한 서비스에 접속하게 되면

브라우저의 devtools에서 확인하면 403(권한 없음) 코드가 나타난 것을 확인할 수 있고,

argocd나 kubectl을 이용하여 metric-oauth2-proxy 컨테이너의 로그를 확인하면 요청이 인증되지 않았다는 것을 확인할 수 있습니다.

... [oauthproxy.go:970] No valid authentication in request. Initiating login.
... admin.<<domain>> GET - "/kiali" HTTP/1.1 "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36" 403 8490 0.001

/oauth2/start 엔드포인트로부터 로그인 과정이 시작됩니다.

oauth2-proxy의 로그인 페이지가 나타납니다.

Sign in with Google 버튼을 클릭하면 먼저 등록해 두었던 oauth 2.0 client를 이용하여 google 계정으로 로그인을 할 수 있도록 요청을 합니다.

redirect uri로 /oauth2/callback의 기본 리디렉션 uri가 등록되어 있고, state로 원래 접속하려던 경로를 확인할 수 있습니다.

인증이 완료된 다음에는 /oauth2/callback 리디렉션 URI로 요청을 보내지고, 302 상태 코드와 함께 원래 접속하려던 주소인 /kiali로 리디렉션이 일어납니다.

0개의 댓글