kubesealr 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 35b04586dd5c804bf697478a11e919da402c87519cdb22bc940c9060b0b07792
4
+ data.tar.gz: cfba159bc9ce48fbe5add4a7640bfb3edfa1f66a79617855e22784e5fa60aaf9
5
+ SHA512:
6
+ metadata.gz: 5b199eef47106927b95b724393e1d4bd9bf003bdad5c13b3251b58e600c3afcac53d9824d2c2e922ef8dc1b0ac848acbd7404f8600a7b5f5769b65c49802a9bf
7
+ data.tar.gz: 41a52d3229fda407fa074cffae4b8b1af562182142d54c664bce35ae52493f5693d6343d638644bbb729b3f36fd931dbd89c4cdfbae4eed8c1c7a98107034ad9
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in kubesealr.gemspec
6
+ gemspec
7
+
8
+ # override dep resolution to fix Ruby 3.0 support
9
+ # (libs dependent on this lib must do this on their own)
10
+ gem 'k8s-ruby', github: 'tsutsu/k8s-ruby', branch: 'fork-master'
11
+ gem 'recursive-open-struct', github: 'tsutsu/recursive-open-struct', branch: 'fix-ruby3-support'
12
+
13
+ # development dependencies
14
+ gem 'rake', '~> 13.0'
15
+ gem 'rspec', '~> 3.0'
@@ -0,0 +1,95 @@
1
+ GIT
2
+ remote: https://github.com/tsutsu/k8s-ruby.git
3
+ revision: 177ba953169f32b6b40b85eedae85659825381b5
4
+ branch: fork-master
5
+ specs:
6
+ k8s-ruby (0.10.5)
7
+ dry-struct (~> 1.3.0)
8
+ dry-types (~> 1.4.0)
9
+ excon (~> 0.78)
10
+ hashdiff (~> 1.0.1)
11
+ jsonpath (~> 1.1.0)
12
+ recursive-open-struct (~> 1.1.3)
13
+ yajl-ruby (~> 1.4.1)
14
+ yaml-safe_load_stream2 (~> 0.1)
15
+
16
+ GIT
17
+ remote: https://github.com/tsutsu/recursive-open-struct.git
18
+ revision: 1467ac3107582ab32de016fa320c1e204b10a1af
19
+ branch: fix-ruby3-support
20
+ specs:
21
+ recursive-open-struct (1.1.3)
22
+
23
+ PATH
24
+ remote: .
25
+ specs:
26
+ kubesealr (0.1.0)
27
+ k8s-ruby
28
+ openssl-oaep
29
+
30
+ GEM
31
+ remote: https://rubygems.org/
32
+ specs:
33
+ concurrent-ruby (1.1.7)
34
+ diff-lcs (1.4.4)
35
+ dry-configurable (0.12.0)
36
+ concurrent-ruby (~> 1.0)
37
+ dry-core (~> 0.5, >= 0.5.0)
38
+ dry-container (0.7.2)
39
+ concurrent-ruby (~> 1.0)
40
+ dry-configurable (~> 0.1, >= 0.1.3)
41
+ dry-core (0.5.0)
42
+ concurrent-ruby (~> 1.0)
43
+ dry-equalizer (0.3.0)
44
+ dry-inflector (0.2.0)
45
+ dry-logic (1.1.0)
46
+ concurrent-ruby (~> 1.0)
47
+ dry-core (~> 0.5, >= 0.5)
48
+ dry-struct (1.3.0)
49
+ dry-core (~> 0.4, >= 0.4.4)
50
+ dry-equalizer (~> 0.3)
51
+ dry-types (~> 1.3)
52
+ ice_nine (~> 0.11)
53
+ dry-types (1.4.0)
54
+ concurrent-ruby (~> 1.0)
55
+ dry-container (~> 0.3)
56
+ dry-core (~> 0.4, >= 0.4.4)
57
+ dry-equalizer (~> 0.3)
58
+ dry-inflector (~> 0.1, >= 0.1.2)
59
+ dry-logic (~> 1.0, >= 1.0.2)
60
+ excon (0.78.1)
61
+ hashdiff (1.0.1)
62
+ ice_nine (0.11.2)
63
+ jsonpath (1.1.0)
64
+ multi_json
65
+ multi_json (1.15.0)
66
+ openssl-oaep (0.1.0)
67
+ rake (13.0.3)
68
+ rspec (3.10.0)
69
+ rspec-core (~> 3.10.0)
70
+ rspec-expectations (~> 3.10.0)
71
+ rspec-mocks (~> 3.10.0)
72
+ rspec-core (3.10.1)
73
+ rspec-support (~> 3.10.0)
74
+ rspec-expectations (3.10.1)
75
+ diff-lcs (>= 1.2.0, < 2.0)
76
+ rspec-support (~> 3.10.0)
77
+ rspec-mocks (3.10.1)
78
+ diff-lcs (>= 1.2.0, < 2.0)
79
+ rspec-support (~> 3.10.0)
80
+ rspec-support (3.10.1)
81
+ yajl-ruby (1.4.1)
82
+ yaml-safe_load_stream2 (0.1.1)
83
+
84
+ PLATFORMS
85
+ x86_64-darwin-20
86
+
87
+ DEPENDENCIES
88
+ k8s-ruby!
89
+ kubesealr!
90
+ rake (~> 13.0)
91
+ recursive-open-struct!
92
+ rspec (~> 3.0)
93
+
94
+ BUNDLED WITH
95
+ 2.2.5
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2021 Levi Aul
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,110 @@
1
+ # KubesealR
2
+
3
+ KubesealR is a pure-Ruby implementation of the CLI client `kubeseal` component
4
+ of Bitnami's [Kubernetes sealed-secrets](https://github.com/bitnami-labs/sealed-secrets)
5
+ system.
6
+
7
+ KubesealR also embeds a transformer plugin for [KustomizeR](https://github.com/tsutsu/kustomizer),
8
+ a pure-Ruby [Kustomize](https://kustomize.io) implementation. This plugin allows
9
+ KustomizeR to seal [generated *or* provided] secrets, as a resource-config
10
+ transformation pass.
11
+
12
+ ## Installation
13
+
14
+ Add this line to your application's Gemfile:
15
+
16
+ ```ruby
17
+ gem 'kubesealr'
18
+ ```
19
+
20
+ And then execute:
21
+
22
+ $ bundle install
23
+
24
+ Or install it yourself as:
25
+
26
+ $ gem install kubesealr
27
+
28
+ ## Usage
29
+
30
+ ### CLI usage
31
+
32
+ (TODO; will mostly match CLI usage of `kubeseal`)
33
+
34
+ ### KustomizeR Plugin usage
35
+
36
+ KubesealR provides one [Kustomize transformer plugin](https://kubectl.docs.kubernetes.io/guides/extending_kustomize/#specification-in-kustomizationyaml),
37
+ `SealSecretsTransform`, with the following K8s document-type:
38
+
39
+ ```yaml
40
+ apiVersion: kubesealr.covalenthq.com/v1
41
+ kind: SealSecretsTransform
42
+ ```
43
+
44
+ Possible fields on this configuration:
45
+
46
+ #### `.spec.match`
47
+
48
+ ```
49
+ .spec.match: "all" | [String] | {pattern: RegexpString}
50
+ ```
51
+
52
+ `.spec.match` controls which secrets will be sealed.
53
+
54
+ * The literal string `all` will match all secrets.
55
+ * A list of strings means "match secrets with these names exactly."
56
+ * Setting `.spec.match.pattern` to a string will treat that string as
57
+ a regular expression and use it to match secret names.
58
+
59
+ Defaults to `all`.
60
+
61
+ #### `.spec.keepUnsealed`
62
+
63
+ ```
64
+ .spec.keepUnsealed: true | false
65
+ ```
66
+
67
+ `.spec.keepUnsealed` controls whether the original, unsealed versions of
68
+ the secrets will be emitted by the transform alongside the sealed versions.
69
+ Defaults to `false`. You may want to set this to `true` if you have further
70
+ transformation passes that depend on the unsealed secrets in some way.
71
+
72
+ Add a new resource path to the `transformers` resource-list in your
73
+ `kustomization.yaml`. For example:
74
+
75
+ #### Full example Kustomize configuration
76
+
77
+ In `kustomization.yaml`:
78
+
79
+ ```yaml
80
+ ---
81
+ transformers:
82
+ - seal-secrets.yaml
83
+ ```
84
+
85
+
86
+ In `seal-secrets.yaml`:
87
+
88
+ ```yaml
89
+ ---
90
+ apiVersion: kubesealr.covalenthq.com/v1
91
+ kind: SealSecretsTransform
92
+ spec:
93
+ match:
94
+ pattern: "-conn$"
95
+ keepUnsealed: false
96
+ ```
97
+
98
+ ## Development
99
+
100
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
101
+
102
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
103
+
104
+ ## Contributing
105
+
106
+ Bug reports and pull requests are welcome on GitHub at https://github.com/tsutsu/kubesealr.
107
+
108
+ ## License
109
+
110
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "kubesealr"
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require "irb"
15
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'kubeseal/cli'
4
+ Kubeseal::CLI.start(ARGV)
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/kubeseal/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "kubesealr"
7
+ spec.version = Kubeseal::VERSION
8
+ spec.authors = ["Levi Aul"]
9
+ spec.email = ["levi@leviaul.com"]
10
+
11
+ spec.summary = "K8s sealed-secret client and KustomizeR transformer"
12
+ spec.description = <<-EOF
13
+ KubesealR is a pure-Ruby implementation of Bitnami's Sealed-Secrets
14
+ CLI client "kubeseal." KubesealR also embeds a plugin for KustomizeR,
15
+ allowing KustomizeR to seal (generated or provided) secrets as a
16
+ resource-config transformation pass.
17
+ EOF
18
+ spec.homepage = "https://github.com/tsutsu/kubesealr"
19
+ spec.license = "MIT"
20
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.7.0")
21
+
22
+ spec.metadata["homepage_uri"] = spec.homepage
23
+ spec.metadata["source_code_uri"] = spec.homepage
24
+
25
+ # Specify which files should be added to the gem when it is released.
26
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
27
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
28
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
29
+ end
30
+ spec.bindir = "exe"
31
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
32
+ spec.require_paths = ["lib"]
33
+
34
+ spec.add_runtime_dependency 'k8s-ruby'
35
+ spec.add_runtime_dependency 'openssl-oaep'
36
+ end
@@ -0,0 +1,259 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
4
+ require 'securerandom'
5
+ require 'base64'
6
+
7
+ require 'openssl/oaep'
8
+
9
+ require 'kubeseal/version'
10
+
11
+ class Kubeseal
12
+ DEFAULT_KEY_FETCHER = lambda do |_|
13
+ raise NotImplementedError, "no cert getter passed"
14
+ end
15
+
16
+ def initialize(&cluster_sealer_key_fetcher)
17
+ @cluster_sealer_key_fetcher = cluster_sealer_key_fetcher || DEFAULT_KEY_FETCHER
18
+ end
19
+
20
+ def cluster_sealer_public_key
21
+ @cluster_sealer_public_key ||= @cluster_sealer_key_fetcher.call(:public_key)
22
+ end
23
+
24
+ def cluster_sealer_private_keys
25
+ @cluster_sealer_private_keys ||= @cluster_sealer_key_fetcher.call(:private_keys)
26
+ end
27
+
28
+ AED_IV = ("\x00" * 12).freeze
29
+ private_constant :AED_IV
30
+
31
+ def seal_and_wrap(secret_rc, scope: :strict)
32
+ rc_name = secret_rc.dig('metadata', 'name')
33
+ rc_namespace = secret_rc.dig('metadata', 'namespace')
34
+ rc_type = secret_rc['type'] || 'Opaque'
35
+ scope_label = label_for(scope, rc_namespace, rc_name)
36
+
37
+ raw_data =
38
+ if secret_rc.has_key?('data')
39
+ unarmor(secret_rc['data'])
40
+ elsif secret_rc.has_key?('stringData')
41
+ secret_rc['stringData']
42
+ else
43
+ {}
44
+ end
45
+
46
+ encrypted_data = raw_data.map do |key, plaintext|
47
+ ciphertext = seal(plaintext, scope_label)
48
+ [key, ciphertext]
49
+ end.to_h
50
+
51
+ secret_type = secret_rc['type'] || 'Opaque'
52
+
53
+ sealed_secret_rc =
54
+ build_sealed_secret_rc(
55
+ rc_namespace,
56
+ rc_name,
57
+ secret_type,
58
+ armor(encrypted_data)
59
+ )
60
+
61
+ patch_with_scope(sealed_secret_rc, scope)
62
+ end
63
+
64
+ def unwrap_and_unseal(sealed_secret_rc, armor: true)
65
+ rc_name = sealed_secret_rc.dig('metadata', 'name')
66
+ rc_namespace = sealed_secret_rc.dig('metadata', 'namespace')
67
+ rc_type = sealed_secret_rc['type'] || 'Opaque'
68
+
69
+ scope = scope_from_annotations(sealed_secret_rc.dig('metadata', 'annotations'))
70
+ scope_label = label_for(scope, rc_namespace, rc_name)
71
+
72
+ # decode data if encoded
73
+ encrypted_data = unarmor(sealed_secret_rc.dig('spec', 'encryptedData'))
74
+
75
+ string_data = encrypted_data.map do |key, ciphertext|
76
+ plaintext = unseal(ciphertext, scope_label)
77
+ [key, plaintext]
78
+ end.to_h
79
+
80
+ merge_part =
81
+ if armor
82
+ {'data' => armor(string_data)}
83
+ else
84
+ {'stringData' => string_data}
85
+ end
86
+
87
+ secret_type = sealed_secret_rc.dig('spec', 'template', 'type') || 'Opaque'
88
+
89
+ build_secret_rc(
90
+ rc_namespace,
91
+ rc_name,
92
+ secret_type,
93
+ merge_part
94
+ )
95
+ end
96
+
97
+ private
98
+ def seal(plaintext, scope_label)
99
+ cs_pubkey = self.cluster_sealer_public_key
100
+
101
+ session_key = SecureRandom.bytes(32)
102
+
103
+ session_key_enc =
104
+ cs_pubkey.public_encrypt_oaep(
105
+ session_key,
106
+ scope_label,
107
+ OpenSSL::Digest::SHA256
108
+ )
109
+
110
+ aed_cipher = OpenSSL::Cipher.new("AES-256-GCM").encrypt
111
+ aed_cipher.key = session_key
112
+ aed_cipher.iv = AED_IV
113
+ aed_cipher.auth_data = ""
114
+
115
+ payload_enc =
116
+ aed_cipher.update(plaintext) +
117
+ aed_cipher.final +
118
+ aed_cipher.auth_tag
119
+
120
+ ciphertext_parts = [
121
+ session_key_enc.length,
122
+ session_key_enc,
123
+ payload_enc
124
+ ]
125
+
126
+ ciphertext_parts.pack('S>A*A*')
127
+ end
128
+
129
+ private
130
+ def unseal(ciphertext, scope_label)
131
+ cs_privkeys_to_try = self.cluster_sealer_private_keys.dup
132
+
133
+ session_key_enc_len, ciphertext = ciphertext.unpack('S>A*')
134
+ session_key_enc, ciphertext = ciphertext.unpack("A#{session_key_enc_len}A*")
135
+
136
+ session_key = nil
137
+ until session_key or cs_privkeys_to_try.empty?
138
+ begin
139
+ try_privkey = cs_privkeys_to_try.shift
140
+ session_key = try_privkey.private_decrypt_oaep(
141
+ session_key_enc,
142
+ scope_label,
143
+ OpenSSL::Digest::SHA256
144
+ )
145
+ rescue OpenSSL::PKey::RSAError => e
146
+ end
147
+ end
148
+
149
+ unless session_key
150
+ raise RuntimeError, "no keys from cluster were applicable to decrypting payload"
151
+ end
152
+
153
+ aed_decipher = OpenSSL::Cipher.new("AES-256-GCM").decrypt
154
+ aed_decipher.key = session_key
155
+ aed_decipher.iv = AED_IV
156
+
157
+ auth_tag_len = 16
158
+ auth_tag = ciphertext[-auth_tag_len .. -1]
159
+ ciphertext = ciphertext[0 ... -auth_tag_len]
160
+
161
+ aed_decipher.auth_tag = auth_tag
162
+ aed_decipher.auth_data = ""
163
+
164
+ plaintext =
165
+ aed_decipher.update(ciphertext) +
166
+ aed_decipher.final
167
+
168
+ plaintext
169
+ end
170
+
171
+ private
172
+ def label_for(scope, rc_namespace, rc_name)
173
+ case scope
174
+ in :strict
175
+ "#{rc_namespace}/#{rc_name}"
176
+ in :"namespace-wide"
177
+ rc_namespace
178
+ in :"cluster-wide"
179
+ ""
180
+ end
181
+ end
182
+
183
+ CLUSTER_WIDE_ANNOT = 'sealedsecrets.bitnami.com/cluster-wide'.freeze
184
+ private_constant :CLUSTER_WIDE_ANNOT
185
+
186
+ NAMESPACE_WIDE_ANNOT = 'sealedsecrets.bitnami.com/namespace-wide'.freeze
187
+ private_constant :NAMESPACE_WIDE_ANNOT
188
+
189
+ private
190
+ def scope_from_annotations(annotations)
191
+ annotations ||= {}
192
+
193
+ if annotations[CLUSTER_WIDE_ANNOT] == 'true'
194
+ :"cluster-wide"
195
+ elsif annotations[NAMESPACE_WIDE_ANNOT] == 'true'
196
+ :"namespace-wide"
197
+ else
198
+ :strict
199
+ end
200
+ end
201
+
202
+ private
203
+ def build_secret_rc(rc_namespace, rc_name, secret_type, data_part)
204
+ {
205
+ 'apiVersion' => 'v1',
206
+ 'kind' => 'Secret',
207
+ 'metadata' => {
208
+ 'namespace' => rc_namespace,
209
+ 'name' => rc_name
210
+ },
211
+ 'type' => secret_type
212
+ }.merge(data_part)
213
+ end
214
+
215
+ private
216
+ def build_sealed_secret_rc(rc_namespace, rc_name, secret_type, encrypted_data)
217
+ {
218
+ 'apiVersion' => 'bitnami.com/v1alpha1',
219
+ 'kind' => 'SealedSecret',
220
+ 'metadata' => {
221
+ 'namespace' => rc_namespace,
222
+ 'name' => rc_name
223
+ },
224
+ 'spec' => {
225
+ 'template' => {
226
+ 'type' => secret_type,
227
+ },
228
+ 'encryptedData' => encrypted_data
229
+ }
230
+ }
231
+ end
232
+
233
+ private
234
+ def patch_with_scope(sealed_secret_rc, scope)
235
+ case scope
236
+ in :strict
237
+ sealed_secret_rc
238
+ in :"namespace-wide"
239
+ sealed_secret_rc['metadata'] ||= {}
240
+ sealed_secret_rc['metadata']['annotations'] ||= {}
241
+ sealed_secret_rc['metadata']['annotations'][NAMESPACE_WIDE_ANNOT] = "true"
242
+ sealed_secret_rc
243
+ in :"cluster-wide"
244
+ sealed_secret_rc['metadata'] ||= {}
245
+ sealed_secret_rc['metadata']['annotations'] ||= {}
246
+ sealed_secret_rc['metadata']['annotations'][CLUSTER_WIDE_ANNOT] = "true"
247
+ sealed_secret_rc
248
+ end
249
+ end
250
+
251
+ def armor(h)
252
+ (h || {}).map{ |k, v| [k, Base64.encode64(v)] }.to_h
253
+ end
254
+
255
+ def unarmor(h)
256
+ (h || {}).map{ |k, v| [k, Base64.decode64(v)] }.to_h
257
+ end
258
+ end
259
+
@@ -0,0 +1,101 @@
1
+ require 'optparse'
2
+ require 'yaml'
3
+
4
+ require 'k8s-ruby'
5
+
6
+ require 'kubeseal'
7
+
8
+ class Kubeseal::CLI
9
+ def self.start(argv = ARGV)
10
+ trap('INT'){ Kernel.exit(0) }
11
+ self.new(argv).run
12
+ end
13
+
14
+ def initialize(argv = ARGV)
15
+ @k8s_client = K8s::Client.autoconfig
16
+
17
+ @mode = :encrypt
18
+ @scope = :strict
19
+ @decrypt_rearmor = true
20
+ self.option_parser.parse(argv)
21
+
22
+ @sealer = Kubeseal.new do |fetch_mode|
23
+ case fetch_mode
24
+ in :public_key
25
+ fetch_cluster_sealer_active_public_key
26
+ in :private_keys
27
+ fetch_cluster_sealer_all_private_keys
28
+ end
29
+ end
30
+ end
31
+
32
+ def option_parser
33
+ OptionParser.new do |parser|
34
+ parser.banner = "Usage: kubesealr [options]"
35
+
36
+ parser.on("-d", "--decrypt", "Unseal sealed secrets (requires k8s User to have get access to secrets in kube-system namespace)") do |t|
37
+ @mode = :decrypt
38
+ end
39
+
40
+ parser.on("-a", "--[no-]armor", "Emit base64-armored secrets when unsealing") do |t|
41
+ @decrypt_rearmor = t
42
+ end
43
+
44
+ parser.on("-sTYPE", "--scope TYPE", [:strict, :"namespace-wide", :"cluster-wide"],
45
+ "Select scope (strict, namespace-wide, cluster-wide)") do |v|
46
+ @scope = v
47
+ end
48
+ end
49
+ end
50
+
51
+ def run
52
+ case @mode
53
+ in :encrypt
54
+ $stdout.puts(seal_stream($stdin.read))
55
+ in :decrypt
56
+ $stdout.puts(unseal_stream($stdin.read))
57
+ end
58
+ end
59
+
60
+ private
61
+ def fetch_cluster_sealer_active_public_key
62
+ cert_req_opts = {
63
+ method: 'GET',
64
+ path: '/api/v1/namespaces/kube-system/services/sealed-secrets-controller:8080/proxy/v1/cert.pem'
65
+ }.merge(@k8s_client.transport.request_options)
66
+
67
+ cert_resp = @k8s_client.transport.excon.request(cert_req_opts)
68
+
69
+ cert_pem_str = cert_resp.body
70
+
71
+ cluster_certificate = OpenSSL::X509::Certificate.new(cert_pem_str)
72
+
73
+ cluster_certificate.public_key
74
+ end
75
+
76
+ private
77
+ def fetch_cluster_sealer_all_private_keys
78
+ privkey_pem_strs =
79
+ @k8s_client.api('v1')
80
+ .resource('secrets', namespace: 'kube-system')
81
+ .list(fieldSelector: {'type' => 'kubernetes.io/tls'})
82
+ .filter{ |r| r.metadata.generateName == 'sealed-secrets-key' }
83
+ .map{ |r| Base64.decode64(r.data['tls.key']) }
84
+
85
+ privkey_pem_strs.map{ |pem| OpenSSL::PKey::RSA.new(pem) }
86
+ end
87
+
88
+ private
89
+ def seal_stream(secret_yaml_stream)
90
+ YAML.load_stream(secret_yaml_stream).map do |secret_rc|
91
+ @sealer.seal_and_wrap(secret_rc, scope: @scope).to_yaml
92
+ end.join("")
93
+ end
94
+
95
+ private
96
+ def unseal_stream(sealed_secret_yaml_stream)
97
+ YAML.load_stream(sealed_secret_yaml_stream).map do |sealed_secret_rc|
98
+ @sealer.unwrap_and_unseal(sealed_secret_rc, armor: @decrypt_rearmor).to_yaml
99
+ end.join("")
100
+ end
101
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Kubeseal
4
+ VERSION = "0.1.0"
5
+ end
metadata ADDED
@@ -0,0 +1,92 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: kubesealr
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Levi Aul
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2021-01-20 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: k8s-ruby
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: openssl-oaep
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description: |2
42
+ KubesealR is a pure-Ruby implementation of Bitnami's Sealed-Secrets
43
+ CLI client "kubeseal." KubesealR also embeds a plugin for KustomizeR,
44
+ allowing KustomizeR to seal (generated or provided) secrets as a
45
+ resource-config transformation pass.
46
+ email:
47
+ - levi@leviaul.com
48
+ executables:
49
+ - kubesealr
50
+ extensions: []
51
+ extra_rdoc_files: []
52
+ files:
53
+ - ".gitignore"
54
+ - ".rspec"
55
+ - Gemfile
56
+ - Gemfile.lock
57
+ - LICENSE
58
+ - README.md
59
+ - Rakefile
60
+ - bin/console
61
+ - bin/setup
62
+ - exe/kubesealr
63
+ - kubesealr.gemspec
64
+ - lib/kubeseal.rb
65
+ - lib/kubeseal/cli.rb
66
+ - lib/kubeseal/version.rb
67
+ homepage: https://github.com/tsutsu/kubesealr
68
+ licenses:
69
+ - MIT
70
+ metadata:
71
+ homepage_uri: https://github.com/tsutsu/kubesealr
72
+ source_code_uri: https://github.com/tsutsu/kubesealr
73
+ post_install_message:
74
+ rdoc_options: []
75
+ require_paths:
76
+ - lib
77
+ required_ruby_version: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: 2.7.0
82
+ required_rubygems_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ requirements: []
88
+ rubygems_version: 3.2.3
89
+ signing_key:
90
+ specification_version: 4
91
+ summary: K8s sealed-secret client and KustomizeR transformer
92
+ test_files: []