Expose plaintext template.data values in template rendering context#1940
Expose plaintext template.data values in template rendering context#1940hamza-younas94 wants to merge 1 commit intobitnami-labs:mainfrom
Conversation
When a SealedSecret defines plaintext keys alongside encrypted keys
inside `spec.template.data`, sibling templates in the same map could
not reference those plaintext keys as `{{ .key }}` variables and the
controller would render `<no value>` instead of the configured
plaintext value.
The template execution context was being built only from the
decrypted entries of `spec.encryptedData`, so any reference to a
plaintext key defined in `spec.template.data` was treated as a
missing field by `text/template`.
Pre-populate the template context with the raw values from
`spec.template.data` before rendering. Encrypted values take
precedence on key collision so a plaintext key can never silently
shadow a real secret value.
Add three regression tests covering:
- pure plaintext template.data with cross-key references
- mixed encrypted + plaintext template.data
- encryptedData precedence over a colliding template.data key
Closes bitnami-labs#1607
Signed-off-by: Hamza Younas <hamza.younas94@gmail.com>
There was a problem hiding this comment.
Pull request overview
Fixes template rendering so plaintext keys in spec.template.data are available in the Go text/template execution context (alongside decrypted spec.encryptedData keys), addressing <no value> substitutions when sibling templates reference plaintext entries.
Changes:
- Pre-populate the template execution context with raw
spec.template.datavalues (without overriding decrypted values). - Add regression tests covering plaintext-only, mixed encrypted+plaintext templating, and collision precedence expectations.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| pkg/apis/sealedsecrets/v1alpha1/sealedsecret_expansion.go | Populates the template rendering context with plaintext template.data entries before executing templates. |
| pkg/apis/sealedsecrets/v1alpha1/sealedsecret_test.go | Adds regression tests for plaintext references, mixed encrypted/plaintext references, and precedence behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Expose raw plaintext values from spec.template.data in the | ||
| // template rendering context, so that templates defined in | ||
| // spec.template.data can reference sibling plaintext keys as | ||
| // {{ .key }} variables (e.g. {{ .username }} alongside an | ||
| // encrypted password). Encrypted values take precedence on key | ||
| // collision so adding a plaintext key can never silently shadow | ||
| // a real secret value. | ||
| // See https://github.com/bitnami-labs/sealed-secrets/issues/1607 | ||
| for key, value := range s.Spec.Template.Data { | ||
| if _, exists := data[key]; !exists { | ||
| data[key] = value | ||
| } | ||
| } |
There was a problem hiding this comment.
The added comment says a plaintext key in spec.template.data can never shadow a real secret value because encrypted values take precedence on key collision, but the current implementation still overwrites secret.Data[key] for every spec.template.data key in the rendering loop below. If a key exists in both encryptedData and template.data, the decrypted value is written first and then replaced by the (plaintext) rendered template value, so plaintext can still shadow the secret in the output Secret. Either (a) enforce precedence by skipping rendering/assignment for colliding keys, or (b) narrow the comment (and PR semantics/tests) to clarify precedence applies only to the template execution context (data map), not the resulting secret.Data.
| ssecret.Spec.Template.Data = map[string]string{ | ||
| "shared": "from-plaintext-should-be-ignored", | ||
| "out.txt": `{{ .shared }}`, | ||
| } | ||
|
|
||
| unsealed, err := ssecret.Unseal(codecs, keys) | ||
| if err != nil { | ||
| t.Fatalf("Unseal returned error: %v", err) | ||
| } | ||
|
|
||
| if got, want := string(unsealed.Data["out.txt"]), "from-encrypted"; got != want { | ||
| t.Errorf("out.txt: got %q, want %q", got, want) | ||
| } |
There was a problem hiding this comment.
TestTemplateDataEncryptedTakesPrecedenceOverPlaintext only asserts that out.txt renders with the encrypted .shared value, but it does not assert what happens to the output shared key itself. Since Unseal() renders every spec.template.data entry into secret.Data, a colliding template.data["shared"] currently overwrites the decrypted shared value in the resulting Secret. Either add an assertion for unsealed.Data["shared"] (if precedence should apply to output), or rename/adjust the test to make it explicit that the precedence guarantee is only about the template rendering context.
|
Hi @agarcia-oss — following up on this PR. DCO is signed, the fix is additive (14 lines), and tests cover the 3 cases from the original issue. Happy to incorporate any feedback. Thanks for your time! |
Summary
When a
SealedSecretdefines plaintext keys alongside encrypted keys insidespec.template.data, sibling templates in the same map could not reference those plaintext keys as{{ .key }}variables — the controller rendered<no value>instead of the configured plaintext value.This PR fixes that by pre-populating the template execution context with the raw plaintext values from
spec.template.databefore rendering, so templates can reference them just like decryptedencryptedDatakeys.Closes #1607
Reproducer (matches the issue)
Before this PR, the rendered
settings.xmlis:After this PR, it is:
Root cause
In
pkg/apis/sealedsecrets/v1alpha1/sealedsecret_expansion.go,Unseal()builds adatamap that is used as thetext/templateexecution context. That map was being populated only from decrypted entries ofspec.encryptedData. The subsequent loop that renders eachspec.template.dataentry then ran with a context that did not contain any of the sibling plaintext values, so{{ .username }}resolved to a missing field and Go'stext/templatepackage emitted the literal string<no value>.Fix
Add a small additional loop before the rendering loop that copies each
spec.template.dataentry into the context as a raw string, but only if the key does not already exist in the context. This means:spec.template.dataare now visible to all sibling templates as{{ .key }}variables.TestSealRoundTripTemplateData) is unchanged.Tests
Three new tests in
pkg/apis/sealedsecrets/v1alpha1/sealedsecret_test.go:TestTemplateDataPlaintextReference— pure plaintexttemplate.datawith cross-key references. Reproduces the exact symptom in the issue.TestTemplateDataMixedEncryptedAndPlaintext— mixed encrypted + plaintexttemplate.data, with a template that references both at once.TestTemplateDataEncryptedTakesPrecedenceOverPlaintext— guards the precedence rule so future refactors cannot accidentally let a plaintext key shadow a decrypted one.I followed TDD: tests 1 and 2 reproduce the bug and fail on
mainwith the exact<no value>output users were seeing. Test 3 already passes onmainand exists as a forward-looking guard.Verification performed locally
go test ./pkg/apis/sealedsecrets/v1alpha1/...— all 11 tests in the package pass (3 new + 8 pre-existing)go test ./pkg/...— every package green (controller,crypto,kubeseal,multidocyaml,pflagenv,flagenv)go vet ./pkg/apis/sealedsecrets/v1alpha1/...— cleangofmt -lon touched files — cleanTest plan for reviewers
Secret'ssettings.xmlcontains bothmyUsernameand the decrypted password.