Skip to content

Secrets: preserve YAML scalar types and switch to authenticated encryption without breaking existing data #677

Description

@alexey-igrychev

Problem

werf's secret encryption has three defects that are all rooted in werf/common-go/pkg/secret (references below are against v0.0.0-20260504183956-43da716392f7).

1. YAML scalar types are lost on encryption

yaml_encoder.go:214 encrypts every scalar as fmt.Sprintf("%v", value), so the YAML tag is never persisted. Decryption always restores !!str. Encrypting foo: 123 and decrypting it yields foo: "123". The code carries two explicit FIXMEs at :200 and :213, the latter stating that this is a compatibility mode with a previous werf version.

Reported in #71 and #80.

2. Block scalar style and comments are lost

deepCopyNode preserves Style and the comment fields, but node.Encode(...) at :192 and :219 overwrites the node wholesale and discards them. A >- block scalar comes back as a plain string.

Reported in #74.

3. Encryption is unauthenticated

aes_encoder.go uses AES-CBC with a random IV and no MAC, so there is no integrity check: a corrupted or tampered ciphertext is not reliably detected, and a wrong key produces garbage rather than a clear error. Related weaknesses: unpad (:113-122) does not validate padding contents and does not reject 0 or values > BlockSize, and IsExtractDataError (:133-146) distinguishes error classes by matching message prefixes.

Note the key is 16 random bytes (GenerateAesSecretKey, :19-28), i.e. AES-128.

Solution (if you have one)

All three can be fixed without breaking existing encrypted data, because the ciphertext format already contains an unused version field.

Encrypt (:57-62) writes a 2-byte little-endian prefix holding aes.BlockSize, and Decrypt (:80-89) reads past it but ignores the stored value — the IV size is hardcoded. That field can be repurposed as a format version: 16 means the existing CBC format, and a reserved high range (0xF000-0xFFFF) is used for new versions. Old ciphertext keeps decrypting exactly as today; unknown versions produce a clear error.

Work items

  1. Format version discriminator — read the prefix, add named constants, document the format contract in the package doc.go.
  2. Authenticated encryption — add an AES-GCM path under a new format version, written by default, with both formats readable. The existing 16-byte key is kept, so generate-secret-key, WERF_SECRET_KEY and .werf_secret_key are unaffected and users do not regenerate keys. pad/unpad stay for legacy reads, with unpad hardened. IsExtractDataError is rewritten so an authentication failure is distinguishable from corrupt data.
  3. YAML fidelity — carry the scalar tag and style inside the encrypted payload and restore them on decryption; restore comments after node.Encode; compare Tag alongside Value in MergeEncodedYamlNode (yaml_helpers.go), otherwise edit keeps stale ciphertext when only a value's type changes.

Explicit non-goal: existing secrets do not gain their types back. The tag was never stored, so the information does not exist and no command can recover it. Existing values keep decrypting as strings, exactly as they do today; a specific value can only be fixed by re-entering it with the intended type via werf helm secret values edit. This should be stated plainly in the documentation.

Additional information

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions