Skip to content

x-radius-sensitive recipe-output connection properties are not delivered to connected containers #12288

Open

Description

@willdavsmith

Summary

An x-radius-sensitive property that is populated by a recipe's secure output (e.g. a connectionString mapped from a Bicep @secure() / securestring module output) is never delivered to a container that connects to that resource. Non-sensitive connection properties (e.g. host) are injected as CONNECTION_<NAME>_<PROP> env vars as expected, but the sensitive one is silently absent. There is no secure (secretKeyRef) injection path for it either.

This breaks the "output‑secret" pattern for user‑defined resource types backed by direct AVM / Terraform modules, where the platform (not the developer) generates the credential and the app is expected to consume it only via the injected connection env var.

Repro

User‑defined type Radius.Messaging/kafka (apiVersion 2025-08-01-preview) with:

properties:
  host:             { type: string, readOnly: true }
  connectionString: { type: string, readOnly: true, x-radius-sensitive: true }

A direct‑module Bicep recipe maps the AVM Event Hubs namespace outputs onto the properties:

'Radius.Messaging/kafka': {
  kind: 'bicep'
  source: 'mcr.microsoft.com/bicep/avm/res/event-hub/namespace:0.14.2'
  outputs: {
    connectionString: 'primaryConnectionString'   // AVM @secure() output
    host: 'name'
  }
}
'Radius.Compute/containers': {
  kind: 'bicep'
  source: 'ghcr.io/radius-project/kube-recipes/containers:latest'
}

A container connects to it:

connections: { kafka: { source: kafkaBroker.id } }

Full, runnable repro (CI workflow + app + recipe): radius-project/resource-types-verification, tests/kafka/azure-app/ (branch willdavsmith-redis-avm-secrets-check, PR #5).

Expected vs. actual

  • Expected: the container receives both CONNECTION_KAFKA_HOST and CONNECTION_KAFKA_CONNECTIONSTRING (the latter ideally via a Kubernetes Secret + secretKeyRef, so the value is never a plaintext literal).
  • Actual: only CONNECTION_KAFKA_HOST is injected. CONNECTION_KAFKA_CONNECTIONSTRING is unset, so the app cannot authenticate (in our repro the pod CrashLoops in its startup shim, which requires the var).

Root‑cause analysis (verified against current main)

The secure value is correctly captured by the deployment engine and the recipe driver, but the redaction pipeline + the recipe‑based container model together have no path to deliver a sensitive recipe‑output property to a connecting container.

  1. Engine + driver capture the secure value correctly. The Bicep deployment engine returns the @secure() output value (secure outputs validate as strings and are not stripped). The driver routes securestring/secureobject outputs to the Secrets map and applies the outputs: mapping:

    • pkg/recipes/driver/bicep/bicep.go — collectARMOutputs (routes secure types to secrets), ApplyOutputsMapping (connectionString ← primaryConnectionString).
  2. The sensitive field is redacted to nil in the persisted resource. Before the recipe runs, the controller detects x-radius-sensitive paths, decrypts a working copy for the recipe, redacts the stored copy to nil, and persists it:

    • pkg/portableresources/backend/controller/createorupdateresource.go — Run() (sensitive‑field detection → RedactFields → Save).
    • pkg/schema/annotations.go — RedactFields / redactAtSegments set the field to nil.
      The processor later writes the secret value into the resource status / secret store (ApplyDeploymentOutput) and into top‑level properties in memory (addOutputValuestoResourceProperties), but the secret’s durable home is the secret store, not a plaintext top‑level property:
    • pkg/dynamicrp/backend/processor/dynamicresource.go.
  3. A connecting resource reads the target’s raw DB properties with no decryption. When the container’s recipe executes, the controller fetches each connected resource’s properties straight from the database and hands them to the recipe context:

    • pkg/portableresources/backend/controller/createorupdateresource.go — executeRecipeIfNeeded() → resourceutil.GetAllPropertiesFromResource() (no decryption) → ConnectedResourcesProperties.
    • pkg/recipes/driver/bicep/bicep.go — recipeContext.Resource.Connections = opts.Recipe.ConnectedResourcesProperties.
      So the container recipe sees whatever is in the DB for the sensitive field — which is the redacted nil, not the secret value.
  4. The Radius.Compute/containers recipe has no secure‑injection path for this case. All connection env‑var injection happens inside the recipe radius-project/resource-types-contrib:Compute/containers/recipes/kubernetes/bicep/kubernetes-containers.bicep, which has exactly three mechanisms:

    • connectionEnvVars — plaintext literal value: string(prop.value) from the connected resource’s properties (used for normal connections like kafka).
    • secretsEnvFrom — envFrom.secretRef, only when the connection source is Radius.Security/secrets.
    • secretNameEnvFrom — envFrom.secretRef, only when the connection carries a secretName property naming a Kubernetes Secret.
      None of these applies to an x-radius-sensitive property produced by a recipe output on an ordinary user‑defined type, so even if the value were present it could only be injected as a plaintext literal.

Why host works but connectionString doesn’t

host is not sensitive, so it is never redacted; it persists as a normal top‑level property and is injected as a literal CONNECTION_KAFKA_HOST. connectionString is x-radius-sensitive, so it is redacted out of the readable top‑level properties (its value living in the secret store), and the container recipe has no secretKeyRef path to pull it back. Hence the asymmetry.

Impact

  • The platform‑generated / output‑secret credential pattern does not work end‑to‑end for direct‑module (AVM/Terraform) backed types whose secret is a recipe output consumed by a connected container. This is the common shape for managed datastores/brokers where the cloud generates the credential (Event Hubs SAS, Redis access key, etc.).
  • It is latent for any such type: e.g. a Redis type mapping url ← primaryConnectionString has the same gap; it just isn’t exercised by an app/connection test today.
  • Note this is not a missing‑test issue (cf. Direct module recipes: add cloud E2E functional tests for limitations not coverable in noncloud CI #12202, which tracks adding cloud E2E for direct‑module secret routing). The routing into Secrets works; the consumption path to a connected container is missing.

Proposed solutions (any one unblocks the pattern)

  1. Secure connection injection (preferred): give the Radius.Compute/containers model a way to inject an x-radius-sensitive connection property via a Kubernetes Secret + secretKeyRef, by surfacing the connected resource’s sensitive values to the container recipe through a secret reference (rather than redacted/raw properties). This keeps the value out of plaintext env literals.
  2. Decrypted connection context for sensitive properties: when building ConnectedResourcesProperties, resolve x-radius-sensitive properties from the secret store so the value is available to the consuming recipe (with the recipe injecting it as a secretKeyRef, not a literal).
  3. Document + first‑class secretName bridge: define a supported convention where a recipe can expose a secretName for its sensitive outputs so the existing secretNameEnvFrom path injects them via secretRef.

References

Activity

  1. radius-triage commented on Jun 30, 2026

    @radius-triage

    👍 We've reviewed this issue and have agreed to add it to our backlog. Please subscribe to this issue for notifications, we'll provide updates when we pick it up.

    We also welcome community contributions! If you would like to pick this item up sooner and submit a pull request, please visit our contribution guidelines and assign this to yourself by commenting "/assign" on this issue.

    For more information on our triage process please visit our triage overview

  2. nicolejms commented on Sep 11, 2026

    @nicolejms
    Contributor

    Partially complete at current main baseline ec979015e67de6222b2b83c462de1ae70790c3e4.

    Implemented

    Remaining gaps

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    triagedThis issue has been reviewed and triaged

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions