You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
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.
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
Format version discriminator — read the prefix, add named constants, document the format contract in the package doc.go.
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.
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
No breaking changes, no migration, no deprecation. Users who want authenticated encryption for existing data today can already get it with werf helm secret rotate-secret-key, which re-encrypts everything.
The public API is unchanged (Encoder, YamlEncoder methods, IsExtractDataError), so nelm is expected to need only a common-go version bump.
The existing e2e fixtures in the werf repository (test/e2e/converge, test/e2e/bundle-publish-apply) must not be re-encrypted — they become the regression coverage for the legacy read path.
Out of scope: Добавьте обратную совместимость с плагином helm-secrets #578 (helm-secrets/SOPS interoperability), Secrets access levels #93 (per-recipient access control) and Plugins for IDEs/editors to work with secrets #92 (editor tooling). None of these can be addressed without adopting Mozilla SOPS, and that migration has been deferred to a future major release — it is not planned for the current cycle and no prototype is being built at this time. These requests are deferred, not rejected: they stay open and will be revisited when there is concrete demand or a clear payoff.
Problem
werf's secret encryption has three defects that are all rooted in
werf/common-go/pkg/secret(references below are againstv0.0.0-20260504183956-43da716392f7).1. YAML scalar types are lost on encryption
yaml_encoder.go:214encrypts every scalar asfmt.Sprintf("%v", value), so the YAML tag is never persisted. Decryption always restores!!str. Encryptingfoo: 123and decrypting it yieldsfoo: "123". The code carries two explicitFIXMEs at:200and: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
deepCopyNodepreservesStyleand the comment fields, butnode.Encode(...)at:192and:219overwrites the node wholesale and discards them. A>-block scalar comes back as a plain string.Reported in #74.
3. Encryption is unauthenticated
aes_encoder.gouses 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 reject0or values> BlockSize, andIsExtractDataError(: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 holdingaes.BlockSize, andDecrypt(:80-89) reads past it but ignores the stored value — the IV size is hardcoded. That field can be repurposed as a format version:16means 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
doc.go.generate-secret-key,WERF_SECRET_KEYand.werf_secret_keyare unaffected and users do not regenerate keys.pad/unpadstay for legacy reads, withunpadhardened.IsExtractDataErroris rewritten so an authentication failure is distinguishable from corrupt data.node.Encode; compareTagalongsideValueinMergeEncodedYamlNode(yaml_helpers.go), otherwiseeditkeeps 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
werf helm secret rotate-secret-key, which re-encrypts everything.Encoder,YamlEncodermethods,IsExtractDataError), so nelm is expected to need only acommon-goversion bump.test/e2e/converge,test/e2e/bundle-publish-apply) must not be re-encrypted — they become the regression coverage for the legacy read path.}}insecret-values.yamlcausewerf renderto fail #89, Keep secret values in values.yaml #77, Alternative for{{ werf_secret_file }}, more interoperable with Helm #103.