Security

Kubernetes에서 Keycloak SSO 구축하기 — GitLab Identity Brokering과 11가지 실전 교훈

Somaz 2026. 10. 29. 00:00
반응형

Overview

한동안 우리 클러스터는 GitLab을 모든 앱의 OIDC Identity Provider로 써 왔다. ArgoCD는 내장 dex의 GitLab connector를, Harbor는 native OIDC 엔드포인트를, Vaultwarden은 SSO authority로 각각 GitLab을 바라보는 구조였다. 잘 굴러가긴 했지만 새 요구사항이 생길 때마다 — LDAP 연동, MFA 정책, 세션 수명, SSO 전용 관리 도구 등 — GitLab에 새 기능을 욱여넣거나 별도 인증 레이어를 또 하나 띄워야 했다.

 

결국 대부분의 팀이 도달하는 결론을 우리도 따랐다. GitLab 앞에 Keycloak을 중앙 IdP로 두고, Identity Brokering으로 기존 GitLab 계정을 그대로 쓰면서, 다운타임 0·비밀번호 재설정 0으로 옮기는 것이다. 이 글은 그 작업의 현장 기록이다.

 

 

작업 범위는 다음과 같다.

  • 다운스트림 앱 3개 (ArgoCD, Harbor, Vaultwarden)
  • realm 1개, Keycloak client 4개
  • 네 번 갈아엎은 끝에 살아남은 부트스트랩 스크립트 1개
  • 공식 문서에는 없는 실전 함정 11가지

 

이 글은 다음 순서로 진행한다.

  • 짧은 아키텍처 개요
  • 왜 Brokering을 선택했는가 (각 앱이 GitLab을 직접 바라보는 대신)
  • 핵심만 추린 구현 단계 (Operator → realm·client → ArgoCD → Harbor)
  • 함정 11가지 — 시간을 가장 많이 잡아먹은 부분
  • 솔직하게 "아직 해결 못 한 것" (Vaultwarden의 HTTP/2 drop)
  • 작업하면서 정리된 베스트 프랙티스

 

 

민감한 값들(도메인, 시크릿, IP)은 모두 `example.com` 형태로 치환했다.

 

 

 

ArgoCD, Harbor, Vaultwarden을 GitLab 직접 OIDC에서 중앙 Keycloak IdP로 마이그레이션한 내용이다.

 

 

 

 

 

 

 


 

 

 

아키텍처 개요

목표 아키텍처는 일부러 분리한 3개 레이어로 이루어져 있다.

  1. Keycloak Operator — 클러스터당 한 번 설치, `Keycloak` 과 `KeycloakRealmImport` CR을 감시
  2. Keycloak 인스턴스 (CR) — 환경별로 `Keycloak` CR 1개 + PostgreSQL 릴리스 1개. Operator가 reconcile
  3. GitLab으로의 Identity Brokering — 다운스트림 앱은 Keycloak을 IdP로 신뢰하고, GitLab은 외부 IdP로 등록한다 (`providerId=oidc`)
 
┌─────────────────┐
│     Browser     │
│      User       │
└────────┬────────┘
         │ HTTPS
         ▼
┌────────────────────────────────────────────────┐
│  Kubernetes Cluster (NGF Gateway)              │
│                                                │
│  ┌─────────┐  ┌─────────┐  ┌────────────┐      │
│  │ ArgoCD  │  │ Harbor  │  │ Vaultwarden│      │
│  └────┬────┘  └────┬────┘  └─────┬──────┘      │
│       │ dex OIDC  │ native OIDC │              │
│       └───────────┼─────────────┘              │
│                   ▼                            │
│             ┌──────────┐    ┌─────────────┐    │
│             │ Keycloak │◄───│  Operator   │    │
│             └────┬─────┘    └─────────────┘    │
└──────────────────┼─────────────────────────────┘
                   │ Identity Brokering
                   ▼
            ┌──────────────┐    ┌─────────┐
            │    GitLab    │    │  LDAP   │
            │  (자체호스팅)   │    │  (예정)  │
            └──────────────┘    └─────────┘

 

 

