muchkeys 0.0.1 → 0.3.3

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
  SHA1:
3
- metadata.gz: 295bc96c509542f52c01c3a7be03cf95d2507dcc
4
- data.tar.gz: 0cd586cb9d7d43e67029f8c9f15fb3979c541480
3
+ metadata.gz: 3ff8551deefce6e7a842d98364ea5667fa4532e3
4
+ data.tar.gz: ff2729f7f21a56f51bb9998729c80a7c96d87ec5
5
5
  SHA512:
6
- metadata.gz: 991c8c9dddc589a1af5da74e80234a7a8152309cfc6b5dbcef2035acdb97b8df33022a182ac8bdebe42064b8358897cbf667a7794decbcc6432b7e1292253a1e
7
- data.tar.gz: ea47aa2d9d97676f423c7c8fa68da3b530645468e8be796076364989e37029ec2a335aee10d87c18642d3dae6ebfed5e7800ff9e6e05358b3f3349c6dad356c7
6
+ metadata.gz: 4d0541d5c159955b22471d808fb2edb0f042e5b3a03fe725deb7ea7c041a3ed5d92f1f5a120223531742da56478976ecde08e833ae0399564a3f4145b26c67e5
7
+ data.tar.gz: 413f9778cd45d3aed726c7c23c685a032259f870c021c62b2bbd7f72b9f73f1d307ef568ed12263bc873feb3e5dc2524fd5e51d2803012833e6b2fcc947528a2
data/.gitignore CHANGED
@@ -9,3 +9,6 @@
9
9
  /tmp/
10
10
  **.un~
11
11
  *.gem
12
+ *.swp
13
+
14
+ tags
data/.rspec CHANGED
@@ -1,2 +1,3 @@
1
1
  --format documentation
2
2
  --color
3
+ --order random
data/Guardfile CHANGED
@@ -1,17 +1,12 @@
1
- guard :rspec, cmd: "bundle exec rspec" do
2
- require "guard/rspec/dsl"
3
- dsl = Guard::RSpec::Dsl.new(self)
1
+ guard_options = {
2
+ cmd: "bundle exec rspec --color --format=doc",
3
+ all_after_pass: false,
4
+ all_after_fail: false,
5
+ all_on_start: false
6
+ }
4
7
 
5
- # RSpec files
6
- rspec = dsl.rspec
7
- watch(rspec.spec_helper) { rspec.spec_dir }
8
- watch(rspec.spec_support) { rspec.spec_dir }
9
- watch(rspec.spec_files)
10
-
11
- # Ruby files
12
- ruby = dsl.ruby
13
- dsl.watch_spec_files_for(ruby.lib_files)
14
-
15
- # TODO: use the new rspec dsl here, but how?
16
- watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
8
+ guard :rspec, guard_options do
9
+ watch(%r{^spec/.+_spec\.rb$})
10
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
11
+ watch('spec/spec_helper.rb') { "spec" }
17
12
  end
data/README.md CHANGED
@@ -1,11 +1,35 @@
1
1
  # MuchKeys
2
2
 
3
- MuchKeys lets you store your ENV key/value settings in consul. It's primary use is
4
- in production where it will check to see if a ENV variable exists and then
5
- look in consul.
3
+ ─────────▄──────────────▄
4
+ ────────▌▒█───────────▄▀▒▌
5
+ ────────▌▒▒▀▄───────▄▀▒▒▒▐
6
+ ───────▐▄▀▒▒▀▀▀▀▄▄▄▀▒▒▒▒▒▐
7
+ ─────▄▄▀▒▒▒▒▒▒▒▒▒▒▒█▒▒▄█▒▐
8
+ ───▄▀▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▀██▀▒▌
9
+ ──▐▒▒▒▄▄▄▒▒▒▒▒▒▒▒▒▒▒▒▒▀▄▒▒▌
10
+ ──▌▒▒▐▄█▀▒▒▒▒▄▀█▄▒▒▒▒▒▒▒█▒▐
11
+ ─▐▒▒▒▒▒▒▒▒▒▒▒▌██▀▒▒▒▒▒▒▒▒▀▄▌
12
+ ─▌▒▀▄██▄▒▒▒▒▒▒▒▒▒▒▒░░░░▒▒▒▒▌
13
+ ─▌▀▐▄█▄█▌▄▒▀▒▒▒▒▒▒░░░░░░▒▒▒▐
14
+ ▐▒▀▐▀▐▀▒▒▄▄▒▄▒▒▒▒▒░░░░░░▒▒▒▒▌
15
+ ▐▒▒▒▀▀▄▄▒▒▒▄▒▒▒▒▒▒░░░░░░▒▒▒▐
16
+ ─▌▒▒▒▒▒▒▀▀▀▒▒▒▒▒▒▒▒░░░░▒▒▒▒▌
17
+ ─▐▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▐
18
+ ──▀▄▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▄▒▒▒▒▌
19
+ ────▀▄▒▒▒▒▒▒▒▒▒▒▄▄▄▀▒▒▒▒▄▀
20
+ ───▐▀▒▀▄▄▄▄▄▄▀▀▀▒▒▒▒▒▄▄▀
21
+ ──▐▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▀▀
6
22
 
