feat(ui): SOPS secret encrypt/decrypt with local age keys#5134
Open
devantler wants to merge 2 commits into
Open
feat(ui): SOPS secret encrypt/decrypt with local age keys#5134devantler wants to merge 2 commits into
devantler wants to merge 2 commits into
Conversation
A2 slice 4: a local-only Secrets view that SOPS-encrypts a plaintext document for an age recipient and decrypts a SOPS document with the local age keys. Backend: - Optional api.CipherService interface (EncryptSecret/DecryptSecret/ CipherRecipients) + a secretsCipher capability (derived from the interface). Cluster-independent routes GET /api/v1/secrets/recipients, POST .../encrypt, POST .../decrypt — registered only when the backend implements it (local only; the operator has no local keys). - Local impl reuses pkg/client/sops in-process: encrypt builds KeyGroups from the recipient (sopsage.MasterKeyFromRecipient) — explicit, not .sops.yaml — and stages plaintext in a 0600 temp file (SOPS reads a path); decrypt loads the local keys via keyservice.NewLocalClient(); recipients are derived from the age key file (sops.GetAgeKeyPath + DerivePublicKey). An empty recipient auto-uses the first local age key. - Extracted Server.registerCapabilityRoutes + a shared decodeJSON helper. Frontend: - SecretsView: side-by-side Encrypt (recipient selector + format + plaintext) and Decrypt panels with copyable output; a Secrets nav item gated on secretsCipher. Tests: capability, endpoints, read-only 403 (api); a real age-key encrypt→decrypt roundtrip, default-recipient, recipient listing, empty/invalid-recipient rejection (clusterapi). go build/test + golangci-lint clean; web UI tsc+vite build pass. Live: roundtrip via `ksail ui` returns the exact plaintext. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
From the adversarial review (0 confirmed, low findings): - Per-pane format selector: a single shared Format control governed both encrypt and decrypt, so decrypting a document whose format differed from the last encrypt failed confusingly. Each pane now has its own format selector (extracted a FormatSelect helper). - Document the read-only/decrypt decision: encrypt/decrypt are POST, so the read-only guard blocks them in read-only mode — intentional (a read-only deployment locks down secret crypto, incl. decrypt's plaintext exposure); the local backend runs writable. Added a comment at the route registration. (Skipped: the empty-recipient first-key default is mitigated by the recipient selector + documented; the handleScaleResource decodeJSON dedup is pre-existing code on another branch.) web UI tsc+vite build + go build pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A2 slice 4: a local-only Secrets view that SOPS-encrypts a plaintext document for an age recipient and decrypts a SOPS document with the local age keys. Stacked on #5133.
Backend
api.CipherServiceinterface (EncryptSecret/DecryptSecret/CipherRecipients) + asecretsCiphercapability (derived from the interface). Cluster-independent routesGET /api/v1/secrets/recipients,POST …/encrypt,POST …/decrypt— registered only when the backend implements it (local only; the operator has no local keys).pkg/client/sopsin-process: encrypt builds KeyGroups from an explicit recipient (MasterKeyFromRecipient, not.sops.yaml— verified the empty-KeyGroups command path won't populate keys) and stages plaintext in a 0600 temp file (SOPS reads a path); decrypt loads keys viakeyservice.NewLocalClient(); recipients are derived from the age key file (GetAgeKeyPath+DerivePublicKey). Empty recipient → first local key.Server.registerCapabilityRoutes+ a shareddecodeJSON(keptHandlerunder funlen).Frontend
SecretsView: side-by-side Encrypt (recipient selector + per-pane format + plaintext) and Decrypt panels with copyable output; a Secrets nav item gated onsecretsCipher.Quality
go build/test+golangci-lintclean (G304 nolint is the trusted SOPS key path); web UItsc+vite buildpass.ksail uiwith a real age key returns the exact plaintext;/api/v1/configreportssecretsCipher:true.🤖 Generated with Claude Code