muchkeys 0.0.1 → 0.3.3

Sign up to get free protection for your applications and to get access to all the features.
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