Project

rkseal

0.0
The project is in a healthy, maintained state
rkseal wraps the kubeseal CLI to author and edit Kubernetes SealedSecrets. The plaintext Secret manifest is edited in $EDITOR on a RAM-backed buffer that never touches persistent disk, then sealed with the controller's public key. Deploys to the cluster are explicit opt-in only and guarded by the active kube context.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Development

~> 13.0
~> 3.13
~> 1.60

Runtime

~> 0.2
~> 1.3
 Project Readme

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 set       <namespace> <secret-name> <key> [value]   # seal one value, no editor
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
  • create opens an empty, commented Secret template for you to fill in.
  • edit reads 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>.yaml exists (e.g. you ran create but never deployed), rkseal switches automatically to an offline local edit (see edit --local). Only if neither the cluster Secret nor a local file exists does it fail fast and point you at create.
  • set seals one value into an existing SealedSecret without opening an editor. Only that key is re-sealed (kubeseal --merge-into); every other entry's ciphertext is kept byte-for-byte and nothing is decrypted. An existing key is replaced, a new key is added. It works on the local <secret-name>.yaml if present, otherwise on the live SealedSecret (which then becomes the local file). See set.
  • reencrypt rotates an existing SealedSecret onto the controller's current sealing key (kubeseal --re-encrypt) without exposing plaintext. It reads the local <secret-name>.yaml if present, otherwise the live SealedSecret; if neither exists it points you at create. The result is written back to <secret-name>.yaml.
  • validate asks 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>. Prints valid and exits 0, or prints the reason and exits non-zero.
  • view prints the live unsealed Secret manifest to STDOUT, read-only — no editor, no RAM workspace, no file written.
  • list prints 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, set, and reencrypt write <secret-name>.yaml into the current working directory. edit, set, and reencrypt can deploy with kubectl 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 default edit preserves the existing scope: it reads the SealedSecret's scope annotation from the cluster, falling back to the local <secret-name>.yaml, then to strict. Pass --scope to override.
  • --deploy — after writing, kubectl apply the 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's data into plaintext stringData for editing, instead of showing it as raw base64.
  • --cert, --controller-name, --controller-namespace — control which controller certificate is used to re-seal (same as create).
  • No-op short-circuit: if you save the buffer without changing anything, rkseal writes no file (re-sealing identical input would only produce a spurious ciphertext diff). Because nothing new is produced, a --deploy on 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 plaintext stringData block instead of base64 data, so you type values in clear (folded into data on save).
  • The controller certificate is resolved up front, so an unreachable controller fails fast before you start editing.

set flags & value sources

echo -n 's3cret' | rkseal set app db password     # from stdin (one trailing newline dropped)
rkseal set app db tls.crt --from-file ./tls.crt   # byte-exact, binary-safe
rkseal set app db password                        # hidden interactive prompt (stdin is a TTY)
rkseal set app db password s3cret                 # positional: lands in shell history and `ps`

The value is taken from the first available of: the positional [value], --from-file <path>, piped stdin, a hidden prompt. Every source keeps the value verbatim, except that stdin and the prompt drop one trailing newline. Values are plaintext; pass --base64 if the value is already base64 (as view prints it; line wraps are ignored). Scope, type, name and namespace are preserved from the existing SealedSecret and cannot be changed here (the kept ciphertext binds them), and a local <secret-name>.yaml for a different name/namespace is rejected. If neither a local file nor a cluster SealedSecret exists, set fails fast and points you at create.

A value piped on stdin leaves nothing for the deploy confirmation to read, so --deploy then requires --yes.

  • --from-file <path> — read the value from a file instead of stdin/prompt.
  • --base64 — the value is already base64; it is validated and stored as-is.
  • --deploy / --yes — same deploy semantics as edit.
  • --cert, --controller-name, --controller-namespace.

Secret types

--type (default Opaque) does two things: it seeds the create buffer with the keys the type requires, and it validates the saved Secret against the type's contract before sealing. kubeseal checks none of this. Without the check a broken Secret is rejected only when the controller unseals it, visible solely in controller events. The rules never exceed what the apiserver enforces on a stored Secret, so an existing Secret is always editable. Every built-in type is supported:

Type rkseal seeds / enforces
Opaque any keys; at least one data item
kubernetes.io/service-account-token annotation kubernetes.io/service-account.name; data may be empty (the token controller fills token, ca.crt, namespace)
kubernetes.io/dockercfg .dockercfg, a JSON object (legacy ~/.dockercfg)
kubernetes.io/dockerconfigjson .dockerconfigjson, a JSON object (~/.docker/config.json)
kubernetes.io/basic-auth username and/or password, at least one non-empty
kubernetes.io/ssh-auth ssh-privatekey
kubernetes.io/tls tls.crt and tls.key (optional ca.crt)
bootstrap.kubernetes.io/token token-id (6 × [a-z0-9]) and token-secret (16 × [a-z0-9]); the Secret must be named bootstrap-token-<token-id> in kube-system

Seeded keys carry an empty value. A seeded key left empty is dropped on save, so a required one is reported as missing and an optional one never becomes an empty item on the cluster. Any other type string is accepted as a custom type with no key rules, except that --type rejects an unknown name under kubernetes.io/ (or bootstrap.kubernetes.io/) as a typo. The offline edit --local applies the presence rules to the resulting key set (kept ciphertext exposes only its keys) and the value and name/namespace rules to the keys it re-seals. It refuses to switch a file to a type whose contract needs annotations (service-account-token), because the redacted buffer cannot carry them.

reencrypt flags

  • --deploy / --yes — same deploy semantics as edit (opt-in, context-confirmed; --yes skips the prompt).
  • --cert, --controller-name, --controller-namespace.

validate flags

  • --file <path> — validate an arbitrary SealedSecret manifest instead of the local <secret-name>.yaml (then NAMESPACE/NAME are optional).
  • --cert, --controller-name, --controller-namespace.

view flags

  • --reveal — decode data and print values as plaintext stringData (default shows raw base64, consistent with edit). Read-only: view never writes a file or opens an editor.

Controller certificate

create, edit, set, reencrypt, and validate resolve the controller's public cert from, in order: --cert <file|URL>, the SEALED_SECRETS_CERT env var (both offline — nothing is contacted), otherwise a fresh --fetch-cert from the live controller. The cert is never cached on disk — it is re-fetched every run, so a seal is always bound to the current kube context's controller key. For offline or reproducible (GitOps/CI) seals, pin --cert or SEALED_SECRETS_CERT to a committed certificate.

Requirements

  • Ruby 4.0.2 (managed with rvm + a dedicated rkseal gemset).
  • kubeseal (developed against v0.36.6) and kubectl on your PATH.
  • 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/shm on Linux, an ephemeral hdiutil RAM 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: rkseal surfaces the current kube context and asks you to confirm before kubectl apply. There is no in-code allow-list — rkseal uses whatever context is active — so switch context deliberately before deploying (--yes bypasses only the prompt, not the --deploy opt-in).
  • A positional set value is the one exception. rkseal set … <key> <value> puts the plaintext into your shell history and into the process list for every user on the host. Prefer stdin, the hidden prompt, or --from-file outside throwaway environments.
  • 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 a kubectl/kubeseal flag, /, uppercase, …) is rejected up front, before any editor, cluster call, or file write.

License

MIT — see LICENSE.txt.