Class: RKSeal::Kubeseal

Inherits:
Object
  • Object
show all
Defined in:
lib/rkseal/kubeseal.rb

Overview

Thin adapter over the ‘kubeseal` binary.

Owns everything kubeseal-flag-shaped: scope, certificate source, and the controller’s name/namespace. Each public method maps to one kubeseal invocation and returns its stdout (the produced SealedSecret YAML or a PEM certificate). Nothing here parses YAML or knows about the domain model – callers pass in manifest text and get sealed text back.

Developed against kubeseal v0.36.6; flag names below assume that CLI.

All process execution funnels through one private runner so that unit tests stub a single seam (or stub the public methods directly). The runner must never echo stdin (the plaintext Secret) into logs or error messages.

rubocop:disable Metrics/ClassLength – the inline CertCache is co-located here by design (the cert cache is intrinsic to this adapter and must not add a new top-level require); that nested class accounts for the extra lines.

Defined Under Namespace

Classes: CertCache

Constant Summary collapse

BINARY =
"kubeseal"
SCOPES =

Allowed sealing scopes, mapped to their kubeseal ‘–scope` argument.

{
  strict: "strict",
  namespace_wide: "namespace-wide",
  cluster_wide: "cluster-wide"
}.freeze
DEFAULT_CONTROLLER_NAME =

kubeseal’s own defaults for the controller’s identity. Used to name the cache entry consistently when the caller does not override them, so a run with implicit defaults and a run with explicit-but-identical flags share one cached cert.

"sealed-secrets-controller"
DEFAULT_CONTROLLER_NAMESPACE =
"kube-system"
VALIDATION_FAILURE_MARKER =