짚어둘 결정사항 몇 가지

  • Operator와 CR은 별개 컴포넌트로 분리한다. ECK(`eck-operator` + `elasticsearch`)와 같은 패턴이다. Operator는 클러스터 단위 싱글톤이고, CR은 환경별로 둔다.
  • 공식 Helm 차트가 없다. `keycloak/keycloak-k8s-resources` 는 raw YAML만 제공하고, Helm Hub의 차트는 전부 커뮤니티가 관리한다. 그래서 공식 raw YAML을 자체 `keycloak-operator` 차트로 감싸고, 인스턴스용으로는 얇은 `keycloak-cr` 래퍼를 따로 만들었다.
  • PostgreSQL은 같은 네임스페이스의 별도 릴리스로 띄우고, `helmfile` 의 `needs:` 로 순서를 강제한다.
  • realm 이름은 `example`, 앱마다 client 1개씩(`argocd`, `harbor`, `oauth2-proxy`, 향후 추가될 `vaultwarden`)을 두고, GitLab의 IdP mapper로 매핑되는 그룹 `server` 를 둔다. ArgoCD의 RBAC(`g, server, role:server-admin`)는 그룹 claim 이름을 처음부터 끝까지 그대로 보존했기 때문에 별도 변경 없이 그대로 동작한다.

 

 

왜 Identity Brokering인가? (vs 직접 OIDC)

Brokering을 선택한 가장 단순한 이유는 시스템이 그대로 유지된다는 점이다.

 

각 앱 입장에서는 `https://auth.example.com/realms/example` 이라는 평범한 OIDC IdP가 하나 있을 뿐이다. 그 뒤에서 Keycloak이 실제 로그인을 GitLab에 위임하지만, 앱은 그 사실을 알 필요도 없고 알아서도 안 된다.

관점 직접 GitLab OIDC (이전) Keycloak + Brokering (이후)
LDAP / SAML / MFA 추가 앱마다 따로 붙이거나 포기 Keycloak에 한 번만 설정하면 모든 앱이 그대로 활용
세션 정책 앱마다의 자체 로직 + GitLab 세션 Keycloak realm 정책 한 곳에서 관리
사용자 경험 앱마다 GitLab 동의 화면을 한 번씩 거침 한 번 로그인하면 앱 간 SSO
마이그레이션 비용 — 1회 재로그인. 비밀번호 재설정 없음
GitLab의 역할 모든 앱의 인증 IdP 사용자·그룹의 단일 source of truth, 단 Keycloak만 GitLab과 통신
향후 확장성 GitLab IdP 기능에 묶임 IdP 레이어에서 OpenLDAP, Google, GitHub, SAML 자유롭게 추가

 

다운타임은 진짜로 0이다. 어떤 앱을 "직접 GitLab OIDC"에서 "Keycloak OIDC"로 전환하는 동안에도 GitLab 자체는 그대로 둔다.

 

Keycloak은 기존의 직접 통합과 나란히 GitLab의 새 클라이언트가 될 뿐이다. 앱 전환이 끝난 다음에 그 앱이 쓰던 GitLab Application을 따로 정리해주면 된다.

 

 

 

 

 

 


 

 

 

 

 

 

구현 단계

전체 계획은 9단계였지만, 공개해서 의미 있는 4단계만 정리한다.

 

 

Phase 1 — Keycloak Operator + 인스턴스

자체 OCI 레지스트리에 래퍼 차트 두 개를 올렸다.

  • keycloak-operator — 업스트림 keycloak-k8s-resources의 raw YAML(CRD + RBAC + Deployment)을 감싼 차트
  • keycloak-cr — 순수 CR 래퍼 (Keycloak + KeycloakRealmImport + 선택적 HTTPRoute)

 

인스턴스 values를 단순화하면 다음과 같다.

