secret_config 0.4.5 → 0.5.0

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
  SHA256:
3
- metadata.gz: 3c5b1779481318edb20fa0b59bc560cf55d890119fd901e7b59339f75232ae2f
4
- data.tar.gz: 19bb4a25a9b9b8037f84f85e183d5be9d853be350132987cd6d2e577782c6e79
3
+ metadata.gz: 515036f2cebc7abd6211448b44f80c99c0872065a7947afaf4dface54c84d808
4
+ data.tar.gz: 2a5c7a8ccfd5d7d91d459a13d8cd5fcce8b1b381e1baa3e83946d99d712cca42
5
5
  SHA512:
6
- metadata.gz: 8b7a542adb565a32e1a8a692a13f560745c7bf29f195ccbe1a681ab70526343b76fa7231a146c4b784df7acb2e429bb5b0a4b1c7f43f06740540c12b0fdee0ac
7
- data.tar.gz: 751e31fce21e043d98208b299b3239b3bedd02e96e09aef6fe7f416a809f2b4236a4e26458a1b8a49ffe0460c1533f6b36bb25068d02aafc9cff47445977cfb9
6
+ metadata.gz: 73c14c00b3b1759b73d04415bb20724775efef6084729907d9377cb4928b513ed324b2edc67cd5953a6e2a8e71969b4add609d74b94ae2fd2467adfa5b0a48b5
7
+ data.tar.gz: 5dfb6fd8832349e7a63b8d6f02023c54cc33538d82e742b3bf24246252f1e0500fa948a9b835ca8ac054dc3c7a6d69b4090448938c782dc332640d361fa8997f
data/README.md CHANGED
@@ -1,21 +1,19 @@
1
1
  # Secret Config
