From 038cc234ca85f2bb7fed0f564044b1a2b028650a Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Mon, 17 Aug 2026 09:41:54 -0700 Subject: [PATCH 1/3] Align Kubernetes postgreSQL recipe secret handling with Azure The Kubernetes recipe returned `secrets: { password, connectionString }`, but the postgreSqlDatabases type declares no `secrets` property, so neither value was reachable from an application. The Azure recipe in recipe-packs/azure/aks-recipepack.bicep maps only `host`, and deliberately does not use the `outputs.secrets` mechanism it uses one entry away for redisCaches. That distinction is intentional. PostgreSQL administrator credentials flow into the recipe as user-supplied properties, so there is nothing to hand back; Redis access keys are generated by the infrastructure and must flow out. Drop the unreachable secrets output so both platforms agree, and document the reasoning. Also emit `port` as a string to match `type: string` on the resource type, and update test/app.bicep to show the supported pattern: the application authors its own Radius.Security/secrets resource and binds it into the container with valueFrom.secretKeyRef. A connection cannot carry the password (sensitive properties redact to null and are skipped by the containers recipe), so this is the only way to deliver it, and it keeps the value out of the pod spec and off the container resource, where env values are stored unencrypted. Signed-off-by: Will Smith Signed-off-by: willdavsmith --- .../bicep/kubernetes-postgresql.bicep | 18 ++++++---- Data/postgreSqlDatabases/test/app.bicep | 36 +++++++++++++++++-- 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/Data/postgreSqlDatabases/recipes/kubernetes/bicep/kubernetes-postgresql.bicep b/Data/postgreSqlDatabases/recipes/kubernetes/bicep/kubernetes-postgresql.bicep index 3cfb533d..797707e7 100644 --- a/Data/postgreSqlDatabases/recipes/kubernetes/bicep/kubernetes-postgresql.bicep +++ b/Data/postgreSqlDatabases/recipes/kubernetes/bicep/kubernetes-postgresql.bicep @@ -188,7 +188,16 @@ resource svc 'core/Service@v1' = { } ////////////////////////////////////////// -// Output Radius result +// Output Radius result +// +// No `secrets` output: the administrator credentials flow *into* this Recipe as +// user-supplied properties, so there is nothing for the Recipe to hand back. An +// application that needs the password for a consuming container already holds it +// and should author its own Radius.Security/secrets resource, binding it with +// `valueFrom.secretKeyRef` (see test/app.bicep). This matches the Azure recipe in +// recipe-packs/azure/aks-recipepack.bicep, which maps only `host`. Contrast with +// redisCaches, where the access key is generated by the infrastructure and so is +// returned via `outputs.secrets`. ////////////////////////////////////////// output result object = { @@ -204,11 +213,8 @@ output result object = { ) values: { host: '${svc.metadata.name}.${svc.metadata.namespace}.svc.cluster.local' - port: port + // `port` is declared `type: string` on the Resource Type, so emit a string. + port: string(port) database: database } - secrets: { - password: password - connectionString: 'postgresql://${username}:${password}@${svc.metadata.name}.${svc.metadata.namespace}.svc.cluster.local:${port}/${database}' - } } diff --git a/Data/postgreSqlDatabases/test/app.bicep b/Data/postgreSqlDatabases/test/app.bicep index 62e78b36..c3ccad34 100644 --- a/Data/postgreSqlDatabases/test/app.bicep +++ b/Data/postgreSqlDatabases/test/app.bicep @@ -3,17 +3,35 @@ extension radius @description('The ID of your Radius Environment. Set automatically by the rad CLI.') param environment string -@description('Database admin password. Set on the resource `password` property (x-radius-sensitive), so Radius encrypts it at rest and injects it decrypted into the Recipe as the flexible server administrator password.') +@description('Database admin password. Set on the `password` property of the database (x-radius-sensitive, so Radius encrypts it at rest and injects it decrypted into the Recipe) and stored in a Radius.Security/secrets resource for the consuming container to bind by reference.') @secure() param password string resource app 'Radius.Core/applications@2025-08-01-preview' = { - name: 'postgresql-azure-test' + name: 'postgresql-test' properties: { environment: environment } } +// The password is also needed by the consuming container. Store it in a +// Radius.Security/secrets resource rather than passing it to the container as a +// plain `env` value: `data.value` is x-radius-sensitive (encrypted at rest, +// redacted on reads), whereas a container `env.value` is stored unencrypted on +// the container resource and rendered literally into the Pod spec. +resource dbCreds 'Radius.Security/secrets@2025-08-01-preview' = { + name: 'postgresql-credentials' + properties: { + environment: environment + application: app.id + data: { + password: { + value: password + } + } + } +} + resource postgresql 'Radius.Data/postgreSqlDatabases@2025-08-01-preview' = { name: 'postgresql' properties: { @@ -34,6 +52,20 @@ resource democontainer 'Radius.Compute/containers@2025-08-01-preview' = { containers: { demo: { image: 'ghcr.io/radius-project/samples/demo:latest' + // Host, port, and database arrive automatically as + // CONNECTION_POSTGRES_* env vars from the connection below. Only the + // password needs wiring, and it is bound by reference so the value + // never lands in the Pod spec or on this container's state. + env: { + POSTGRES_PASSWORD: { + valueFrom: { + secretKeyRef: { + secretName: dbCreds.name + key: 'password' + } + } + } + } ports: { web: { containerPort: 3000 From 400d66af3df0c4ff8d5eb74575a41c0881972127 Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Mon, 17 Aug 2026 09:49:10 -0700 Subject: [PATCH 2/3] Fix PostgreSQL connection variable names in the test application The test application bound the password as POSTGRES_PASSWORD and named the connection `postgres`, yielding CONNECTION_POSTGRES_* variables. The demo image reads neither: samples/demo/src/db/repository.ts gates on CONNECTION_POSTGRESQL_HOST and reads CONNECTION_POSTGRESQL_{PORT,USERNAME, PASSWORD,DATABASE}. The container therefore never matched, silently fell back to its in-memory store, and the test still passed because it only asserts that the deployment succeeded. Rename the connection to `postgresql` and bind the secret as CONNECTION_POSTGRESQL_PASSWORD. That is the one variable Radius cannot supply through the connection, because x-radius-sensitive properties redact to null on reads and the containers recipe skips null values, so the binding fills the gap rather than colliding with an injected variable. Signed-off-by: Will Smith Signed-off-by: willdavsmith --- Data/postgreSqlDatabases/test/app.bicep | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/Data/postgreSqlDatabases/test/app.bicep b/Data/postgreSqlDatabases/test/app.bicep index c3ccad34..b5136b36 100644 --- a/Data/postgreSqlDatabases/test/app.bicep +++ b/Data/postgreSqlDatabases/test/app.bicep @@ -52,12 +52,14 @@ resource democontainer 'Radius.Compute/containers@2025-08-01-preview' = { containers: { demo: { image: 'ghcr.io/radius-project/samples/demo:latest' - // Host, port, and database arrive automatically as - // CONNECTION_POSTGRES_* env vars from the connection below. Only the - // password needs wiring, and it is bound by reference so the value - // never lands in the Pod spec or on this container's state. + // Host, port, username, and database arrive automatically as + // CONNECTION_POSTGRESQL_* env vars from the connection below. The + // connection cannot carry the password (x-radius-sensitive properties + // redact to null on reads and are skipped), so it is bound by reference + // here under the same naming scheme, filling the one gap the connection + // leaves. The value never lands in the Pod spec or on this container. env: { - POSTGRES_PASSWORD: { + CONNECTION_POSTGRESQL_PASSWORD: { valueFrom: { secretKeyRef: { secretName: dbCreds.name @@ -74,7 +76,9 @@ resource democontainer 'Radius.Compute/containers@2025-08-01-preview' = { } } connections: { - postgres: { + // Named `postgresql` so the injected variables are CONNECTION_POSTGRESQL_*, + // which is the prefix the demo image looks for. + postgresql: { source: postgresql.id } } From 6ffec56725f28ad87d497e00f853873145bda34a Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Thu, 20 Aug 2026 14:56:52 -0700 Subject: [PATCH 3/3] Fix PostgreSQL secret ownership across recipes Signed-off-by: willdavsmith --- .../recipes/kubernetes/terraform/main.tf | 33 +++++++++---------- Data/postgreSqlDatabases/test/app.bicep | 4 ++- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/Data/postgreSqlDatabases/recipes/kubernetes/terraform/main.tf b/Data/postgreSqlDatabases/recipes/kubernetes/terraform/main.tf index bffd681e..76ad8ddf 100644 --- a/Data/postgreSqlDatabases/recipes/kubernetes/terraform/main.tf +++ b/Data/postgreSqlDatabases/recipes/kubernetes/terraform/main.tf @@ -10,7 +10,7 @@ terraform { variable "context" { description = "This variable contains Radius Recipe context." - type = any + type = any } variable "memory" { @@ -32,17 +32,17 @@ variable "memory" { } locals { - resource_name = var.context.resource.name - application_name = var.context.application != null ? var.context.application.name : "" - environment_name = var.context.environment != null ? var.context.environment.name : "" - resource_group = element(split("/", var.context.resource.id), 5) - namespace = var.context.runtime.kubernetes.namespace - port = 5432 - tag = "16-alpine" - username = var.context.resource.properties.username - password = var.context.resource.properties.password - database = try(var.context.resource.properties.database, "postgres_db") - size_value = try(var.context.resource.properties.size, "S") + resource_name = var.context.resource.name + application_name = var.context.application != null ? var.context.application.name : "" + environment_name = var.context.environment != null ? var.context.environment.name : "" + resource_group = element(split("/", var.context.resource.id), 5) + namespace = var.context.runtime.kubernetes.namespace + port = 5432 + tag = "16-alpine" + username = var.context.resource.properties.username + password = var.context.resource.properties.password + database = try(var.context.resource.properties.database, "postgres_db") + size_value = try(var.context.resource.properties.size, "S") labels = { "radapp.io/resource" = local.resource_name @@ -150,6 +150,8 @@ resource "kubernetes_service" "postgres" { } } +# The administrator credentials are user-supplied Recipe inputs. They configure +# PostgreSQL through the Kubernetes Secret above but are not Recipe outputs. output "result" { value = { resources = [ @@ -159,13 +161,8 @@ output "result" { ] values = { host = "${kubernetes_service.postgres.metadata[0].name}.${kubernetes_service.postgres.metadata[0].namespace}.svc.cluster.local" - port = local.port + port = tostring(local.port) database = local.database } - secrets = { - password = local.password - connectionString = "postgresql://${local.username}:${local.password}@${kubernetes_service.postgres.metadata[0].name}.${kubernetes_service.postgres.metadata[0].namespace}.svc.cluster.local:${local.port}/${local.database}" - } } - sensitive = true } diff --git a/Data/postgreSqlDatabases/test/app.bicep b/Data/postgreSqlDatabases/test/app.bicep index b5136b36..e1fef655 100644 --- a/Data/postgreSqlDatabases/test/app.bicep +++ b/Data/postgreSqlDatabases/test/app.bicep @@ -20,7 +20,9 @@ resource app 'Radius.Core/applications@2025-08-01-preview' = { // redacted on reads), whereas a container `env.value` is stored unencrypted on // the container resource and rendered literally into the Pod spec. resource dbCreds 'Radius.Security/secrets@2025-08-01-preview' = { - name: 'postgresql-credentials' + // Keep this distinct from the Recipe-owned `postgresql-credentials` + // Kubernetes Secret that configures the PostgreSQL container. + name: 'postgresql-client-credentials' properties: { environment: environment application: app.id