rkseal
Interactively create and edit Kubernetes SealedSecrets
from your terminal, in the spirit of knife vault create/edit.
rkseal wraps the kubeseal CLI. You edit a full Kubernetes Secret manifest in
$EDITOR; rkseal seals it with the controller's public key and writes the resulting
SealedSecret to the current directory. The plaintext buffer lives only on a RAM-backed
path and is destroyed when you are done — it never touches persistent disk.
Commands
rkseal create <namespace> <secret-name> # author a new sealed secret
rkseal edit <namespace> <secret-name> # edit an existing one
rkseal reencrypt <namespace> <secret-name> # rotate to the controller's newest key
rkseal validate <namespace> <secret-name> # check a SealedSecret with the controller
rkseal view <namespace> <secret-name> # print the live Secret (read-only)
rkseal list [namespace] # list SealedSecrets (metadata only)
rkseal version # print the installed rkseal version
createopens an empty, commented Secret template for you to fill in.editreads the live unsealed Secret from the cluster (kubectl get secret … -o json) — the only way to recover current values — opens it for editing, then re-seals. If the Secret is absent from the cluster but a local<secret-name>.yamlexists (e.g. you rancreatebut never deployed),rksealswitches automatically to an offline local edit (seeedit --local). Only if neither the cluster Secret nor a local file exists does it fail fast and point you atcreate.reencryptrotates an existing SealedSecret onto the controller's current sealing key (kubeseal --re-encrypt) without exposing plaintext. It reads the local<secret-name>.yamlif present, otherwise the live SealedSecret; if neither exists it points you atcreate. The result is written back to<secret-name>.yaml.validateasks the controller whether a SealedSecret is well-formed and decryptable for its target (kubeseal --validate) — a safe pre-flight check. It validates the local<secret-name>.yaml, or any file via--file <path>. Printsvalidand exits 0, or prints the reason and exits non-zero.viewprints the live unsealed Secret manifest to STDOUT, read-only — no editor, no RAM workspace, no file written.listprints a table of the SealedSecret objects in the cluster (columns NAMESPACE, NAME, SCOPE, AGE). Give a[namespace]to scope it to one namespace; omit it to list all. Read-only and metadata-only — it never prints encrypted data (not even the data keys).create,edit, andreencryptwrite<secret-name>.yamlinto the current working directory.editandreencryptcan deploy withkubectl apply, but only with an explicit opt-in flag, and only after confirming the active kube context.
In the edit buffer, data: values are shown as base64, verbatim — they are never
decoded to plaintext. To change a value readably, add it under a stringData: block; on
save it is folded into data (and wins per key). The seeded buffer includes a worked
example to make this obvious. Pass --string-data to decode the whole buffer to plaintext
stringData up front (an opt-in plaintext exposure).
edit flags & behaviour
--scope strict|namespace-wide|cluster-wide— by defaulteditpreserves the existing scope: it reads the SealedSecret's scope annotation from the cluster, falling back to the local<secret-name>.yaml, then tostrict. Pass--scopeto override.--deploy— after writing,kubectl applythe result. Surfaces the active kube context and asks you to confirm first. Never the default.--yes— skip the interactive deploy confirmation (only meaningful together with--deploy), for non-interactive pipelines.--local— force the offline local edit without contacting the cluster at all (see below).--string-data— decode the live Secret'sdatainto plaintextstringDatafor editing, instead of showing it as raw base64.--cert,--controller-name,--controller-namespace,--refresh-cert— control which controller certificate is used to re-seal (same ascreate).- No-op short-circuit: if you save the buffer without changing anything,
rksealwrites no file (re-sealing identical input would only produce a spurious ciphertext diff). Because nothing new is produced, a--deployon an unchanged secret deploys nothing.
edit --local (offline)
When a SealedSecret was authored with create but never deployed, there is no unsealed
Secret in the cluster to recover values from. rkseal then edits the local
<secret-name>.yaml offline — reached automatically when the cluster Secret is absent
but the local file exists, or forced with --local (which never contacts the cluster, useful
when it is unreachable).
Because a SealedSecret cannot be decrypted, every existing key is shown as <redacted>:
- leave it
<redacted>— the existing ciphertext is kept byte-for-byte (no re-seal), - replace the value (or add a new key) — that key is re-sealed and merged in,
- delete the line — that key is removed.
Scope is fixed in this mode (--scope is rejected) and name/namespace cannot change —
kept ciphertext binds them and cannot be re-sealed without its plaintext. The automatic
fallback fires only on a definitive "not found"; an unreachable cluster surfaces as an
error instead (use --local to force offline). --deploy / --yes behave exactly as for the
online edit.
create flags
--scope,--type,--cert,--controller-name,--controller-namespace.--from-file key=path(repeatable) — pre-seed a value from a file (binary-safe, stored as base64) before the editor opens.--no-edit— seal the pre-seeded Secret directly, without opening$EDITOR(handy for TLS / dockerconfig / binary payloads).--string-data— seed the buffer with a plaintextstringDatablock instead of base64data, so you type values in clear (folded intodataon save).--refresh-cert— bypass the cached controller cert and re-fetch it for this run.- The controller certificate is resolved up front, so an unreachable controller fails fast before you start editing.
reencrypt flags
--deploy/--yes— same deploy semantics asedit(opt-in, context-confirmed;--yesskips the prompt).--cert,--controller-name,--controller-namespace,--refresh-cert.
validate flags
--file <path>— validate an arbitrary SealedSecret manifest instead of the local<secret-name>.yaml(thenNAMESPACE/NAMEare optional).--cert,--controller-name,--controller-namespace,--refresh-cert.
view flags
--reveal— decodedataand print values as plaintextstringData(default shows raw base64, consistent withedit). Read-only:viewnever writes a file or opens an editor.
--refresh-cert
create, edit, reencrypt, and validate accept --refresh-cert to bypass the cached
controller certificate and re-fetch it from the live controller for that run.
Requirements
- Ruby 4.0.2 (managed with
rvm+ a dedicatedrksealgemset). kubeseal(developed against v0.36.6) andkubectlon yourPATH.- Access to a cluster running the sealed-secrets controller (for
--fetch-cert,edit, and deploys).
Development
rvm use 4.0.2@rkseal --create # isolated gemset — never install gems globally
bundle install
bundle exec rspec # unit suite (adapters stubbed; no cluster needed)
bundle exec rubocop # lint
bundle exec exe/rkseal create my-namespace my-secret
Unit tests stub the kubeseal / kubectl / $EDITOR adapters, so the suite runs without
a cluster. Real cluster operations are reserved for explicit integration tests gated on the
docker-desktop context.
Security model
- Plaintext never hits persistent disk. The edit buffer is RAM-backed
(
tmpfs//dev/shmon Linux, an ephemeralhdiutilRAM disk on macOS) and is shredded and torn down on exit, including on error or signal. - Deploys confirm the active context. Applying to the wrong cluster is the dangerous
operation, so deploy is never the default:
rksealsurfaces the current kube context and asks you to confirm beforekubectl apply. There is no in-code allow-list —rksealuses whatever context is active — so switch context deliberately before deploying (--yesbypasses only the prompt, not the--deployopt-in). - Names are validated at the boundary.
<namespace>and<secret-name>must be valid Kubernetes DNS-1123 names; anything else (path traversal like../, a leading-that could be read as akubectl/kubesealflag,/, uppercase, …) is rejected up front, before any editor, cluster call, or file write.
License
MIT — see LICENSE.txt.