7
- It's sort of the opposite of `envconsul` (golang) or similar to `dotenv` designed
8
- for use in production.
23
+
24
+ MuchKeys lets you store your application keys in consul and then leverages many conventions to create a
25
+ pleasant API. Keys in this context can mean application settings, knobs, dials, passwords, api keys
26
+ or anything. It's primary use is in production where it will check to see if a ENV variable exists first and then
27
+ search consul for a key based on an opinionated hierarchy convention.
28
+
29
+ You will want to read below about the convention and assumptions first to see if this works for
30
+ you.
31
+
32
+ MuchKeys also has a way of using encrypted secrets stored in consul. MuchKeys has a command line interface to help you encrypt or read secrets stored in consul.
9
33
 
10
34
 
11
35
  ## Installation
@@ -18,28 +42,242 @@ gem 'muchkeys'
18
42
 
19
43
  And then execute:
20
44
 
21
- $ bundle
45
+ ```
46
+ $ bundle
47
+ ```
22
48
 
23
49
  Or install it yourself as:
24
50
 
25
- $ gem install muchkeys
51
+ ```
52
+ $ gem install muchkeys
53
+ ```
54
+
26
55
 
27
56
  ## Usage
28
57
 
58
+ Use a snippet like this below in your YAML config files and anywhere else to use
59
+ a central configuration store while retaining control of your local development environment.
60
+
61
+ ```
62
+ <%= MUCHKEYS['widget_api_key'] %>
63
+ ```
64
+
65
+ Now the `widget_api_key` is coming from a central location. Devs can override `widget_api_key` with an ENV setting. `export WIDGET_API_KEY="development_key76"` MuchKeys defers to ENV when
66
+ it sees one set.
67
+
68
+ MuchKeys will look in consul (a key/value store) for keys (settings/secrets/knobs to turn).
69
+ It searches in an order of convention. It is also assuming you want to use git2consul
70
+ to enable your developers to add their own keys. Git2consul syncs a git repo to consul.
71
+
72
+ So let's walk through what happens when a developer is adding a new feature to an app.
73
+ The developer is integrating an API from reddit into an app called `feed_reader`. So now they have a reddit API key
74
+ and this is something new to the app.
75
+
76
+ * The developer adds twitter_api_key to `.env` from the dotenv gem to control their own development
77
+ environment. Staging and production have no idea this has happened.
78
+ * The developer adds/commits a file to `<git2consul repo>/feed_reader/twitter_api_key`
79
+ * git2consul is sync'd to consul and their new key is there. (This might happen through cron or some other way)
80
+ * They use their key in a configuration file or in the app: `<%= MUCHKEYS['redis_api_key'] %>`
81
+ * When they deploy to staging/production the key is there and the `feed_reader` app doesn't explode.
82
+ * Ops wasn't involved. Developers are empowered.
83
+
84
+
85
+ MuchKeys does this magic through a search order. It searches consul for the key
86
+ in certain order: (notice that the key isn't prefixed when you look up `twitter_api_key` with
87
+ `MUCHKEYS['twitter_api_key']`)
88
+
89
+ It will search a consul hierarchy looking for `twitter_api_key` in this search order:
90
+
91
+ ```
92
+ git/feed_reader/secrets
93
+ git/feed_reader/config
94
+ git/shared/secrets
95
+ git/shared/config
96
+ ```
97
+
98
+ The above `feed_reader` is detected from `Rails.application`. If you don't have a Rails app, then you can set
99
+ the application name in the configure block:
100
+
29
101
  ```
102
+ MuchKeys.configure do |config|
103
+ config.application_name = "rack_app"
104
+ end
105
+ ```
106
+
107
+ The order and paths that it searches is configurable:
108
+
109
+ ```
110
+ MuchKeys.configure do |config|
111
+ config.search_paths = %W(
112
+ app_name/keys app_name/secrets shared/keys shared/secrets
113
+ )
114
+ end
115
+ ```
116
+
117
+ This would look for keys and settings in consul under these paths instead:
118
+
119
+ ```
120
+ app_name/keys
121
+ app_name/secrets
122
+ shared/keys
123
+ shared/secrets
124
+ ```
125
+
126
+ Anything that has /secrets/ in it is going to be assumed to be a secret. There is an assumption that you have
127
+ your keys and secrets organized like this (one deep nesting). This is configurable.
128
+
129
+ ```
130
+ # if you don't put your secrets in something like shared/secrets/
131
+ # but shared/passwords/
132
+ MuchKeys.configure do |config|
133
+ config.secrets_path_hint = "passwords/"
134
+ end
135
+ ```
136
+
137
+ ### Using in a Rails App
138
+
139
+ Add to your Gemfile.
140
+ ```
141
+ gem 'muchkeys'
142
+ ```
143
+
144
+ Run `bundle install`.
145
+
146
+ If you don't need to change your hostname (production you do not need to) or any other settings, then you are
147
+ done. If you need to override defaults, create an initializer.
148
+
149
+ ```
150
+ # config/initializers/muchkeys.rb
151
+ MuchKeys.configure do |config|
152
+ # your settings if desired
153
+ end
154
+ ```
155
+
156
+ Then, use in YAML files, in the app or in other initializers.
157
+
158
+ ```
159
+ # example of centralizing a database password in database.yml
160
+ # blog/config/database.yml
161
+ production:
162
+ <<: *default
163
+ username: MUCHKEYS['database_user'] # set in consul at git/blog/config/database_user
164
+ password: MUCHKEYS['database_password'] # set in consul at git/blog/secrets/database_password
165
+ ```
166
+
167
+ In the above example, `database_password` needs to be encrypted with an SSL certificate
168
+ and then set in consul (paste the PEM text). The private and public key are needed to decrypt
169
+ and these keys should be stored in `<deploy_user_home>/.keys/blog.pem`. With these conventions
170
+ set once, you won't have to do anything else.
171
+
172
+
173
+ ### Encrypted Secrets
174
+
175
+ Generate an SSL cert or use an existing one. You will need the private and public key to encrypt and the public key to decrypt.
176
+
177
+ ```bash
178
+ openssl req -new -newkey rsa:4096 -nodes -x509 -keyout /tmp/self_signed_test.pem -out /tmp/self_signed_test.pem
179
+ ```
180
+
181
+ ```ruby
182
+ MuchKeys.configure do |config|
183
+ config.public_key = "/tmp/self_signed_test.pem"
184
+ config.private_key = "/tmp/self_signed_test.pem"
185
+ end
186
+
187
+ puts MuchKeys::Secret.encrypt_string("bacon is just ok").to_s
188
+ # => -----BEGIN PKCS7-----
189
+ # => MIICuwYJKoZIhvcNAQcDoIICrDCCAqgCAQAxggJuMIICagIBADBSMEUxCzAJ ...
190
+ # => ...
191
+
192
+ # Copy and paste this into consul under a key: ie: secrets/fake
193
+
194
+ # Now you can fetch the secret with the same public key.
195
+ MuchKeys::Secret.get_consul_secret('secrets/fake')
196
+ # => bacon is just ok
197
+ ```
198
+
199
+ Inside your app:
200
+ ```ruby
201
+ # inside a rails initializer or other setup file you have
202
+ MuchKeys.configure do |config|
203
+ config.consul_url = "http://myrealhost" # Default is "http://localhost:8500"
204
+ config.public_key = "path to public .pem" # this is only required if you are encrypting secrets
205
+ config.private_key = "path to private .pem" # this is only required if you are encrypting secrets
206
+ end
207
+
208
+ # then inside your YAML or app:
209
+ MuchKeys.fetch_key('number_of_threads')
210
+ # goes to git/config/myapp/number_of_threads in consul
211
+ ```
212
+
213
+
214
+ ## Automagic Certificates
215
+
216
+ MuchKeys can find certs automatically in `~/.keys`. `MuchKeys.fetch_key("git/waffles/secrets/a_password")` will try to decrypt `a_password`
217
+ with a cert called `~/.keys/waffles.pem`. The private key can be in the same file but should be protected
218
+ with file permissions `0600`.
219
+
220
+
221
+ ## CLI
222
+
223
+ Example of decrypted an encrypted key:
224
+
225
+ ```
226
+ $ muchkeys -d --public_key=~/.keys/staging.pem --private_key=~/.keys/staging.pem --consul_url=http://consul.domain:8500 --consul_key git/pants/secrets/some_password
227
+ <unencrypted secret is displayed>
228
+ ```
229
+
230
+ ### Other Usage
231
+
232
+ You can fetch individual keys if you want.
233
+
234
+ ```bash
30
235
  ruby -e 'require "muchkeys"; puts MuchKeys.fetch_key("mail_server")'
31
236
  # => smtp.example.com (from consul)
32
237
  ```
