rkseal 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ebc10a4204a042490b3ad908bed3d1cfd625ee03743f21f147f1d80b21fb306f
4
- data.tar.gz: ba5ffd88f83075193e57357edadfd0539f64e9587b76664d8f1fd0c2a89b95b0
3
+ metadata.gz: e119d50bf74489108689a815572225254b7dba2e0efcaa0f5e2a70b23bb84624
4
+ data.tar.gz: 8ab17b1fc3043580cebb850395321a92bbba7c78052531a3814f8ab888c37acb
5
5
  SHA512:
6
- metadata.gz: 04d897666453e1e63e870c0c4c31f5dc8adb2a00d4b7167b5859c5f3adb0e73366fad81d6be29843d7e35ed44fc365f793529d45161cead4857800bea963bcbc
7
- data.tar.gz: 56ff59be8191ec4c27d612422766f174f92d393417f8d3e47f6a8185d26feccde71bea6da8353a5e511c15f4e140a7ed7bea2bb894f7da213e2bfcfd752f5533
6
+ metadata.gz: fd59fede38ef24369b8aca1c8de658be6e3cfb9300a5d96ebe4f0d5b5bcdec500f2949cb6073be2a053d7a1aaeafa809676f53f0b70ca175c062e19a15491cb2
7
+ data.tar.gz: 89516ab7c39d5ad37070bc83017d37fa2ecc707594079d6973456490a63f814d3355d70eb86fa7663ee8a6bee36fcaabc349d543868efcdda9839f3fe2794815
data/README.md CHANGED
@@ -63,7 +63,7 @@ example to make this obvious. Pass `--string-data` to decode the **whole** buffe
63
63
  - `--local` — force the offline local edit without contacting the cluster at all (see below).
64
64
  - `--string-data` — decode the live Secret's `data` into plaintext `stringData` for editing,
65
65
  instead of showing it as raw base64.
66
- - `--cert`, `--controller-name`, `--controller-namespace`, `--refresh-cert` — control which
66
+ - `--cert`, `--controller-name`, `--controller-namespace` — control which
67
67
  controller certificate is used to re-seal (same as `create`).
68
68
  - **No-op short-circuit:** if you save the buffer without changing anything, `rkseal` writes
69
69
  no file (re-sealing identical input would only produce a spurious ciphertext diff). Because
@@ -98,7 +98,6 @@ online `edit`.
98
98
  TLS / dockerconfig / binary payloads).
99
99
  - `--string-data` — seed the buffer with a plaintext `stringData` block instead of base64
100
100
  `data`, so you type values in clear (folded into `data` on save).
101
- - `--refresh-cert` — bypass the cached controller cert and re-fetch it for this run.
102
101
  - The controller certificate is resolved up front, so an unreachable controller fails fast
103
102
  **before** you start editing.
104
103
 
@@ -106,23 +105,27 @@ online `edit`.
106
105
 
107
106
  - `--deploy` / `--yes` — same deploy semantics as `edit` (opt-in, context-confirmed; `--yes`
108
107
  skips the prompt).
109
- - `--cert`, `--controller-name`, `--controller-namespace`, `--refresh-cert`.
108
+ - `--cert`, `--controller-name`, `--controller-namespace`.
110
109
 
111
110
  ### `validate` flags
112
111
 
113
112
  - `--file <path>` — validate an arbitrary SealedSecret manifest instead of the local
114
113
  `<secret-name>.yaml` (then `NAMESPACE`/`NAME` are optional).
115
- - `--cert`, `--controller-name`, `--controller-namespace`, `--refresh-cert`.
114
+ - `--cert`, `--controller-name`, `--controller-namespace`.
116
115
 
117
116
  ### `view` flags
118
117
 
119
118
  - `--reveal` — decode `data` and print values as plaintext `stringData` (default shows raw
120
119
  base64, consistent with `edit`). Read-only: `view` never writes a file or opens an editor.
121
120
 
122
- ### `--refresh-cert`
121
+ ### Controller certificate
123
122
 
