Skip to content

feat: redact sensitive data in dry runs outputs#43

Open
sebastian-stephan wants to merge 5 commits intomainfrom
masked-printer
Open

feat: redact sensitive data in dry runs outputs#43
sebastian-stephan wants to merge 5 commits intomainfrom
masked-printer

Conversation

@sebastian-stephan
Copy link
Copy Markdown
Contributor

Fixes #9.

Redacts any obvious secrets in the dry run output by collecting secrets and using a custom writer.

@clementnuss
Copy link
Copy Markdown
Contributor

after a discussion with Claude, the following points surfaced.

  1. is pretty important.
  2. I'm not sure if that's really an issue. if a diff ends with a secret and the secret isn't printed that's not too bad. but ideally it should end with the **redacted** string.
  3. I didn't dive in the logic enough for that one, up to you.
  4. I think this is a valid point as well, we discussed and decided we would include the ClusterID.

I'll try to fix 1, 2 and 4, and leave 3 to you if that's ok for you


  1. Data race on maskedPrinterAddSecrets is called concurrently from all node goroutines in Nodes() on the shared topf.maskedPrinter, no mutex protection

  2. Flush() never calledmaskedwriter buffers bytes internally to handle secrets that span multiple Write calls. If the dry-run output ends with bytes that happen to be a prefix of a registered secret, those bytes stay in the buffer and are never written to stdout. apply.go only calls fmt.Fprintln(n.t.MaskedPrinter(), ...) with no subsequent Flush(). A simple fix would be to call Flush() after each print, or wrap MaskedPrinter() in an io.WriteCloser that flushes on Close.

  3. Prefix-of-a-prefix bug in maskedwriter — In bufferEndsWithSecretPrefix, maxFound accumulates across the secrets loop. Once a longer partial match has raised maxFound to N, any shorter secret of length ≤ N can never be detected as a full match because the inner for i := maxLen; i > maxFound loop never executes. Concretely: with secrets ["abcd", "abc"] and input "abce", the buffer holds "abc" waiting for a possible "abcd" match; when 'e' arrives no suffix matches any secret prefix, so all 4 bytes are flushed as-is and "abc" is never redacted.

  4. Cluster.ID missing from collectSecrets — The cluster ID appears in the Talos machine config that the dry-run diff outputs, but bundle.Cluster.ID is not included in the list of secrets collected in secrets.go.

@sebastian-stephan
Copy link
Copy Markdown
Contributor Author

Should be addressed now with a different approach. Flush was also the wrong semantics. io.WriteCloser makes more sense here. Close() is the signal that tells the writer that we're finished, so it can flush out whatever is pending (while making sure any pending short matches are redacted).

The maskedwriter is not optimized for performance, but I think it's more readable now and should be correct.

@sebastian-stephan sebastian-stephan marked this pull request as ready for review April 16, 2026 13:33
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.

redact sensitive material in dry-run apply

2 participants