Skip to content

Expose plaintext template.data values in template rendering context#1940

Open
hamza-younas94 wants to merge 1 commit intobitnami-labs:mainfrom
hamza-younas94:fix-template-data-plaintext-reference
Open

Expose plaintext template.data values in template rendering context#1940
hamza-younas94 wants to merge 1 commit intobitnami-labs:mainfrom
hamza-younas94:fix-template-data-plaintext-reference

Conversation

@hamza-younas94
Copy link
Copy Markdown

Summary

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 — 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.data before rendering, so templates can reference them just like decrypted encryptedData keys.

Closes #1607

Reproducer (matches the issue)

apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
  name: mySecret
spec:
  encryptedData:
    password: <ciphertext>
  template:
    data:
      username: "myUsername"
      settings.xml: |-
        <server>
          <username>{{ .username }}</username>
          <password>{{ .password }}</password>
        </server>

Before this PR, the rendered settings.xml is:

<server>
  <username><no value></username>
  <password>plaintext password</password>
</server>

After this PR, it is:

<server>
  <username>myUsername</username>
  <password>plaintext password</password>
</server>

Root cause

In pkg/apis/sealedsecrets/v1alpha1/sealedsecret_expansion.go, Unseal() builds a data map that is used as the text/template execution context. That map was being populated only from decrypted entries of spec.encryptedData. The subsequent loop that renders each spec.template.data entry then ran with a context that did not contain any of the sibling plaintext values, so {{ .username }} resolved to a missing field and Go's text/template package emitted the literal string <no value>.

Fix

Add a small additional loop before the rendering loop that copies each spec.template.data entry into the context as a raw string, but only if the key does not already exist in the context. This means:

  • Plaintext keys defined in spec.template.data are now visible to all sibling templates as {{ .key }} variables.
  • Encrypted values always take precedence on key collision, so adding a plaintext key with the same name as an encrypted key can never silently shadow a real secret value. This is covered by a regression test.
  • Existing behavior for encrypted-only templating (e.g. the existing TestSealRoundTripTemplateData) is unchanged.

Tests

Three new tests in pkg/apis/sealedsecrets/v1alpha1/sealedsecret_test.go:

  1. TestTemplateDataPlaintextReference — pure plaintext template.data with cross-key references. Reproduces the exact symptom in the issue.
  2. TestTemplateDataMixedEncryptedAndPlaintext — mixed encrypted + plaintext template.data, with a template that references both at once.
  3. 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 main with the exact <no value> output users were seeing. Test 3 already passes on main and 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/... — clean
  • gofmt -l on touched files — clean
  • Commit is signed off (DCO)

Test plan for reviewers

  • CI green
  • Apply the reproducer YAML above against a cluster running this build and confirm the rendered Secret's settings.xml contains both myUsername and the decrypted password.

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>
Copilot AI review requested due to automatic review settings April 10, 2026 15:53
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

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.data values (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.

Comment on lines +301 to +313
// 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
}
}
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +478 to +490
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)
}
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
@hamza-younas94
Copy link
Copy Markdown
Author

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!

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.

Adding not encrypted data to a template does not work

2 participants