Substring kubeseal prints to stderr when the controller could decrypt-test the SealedSecret but it is NOT valid. Anything else on a non-zero ‘–validate` exit is treated as operational (CommandError), not a verdict.

"unable to decrypt"

Instance Method Summary collapse

Constructor Details

#initialize(binary: BINARY, controller_name: nil, controller_namespace: nil, cert: nil, refresh_cert: false) ⇒ Kubeseal

Returns a new instance of Kubeseal.

Parameters:

  • binary (String) (defaults to: BINARY)

    override the executable name/path (testing/env).

  • controller_name (String, nil) (defaults to: nil)

    ‘–controller-name` value.

  • controller_namespace (String, nil) (defaults to: nil)

    ‘–controller-namespace` value.

  • cert (String, nil) (defaults to: nil)

    ‘–cert <file|URL>` source; when nil and no env cert is present, the cert is fetched from the controller and cached.

  • refresh_cert (Boolean) (defaults to: false)

    when true, ignore any cached cert and overwrite it with a freshly fetched one (wired to ‘–refresh-cert`).



54
55
56
57
58
59
60
61
# File 'lib/rkseal/kubeseal.rb', line 54

def initialize(binary: BINARY, controller_name: nil, controller_namespace: nil,
               cert: nil, refresh_cert: false)
  @binary = binary
  @controller_name = controller_name
  @controller_namespace = controller_namespace
  @cert = cert
  @refresh_cert = refresh_cert
end

Instance Method Details

#ensure_available!void

This method returns an undefined value.

Verify the kubeseal binary is present and executable; raise otherwise. Called early so the flow fails fast on a missing dependency.

Raises:



68
69
70
71
72
73
74
# File 'lib/rkseal/kubeseal.rb', line 68

def ensure_available!
  return if executable_on_path?(@binary)

  raise DependencyMissingError,
        "kubeseal not found on PATH (looked for #{@binary.inspect}). " \
        "Install it from https://github.com/bitnami-labs/sealed-secrets/releases."
end

#ensure_cert!void

This method returns an undefined value.

Resolve the encryption certificate up front so a flow fails fast before any editor opens. When an offline cert is configured (‘–cert` or the `SEALED_SECRETS_CERT` env var) nothing is contacted. Otherwise the cert is resolved through the on-disk cache: a cached PEM is reused as-is, otherwise it is fetched from the live controller and written to the cache (which is what makes a subsequent #seal offline). `refresh_cert: true` skips the cached copy and refetches.

Raises:

  • (RKSeal::CommandError)

    if no offline cert is configured and the controller is unreachable (the underlying ‘–fetch-cert` exits non-zero).



87
88
89
90
91
92
# File 'lib/rkseal/kubeseal.rb', line 87

def ensure_cert!
  return if offline_cert?

  resolve_cached_cert_path
  nil
end

#fetch_certString

Fetch the controller’s public certificate (‘kubeseal –fetch-cert`) so it can be cached and reused, avoiding an API round-trip per seal.

NOTE: unlike #seal, this method contacts the cluster API by design.

Returns:

  • (String)

    the certificate in PEM format.

Raises:



151
152
153
# File 'lib/rkseal/kubeseal.rb', line 151

def fetch_cert
  run("--fetch-cert", *controller_flags)
end

#merge_into(manifest_yaml, file:, scope: :strict) ⇒ void

This method returns an undefined value.

Blind-append freshly-encrypted items to an existing SealedSecret file (‘kubeseal –merge-into <file>`). Does NOT decrypt anything: it appends or overwrites the items in the input Secret while leaving every other sealed entry untouched. This is what powers the offline `edit –local` flow, where kept keys must stay byte-for-byte unchanged.

The certificate is resolved offline-first, exactly like #seal: an explicit ‘–cert` or the cached controller PEM is passed via `–cert` (no API round-trip); only when neither is available does kubeseal fall back to the env var or the controller. The output format is inherited from the existing file, so `-o` is NOT forced here.

Parameters:

  • manifest_yaml (String)

    Secret manifest with the items to add.

  • file (String)

    path to the existing SealedSecret to merge into.

  • scope (Symbol) (defaults to: :strict)

    sealing scope for the new items.

Raises:



172
173
174
175
176
177
178
179
180
# File 'lib/rkseal/kubeseal.rb', line 172

def merge_into(manifest_yaml, file:, scope: :strict)
  argv = ["--merge-into", file, "--scope", scope_flag(scope)]
  cert_path = resolved_cert_path
  argv += ["--cert", cert_path] if cert_path
  argv += controller_flags

  run(*argv, stdin: manifest_yaml)
  nil
end

#re_encrypt(sealed_yaml) ⇒ String

Upgrade an existing SealedSecret to the controller’s newest key without exposing plaintext (‘kubeseal –re-encrypt`). Out of scope for the initial create/edit flows but part of the adapter surface.

NOTE: contacts the cluster API by design.

Parameters:

  • sealed_yaml (String)

    an existing SealedSecret manifest.

Returns:

  • (String)

    the re-encrypted SealedSecret YAML.

Raises:



191
192
193
# File 'lib/rkseal/kubeseal.rb', line 191

def re_encrypt(sealed_yaml)
  run("--re-encrypt", "-o", "yaml", *controller_flags, stdin: sealed_yaml)
end

#seal(manifest_yaml, scope: :strict) ⇒ String

Seal a Secret manifest into a SealedSecret.

Pipes ‘manifest_yaml` to kubeseal on stdin with `-o yaml` and the resolved `–scope`. The certificate is resolved offline-first: an explicit `–cert` or the cached controller PEM is passed via `–cert` (no API round-trip); only when neither is available does kubeseal fall back to the env var or the controller itself. Returns the SealedSecret YAML on stdout.

Parameters:

  • manifest_yaml (String)

    a full Secret manifest (from Secret#to_manifest).

  • scope (Symbol) (defaults to: :strict)

    one of SCOPES keys; defaults to :strict.

Returns:

  • (String)

    SealedSecret YAML.

Raises:



109
110
111
112
113
114
115
116
117
118
# File 'lib/rkseal/kubeseal.rb', line 109

def seal(manifest_yaml, scope: :strict)
  # `-o yaml` is mandatory: kubeseal defaults to JSON, so without it the
  # output written to `<name>.yaml` would actually contain JSON.
  argv = ["--scope", scope_flag(scope), "-o", "yaml"]
  cert_path = resolved_cert_path
  argv += ["--cert", cert_path] if cert_path
  argv += controller_flags

  run(*argv, stdin: manifest_yaml)
end

#validate(sealed_secret_yaml) ⇒ true

Validate that a SealedSecret can be decrypted by the controller (‘kubeseal –validate`, SealedSecret piped on stdin). Contacts the cluster: the controller performs the decrypt-test.

kubeseal v0.36.6 exits 0 when valid and non-zero otherwise, printing the reason to stderr. A non-zero exit whose stderr names a decrypt failure is a validity verdict (ValidationError); any other non-zero exit (missing binary, unreachable cluster, controller service not found) is operational (CommandError) and says nothing about the SealedSecret itself.

Parameters:

  • sealed_secret_yaml (String)

    a SealedSecret manifest.

Returns:

  • (true)

    when the controller can decrypt it.

Raises:



134
135
136
137
138
139
140
141
142
# File 'lib/rkseal/kubeseal.rb', line 134

def validate(sealed_secret_yaml)
  run("--validate", *controller_flags, stdin: sealed_secret_yaml)
  true
rescue CommandError => e
  raise unless validation_failure?(e.stderr)

  raise ValidationError,
        "SealedSecret failed validation: #{e.stderr.strip}"
end