# Keycloak 예시 Keycloak 26+ (Quarkus distribution) + Keycloak Operator 기준. 모든 YAML은 그대로 `kubectl apply`로 적용 가능한 완전한 manifest다. --- ## 좋은 예시 1: optimized 이미지 빌드 (두 단계) `kc.sh build`로 Quarkus augmentation을 굽고, 실행 이미지를 분리한다. ```dockerfile # Dockerfile.keycloak FROM quay.io/keycloak/keycloak:26.0.7 AS builder ENV KC_DB=postgres ENV KC_HEALTH_ENABLED=true ENV KC_METRICS_ENABLED=true ENV KC_CACHE=ispn ENV KC_CACHE_STACK=jdbc-ping ENV KC_FEATURES=token-exchange,admin-fine-grained-authz ENV KC_HTTP_ENABLED=true RUN /opt/keycloak/bin/kc.sh build FROM quay.io/keycloak/keycloak:26.0.7 COPY --from=builder /opt/keycloak/ /opt/keycloak/ USER 1000 ENTRYPOINT ["/opt/keycloak/bin/kc.sh", "start", "--optimized"] ``` **왜 좋은가:** - 빌드 단계에서 augmentation 완료, 런타임은 runtime-only config만 수신 - `--optimized` 플래그로 매 기동 시 build 재실행 방지 (cold start 50% 단축) - v26+ `--proxy` 제거 대응: legacy 옵션이 build 시 포함되지 않음 --- ## 나쁜 예시 1: dev mode / 매 기동 build ```yaml args: - start-dev ``` 또는 ```yaml args: - start ``` **문제:** - `start-dev`는 hostname-strict=false, H2 in-memory DB, TLS 해제 — production 부적합 - `start`는 optimized 이미지가 아니면 매 기동마다 Quarkus augmentation 수행 → cold start 2배+ - v26에서 `--proxy edge` 같은 legacy 옵션은 아예 기동 실패 --- ## 좋은 예시 2: Keycloak Operator Keycloak CR (1차 권장) ```yaml --- apiVersion: v1 kind: Namespace metadata: name: keycloak labels: pod-security.kubernetes.io/enforce: restricted pod-security.kubernetes.io/audit: restricted --- apiVersion: v1 kind: Secret metadata: name: keycloak-db-secret namespace: keycloak type: Opaque stringData: username: keycloak password: REPLACE_VIA_VSO --- apiVersion: v1 kind: Secret metadata: name: keycloak-tls namespace: keycloak type: kubernetes.io/tls data: tls.crt: LS0tLS1CRUdJTi... # cert-manager 발급 권장 tls.key: LS0tLS1CRUdJTi... --- apiVersion: k8s.keycloak.org/v2alpha1 kind: Keycloak metadata: name: keycloak namespace: keycloak labels: app.kubernetes.io/name: keycloak app.kubernetes.io/instance: keycloak-prod app.kubernetes.io/part-of: identity-platform app.kubernetes.io/managed-by: keycloak-operator spec: instances: 3 image: registry.example.com/platform/keycloak:26.0.7-optimized # gitleaks:allow startOptimized: true db: vendor: postgres host: keycloak-db-rw.keycloak.svc.cluster.local port: 5432 database: keycloak usernameSecret: name: keycloak-db-secret key: username passwordSecret: name: keycloak-db-secret key: password poolMinSize: 5 poolInitialSize: 5 poolMaxSize: 20 hostname: hostname: https://auth.example.com admin: https://admin-auth.example.com strict: true backchannelDynamic: false http: httpEnabled: true tlsSecret: keycloak-tls proxy: headers: xforwarded features: enabled: - token-exchange - admin-fine-grained-authz additionalOptions: - name: cache value: ispn - name: cache-stack value: jdbc-ping - name: log-console-output value: json - name: metrics-enabled value: "true" - name: health-enabled value: "true" resources: requests: cpu: 500m memory: 1Gi limits: cpu: "2" memory: 2Gi scheduling: topologySpreadConstraints: - maxSkew: 1 topologyKey: topology.kubernetes.io/zone whenUnsatisfiable: ScheduleAnyway labelSelector: matchLabels: app: keycloak affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: topologyKey: kubernetes.io/hostname labelSelector: matchLabels: app: keycloak --- apiVersion: policy/v1 kind: PodDisruptionBudget metadata: name: keycloak namespace: keycloak spec: minAvailable: 2 unhealthyPodEvictionPolicy: AlwaysAllow selector: matchLabels: app: keycloak ``` **왜 좋은가:** - Operator가 StatefulSet, Service, cache stack 설정을 자동 관리 - hostname v2 (full URL, admin host 분리, strict=true, backchannelDynamic=false) 명시 - `startOptimized: true`로 Operator가 `kc.sh start --optimized` 실행 - PDB `minAvailable: 2` + topologySpread로 zone-level disruption 방어 --- ## 나쁜 예시 2: 수제 Deployment + `--proxy edge` ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: keycloak spec: replicas: 1 template: spec: containers: - name: keycloak image: quay.io/keycloak/keycloak:26.0.7 args: ["start", "--proxy", "edge"] env: - name: KC_HOSTNAME value: auth.example.com - name: KC_HOSTNAME_STRICT value: "false" ``` **문제:** - `--proxy` 옵션은 v26에서 제거되어 기동 실패 - `KC_HOSTNAME`에 scheme 없는 호스트명 단독 전달 → v2 검증에서 경고 - `KC_HOSTNAME_STRICT=false`는 proxy hop이 Host 헤더를 조작할 수 있는 공격 벡터를 열어둠 - replicas: 1 + Deployment → rolling update 시 Infinispan cluster membership 이슈 + 단일 장애 --- ## 좋은 예시 3: Probe (management port 9000) ```yaml ports: - name: http containerPort: 8080 protocol: TCP - name: management containerPort: 9000 protocol: TCP startupProbe: httpGet: path: /health/started port: 9000 scheme: HTTP periodSeconds: 5 failureThreshold: 60 timeoutSeconds: 3 readinessProbe: httpGet: path: /health/ready port: 9000 scheme: HTTP periodSeconds: 10 failureThreshold: 3 timeoutSeconds: 3 livenessProbe: httpGet: path: /health/live port: 9000 scheme: HTTP initialDelaySeconds: 60 periodSeconds: 30 failureThreshold: 3 timeoutSeconds: 3 ``` **왜 좋은가:** - 9000은 management port (`KC_HTTP_MANAGEMENT_PORT` 기본값) - startupProbe 5분 유예: JVM + Quarkus + DB migration cold start 수용 - readiness는 `/health/ready` (DB connectivity 포함), liveness는 `/health/live` (프로세스 생존) --- ## 나쁜 예시 3: Probe를 8080 `/` 로 설정 ```yaml readinessProbe: httpGet: path: / port: 8080 periodSeconds: 3 failureThreshold: 2 ``` **문제:** - 8080 `/`는 redirect 응답이고 DB / cache readiness를 검증하지 않음 - `failureThreshold: 2` + `periodSeconds: 3`은 cold start 중 pod 재시작 유발 - health endpoint가 켜져 있어도 사용하지 않아 관찰 포인트 상실 --- ## 좋은 예시 4: Service + ServiceMonitor ```yaml --- apiVersion: v1 kind: Service metadata: name: keycloak namespace: keycloak labels: app.kubernetes.io/name: keycloak app.kubernetes.io/instance: keycloak-prod spec: type: ClusterIP selector: app: keycloak ports: - name: http port: 8080 targetPort: 8080 - name: management port: 9000 targetPort: 9000 --- apiVersion: v1 kind: Service metadata: name: keycloak-headless namespace: keycloak spec: type: ClusterIP clusterIP: None selector: app: keycloak ports: - name: http port: 8080 targetPort: 8080 --- apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor metadata: name: keycloak namespace: keycloak labels: app.kubernetes.io/name: keycloak release: kube-prometheus-stack spec: selector: matchLabels: app.kubernetes.io/name: keycloak endpoints: - port: management path: /metrics interval: 30s scrapeTimeout: 10s ``` **왜 좋은가:** - ClusterIP Service가 사용자 트래픽용(8080), management(9000)을 분리 expose - Headless service는 cache peer discovery 보조 (jdbc-ping에서는 불필요하지만 DNS_PING fallback 대비) - ServiceMonitor는 management port의 `/metrics`만 scrape --- ## 좋은 예시 5: Ingress — SSO host + Admin host 분리 ```yaml --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: keycloak-sso namespace: keycloak annotations: cert-manager.io/cluster-issuer: letsencrypt-prod nginx.ingress.kubernetes.io/proxy-buffer-size: "128k" nginx.ingress.kubernetes.io/proxy-body-size: "4m" spec: ingressClassName: nginx tls: - hosts: - auth.example.com secretName: keycloak-sso-tls rules: - host: auth.example.com http: paths: - path: /realms/ pathType: Prefix backend: service: name: keycloak port: number: 8080 - path: /resources/ pathType: Prefix backend: service: name: keycloak port: number: 8080 - path: /.well-known/ pathType: Prefix backend: service: name: keycloak port: number: 8080 - path: /js/ pathType: Prefix backend: service: name: keycloak port: number: 8080 --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: keycloak-admin namespace: keycloak annotations: cert-manager.io/cluster-issuer: letsencrypt-prod nginx.ingress.kubernetes.io/whitelist-source-range: "10.0.0.0/8,192.168.0.0/16" nginx.ingress.kubernetes.io/auth-url: "https://oauth2-proxy.example.com/oauth2/auth" spec: ingressClassName: nginx tls: - hosts: - admin-auth.example.com secretName: keycloak-admin-tls rules: - host: admin-auth.example.com http: paths: - path: / pathType: Prefix backend: service: name: keycloak port: number: 8080 ``` **왜 좋은가:** - SSO host는 `/realms/`, `/resources/`, `/.well-known/`, `/js/` 만 공개 (필요 최소) - Admin host는 별도 hostname + IP whitelist + forward-auth 2중 보호 - `/metrics`, `/health*`, `/admin/`이 SSO host에 노출되지 않음 --- ## 나쁜 예시 4: 전체 공개 + 9000 노출 ```yaml apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: keycloak spec: rules: - host: auth.example.com http: paths: - path: / pathType: Prefix backend: service: name: keycloak port: number: 8080 - path: /metrics pathType: Prefix backend: service: name: keycloak port: number: 9000 ``` **문제:** - `/` 공개 → `/admin/` 포함 전부 외부 노출 → credential stuffing / brute force 표면 확장 - `/metrics`는 인증이 없는 운영 데이터 endpoint → 정보 유출 - 9000 management port가 인터넷에 노출 → health/metrics 둘 다 오픈 --- ## 좋은 예시 6: KeycloakRealmImport CR ```yaml --- apiVersion: v1 kind: Secret metadata: name: platform-realm namespace: keycloak type: Opaque stringData: realm.json: | { "realm": "platform", "enabled": true, "sslRequired": "external", "registrationAllowed": false, "loginWithEmailAllowed": true, "accessTokenLifespan": 300, "clients": [ { "clientId": "auth-server", "protocol": "openid-connect", "publicClient": false, "standardFlowEnabled": true, "redirectUris": ["https://auth-server.example.com/*"], "webOrigins": ["https://auth-server.example.com"] } ], "roles": { "realm": [ {"name": "platform-admin"}, {"name": "platform-user"} ] } } --- apiVersion: k8s.keycloak.org/v2alpha1 kind: KeycloakRealmImport metadata: name: platform-realm namespace: keycloak spec: keycloakCRName: keycloak realm: realm: platform enabled: true sslRequired: external registrationAllowed: false loginWithEmailAllowed: true accessTokenLifespan: 300 ``` **왜 좋은가:** - Realm을 선언적으로 관리 (GitOps 연계) - Operator가 `keycloak` CR ready 이후 server-side import Job을 자동 생성 - client secret처럼 민감한 값은 별도 Vault 경로로 분리, realm JSON은 Git 안전 --- ## 나쁜 예시 5: kcadm.sh pipeline 직접 호출 ```bash # CI pipeline kcadm.sh config credentials \ --server https://auth.example.com \ --realm master \ --user admin \ --password $KEYCLOAK_ADMIN_PASSWORD kcadm.sh create realms -s realm=platform -s enabled=true kcadm.sh create clients -r platform -s clientId=auth-server ``` **문제:** - 상태가 선언적이지 않아 drift 탐지 불가 - admin credential이 CI runner 환경에 상주 - 실패 시 재실행 안전성(idempotency) 없음 - GitOps 원칙과 충돌 --- ## 좋은 예시 7: SecurityContext + Resource ```yaml securityContext: runAsNonRoot: true runAsUser: 1000 runAsGroup: 1000 fsGroup: 1000 seccompProfile: type: RuntimeDefault containers: - name: keycloak securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: drop: - ALL env: - name: JAVA_OPTS_APPEND value: "-XX:MaxRAMPercentage=70 -XX:InitialRAMPercentage=50 -Djgroups.dns.query=keycloak-headless.keycloak.svc.cluster.local" resources: requests: cpu: 500m memory: 1Gi limits: cpu: "2" memory: 2Gi volumeMounts: - name: tmp mountPath: /tmp - name: data mountPath: /opt/keycloak/data volumes: - name: tmp emptyDir: {} - name: data emptyDir: {} ``` **왜 좋은가:** - Restricted PSS 전부 충족: non-root, no privilege escalation, RO root fs, cap drop ALL - `MaxRAMPercentage=70`은 JVM이 container limit의 70%까지만 heap 사용 (나머지는 direct memory / metaspace) - `readOnlyRootFilesystem: true` + emptyDir 마운트로 runtime write path 격리 --- ## 좋은 예시 8: Vault에서 DB credential 주입 (VSO) ```yaml --- apiVersion: secrets.hashicorp.com/v1beta1 kind: VaultStaticSecret metadata: name: keycloak-db namespace: keycloak spec: vaultAuthRef: default mount: kv path: keycloak/db type: kv-v2 refreshAfter: 1h destination: name: keycloak-db-secret create: true overwrite: true transformation: excludeRaw: true templates: username: text: '{{ .Secrets.username }}' password: text: '{{ .Secrets.password }}' ``` **왜 좋은가:** - Vault KV v2의 `keycloak/db`에서 credential을 K8s Secret으로 동기화 - 1시간 주기 refresh, VSO가 Pod를 재시작시켜 rotation 적용 가능 (별도 `rolloutRestartTargets` 설정 시) - Git에 평문 credential이 없다 --- ## 나쁜 예시 6: env에 평문 credential ```yaml env: - name: KC_DB_PASSWORD value: "SuperSecret123!" - name: KEYCLOAK_ADMIN_PASSWORD value: "admin" ``` **문제:** - Git에 평문 저장 → 권한 있는 모든 인원이 조회 가능 - 기본 `admin/admin` credential → bootstrap 직후 자동화된 스캐너에 탈취 위험 - rotation 경로 없음