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.

The controller certificate is never cached on disk: an explicit ‘–cert` or `SEALED_SECRETS_CERT` is used offline, otherwise it is fetched fresh from the live controller on every invocation. This keeps a seal always bound to the current context’s controller key – no stale or cross-cluster cert can sneak in (see #ensure_cert!).

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
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) ⇒ 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 fresh from the controller per seal.



45
46
47
48
49
50
51
# File 'lib/rkseal/kubeseal.rb', line 45

def initialize(binary: BINARY, controller_name: nil, controller_namespace: nil,
               cert: nil)
  @binary = binary
  @controller_name = controller_name
  @controller_namespace = controller_namespace
  @cert = 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:



58
59
60
61
62
63
64
# File 'lib/rkseal/kubeseal.rb', line 58

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.

Confirm the encryption certificate is obtainable 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 controller is probed with `–fetch-cert`; the fetched PEM is intentionally discarded – #seal re-fetches at seal time, so the freshest controller key is always used and nothing is persisted between invocations.

Raises:

  • (RKSeal::CommandError)

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



76
77
78
79
80
81
# File 'lib/rkseal/kubeseal.rb', line 76

def ensure_cert!
  return if offline_cert?

  fetch_cert
  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:



140
141
142
# File 'lib/rkseal/kubeseal.rb', line 140

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 exactly like #seal: an explicit ‘–cert` is passed through, otherwise kubeseal resolves it itself (env var, else fresh from 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:



160
161
162
163
164
165
166
167
168
# File 'lib/rkseal/kubeseal.rb', line 160

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:



179
180
181
# File 'lib/rkseal/kubeseal.rb', line 179

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`. An explicit `–cert` is passed straight through; otherwise no `–cert` is given and kubeseal resolves the cert itself – from `SEALED_SECRETS_CERT`, or failing that fresh from the live controller. 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:



98
99
100
101
102
103
104
105
106
107
# File 'lib/rkseal/kubeseal.rb', line 98

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:



123
124
125
126
127
128
129
130
131
# File 'lib/rkseal/kubeseal.rb', line 123

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