124
- `create`, `edit`, `reencrypt`, and `validate` accept `--refresh-cert` to bypass the cached
125
- controller certificate and re-fetch it from the live controller for that run.
123
+ `create`, `edit`, `reencrypt`, and `validate` resolve the controller's public cert from, in
124
+ order: `--cert <file|URL>`, the `SEALED_SECRETS_CERT` env var (both offline nothing is
125
+ contacted), otherwise a fresh `--fetch-cert` from the live controller. **The cert is never
126
+ cached on disk** — it is re-fetched every run, so a seal is always bound to the current kube
127
+ context's controller key. For offline or reproducible (GitOps/CI) seals, pin `--cert` or
128
+ `SEALED_SECRETS_CERT` to a committed certificate.
126
129
 
127
130
  ## Requirements
128
131
 
data/lib/rkseal/cli.rb CHANGED
@@ -83,8 +83,6 @@ module RKSeal
83
83
  desc: "sealed-secrets controller name"
84
84
  method_option :"controller-namespace", type: :string,
85
85
  desc: "controller namespace"
86
- method_option :"refresh-cert", type: :boolean, default: false,
87
- desc: "Bypass the cert cache and re-fetch from the controller"
88
86
  method_option :"from-file", type: :array,
89
87
  desc: "Pre-seed key=path value(s) into the buffer before editing"
90
88
  method_option :"no-edit", type: :boolean, default: false,
@@ -163,8 +161,6 @@ module RKSeal
163
161
  desc: "sealed-secrets controller name"
164
162
  method_option :"controller-namespace", type: :string,
165
163
  desc: "controller namespace"
166
- method_option :"refresh-cert", type: :boolean, default: false,
167
- desc: "Bypass the cert cache and re-fetch from the controller"
168
164
  # Edit an existing SealedSecret. Reads current values from the cluster; if
169
165
  # the Secret is absent there but a local <NAME>.yaml exists, automatically
170
166
  # falls back to the offline local edit. `--local` forces the offline path.
@@ -201,8 +197,6 @@ module RKSeal
201
197
  desc: "sealed-secrets controller name"
202
198
  method_option :"controller-namespace", type: :string,
203
199
  desc: "controller namespace"
204
- method_option :"refresh-cert", type: :boolean, default: false,
205
- desc: "Bypass the cert cache and re-fetch from the controller"
206
200
  # Re-encrypt an existing SealedSecret to the newest controller key.
207
201
  #
208
202
  # @param namespace [String] target namespace.
@@ -238,8 +232,6 @@ module RKSeal
238
232
  desc: "sealed-secrets controller name"
239
233
  method_option :"controller-namespace", type: :string,
240
234
  desc: "controller namespace"
241
- method_option :"refresh-cert", type: :boolean, default: false,
242
- desc: "Bypass the cert cache and re-fetch from the controller"
243
235
  # Validate a SealedSecret (local <NAME>.yaml, or --file <path>).
244
236
  #
245
237
  # @param namespace [String, nil] target namespace (omit with --file).
@@ -428,22 +420,20 @@ module RKSeal
428
420
  end
429
421
 
430
422
  # Build the kubeseal adapter from the cert/controller options. Dashed option
431
- # names are string keys (Thor does not auto-underscore them). `--refresh-cert`
432
- # bypasses the cert cache (`Kubeseal.new(refresh_cert:)`); it defaults to
433
- # false for every command that builds a kubeseal adapter.
423
+ # names are string keys (Thor does not auto-underscore them).
434
424
  #
435
- # The controller name/namespace are Kubernetes identifiers that flow into the
436
- # on-disk cert-cache path, so they are validated as DNS-1123 here (same gate
437
- # as the positional args) -- this prevents `../`, `/`, a leading `-`, or NUL
438
- # from escaping the cache directory before any path is built.
425
+ # The controller name/namespace are Kubernetes identifiers that flow straight
426
+ # into kubeseal's `--controller-name`/`--controller-namespace` flags, so they
427
+ # are validated as DNS-1123 here (same gate as the positional args) -- this
428
+ # rejects flag-injection (`-oyaml`, a leading `-`) and NUL before any value
429
+ # reaches the shell-out.
439
430
  def build_kubeseal
440
431
  controller_name = validated_controller("controller-name")
441
432
  controller_namespace = validated_controller("controller-namespace")
442
433
  Kubeseal.new(
443
434
  cert: options["cert"],
444
435
  controller_name: controller_name,
445
- controller_namespace: controller_namespace,
446
- refresh_cert: options.fetch("refresh-cert", false)
436
+ controller_namespace: controller_namespace
447
437
  )
448
438
  end
449
439
 
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "open3"
4
- require "fileutils"
5
- require "securerandom"
6
4
 