keycloak:
  instances: 1
  hostname:
    hostname: auth.example.com
  proxy:
    headers: xforwarded
  db:
    host: keycloak-postgresql.keycloak.svc
    existingSecret: keycloak-db-credentials

httproute:
  enabled: true
  parentRefs:
    - name: nginx-gateway
      namespace: nginx-gateway
      sectionName: https
  hostnames:
    - auth.example.com

 

 

  • `proxy.headers`: xforwarded는 필수다. Keycloak이 Gateway나 Ingress 뒤에 있다면 절대 빠뜨리지 말 것. 이 설정이 없으면 admin 콘솔이 hostname에서 무한 루프를 돈다.
  • `KeycloakRealmImport.spec.realm` 은 `inline RealmRepresentation` 만 받는다. `realmFromConfigMap` 같은 필드는 CRD에 없다. 그래서 `helmfile의 --set-file` 로 realm을 임베드한다.
  • Operator 차트에 `rbac.useClusterBindings` 옵션을 추가했다. 업스트림 raw YAML은 controller의 cluster-scoped role을 RoleBinding으로 묶어두는데, 이게 `watchNamespaces=*` 를 조용히 깨뜨린다. 옵션을 켜면 ClusterRoleBinding으로 바뀐다.

 

 

 

 

Phase 2 — Realm, Client, GitLab Brokering

example이라는 단일 realm을 사용한다. 안에는 다음 항목들이 있다.

  • 그룹: server (앱 단위 admin 권한 부여), global-admin (소수의 수동 그룹)
  • Client: argocd, harbor, oauth2-proxy (Standard Flow + PKCE, confidential)
  • Identity Provider: GitLab 1개 (providerId=oidc로 설정. 내장 providerId=gitlab이 아니다 — 아래 함정 1 참고)
  • groups client scope, GitLab의 groups claim을 brokered 사용자에 복사하는 groups IdP mapper, GitLab server 그룹 사용자에게 하드코딩된 server 그룹을 부여하는 매퍼

realm은 `kcadm-bootstrap.sh` 로 부트스트랩하고 `kcadm-verify.sh` 로 검증한다(작업 종료 시점 기준 38개 체크). 둘 다 idempotent하게 동작하면서 realm을 선언적으로 유지한다.

 

 

 

 

 

Phase 3 — ArgoCD 마이그레이션 (dex.config 교체)

ArgoCD에는 이미 dex gitlab connector가 설정되어 있었다. Keycloak으로의 전환은 결국 dex.config를 갈아 끼우는 작업이다.

configs:
  cm:
    dex.config: |
      connectors:
        - type: oidc
          id: keycloak
          name: Keycloak
          config:
            issuer: https://auth.example.com/realms/example
            clientID: argocd
            clientSecret: $dex.keycloak.clientSecret
            redirectURI: https://argocd.example.com/api/dex/callback
            insecureEnableGroups: true
            getUserInfo: true
  rbac:
    policy.csv: |
      g, server, role:server-admin
      g, admin@example.com, role:global-admin
  secret:
    extra:
      dex.keycloak.clientSecret: <KEYCLOAK_CLIENT_SECRET>

 

자명하지 않은 두 줄은 `insecureEnableGroups: true` 와 `getUserInfo: true` 다. 이 두 줄이 없으면 Keycloak이 groups claim을 정상적으로 보내고 있어도 dex가 조용히 그것을 떨어뜨린다(함정 10).

 

RBAC `policy.csv` 는 변경하지 않았다. 그룹 claim 이름(server)을 처음부터 끝까지 보존한 핵심 이유가 바로 이것이다 — ArgoCD는 IdP가 바뀌었다는 사실 자체를 몰라도 된다.

 

 

 

 

 

Phase 4 — Harbor 마이그레이션 (UI/API)

Harbor는 까다로운 친구다. OIDC 설정이 Helm으로 관리되지 않는다 — Harbor DB에 저장되어 있고 REST API로 토글한다. 게다가 한 번 켜면 못 끄는 일방통행 스위치가 있다. OIDC auth mode를 일단 켜면, DB 직접 수정 없이는 되돌릴 수 없다.

 

 