2
- [![Gem Version](https://img.shields.io/gem/v/secret_config.svg)](https://rubygems.org/gems/secret_config) [![Build Status](https://travis-ci.org/rocketjob/secret_config.svg?branch=master)](https://travis-ci.org/rocketjob/secret_config) [![License](https://img.shields.io/badge/license-Apache%202.0-brightgreen.svg)](http://opensource.org/licenses/Apache-2.0) ![](https://img.shields.io/badge/status-Beta-yellow.svg) [![Gitter chat](https://img.shields.io/badge/IRC%20(gitter)-Support-brightgreen.svg)](https://gitter.im/rocketjob/support)
2
+ [![Gem Version](https://img.shields.io/gem/v/secret_config.svg)](https://rubygems.org/gems/secret_config) [![Build Status](https://travis-ci.org/rocketjob/secret_config.svg?branch=master)](https://travis-ci.org/rocketjob/secret_config) [![License](https://img.shields.io/badge/license-Apache%202.0-brightgreen.svg)](http://opensource.org/licenses/Apache-2.0) ![](https://img.shields.io/badge/status-Production%20Ready-blue.svg) [![Gitter chat](https://img.shields.io/badge/IRC%20(gitter)-Support-brightgreen.svg)](https://gitter.im/rocketjob/support)
3
3
 
4
4
  Centralized Configuration and Secrets Management for Ruby and Rails applications.
5
5
 
6
- Securely store configuration information centrally.
7
-
8
- ## Project Status
9
-
10
- Early development.
6
+ Securely store configuration information centrally, supporting multiple tenants of the same application.
11
7
 
12
8
  ## Features
13
9
 
14
10
  Supports storing configuration information in:
15
11
  * File
16
12
  * Development and testing use.
13
+ * Environment Variables
14
+ * Environment Variables take precedence and can be used to override any setting.
17
15
  * AWS System Manager Parameter Store
18
- * Encrypt and store secrets such as passwords centrally.
16
+ * Encrypt and securely store secrets such as passwords centrally.
19
17
 
20
18
  ## Benefits
21
19
 
@@ -27,13 +25,156 @@ Benefits of moving sensitive configuration information into AWS System Manager P
27
25
  * In a large application the number of secrets can grow dramatically.
28
26
  * Removes the need to encrypt sensitive data config files.
29
27
  * Including securing and managing encryption keys.
30
- * When encryption keys change, such as during a key rotation, config files don;t have to be changed.
28
+ * When encryption keys change, such as during a key rotation, config files don't have to be changed.
31
29
  * Removes security concerns with placing passwords in the clear into environment variables.
32
30
  * AWS System Manager Parameter Store does not charge for parameters.
33
31
  * Still recommend using a custom KMS key that charges only $1 per month.
34
32
  * Amounts as of 4/2019. Confirm what AWS charges you for these services.
35
- * AWS Secrets Manager charges for every secret being managed, which can accumulate quickly with large projects.
33
+ * AWS Secrets Manager charges for every secret being managed, which can accumulate quickly with large projects.
34
+ * Configure multiple distinct application instances to support multiple tenants.
35
+ * For example, use separate databases with unique credentials for each tenant.
36
+ * Separation of responsibilities is achieved since operations can manage production configuration.
37
+ * Developers do not need to be involved with production configuration such as host names and passwords.
38
+ * All values are encrypted by default when stored in the AWS Parameter Store.
39
+ * Prevents accidentally not encrypting sensitive data.
40
+
41
+ ## Introduction
42
+
43
+ When Secret Config starts up it reads all configuration entries into memory for all keys under the configured path.
44
+ This means that once Secret Config has initialized all calls to Secret Config are extremely fast.
45
+
46
+ The in-memory copy of the registry can be refreshed at any time by calling `SecretConfig.refresh!`. It can be refreshed
47
+ via a process signal, or by calling it through an event, or via a messaging system.
48
+
49
+ It is suggested that any programmatic lookup to values stored in Secret Config are called every time a value is
50
+ being used, rather than creating a local copy of the value. This ensures that a refresh of the registry will take effect
51
+ immediately for any code reading from Secret Config.
52
+
53
+ ## API
54
+
55
+ When Secret Config starts up it reads all configuration entries immediately for all keys under the configured path.
56
+ This means that once Secret Config has initialized all calls to Secret Config are extremely fast.
57
+
58
+ Secret Config supports the following programmatic interface:
59
+
60
+ ### Read values
61
+
62
+ Fetch the value for the supplied key, returning nil if not found:
63
+
64
+ ~~~ruby
65
+ # Key is present:
66
+ SecretConfig["logger/level"]
67
+ # => "info"
68
+
69
+ # Key is missing:
70
+ SecretConfig["logger/blah"]
71
+ # => nil
72
+ ~~~
73
+
74
+ Fetch the value for the supplied key, raising `SecretConfig::MissingMandatoryKey` if not found:
75
+
76
+ ~~~ruby
77
+ # Key is present:
78
+ SecretConfig.fetch("logger/level")
79
+ # => "info"
80
+
81
+ # Key is missing:
82
+ SecretConfig.fetch("logger/blah")
83
+ # => SecretConfig::MissingMandatoryKey (Missing configuration value for /development/logger/blah)
84
+ ~~~
85
+
86
+ A default value can be supplied when the key is not found in the registry:
87
+
88
+ ~~~ruby
89
+ SecretConfig.fetch("logger/level", default: "info")
90
+ # => "info"
91
+ ~~~
92
+
93
+ Since AWS SSM Parameter store and environment variables only support string values,
94
+ it is neccessary to convert the string back to the type required by the program.
95
+
96
+ The following types are supported:
97
+ `:integer`
98
+ `:float`
99
+ `:string`
100
+ `:boolean`
101
+ `:symbol`
102
+
103
+ ~~~ruby
104
+ # Without type conversion:
105
+ SecretConfig.fetch("symmetric_encryption/version")
106
+ # => "0"
107
+
108
+ # With type conversion:
109
+ SecretConfig.fetch("symmetric_encryption/version", type: :integer)
110
+ # => 0
111
+ ~~~
112
+
113
+ When storing binary data, it should be encoded with strict base64 encoding. To automatically convert it back to binary
114
+ specify the encoding as `:base64`
115
+
116
+ ~~~ruby
117
+ # Return a value that was stored in Base64 encoding format:
118
+ SecretConfig.fetch("symmetric_encryption/iv")
119
+ # => "FW+/wLubAYM+ZU0bWQj59Q=="
36
120
 
121
+ # Base64 decode a value that was stored in Base64 encoding format:
122
+ SecretConfig.fetch("symmetric_encryption/iv", encoding: :base64)
123
+ # => "\x15o\xBF\xC0\xBB\x9B\x01\x83>eM\eY\b\xF9\xF5"
124
+ ~~~
125
+
126
+ ### Key presence
127
+
128
+ Returns whether a key is present in the registry:
129
+
130
+ ~~~ruby
131
+ SecretConfig.key?("logger/level")
132
+ # => true
133
+ ~~~
134
+
135
+ ### Write values
136
+
137
+ When Secret Config is configured to use the AWS SSM Parameter store, its values can be modified:
138
+
139
+ ~~~ruby
140
+ SecretConfig["logger/level"] = "debug"
141
+ ~~~
142
+
143
+ ~~~ruby
144
+ SecretConfig.set("logger/level", "debug")
145
+ ~~~
146
+
147
+ ### Configuration
148
+
149
+ Returns a Hash copy of the configuration as a tree:
150
+
151
+ ~~~ruby
152
+ SecretConfig.configuration
153
+ ~~~
154
+
155
+ ### Refresh Configuration
156
+
157
+ Tell Secret Config to refresh its in-memory copy of the configuration settings.
158
+
159
+ ~~~ruby
160
+ SecretConfig.refresh!
161
+ ~~~
162
+
163
+ Example, refresh the registry any time a SIGUSR2 is raised, add the following code on startup:
164
+
165
+ ~~~ruby
166
+ Signal.trap('USR2') do
167
+ SecretConfig.refresh!
168
+ end
169
+ ~~~
170
+
171
+ Then to make the process refresh it registry:
172
+ ~~~shell
173
+ kill -SIGUSR2 1234
174
+ ~~~
175
+
176
+ Where `1234` above is the process PID.
177
+
37
178
  ## Development and Test use
38
179
 
39
180
  In the development environment create the file `config/application.yml` within which to store local development credentials.
@@ -173,7 +314,7 @@ For example, somewhere in your codebase you need a persistent http connection:
173
314
  ~~~
174
315
 
175
316
  Then the application that uses the above library / gem just needs to add the relevant entries to their
176
- `application.rb` file:
317
+ `application.yml` file:
177
318
 
178
319
  ~~~yaml
179
320
  http_client:
@@ -199,18 +340,39 @@ as covered above. By default it will use env var `RAILS_ENV` to define the path
199
340
 
200
341
  The default settings are great for getting started in development and test, but should not be used in production.
201
342
 
202
- Add the setting to `config/environments/production.rb` to make it fetch its settings from
203
- AWS System Manager Parameter Store:
343
+ To ensure Secret Config is configured and available for use within any of the config files, add
344
+ the following lines to the very top of `application.rb` under the line `class Application < Rails::Application`:
204
345
 
205
346
  ~~~ruby
206
- Rails.application.configure do
207
- # Read configuration from AWS Parameter Store
208
- config.secret_config.use :ssm, path: '/production/my_application'
347
+ module MyApp
348
+ class Application < Rails::Application
349
+
350
+ # Add the following lines to configure Secret Config:
351
+ if Rails.env.development? || Rails.env.test?
352
+ # Use 'config/application.yml'
353
+ config.secret_config.use :file
354
+ else
355
+ # Read configuration from AWS SSM Parameter Store
356
+ config.secret_config.use :ssm, path: "/#{Rails.env}/my_app"
357
+ end
358
+
359
+ # ....
360
+ end
209
361
  end
210
362
  ~~~
211
363
 
212
364
  `path` is the path from which the configuration data will be read. This path uniquely identifies the
213
- configuration for this instance of the application.
365
+ configuration for this instance of the application. In the example above it uses the rails env and application name
366
+ by default. This can be overridden using the `SECRET_CONFIG_PATH` environment variable when needed.
367
+
368
+ By placing the secret config configuration as the very first configuration item, it allows any subsequent
369
+ configuration item to access the centralized configuration in AWS System Manager Parameter Store.
370
+
371
+ The environment variable `SECRET_CONFIG_PROVIDER` can be used to override the provider when needed.
372
+ For example:
373
+ `export SECRET_CONFIG_PROVIDER=ssm`
374
+ Or,
375
+ `export SECRET_CONFIG_PROVIDER=file`
214
376
 
215
377
  If we need 2 completely separate instances of the application running in a single AWS account then we could use
216
378
  multiple paths. For example:
@@ -228,11 +390,22 @@ When writing settings to the parameter store, it is recommended to use a custom
228
390
  To supply the key to encrypt the values with, add the `key_id` parameter:
229
391
 
230
392
  ~~~ruby
231
- Rails.application.configure do
232
- # Read configuration from AWS Parameter Store
233
- config.secret_config.use :ssm,
234
- key_id: 'alias/production/myapplication',
235
- path: '/production/my_application'
393
+ module MyApp
394
+ class Application < Rails::Application
395
+
396
+ # Add the following lines to configure Secret Config:
397
+ if Rails.env.development? || Rails.env.test?
398
+ # Use 'config/application.yml'
399
+ config.secret_config.use :file
400
+ else
401
+ # Read configuration from AWS SSM Parameter Store
402
+ config.secret_config.use :ssm,
403
+ path: "/#{Rails.env}/my_app",
404
+ key_id: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
405
+ end
406
+
407
+ # ....
408
+ end
236
409
  end
237
410
  ~~~
238
411
 
@@ -241,6 +414,31 @@ Note: The relevant KMS key must be created first prior to using it here.
241
414
  The `key_id` is only used when writing settings to the AWS Parameter store and can be left off when that instance
242
415
  will only read from the parameter store.
243
416
 
417
+ ### Shared configuration for development and test
418
+
419
+ When running multiple engines or private "gems" inside the same code repository, the development and test
420
+ configuration file `application.yml` can be shared. Update the lines above to:
421
+
422
+ ~~~ruby
423
+ module MyApp
424
+ class Application < Rails::Application
425
+
426
+ # Add the following lines:
427
+ if Rails.env.development? || Rails.env.test?
428
+ # Use 'config/application.yml'
429
+ config.secret_config.use :file
430
+ else
431
+ # Read configuration from AWS SSM Parameter Store
432
+ config.secret_config.use :ssm, path: "/#{Rails.env}/my_app"
433
+ end
434
+
435
+ # ....
436
+ end
437
+ end
438
+ ~~~
439
+
440
+ Where `file_name` is the full path and filename for where `application.yml` is located.
441
+
244
442
  ### Authorization
245
443
 
246
444
  The following policy needs to be added to the IAM Group under which the application will be running:
@@ -253,8 +451,9 @@ The following policy needs to be added to the IAM Group under which the applicat
253
451
  "Sid": "VisualEditor0",
254
452
  "Effect": "Allow",
255
453
  "Action": [
256
- "ssm:PutParameter",
257
454
  "ssm:GetParametersByPath",
455
+ "ssm:PutParameter",
456
+ "ssm:DeleteParameter",
258
457
  ],
259
458
  "Resource": "*"
260
459
  }
@@ -279,11 +478,14 @@ Secret Config has a command line interface for exporting, importing and copying
279
478
  secret_config [options]
280
479
  -e, --export [FILE_NAME] Export configuration to a file or stdout if no file_name supplied.
281
480
  -i, --import [FILE_NAME] Import configuration from a file or stdin if no file_name supplied.
282
- -c, --copy SOURCE_PATH Import configuration from a file or stdin if no file_name supplied.
481
+ -C, --copy SOURCE_PATH Import configuration from a file or stdin if no file_name supplied.
482
+ -D, --diff [FILE_NAME] Compare configuration from a file or stdin if no file_name supplied.
483
+ -c, --console Start interactive console.
283
484
  -p, --path PATH Path to import from / export to.
284
485
  -P, --provider PROVIDER Provider to use. [ssm | file]. Default: ssm
285
486
  -U, --no-filter Do not filter passwords and keys.
286
- -k, --key KEY_ID | KEY_ALIAS AWS KMS Key id or Key Alias to use when importing configuration values. Default: AWS Default key.
487
+ -d, --prune During import delete all existing keys for which there is no key in the import file.
488
+ -k, --key_id KEY_ID AWS KMS Key id or Key Alias to use when importing configuration values. Default: AWS Default key.
287
489
  -r, --region REGION AWS Region to use. Default: AWS_REGION env var.
288
490
  -R, --random_size INTEGER Size to use when generating random values. Whenever $random is encountered during an import. Default: 32
289
491
  -v, --version Display Symmetric Encryption version.
@@ -292,34 +494,219 @@ secret_config [options]
292
494
 
293
495
  ### CLI Examples
294
496
 
497
+ #### Import from a file into SSM parameters
498
+
499
+ To get started it is useful to create a YAML file with all the relevant settings and then import
500
+ it into AWS SSM Parameter store. This file is the same as `applcation.yml` except that each file
501
+ is just for one environment. I.e. It does not contain the `test` or `development` root level entries.
502
+
503
+ For example: `production.yml`
504
+
505
+ ~~~yaml
506
+ mysql:
507
+ database: secret_config_production
508
+ username: secret_config
509
+ password: secret_configrules
510
+ host: mysql_server.example.net
511
+
512
+ mongo:
513
+ database: secret_config_production
514
+ primary: mongo_primary.example.net:27017
515
+ secondary: mongo_secondary.example.net:27017
516
+
517
+ secrets:
518
+ secret_key_base: somereallylongproductionstring
519
+ ~~~
520
+
521
+ Import a yaml file, into a path in AWS SSM Parameter Store:
522
+
523
+ secret_config --import production.yml --path /production/my_application
524
+
525
+ Import a yaml file, into a path in AWS SSM Parameter Store, using a custom KMS key to encrypt the values:
526
+
527
+ secret_config --import production.yml --path /production/my_application --key_id "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
528
+
529
+ #### Diff
530
+
531
+ Before importing a new config file into the AWS SSM Parameter store, a diff can be performed to determine
532
+ what the differences are that will be applied when the import is run with the `--prune` option.
533
+
534
+ secret_config --diff production.yml --path /production/my_application
535
+
536
+ Key:
537
+
538
+ + Adding a new key to the registry.
539
+ - The key will be removed from the registry during the import if --prune is specified.
540
+ * The value for that key will change during an import.
541
+
542
+ #### Export SSM parameters
543
+
544
+ In AWS SSM Parameter store it can be difficult to
545
+ Export the values from a specific path into a yaml or json file so that they are easier to read.
546
+
295
547
  Export from a path in AWS SSM Parameter Store to a yaml file, where passwords are filtered:
296
548
 
297
- secret_config --export test.yml --path /test/my_application
549
+ secret_config --export production.yml --path /production/my_application
298
550
 
299
551
  Export from a path in AWS SSM Parameter Store to a yaml file, _without_ filtering out passwords:
300
552
 
301
- secret_config --export test.yml --path /test/my_application --no-filter
553
+ secret_config --export production.yml --path /production/my_application --no-filter
302
554
 
303
555
  Export from a path in AWS SSM Parameter Store to a json file, where passwords are filtered:
304
556
 
305
- secret_config --export test.json --path /test/my_application
557
+ secret_config --export production.json --path /production/my_application
306
558
 
307
- Import a yaml file, into a path in AWS SSM Parameter Store:
559
+ #### Copy values between paths in AWS SSM parameter store
308
560
 
309
- secret_config --import test.yml --path /production/my_application
561
+ It can be useful to keep a "master" copy of the values for an environment or stack in a custom path
562
+ in AWS Parameter Store. Then for each stack or environment that is spun up, copy the "master" / "common" values
563
+ into the new path. Once copied the values specific to that path can be updated accordingly.
310
564
 
311
- Import a yaml file, into a path in AWS SSM Parameter Store, using a custom KMS key to encrypt the values:
565
+ Copy configuration from one path in AWS SSM Parameter Store to another path in AWS SSM Parameter Store:
312
566
 
313
- secret_config --import test.yml --path /production/my_application --key_id "arn:aws:kms:us-east-1:23643632463:key/UUID"
567
+ secret_config --copy /production/my_application --path /tenant73/my_application
314
568
 
315
- Copy configuration from one path in AWS SSM Parameter Store to another path in AWS SSM Parameter Store:
569
+ #### Generating random passwords
570
+
571
+ In the multi-tenant example above, we may want to generate a secure random password for each tenant.
572
+ In the source file or registry, set the value to `$random`, this will ensure that during the `import` or `copy`
573
+ that the destination will receive a secure random value.
574
+
575
+ By default the length of the randomized value is 32 bytes, use `--random_size` to adjust the length of
576
+ the randomized string.
577
+
578
+ ## Docker
579
+
580
+ Secret Config is at its best when the application is containerized. By externalizing the configuration the same
581
+ docker container can be tested in one or more environments and then deployed directly to production without
582
+ any changes. The only difference being the path that container uses to read its configuration from.
583
+
584
+ Another important benefit is that the docker image does not contain any production or test credentials since
585
+ these are all stored in AWS SSM Parameter Store.
586
+
587
+ When a Ruby / Rails application is using Secret Config for its configuration settings, it only requires the
588
+ following environment variables when starting up the container in for example AWS ECS or AWS Fargate:
316
589
 
317
- secret_config --copy /test/my_application --path /production/my_application
590
+ ~~~shell
591
+ export SECRET_CONFIG_PATH=/production/my_application
592
+ ~~~
593
+
594
+ For rails applications, typically the `RAILS_ENV` is also needed, but not required for Secret Config.
595
+
596
+ ~~~shell
597
+ export RAILS_ENV=production
598
+ ~~~
599
+
600
+ ### Logging
601
+
602
+ When using Semantic Logger, the following code could be added to `application.rb` to facilitate configuration
603
+ of the logging output via Secret Config:
604
+
605
+ ~~~ruby
606
+ # Logging
607
+ config.log_level = config.secret_config.fetch("logger/level", default: :info, type: :symbol)
608
+ config.semantic_logger.backtrace_level = config.secret_config.fetch("logger/backtrace_level", default: :error, type: :symbol)
609
+ config.semantic_logger.application = config.secret_config.fetch("logger/application", default: "my_app")
610
+ config.semantic_logger.environment = config.secret_config.fetch("logger/environment", default: Rails.env)
611
+ ~~~
612
+
613
+ In any environment the log level can be changed, for example set `logger/level` to `debug`. And it can be changed
614
+ in the AWS SSM Parameter Store, or directly with the environment variable `export LOGGER_LEVEL=debug`
615
+
616
+ `logger/environment` can be used to identify which tenant the log messages are emanating from. By default it is just
617
+ the rails environment. For example set `logger/environment` to `tenant73`.
618
+
619
+ Additionally the following code can be used with containers to send log output to standard out:
620
+
621
+ ~~~ruby
622
+ destination = config.secret_config.fetch("logger/destination", default: :file, type: :symbol)
623
+ if destination == :stdout
624
+ STDOUT.sync = true
625
+ config.rails_semantic_logger.add_file_appender = false
626
+ config.semantic_logger.add_appender(
627
+ io: STDOUT,
628
+ level: config.log_level,
629
+ formatter: config.secret_config.fetch("logger/formatter", default: :default, type: :symbol)
630
+ )
631
+ end
632
+ ~~~
633
+
634
+ Specifically for docker containers it is necessary to turn off file logging and turn on logging to standard out
635
+ so that AWS Cloud Watch can pick up the log data.
636
+
637
+ To start with `logger/destination` of `stdout` will work with regular non-colorized output. When feeding the
638
+ log output into something that can process JSON, set `logger/formatter` to `json`.
639
+
640
+ The benefit with the above approach is that a developer can pull the exact same container image that is running
641
+ in production and configure it to run locally on their laptop. For example, set `logger/destination` to `file`.
318
642
 
319
- During an `import` or `copy` if any of the source values consist only of `$random`,
320
- they will be replaced with a secure 32 byte random value.
321
- This is deal for when a secure random password needs to be generated.
322
- Use `--random_size` to adjust the length of the randomized string.
643
+ The above code can be modified as necessary to add any Semantic Logger appender to write directly to external
644
+ centralized logging systems, instead of writing to standard out or local files.
645
+
646
+ ### Email Server and Assets
647
+
648
+ An example of how to setup the email server and the assets for html emails. Add to `application.rb`:
649
+
650
+ ~~~ruby
651
+ # Emails
652
+ application_url = config.secret_config.fetch("emails/asset_host")
653
+ uri = URI.parse(application_url)
654
+
655
+ config.action_mailer.default_url_options = {host: uri.host, protocol: uri.scheme}
656
+ config.action_mailer.asset_host = application_url
657
+ config.action_mailer.smtp_settings = {address: config.secret_config.fetch("emails/smtp/address", default: "localhost")}
658
+ config.action_mailer.raise_delivery_errors = config.secret_config.fetch("emails/raise_delivery_errors", default: true, type: :boolean)
659
+ ~~~
660
+
661
+ ### Symmetric Encryption
662
+
663
+ An example of how to setup Symmetric Encryption. Add to `application.rb`:
664
+
665
+ ~~~ruby
666
+ # Encryption
667
+ config.symmetric_encryption.cipher =
668
+ SymmetricEncryption::Cipher.new(
669
+ key: config.secret_config.fetch('symmetric_encryption/key', encoding: :base64),
670
+ iv: config.secret_config.fetch('symmetric_encryption/iv', encoding: :base64),
671
+ version: config.secret_config.fetch('symmetric_encryption/version', type: :integer),
672
+ )
673
+
674
+ # Also support one prior encryption key version during key rotation
675
+ if config.secret_config.key?('symmetric_encryption/old/key')
676
+ SymmetricEncryption.secondary_ciphers = [
677
+ SymmetricEncryption::Cipher.new(
678
+ key: config.secret_config.fetch('symmetric_encryption/old/key', encoding: :base64),
679
+ iv: config.secret_config.fetch('symmetric_encryption/old/iv', encoding: :base64),
680
+ version: config.secret_config.fetch('symmetric_encryption/old/version', type: :integer),
681
+ ),
682
+ ]
683
+ end
684
+ ~~~
685
+
686
+ Using this approach the file `config/symmetric-encryption.yml` can be removed once the keys have been moved to
687
+ the registry.
688
+
689
+ To extract existing keys from the config file so that they can be imported into the registry,
690
+ run the code below inside a console in each of the respective environments.
691
+
692
+ ~~~ruby
693
+ require "yaml"
694
+ require "base64"
695
+
696
+ def se_config(cipher)
697
+ {
698
+ "key" => Base64.strict_encode64(cipher.send(:key)),
699
+ "iv" => Base64.strict_encode64(cipher.iv),
700
+ "version" => cipher.version
701
+ }
702
+ end
703
+
704
+ config = { "symmetric_encryption" => se_config(SymmetricEncryption.cipher) }
705
+ if cipher = SymmetricEncryption.secondary_ciphers.first
706
+ config["symmetric_encryption"]["old"] = se_config(cipher)
707
+ end
708
+ puts config.to_yaml
709
+ ~~~
323
710
 
324
711
  ## Versioning
325
712
 
@@ -11,7 +11,7 @@ module SecretConfig
11
11
  attr_reader :path, :region, :provider,
12
12
  :export, :no_filter,
13
13
  :import, :key_id, :random_size, :prune, :overwrite,
14
- :copy_path,
14
+ :copy_path, :diff,
15
15
  :console,
16
16
  :show_version
17
17
 
@@ -35,6 +35,7 @@ module SecretConfig
35
35
  @copy_path = nil
36
36
  @show_version = false
37
37
  @console = false
38
+ @diff = false
38
39
 
39
40
  if argv.empty?
40
41
  puts parser
@@ -51,10 +52,14 @@ module SecretConfig
51
52
  run_console
52
53
  elsif export
53
54
  run_export(export, filtered: !no_filter)
55
+ elsif import && prune
56
+ run_import_and_prune(import)
54
57
  elsif import
55
58
  run_import(import)
56
59
  elsif copy_path
57
60
  run_copy(copy_path, path)
61
+ elsif diff
62
+ run_diff(diff)
58
63
  else
59
64
  puts parser
60
65
  end
@@ -78,11 +83,15 @@ module SecretConfig
78
83
  @import = file_name || STDIN
79
84
  end
80
85
 
81
- opts.on '-c', '--copy SOURCE_PATH', 'Import configuration from a file or stdin if no file_name supplied.' do |path|
86
+ opts.on '-C', '--copy SOURCE_PATH', 'Import configuration from a file or stdin if no file_name supplied.' do |path|
82
87
  @copy_path = path
83
88
  end
84
89
 
85
- opts.on '-i', '--console', 'Start interactive console.' do
90
+ opts.on '-D', '--diff [FILE_NAME]', 'Compare configuration from a file or stdin if no file_name supplied.' do |file_name|
91
+ @diff = file_name
92
+ end
93
+
94
+ opts.on '-c', '--console', 'Start interactive console.' do
86
95
  @console = true
87
96
  end
88
97
 
@@ -98,14 +107,9 @@ module SecretConfig
98
107
  @no_filter = true
99
108
  end
100
109
 
101
- # TODO:
102
- # opts.on '-Q', '--prune', 'During import delete all existing keys for which their is no key in the import file' do
103
- # @prune = true
104
- # end
105
- #
106
- # opts.on '-r', '--replace', 'During import replace existing keys if present' do
107
- # @replace = true
108
- # end
110
+ opts.on '-d', '--prune', 'During import delete all existing keys for which there is no key in the import file.' do
111
+ @prune = true
112
+ end
109
113
 
110
114
  opts.on '-k', '--key_id KEY_ID', 'AWS KMS Key id or Key Alias to use when importing configuration values. Default: AWS Default key.' do |key_id|
111
115
  @key_id = key_id
@@ -145,19 +149,35 @@ module SecretConfig
145
149
 
146
150
  def run_export(file_name, filtered: true)
147
151
  config = fetch_config(path, filtered: filtered)
148
-
149
- format = file_format(file_name)
150
- data = render(config, format)
151
- write_file(file_name, data)
152
+ write_config_file(file_name, config)
152
153
 
153
154
  puts("Exported #{path} from #{provider} to #{file_name}") if file_name.is_a?(String)
154
155
  end
155
156
 
156
157
  def run_import(file_name)
157
- format = file_format(file_name)
158
- data = read_file(file_name)
159
- config = parse(data, format)
160
- set_config(config, path)
158
+ config = read_config_file(file_name)
159
+
160
+ set_config(config, path, current_values)
161
+
162
+ puts("Imported #{file_name} to #{provider} at #{path}") if file_name.is_a?(String)
163
+ end
164
+
165
+ def run_import_and_prune(file_name)
166
+ config = read_config_file(file_name)
167
+ delete_keys = current_values.keys - Utils.flatten(config, path).keys
168
+
169
+ unless delete_keys.empty?
170
+ puts "Going to delete the following keys:"
171
+ delete_keys.each {|key| puts " #{key}"}
172
+ sleep(5)
173
+ end
174
+
175
+ set_config(config, path, current_values)
176
+
177
+ delete_keys.each do |key|
178
+ puts "Deleting: #{key}"
179
+ provider_instance.delete(key)
180
+ end
161
181
 
162
182
  puts("Imported #{file_name} to #{provider} at #{path}") if file_name.is_a?(String)
163
183
  end
@@ -165,21 +185,64 @@ module SecretConfig
165
185
  def run_copy(source_path, target_path)
166
186
  config = fetch_config(source_path, filtered: false)
167
187
 
168
- set_config(config, target_path)
188
+ set_config(config, target_path, current_values)
169
189
 
170
190
  puts "Copied #{source_path} to #{target_path} using #{provider}"
171
191
  end
172
192
 
193
+ def run_diff(file_name)
194
+ file_config = read_config_file(file_name)
195
+ file = Utils.flatten(file_config, path)
196
+
197
+ registry_config = fetch_config(path, filtered: false)
198
+ registry = Utils.flatten(registry_config, path)
199
+
200
+ (file.keys + registry.keys).sort.uniq.each do |key|
201
+ if registry.key?(key)
202
+ if file.key?(key)
203
+ if file[key].to_s != registry[key].to_s
204
+ puts "* #{key}: #{registry[key]} => #{file[key]}"
205
+ end
206
+ else
207
+ puts "- #{key}"
208
+ end
209
+ elsif file.key?(key)
210
+ puts "+ #{key}: #{file[key]}"
211
+ end
212
+ end
213
+
214
+ puts("Compared #{file_name} to #{provider} at #{path}") if file_name.is_a?(String)
215
+ end
216
+
173
217
  def run_console
174
218
  IRB.start
175
219
  end
176
220
 
177
- def set_config(config, path)
178
- # TODO: prune, replace
221
+ def current_values
222
+ @current_values ||= Utils.flatten(fetch_config(path, filtered: false), path)
223
+ end
224
+
225
+ def read_config_file(file_name)
226
+ format = file_format(file_name)
227
+ data = read_file(file_name)
228
+ parse(data, format)
229
+ end
230
+
231
+ def write_config_file(file_name, config)
232
+ format = file_format(file_name)
233
+ data = render(config, format)
234
+ write_file(file_name, data)
235
+ end
236
+
237
+ def set_config(config, path, current_values = {})
179
238
  Utils.flatten_each(config, path) do |key, value|
180
239
  next if value.nil?
240
+ next if current_values[key].to_s == value.to_s
181
241
 
182
- value = random_password if value.to_s.strip == '$random'
242
+ if value.to_s.strip == '$random'
243
+ next if current_values[key]
244
+ value = random_password
245
+ end
183
246
  puts "Setting: #{key}"
184
247
  provider_instance.set(key, value)
185
248
  end
@@ -4,7 +4,7 @@ require 'erb'
4
4
  module SecretConfig
5
5
  module Providers
6
6
  # Read configuration from a local file
7
- class File
7
+ class File < Provider
8
8
  attr_reader :file_name
9
9
 
10
10
  def initialize(file_name: "config/application.yml")
@@ -18,16 +18,11 @@ module SecretConfig
18
18
  paths = path.sub(/\A\/*/, '').sub(/\/*\Z/, '').split("/")
19
19
  settings = config.dig(*paths)
20
20
 
21
- raise(ConfigurationError, "Path #{paths.join(".")} not found in file: #{file_name}") unless settings
21
+ raise(ConfigurationError, "Path #{paths.join("/")} not found in file: #{file_name}") unless settings
22
22
 
23
23
  Utils.flatten_each(settings, path, &block)
24
24
  nil
25
25
  end
26
-
27
- def set(key, value)
28
- raise NotImplementedError
29
- end
30
-
31
26
  end
32
27
  end
33
28
  end
@@ -0,0 +1,20 @@
1
+ module SecretConfig
2
+ module Providers
3
+ # Abstract Base provider
4
+ class Provider
5
+ def set(key, value)
6
+ raise NotImplementedError
7
+ end
8
+
9
+ def delete(key)
10
+ raise NotImplementedError
11
+ end
12
+
13
+ def to_h(path)
14
+ h = {}
15
+ each(path) { |key, value| h[key] = value }
16
+ h
17
+ end
18
+ end
19
+ end
20
+ end
@@ -7,7 +7,7 @@ end
7
7
  module SecretConfig
8
8
  module Providers
9
9
  # Use the AWS System Manager Parameter Store for Centralized Configuration / Secrets Management
10
- class Ssm
10
+ class Ssm < Provider
11
11
  attr_reader :client, :key_id
12
12
 
13
13
  def initialize(key_id: nil)
@@ -40,6 +40,10 @@ module SecretConfig
40
40
  overwrite: true
41
41
  )
42
42
  end
43
+
44
+ def delete(key)
45
+ client.delete_parameter(name: key)
46
+ end
43
47
  end
44
48
  end
45
49
  end
@@ -7,11 +7,14 @@ module SecretConfig
7
7
  attr_reader :provider
8
8
  attr_accessor :path
9
9
 
10
- def initialize(path: nil, provider: :file, provider_args: nil)
11
- @path = path || default_path
10
+ def initialize(path: nil, provider: nil, provider_args: nil)
11
+ @path = default_path(path)
12
12
  raise(UndefinedRootError, 'Root must start with /') unless @path.start_with?('/')
13
13
 
14
- @provider = create_provider(provider, provider_args)
14
+ resolved_provider = default_provider(provider)
15
+ provider_args = nil if resolved_provider != provider
16
+
17
+ @provider = create_provider(resolved_provider, provider_args)
15
18
  @cache = Concurrent::Map.new
16
19
  refresh!
17
20
  end
@@ -53,7 +56,7 @@ module SecretConfig
53
56
  # Set the value for a key in the centralized configuration store.
54
57
  def set(key:, value:, encrypt: true)
55
58
  key = expand_key(key)
56
- provider.set(key, value, encrypt: true)
59
+ provider.set(key, value, encrypt: encrypt)
57
60
  cache[key] = value
58
61
  end
59
62
 
@@ -152,13 +155,21 @@ module SecretConfig
152
155
  args && args.size > 0 ? klass.new(**args) : klass.new
153
156
  end
154
157
 
155
- def default_path
156
- path = ENV["SECRET_CONFIG_PATH"] || ENV["RAILS_ENV"]
158
+ def default_path(configured_path)
159
+ path = ENV["SECRET_CONFIG_PATH"] || configured_path || ENV["RAILS_ENV"]
157
160
  path = Rails.env if path.nil? && defined?(Rails) && Rails.respond_to?(:env)
158
161
 
159
162
  raise(UndefinedRootError, "Either set env var 'SECRET_CONFIG_PATH' or call SecretConfig.use") unless path
160
163
 
161
164
  path.start_with?('/') ? path : "/#{path}"
162
165
  end
166
+
167
+ def default_provider(provider)
168
+ provider = (ENV["SECRET_CONFIG_PROVIDER"] || provider || 'file')
169
+
170
+ return provider if provider.respond_to?(:each) && provider.respond_to?(:set)
171
+
172
+ provider.to_s.downcase.to_sym
173
+ end
163
174
  end
164
175
  end
@@ -1,7 +1,7 @@
1
1
  module SecretConfig
2
2
  module Utils
3
- # Takes a hierarchical structure and flattens it to a single level
4
- # If path is supplied it is prepended to every key returned
3
+ # Takes a hierarchical structure and flattens it to a single level.
4
+ # If path is supplied it is prepended to every key returned.
5
5
  def self.flatten_each(hash, path = nil, &block)
6
6
  hash.each_pair do |key, value|
7
7
  name = path.nil? ? key : "#{path}/#{key}"
@@ -9,6 +9,14 @@ module SecretConfig
9
9
  end
10
10
  end
11
11
 
12
+ # Takes a hierarchical structure and flattens it to a single level hash.
13
+ # If path is supplied it is prepended to every key returned.
14
+ def self.flatten(hash, path = nil)
15
+ h = {}
16
+ flatten_each(hash, path) { |key, value| h[key] = value }
17
+ h
18
+ end
19
+
12
20
  def self.constantize_symbol(symbol, namespace = 'SecretConfig::Providers')
13
21
  klass = "#{namespace}::#{camelize(symbol.to_s)}"
14
22
  begin
@@ -1,3 +1,3 @@
1
1
  module SecretConfig
2
- VERSION = '0.4.5'
2
+ VERSION = '0.5.0'
3
3
  end
data/lib/secret_config.rb CHANGED
@@ -8,6 +8,7 @@ require 'secret_config/railtie' if defined?(Rails)
8
8
  module SecretConfig
9
9
  module Providers
10
10
  autoload :File, 'secret_config/providers/file'
11
+ autoload :Provider, 'secret_config/providers/provider'
11
12
  autoload :Ssm, 'secret_config/providers/ssm'
12
13
  end
13
14
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: secret_config
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.5
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Reid Morrison
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-05-01 00:00:00.000000000 Z
11
+ date: 2019-05-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -40,6 +40,7 @@ files:
40
40
  - lib/secret_config/cli.rb
41
41
  - lib/secret_config/errors.rb
42
42
  - lib/secret_config/providers/file.rb
43
+ - lib/secret_config/providers/provider.rb
43
44
  - lib/secret_config/providers/ssm.rb
44
45
  - lib/secret_config/railtie.rb
45
46
  - lib/secret_config/registry.rb