7
5
  module RKSeal
8
6
  # Thin adapter over the `kubeseal` binary.
@@ -19,9 +17,11 @@ module RKSeal
19
17
  # stub a single seam (or stub the public methods directly). The runner must
20
18
  # never echo stdin (the plaintext Secret) into logs or error messages.
21
19
  #
22
- # rubocop:disable Metrics/ClassLength -- the inline {CertCache} is co-located
23
- # here by design (the cert cache is intrinsic to this adapter and must not add
24
- # a new top-level require); that nested class accounts for the extra lines.
20
+ # The controller certificate is never cached on disk: an explicit `--cert` or
21
+ # `SEALED_SECRETS_CERT` is used offline, otherwise it is fetched fresh from the
22
+ # live controller on every invocation. This keeps a seal always bound to the
23
+ # current context's controller key -- no stale or cross-cluster cert can sneak
24
+ # in (see {#ensure_cert!}).
25
25
  class Kubeseal
26
26
  BINARY = "kubeseal"
27
27
 
@@ -32,13 +32,6 @@ module RKSeal
32
32
  cluster_wide: "cluster-wide"
33
33
  }.freeze
34
34
 
35
- # kubeseal's own defaults for the controller's identity. Used to name the
36
- # cache entry consistently when the caller does not override them, so a run
37
- # with implicit defaults and a run with explicit-but-identical flags share
38
- # one cached cert.
39
- DEFAULT_CONTROLLER_NAME = "sealed-secrets-controller"
40
- DEFAULT_CONTROLLER_NAMESPACE = "kube-system"
41
-
42
35
  # Substring kubeseal prints to stderr when the controller could decrypt-test
43
36
  # the SealedSecret but it is NOT valid. Anything else on a non-zero
44
37
  # `--validate` exit is treated as operational (CommandError), not a verdict.
@@ -48,16 +41,13 @@ module RKSeal
48
41
  # @param controller_name [String, nil] `--controller-name` value.
49
42
  # @param controller_namespace [String, nil] `--controller-namespace` value.
50
43
  # @param cert [String, nil] `--cert <file|URL>` source; when nil and no env
51
- # cert is present, the cert is fetched from the controller and cached.
52
- # @param refresh_cert [Boolean] when true, ignore any cached cert and
53
- # overwrite it with a freshly fetched one (wired to `--refresh-cert`).
44
+ # cert is present, the cert is fetched fresh from the controller per seal.
54
45
  def initialize(binary: BINARY, controller_name: nil, controller_namespace: nil,
55
- cert: nil, refresh_cert: false)
46
+ cert: nil)
56
47
  @binary = binary
57
48
  @controller_name = controller_name
58
49
  @controller_namespace = controller_namespace
59
50
  @cert = cert
60
- @refresh_cert = refresh_cert
61
51
  end
62
52
 
63
53
  # Verify the kubeseal binary is present and executable; raise otherwise.
@@ -73,13 +63,12 @@ module RKSeal
73
63
  "Install it from https://github.com/bitnami-labs/sealed-secrets/releases."
74
64
  end
75
65
 
76
- # Resolve the encryption certificate up front so a flow fails fast before any
77
- # editor opens. When an offline cert is configured (`--cert` or the
78
- # `SEALED_SECRETS_CERT` env var) nothing is contacted. Otherwise the cert is
79
- # resolved through the on-disk cache: a cached PEM is reused as-is, otherwise
80
- # it is fetched from the live controller and written to the cache (which is
81
- # what makes a subsequent {#seal} offline). `refresh_cert: true` skips the
82
- # cached copy and refetches.
66
+ # Confirm the encryption certificate is obtainable up front so a flow fails
67
+ # fast before any editor opens. When an offline cert is configured (`--cert`
68
+ # or the `SEALED_SECRETS_CERT` env var) nothing is contacted. Otherwise the
69
+ # controller is probed with `--fetch-cert`; the fetched PEM is intentionally
70
+ # discarded -- {#seal} re-fetches at seal time, so the freshest controller
71
+ # key is always used and nothing is persisted between invocations.
83
72
  #
84
73
  # @return [void]
85
74
  # @raise [RKSeal::CommandError] if no offline cert is configured and the
@@ -87,17 +76,17 @@ module RKSeal
87
76
  def ensure_cert!
88
77
  return if offline_cert?
89
78
 