마이그레이션 절차는 이렇다.

  1. Harbor admin API로 `auth_mode=oidc_auth` 를 설정하고 새 OIDC 정보(`issuer`, `client ID/secret`, `group claim`, `group filter server` )를 적는다.
  2. Harbor OIDC 설정에서 `verify_certificate=false` 를 켠다 (내부적으로 self-signed wildcard 인증서를 쓰기 때문 — 함정 11에서 자세히).
  3. 기존 OIDC 사용자들을 모두 로그아웃 후 재로그인시킨다. IdP가 바뀌었으므로 sub 값이 변경되고, 같은 이메일이 Harbor에 이미 존재하면 함정 11이 발동한다.

 

이 과정 전체를 `harbor-admin.sh set-oidc` 서브커맨드로 감싸서 전환을 반복 가능하고 리뷰 가능하게 만들었다.

 

 

 

 


 

 

 

함정 11가지 — 공식 문서에 없는 것들

여기서부터가 공식 문서에는 없는 부분이다. 각 함정마다 증상, 원인, 해결책 순으로 정리했다.

  • 아래의 소스를 참고하면 된다.
  • https://github.com/somaz94/cicd-monitoring/tree/main/onpremis

 

 

함정 1 — Keycloak 내장 `providerId=gitlab` 은 자체호스팅 GitLab에서 동작하지 않는다

증상. Keycloak 로그인 페이지에서 "Sign in with GitLab"을 누른다. `https://gitlab.example.com` 으로 갈 줄 알았는데 `https://gitlab.com/users/sign_in으로`  리다이렉트된다.

 

원인. Keycloak 내장 social IdP들(gitlab, google, github)은 엔드포인트 URL이 SaaS 도메인으로 하드코딩되어 있다. 자체호스팅 인스턴스용으로 파라미터화되지 않았다.

 

해결. `providerId=oidc` 를 쓰고 5개 엔드포인트를 명시한다. `issuer`, `authorizationUrl`, `tokenUrl`, `userInfoUrl`, `jwksUrl` 그리고 `clientAuthMethod=client_secret_post`, `validateSignature=true` 를 함께 지정한다. 부트스트랩 스크립트는 providerId가 잘못된 IdP를 자동 감지해서 재생성한다.

 

 

 

함정 2 — providerId는 immutable, 결국 삭제 후 재생성

증상. 함정 1을 고쳐 보겠다고 `kcadm.sh` `update identity-provider/instances/gitlab -s providerId=oidc` 를 실행한다. 아무 일도 일어나지 않는다.

 

원인. Keycloak은 providerId를 IdP의 type으로 취급한다. mutable 필드가 아니며, patch 경로 자체가 없다.

 

해결. 부트스트랩 스크립트는 기존 IdP의 providerId를 검사해서 원하는 값과 다르면 삭제 후 재생성해야 한다. `idempotency` 를 단순히 "없으면 만든다"가 아니라 "없거나 모양이 다르면 만든다"로 정의해야 한다.

 

 

 

함정 3 — GitLab 전용 scope를 OIDC provider가 거절한다.

증상. `providerId=oidc` 로 바꾼 직후, 모든 brokered 로그인이 token endpoint 단계에서 `invalid scope: read_user` 같은 에러로 실패한다.

 

원인. GitLab 내장 IdP는 커스텀 scope(read_user, read_api 등)를 지원한다. 표준 OIDC 엔드포인트는 이런 scope를 모르므로 거부한다.

 

해결. defaultScope는 표준 OIDC 3종 세트만 둔다: `openid email profile.` GitLab 전용 scope는 내장 gitlab IdP 설정을 그대로 복사하면서 남은 잔재일 뿐이다.

 

 

 

함정 4 — `trustEmail=false` 면 brokered 사용자가 "Not verified"로 보인다

증상. 로그인은 되는데 Keycloak Admin UI에서 모든 brokered 사용자가 "Email not verified"로 표시된다. 검증 메일도 오지 않는다(SMTP 설정 자체가 없으니 당연하다).

 

