Skip to content

Commit f2cf747

Browse files
committed
Narrow managed secret environment variables to phase 1
Signed-off-by: willdavsmith <willdavsmith@gmail.com>
1 parent 9b353de commit f2cf747

25 files changed

Lines changed: 169 additions & 114 deletions

File tree

AI/models/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,4 @@ Recipes for this resource type are provided through the platform Recipe Packs at
2626

2727
## Using the resource type
2828

29-
Add a `models` resource to your application and connect a container to it. One connection named `llm` injects ordinary `CONNECTION_LLM_MODEL` and `CONNECTION_LLM_ENDPOINT` values plus the secret-backed `CONNECTION_LLM_APIKEY`. No second managed-Secret connection is needed. For custom Kubernetes configuration, `model.properties.secrets.name` remains available as the `secretName` for an explicitly authored `secretKeyRef`. See [`test/app.bicep`](test/app.bicep) for a complete example.
29+
Add a `models` resource to your application and connect a container to it. Radius injects the model's connection properties into the container as environment variables named `CONNECTION_<CONNECTION-NAME>_<PROPERTY-NAME>` (for example `CONNECTION_LLM_MODEL` and `CONNECTION_LLM_ENDPOINT`). The `apiKey` secret is not injected — bind it from the managed `Radius.Security/secrets` resource with a container `secretKeyRef` using `model.properties.secrets.name`. See [`test/app.bicep`](test/app.bicep) for a complete example.

AI/models/models.yaml

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,11 @@ types:
4646
4747
- CONNECTION_LLM_MODEL
4848
- CONNECTION_LLM_ENDPOINT
49-
- CONNECTION_LLM_APIKEY (secret-backed)
5049
51-
The same connection injects `apiKey` through a Kubernetes secret reference;
52-
no second managed-Secret connection is needed. For custom Kubernetes
53-
configuration, use `model.properties.secrets.name` as the `secretName` and
54-
`apiKey` as the key in an explicitly authored `secretKeyRef`.
50+
The `apiKey` secret is NOT injected via the connection — it is materialized
51+
into a managed `Radius.Security/secrets` resource. Bind it into a container
52+
env var with a `secretKeyRef`, using `model.properties.secrets.name` as the
53+
`secretName` and key `apiKey` (see the `secrets` property).
5554
5655
apiVersions:
5756
'2025-08-01-preview':

