Releases: OpScaleHub/keyfold
Release list
v0.10.0
v0.10.0 — 2026-09-03
Whole-file encryption binds the repository (#79)
- A ciphertext blob sealed in one repo now fails authentication if dropped into
another that merely shares the key.initgenerates a randomrepo_id,
commits it in.repo-enc.yml, and whole-file encryption folds it into the
AEAD's additional-authenticated-data (a v2 envelope, which also authenticates
the envelope header). Repos created before this keep sealing/reading v1
(path-only AAD) unchanged — no migration is forced; opt in with a freshinit
or by addingrepo_idand runningrotate-keys. Don't changerepo_id
once set — existing v2 blobs would stop verifying. - This does not add freshness: rolling a tracked file back to an earlier
ciphertext for the same path still authenticates (no counter on the CLI path).
Newthreat-model.mdT10-CLI row. - Documented the AES-GCM 96-bit-nonce ceiling that keeps it off the default path
(crypto.Defaultis always XChaCha20-Poly1305 for new seals).
CI/CD & supply-chain hardening (#78)
- Every third-party GitHub Action is pinned to a commit SHA;
.github/dependabot.yml
keeps the pins, Go modules, and Docker base images current (the Kubernetes
client stack is its own group — see #86). release.ymlruns with a default-deny token (permissions: {}), and every
released binary now carries a signed SLSA build-provenance attestation
(gh attestation verify …); container images are signed keyless with
cosign and carry provenance + SBOM attestations. An SPDX SBOM of the
module graph is attached to each release. See SECURITY.md → "Verifying a
release".- Dockerfile base images pinned by digest. CI gained advisory
govulncheck+
Trivy scans, runs the race detector on Linux, and fails if the GPG test suite
is silently skipped there.
Controller hardening (Beta → GA, #77)
- Reconcile no longer self-triggers. The reconciler wrote
status.lastSyncTime
on every pass; that write returned through its own watch and re-enqueued the
object, so a steady-stateGitSecretreconciled forever. Status is now written
only when a field other thanlastSyncTimechanges. - Webhook now requires
replicaCount: 1. The serving cert is per-pod and the
ValidatingWebhookConfiguration.caBundleholds one CA; the chart refuses
webhook.enabledwith more than one replica, and the CA injector is
leader-gated. Reconcile HA via leader election is unchanged. - Tighter controller RBAC. Dropped unused verbs (
secrets: delete,
gitsecrets: update/patch); scopedvalidatingwebhookconfigurations
update/patchto the controller's own config by name. NewwatchNamespaces
value confines the cache and Secret RBAC to a fixed namespace list
(--watch-namespaces). - New UPGRADING.md: the
v1alpha1"additive only" compatibility
policy.
Deprecations
git-secret-server(the ESO webhook bridge) is now explicitly deprecated.
It is superseded by the nativeGitSecretCRD +git-secret-controller. The
binary, image, and Helm chart are still published so existing External Secrets
Operator users are not broken, and it receives security fixes only — no new
features. New deployments should use theGitSecretCRD. The chart is marked
deprecated: true.
Docs
- Landing page: corrected the hook-skip note — only
SECRETIZE_SKIP_HOOKS=1
skips hooks; the ambientCIvariable deliberately does not (it previously
impliedCI=1would). Rewrote theGitSecretsection to stand on its own —
no competitor named. disaster-recovery.md/recipient-lifecycle.md: clarified that CRD
recipients removeperforms a rewrap (same content key,encryptedData
untouched), not a content-key rotation — a removed recipient who cached the
content key can still decrypt values that have not since changed. The CLI
git secret removeuserstill forces a fullrotate-keysand is unchanged.threat-model.mdT11 (recipient substitution) reworded "Handled" →
"Partial" —sealer.VerifyRecipientsis a count check, not per-fingerprint
authentication.overview.md: on unseal failure the controller leaves the last-written
targetSecretin place (fail-safe) — documented, with how to force removal.
Verifying this release
gh attestation verify ./git-secret-linux-amd64 --repo OpScaleHub/git-secret
cosign verify \
--certificate-identity-regexp '^https://github.com/OpScaleHub/git-secret/' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
ghcr.io/opscalehub/git-secret-controller:v0.10.0
v0.9.0
v0.9: git-secret-seal ui (public-key-only sealing web form); pre-free…
v0.8.0
v0.8: security foundation — threat model, disaster recovery, recipien…
v0.7.1
v0.7: native GitSecret CRD + controller; ship the controller image an…
v0.7.0
v0.7: native GitSecret CRD + controller; ship the controller image an…
v0.6.3
v0.6: git-secret-server ESO webhook bridge (later superseded); contai…
v0.6.2
v0.6: git-secret-server ESO webhook bridge (later superseded); contai…
v0.6.1
v0.6: git-secret-server ESO webhook bridge (later superseded); contai…
v0.6.0
v0.6: git-secret-server ESO webhook bridge (later superseded); contai…
v0.5.0
Security-focused release covering 25 issues from a self-run audit, plus a redesign of the core verification/enforcement model that most of them shared a root cause with, plus ArgoCD/GitOps documentation for the gpg key backend.
⚠️ Upgrade notes / behavior changes
verifynow requires the key to be available. It authenticates every candidate ciphertext instead of trusting a magic-byte prefix, so it needs to actually decrypt. If your CI ranverifywithout key access before, it will now fail with exit code 2 instead of a false "OK" — this is intentional (see #1, #2).- Ambient
CI=1no longer bypasses hooks. Only the explicitSECRETIZE_SKIP_HOOKS=1does now. If your CI relied on the ambientCIvar to skip encryption/push-protection, setSECRETIZE_SKIP_HOOKS=1explicitly (#21). - kubectl-secret ciphertext is now bound to the full object identity (
apiVersion/kind/metadata.name/namespace), not just the file and key. Values encrypted with prior versions were sealed under the old, narrower AAD and will need to be re-encrypted withencrypt-valueunder this version (#23). kubectl secret encrypt-valuereads the plaintext from stdin by default now (echo -n VALUE | kubectl secret encrypt-value -f FILE -k KEY). Scripts passing it as a positional argument need--allow-argv(#5).k8s_secret_pathsmanifests are now enforced byverify/pre-commit. A stringData value that isn't ciphertext and isn't explicitly allowlisted via the newk8s_plaintext_keysconfig field will now be flagged — add it to the allowlist if it's intentionally plaintext (#15).
Verification & enforcement redesign
The core design change: verify/hooks used to check encryption status via a magic-byte prefix, only at HEAD, using working-tree config — a checkpoint with several independent ways around it. verify is now revision-pinned and authenticated; pre-push walks every commit in the pushed range via git's ref-update protocol, not just HEAD.
- #1 —
verifyaccepted fakeRENC-prefixed plaintext as encrypted - #2 —
verifyused mutable index/worktree config, could miss plaintext at HEAD - #3 —
pre-pushverified only HEAD, letting earlier leaked commits reach remotes - #4 — a committed raw
file-backend key wasn't detected byverify - #6 — git path parsing wasn't NUL-safe, letting quoted filenames bypass protection
- #15 —
k8s_secret_pathswere ignored by hooks andverify - #16 — a global config could silently disable repo-local encryption policy via
exclude - #20 — root-anchored patterns (
/secrets/**) validated but failed open - #21 — ambient
CIdisabled both encryption and push-protection hooks
Path, symlink, and key-lifecycle safety
- #7 —
key_sourceand explicit paths could escape the repository root - #8 — GPG recipients weren't validated as full fingerprints
- #9 —
unlockwrote plaintext secrets with world-readable0644mode - #10 —
removeusersaved the recipient removal before key rotation succeeded - #11 — global GPG recipients could be silently included in the committed
key.gpg - #12 —
addusercould grant a new GPG recipient even when the config save failed - #18 — matched symlinks could capture local files into encrypted commits
- #19 — a failed
rotate-keyscould leave an unignored raw staging key
kubectl-secret per-value crypto
- #5 —
encrypt-valueexposed plaintext through argv - #23 — Kubernetes value ciphertext wasn't bound to the Secret identity
- #24 — YAML aliases could copy decrypted
stringDatainto other fields - #25 — manifest-state verification gaps (undetected plaintext, argv leak, ArgoCD
selfHealrace) — all three sub-items fixed
Release/supply-chain hardening
- #13 — ArgoCD CMP docs now carry ArgoCD's own warning about manifest-generation secret injection
- #14 — release binaries are now checksummed (
.sha256assets below); the ArgoCD install snippet verifies beforechmod +x - #17 — the release workflow no longer interpolates the pushed tag name directly into shell (was shell-injectable)
- #22 — the ArgoCD CMP key-copy step no longer follows a repo-controlled symlink
Docs
New "Recommended: use the gpg backend instead of file" section for the ArgoCD integration — granting/revoking ArgoCD's access becomes the same adduser/removeuser commands used for teammates, and no raw key material ever needs to be copied into the repo checkout.
Every fix has a regression test. Full commit history: v0.4.1...v0.5.0