90
- resolve_cached_cert_path
79
+ fetch_cert
91
80
  nil
92
81
  end
93
82
 
94
83
  # Seal a Secret manifest into a SealedSecret.
95
84
  #
96
85
  # Pipes `manifest_yaml` to kubeseal on stdin with `-o yaml` and the resolved
97
- # `--scope`. The certificate is resolved offline-first: an explicit `--cert`
98
- # or the cached controller PEM is passed via `--cert` (no API round-trip);
99
- # only when neither is available does kubeseal fall back to the env var or
100
- # the controller itself. Returns the SealedSecret YAML on stdout.
86
+ # `--scope`. An explicit `--cert` is passed straight through; otherwise no
87
+ # `--cert` is given and kubeseal resolves the cert itself -- from
88
+ # `SEALED_SECRETS_CERT`, or failing that fresh from the live controller.
89
+ # Returns the SealedSecret YAML on stdout.
101
90
  #
102
91
  # @param manifest_yaml [String] a full Secret manifest (from
103
92
  # {RKSeal::Secret#to_manifest}).
@@ -158,11 +147,10 @@ module RKSeal
158
147
  # entry untouched. This is what powers the offline `edit --local` flow,
159
148
  # where kept keys must stay byte-for-byte unchanged.
160
149
  #
161
- # The certificate is resolved offline-first, exactly like {#seal}: an
162
- # explicit `--cert` or the cached controller PEM is passed via `--cert` (no
163
- # API round-trip); only when neither is available does kubeseal fall back to
164
- # the env var or the controller. The output format is inherited from the
165
- # existing file, so `-o` is NOT forced here.
150
+ # The certificate is resolved exactly like {#seal}: an explicit `--cert` is
151
+ # passed through, otherwise kubeseal resolves it itself (env var, else fresh
152
+ # from the controller). The output format is inherited from the existing
153
+ # file, so `-o` is NOT forced here.
166
154
  #
167
155
  # @param manifest_yaml [String] Secret manifest with the items to add.
168
156
  # @param file [String] path to the existing SealedSecret to merge into.
@@ -218,30 +206,12 @@ module RKSeal
218
206
  ENV.fetch("SEALED_SECRETS_CERT", "")
219
207
  end
220
208
 
221
- # Resolve a cert file/URL to pass to `--cert`, or nil to let kubeseal fall
222
- # back to its own sources. Precedence: explicit `--cert` > env var (kubeseal
223
- # reads SEALED_SECRETS_CERT itself, so we pass nil) > cached/fetched PEM.
209
+ # The cert file/URL to pass to `--cert`, or nil to let kubeseal resolve it
210
+ # itself. Only an explicit `--cert` is forwarded; with none configured we
211
+ # pass nil so kubeseal reads SEALED_SECRETS_CERT or fetches fresh from the
212
+ # controller (the env var is never passed as a path -- kubeseal reads it).
224
213
  def resolved_cert_path
225
- return @cert if @cert
226
- return nil unless env_cert.strip.empty?
227
-
228
- resolve_cached_cert_path
229
- end
230
-
231
- # Return the path to a usable cached controller PEM, fetching and writing it
232
- # first if absent (or if a refresh was requested). The cert is public, so the
233
- # cache file is world-readable.
234
- def cert_cache
235
- @cert_cache ||= CertCache.new(
236
- controller_namespace: @controller_namespace || DEFAULT_CONTROLLER_NAMESPACE,
237
- controller_name: @controller_name || DEFAULT_CONTROLLER_NAME
238
- )
239
- end
240
-
241
- def resolve_cached_cert_path
242
- return cert_cache.path if !@refresh_cert && cert_cache.exist?
243
-
244
- cert_cache.write(fetch_cert)
214
+ @cert
245
215
  end
246
216
 
247
217
  # Whether kubeseal's stderr from a failed `--validate` is a validity verdict
@@ -302,81 +272,5 @@ module RKSeal
302
272
  def command_label(argv)
303
273
  [@binary, *argv].join(" ")
304
274
  end