원인. IdP의 trustEmail 기본값이 false다. GitLab이 userinfo에서 `email_verified=true` 를 돌려줘도 Keycloak은 기본적으로 업스트림 IdP를 신뢰하지 않으므로 사용자를 `emailVerified=false` 로 import해버린다.

 

해결. IdP 정의에 `trustEmail=true` 를 설정한다. GitLab을 사용자의 source of truth로 두는 한 — 그게 brokering의 본래 목적이니 — 이게 합리적인 기본값이다.

 

 

 

함정 5 — ArgoCD 차트 9.x에서 secrets 경로가 `configs.secret.extra` 로 옮겨졌다

증상. ArgoCD-server가 시작되고 dex가 시작되고, dex가 곧바로 크래시한다: `failed to get signing algorithm: no signing key found.`

 

원인. 구버전 ArgoCD Helm 차트는 `configs.secrets` 를 썼다. 9.x에서 `configs.secret.extra` 로 옮겨졌다. 옛 키는 조용히 무시되므로 dex가 참조하는 `$dex.keycloak.clientSecret` 이 결국 비어 있는 상태가 된다.

 

해결. secret을 `configs.secret.extra:` 아래로 옮긴다. (또는 우리처럼 client secret을 `dex.config` 에 인라인으로 박아두는 방법도 있다 — GitLab 시절에 쓰던 컨벤션 그대로.)

 

 

 

함정 6 — HTTP→HTTPS 리다이렉트가 차트의 HTTPRoute와 충돌한다

증상. `curl -I http://argocd.example.com` 이 301이 아닌 200을 돌려준다. 우리가 추가한 리다이렉트 HTTPRoute가 동작하지 않는다.

 

원인. 차트에 기본 포함된 argocd-server HTTPRoute가 sectionName 지정 없이 두 Gateway listener(HTTP, HTTPS)에 모두 붙어 있다. HTTP listener에서는 그쪽의 backendRef 룰이 우리가 추가한 `argocd-https-redirect filter` 룰을 이긴다.

 

해결. 차트 기본 HTTPRoute를 HTTPS listener에만 핀(pin)한다.

server:
  httproute:
    parentRefs:
      - name: nginx-gateway
        namespace: nginx-gateway
        sectionName: https

 

 

함정 7 — dex가 openid를 자동으로 prepend해서 scope 리스트를 깨뜨린다

증상. Keycloak이 dex 로그인을 거부한다: `Invalid scopes: openid openid profile email groups.`

 

원인. dex는 scopes:에 적은 값 앞에 openid를 자동으로 붙인다. 여기에 openid를 명시적으로 또 적으면 위처럼 중복이 되고 Keycloak이 거부한다.

 

해결. dex connector 설정에서 `scopes:` 블록을 아예 빼버린다. 

 

 

 

함정 8 — groups client scope가 없으면 "unknown scope"로 거부된다

증상. 함정 7을 고친 뒤에도 로그인이 `Invalid scopes: groups` 로 실패한다.

 

원인. dex는 groups scope를 요청한다. Keycloak은 모르는 client scope를 거부한다 — 그리고 groups는 빌트인 client scope가 아니다(claim은 빌트인이지만 scope는 별도로 만들어줘야 한다).

 

해결. realm에 groups client scope를 만들어 관련 client에 붙인다. 부트스트랩 스크립트가 idempotent하게 처리한다.

 

 

 

함정 9 — `kcadm.sh -s 'config."key"=value'` 가 nested field를 제거한다.

증상. protocol mapper가 만들어지긴 한다. `kcadm.sh get` 에도 보인다. 그런데 토큰에는 추가되어야 할 claim이 없다.

 