33
238
 
34
- ```
239
+ ```bash
35
240
  mail_server=muffin.ninja.local ruby -e 'require "muchkeys"; puts MuchKeys.fetch_key("mail_server")'
36
241
  # => muffin.ninja.local (from ENV)
37
242
  ```
38
243
 
39
- Use the above in your YAMLs, your configs and your ERBs for maximum fun time.
244
+ ### Keys to Store
245
+
246
+ You will probably just want to store things that change between environments in consul.
247
+ If `database_pool_size` never changes between development and production then don't store it in consul.
248
+ If `sidekiq_number_of_workers` is 1 in development, 2 in staging and 16 in production, then this is a
249
+ good thing to store in consul because you'll have a central place that all app servers can
250
+ read from.
251
+
252
+ You could store things in rails' `environments/` path but then you'll need to do a deploy
253
+ to change a setting and some things might be shared between apps like URLs, api keys, secrets and scaling settings.
254
+
255
+
256
+ ### Limits and Caveats
257
+
258
+ Searching multiple paths in consul is because consul is hierarchical (or can be) and ENV is not.
259
+ Making an easy to use API that looks and behaves like `ENV` was one of the goals to make
260
+ it more familiar to developers. So worst case, by default, MuchKeys will search consul 4 times
261
+ to find a key.
262
+
263
+ Encrypted secrets can be slightly more clunky. Developers may not have signing keys,
264
+ in that case, an admin or someone from ops maybe has to manage the secrets.
265
+
266
+
267
+ ### Compared to Other Tools
268
+
269
+ It's sort of the opposite of `envconsul` or similar to `dotenv` designed
270
+ for use in production.
271
+
272
+ * Dotenv doesn't centralize keys.
273
+ * envconsul has a auto restart the launched process on change feature, this project does not.
274
+
275
+
276
+ ### Future
277
+
278
+ It'd be nice if this project wasn't so tied to consul. I don't think it's impossible to decouple it. Etcd basically behaves the same afaik so we could add an etcd adapter.
40
279
 
41
280
 
42
281
  ## License
43
282
 
44
283
  The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
45
-
data/Rakefile CHANGED
@@ -1,6 +1,21 @@
1
1
  require "bundler/gem_tasks"
2
2
  require "rspec/core/rake_task"
3
3
 
4
- RSpec::Core::RakeTask.new(:spec)
4
+ namespace :spec do
5
+ desc "Run Unit Tests"
6
+ RSpec::Core::RakeTask.new(:unit) do |t|
7
+ t.pattern = 'spec/lib/**/*_spec.rb'
8
+ end
5
9
 
6
- task :default => :spec
10
+ desc "Run Feature/Integration Tests"
11
+ RSpec::Core::RakeTask.new(:features) do |t|
12
+ t.pattern = 'spec/features/**/*.rb'
13
+ end
14
+
15
+ desc "Run All Tests"
16
+ RSpec::Core::RakeTask.new(:all) do |t|
17
+ t.pattern = 'spec/lib/**/*_spec.rb, spec/features/**/*.rb'
18
+ end
19
+ end
20
+
21
+ task :default => "spec:all"
data/exe/muchkeys ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "muchkeys"
4
+
5
+ cli = MuchKeys::CLI.new ARGV
6
+ cli.run
@@ -0,0 +1,41 @@
1
+ require_relative "../errors"
2
+
3
+ class MuchKeys::CLI::Validator
4
+
5
+ def self.validate_primary_mode_option(options)
6
+ raise MuchKeys::CLIOptionsError, primary_mode_error_message unless options_has_one_mode?(options)
7
+ end
8
+
9
+ def self.validate_encrypt(options)
10
+ abort "--encrypt needs the --file option passed." if !options[:file]
11
+ abort "--encrypt needs the --public_key option passed." if !options[:public_key]
12
+ end
13
+
14
+ def self.validate_decrypt(options)
15
+ key_name = options[:consul_key]
16
+ abort "--decrypt needs the --consul_key option passed." if !key_name
17
+
18
+ if !secret_adapter.auto_certificates_exist_for_key?(key_name)
19
+ certfile_expected = secret_adapter.certfile_name(key_name)
20
+ abort "--decrypt needs the --public_key option passed or a PEM file needs to be at #{certfile_expected}." if !options[:public_key]
21
+ abort "--decrypt needs the --private_key option passed or a PEM file needs to be at #{certfile_expected}." if !options[:private_key]
22
+ end
23
+ end
24
+
25
+ def self.validate_plain(options)
26
+ abort "--plain needs the --consul_key option passed." if !options[:consul_key]
27
+ end
28
+
29
+ def self.options_has_one_mode?(options)
30
+ [ options[:encrypt], options[:decrypt], options[:plain] ].count(true) == 1
31
+ end
32
+
33
+ def self.primary_mode_error_message
34
+ "You must pass only one and at least one of the following flags: --encrypt, --decrypt, or --plain"
35
+ end
36
+
37
+ def self.secret_adapter
38
+ MuchKeys::Secret
39
+ end
40
+
41
+ end
@@ -0,0 +1,103 @@
1
+ require "slop"
2
+
3
+ module MuchKeys
4
+ class CLI
5
+ attr_reader :mode
6
+
7
+ def initialize(arguments)
8
+ # I'd like to not be doing this kind of check
9
+ # But this is tricky because we need to know
10
+ # later if help was invoked so we don't execute run with blank options.
11
+ @help_invoked = false
12
+ parsed = parse_options(arguments)
13
+ return if @help_invoked
14
+
15
+ begin
16
+ set_primary_mode
17
+ rescue MuchKeys::CLIOptionsError => e
18
+ puts e.message
19
+ puts @opts
20
+ end
21
+
22
+ configure_muchkeys
23
+ end
24
+
25
+ def parse_options(arguments)
26
+ @opts = Slop.parse arguments do |o|
27
+ o.bool "-e", "--encrypt", "Encrypt keys from a file to put in consul."
28
+ o.bool "-d", "--decrypt", "Decrypt keys from consul."
29
+ o.bool "-p", "--plain", "Fetch plaintext key from consul."
30
+
31
+ o.string "--consul_url", "Consul server http address", default: "http://localhost:8500"
32
+ o.string "--private_key", "Location of your private key"
33
+ o.string "--public_key", "Location of your public key"
34
+
35
+ # add -f
36
+ o.string "--file", "File to encrypt"
37
+ o.string "-c", "--consul_key", "Consul key to decrypt"
38
+
39
+ o.on "-h", "--help" do
40
+ # Save the fact we ran help so when run() runs, it prints help and exits.
41
+ @help_invoked = true
42
+ end
43
+ end
44
+ end
45
+
46
+ def set_primary_mode(options=@opts)
47
+ MuchKeys::CLI::Validator.validate_primary_mode_option(options)
48
+ @mode = select_primary_mode(options)
49
+ end
50
+
51
+ def configure_muchkeys(options=@opts)
52
+ MuchKeys.configure do |config|
53
+ config.consul_url = options[:consul_url]
54
+ config.public_key = options[:public_key]
55
+ config.private_key = options[:private_key]
56
+ end
57
+ end
58
+
59
+ # this guy figures out the appropriate action to take given CLI options
60
+ def run
61
+ if @opts[:help]
62
+ puts @opts
63
+ return
64
+ end
65
+
66
+ if @opts[:encrypt]
67
+ MuchKeys::CLI::Validator.validate_encrypt(file: @opts[:file], public_key: @opts[:public_key])
68
+ encrypt(@opts[:file], @opts[:public_key])
69
+ elsif @opts[:decrypt]
70
+ MuchKeys::CLI::Validator.validate_decrypt(consul_key: @opts[:consul_key], public_key: @opts[:public_key], private_key: @opts[:private_key])
71
+ decrypt(@opts[:consul_key], @opts[:public_key], @opts[:private_key])
72
+ elsif @opts[:plain]
73
+ plain(@opts[:consul_key])
74
+ end
75
+ end
76
+
77
+ def encrypt(file, public_key)
78
+ string_to_encrypt = File.read(file)
79
+ puts secret_adapter.encrypt_string(string_to_encrypt, public_key)
80
+ end
81
+
82
+ def decrypt(consul_key, public_key, private_key)
83
+ puts MuchKeys.fetch_key(consul_key, public_key:public_key, private_key:private_key)
84
+ end
85
+
86
+ def plain(consul_key)
87
+ puts MuchKeys.fetch_key(consul_key)
88
+ end
89
+
90
+
91
+ private
92
+ def select_primary_mode(options)
93
+ possible_primary_modes = { encrypt: options[:encrypt], decrypt: options[:decrypt], plain: options[:plain] }
94
+ primary_mode = possible_primary_modes.select {|k,v| v == true } # validation has already happened, we can access modes safely
95
+ primary_mode.keys.first
96
+ end
97
+
98
+ def secret_adapter
99
+ MuchKeys::Secret
100
+ end
101
+
102
+ end
103
+ end
@@ -0,0 +1,30 @@
1
+ require 'muchkeys'
2
+ require 'uri'
3
+
4
+ module MuchKeys
5
+ class Configuration
6
+ attr_accessor :consul_url, :private_key, :public_key, :application_name, :search_paths, :secrets_hint
7
+
8
+ # sensible defaults
9
+ def initialize
10
+ @consul_url = "http://localhost:8500"
11
+ end
12
+
13
+ # url parsing sanity check
14
+ def consul_url=(url)
15
+ raise URI::InvalidURIError unless url =~ URI::regexp
16
+ @consul_url = url
17
+ end
18
+
19
+ def attributes
20
+ {
21
+ consul_url: @consul_url,
22
+ private_key: @private_key,
23
+ public_key: @public_key,
24
+ application_name: @application_name,
25
+ search_paths: @search_paths,
26
+ secrets_hint: @secrets_hint
27
+ }.delete_if {|k,v| v.nil? }
28
+ end
29
+ end
30
+ end
@@ -1,3 +1,5 @@
1
1
  module MuchKeys
2
- class InvalidKey < StandardError; end
2
+ InvalidKey = Class.new(StandardError)
3
+ CLIOptionsError = Class.new(StandardError)
4
+ NoKeysSet = Class.new(StandardError)
3
5
  end
@@ -0,0 +1,43 @@
1
+ module MuchKeys
2
+ class KeyValidator
3
+
4
+ class << self
5
+
6
+ # key should pass validation rules
7
+ def valid? keyname
8
+ exists?(keyname) &&
9
+ secret_key_has_namespace?(keyname)
10
+ end
11
+
12
+ def secret_key_namespace keyname
13
+ match = keyname.match(/^secrets\/(.*?)\/.*/)
14
+ if match
15
+ match[1]
16
+ else
17
+ ""
18
+ end
19
+ end
20
+
21
+ def secret_key_has_namespace? keyname
22
+ if is_secret?(keyname)
23
+ namespace = secret_key_namespace(keyname)
24
+ exists?(namespace)
25
+ else
26
+ # a plain key passes, it doesn't need a namespace
27
+ true
28
+ end
29
+ end
30
+
31
+ def is_secret? keyname
32
+ keyname.match(/^secret/) != nil
33
+ end
34
+
35
+
36
+ private
37
+ def exists? keyname
38
+ !keyname.nil? && !keyname.empty?
39
+ end
40
+
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,66 @@
1
+ require 'openssl'
2
+ require 'net/http'
3
+ require 'muchkeys/configuration'
4
+ require 'muchkeys/errors'
5
+
6
+
7
+ class MuchKeys::Secret
8
+ CIPHER_SUITE = "AES-256-CFB"
9
+
10
+ class << self
11
+
12
+ # the path that clues MuchKeys that this path contains secrets
13
+ def secrets_path_hint
14
+ MuchKeys.configuration.secrets_hint || "secrets/"
15
+ end
16
+
17
+ def encrypt_string(val, public_key)
18
+ cipher = OpenSSL::Cipher.new CIPHER_SUITE
19
+ cert = OpenSSL::X509::Certificate.new File.read(public_key)
20
+ OpenSSL::PKCS7::encrypt([cert], val, cipher, OpenSSL::PKCS7::BINARY)
21
+ end
22
+
23
+ # turn a key_name into a SSL cert file name by convention
24
+ def certfile_name(key_name)
25
+ key_parts = key_name.match /(.*)\/#{secrets_path_hint}(.*)/
26
+ raise MuchKeys::InvalidKey, "#{key_name} doesn't look like a secret" if key_parts.nil?
27
+ key_base = key_parts[1].gsub(/^git\//, "")
28
+ MuchKeys.configuration.public_key || "#{ENV['HOME']}/.keys/#{key_base}.pem"
29
+ end
30
+
31
+ def is_secret?(key_name)
32
+ key_name.match(/\/#{secrets_path_hint}/) != nil
33
+ end
34
+
35
+ def auto_certificates_exist_for_key?(key)
36
+ file_exists?(secret_adapter.certfile_name(key))
37
+ end
38
+
39
+ def decrypt_string(val, public_key, private_key)
40
+ cert = OpenSSL::X509::Certificate.new(read_ssl_key(public_key))
41
+ key = OpenSSL::PKey::RSA.new(read_ssl_key(private_key))
42
+ OpenSSL::PKCS7.new(val).decrypt(key, cert)
43
+ end
44
+
45
+
46
+ private
47
+
48
+ def read_ssl_key(file_name)
49
+ File.read file_name
50
+ end
51
+
52
+ # Why would we even do this? For stubbing.
53
+ def file_exists?(path)
54
+ File.exist? path
55
+ end
56
+
57
+ def key_validator
58
+ MuchKeys::KeyValidator
59
+ end
60
+
61
+ def secret_adapter
62
+ MuchKeys::Secret
63
+ end
64
+
65
+ end
66
+ end
@@ -1,3 +1,3 @@
1
1
  module MuchKeys
2
- VERSION = "0.0.1"
2
+ VERSION = "0.3.3"
3
3
  end
data/lib/muchkeys.rb CHANGED
@@ -1,59 +1,171 @@
1
1
  require "muchkeys/version"
2
2
  require "muchkeys/errors"
3
+ require "muchkeys/configuration"
4
+ require "muchkeys/secret"
5
+ require "muchkeys/key_validator"
6
+ require "muchkeys/cli"
7
+ require "muchkeys/cli/validator"
8
+
3
9
  require "net/http"
4
10
 
5
11
  module MuchKeys
6
12
 
7
13
  class << self
8
14
  attr_accessor :configuration
9
- end
10
15
 
11
- def self.configure
12
- @configuration ||= Configuration.new
13
- yield(configuration)
14
- end
16
+ def configure
17
+ self.configuration ||= MuchKeys::Configuration.new
18
+ if block_given?
19
+ yield configuration
20
+ end
21
+ end
15
22
 
16
- def self.default_configure
17
- configure do
23
+ def search_order(application_name, key_name)
24
+ if MuchKeys.configuration.search_paths
25
+ search_paths = MuchKeys.configuration.search_paths.collect do |path|
26
+ "#{path}/#{key_name}"
27
+ end
28
+ else
29
+ search_paths = [
30
+ "git/#{application_name}/secrets/#{key_name}",
31
+ "git/#{application_name}/config/#{key_name}",
32
+ "git/shared/secrets/#{key_name}",
33
+ "git/shared/config/#{key_name}"
34
+ ]
35
+ end
18
36
  end
19
- end
20
37
 
21
- class Configuration
22
- attr_accessor :consul_host
38
+ def find_first(key_name)
39
+ return ENV[key_name] unless ENV[key_name].nil?
40
+
41
+ unless MuchKeys.configuration.search_paths
42
+ application_name = detect_app_name
43
+ return false if !application_name
44
+ end
23
45
 
24
- # sensible defaults
25
- def initialize
26
- @consul_host = 'http://localhost:8500'
46
+ consul_paths = search_order(application_name, key_name)
47
+
48
+ response = nil
49
+ search_order(application_name, key_name).detect do |consul_path|
50
+ response = fetch_key(consul_path)
51
+ end
52
+
53
+ if !response
54
+ raise MuchKeys::NoKeysSet, "Bailing. Consul isn't set with any keys for #{key_name}."
55
+ end
56
+
57
+ response
27
58
  end
28
- end
29
59
 
30
- def self.fetch_key(key_name)
31
- default_configure if !configuration
32
- return ENV[key_name] unless ENV[key_name].nil?
60
+ def fetch_key(key_name, public_key:nil, private_key:nil)
61
+ return ENV[key_name] unless ENV[key_name].nil?
33
62
 
34
- response = Net::HTTP.get_response(URI(consul_url(key_name)))
35
- handle_response(response)
36
- fetch_body(response)
37
- end
63
+ if secret_adapter.is_secret?(key_name)
64
+ raise InvalidKey unless validate_key_name(key_name)
38
65
 
39
- def self.consul_url(key_name)
40
- converted_key_name = convert_keyname(key_name)
41
- "#{configuration.consul_host}/v1/kv/#{converted_key_name}?raw"
42
- end
66
+ # configure automatic certificates
67
+ public_key = find_certfile_for_key(key_name) if public_key.nil?
68
+ private_key = find_certfile_for_key(key_name) if private_key.nil?
43
69
 
44
- def self.fetch_body(response)
45
- response.body
46
- end
70
+ response = fetch_secret_key(key_name, public_key, private_key)
71
+ else
72
+ response = fetch_plain_key(key_name)
73
+ end
74
+
75
+ # empty and unset keys from consul are empty strings, so return false
76
+ # here TODO: UnsetKey would be better here.
77
+ return false if response == ""
47
78
 
48
- def self.handle_response(response_code)
49
- case response_code
50
- when (400..599)
51
- raise MuchKeys::InvalidKey
79
+ # otherwise, we got consul data so return that
80
+ response
52
81
  end
82
+
83
+ def consul_url(key_name)
84
+ URI("#{configuration.consul_url}/v1/kv/#{key_name}?raw")
85
+ end
86
+
87
+ # TODO: inline this
88
+ def fetch_body(response)
89
+ response.body
90
+ end
91
+
92
+ def handle_response(response_code)
93
+ case response_code
94
+ when (400..599)
95
+ raise MuchKeys::InvalidKey
96
+ end
97
+ end
98
+
99
+
100
+ private
101
+ def fetch_plain_key(key_name)
102
+ url = consul_url(key_name)
103
+ response = Net::HTTP.get_response url
104
+ handle_response(response)
105
+ fetch_body(response)
106
+ end
107
+
108
+ def fetch_secret_key(key_name, public_pem=nil, private_pem=nil)
109
+ result = fetch_plain_key(key_name)
110
+ # FIXME: omg use a class like MuchKeys::UnsetKey instead of "" as a
111
+ # return value -- there are other places like this too.
112
+
113
+ # we hit a key that doesn't exist, so don't try to decrypt it
114
+ return "" if !result || result.empty?
115
+ secret_adapter.decrypt_string(result, public_pem, private_pem)
116
+ end
117
+
118
+ def find_certfile_for_key(key_name)
119
+ secret_adapter.certfile_name key_name
120
+ end
121
+
122
+ def secret_adapter
123
+ MuchKeys::Secret
124
+ end
125
+
126
+ def validate_key_name(key_name)
127
+ MuchKeys::KeyValidator.valid? key_name
128
+ end
129
+
130
+ # Detecting Rails app names is a known quantity.
131
+ # TODO: figure out how to detect plain Rack apps. :(
132
+ def detect_app_name
133
+ return configuration.application_name if configuration.application_name
134
+
135
+ # Rails.application is "Monorail::Application".
136
+ if defined?(Rails)
137
+ application_name = Rails.application.class.to_s.split("::").first
138
+ elsif defined?(Rack)
139
+ application_name = "Asset_Server"
140
+ else
141
+ $stderr.puts "can't detect app name"
142
+ return
143
+ end
144
+
145
+ snakecase(application_name)
146
+ end
147
+
148
+ # MyApp should become my_app
149
+ def snakecase(string)
150
+ string.gsub(/::/, '/').
151
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
152
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
153
+ tr("-", "_").
154
+ downcase
155
+ end
156
+
53
157
  end
158
+ end
159
+
160
+ # default configure the gem on gem load
161
+ MuchKeys.configuration ||= MuchKeys::Configuration.new
162
+
163
+
164
+ # This interface looks like ENV which is more friendly to devs
165
+ class MUCHKEYS
54
166
 
55
- def self.convert_keyname(key_name)
56
- key_name.gsub('___', '/')
167
+ def self.[](i)
168
+ MuchKeys.find_first(i)
57
169
  end
58
170
 
59
171
  end
data/muchkeys.gemspec CHANGED
@@ -9,8 +9,8 @@ Gem::Specification.new do |spec|
9
9
  spec.authors = ["Pat O'Brien", "Chris Dillon"]
10
10
  spec.email = ["pobrien@goldstar.com", "cdillon@goldstar.com"]
11
11
 
12
- spec.summary = %q{MuchKeys first fetches keys from the ENV and then falls back to consul}
13
- spec.description = %q{Much keys, such values ...}
12
+ spec.summary = %q{MuchKeys fetches keys from the ENV and then falls back to consul}
13
+ spec.description = %q{MuchKeys can handle app configuration and appsecrets}
14
14
  spec.homepage = "https://www.goldstar.com"
15
15
  spec.license = "MIT"
16
16
 
@@ -19,11 +19,16 @@ Gem::Specification.new do |spec|
19
19
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
20
  spec.require_paths = ["lib"]
21
21
 
22
+ # slop 4 was a rewrite and it breaks a few gems:
23
+ # guard / pry. This is sad for development but slop4 is better than slop3.
24
+ spec.add_runtime_dependency "slop", "~> 4.2"
25
+
22
26
  spec.add_development_dependency "bundler", "~> 1.10"
23
27
  spec.add_development_dependency "rake", "~> 10.0"
24
- spec.add_development_dependency "rspec"
25
- spec.add_development_dependency "pry"
26
- spec.add_development_dependency "guard"
27
- spec.add_development_dependency "guard-rspec"
28
- spec.add_development_dependency "fsevents"
28
+ spec.add_development_dependency "rspec", "~> 3.3"
29
+ spec.add_development_dependency "webmock", "~> 1.20"
30
+ spec.add_development_dependency "vcr", "~> 2.9"
31
+
32
+ # slop 4 really, really needs this version of pry since they vendored it
33
+ spec.add_development_dependency "pry", '=0.10.3'
29
34
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: muchkeys
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.3.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Pat O'Brien
@@ -9,111 +9,112 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2015-07-27 00:00:00.000000000 Z
12
+ date: 2016-04-28 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
- name: bundler
15
+ name: slop
16
16
  requirement: !ruby/object:Gem::Requirement
17
17
  requirements:
18
18
  - - "~>"
19
19
  - !ruby/object:Gem::Version
20
- version: '1.10'
21
- type: :development
20
+ version: '4.2'
21
+ type: :runtime
22
22
  prerelease: false
23
23
  version_requirements: !ruby/object:Gem::Requirement
24
24
  requirements:
25
25
  - - "~>"
26
26
  - !ruby/object:Gem::Version
27
- version: '1.10'
27
+ version: '4.2'
28
28
  - !ruby/object:Gem::Dependency
29
- name: rake
29
+ name: bundler
30
30
  requirement: !ruby/object:Gem::Requirement
31
31
  requirements:
32
32
  - - "~>"
33
33
  - !ruby/object:Gem::Version
34
- version: '10.0'
34
+ version: '1.10'
35
35
  type: :development
36
36
  prerelease: false
37
37
  version_requirements: !ruby/object:Gem::Requirement
38
38
  requirements:
39
39
  - - "~>"
40
40
  - !ruby/object:Gem::Version
41
- version: '10.0'
41
+ version: '1.10'
42
42
  - !ruby/object:Gem::Dependency
43
- name: rspec
43
+ name: rake
44
44
  requirement: !ruby/object:Gem::Requirement
45
45
  requirements:
46
- - - ">="
46
+ - - "~>"
47
47
  - !ruby/object:Gem::Version
48
- version: '0'
48
+ version: '10.0'
49
49
  type: :development
50
50
  prerelease: false
51
51
  version_requirements: !ruby/object:Gem::Requirement
52
52
  requirements:
53
- - - ">="
53
+ - - "~>"
54
54
  - !ruby/object:Gem::Version
55
- version: '0'
55
+ version: '10.0'
56
56
  - !ruby/object:Gem::Dependency
57
- name: pry
57
+ name: rspec
58
58
  requirement: !ruby/object:Gem::Requirement
59
59
  requirements:
60
- - - ">="
60
+ - - "~>"
61
61
  - !ruby/object:Gem::Version
62
- version: '0'
62
+ version: '3.3'
63
63
  type: :development
64
64
  prerelease: false
65
65
  version_requirements: !ruby/object:Gem::Requirement
66
66
  requirements:
67
- - - ">="
67
+ - - "~>"
68
68
  - !ruby/object:Gem::Version
69
- version: '0'
69
+ version: '3.3'
70
70
  - !ruby/object:Gem::Dependency
71
- name: guard
71
+ name: webmock
72
72
  requirement: !ruby/object:Gem::Requirement
73
73
  requirements:
74
- - - ">="
74
+ - - "~>"
75
75
  - !ruby/object:Gem::Version
76
- version: '0'
76
+ version: '1.20'
77
77
  type: :development
78
78
  prerelease: false
79
79
  version_requirements: !ruby/object:Gem::Requirement
80
80
  requirements:
81
- - - ">="
81
+ - - "~>"
82
82
  - !ruby/object:Gem::Version
83
- version: '0'
83
+ version: '1.20'
84
84
  - !ruby/object:Gem::Dependency
85
- name: guard-rspec
85
+ name: vcr
86
86
  requirement: !ruby/object:Gem::Requirement
87
87
  requirements:
88
- - - ">="
88
+ - - "~>"
89
89
  - !ruby/object:Gem::Version
90
- version: '0'
90
+ version: '2.9'
91
91
  type: :development
92
92
  prerelease: false
93
93
  version_requirements: !ruby/object:Gem::Requirement
94
94
  requirements:
95
- - - ">="
95
+ - - "~>"
96
96
  - !ruby/object:Gem::Version
97
- version: '0'
97
+ version: '2.9'
98
98
  - !ruby/object:Gem::Dependency
99
- name: fsevents
99
+ name: pry
100
100
  requirement: !ruby/object:Gem::Requirement
101
101
  requirements:
102
- - - ">="
102
+ - - '='
103
103
  - !ruby/object:Gem::Version
104
- version: '0'
104
+ version: 0.10.3
105
105
  type: :development
106
106
  prerelease: false
107
107
  version_requirements: !ruby/object:Gem::Requirement
108
108
  requirements:
109
- - - ">="
109
+ - - '='
110
110
  - !ruby/object:Gem::Version
111
- version: '0'
112
- description: Much keys, such values ...
111
+ version: 0.10.3
112
+ description: MuchKeys can handle app configuration and appsecrets
113
113
  email:
114
114
  - pobrien@goldstar.com
115
115
  - cdillon@goldstar.com
116
- executables: []
116
+ executables:
117
+ - muchkeys
117
118
  extensions: []
118
119
  extra_rdoc_files: []
119
120
  files:
@@ -125,10 +126,14 @@ files:
125
126
  - LICENSE.txt
126
127
  - README.md
127
128
  - Rakefile
128
- - bin/console
129
- - bin/setup
129
+ - exe/muchkeys
130
130
  - lib/muchkeys.rb
131
+ - lib/muchkeys/cli.rb
132
+ - lib/muchkeys/cli/validator.rb
133
+ - lib/muchkeys/configuration.rb
131
134
  - lib/muchkeys/errors.rb
135
+ - lib/muchkeys/key_validator.rb
136
+ - lib/muchkeys/secret.rb
132
137
  - lib/muchkeys/version.rb
133
138
  - muchkeys.gemspec
134
139
  homepage: https://www.goldstar.com
@@ -151,8 +156,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
151
156
  version: '0'
152
157
  requirements: []
153
158
  rubyforge_project:
154
- rubygems_version: 2.2.3
159
+ rubygems_version: 2.4.5.1
155
160
  signing_key:
156
161
  specification_version: 4
157
- summary: MuchKeys first fetches keys from the ENV and then falls back to consul
162
+ summary: MuchKeys fetches keys from the ENV and then falls back to consul
158
163
  test_files: []
164
+ has_rdoc:
data/bin/console DELETED
@@ -1,14 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- require "bundler/setup"
4
- require "muchkeys"
5
-
6
- # You can add fixtures and/or initialization code here to make experimenting
7
- # with your gem easier. You can also use a different console, if you like.
8
-
9
- # (If you use this, don't forget to add pry to your Gemfile!)
10
- # require "pry"
11
- # Pry.start
12
-
13
- require "irb"
14
- IRB.start
data/bin/setup DELETED
@@ -1,7 +0,0 @@
1
- #!/bin/bash
2
- set -euo pipefail
3
- IFS=$'\n\t'
4
-
5
- bundle install
6
-
7
- # Do any other automated setup that you need to do here