AI/models/test/app.bicep

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,19 @@ resource democontainer 'Radius.Compute/containers@2025-08-01-preview' = {
2727
containers: {
2828
demo: {
2929
image: 'ghcr.io/radius-project/samples/demo:latest'
30+
// The recipe's secret output(s) are materialized into a managed
31+
// Radius.Security/secrets resource and consumed here BY REFERENCE via
32+
// secretKeyRef — the value never lands on model state.
33+
env: {
34+
MODEL_APIKEY: {
35+
valueFrom: {
36+
secretKeyRef: {
37+
secretName: model.properties.secrets.name
38+
key: 'apiKey'
39+
}
40+
}
41+
}
42+
}
3043
ports: {
3144
web: {
3245
containerPort: 3000
@@ -35,7 +48,6 @@ resource democontainer 'Radius.Compute/containers@2025-08-01-preview' = {
3548
}
3649
}
3750
connections: {
38-
// Injects model/endpoint plus secret-backed CONNECTION_LLM_APIKEY.
3951
llm: {
4052
source: model.id
4153
}

AI/search/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,4 @@ Recipes for this resource type are provided through the platform Recipe Packs at
2525

2626
## Using the resource type
2727

28-
Add a `search` resource to your application and connect a container to it. One connection named `search` injects the ordinary `CONNECTION_SEARCH_ENDPOINT` value plus the secret-backed `CONNECTION_SEARCH_APIKEY`. No second managed-Secret connection is needed. For custom Kubernetes configuration, `search.properties.secrets.name` remains available as the `secretName` for an explicitly authored `secretKeyRef`. See [`test/app.bicep`](test/app.bicep) for a complete example.
28+
Add a `search` resource to your application and connect a container to it. Radius injects the search service's connection properties into the container as environment variables named `CONNECTION_<CONNECTION-NAME>_<PROPERTY-NAME>` (for example `CONNECTION_SEARCH_ENDPOINT`). The `apiKey` secret is not injected — bind it from the managed `Radius.Security/secrets` resource with a container `secretKeyRef` using `search.properties.secrets.name`. See [`test/app.bicep`](test/app.bicep) for a complete example.

AI/search/search.yaml

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,11 @@ types:
4646
example the connection name is `search` so the environment variables will be:
4747
4848
- CONNECTION_SEARCH_ENDPOINT
49-
- CONNECTION_SEARCH_APIKEY (secret-backed)
5049
51-
The same connection injects `apiKey` through a Kubernetes secret reference;
52-
no second managed-Secret connection is needed. For custom Kubernetes
53-
configuration, use `search.properties.secrets.name` as the `secretName` and
54-
`apiKey` as the key in an explicitly authored `secretKeyRef`.
50+
The `apiKey` secret is NOT injected via the connection — it is materialized
51+
into a managed `Radius.Security/secrets` resource. Bind it into a container
52+
env var with a `secretKeyRef`, using `search.properties.secrets.name` as the
53+
`secretName` and key `apiKey` (see the `secrets` property).
5554
5655
apiVersions:
5756
'2025-08-01-preview':

AI/search/test/app.bicep

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,19 @@ resource democontainer 'Radius.Compute/containers@2025-08-01-preview' = {
2626
containers: {
2727
demo: {
2828
image: 'ghcr.io/radius-project/samples/demo:latest'
29+
// The recipe's secret output(s) are materialized into a managed
30+
// Radius.Security/secrets resource and consumed here BY REFERENCE via
31+
// secretKeyRef — the value never lands on searchService state.
32+
env: {
33+
SEARCH_APIKEY: {
34+
valueFrom: {
35+
secretKeyRef: {
36+
secretName: searchService.properties.secrets.name
37+
key: 'apiKey'
38+
}
39+
}
40+
}
41+
}
2942
ports: {
3043
web: {
3144
containerPort: 3000
@@ -34,7 +47,6 @@ resource democontainer 'Radius.Compute/containers@2025-08-01-preview' = {
3447
}
3548
}
3649
connections: {
37-
// Injects endpoint plus secret-backed CONNECTION_SEARCH_APIKEY.
3850
search: {
3951
source: searchService.id
4052
}

Compute/containers/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,11 @@ A list of available Recipes for this Resource Type, including links to the Bicep
5151

5252
### Connections and secrets
5353

54-
For ordinary connections, the Kubernetes Recipes inject `context.resource.connections.<name>.properties` as `CONNECTION_<CONNECTION-NAME>_<PROPERTY-NAME>` environment variables. When a producer Recipe returns secrets, Radius supplies reference metadata under `context.resource.connections.<name>.secrets`; the same connection injects each secret through a Kubernetes `secretKeyRef`. For example, Redis connection `redis` supplies its ordinary `host` and `port` values together with the secret-backed `CONNECTION_REDIS_URL`.
54+
For ordinary connections, the Kubernetes Recipes preserve the existing behavior of injecting scalar metadata from `context.resource.connections.<name>` and values from `context.resource.connections.<name>.properties` as `CONNECTION_<CONNECTION-NAME>_<PROPERTY-NAME>` environment variables. When a producer Recipe returns secrets, Radius supplies reference metadata under `context.resource.connections.<name>.secrets`; the same connection injects each secret through a Kubernetes `secretKeyRef`. For example, Redis connection `redis` supplies its ordinary `host` and `port` values together with the secret-backed `CONNECTION_REDIS_URL`.
5555

5656
Direct connections to user-authored `Radius.Security/secrets` resources remain supported and inject one secret-backed variable per data key. Both regular and init containers receive generated variables. Explicit container environment variables take precedence, managed secret references take precedence over ordinary properties with the same generated name, and `disableDefaultEnvVars: true` disables both ordinary and secret-backed variables for that connection.
5757

58-
Connection names, property names, and secret names are uppercased when generating environment variable names. Names that collide after uppercasing are rejected. The Kubernetes Secret name is derived from the final segment of each full Radius Secret resource ID. Secret values remain in Kubernetes references and are never copied into Recipe output or plaintext container configuration.
58+
Connection names, property names, and secret names are uppercased when generating environment variable names. Secret names that collide after uppercasing are rejected. Direct Secret connections now fully uppercase the generated variable name, including the data key; this intentionally replaces the previous `envFrom` behavior, which preserved the Secret data key's casing after the uppercase connection prefix. The Kubernetes Secret name is derived from the final segment of each full Radius Secret resource ID. Secret values remain in Kubernetes references and are never copied into Recipe output or plaintext container configuration.
5959

6060
The Azure ACI Recipe is unchanged and does not consume the Kubernetes secret reference metadata described above.
6161

Compute/containers/recipes/kubernetes/bicep/kubernetes-containers.bicep

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,11 @@ var connectionDefinitions = context.resource.properties.?connections ?? {}
4141

4242
// Properties to exclude from connection environment variables
4343
var excludedProperties = ['recipe', 'secrets', 'status', 'provisioningState']
44+
var topLevelExcludedProperties = ['recipe', 'secrets', 'status', 'provisioningState', 'properties', 'secretName']
4445

4546
// Identify direct Radius secret connections from resolved resource metadata.
4647
var isSecretsResource = reduce(items(resourceConnections), {}, (acc, conn) => union(acc, {
47-
'${conn.key}': string(conn.value.?type ?? '') == 'Radius.Security/secrets'
48+
'${conn.key}': string(conn.value.?type ?? '') == 'Radius.Security/secrets' || (contains(connectionDefinitions, conn.key) && contains(string(connectionDefinitions[conn.key].?source ?? ''), 'Radius.Security/secrets'))
4849
}))
4950

5051
// Resolved and declared connection maps can briefly differ while dependencies update.
@@ -73,12 +74,7 @@ var directSecretEnvVars = reduce(items(resourceConnections), [], (acc, conn) =>
7374
)
7475

7576
// Producer Recipe secrets arrive as reference metadata, separate from ordinary properties.
76-
var managedSecretReferencesValid = reduce(items(resourceConnections), true, (connectionsValid, conn) =>
77-
connectionsValid && reduce(items(conn.value.?secrets ?? {}), true, (secretsValid, secret) =>
78-
secretsValid && secret.value.?source != null && secret.value.?key != null
79-
)
80-
)
81-
var managedSecretEnvVars = managedSecretReferencesValid ? reduce(items(resourceConnections), [], (acc, conn) =>
77+
var managedSecretEnvVars = reduce(items(resourceConnections), [], (acc, conn) =>
8278
disableDefaultEnvVars[conn.key] || isSecretsResource[conn.key]
8379
? acc
8480
: concat(
@@ -93,7 +89,7 @@ var managedSecretEnvVars = managedSecretReferencesValid ? reduce(items(resourceC
9389
}
9490
}]))
9591
)
96-
) : fail('Managed connection secret references must include source and key.')
92+
)
9793

9894
var secretConnectionEnvVars = concat(directSecretEnvVars, managedSecretEnvVars)
9995
var secretConnectionEnvVarNames = map(secretConnectionEnvVars, envVar => envVar.name)
@@ -122,7 +118,8 @@ var secretNameEnvFrom = reduce(items(resourceConnections), [], (acc, conn) =>
122118
: acc
123119
)
124120

125-
// Ordinary producer properties become CONNECTION_<CONNECTION_NAME>_<PROPERTY_NAME>.
121+
// Ordinary top-level scalar values and nested producer properties become
122+
// CONNECTION_<CONNECTION_NAME>_<PROPERTY_NAME>.
126123
// Null-valued properties are skipped: sensitive properties (e.g. a database
127124
// password marked x-radius-sensitive) are redacted to null on reads, and
128125
// string(null) fails ARM template validation with "InvalidTemplate".
@@ -131,6 +128,14 @@ var rawConnectionEnvVars = reduce(items(resourceConnections), [], (acc, conn) =>
131128
? acc
132129
: concat(
133130
acc,
131+
reduce(items(conn.value ?? {}), [], (envAcc, prop) =>
132+
(contains(topLevelExcludedProperties, prop.key) || prop.value == null)
133+
? envAcc
134+
: concat(envAcc, [{
135+
name: toUpper('CONNECTION_${conn.key}_${prop.key}')
136+
value: string(prop.value)
137+
}])
138+
),
134139
reduce(items(conn.value.?properties ?? {}), [], (envAcc, prop) =>
135140
(prop.key == 'secretName' || contains(excludedProperties, prop.key) || prop.value == null)
136141
? envAcc
@@ -141,13 +146,9 @@ var rawConnectionEnvVars = reduce(items(resourceConnections), [], (acc, conn) =>
141146
)
142147
)
143148
)
144-
var connectionEnvVarNames = map(rawConnectionEnvVars, envVar => envVar.name)
145-
var validatedConnectionEnvVars = length(connectionEnvVarNames) == length(union(connectionEnvVarNames, connectionEnvVarNames))
146-
? rawConnectionEnvVars
147-
: fail('Connection properties must produce unique environment variable names after uppercasing.')
148149

149150
// Managed secret references take precedence over an ordinary output with the same name.
150-
var connectionEnvVars = filter(validatedConnectionEnvVars, envVar => !contains(secretConnectionEnvVarNames, envVar.name))
151+
var connectionEnvVars = filter(rawConnectionEnvVars, envVar => !contains(secretConnectionEnvVarNames, envVar.name))
151152

152153
// Use replicas from properties, default to 1 if not specified
153154
var replicaCount = resourceProperties.?replicas != null ? int(resourceProperties.replicas) : 1

Compute/containers/recipes/kubernetes/terraform/main.tf

Lines changed: 28 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,20 @@ locals {
4444

4545
# Properties to exclude from connection environment variables
4646
excluded_properties = ["recipe", "secrets", "status", "provisioningState"]
47+
top_level_excluded_properties = [
48+
"recipe",
49+
"secrets",
50+
"status",
51+
"provisioningState",
52+
"properties",
53+
"secretName",
54+
]
4755

4856
# Identify direct Radius secret connections from resolved resource metadata.
4957
is_secrets_resource = {
5058
for conn_name, conn in local.connections :
51-
conn_name => try(conn.type, "") == "Radius.Security/secrets"
59+
conn_name => try(conn.type, "") == "Radius.Security/secrets" ||
60+
can(regex("Radius.Security/secrets", try(local.connection_definitions[conn_name].source, "")))
5261
}
5362

5463
# Direct Radius.Security/secrets connections inject every declared data key.
@@ -90,14 +99,6 @@ locals {
9099
]
91100
: []
92101
])
93-
managed_secret_references_valid = alltrue(flatten([
94-
for conn_name, conn in local.connections : [
95-
for secret_name, secret_ref in try(conn.secrets, {}) :
96-
can(tostring(secret_ref.source)) && can(tostring(secret_ref.key))
97-
]
98-
if !try(local.is_secrets_resource[conn_name], false)
99-
]))
100-
101102
secret_connection_env_vars = concat(local.direct_secret_env_vars, local.managed_secret_env_vars)
102103
secret_connection_env_var_names = [
103104
for env in local.secret_connection_env_vars : env.name
@@ -116,24 +117,31 @@ locals {
116117
(can(tostring(conn.secretName)) || can(tostring(conn.properties.secretName)))
117118
]
118119

119-
# Ordinary producer properties become CONNECTION_<CONNECTION_NAME>_<PROPERTY_NAME>.
120+
# Ordinary top-level scalar values and nested producer properties become
121+
# CONNECTION_<CONNECTION_NAME>_<PROPERTY_NAME>.
120122
# Note: disableDefaultEnvVars is on connection_definitions, not the merged connections data
121123
raw_connection_env_vars = flatten([
122124
for conn_name, conn in local.connections :
123125
!try(local.is_secrets_resource[conn_name], false) &&
124126
try(local.connection_definitions[conn_name].disableDefaultEnvVars, false) != true
125-
? [
126-
for prop_name, prop_value in try(conn.properties, {}) : {
127-
name = upper("CONNECTION_${conn_name}_${prop_name}")
128-
value = tostring(prop_value)
129-
}
130-
if prop_name != "secretName" && !contains(local.excluded_properties, prop_name) && can(tostring(prop_value))
131-
]
127+
? concat(
128+
[
129+
for prop_name, prop_value in conn : {
130+
name = upper("CONNECTION_${conn_name}_${prop_name}")
131+
value = tostring(prop_value)
132+
}
133+
if !contains(local.top_level_excluded_properties, prop_name) && can(tostring(prop_value))
134+
],
135+
[
136+
for prop_name, prop_value in try(conn.properties, {}) : {
137+
name = upper("CONNECTION_${conn_name}_${prop_name}")
138+
value = tostring(prop_value)
139+
}
140+
if prop_name != "secretName" && !contains(local.excluded_properties, prop_name) && can(tostring(prop_value))
141+
]
142+
)
132143
: []
133144
])
134-
connection_env_var_names = [
135-
for env in local.raw_connection_env_vars : env.name
136-
]
137145

138146
# Managed secret references take precedence over ordinary outputs with the same name.
139147
connection_env_vars = [
@@ -353,14 +361,6 @@ resource "kubernetes_deployment" "deployment" {
353361
condition = length(local.secret_connection_env_var_names) == length(distinct(local.secret_connection_env_var_names))
354362
error_message = "Connection secret keys must produce unique environment variable names after uppercasing."
355363
}
356-
precondition {
357-
condition = local.managed_secret_references_valid
358-
error_message = "Managed connection secret references must include source and key."
359-
}
360-
precondition {
361-
condition = length(local.connection_env_var_names) == length(distinct(local.connection_env_var_names))
362-
error_message = "Connection properties must produce unique environment variable names after uppercasing."
363-
}
364364
}
365365

366366
metadata {

Data/mongoDatabases/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,4 @@ Recipes for this resource type are provided through the platform Recipe Packs at
2626

2727
## Using the resource type
2828

29-
Add a `mongoDatabases` resource to your application and connect a container to it. One connection named `mongodb` injects ordinary `CONNECTION_MONGODB_DATABASE` and `CONNECTION_MONGODB_ENDPOINT` values plus the secret-backed `CONNECTION_MONGODB_CONNECTIONSTRING`. No second managed-Secret connection is needed. For custom Kubernetes configuration, `mongo.properties.secrets.name` remains available as the `secretName` for an explicitly authored `secretKeyRef`. See [`test/app.bicep`](test/app.bicep) for a complete example.
29+
Add a `mongoDatabases` resource to your application and connect a container to it. Radius injects the database's connection properties into the container as environment variables named `CONNECTION_<CONNECTION-NAME>_<PROPERTY-NAME>` (for example `CONNECTION_MONGODB_DATABASE` and `CONNECTION_MONGODB_ENDPOINT`). The `connectionString` secret is not injected — bind it from the managed `Radius.Security/secrets` resource with a container `secretKeyRef` using `mongo.properties.secrets.name`. See [`test/app.bicep`](test/app.bicep) for a complete example.

0 commit comments

Comments
 (0)