diff --git a/AuthBridge/client-registration/example_deployment.yaml b/AuthBridge/client-registration/example_deployment.yaml index fbbf4bf24..c2dfa742a 100644 --- a/AuthBridge/client-registration/example_deployment.yaml +++ b/AuthBridge/client-registration/example_deployment.yaml @@ -10,14 +10,24 @@ metadata: namespace: my-agent data: SPIRE_ENABLED: "false" - # Replace these placeholder values with real endpoints/credentials + # Replace these placeholder values with real endpoints KEYCLOAK_URL: "http://keycloak.keycloak.svc:8080" KEYCLOAK_REALM: "master" - KEYCLOAK_ADMIN_USERNAME: "admin" - KEYCLOAK_ADMIN_PASSWORD: "admin" KEYCLOAK_TOKEN_EXCHANGE_ENABLED: "true" KEYCLOAK_CLIENT_REGISTRATION_ENABLED: "true" --- +# keycloak-admin-secret - Keycloak admin credentials for client registration +# Replace the placeholder values with your actual admin credentials. +apiVersion: v1 +kind: Secret +metadata: + name: keycloak-admin-secret + namespace: my-agent +type: Opaque +stringData: + KEYCLOAK_ADMIN_USERNAME: "admin" + KEYCLOAK_ADMIN_PASSWORD: "changeme" +--- apiVersion: v1 kind: ServiceAccount metadata: @@ -77,13 +87,13 @@ spec: key: KEYCLOAK_REALM - name: KEYCLOAK_ADMIN_USERNAME valueFrom: - configMapKeyRef: - name: environments + secretKeyRef: + name: keycloak-admin-secret key: KEYCLOAK_ADMIN_USERNAME - name: KEYCLOAK_ADMIN_PASSWORD valueFrom: - configMapKeyRef: - name: environments + secretKeyRef: + name: keycloak-admin-secret key: KEYCLOAK_ADMIN_PASSWORD - name: CLIENT_NAME value: my-app diff --git a/AuthBridge/client-registration/example_deployment_spiffe.yaml b/AuthBridge/client-registration/example_deployment_spiffe.yaml index 327838a94..e1327b747 100644 --- a/AuthBridge/client-registration/example_deployment_spiffe.yaml +++ b/AuthBridge/client-registration/example_deployment_spiffe.yaml @@ -10,14 +10,24 @@ metadata: namespace: my-agent data: SPIRE_ENABLED: "true" - # Replace these placeholder values with real endpoints/credentials + # Replace these placeholder values with real endpoints KEYCLOAK_URL: "http://keycloak.keycloak.svc:8080" KEYCLOAK_REALM: "master" - KEYCLOAK_ADMIN_USERNAME: "admin" - KEYCLOAK_ADMIN_PASSWORD: "admin" KEYCLOAK_TOKEN_EXCHANGE_ENABLED: "true" KEYCLOAK_CLIENT_REGISTRATION_ENABLED: "true" --- +# keycloak-admin-secret - Keycloak admin credentials for client registration +# Replace the placeholder values with your actual admin credentials. +apiVersion: v1 +kind: Secret +metadata: + name: keycloak-admin-secret + namespace: my-agent +type: Opaque +stringData: + KEYCLOAK_ADMIN_USERNAME: "admin" + KEYCLOAK_ADMIN_PASSWORD: "changeme" +--- apiVersion: v1 kind: ServiceAccount metadata: @@ -91,13 +101,13 @@ spec: key: KEYCLOAK_REALM - name: KEYCLOAK_ADMIN_USERNAME valueFrom: - configMapKeyRef: - name: environments + secretKeyRef: + name: keycloak-admin-secret key: KEYCLOAK_ADMIN_USERNAME - name: KEYCLOAK_ADMIN_PASSWORD valueFrom: - configMapKeyRef: - name: environments + secretKeyRef: + name: keycloak-admin-secret key: KEYCLOAK_ADMIN_PASSWORD - name: CLIENT_NAME value: my-app diff --git a/AuthBridge/demos/github-issue/demo-manual.md b/AuthBridge/demos/github-issue/demo-manual.md index 40bbde8d0..b67459718 100644 --- a/AuthBridge/demos/github-issue/demo-manual.md +++ b/AuthBridge/demos/github-issue/demo-manual.md @@ -282,13 +282,26 @@ This creates: --- -## Step 3: Apply Demo ConfigMaps +## Step 3: Create Keycloak Admin Secret and Apply Demo ConfigMaps The Kagenti installer creates default ConfigMaps (`environments`, `spiffe-helper-config`, `envoy-config`, `authbridge-config`) with the correct -`kagenti` realm settings and 300s Envoy timeouts. This step overrides only -`authbridge-config` with demo-specific values — the token exchange target -audience (`github-tool`), scopes, and the agent's SPIFFE ID for inbound +`kagenti` realm settings and 300s Envoy timeouts. + +The client-registration sidecar needs Keycloak admin credentials to register +agents as OAuth clients. These are stored in a Kubernetes Secret (not a +ConfigMap) to follow security best practices. Create the secret **before** +deploying the agent: + +```bash +kubectl create secret generic keycloak-admin-secret -n team1 \ + --from-literal=KEYCLOAK_ADMIN_USERNAME=admin \ + --from-literal=KEYCLOAK_ADMIN_PASSWORD=admin \ + --dry-run=client -o yaml | kubectl apply -f - +``` + +Then apply the demo-specific `authbridge-config` override — the token exchange +target audience (`github-tool`), scopes, and the agent's SPIFFE ID for inbound audience validation. Apply this **before** deploying the agent. ```bash @@ -806,17 +819,20 @@ kubectl delete pod test-client -n team1 --ignore-not-found **Symptom:** `{"error":"invalid_client","error_description":"Invalid client or Invalid client credentials"}` -**Cause:** The agent pod's `environments` ConfigMap was missing or incorrect at startup, -so the client-registration sidecar registered the client with wrong settings. +**Cause:** The `keycloak-admin-secret` Secret or `environments` ConfigMap was missing +or incorrect at startup, so the client-registration sidecar couldn't register the client. **Fix:** ```bash -# 1. Verify the installer's environments ConfigMap has the correct realm +# 1. Verify the keycloak-admin-secret exists +kubectl get secret keycloak-admin-secret -n team1 + +# 2. Verify the installer's environments ConfigMap has the correct realm kubectl get configmap environments -n team1 -o jsonpath='{.data.KEYCLOAK_REALM}' # Should show: kagenti -# 2. Re-apply the demo ConfigMap and restart +# 3. Re-apply the demo ConfigMap and restart kubectl apply -f demos/github-issue/k8s/configmaps.yaml kubectl rollout restart deployment/git-issue-agent -n team1 ``` diff --git a/AuthBridge/demos/github-issue/demo-ui.md b/AuthBridge/demos/github-issue/demo-ui.md index 0485b6b15..b4b0d5e86 100644 --- a/AuthBridge/demos/github-issue/demo-ui.md +++ b/AuthBridge/demos/github-issue/demo-ui.md @@ -173,14 +173,26 @@ This creates: --- -## Step 2: Apply Demo ConfigMaps +## Step 2: Create Keycloak Admin Secret and Apply Demo ConfigMaps The Kagenti installer creates default ConfigMaps (`environments`, `spiffe-helper-config`, `envoy-config`, `authbridge-config`) with the correct -`kagenti` realm settings and 300s Envoy timeouts. This step overrides only -`authbridge-config` with demo-specific values — the token exchange target -audience (`github-tool`), scopes, and the agent's SPIFFE ID for inbound -audience validation. +`kagenti` realm settings and 300s Envoy timeouts. + +The client-registration sidecar needs Keycloak admin credentials to register +agents as OAuth clients. These are stored in a Kubernetes Secret (not a +ConfigMap) to follow security best practices: + +```bash +kubectl create secret generic keycloak-admin-secret -n team1 \ + --from-literal=KEYCLOAK_ADMIN_USERNAME=admin \ + --from-literal=KEYCLOAK_ADMIN_PASSWORD=admin \ + --dry-run=client -o yaml | kubectl apply -f - +``` + +Then apply the demo-specific `authbridge-config` override — the token exchange +target audience (`github-tool`), scopes, and the agent's SPIFFE ID for inbound +audience validation: ```bash cd AuthBridge @@ -682,17 +694,20 @@ kubectl rollout status deployment/git-issue-agent -n team1 --timeout=180s **Symptom:** `{"error":"invalid_client","error_description":"Invalid client or Invalid client credentials"}` -**Cause:** The agent pod's `environments` ConfigMap was missing or incorrect at startup, -so the client-registration sidecar registered the client with wrong settings. +**Cause:** The `keycloak-admin-secret` Secret or `environments` ConfigMap was missing +or incorrect at startup, so the client-registration sidecar couldn't register the client. **Fix:** ```bash -# 1. Verify the installer's environments ConfigMap has the correct realm +# 1. Verify the keycloak-admin-secret exists +kubectl get secret keycloak-admin-secret -n team1 + +# 2. Verify the installer's environments ConfigMap has the correct realm kubectl get configmap environments -n team1 -o jsonpath='{.data.KEYCLOAK_REALM}' # Should show: kagenti -# 2. Re-apply the demo ConfigMap and restart +# 3. Re-apply the demo ConfigMap and restart kubectl apply -f demos/github-issue/k8s/configmaps.yaml kubectl rollout restart deployment/git-issue-agent -n team1 ``` diff --git a/AuthBridge/demos/webhook/k8s/configmaps-webhook.yaml b/AuthBridge/demos/webhook/k8s/configmaps-webhook.yaml index 6cb6f6b33..d72f3c566 100644 --- a/AuthBridge/demos/webhook/k8s/configmaps-webhook.yaml +++ b/AuthBridge/demos/webhook/k8s/configmaps-webhook.yaml @@ -17,17 +17,25 @@ data: SPIRE_ENABLED: "true" KEYCLOAK_URL: "http://keycloak-service.keycloak.svc:8080" KEYCLOAK_REALM: "demo" - # Demo/development only: do NOT use hardcoded admin credentials in production. - # For production deployments, supply these via a Kubernetes Secret and reference - # them from your Pod/Deployment using env.valueFrom.secretKeyRef instead of this ConfigMap. - KEYCLOAK_ADMIN_USERNAME: "admin" - KEYCLOAK_ADMIN_PASSWORD: "admin" # Comma-separated list of Keycloak client IDs that should receive the agent's # audience scope. This allows existing clients (like the UI) to include the # agent's audience in their tokens without manual Keycloak configuration. # Default: "kagenti" (the platform UI client) # PLATFORM_CLIENT_IDS: "kagenti" +--- +# keycloak-admin-secret - Keycloak admin credentials for client registration +# Replace the placeholder values with your actual admin credentials. +apiVersion: v1 +kind: Secret +metadata: + name: keycloak-admin-secret + namespace: team1 +type: Opaque +stringData: + KEYCLOAK_ADMIN_USERNAME: "admin" + KEYCLOAK_ADMIN_PASSWORD: "admin" + --- # authbridge-config ConfigMap - Used by envoy-proxy for token exchange and inbound validation apiVersion: v1 diff --git a/CLAUDE.md b/CLAUDE.md index ce27fc46a..2e9cfdc6e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -196,12 +196,13 @@ Hooks: When the webhook injects sidecars, the target namespace needs these ConfigMaps: -| ConfigMap | Used by | Keys | -|-----------|---------|------| -| `environments` | client-registration | `KEYCLOAK_URL`, `KEYCLOAK_REALM`, `KEYCLOAK_ADMIN_USERNAME`, `KEYCLOAK_ADMIN_PASSWORD` | -| `authbridge-config` | envoy-proxy (ext-proc) | `TOKEN_URL`, `ISSUER`, `TARGET_AUDIENCE`, `TARGET_SCOPES` | -| `spiffe-helper-config` | spiffe-helper | SPIFFE helper configuration file | -| `envoy-config` | envoy-proxy | Envoy YAML configuration | +| Resource | Kind | Used by | Keys | +|----------|------|---------|------| +| `environments` | ConfigMap | client-registration | `KEYCLOAK_URL`, `KEYCLOAK_REALM` | +| `keycloak-admin-secret` | Secret | client-registration | `KEYCLOAK_ADMIN_USERNAME`, `KEYCLOAK_ADMIN_PASSWORD` | +| `authbridge-config` | ConfigMap | envoy-proxy (ext-proc) | `TOKEN_URL`, `ISSUER`, `TARGET_AUDIENCE`, `TARGET_SCOPES` | +| `spiffe-helper-config` | ConfigMap | spiffe-helper | SPIFFE helper configuration file | +| `envoy-config` | ConfigMap | envoy-proxy | Envoy YAML configuration | ## Common Development Tasks diff --git a/charts/kagenti-webhook/templates/clusterrole.yaml b/charts/kagenti-webhook/templates/clusterrole.yaml index 780ee2705..6056c0401 100644 --- a/charts/kagenti-webhook/templates/clusterrole.yaml +++ b/charts/kagenti-webhook/templates/clusterrole.yaml @@ -15,6 +15,9 @@ rules: - apiGroups: [""] resources: ["configmaps"] verbs: ["get", "list", "watch"] +- apiGroups: [""] + resources: ["secrets"] + verbs: ["get", "list", "watch"] - apiGroups: [""] resources: ["serviceaccounts"] verbs: ["get", "create", "update"] diff --git a/kagenti-webhook/CLAUDE.md b/kagenti-webhook/CLAUDE.md index 6ee4a72f2..fd7f3770b 100644 --- a/kagenti-webhook/CLAUDE.md +++ b/kagenti-webhook/CLAUDE.md @@ -149,13 +149,18 @@ make generate - **Container existence checks**: `containerExists()` and `volumeExists()` helpers prevent duplicate injection. - **Kubebuilder markers**: Webhook path markers (e.g., `+kubebuilder:webhook:path=...`) in Go comments generate the webhook manifests. Do not change these without running `make manifests`. -### ConfigMap Dependencies at Runtime -Injected sidecars expect these ConfigMaps to exist in the target namespace: -- `environments` -- `KEYCLOAK_URL`, `KEYCLOAK_REALM`, `KEYCLOAK_ADMIN_USERNAME`, `KEYCLOAK_ADMIN_PASSWORD` +### Runtime Dependencies +Injected sidecars expect these resources to exist in the target namespace: + +ConfigMaps: +- `environments` -- `KEYCLOAK_URL`, `KEYCLOAK_REALM` - `authbridge-config` -- `TOKEN_URL`, `ISSUER`, `TARGET_AUDIENCE`, `TARGET_SCOPES` - `spiffe-helper-config` -- SPIFFE helper configuration (when SPIRE is enabled) - `envoy-config` -- Envoy proxy configuration +Secrets: +- `keycloak-admin-secret` -- `KEYCLOAK_ADMIN_USERNAME`, `KEYCLOAK_ADMIN_PASSWORD` + ### Security Model - `proxy-init` runs as an init container with a short lifetime (iptables setup). - `envoy-proxy` runs as UID 1337. diff --git a/kagenti-webhook/config/rbac/role.yaml b/kagenti-webhook/config/rbac/role.yaml index 36af9f0a5..77114f2d2 100644 --- a/kagenti-webhook/config/rbac/role.yaml +++ b/kagenti-webhook/config/rbac/role.yaml @@ -15,6 +15,9 @@ rules: - apiGroups: [""] resources: ["configmaps"] verbs: ["get", "list", "watch"] +- apiGroups: [""] + resources: ["secrets"] + verbs: ["get", "list", "watch"] - apiGroups: [""] resources: ["serviceaccounts"] verbs: ["get", "create", "update"] diff --git a/kagenti-webhook/internal/webhook/injector/container_builder.go b/kagenti-webhook/internal/webhook/injector/container_builder.go index 5e08d8544..c5a79a014 100644 --- a/kagenti-webhook/internal/webhook/injector/container_builder.go +++ b/kagenti-webhook/internal/webhook/injector/container_builder.go @@ -135,9 +135,9 @@ func (b *ContainerBuilder) BuildClientRegistrationContainerWithSpireOption(name, { Name: "KEYCLOAK_ADMIN_USERNAME", ValueFrom: &corev1.EnvVarSource{ - ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ + SecretKeyRef: &corev1.SecretKeySelector{ LocalObjectReference: corev1.LocalObjectReference{ - Name: "environments", + Name: "keycloak-admin-secret", }, Key: "KEYCLOAK_ADMIN_USERNAME", }, @@ -146,9 +146,9 @@ func (b *ContainerBuilder) BuildClientRegistrationContainerWithSpireOption(name, { Name: "KEYCLOAK_ADMIN_PASSWORD", ValueFrom: &corev1.EnvVarSource{ - ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ + SecretKeyRef: &corev1.SecretKeySelector{ LocalObjectReference: corev1.LocalObjectReference{ - Name: "environments", + Name: "keycloak-admin-secret", }, Key: "KEYCLOAK_ADMIN_PASSWORD", }, diff --git a/kagenti-webhook/internal/webhook/injector/container_builder_test.go b/kagenti-webhook/internal/webhook/injector/container_builder_test.go index 6a06f1cf0..72efa94fc 100644 --- a/kagenti-webhook/internal/webhook/injector/container_builder_test.go +++ b/kagenti-webhook/internal/webhook/injector/container_builder_test.go @@ -134,3 +134,55 @@ func TestBuildClientRegistrationContainer_HasPlatformClientIDsEnv(t *testing.T) t.Error("client-registration container missing PLATFORM_CLIENT_IDS env var") } } + +func TestBuildClientRegistrationContainer_AdminCredentialsFromSecret(t *testing.T) { + builder := NewContainerBuilder(config.CompiledDefaults()) + container := builder.BuildClientRegistrationContainerWithSpireOption("my-app", "my-ns", true) + + sensitiveKeys := []string{"KEYCLOAK_ADMIN_USERNAME", "KEYCLOAK_ADMIN_PASSWORD"} + for _, key := range sensitiveKeys { + found := false + for _, env := range container.Env { + if env.Name != key { + continue + } + found = true + if env.ValueFrom == nil || env.ValueFrom.SecretKeyRef == nil { + t.Errorf("env %q must use SecretKeyRef, got ConfigMapKeyRef or literal", key) + continue + } + if env.ValueFrom.SecretKeyRef.Name != "keycloak-admin-secret" { + t.Errorf("env %q SecretKeyRef.Name = %q, want %q", key, env.ValueFrom.SecretKeyRef.Name, "keycloak-admin-secret") + } + } + if !found { + t.Errorf("client-registration container missing env var %q", key) + } + } +} + +func TestBuildClientRegistrationContainer_NonSensitiveKeysFromConfigMap(t *testing.T) { + builder := NewContainerBuilder(config.CompiledDefaults()) + container := builder.BuildClientRegistrationContainerWithSpireOption("my-app", "my-ns", true) + + nonSensitiveKeys := []string{"KEYCLOAK_URL", "KEYCLOAK_REALM"} + for _, key := range nonSensitiveKeys { + found := false + for _, env := range container.Env { + if env.Name != key { + continue + } + found = true + if env.ValueFrom == nil || env.ValueFrom.ConfigMapKeyRef == nil { + t.Errorf("env %q must use ConfigMapKeyRef", key) + continue + } + if env.ValueFrom.ConfigMapKeyRef.Name != "environments" { + t.Errorf("env %q ConfigMapKeyRef.Name = %q, want %q", key, env.ValueFrom.ConfigMapKeyRef.Name, "environments") + } + } + if !found { + t.Errorf("client-registration container missing env var %q", key) + } + } +}