From 34fd7a25cbf85e8737d5c67de092a7037b2c129e Mon Sep 17 00:00:00 2001 From: John Sell Date: Tue, 9 Jun 2026 11:00:39 -0400 Subject: [PATCH 1/5] feat(manifests): enable RBAC authorization in SaaS template Flip --enable-authz from false to true now that the RBAC enforcement middleware has landed (PR #1660). Already live-patched on hcmais. Co-Authored-By: Claude Sonnet 4.6 --- components/manifests/templates/template-services.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/manifests/templates/template-services.yaml b/components/manifests/templates/template-services.yaml index ebfdd46f6..c5fb5a8f2 100644 --- a/components/manifests/templates/template-services.yaml +++ b/components/manifests/templates/template-services.yaml @@ -230,7 +230,7 @@ objects: - --db-password-file=/secrets/db/db.password - --db-name-file=/secrets/db/db.name - --enable-jwt=true - - --enable-authz=false + - --enable-authz=true - --jwk-cert-file=/configs/authentication/jwks.json - --enable-https=false - --enable-grpc=true From 3acdd02736b039a7265653d5cfe034dc67018d1b Mon Sep 17 00:00:00 2001 From: John Sell Date: Tue, 9 Jun 2026 11:14:05 -0400 Subject: [PATCH 2/5] fix(manifests): add jwk-cert-url flag, harden SaaS template security - Add --jwk-cert-url CLI flag pointing to Keycloak JWKS endpoint; without it the framework defaults to sso.redhat.com and rejects tokens signed by our Keycloak instance - Remove unused JWK_CERT_URL env var (framework reads the CLI flag) - Remove CREDENTIAL_ENCRYPTION_ALLOW_PLAINTEXT=true - Add readOnlyRootFilesystem: true to all container securityContexts Co-Authored-By: Claude Sonnet 4.6 --- components/manifests/templates/template-services.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/components/manifests/templates/template-services.yaml b/components/manifests/templates/template-services.yaml index c5fb5a8f2..949cb2902 100644 --- a/components/manifests/templates/template-services.yaml +++ b/components/manifests/templates/template-services.yaml @@ -105,6 +105,7 @@ objects: initialDelaySeconds: 30 periodSeconds: 30 securityContext: + readOnlyRootFilesystem: true allowPrivilegeEscalation: false capabilities: drop: @@ -189,6 +190,7 @@ objects: cpu: 500m memory: 512Mi securityContext: + readOnlyRootFilesystem: true allowPrivilegeEscalation: false capabilities: drop: @@ -205,8 +207,6 @@ objects: secretKeyRef: name: ambient-control-plane-token key: token - - name: JWK_CERT_URL - value: "${KEYCLOAK_REALM_URL}/protocol/openid-connect/certs" - name: CREDENTIAL_ENCRYPTION_KEYRING valueFrom: secretKeyRef: @@ -219,8 +219,6 @@ objects: name: credential-encryption-key key: version optional: true - - name: CREDENTIAL_ENCRYPTION_ALLOW_PLAINTEXT - value: "true" command: - /usr/local/bin/ambient-api-server - serve @@ -232,6 +230,7 @@ objects: - --enable-jwt=true - --enable-authz=true - --jwk-cert-file=/configs/authentication/jwks.json + - --jwk-cert-url=${KEYCLOAK_REALM_URL}/protocol/openid-connect/certs - --enable-https=false - --enable-grpc=true - --grpc-enable-tls=false @@ -295,6 +294,7 @@ objects: initialDelaySeconds: 20 periodSeconds: 10 securityContext: + readOnlyRootFilesystem: true allowPrivilegeEscalation: false capabilities: drop: From efb43f6f92f8ba07361e167aa009bbd0bc60d028 Mon Sep 17 00:00:00 2001 From: John Sell Date: Tue, 9 Jun 2026 11:28:24 -0400 Subject: [PATCH 3/5] feat(manifests): switch control-plane auth from static token to OIDC client credentials MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace AMBIENT_API_TOKEN (static bearer token) with OIDC client credentials (client_credentials grant) for control-plane → api-server authentication. The control-plane exchanges a Keycloak client-id and client-secret for short-lived JWTs, validated by the same JWKS path the api-server already uses for user tokens. - Update SaaS template: replace AMBIENT_API_TOKEN with OIDC_TOKEN_URL, OIDC_CLIENT_ID, OIDC_CLIENT_SECRET from ambient-control-plane-oidc secret - Remove AMBIENT_API_TOKEN from api-server env (no longer needed) - Update hcmais overlay with concrete Keycloak token URL - Add deployment prerequisites section to manifests README Co-Authored-By: Claude Sonnet 4.6 --- components/manifests/README.md | 26 +++++++++++++++++++ .../hcmais/control-plane-env-patch.yaml | 13 +++++++--- .../templates/template-services.yaml | 18 +++++++------ 3 files changed, 46 insertions(+), 11 deletions(-) diff --git a/components/manifests/README.md b/components/manifests/README.md index 41d00f90c..b673fcb5b 100644 --- a/components/manifests/README.md +++ b/components/manifests/README.md @@ -124,6 +124,32 @@ Components are opt-in kustomize modules included via the `components:` block in | `ambient-api-server-db` | Same RHEL patch for the ambient-api-server's dedicated DB | `production`, `local-dev` | | `postgresql-init-scripts` | ConfigMap + volume for DB init SQL (vanilla postgres only) | `kind`, `e2e` | +## Prerequisites for New Deployments + +Before deploying, create these secrets in the target namespace: + +### Control-plane OIDC credentials + +The control-plane authenticates to the api-server using Keycloak client credentials (OAuth2 `client_credentials` grant). Create a **confidential** Keycloak client with only the **Service accounts roles** flow enabled, then: + +```bash +oc create secret generic ambient-control-plane-oidc \ + -n \ + --from-literal=client-id= \ + --from-literal=client-secret= +``` + +### API server auth ConfigMap + +The api-server validates JWTs using keys from the Keycloak JWKS endpoint (configured via `--jwk-cert-url`). A local fallback is also loaded from a ConfigMap: + +```bash +oc create configmap ambient-api-server-auth \ + -n \ + --from-file=jwks.json=<(curl -s /protocol/openid-connect/certs) \ + --from-file=acl.yml=<(echo '- claim: email\n pattern: ^.*$') +``` + ## Building and Validating ```bash diff --git a/components/manifests/overlays/hcmais/control-plane-env-patch.yaml b/components/manifests/overlays/hcmais/control-plane-env-patch.yaml index d0841fabd..139adb9b3 100644 --- a/components/manifests/overlays/hcmais/control-plane-env-patch.yaml +++ b/components/manifests/overlays/hcmais/control-plane-env-patch.yaml @@ -16,8 +16,15 @@ spec: value: "false" - name: CP_TOKEN_URL value: "http://ambient-control-plane.ambient-api.svc:8080/token" - - name: AMBIENT_API_TOKEN + - name: OIDC_TOKEN_URL + value: "https://keycloak-ambient-keycloak.apps.rosa.hcmais01ue1.s9m2.p3.openshiftapps.com/realms/ambient-code/protocol/openid-connect/token" + - name: OIDC_CLIENT_ID valueFrom: secretKeyRef: - name: ambient-control-plane-token - key: token + name: ambient-control-plane-oidc + key: client-id + - name: OIDC_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: ambient-control-plane-oidc + key: client-secret diff --git a/components/manifests/templates/template-services.yaml b/components/manifests/templates/template-services.yaml index 949cb2902..b7dd064f7 100644 --- a/components/manifests/templates/template-services.yaml +++ b/components/manifests/templates/template-services.yaml @@ -202,11 +202,6 @@ objects: env: - name: AMBIENT_ENV value: production - - name: AMBIENT_API_TOKEN - valueFrom: - secretKeyRef: - name: ambient-control-plane-token - key: token - name: CREDENTIAL_ENCRYPTION_KEYRING valueFrom: secretKeyRef: @@ -375,11 +370,18 @@ objects: value: standard - name: LOG_LEVEL value: info - - name: AMBIENT_API_TOKEN + - name: OIDC_TOKEN_URL + value: "${KEYCLOAK_REALM_URL}/protocol/openid-connect/token" + - name: OIDC_CLIENT_ID + valueFrom: + secretKeyRef: + name: ambient-control-plane-oidc + key: client-id + - name: OIDC_CLIENT_SECRET valueFrom: secretKeyRef: - name: ambient-control-plane-token - key: token + name: ambient-control-plane-oidc + key: client-secret - name: AMBIENT_API_SERVER_URL value: "http://ambient-api-server.${NAMESPACE}.svc:8000" - name: AMBIENT_GRPC_SERVER_ADDR From 61894f1bea9878e4d6a691687441977762981feb Mon Sep 17 00:00:00 2001 From: John Sell Date: Tue, 9 Jun 2026 11:40:14 -0400 Subject: [PATCH 4/5] fix(manifests): add GRPC_SERVICE_ACCOUNT to api-server template The gRPC interceptor needs to know the control-plane's OIDC client ID to tag it as a service caller. Without this, gRPC watch streams authenticate the JWT but don't grant service-caller privileges, so session/project events are silently filtered out. Read from the same ambient-control-plane-oidc secret to stay in sync. Co-Authored-By: Claude Sonnet 4.6 --- components/manifests/templates/template-services.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/components/manifests/templates/template-services.yaml b/components/manifests/templates/template-services.yaml index b7dd064f7..cb72754de 100644 --- a/components/manifests/templates/template-services.yaml +++ b/components/manifests/templates/template-services.yaml @@ -202,6 +202,11 @@ objects: env: - name: AMBIENT_ENV value: production + - name: GRPC_SERVICE_ACCOUNT + valueFrom: + secretKeyRef: + name: ambient-control-plane-oidc + key: client-id - name: CREDENTIAL_ENCRYPTION_KEYRING valueFrom: secretKeyRef: From b19f762da34a2e8e4d6866a879a79c31cfe98d15 Mon Sep 17 00:00:00 2001 From: John Sell Date: Tue, 9 Jun 2026 11:49:01 -0400 Subject: [PATCH 5/5] fix(api-server): mark session_messages seq as read-only in Gorm model The refactor from raw SQL to g2.Create(msg) in PR #1660 caused Gorm to include seq=0 in INSERT statements, colliding with the unique constraint when a seq=0 row already existed. The -> tag tells Gorm to omit seq from writes and let the Postgres DEFAULT nextval() fire, while still reading the value back via the RETURNING clause. Co-Authored-By: Claude Sonnet 4.6 --- components/ambient-api-server/plugins/sessions/message_model.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/ambient-api-server/plugins/sessions/message_model.go b/components/ambient-api-server/plugins/sessions/message_model.go index 7ef7396c1..8a4f88237 100644 --- a/components/ambient-api-server/plugins/sessions/message_model.go +++ b/components/ambient-api-server/plugins/sessions/message_model.go @@ -5,7 +5,7 @@ import "time" type SessionMessage struct { ID string `gorm:"column:id;primaryKey;type:varchar(36)" json:"id"` SessionID string `gorm:"column:session_id;type:varchar(36)" json:"session_id"` - Seq int64 `gorm:"column:seq" json:"seq"` + Seq int64 `gorm:"column:seq;->" json:"seq"` EventType string `gorm:"column:event_type;type:varchar(255)" json:"event_type"` Payload string `gorm:"column:payload;type:text" json:"payload"` CreatedAt time.Time `gorm:"column:created_at;type:timestamptz" json:"created_at"`