원인. Keycloak protocol mapper는 6개 config 필드(`claim.name, full.path, id.token.claim, access.token.claim, userinfo.token.claim, introspection.token.claim`)를 갖는다. 이걸 dotted-key 셸 문법(`-s 'config."access.token.claim"=true'`)으로 설정하면 Keycloak 26.x에서 일부 필드가 조용히 누락된다. mapper는 결국 config: {} 상태로 만들어지고, Keycloak은 빠진 플래그를 모두 false로 해석한다.

문제를 더 키우는 건, `kcadm.sh get ... --fields config` 가 dotted key를 제대로 렌더링하지 못한다는 점이다. 그래서 정상 설정된 mapper도 출력에서는 `{}` 로 보인다.

 

해결. JSON 파일 방식을 쓴다. 6개 필드를 모두 명시한 JSON 문서를 만들어 kcadm.sh create -f mapper.json으로 넘긴다. 6개 필드가 모두 true인지 다시 읽어 검증하는 verifier도 같이 둔다.

 

 

 

함정 10 — dex의 OIDC connector는 기본적으로 groups를 무시한다

증상. Keycloak 토큰의 groups claim에 ["server"]가 분명히 들어 있다. 그런데 ArgoCD는 같은 로그인에서 `groups: []` 로 보여준다.

 

원인. dex의 oidc connector는 groups를 보안 민감 항목으로 취급해서, 명시적으로 opt-in 하지 않으면 떨어뜨린다. 의도된 동작이다.

 

해결. dex connector 설정에 `insecureEnableGroups: true` 와 `getUserInfo: true` 를 추가한다. (insecure 접두어는 dex의 편집적 선택일 뿐, 실제 위험을 의미하는 게 아니다.)

 

 

함정 11 — Harbor가 brokered 사용자를 거부한다 (이메일 중복)

증상. 옛 GitLab 직접 OIDC로 `admin@example.com` 로 로그인하던 Harbor 사용자가 새 Keycloak brokered flow로 로그인을 시도하면 다음과 같은 에러를 만난다.

failed to create user record:
user admin or email admin@example.com already exists

 

원인. OIDC sub가 이제 GitLab이 아니라 Keycloak(자체 internal user ID)에서 만들어진다. Harbor 입장에서는 onboarding 하려는 "새" OIDC identity로 보이는데, 이메일이 기존 사용자 레코드와 충돌한다.

 

해결. 우리는 option B(재온보딩)를 골랐다. API로 충돌하는 Harbor 사용자를 삭제하고, brokered 로그인이 그 사용자를 다시 만들도록 둔 뒤, sysadmin 권한을 다시 부여한다. 대안인 option A(sub 재매핑)는 DB에 직접 손을 대야 해서 스크립트화가 더 어렵다.

 

소규모 사용자 환경이라면 사용자당 curl 한 번이면 끝난다. 사용자 수가 많다면 `auth_mode` 를 켜기 전에 이메일로 일괄 삭제하는 cutover 스크립트를 미리 준비해야 한다.

 

 

 

 


 

 

 

 

아직 해결 못 한 것 — Vaultwarden의 HTTP/2 Drop

해피엔딩이 아니더라도, 정직한 포스트모텀에는 이런 섹션이 들어가는 게 맞다.

Vaultwarden의 SSO를 GitLab 직접 → Keycloak으로 두 번 옮겨봤고, 두 번 다 롤백했다.

 

 

증상. `Failed to discover OpenID provider: Request failed.` Rust 클라이언트 로그를 보면 TCP connect는 깨끗하게 성공하고, TLS handshake도 `ALPN: h2` 로 정상 협상되며, 그 다음 HTTP/2 frame이 단 하나도 오가기 전에 connection이 drop된다.

 

