Skip to content

Resolve secretName / Radius.Security/secrets references into direct-module recipe parameters#12254

Closed
willdavsmith wants to merge 2 commits into
willdavsmith-recipe-secret-injectionfrom
willdavsmith-recipe-secret-reference
Closed

Resolve secretName / Radius.Security/secrets references into direct-module recipe parameters#12254
willdavsmith wants to merge 2 commits into
willdavsmith-recipe-secret-injectionfrom
willdavsmith-recipe-secret-reference

Conversation

@willdavsmith

@willdavsmith willdavsmith commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Part 2 of 2 · stacked on #12249 (Part 1: connection-based secrets) · this is the secretNameRadius.Security/secrets half ("Option A": read the deployed Kubernetes Secret via status.outputResources). Review/merge #12249 first — this PR's diff is shown against its branch (Part 2 only, 16 files, +942/−6) and will be retargeted to main once Part 1 lands.

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/secrets resource by name (marked x-radius-secret-reference, e.g. secretName), resolved into recipe parameters as context.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/secretStores already uses in production — so it is in-scope Phase 1. Scoped to Kubernetes-backed Radius.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 redacted Properties.Data).

  • Schema (pkg/schema): adds the x-radius-secret-reference annotation and ExtractSecretReferenceFieldPaths, which returns the property paths that name a secret.
  • Controller (pkg/portableresources/backend/controller): resolves each referenced property value (a secret name) into a sibling Radius.Security/secrets resource ID scoped to the consuming resource's resource group, threaded onto ResourceMetadata.SecretReferences. A malformed reference is an error (fail-closed).
  • Engine (pkg/recipes/engine): enrichSecretReferences loads the referenced secret's material into the recipe's tainted Secrets. Fail-closed: a referenced secret that cannot be loaded fails the deployment rather than passing a literal {{…}} placeholder to the module.
  • Loader (pkg/dynamicrp/secretsloader, new): a type-aware dispatcher. Radius.Security/secrets is read from its backing Kubernetes Secret (located via the resource's core/Secret output resource); all other secret store types delegate to the existing Applications.Core/secretStores loader. Cleartext is reachable only by the in-process engine — never re-persisted to the database and never exposed via a GET.
  • Param resolver / drivers: exposes context.resource.secrets.<key> with the same whole-value-only + secure-tagging rules as the connection path (ARM @secure() / Terraform sensitive = true).

Testing

  • Loader (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.
  • Engine (Test_enrichSecretReferences): populates recipe.Secrets; fails closed on loader error / missing data / unconfigured loader; empty references are a no-op.
  • Schema (TestExtractSecretReferenceFieldPaths), controller (Test_buildSecretReferences, Test_stringValueAtPath), and resolver (Test_SecretExpressions, extended for resource.secrets.<key> resolution, secure-tagging, and whole-value-only enforcement).
  • gofmt -l (clean), go build ./..., go vet, and go test are green across all affected packages; existing Securely flow developer secrets into direct-module recipe parameters #12249 tests are unaffected.

Out of scope

  • External (Key Vault / Secrets Manager / Vault) secret references — Phase 2.
  • Docs — planned follow-up.

Addresses #12244 (Phase 2 reference-passthrough remains open, so this does not close the issue).

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]>
@willdavsmith willdavsmith requested review from a team as code owners June 25, 2026 20:14
@github-actions

Copy link
Copy Markdown

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

@willdavsmith willdavsmith changed the title Resolve Radius.Security/secrets references into direct-module recipe parameters Resolve secretName / Radius.Security/secrets references into direct-module recipe parameters Jun 25, 2026
@willdavsmith willdavsmith requested a review from Copilot June 25, 2026 23:44

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-reference schema 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/secrets from the backing Kubernetes Secret and wires drivers/resolver to support context.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]>

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"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

checking: why are we referring to Applications.Core/secretsStores in this pr. This type is planned to be deprecated in the future.

@willdavsmith

Copy link
Copy Markdown
Contributor Author

Superseded by #12286. The consolidated PR also replaces this PR's "read the deployed Kubernetes Secret" approach with retaining the encrypted value at rest and decrypting from Radius's own store (multi-cluster-safe; the Kubernetes fallback was removed). Closing in favor of #12286.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants