f9900a5fb9
configmap-realm.yaml carried fourteen client secrets and five user
passwords in clear, in a tracked file. Five were there from its first
commit; I added the fourteenth yesterday by following the existing pattern
without questioning it. Making this file authoritative on every start did
not increase the exposure, but it did make the file the place those
secrets live for good — and it is inconsistent with the rest of the
estate, where infra-observability and kong-v2 both take theirs from Vault.
Keycloak resolves ${PLACEHOLDER} in a realm import file from the
environment, so the structure stays in git and the values move to Vault at
secrets/keycloak/classic-models-realm, reaching the import container
through External Secrets.
A store of its own, vault-keycloak, rather than reusing vault-kong-v2 or
vault-observability: each carries a policy scoped to its own path, so a
reader for one estate cannot see another's. Widening an existing policy
would have handed Kong's reader every client secret in the realm.
dataFrom.extract pulls the whole path, so adding a client means adding its
secret in Vault with no edit to the ExternalSecret.
Verified after the switch: the import logs "Realm 'classic-models' imported"
under OVERWRITE_EXISTING, and all three credential paths still work — the
broker's client_credentials, librechat with pm-user's password, and the
Catalogue agent's exchange reaching catalog-mcp with 200. Re-checked that
none of the nineteen original values appears anywhere in the file.
Note the values themselves were committed and are in this repo's history.
They are lab credentials behind a private CA with no real data, but they
should be rotated if that ever stops being true.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
289 lines
10 KiB
YAML
289 lines
10 KiB
YAML
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: keycloak
|
|
namespace: keycloak
|
|
labels:
|
|
app.kubernetes.io/name: keycloak
|
|
app.kubernetes.io/component: idp
|
|
app.kubernetes.io/part-of: keycloak
|
|
spec:
|
|
replicas: 1
|
|
selector:
|
|
matchLabels:
|
|
app: keycloak
|
|
template:
|
|
metadata:
|
|
labels:
|
|
app: keycloak
|
|
app.kubernetes.io/name: keycloak
|
|
app.kubernetes.io/component: idp
|
|
app.kubernetes.io/part-of: keycloak
|
|
annotations:
|
|
# Discovered by the Prometheus in the observability namespace.
|
|
# Endpoint exposed by KC_METRICS_ENABLED=true, on the management
|
|
# port (9000), never on the application port.
|
|
prometheus.io/scrape: "true"
|
|
prometheus.io/port: "9000"
|
|
prometheus.io/path: "/metrics"
|
|
spec:
|
|
initContainers:
|
|
# Re-imports the realm from configmap-realm.yaml on every start, so that
|
|
# file is the source of truth rather than a first-install seed.
|
|
#
|
|
# It has to be a separate command: `start --import-realm` skips a realm
|
|
# that already exists and there is no option to change that — the
|
|
# --override flag exists only on the standalone `import` command. With
|
|
# the default behaviour, editing the ConfigMap changed nothing on a
|
|
# restart and the truth drifted into the database. It already had: four
|
|
# scope grants existed only in the running realm.
|
|
#
|
|
# What this costs, deliberately: the realm is replaced at every start,
|
|
# so anything created at runtime and not written back to the file is
|
|
# lost, and active sessions are dropped. Users survive only because they
|
|
# are declared in the file. Create nothing through the admin console
|
|
# expecting it to last.
|
|
- name: import-realm
|
|
image: quay.io/keycloak/keycloak:26.6.0
|
|
args:
|
|
- "import"
|
|
- "--dir"
|
|
- "/opt/keycloak/data/import"
|
|
- "--override"
|
|
- "true"
|
|
env:
|
|
- name: KC_DB
|
|
value: "postgres"
|
|
- name: KC_DB_URL_HOST
|
|
value: "keycloak-postgresql"
|
|
- name: KC_DB_URL_PORT
|
|
value: "5432"
|
|
- name: KC_DB_URL_DATABASE
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: keycloak-secrets
|
|
key: POSTGRES_DB
|
|
- name: KC_DB_USERNAME
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: keycloak-secrets
|
|
key: KC_DB_USERNAME
|
|
- name: KC_DB_PASSWORD
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: keycloak-secrets
|
|
key: KC_DB_PASSWORD
|
|
- name: KC_BOOTSTRAP_ADMIN_USERNAME
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: keycloak-secrets
|
|
key: KC_BOOTSTRAP_ADMIN_USERNAME
|
|
- name: KC_BOOTSTRAP_ADMIN_PASSWORD
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: keycloak-secrets
|
|
key: KC_BOOTSTRAP_ADMIN_PASSWORD
|
|
- name: KEYCLOAK_ADMIN
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: keycloak-secrets
|
|
key: KEYCLOAK_ADMIN
|
|
- name: KEYCLOAK_ADMIN_PASSWORD
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: keycloak-secrets
|
|
key: KEYCLOAK_ADMIN_PASSWORD
|
|
- name: KC_HTTP_ENABLED
|
|
value: "true"
|
|
- name: KC_PROXY_HEADERS
|
|
value: "xforwarded"
|
|
- name: KC_HOSTNAME
|
|
value: "https://keycloak.sttlab.local"
|
|
- name: KC_HOSTNAME_STRICT
|
|
value: "false"
|
|
- name: KC_HOSTNAME_BACKCHANNEL_DYNAMIC
|
|
value: "false"
|
|
- name: KC_FEATURES
|
|
value: "token-exchange,admin-fine-grained-authz,log-mdc"
|
|
# Emptied for this container only: it inherits the main container's env,
|
|
# which loads the OpenTelemetry agent from a volume the other init
|
|
# container downloads into and this one does not mount. Inheriting it
|
|
# verbatim fails the JVM before the import runs.
|
|
- name: JAVA_OPTS_APPEND
|
|
value: ""
|
|
- name: OTEL_SERVICE_NAME
|
|
value: "keycloak"
|
|
- name: OTEL_EXPORTER_OTLP_ENDPOINT
|
|
value: "http://tempo.observability.svc.cluster.local:4318"
|
|
- name: OTEL_EXPORTER_OTLP_PROTOCOL
|
|
value: "http/protobuf"
|
|
- name: OTEL_TRACES_EXPORTER
|
|
value: "otlp"
|
|
- name: OTEL_METRICS_EXPORTER
|
|
value: "none"
|
|
- name: OTEL_LOGS_EXPORTER
|
|
value: "none"
|
|
- name: KC_HTTP_ACCESS_LOG_ENABLED
|
|
value: "true"
|
|
- name: KC_LOG_LEVEL
|
|
value: "INFO,org.keycloak.events:DEBUG"
|
|
- name: KC_LOG_CONSOLE_OUTPUT
|
|
value: "json"
|
|
- name: KC_LOG_MDC_ENABLED
|
|
value: "true"
|
|
- name: KC_LOG_SERVICE_NAME
|
|
value: "keycloak"
|
|
- name: KC_LOG_SERVICE_ENVIRONMENT
|
|
value: "local"
|
|
- name: KC_HEALTH_ENABLED
|
|
value: "true"
|
|
- name: KC_METRICS_ENABLED
|
|
value: "true"
|
|
volumeMounts:
|
|
- name: realm-import
|
|
mountPath: /opt/keycloak/data/import
|
|
envFrom:
|
|
# Supplies the ${KC_SECRET_*} and ${KC_USER_*} placeholders the realm
|
|
# file carries in place of the values it used to hold in clear.
|
|
# Keycloak resolves them from the environment at import time.
|
|
- secretRef:
|
|
name: keycloak-realm-secrets
|
|
- name: download-otel-agent
|
|
image: curlimages/curl:8.10.0
|
|
command:
|
|
- "sh"
|
|
- "-c"
|
|
- "curl -sSL -o /otel/opentelemetry-javaagent.jar https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/download/v2.31.0/opentelemetry-javaagent.jar"
|
|
volumeMounts:
|
|
- name: otel-agent
|
|
mountPath: /otel
|
|
containers:
|
|
- name: keycloak
|
|
image: quay.io/keycloak/keycloak:26.6.0
|
|
args:
|
|
- "start"
|
|
ports:
|
|
- name: http
|
|
containerPort: 8080
|
|
- name: https
|
|
containerPort: 8443
|
|
- name: management
|
|
containerPort: 9000
|
|
env:
|
|
- name: KC_DB
|
|
value: "postgres"
|
|
- name: KC_DB_URL_HOST
|
|
value: "keycloak-postgresql"
|
|
- name: KC_DB_URL_PORT
|
|
value: "5432"
|
|
- name: KC_DB_URL_DATABASE
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: keycloak-secrets
|
|
key: POSTGRES_DB
|
|
- name: KC_DB_USERNAME
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: keycloak-secrets
|
|
key: KC_DB_USERNAME
|
|
- name: KC_DB_PASSWORD
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: keycloak-secrets
|
|
key: KC_DB_PASSWORD
|
|
- name: KC_BOOTSTRAP_ADMIN_USERNAME
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: keycloak-secrets
|
|
key: KC_BOOTSTRAP_ADMIN_USERNAME
|
|
- name: KC_BOOTSTRAP_ADMIN_PASSWORD
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: keycloak-secrets
|
|
key: KC_BOOTSTRAP_ADMIN_PASSWORD
|
|
- name: KEYCLOAK_ADMIN
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: keycloak-secrets
|
|
key: KEYCLOAK_ADMIN
|
|
- name: KEYCLOAK_ADMIN_PASSWORD
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: keycloak-secrets
|
|
key: KEYCLOAK_ADMIN_PASSWORD
|
|
- name: KC_HTTP_ENABLED
|
|
value: "true"
|
|
- name: KC_PROXY_HEADERS
|
|
value: "xforwarded"
|
|
- name: KC_HOSTNAME
|
|
value: "https://keycloak.sttlab.local"
|
|
- name: KC_HOSTNAME_STRICT
|
|
value: "false"
|
|
- name: KC_HOSTNAME_BACKCHANNEL_DYNAMIC
|
|
value: "false"
|
|
- name: KC_FEATURES
|
|
value: "token-exchange,admin-fine-grained-authz,log-mdc"
|
|
- name: JAVA_OPTS_APPEND
|
|
value: "-javaagent:/otel/opentelemetry-javaagent.jar"
|
|
- name: OTEL_SERVICE_NAME
|
|
value: "keycloak"
|
|
- name: OTEL_EXPORTER_OTLP_ENDPOINT
|
|
value: "http://tempo.observability.svc.cluster.local:4318"
|
|
- name: OTEL_EXPORTER_OTLP_PROTOCOL
|
|
value: "http/protobuf"
|
|
- name: OTEL_TRACES_EXPORTER
|
|
value: "otlp"
|
|
- name: OTEL_METRICS_EXPORTER
|
|
value: "none"
|
|
- name: OTEL_LOGS_EXPORTER
|
|
value: "none"
|
|
- name: KC_HTTP_ACCESS_LOG_ENABLED
|
|
value: "true"
|
|
- name: KC_LOG_LEVEL
|
|
value: "INFO,org.keycloak.events:DEBUG"
|
|
- name: KC_LOG_CONSOLE_OUTPUT
|
|
value: "json"
|
|
- name: KC_LOG_MDC_ENABLED
|
|
value: "true"
|
|
- name: KC_LOG_SERVICE_NAME
|
|
value: "keycloak"
|
|
- name: KC_LOG_SERVICE_ENVIRONMENT
|
|
value: "local"
|
|
- name: KC_HEALTH_ENABLED
|
|
value: "true"
|
|
- name: KC_METRICS_ENABLED
|
|
value: "true"
|
|
readinessProbe:
|
|
httpGet:
|
|
path: /health/ready
|
|
port: 9000
|
|
initialDelaySeconds: 15
|
|
periodSeconds: 10
|
|
failureThreshold: 5
|
|
livenessProbe:
|
|
httpGet:
|
|
path: /health/live
|
|
port: 9000
|
|
initialDelaySeconds: 30
|
|
periodSeconds: 15
|
|
failureThreshold: 5
|
|
resources:
|
|
requests:
|
|
cpu: 500m
|
|
memory: 1024Mi
|
|
limits:
|
|
cpu: 2000m
|
|
memory: 2048Mi
|
|
volumeMounts:
|
|
- name: realm-import
|
|
mountPath: /opt/keycloak/data/import
|
|
readOnly: true
|
|
- name: otel-agent
|
|
mountPath: /otel
|
|
volumes:
|
|
- name: realm-import
|
|
configMap:
|
|
name: keycloak-realm-classic-models
|
|
- name: otel-agent
|
|
emptyDir: {}
|