15 KiB
15 KiB
Keycloak 예시
Keycloak 26+ (Quarkus distribution) + Keycloak Operator 기준. 모든 YAML은 그대로 kubectl apply로 적용 가능한 완전한 manifest다.
좋은 예시 1: optimized 이미지 빌드 (두 단계)
kc.sh build로 Quarkus augmentation을 굽고, 실행 이미지를 분리한다.
# 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
args:
- start-dev
또는
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차 권장)
---
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
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)
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 / 로 설정
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
---
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 분리
---
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 노출
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
---
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가
keycloakCR ready 이후 server-side import Job을 자동 생성 - client secret처럼 민감한 값은 별도 Vault 경로로 분리, realm JSON은 Git 안전
나쁜 예시 5: kcadm.sh pipeline 직접 호출
# 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
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)
---
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
env:
- name: KC_DB_PASSWORD
value: "SuperSecret123!"
- name: KEYCLOAK_ADMIN_PASSWORD
value: "admin"
문제:
- Git에 평문 저장 → 권한 있는 모든 인원이 조회 가능
- 기본
admin/admincredential → bootstrap 직후 자동화된 스캐너에 탈취 위험 - rotation 경로 없음