Resolve secretName / Radius.Security/secrets references into direct-module recipe parameters#12254
Closed
willdavsmith wants to merge 2 commits into
Closed
Conversation
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]>
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Scanned FilesNone |
5 tasks
Contributor
There was a problem hiding this comment.
Pull request overview
Adds support for resolving x-radius-secret-reference string properties (e.g., secretName referencing a Radius.Security/secrets resource by name) into direct-module recipe parameters via {{context.resource.secrets.<key>}}, by loading cleartext from the secret’s backing Kubernetes core/Secret output resource at execution time.
Changes:
- Introduces
x-radius-secret-referenceschema annotation plus schema traversal to find annotated property paths. - Extends the portable-resources controller and recipe engine to resolve annotated property values into secret IDs and fail-closed when referenced secrets can’t be loaded.
- Adds a DynamicRP secrets loader that can read
Radius.Security/secretsfrom the backing Kubernetes Secret and wires drivers/resolver to supportcontext.resource.secrets.<key>.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| pkg/schema/validator.go | Defines the x-radius-secret-reference annotation constant and documents semantics. |
| pkg/schema/annotations.go | Implements schema traversal to extract secret-reference property paths. |
| pkg/schema/annotations_test.go | Adds unit coverage for extracting secret-reference field paths. |
| pkg/recipes/types.go | Extends ResourceMetadata to carry SecretReferences and tainted Secrets (non-serialized). |
| pkg/recipes/terraform/execute.go | Threads recipe-level secrets into the in-memory recipe context before resolving params. |
| pkg/recipes/recipecontext/types.go | Adds tainted Resource.Secrets field (non-serialized) for context.resource.secrets.*. |
| pkg/recipes/paramresolver/resolver.go | Extends secret lookup to include context.resource.secrets.<key>. |
| pkg/recipes/paramresolver/resolver_test.go | Adds resolver tests for context.resource.secrets.* and whole-value enforcement. |
| pkg/recipes/engine/engine.go | Adds fail-closed secret-reference enrichment (enrichSecretReferences). |
| pkg/recipes/engine/engine_test.go | Adds unit tests for secret-reference enrichment success/failure paths. |
| pkg/recipes/driver/bicep/bicep.go | Threads secret-reference secrets into context for direct-module param resolution. |
| pkg/portableresources/backend/controller/createorupdateresource.go | Extracts secret-reference paths from schema and builds SecretReferences in recipe metadata. |
| pkg/portableresources/backend/controller/createorupdateresource_test.go | Adds unit tests for building secret references and path lookup helper. |
| pkg/dynamicrp/secretsloader/secretsloader.go | Adds dispatching secrets loader: UDT secrets from K8s Secret, others via existing store loader. |
| pkg/dynamicrp/secretsloader/secretsloader_test.go | Adds tests for K8s-backed secret loading, key filtering, routing, and fail-closed cases. |
| pkg/dynamicrp/options.go | Wires the new dispatching secrets loader into DynamicRP recipe engine options. |
Comment on lines
+336
to
+351
| for secretID := range secretIDs { | ||
| // A nil keys filter loads all secret keys for the referenced secret. | ||
| loaded, err := e.options.SecretsLoader.LoadSecrets(ctx, map[string][]string{secretID: nil}) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to load referenced secret %q: %w", secretID, err) | ||
| } | ||
|
|
||
| data, ok := loaded[secretID] | ||
| if !ok { | ||
| return fmt.Errorf("referenced secret %q returned no data", secretID) | ||
| } | ||
|
|
||
| for key, val := range data.Data { | ||
| recipe.Secrets[key] = val | ||
| } | ||
| } |
Comment on lines
+362
to
+375
| references := map[string]string{} | ||
| for _, path := range secretReferencePaths { | ||
| name, ok := stringValueAtPath(properties, path) | ||
| if !ok || name == "" { | ||
| // The reference property is optional and unset; nothing to resolve. | ||
| continue | ||
| } | ||
|
|
||
| secretID := fmt.Sprintf("%s/providers/%s/%s", parsed.RootScope(), secretReferenceResourceType, name) | ||
| if _, err := resources.ParseResource(secretID); err != nil { | ||
| return nil, fmt.Errorf("failed to construct secret resource ID for property %q with value %q: %w", path, name, err) | ||
| } | ||
| references[path] = secretID | ||
| } |
…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]>
lakshmimsft
reviewed
Jun 26, 2026
|
|
||
| const ( | ||
| testSecretID = "/planes/radius/local/resourceGroups/test-rg/providers/Radius.Security/secrets/db-secret" | ||
| testStoreID = "/planes/radius/local/resourceGroups/test-rg/providers/Applications.Core/secretStores/store" |
Contributor
There was a problem hiding this comment.
checking: why are we referring to Applications.Core/secretsStores in this pr. This type is planned to be deprecated in the future.
This was referenced Jun 30, 2026
Contributor
Author
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
This PR adds the second developer-facing path for handing a secret to a direct-module recipe: a resource property that references a
Radius.Security/secretsresource by name (markedx-radius-secret-reference, e.g.secretName), resolved into recipe parameters ascontext.resource.secrets.<key>. It builds on the connection-based path from #12249.Why this is Phase 1 (not Phase 2)
Phase 2 = external secret stores (Azure Key Vault / AWS Secrets Manager / Vault) where Radius holds only a reference. This PR reads Radius's own Kubernetes materialization of a developer-supplied secret — the same consumption model
Applications.Core/secretStoresalready uses in production — so it is in-scope Phase 1. Scoped to Kubernetes-backedRadius.Security/secrets; externally-backed secrets remain Phase 2.How it works
Because a secret's sensitive data is redacted from the database before recipe execution, the cleartext is read on demand from the secret's deployed Kubernetes Secret via
status.outputResources(never the redactedProperties.Data).pkg/schema): adds thex-radius-secret-referenceannotation andExtractSecretReferenceFieldPaths, which returns the property paths that name a secret.pkg/portableresources/backend/controller): resolves each referenced property value (a secret name) into a siblingRadius.Security/secretsresource ID scoped to the consuming resource's resource group, threaded ontoResourceMetadata.SecretReferences. A malformed reference is an error (fail-closed).pkg/recipes/engine):enrichSecretReferencesloads the referenced secret's material into the recipe's taintedSecrets. Fail-closed: a referenced secret that cannot be loaded fails the deployment rather than passing a literal{{…}}placeholder to the module.pkg/dynamicrp/secretsloader, new): a type-aware dispatcher.Radius.Security/secretsis read from its backing Kubernetes Secret (located via the resource'score/Secretoutput resource); all other secret store types delegate to the existingApplications.Core/secretStoresloader. Cleartext is reachable only by the in-process engine — never re-persisted to the database and never exposed via a GET.context.resource.secrets.<key>with the same whole-value-only + secure-tagging rules as the connection path (ARM@secure()/ Terraformsensitive = true).Testing
Test_DispatchingLoader_*): reads cleartext from the backing K8s Secret, honors a requested-key filter, routes UDT vs store types and merges results, and fails closed when there is no output resource / the Secret is absent / a requested key is missing / the resource can't be read.Test_enrichSecretReferences): populatesrecipe.Secrets; fails closed on loader error / missing data / unconfigured loader; empty references are a no-op.TestExtractSecretReferenceFieldPaths), controller (Test_buildSecretReferences,Test_stringValueAtPath), and resolver (Test_SecretExpressions, extended forresource.secrets.<key>resolution, secure-tagging, and whole-value-only enforcement).gofmt -l(clean),go build ./...,go vet, andgo testare green across all affected packages; existing Securely flow developer secrets into direct-module recipe parameters #12249 tests are unaffected.Out of scope
Addresses #12244 (Phase 2 reference-passthrough remains open, so this does not close the issue).