305
-
306
- # On-disk cache for the controller's PUBLIC certificate, so repeated seals do
307
- # not each hit the cluster for `--fetch-cert`. The cert is public, hence the
308
- # world-readable 0644 perms. One entry per controller identity, under the XDG
309
- # cache dir:
310
- # ${XDG_CACHE_HOME:-$HOME/.cache}/rkseal/<namespace>/<name>.pem
311
- #
312
- # The namespace is a path segment rather than a `<namespace>-<name>` prefix
313
- # so two distinct identities can never collide on one file (e.g. `a-b`/`c`
314
- # vs `a`/`b-c`); a DNS-1123 name contains no `/`, so the layout is
315
- # unambiguous. Writes go through a temp file + atomic rename so a concurrent
316
- # seal never reads a half-written cert.
317
- #
318
- # Defined inline (not a separate file) so this adapter stays self-contained
319
- # and adds no new top-level require.
320
- class CertCache
321
- DIR_PERMS = 0o755
322
- FILE_PERMS = 0o644
323
-
324
- # @param controller_namespace [String]
325
- # @param controller_name [String]
326
- def initialize(controller_namespace:, controller_name:)
327
- @controller_namespace = controller_namespace
328
- @controller_name = controller_name
329
- end
330
-
331
- # @return [String] absolute path to this controller's cached PEM.
332
- def path
333
- File.join(cache_dir, @controller_namespace, "#{@controller_name}.pem")
334
- end
335
-
336
- # @return [Boolean] whether a cached PEM already exists.
337
- def exist?
338
- File.exist?(path)
339
- end
340
-
341
- # @return [String] the cached PEM contents.
342
- def read
343
- File.read(path)
344
- end
345
-
346
- # Persist a freshly fetched PEM (overwriting any existing entry) and return
347
- # its path so the caller can hand it to `--cert`.
348
- #
349
- # @param pem [String] certificate contents.
350
- # @return [String] the cache path that now holds the PEM.
351
- def write(pem)
352
- FileUtils.mkdir_p(File.dirname(path), mode: DIR_PERMS)
353
- write_atomically(pem)
354
- path
355
- end
356
-
357
- private
358
-
359
- # Write the PEM to a uniquely-named temp file in the same directory, then
360
- # rename it over the target. rename(2) is atomic within one filesystem, so
361
- # a concurrent seal sees either the old cert or the new one -- never a
362
- # half-written file. The temp file carries the final 0644 perms and is
363
- # removed if the rename never happens (e.g. an error mid-write).
364
- def write_atomically(pem)
365
- tmp = File.join(File.dirname(path), ".#{File.basename(path)}.#{SecureRandom.hex(8)}.tmp")
366
- File.write(tmp, pem)
367
- File.chmod(FILE_PERMS, tmp)
368
- File.rename(tmp, path)
369
- ensure
370
- File.unlink(tmp) if tmp && File.exist?(tmp)
371
- end
372
-
373
- # ${XDG_CACHE_HOME:-$HOME/.cache}/rkseal
374
- def cache_dir
375
- base = ENV.fetch("XDG_CACHE_HOME", nil)
376
- base = File.join(Dir.home, ".cache") if base.nil? || base.strip.empty?
377
- File.join(base, "rkseal")
378
- end
379
- end
380
275
  end
381
- # rubocop:enable Metrics/ClassLength
382
276
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RKSeal
4
- VERSION = "0.1.0"
4
+ VERSION = "0.1.1"
5
5
  end
data/rkseal.gemspec CHANGED
@@ -48,6 +48,7 @@ Gem::Specification.new do |spec|
48
48
 
49
49
  # Development dependencies. Versions are intentionally left as compatible ranges
50
50
  # rather than hard pins until the toolchain is proven on Ruby 4.0.2.
51
+ spec.add_development_dependency "rake", "~> 13.0"
51
52
  spec.add_development_dependency "rspec", "~> 3.13"
52
53
  spec.add_development_dependency "rubocop", "~> 1.60"
53
54
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rkseal
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Piotr Wojcieszonek
@@ -37,6 +37,20 @@ dependencies:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
39
  version: '0.2'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rake
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '13.0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '13.0'
40
54
  - !ruby/object:Gem::Dependency
41
55
  name: rspec
42
56
  requirement: !ruby/object:Gem::Requirement
@@ -121,7 +135,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
121
135
  - !ruby/object:Gem::Version
122
136
  version: '0'
123
137
  requirements: []
124
- rubygems_version: 4.0.6
138
+ rubygems_version: 4.0.10
125
139
  specification_version: 4
126
140
  summary: Interactively create and edit Kubernetes SealedSecrets via $EDITOR.
127
141
  test_files: []