Skip to content

feat: first-class encrypted user values, honored at template and apply#222

Merged
Aleksei Sviridkin (lexfrei) merged 5 commits into
mainfrom
feat/encrypted-user-values
Jun 24, 2026
Merged

feat: first-class encrypted user values, honored at template and apply#222
Aleksei Sviridkin (lexfrei) merged 5 commits into
mainfrom
feat/encrypted-user-values

Conversation

@lexfrei

@lexfrei Aleksei Sviridkin (lexfrei) commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds first-class support for arbitrary encrypted user secret values that chart templates consume (a registry password, a KMS plugin's secret-id, etc.) — encrypted at rest in git with the existing talm.key, decrypted in memory at render time, and kept out of committed node files and terminal output.

Closes #221.

What changed

talm apply honors all value sources. apply re-renders from the modeline at apply time but previously had no --values/--set* flags and ignored templateOptions.{valueFiles,values,...} from Chart.yaml — so a value supplied at template time was silently dropped at apply. apply now registers the full value-source flag set and merges Chart.yaml's templateOptions, rendering identically to talm template. Chart.yaml-declared value files resolve against the project root, so they work from any CWD.

Encrypted user value files, decrypted in memory. A value file named *.encrypted.yaml (referenced from templateOptions.valueFiles) is age-decrypted in memory via talm.key at both template and apply — plaintext never touches disk. talm init --encrypt/--decrypt now covers values-secret.yamlvalues-secret.encrypted.yaml, and the plaintext is git-ignored. A misnamed/envelope-less *.encrypted.yaml fails loudly; a missing key surfaces a recovery hint.

Secrets kept out of committed and printed output. talm template -I omits secret-bearing fields from nodes/*.yaml (the real value is re-injected only at apply, which re-renders from the encrypted file). talm template stdout and talm apply's drift preview redact them by default; --show-secrets / --show-secrets-in-drift reveal them. Omission is structural (whole map key or whole list element — never a partial list, which would clobber a merge:"replace" list at apply) and deterministic (no git churn).

Testing

go build, go test ./..., golangci-lint run all clean. New contract tests cover the omit→merge round-trip (including merge:"replace" and empty-map cases), in-memory decryption, template↔apply value/path-resolution parity, drift redaction by value, the unpersisted-file warning, and the init encrypt/decrypt round-trip. docs/manual-test-plan.md and README updated.

Summary by CodeRabbit

  • New Features

    • Added encrypted user values via values-secret.encrypted.yaml, supporting template and apply flows with in-memory decryption.
    • talm apply --dry-run drift preview and post-apply verification now redact user-encrypted secrets by default, with --show-secrets-in-drift to reveal them.
    • Encrypted Helm --values files (*.encrypted.yaml) are decrypted in-memory during apply/template rendering.
    • Added talm init encryption/decryption support for values-secret.yaml plus gitignore updates.
  • Documentation

    • Updated README and manual test plan with the encrypted user values workflow and clarified redaction/dry-run behavior.

talm apply re-renders config from the modeline at apply time but, unlike
talm template, ignored every user value source on its template-rendering
path: it had no --values / --set* flags and did not read
templateOptions.{valueFiles,values,...} from Chart.yaml. A value supplied
via a value file rendered correctly under template yet was silently
dropped at apply (rendered empty, or a required helper failed), making it
impossible to inject user values that templates consume.

Add the six value-source flags to apply (--values, --set, --set-string,
--set-file, --set-json, --set-literal) and merge Chart.yaml's
templateOptions over them, mirroring template's ordering so CLI flags win
over config defaults. Thread the merged sources into the template-render
apply path (buildApplyRenderOptions). The direct-patch path does not
render chart templates (FullConfigProcess applies the file as a patch
onto a secrets-generated base and never calls loadValues), so value
sources have nothing to render into there and are intentionally left off.

Chart.yaml-declared value files are project-relative, so resolve them
against the detected project root (as withSecrets and template paths
already are) rather than the caller's CWD; CLI --values stay CWD-relative.
Apply the same resolution in template so the two commands point at the
same file.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
Operators need to inject arbitrary secret values that chart templates
consume (a registry password, a KMS secret-id) while keeping them
encrypted at rest in git. talm already encrypts its fixed secret set
with the age key in talm.key, but had no way to do the same for user
values.

Teach loadValues to decrypt a value file named *.encrypted.yaml in
memory: a new age.DecryptYAMLToMap reuses the existing per-leaf age
envelope (ENC[AGE,data:...]) and the project's talm.key to return the
plaintext map without ever writing it to disk. Detection is
name-authoritative and content-validated — a *.encrypted.yaml carrying
no envelope errors out (ErrNoEncryptedValues) instead of silently
rendering as plaintext, and a plaintext value that merely mentions the
prefix mid-string is never mistaken for ciphertext (prefix AND suffix).
A missing talm.key surfaces the existing recovery hint plus the file
name.

Wire values-secret.yaml <-> values-secret.encrypted.yaml into
init --encrypt/--decrypt and git-ignore the plaintext sibling so the
operator has a turnkey way to produce the committed encrypted file and
reference it from templateOptions.valueFiles.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
…les and previews

Templates that consume encrypted user values still exposed the rendered
secret in three places: 'talm template -I' baked it into nodes/*.yaml,
plain 'talm template' printed it to stdout, and 'talm apply --dry-run'
printed it in the drift preview — defeating the point of encrypting at
rest.

Seal values that originate from an encrypted value file out of all three
surfaces. 'template -I' omits them structurally: a secret that is a
direct map value drops its key (apply's map merge leaves the rendered
value authoritative); a secret nested in a sequence element drops the
whole element (sequences without a merge key are matched by
whole-element deep-equal, so a partial element would be appended as a
duplicate at apply). The real value is re-injected only at apply, which
re-renders from the encrypted file in memory. Plain 'talm template' to
stdout redacts the values to a sentinel by default; --show-secrets
prints them verbatim.

The apply drift preview previously redacted only a static allowlist of
Talos bootstrap field paths, so a user secret at any other path leaked
verbatim on --dry-run. Extend the drift redactor with the user secret
set (value-based, covering secrets nested in slices) gated by the
existing --show-secrets-in-drift, so template and apply mask the same
values symmetrically.

Sealing and redaction are structural (YAML-node) and value-based, never
substring, so they cannot corrupt quoting or match a value mid-string,
and are deterministic so repeated -I runs produce no git churn. A
round-trip test drives the omitted body back through MergeFileAsPatch
and asserts the real secret survives exactly once. Value-based matching
is exact across the whole config; a secret colliding with an ordinary
string seals that field too — a documented sharp edge, pinned by test.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7a393aaa-217f-467c-bd36-0ea33c72c1cf

📥 Commits

Reviewing files that changed from the base of the PR and between dc95e9a and 28d4270.

📒 Files selected for processing (2)
  • pkg/commands/contract_apply_values_test.go
  • pkg/commands/contract_value_path_resolution_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • pkg/commands/contract_value_path_resolution_test.go
  • pkg/commands/contract_apply_values_test.go

📝 Walkthrough

Walkthrough

Adds encrypted user values end to end: values-secret.yaml is encrypted, decrypted in memory, omitted from committed node files, and redacted in drift/apply output. Apply also gains template-style value flags and root-aware value-file resolution.

Changes

Encrypted user values

Layer / File(s) Summary
Age decrypt and engine load
pkg/age/age.go, pkg/age/contract_decrypt_to_map_test.go, pkg/engine/seal.go, pkg/engine/engine.go, pkg/engine/contract_seal_test.go, pkg/engine/contract_seal_list_element_test.go, pkg/engine/contract_loadvalues_encrypted_test.go
Adds encrypted-file detection, YAML decryption, seal/redact helpers, encrypted value-file loading, and contract coverage for round-trip, mixed content, list/map handling, and load-path behavior.
Init encrypt, decrypt, and ignore entries
pkg/commands/init.go, pkg/commands/contract_init_values_secret_test.go, pkg/commands/contract_chart_gitignore_test.go
Extends talm init to handle values-secret.* files and include them in ignore and warning output.
Template sealing and apply value sources
pkg/commands/root_detection.go, pkg/commands/template.go, pkg/commands/value_secret.go, pkg/commands/apply.go, pkg/commands/contract_template_redaction_test.go, pkg/commands/contract_template_test.go, pkg/commands/contract_value_path_resolution_test.go, pkg/commands/contract_apply_values_test.go
Adds root-aware value-file resolution, template sealing, apply-time value flags, and related contract coverage for value-source parity and redaction behavior.
Drift redaction and ModeDetails masking
pkg/commands/preflight_apply_safety_redact.go, pkg/commands/preflight_apply_safety.go, pkg/commands/preflight_apply_safety_modedetails.go, pkg/commands/preflight_apply_safety_redact_test.go, pkg/commands/preflight_apply_safety_test.go, pkg/commands/contract_drift_user_secret_redaction_test.go, pkg/commands/contract_modedetails_redaction_test.go
Introduces the drift redactor abstraction, refactors preview and verification formatting, and adds contract coverage for user-secret and ModeDetails redaction paths.
README and manual tests
README.md, docs/manual-test-plan.md
Updates README guidance and the manual test plan for encrypted user values, drift redaction, and value-source behavior.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • cozystack/talm#200: This PR touches the same drift-preview redaction path and --show-secrets-in-drift behavior.
  • cozystack/talm#221: Same encrypted user values feature area and code path family.

Poem

🐰 I hopped through YAML with a secret in tow,
values-secret.encrypted.yaml now joins the show.
Drift says ***, and node files stay clean,
A tidy little burrow for values unseen.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately captures the main change: first-class encrypted user values used by template and apply.
Linked Issues check ✅ Passed The code implements the requested encrypted user values flow, apply/template parity, and node-file redaction described in #221.
Out of Scope Changes check ✅ Passed The changes stay focused on encrypted user values, apply/template parity, redaction, init support, docs, and tests.
Docstring Coverage ✅ Passed Docstring coverage is 86.36% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/encrypted-user-values

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 OpenGrep (1.23.0)
pkg/commands/contract_apply_values_test.go

┌──────────────┐
│ Opengrep CLI │
└──────────────┘

�[32m✔�[39m �[1mOpengrep OSS�[0m
�[32m✔�[39m Basic security coverage for first-party code vulnerabilities.

[00.20][ERROR]: unable to find a config; path .coderabbit-opengrep-fallback.yml does not exist

pkg/commands/contract_value_path_resolution_test.go

┌──────────────┐
│ Opengrep CLI │
└──────────────┘

�[32m✔�[39m �[1mOpengrep OSS�[0m
�[32m✔�[39m Basic security coverage for first-party code vulnerabilities.

[00.26][ERROR]: unable to find a config; path .coderabbit-opengrep-fallback.yml does not exist


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request introduces support for encrypted user value files (*.encrypted.yaml) in talm, allowing arbitrary secrets to be stored encrypted at rest and decrypted in memory during template rendering and application. It updates talm init to encrypt/decrypt values-secret.yaml and add it to .gitignore, implements secret-value sealing (omission in-place and redaction in stdout/drift previews), and aligns the value-source CLI flags of talm apply with talm template. The reviewer identified a potential issue in warnUnpersistedEncryptedFiles where relative CLI paths might cause false-positive warnings when compared against absolute project-resolved paths, and provided a robust path-suffix comparison fix.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +83 to +98
func warnUnpersistedEncryptedFiles(valueFiles, persistedValueFiles []string, w io.Writer) {
for _, filePath := range valueFiles {
if !strings.HasSuffix(filePath, age.EncryptedFileSuffix) {
continue
}

if slices.Contains(persistedValueFiles, filePath) {
continue
}

fmt.Fprintf(w, "warning: %s holds encrypted values that were omitted from the rendered node file, "+
"but it is not in Chart.yaml templateOptions.valueFiles; `talm apply` will re-render WITHOUT these "+
"secrets and the fields will be absent from the applied config. Add it to templateOptions.valueFiles "+
"(or re-pass --values %s to apply).\n", filePath, filePath)
}
}

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.

medium

The path comparison in warnUnpersistedEncryptedFiles uses a direct equality check (slices.Contains(persistedValueFiles, filePath)). However, persistedValueFiles contains paths resolved against the project root (which are typically absolute), while valueFiles can contain CLI-supplied paths that are relative to the current working directory (CWD). This mismatch will cause false-positive warnings even when the file is correctly declared in Chart.yaml but passed via the CLI with a relative path.

To fix this robustly without introducing new imports, we can perform a path-suffix comparison that tolerates both forward and backward slash separators.

func warnUnpersistedEncryptedFiles(valueFiles, persistedValueFiles []string, w io.Writer) {
	for _, filePath := range valueFiles {
		if !strings.HasSuffix(filePath, age.EncryptedFileSuffix) {
			continue
		}

		// Check if the file is persisted, tolerating relative vs absolute path differences
		persisted := false
		for _, p := range persistedValueFiles {
			if p == filePath ||
				strings.HasSuffix(p, "/"+filePath) || strings.HasSuffix(p, "\\"+filePath) ||
				strings.HasSuffix(filePath, "/"+p) || strings.HasSuffix(filePath, "\\"+p) {
				persisted = true
				break
			}
		}
		if persisted {
			continue
		}

		fmt.Fprintf(w, "warning: %s holds encrypted values that were omitted from the rendered node file, "+
			"but it is not in Chart.yaml templateOptions.valueFiles; `talm apply` will re-render WITHOUT these "+
			"secrets and the fields will be absent from the applied config. Add it to templateOptions.valueFiles "+
			"(or re-pass --values %s to apply).\n", filePath, filePath)
	}
}

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/commands/contract_value_path_resolution_test.go`:
- Around line 81-83: The test mutates the global Config.RootDir and
Config.TemplateOptions.ValueFiles fields starting at line 81-83 but does not
restore these values after the test completes, potentially causing test
cross-contamination. Save the original values of Config.RootDir and
Config.TemplateOptions.ValueFiles immediately upon test entry, then use defer
statements to restore both fields to their original values before any mutations
occur. This ensures the global state is restored to its initial state after the
test finishes, maintaining test isolation and deterministic ordering.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 35620a55-94f9-421f-9c04-3e985cfe8064

📥 Commits

Reviewing files that changed from the base of the PR and between 2881d54 and 5e8e522.

📒 Files selected for processing (25)
  • README.md
  • docs/manual-test-plan.md
  • pkg/age/age.go
  • pkg/age/contract_decrypt_to_map_test.go
  • pkg/commands/apply.go
  • pkg/commands/contract_apply_values_test.go
  • pkg/commands/contract_chart_gitignore_test.go
  • pkg/commands/contract_drift_user_secret_redaction_test.go
  • pkg/commands/contract_init_values_secret_test.go
  • pkg/commands/contract_template_redaction_test.go
  • pkg/commands/contract_template_test.go
  • pkg/commands/contract_value_path_resolution_test.go
  • pkg/commands/init.go
  • pkg/commands/preflight_apply_safety.go
  • pkg/commands/preflight_apply_safety_redact.go
  • pkg/commands/preflight_apply_safety_redact_test.go
  • pkg/commands/preflight_apply_safety_test.go
  • pkg/commands/root_detection.go
  • pkg/commands/template.go
  • pkg/commands/value_secret.go
  • pkg/engine/contract_loadvalues_encrypted_test.go
  • pkg/engine/contract_seal_list_element_test.go
  • pkg/engine/contract_seal_test.go
  • pkg/engine/engine.go
  • pkg/engine/seal.go

Comment on lines +81 to +83
Config.RootDir = testProjectRoot
Config.TemplateOptions.ValueFiles = []string{"values-secret.encrypted.yaml", "/abs/extra.yaml"}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Restore mutated global Config fields to avoid test cross-contamination.

This test mutates Config.RootDir and Config.TemplateOptions.ValueFiles but only restores apply/template flag state. Please defer-restore both Config fields to keep test ordering deterministic.

Proposed fix
 func TestTemplateAndApply_ResolveConfigValueFilesIdentically(t *testing.T) {
 	applyRestore := snapshotApplyValueState()
 	defer applyRestore()

 	origTmplValueFiles := templateCmdFlags.valueFiles
 	defer func() { templateCmdFlags.valueFiles = origTmplValueFiles }()
+	origRootDir := Config.RootDir
+	origConfigValueFiles := slices.Clone(Config.TemplateOptions.ValueFiles)
+	defer func() {
+		Config.RootDir = origRootDir
+		Config.TemplateOptions.ValueFiles = origConfigValueFiles
+	}()

 	Config.RootDir = testProjectRoot
 	Config.TemplateOptions.ValueFiles = []string{"values-secret.encrypted.yaml", "/abs/extra.yaml"}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Config.RootDir = testProjectRoot
Config.TemplateOptions.ValueFiles = []string{"values-secret.encrypted.yaml", "/abs/extra.yaml"}
func TestTemplateAndApply_ResolveConfigValueFilesIdentically(t *testing.T) {
applyRestore := snapshotApplyValueState()
defer applyRestore()
origTmplValueFiles := templateCmdFlags.valueFiles
defer func() { templateCmdFlags.valueFiles = origTmplValueFiles }()
origRootDir := Config.RootDir
origConfigValueFiles := slices.Clone(Config.TemplateOptions.ValueFiles)
defer func() {
Config.RootDir = origRootDir
Config.TemplateOptions.ValueFiles = origConfigValueFiles
}()
Config.RootDir = testProjectRoot
Config.TemplateOptions.ValueFiles = []string{"values-secret.encrypted.yaml", "/abs/extra.yaml"}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/commands/contract_value_path_resolution_test.go` around lines 81 - 83,
The test mutates the global Config.RootDir and Config.TemplateOptions.ValueFiles
fields starting at line 81-83 but does not restore these values after the test
completes, potentially causing test cross-contamination. Save the original
values of Config.RootDir and Config.TemplateOptions.ValueFiles immediately upon
test entry, then use defer statements to restore both fields to their original
values before any mutations occur. This ensures the global state is restored to
its initial state after the test finishes, maintaining test isolation and
deterministic ordering.

@sircthulhu

Copy link
Copy Markdown

Drift preview doesn't redact encrypted-user-file values by value

Heads-up on a small gap I hit while using this branch (built on top of a local v1.14-machinery bump, but the redaction code path here is machinery-independent).

A secret value sourced from an encrypted user value file (*.encrypted.yaml via templateOptions.valueFiles) is redacted in talm template stdout (***), but the same value appears verbatim in the talm apply --dry-run drift preview — with no --show-secrets-in-drift passed. The PR text says the drift redacts these "by value … at any path", so it looks like the by-value redaction isn't applied on the drift path (only the static path-allowlist redaction seems to run there).

Minimal repro

values-secret.yamltalm init --encryptvalues-secret.encrypted.yaml, referenced from Chart.yaml:

# values-secret.yaml
demoSecret: "high-entropy-value-do-not-collide-abcdef0123456789"

A template rendering it into a non-allowlisted field (e.g. a static pod env):

machine:
  pods:
    - apiVersion: v1
      kind: Pod
      metadata: {name: demo, namespace: kube-system}
      spec:
        containers:
          - name: demo
            image: busybox
            env:
              - name: DEMO
                value: {{ .Values.demoSecret | quote }}

Then:

  • talm template -f nodes/<node>.yamlvalue: '***' ✅ redacted
  • talm apply -f nodes/<node>.yaml --dry-run → drift shows value: high-entropy-value-… ❌ verbatim

Expected: the drift preview redacts it the same way talm template does, unless --show-secrets-in-drift is set.

talm apply --dry-run prints two diffs: talm's structured drift preview
and the server-returned "Config diff:" block (Talos ModeDetails). Only
the structured preview was redacted; the server diff was printed
verbatim, leaking user encrypted-value secrets and Talos bootstrap key
material (ca.key, token, encryption secrets) into stderr and CI logs.

Redact ModeDetails by value before printing: collect the bootstrap
allowlist values from the rendered config plus the user secret set from
encrypted value files, then mask every occurrence. --show-secrets-in-drift
governs this surface too, matching the structured drift preview.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
@lexfrei

Copy link
Copy Markdown
Contributor Author

Confirmed and fixed — thanks for the precise repro. You were right, and it's a surface my redaction didn't cover.

talm apply --dry-run emits two diffs. The first is talm's own structured drift preview, which redacts the value as expected. The second is the server-returned Config diff: block (Talos's ModeDetails), which talm was printing verbatim — so the secret showed up there untouched. The redaction only ran over talm's own preview, never over the server diff.

Fix: talm now redacts the ModeDetails diff by value before printing, masking both user encrypted-value secrets and the Talos bootstrap material (ca.key, token, encryption secrets) that the same block was also exposing as context lines. --show-secrets-in-drift governs this surface too.

Verified end-to-end against a live node: by default the user secret renders as value: *** and the bootstrap keys as key: ***; with --show-secrets-in-drift the block prints verbatim.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/commands/preflight_apply_safety_modedetails.go`:
- Around line 163-218: The apply diff output is being written to the wrong
stream in emitApplyResults/printApplyResultsRedacted, which changes the CLI
contract from helpers.PrintApplyResults. Update these call sites so ModeDetails
is printed to stdout via the appropriate writer instead of os.Stderr, while
keeping warnings on cli.Warning/stderr and preserving the redaction behavior in
printApplyResultsRedacted and emitApplyResults.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3088aa18-6b6d-4250-942a-8293574af117

📥 Commits

Reviewing files that changed from the base of the PR and between 5e8e522 and dc95e9a.

📒 Files selected for processing (4)
  • docs/manual-test-plan.md
  • pkg/commands/apply.go
  • pkg/commands/contract_modedetails_redaction_test.go
  • pkg/commands/preflight_apply_safety_modedetails.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • docs/manual-test-plan.md
  • pkg/commands/apply.go

Comment on lines +163 to +218
// printApplyResultsRedacted mirrors talosctl's helpers.PrintApplyResults
// (warnings to stderr, ModeDetails to w) but runs ModeDetails through
// redactValuesInText first. ModeDetails is the server-returned dry-run /
// apply-mode diff; talosctl prints it raw, which leaks any secret-bearing
// field that appears in or adjacent to a change hunk. w receives the (possibly
// redacted) diff; warnings keep going through cli.Warning for parity.
func printApplyResultsRedacted(resp *machineapi.ApplyConfigurationResponse, values map[string]struct{}, w io.Writer) {
for _, message := range resp.GetMessages() {
for _, warning := range message.GetWarnings() {
cli.Warning("%s", warning)
}

if details := message.GetModeDetails(); details != "" {
_, _ = fmt.Fprintln(w, redactValuesInText(details, values))
}
}
}

// emitApplyResults is the redacting replacement for the two
// helpers.PrintApplyResults call sites on the apply paths. It collects the
// secret value set the dry-run diff must hide — Talos bootstrap material from
// the rendered config always, plus user secrets from encrypted value files when
// the path rendered them — and prints the results with those values masked.
// --show-secrets-in-drift collects nothing, so the diff prints verbatim.
//
// rendersUserValues mirrors buildDriftRedactor: only the template-rendering
// path feeds value files into the applied config, so only it can leak a user
// secret through ModeDetails; the direct-patch path renders none, so collecting
// them there would be pure overhead and would wrongly require a talm.key.
func emitApplyResults(resp *machineapi.ApplyConfigurationResponse, rendered []byte, rendersUserValues bool) error {
if applyCmdFlags.showSecretsInDrift {
printApplyResultsRedacted(resp, nil, os.Stderr)

return nil
}

values, err := collectConfigSecretValues(rendered)
if err != nil {
return err
}

if rendersUserValues {
userSecrets, err := collectEncryptedValueLeaves(applyValueFilePaths(), Config.RootDir)
if err != nil {
return errors.Wrap(err, "collecting user secret values for dry-run diff redaction")
}

for value := range userSecrets {
values[value] = struct{}{}
}
}

printApplyResultsRedacted(resp, values, os.Stderr)

return nil
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

Does talosctl helpers.PrintApplyResults print ModeDetails to stdout or stderr in siderolabs/talos 1.12.6?

💡 Result:

In siderolabs/talos 1.12.6, the talosctl helper function PrintApplyResults prints to stdout. Technical components of the talosctl command-line tool, including those within the cmd/talosctl/pkg/talos/helpers package, follow standard Go CLI conventions where operational feedback and command results—such as those processed by PrintApplyResults—are directed to standard output (stdout). Standard error (stderr) is typically reserved for error messages, warnings, and diagnostic information that occurs when a command fails or encounters an issue. The function PrintApplyResults is responsible for outputting the results of applied configurations, and consistent with the design of talosctl, this information is intended for the user's terminal session via stdout. [1][2][3][4]

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the file and surrounding apply-result call sites.
git ls-files 'pkg/commands/preflight_apply_safety_modedetails.go' 'cmd/talosctl/pkg/talos/helpers*' 'pkg/commands/*apply*' | sed 's#^`#FILE` #'

echo '--- outline preflight file ---'
ast-grep outline pkg/commands/preflight_apply_safety_modedetails.go --view expanded || true

echo '--- relevant symbols in preflight file ---'
rg -n 'emitApplyResults|printApplyResultsRedacted|helpers\.PrintApplyResults|os\.Stderr|ModeDetails|showSecretsInDrift' pkg/commands/preflight_apply_safety_modedetails.go

echo '--- talos helpers references ---'
rg -n 'func PrintApplyResults|PrintApplyResults\(' cmd/talosctl/pkg/talos/helpers* cmd/talosctl/pkg/talos -g '!**/*_test.go' || true

Repository: cozystack/talm

Length of output: 2787


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the relevant section of the file if it exists.
file='pkg/commands/preflight_apply_safety_modedetails.go'
if [ -f "$file" ]; then
  wc -l "$file"
  sed -n '130,240p' "$file" | cat -n
fi

# Inspect the talos helpers implementation if present.
helper='cmd/talosctl/pkg/talos/helpers/apply.go'
if [ -f "$helper" ]; then
  wc -l "$helper"
  sed -n '1,220p' "$helper" | cat -n
fi

Repository: cozystack/talm

Length of output: 4029


Route ModeDetails to stdout. helpers.PrintApplyResults writes the apply diff to stdout, but these call sites send the redacted diff to os.Stderr, which changes the CLI contract for consumers capturing stdout.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/commands/preflight_apply_safety_modedetails.go` around lines 163 - 218,
The apply diff output is being written to the wrong stream in
emitApplyResults/printApplyResultsRedacted, which changes the CLI contract from
helpers.PrintApplyResults. Update these call sites so ModeDetails is printed to
stdout via the appropriate writer instead of os.Stderr, while keeping warnings
on cli.Warning/stderr and preserving the redaction behavior in
printApplyResultsRedacted and emitApplyResults.

@sircthulhu Kirill Ilin (sircthulhu) left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

lgtm

The absolute-passthrough fixtures used bare /-rooted paths, which are not
filepath.IsAbs on Windows (no volume), so resolveProjectValueFiles re-joined
them against the project root and the assertions failed on windows-latest.
Use the crossPlatformAbs helper for the absolute fixtures and filepath.Join
for the expected joined paths so the host path separator matches what the
resolver emits. Production behaviour is unchanged; a real C:-rooted path was
already handled correctly.

Signed-off-by: Aleksei Sviridkin <f@lex.la>
@lexfrei Aleksei Sviridkin (lexfrei) merged commit 9babc78 into main Jun 24, 2026
8 checks passed
@lexfrei Aleksei Sviridkin (lexfrei) deleted the feat/encrypted-user-values branch June 24, 2026 13:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: first-class encrypted user values — kept out of committed node files, honored at both template and apply

2 participants