init: 폴더구조 설계 및 인프라 설계
This commit is contained in:
@@ -0,0 +1,668 @@
|
||||
# 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
|
||||
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 경로 없음
|
||||
Reference in New Issue
Block a user