검증한 가설들

  1. DNS / hickory-resolver 버그 (Vaultwarden 1.35.7 → 1.35.8, PR #7108). 이미지 bump. 같은 drop 패턴.
  2. SSRF guard가 RFC1918 주소를 차단했을 가능성. `ICON_BLACKLIST_NON_GLOBAL_IPS=false` 로 비활성화. 같은 drop 패턴. 게다가 SSRF guard였다면 TCP connect 자체가 막혔을 텐데, 우리는 connect 성공을 직접 확인했다.
  3. 서버 측 문제 (Keycloak proxy 헤더, Gateway 설정 오류). Gateway access log와 Keycloak access log를 5분 윈도우로 검색했더니 Vaultwarden 요청의 흔적이 0건이었다. 서버는 무죄다 — 요청이 거기까지 도달조차 못 했다.

 

 

가장 유력한 원인. Vaultwarden의 `sso_client.rs::_get_client()` 에서 발생하는 클라이언트 측 `HTTP/2 setup race` 로 보인다. discovery 호출마다 새 `reqwest::ClientBuilder` 를 `default-features=false + ["rustls-tls", "rustls-tls-native-roots", "http2", ...]` 조합으로 빌드하는데, ALPN이 h2로 협상된 직후 hyper-util connection pool이 H2 stream을 실제로 열기 전에 connection을 drop하는 것으로 추정된다.

 

trace에 H2 frame이 없는 이유도 자연스럽다 — H2 stream 자체가 열린 적이 없으니까. 같은 클러스터의 다른 OIDC 클라이언트들(Harbor는 Go, ArgoCD는 dex 통한 Go)에서는 이 현상이 없다.

 

 

왜 Harbor는 됐고 Vaultwarden은 안 됐나. Harbor는 `verify_certificate=false` 라는 옵션을 노출해서 trust 경로를 통째로 우회할 수 있게 해준다. Vaultwarden은 SSO client에서 HTTP/1.1을 강제할 동등한 옵션이 없다.

 

 

남은 선택지. 셋 다 아직 손 안 댔다.

  • (a) 업스트림 패치 — `_get_client()` 가 `reqwest::ClientBuilder::http1_only()` 를 호출하도록 하거나, `SSO_HTTP1_ONLY=true` 같은 환경변수를 추가하는 이슈를 올린다.
  • (b) Gateway 레벨에서 ALPN을 h1-only로 제한 — 가능은 하지만 ArgoCD와 Harbor의 OIDC 클라이언트도 HTTP/1.1로 동반 다운그레이드된다.
  • (c) Vaultwarden 소스 빌드 — fork → 패치 → 사설 이미지 운영. 가장 무거운 commitment.

 

 

베이스라인(이미지 1.35.7, cert 마운트 없음, 기본 SSRF guard)으로 롤백하고 나머지 스택은 Keycloak에 그대로 두었다. 이 글을 쓰는 시점에 Vaultwarden만 직접 GitLab OIDC에 남아 있다. cert-trust 관련 설정은 (a)/(b)/(c) 중 하나가 land하는 시점에 다시 켤 수 있도록 values 파일에 주석 처리된 블록으로 남겨두었다.

 

이 섹션을 공개적으로 적어두는 건 그 자체로 의미가 있다. 위의 함정 섹션이 존재할 수 있는 이유 자체가, 누군가가 어느 시점에 같은 벽에 부딪혀서 고쳤거나 적어두었기 때문이다. 지금 이 케이스는 아직 열려 있다.

 

 

 

 

 


 

 

 

베스트 프랙티스 정리

순서 무관하게, 다시 해도 똑같이 할 것들이다.

 

 

운영적으로 보상이 컸던 실천들

1. 부트스트랩 스크립트와 verify 스크립트를 한 쌍으로 운영 부트스트랩은 idempotent하게 원하는 realm 모양을 적용한다. verify는 단순한 assertion 모음이다(우리 경우 38개). 변경할 때마다 실행해서 pass/fail 카운트를 본다. 함정 1, 4, 8, 9를 빨리 잡아낸 가장 큰 이유가 이것이다.

 

2. Operator와 CR을 별개 컴포넌트로 둔다 ECK와 같은 패턴이다. Operator는 클러스터당 한 번 설치, CR은 환경별. 나중에 두 번째 Keycloak 인스턴스(예: 스테이징)를 추가할 때 Operator를 다시 깔지 않아도 된다.

 

3. claim 이름을 처음부터 끝까지 보존한다 GitLab 그룹 이름 server를 IdP mapper, Keycloak 그룹, realm 토큰 어디에서도 바꾸지 않았다. 그 결과 ArgoCD policy.csv는 한 줄도 손대지 않았다. RBAC 편집 비용이 0이었다.

 

4. realm을 export해서 git으로 추적한다 Phase 3가 안정화된 시점에 realm을 export해서 커밋했다. 이후의 drift는 live 상태와 git에 들어 있는 export를 비교해서 잡는다. 부트스트랩 스크립트도 같은 JSON을 읽는다.

 

5. 함정을 발견한 그날 바로 문서화한다 함정 5~10은 모두 한 오후에 잡고 고쳤지만, 문서도 같은 날 갱신했다. 두 달 뒤 클러스터를 처음부터 다시 배포할 때 어느 것도 재발하지 않았다 — 부트스트랩과 verify 스크립트가 그동안의 교훈을 흡수하고 있었기 때문이다.

 

6. 인증뿐 아니라 RBAC 강제도 같이 검증한다 전환이 끝난 직후, 사용자의 global-admin 매핑을 잠시 주석 처리한 채로 server 그룹만으로 ArgoCD에 접근이 되는지 확인한다. 그다음에는 앱별 권한도 잠시 주석 처리해서 UI에서 사라지는지 확인한다. 이때 만든 임시 수정은 절대 커밋하지 말 것.

 

 

한 줄짜리 작은 것들

  • Keycloak이 Gateway나 Ingress 뒤에 있을 때는 항상 `proxy.headers: xforwarded` 를 켠다.
  • 차트가 관리하는 HTTPRoute는 sectionName으로 특정 listener에 핀해둔다.
  • GitLab을 brokered IdP로 쓸 때는 항상 `providerId=oidc` + 명시적 엔드포인트 + `trustEmail=true` 조합이다.
  • realm에 groups client scope를 꼭 둔다. 없으면 dex 요청이 fail-closed로 깨진다.
  • verifier 스크립트는 비용이 싸다. realm마다 하나씩 두고, reconcile할 때마다 돌린다.

 

 

 


 

 

 

마무리

마이그레이션은 다운타임 0, 사용자당 재로그인 1회로 끝났다. ArgoCD와 Harbor는 이제 Keycloak으로 인증하고, Keycloak은 GitLab으로 federate한다. GitLab은 사용자·그룹의 source of truth라는 기존 역할을 그대로 유지한다. 클러스터는 LDAP, MFA, SAML SP를 추가할 단일 지점을 얻었다 — 다운스트림 앱을 어디에도 손대지 않고도.

 

Vaultwarden은 여전히 직접 GitLab OIDC에 남아 있고, 업스트림 fix 또는 우회책을 기다리는 중이다. 그것 때문에 나머지를 묶어두는 대신 나머지를 먼저 출시했고, 다음 사람이 같은 세 가설에 하루를 날리지 않도록 실패 모드를 문서화해두었다.

 

이 글에서 단 하나만 가져간다면 이거다. 부트스트랩 스크립트와 verify 스크립트의 페어는 "구전(口傳)되는 SSO 지식"을 git-blame이 가능한 무언가로 바꿔준다. 함정 11가지 대부분도 verifier에 OK/FAIL을 찍는 assertion으로 들어가는 순간 자명해졌다. 그 전에는 모두 "지난주에는 됐었는데"였을 뿐이다.

 

다음 글에서는 후속 작업을 다룰 예정이다. OpenLDAP을 Keycloak 뒤에 upstream User Federation 소스로 추가해서, 같은 realm이 다운스트림을 손대지 않고도 GitLab-brokered 사용자와 LDAP-only 사용자를 동시에 서비스하도록 만드는 작업이다.

 

 

 

 

 

 

 


Reference

https://github.com/somaz94/cicd-monitoring/tree/main/onpremise

 

 

Somaz | DevOps Engineer | Kubernetes & Cloud Infrastructure Specialist

반응형