Securely flow developer secrets into direct-module recipe parameters#12286
Draft
willdavsmith wants to merge 9 commits into
Draft
Securely flow developer secrets into direct-module recipe parameters#12286willdavsmith wants to merge 9 commits into
willdavsmith wants to merge 9 commits into
Conversation
Implements Phase 1 of #12244: securely flow developer-authored secrets into direct-module recipe parameters. The engine enriches secret-typed connections with secret material, the param resolver resolves whole-value secret references and tags them, and the Bicep and Terraform drivers route secret values through @secure() / sensitive variables. Docs are a planned follow-up. Signed-off-by: willdavsmith <[email protected]>
Extends Phase 1 of #12244 so a developer-authored Radius.Security/secrets resource referenced by a resource property (x-radius-secret-reference, e.g. secretName) flows its real cleartext into direct-module recipe parameters via the context.resource.secrets.<key> expression, tagged for secure routing (ARM @secure() / Terraform sensitive = true). Because a secret's sensitive data is redacted from the database before recipe execution, the new dynamic-RP secrets loader reads cleartext on demand from the secret's deployed Kubernetes Secret via status.outputResources (never the redacted Properties.Data), scoped to Kubernetes-backed secrets. Enrichment is fail-closed: a referenced secret that cannot be loaded fails the deployment rather than passing a literal placeholder to the module. Docs are a planned follow-up. Signed-off-by: willdavsmith <[email protected]>
…ters" This reverts commit 3876f0a. The #12244 owner has parked the secretName / Radius.Security/secrets path pending reconciliation of its scope (Phase 1 vs deferred Phase 2 reference passthrough, the latter potentially needing security-owner sign-off). This restores PR #12249 to the merge-ready connection-based Phase 1 deliverable. The reverted work remains in history at 3876f0a and can be re-applied when the owner picks it back up. Signed-off-by: willdavsmith <[email protected]>
Extends Phase 1 of #12244 so a developer-authored Radius.Security/secrets resource referenced by a resource property (x-radius-secret-reference, e.g. secretName) flows its real cleartext into direct-module recipe parameters via the context.resource.secrets.<key> expression, tagged for secure routing (ARM @secure() / Terraform sensitive = true). Because a secret's sensitive data is redacted from the database before recipe execution, the new dynamic-RP secrets loader reads cleartext on demand from the secret's deployed Kubernetes Secret via status.outputResources (never the redacted Properties.Data), scoped to Kubernetes-backed secrets. Enrichment is fail-closed: a referenced secret that cannot be loaded fails the deployment rather than passing a literal placeholder to the module. Docs are a planned follow-up. Signed-off-by: willdavsmith <[email protected]>
…ecipes Introduce the x-radius-secret-binding schema annotation, which marks an array property listing the Radius.Security/secrets resource IDs a resource depends on. The engine parses each ID, loads every key of each listed secret, and exposes the values to recipes (through the Bicep @secure() / Terraform sensitive lanes) as context.resource.secrets.<secretName>.<key>, where <secretName> is the secret resource's name. This lets direct-module recipes (for example Azure AVM or AWS Terraform modules) consume developer-authored Radius secrets without the module performing its own Kubernetes secret lookup. - schema: register the annotation and add the ExtractSecretBindingPaths extractor (the marked array property is treated as a leaf) - controller: buildSecretBindings reads the array of secret IDs and validates each, fail-closed on malformed input - engine: enrichSecretBindings loads all keys of each bound secret, namespaces them by the secret resource name, and detects collisions - types: ResourceMetadata.SecretBindings is now []string Signed-off-by: willdavsmith <[email protected]>
Add an x-radius-retain schema annotation so Radius.Security/secrets values persist as ciphertext in the control-plane store instead of being redacted to nil after recipe execution. The dynamic-rp secrets loader now decrypts secret values from Radius's own store, falling back to reading the target-cluster Kubernetes Secret only for pre-retain (nil-at-rest) secrets. This fixes secret resolution for multi-cluster deployments, where the created Kubernetes Secret lives on the application's target cluster rather than the cluster running the control plane. - pkg/schema: x-radius-retain annotation, Extract/GetRetainFieldPaths, and a validator invariant that retain fields must also be sensitive. - pkg/portableresources/backend: persist the encrypted envelope for retain fields (redact only sensitive-minus-retain); recipes still get cleartext. - pkg/dynamicrp/frontend: redact retain fields on GET and LIST even in the Succeeded state, since they survive as ciphertext. - pkg/dynamicrp/secretsloader: decrypt from store with Kubernetes fallback. - pkg/dynamicrp/options: thread the UCP schema client into the loader. Signed-off-by: willdavsmith <[email protected]>
The udtSecretsLoader previously fell back to reading the value from a single cluster's Kubernetes Secret when a Radius.Security/secrets value was not retained encrypted at rest (e.g. a pre-retain secret). That fallback is not multi-cluster safe and silently masked secrets that were never persisted encrypted. Remove the fallback so the loader fails closed: a secret with no value stored at rest, missing properties, or no available schema now returns an operator-facing error directing them to redeploy the secret, rather than quietly reading from the control-plane cluster. The decryption key read from radius-system is retained, since it lives on the control-plane cluster and is not part of the fallback. Signed-off-by: willdavsmith <[email protected]>
The Radius.Security/secrets value field was x-radius-sensitive but never x-radius-retain, so dynamic-rp redacted data[*].value to nil at rest (retain set minus sensitive set was the full sensitive set). The secrets loader then had no encrypted value to decrypt from the store and relied on the (now removed) Kubernetes Secret fallback in every deployment, not just migration. Adding x-radius-retain to the built-in dev and self-hosted manifests makes the encrypted value persist at rest so the loader can decrypt it from Radius's own store, which is required for multi-cluster-safe secret resolution. Signed-off-by: willdavsmith <[email protected]>
|
This PR requires exactly 1 of the following labels: pr:standard, pr:important. Label descriptions:
@willdavsmith, please add the appropriate label to this PR before merging. |
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Scanned FilesNone |
This was referenced Jun 30, 2026
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #12286 +/- ##
==========================================
+ Coverage 52.88% 53.22% +0.34%
==========================================
Files 751 757 +6
Lines 48353 49321 +968
==========================================
+ Hits 25570 26250 +680
- Misses 20385 20593 +208
- Partials 2398 2478 +80 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Implements the bidirectional ("backwards") secret flow (radius#12288): a recipe
can declare a secretOutputs mapping from a bound Radius.Security/secrets resource's
data keys to a module's secure outputs. Radius resolves these against the raw module
outputs, writes the values into the bound secret (encrypted at rest via x-radius-retain),
and refreshes the secret's backing Kubernetes Secret so connected containers consume
them through a plain secretKeyRef.
- pkg/recipes/secretwriteback.go: ResolveSecretWriteBacks resolver (fail-closed)
- pkg/recipes/types.go: SecretWriteBack type and RecipeOutput.SecretWriteBacks
- pkg/recipes/driver/{bicep,terraform}: resolve secretOutputs on raw module outputs,
routing secure-typed outputs into secrets so they are not surfaced as plain values
- pkg/portableresources/backend/controller: applySecretWriteBacks executor
- pkg/corerp + swagger + typespec: recipePack secretOutputs API field and conversions
Signed-off-by: willdavsmith <[email protected]>
Radius functional test overviewClick here to see the test run details
Test Status⌛ Building Radius and pushing container images for functional tests... |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Securely flows developer-authored secrets into the parameters of direct-module recipes, and makes
Radius.Security/secretsdurable and multi-cluster-safe by retaining the encrypted value at rest and decrypting it from Radius's own store at recipe time. Addresses #12244 (Phase 2 external secret stores remain out of scope, so this does not close the issue).This consolidates the previously stacked drafts #12249 (connection-based secrets) and #12254 (secret-binding references) into a single PR, and supersedes #12254's "read the deployed Kubernetes Secret" approach with retain-at-rest (see Secret handling at rest below).
Two developer-facing ways to hand a secret to a recipe
Applications.Core/secretStores,Radius.Security/secrets) resolves as{{context.resource.connections.<name>.secrets.<key>}}.x-radius-secret-binding(asecretsarray ofRadius.Security/secretsresource IDs) resolves as{{context.resource.secrets.<secretName>.<key>}}.Both paths resolve secrets as whole values only (a secret interpolated into a surrounding string is rejected), tag the resulting parameters secure, and route them through each driver's native secret mechanism (ARM
@secure()/ Terraformsensitive = true). Radius never logs the parameter map.Secret handling at rest (multi-cluster-safe)
Radius.Security/secretsdata values are markedx-radius-sensitiveandx-radius-retain, so the encrypted value is persisted at rest instead of being nulled, while remaining redacted on GET/LIST. At recipe time the secrets loader decrypts the retained value from Radius's own store — it does not read a Kubernetes Secret. This lets a recipe deploy to a different target cluster (the app-owned K8s Secret is not part of the infrastructure deploy). The earlier Kubernetes-Secret migration fallback has been removed: if a secret value is not retained at rest, the loader fails closed (loudly) rather than silently reading from the cluster.Changes by layer
pkg/schema): addsx-radius-secret-binding(+ extractor) andx-radius-retainannotations and helpers; the validator enforcesx-radius-retainonly onx-radius-sensitivefields.pkg/recipes/types.go): a taintedSecrets map[string]string(json:"-") lane onConnectedResource/recipe metadata, so secret material is never serialized alongside regular data.pkg/portableresources/backend/controller): persists the encrypted retained value at rest; redacts retain fields on GET/LIST even atSucceeded; resolvesx-radius-secret-bindingproperties into siblingRadius.Security/secretsresource IDs (fail-closed on malformed references).pkg/dynamicrp/secretsloader, new): a type-aware dispatcher.Radius.Security/secretsis decrypted from its retained at-rest value (schema-driven, via the UCP schema client); other secret store types delegate to the existingApplications.Core/secretStoresloader. Cleartext is reachable only by the in-process engine — never re-persisted, never exposed via GET.pkg/recipes/engine): enriches connection secrets and secret bindings into the recipe's taintedSecrets(fail-closed: a bound secret that cannot be loaded fails the deployment rather than passing a literal{{…}}placeholder).pkg/recipes/paramresolver,driver/bicep,terraform): resolvescontext.resource.connections.<name>.secrets.<key>andcontext.resource.secrets.<secretName>.<key>, whole-value-only, secure-tagged, routed to@secure()/sensitive.deploy/manifest/built-in-providers/{dev,self-hosted}/secrets.yaml): marks the secretvaluefieldx-radius-retain: trueso the built-inRadius.Security/secretstype retains its encrypted value at rest.Testing
pkg/schema,pkg/dynamicrp/secretsloader,pkg/portableresources/backend/controller,pkg/recipes/{engine,paramresolver,terraform}, andpkg/dynamicrp/frontend(redaction).gofmt/go vetclean; packages green, including the UCP initializer test that loads the edited built-in manifests.resource-types-verificationprovisions a real Azure PostgreSQL flexible server via a recipe that consumes a developerRadius.Security/secrets, and proves the app container connects to the DB using the password decrypted from Radius's store with the Kubernetes fallback removed — the resource value is retained encrypted at rest, never read from a cluster Secret.Out of scope