vault-rails 0.1.2 → 0.2.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
  SHA1:
3
- metadata.gz: f424a57360c1017f40c429ec2570a08eb81f582a
4
- data.tar.gz: 28d0fadd3e06908c2d0b35ff7e4a1eddcbc3a4db
3
+ metadata.gz: 2372e9033c1ad9acaed9ba40d0a16ecaf9255389
4
+ data.tar.gz: 1ad8eba93c1e7214da702473a92729131902ccb5
5
5
  SHA512:
6
- metadata.gz: e1d46eea0dd84f894af558a79379b470b0ace0c0d0efeb0abcd2e1d29b3b55c9985a6b78a213179176713c8a09f665d5e5241af18abb226457716b1d9190d513
7
- data.tar.gz: bbe7d51daac322bf3eab796cc95912de7016097e7d188e0cebb2ae3a2982c566e0b8630ae8692cec93799d3f54d487079b7bda17ddaf86921abc81975dda0aeb
6
+ metadata.gz: 5fb7fc74a89248efa68c5dd0698fa08940b44d1cf129957b30922b19765b2d5a121f34c1aa821f9c9b944794a488d31ef97932d31c55aaab7867c2b382824fb8
7
+ data.tar.gz: 685d0b2d9a6ea88c0308defc8e7dd06f806b0dbf06d19bc3faf8d4e58353564cc37df8cf009063d5b6a61db35a4adf385e25c5140d711f0e3d92912fd41c67c2
data/README.md CHANGED
@@ -3,6 +3,7 @@ Vault Rails [![Build Status](https://secure.travis-ci.org/hashicorp/vault-rails.
3
3
 
4
4
  Vault is the official Rails plugin for interacting with [Vault](https://vaultproject.io) by HashiCorp.
5
5
 
6
+ **The documentation in this README corresponds to the master branch of the Vault Rails plugin. It may contain unreleased features or different APIs than the most recently released version. Please see the Git tag that corresponds to your version of the Vault Rails plugin for the proper documentation.**
6
7
 
7
8
  Quick Start
8
9
  -----------
@@ -19,13 +20,25 @@ Quick Start
19
20
  ```ruby
20
21
  require "vault/rails"
21
22
 
22
- Vault.configure do |vault|
23
+ Vault::Rails.configure do |vault|
24
+ # Use Vault in transit mode for encrypting and decrypting data. If
25
+ # disabled, vault-rails will encrypt data in-memory using a similar
26
+ # algorithm to Vault. The in-memory store uses a predictable encryption
27
+ # which is great for development and test, but should _never_ be used in
28
+ # production.
29
+ vault.enabled = Rails.env.production?
30
+
31
+ # The name of the application. All encrypted keys in Vault will be
32
+ # prefixed with this application name. If you change the name of the
33
+ # application, you will need to migrate the encrypted data to the new
34
+ # key namespace.
23
35
  vault.application = "my_app"
24
36
 
25
- # Default: ENV["VAULT_ADDR"]
37
+ # The address of the Vault server. Default: ENV["VAULT_ADDR"].
26
38
  vault.address = "https://vault.corp"
27
39
 
28
- # Default: ENV["VAULT_TOKEN"]
40
+ # The token to communicate with the Vault server.
41
+ # Default: ENV["VAULT_TOKEN"].
29
42
  vault.token = "abcd1234"
30
43
  end
31
44
  ```
@@ -60,21 +73,85 @@ Quick Start
60
73
  person.ssn_encrypted #=> "vault:v0:EE3EV8P5hyo9h..."
61
74
  ```
62
75
 
63
- You can customize the `vault_attribute` method with the following options:
64
76
 
65
- ```ruby
66
- vault_attribute :credit_card,
67
- encrypted_column: :cc_encrypted,
68
- path: "credit-secrets",
69
- key: "people_credit_cards"
70
- ```
77
+ Advanced Configuration
78
+ ----------------------
79
+ The following section details some of the more advanced configuration options for vault-rails. As a general rule, you should try to use vault-rails without these options until absolutely necessary.
80
+
81
+ #### Specifying the encrypted column
82
+ By default, the name of the encrypted column is `#{column}_encrypted`. This is customizable by setting the `:encrypted_column` option when declaring the attribute:
83
+
84
+ ```ruby
85
+ vault_attribute :credit_card,
86
+ encrypted_column: :cc_encrypted
87
+ ```
88
+
89
+ - **Note** Changing this value for an existing application will make existing values no longer decryptable!
90
+ - **Note** This value **cannot** be the same name as the vault attribute!
91
+
92
+ #### Specifying a custom key
93
+ By default, the name of the key in Vault is `#{app}_#{table}_#{column}`. This is customizable by setting the `:key` coption when declaring the attribute:
94
+
95
+ ```ruby
96
+ vault_attribute :credit_card,
97
+ key: "pci-data"
98
+ ```
99
+
100
+ - **Note** Changing this value for an existing application will make existing values no longer decryptable!
71
101
 
72
- - `:encrypted_column` - the name of the encrypted column
73
- (default: `attribute_encrypted`)
74
- - `:key` - the name of the key
75
- (default: `#{app}_#{table}_#{column}`)
76
- - `:path` - the path to the transit backend to use
77
- (default: `transit/`)
102
+ #### Specifying a different Vault path
103
+ By default, the path to the transit backend in Vault is `transit/`. This is customizable by setting the `:path` option when declaring the attribute:
104
+
105
+ ```ruby
106
+ vault_attribute :credit_card,
107
+ path: "transport"
108
+ ```
109
+
110
+ - **Note** Changing this value for an existing application will make existing values no longer decryptable!
111
+
112
+ #### Automatic serializing
113
+ By default, all values are assumed to be "text" fields in the database. Sometimes it is beneficial for your application to work with a more flexible data structure (such as a Hash or Array). Vault-rails can automatically serialize and deserialize these structures for you:
114
+
115
+ ```ruby
116
+ vault_attribute :details
117
+ serialize: :json
118
+ ```
119
+
120
+ - **Note** You can view the source for the exact serialization and deserialization options, but they are intentionally not customizable and cannot be used for a full object marshal/unmarshal.
121
+
122
+ For customized solutions, you can also pass a module to the `:serializer` key. This module must have the following API:
123
+
124
+ ```ruby
125
+ module MySerializer
126
+ # @param [String, nil] raw
127
+ # @return [String, nil]
128
+ def self.encode(raw); end
129
+
130
+ # @param [String, nil] raw
131
+ # @return [String, nil]
132
+ def self.decode(raw); end
133
+ end
134
+ ```
135
+
136
+ Your class must account for `nil` and "empty" values if necessary. Then specify the class as the serializer:
137
+
138
+ ```ruby
139
+ vault_attribute :details,
140
+ serialize: MySerializer
141
+ ```
142
+
143
+ - **Note** It is possible to encode and decode entire Ruby objects using a custom serializer. Please do not do that. You will have a bad time.
144
+
145
+ #### Custom encoding/decoding
146
+ If a custom serializer seems too heavy, you can declare an `:encode` and `:decode` proc when declaring the attribute. Both options must be given:
147
+
148
+ ```ruby
149
+ vault_attribute :address,
150
+ encode: ->(raw) { raw.to_s.upcase },
151
+ decode: ->(raw) { raw.to_s }
152
+ ```
153
+
154
+ - **Note** Changing the algorithm for encoding/decoding for an existing application will probably make the application crash when attempting to retrive existing values!
78
155
 
79
156
  Caveats
80
157
  -------
@@ -86,11 +163,31 @@ The Vault Rails plugin does not automatically mount a backend. It is assumed the
86
163
  $ vault mount transit
87
164
  ```
88
165
 
89
- The Vault Rails plugin does not automatically create transit keys in Vault. This is intentional so that you can give the policy for the token associated with the Rails application read/write access to the encrypt/decrypt backends _only_, without giving the Rails application the ability to read encryption keys from Vault.
166
+ If you are running Vault 0.2.0 or later, the Vault Rails plugin will automatically create keys in the transit backend if it has permission. Here is an example policy to grant permissions:
167
+
168
+ ```javascript
169
+ # Allow renewal of leases for secrets
170
+ path "sys/renew/*" {
171
+ policy = "write"
172
+ }
90
173
 
91
- If an attacker gained access to the Rails application server, they would be able to read all encryption keys in Vault, making decryption of sensitive data in the database easy.
174
+ # Allow renewal of token leases
175
+ path "auth/token/renew/*" {
176
+ policy = "write"
177
+ }
92
178
 
93
- Instead, you should create keys for each column you plan to encrypt using a different policy, out-of-band from the Rails application. For example:
179
+ path "transit/encrypt/myapp_*" {
180
+ policy = "write"
181
+ }
182
+
183
+ path "transit/decrypt/myapp_*" {
184
+ policy = "write"
185
+ }
186
+ ```
187
+
188
+ Note that you will need to have an out-of-band process to renew your Vault token.
189
+
190
+ For lower versions of Vault, the Vault Rails plugin does not automatically create transit keys in Vault. Instead, you should create keys for each column you plan to encrypt using a different policy, out-of-band from the Rails application. For example:
94
191
 
95
192
  ```shell
96
193
  $ vault write transit/keys/<key> create=1
@@ -118,19 +215,6 @@ This is because the database is unaware of the plain-text data (which is part of
118
215
  the security model).
119
216
 
120
217
 
121
- Testing
122
- -------
123
- The Vault Rails plugin includes a testing harness to avoid needing to spin up a
124
- real Vault server during tests:
125
-
126
- ```ruby
127
- require "vault/rails/testing"
128
- Vault::Rails::Testing.enable!
129
- ```
130
-
131
- This will stub all requests to encrypted attributes to use an in-memory store.
132
-
133
-
134
218
  Development
135
219
  -----------
136
220
  1. Clone the project on GitHub
@@ -1,3 +1,5 @@
1
+ require "active_support/concern"
2
+
1
3
  module Vault
2
4
  module EncryptedModel
3
5
  extend ActiveSupport::Concern
@@ -27,39 +29,85 @@ module Vault
27
29
  # the path to the transit backend (default: +transit+)
28
30
  # @option options [String] :key
29
31
  # the name of the encryption key (default: +#{app}_#{table}_#{column}+)
30
- def vault_attribute(column, options = {})
31
- encrypted_column = options[:encrypted_column] || "#{column}_encrypted"
32
+ # @option options [Symbol, Class] :serializer
33
+ # the name of the serializer to use (or a class)
34
+ # @option options [Proc] :encode
35
+ # a proc to encode the value with
36
+ # @option options [Proc] :decode
37
+ # a proc to decode the value with
38
+ def vault_attribute(attribute, options = {})
39
+ encrypted_column = options[:encrypted_column] || "#{attribute}_encrypted"
32
40
  path = options[:path] || "transit"
33
- key = options[:key] || "#{Vault.application}_#{table_name}_#{column}"
41
+ key = options[:key] || "#{Vault::Rails.application}_#{table_name}_#{attribute}"
34
42
 
35
- class_eval <<-EOH, __FILE__, __LINE__ + 1
36
- def #{column}
37
- value = instance_variable_get(:@#{column})
38
- return value if !value.nil?
43
+ # Sanity check options!
44
+ _vault_validate_options!(options)
39
45
 
40
- ciphertext = read_attribute(:#{encrypted_column})
41
- return nil if ciphertext.nil?
46
+ # Get the serializer if one was given.
47
+ serializer = options[:serialize]
42
48
 
43
- plaintext = Vault::Rails.decrypt("#{path}", "#{key}", ciphertext)
44
- instance_variable_set(:@#{column}, plaintext)
45
- end
49
+ # Unless a class or module was given, construct our serializer. (Slass
50
+ # is a subset of Module).
51
+ if serializer && !serializer.is_a?(Module)
52
+ serializer = Vault::Rails.serializer_for(serializer)
53
+ end
54
+
55
+ # See if custom encoding or decoding options were given.
56
+ if options[:encode] && options[:decode]
57
+ serializer = Class.new
58
+ serializer.define_singleton_method(:encode, &options[:encode])
59
+ serializer.define_singleton_method(:decode, &options[:decode])
60
+ end
46
61
 
47
- def #{column}=(plaintext)
48
- ciphertext = Vault::Rails.encrypt("#{path}", "#{key}", plaintext)
49
- write_attribute(:#{encrypted_column}, ciphertext)
50
- instance_variable_set(:@#{column}, plaintext)
62
+ # Getter
63
+ define_method("#{attribute}") do
64
+ instance_variable_get("@#{attribute}")
65
+ end
66
+
67
+ # Setter
68
+ define_method("#{attribute}=") do |value|
69
+ # If the currently set value is not the same as the given value (or
70
+ # not set at all), update the instance variable and mark it as dirty.
71
+ if instance_variable_get("@#{attribute}") != value
72
+ attribute_will_change!("#{attribute}")
73
+ instance_variable_set("@#{attribute}", value)
51
74
  end
52
75
 
53
- def #{column}?
54
- read_attribute(:#{encrypted_column}).present?
76
+ # Return the value to be consistent with other AR methods.
77
+ value
78
+ end
79
+
80
+ # Checker
81
+ define_method("#{attribute}?") do
82
+ instance_variable_get("@#{attribute}").present?
83
+ end
84
+
85
+ # Dirty method
86
+ define_method("#{attribute}_change") do
87
+ changes["#{attribute}"]
88
+ end
89
+
90
+ # Dirty method
91
+ define_method("#{attribute}_changed?") do
92
+ changed.include?("#{attribute}")
93
+ end
94
+
95
+ # Dirty method
96
+ define_method("#{attribute}_was") do
97
+ if changes["#{attribute}"]
98
+ changes["#{attribute}"][0]
99
+ else
100
+ public_send("#{attribute}")
55
101
  end
56
- EOH
102
+ end
57
103
 
58
- _vault_attributes.store(column.to_sym,
59
- encrypted_column: encrypted_column,
60
- path: path,
104
+ # Make a note of this attribute so we can use it in the future (maybe).
105
+ __vault_attributes[attribute.to_sym] = {
61
106
  key: key,
62
- )
107
+ path: path,
108
+ serializer: serializer,
109
+ encrypted_column: encrypted_column,
110
+ }
63
111
 
64
112
  self
65
113
  end
@@ -67,9 +115,152 @@ module Vault
67
115
  # The list of Vault attributes.
68
116
  #
69
117
  # @return [Hash]
70
- def _vault_attributes
118
+ def __vault_attributes
71
119
  @vault_attributes ||= {}
72
120
  end
121
+
122
+ # Validate that Vault options are all a-okay! This method will raise
123
+ # exceptions if something does not make sense.
124
+ def _vault_validate_options!(options)
125
+ if options[:serializer]
126
+ if options[:encode] || options[:decode]
127
+ raise Vault::Rails::ValidationFailedError, "Cannot use a " \
128
+ "custom encoder/decoder if a `:serializer' is specified!"
129
+ end
130
+ end
131
+
132
+ if options[:encode] && !options[:decode]
133
+ raise Vault::Rails::ValidationFailedError, "Cannot specify " \
134
+ "`:encode' without specifying `:decode' as well!"
135
+ end
136
+
137
+ if options[:decode] && !options[:encode]
138
+ raise Vault::Rails::ValidationFailedError, "Cannot specify " \
139
+ "`:decode' without specifying `:encode' as well!"
140
+ end
141
+ end
142
+ end
143
+
144
+ included do
145
+ # After a resource has been initialized, immediately communicate with
146
+ # Vault and decrypt any attributes.
147
+ after_initialize :__vault_load_attributes!
148
+
149
+ # After we save the record, persist all the values to Vault and reload
150
+ # them attributes from Vault to ensure we have the proper attributes set.
151
+ # The reason we use `after_save` here is because a `before_save` could
152
+ # run too early in the callback process. If a user is changing Vault
153
+ # attributes in a callback, it is possible that our callback will run
154
+ # before theirs, resulting in attributes that are not persisted.
155
+ after_save :__vault_persist_attributes!
156
+
157
+ # Decrypt all the attributes from Vault.
158
+ # @return [true]
159
+ def __vault_load_attributes!
160
+ self.class.__vault_attributes.each do |attribute, options|
161
+ self.__vault_load_attribute!(attribute, options)
162
+ end
163
+
164
+ return true
165
+ end
166
+
167
+ # Decrypt and load a single attribute from Vault.
168
+ def __vault_load_attribute!(attribute, options)
169
+ key = options[:key]
170
+ path = options[:path]
171
+ serializer = options[:serializer]
172
+ column = options[:encrypted_column]
173
+
174
+ # Load the ciphertext
175
+ ciphertext = read_attribute(column)
176
+
177
+ # If the user provided a value for the attribute, do not try to load
178
+ # it from Vault
179
+ if instance_variable_get("@#{attribute}")
180
+ return
181
+ end
182
+
183
+ # Load the plaintext value
184
+ plaintext = Vault::Rails.decrypt(path, key, ciphertext)
185
+
186
+ # Deserialize the plaintext value, if a serializer exists
187
+ if serializer
188
+ plaintext = serializer.decode(plaintext)
189
+ end
190
+
191
+ # Write the virtual attribute with the plaintext value
192
+ instance_variable_set("@#{attribute}", plaintext)
193
+ end
194
+
195
+ # Encrypt all the attributes using Vault and set the encrypted values back
196
+ # on this model.
197
+ # @return [true]
198
+ def __vault_persist_attributes!
199
+ changes = {}
200
+
201
+ self.class.__vault_attributes.each do |attribute, options|
202
+ if c = self.__vault_persist_attribute!(attribute, options)
203
+ changes.merge!(c)
204
+ end
205
+ end
206
+
207
+ # If there are any changes to the model, update them all at once,
208
+ # skipping any callbacks and validation. This is okay, because we are
209
+ # already in a transaction due to the callback.
210
+ if !changes.empty?
211
+ self.update_columns(changes)
212
+ end
213
+
214
+ return true
215
+ end
216
+
217
+ # Encrypt a single attribute using Vault and persist back onto the
218
+ # encrypted attribute value.
219
+ def __vault_persist_attribute!(attribute, options)
220
+ key = options[:key]
221
+ path = options[:path]
222
+ serializer = options[:serializer]
223
+ column = options[:encrypted_column]
224
+
225
+ # Only persist changed attributes to minimize requests - this helps
226
+ # minimize the number of requests to Vault.
227
+ if !changed.include?("#{attribute}")
228
+ return
229
+ end
230
+
231
+ # Get the current value of the plaintext attribute
232
+ plaintext = instance_variable_get("@#{attribute}")
233
+
234
+ # Apply the serialize to the plaintext value, if one exists
235
+ if serializer
236
+ plaintext = serializer.encode(plaintext)
237
+ end
238
+
239
+ # Generate the ciphertext and store it back as an attribute
240
+ ciphertext = Vault::Rails.encrypt(path, key, plaintext)
241
+
242
+ # Write the attribute back, so that we don't have to reload the record
243
+ # to get the ciphertext
244
+ write_attribute(column, ciphertext)
245
+
246
+ # Return the updated column so we can save
247
+ { column => ciphertext }
248
+ end
249
+
250
+ # Override the reload method to reload the Vault attributes. This will
251
+ # ensure that we always have the most recent data from Vault when we
252
+ # reload a record from the database.
253
+ def reload(*)
254
+ super.tap do
255
+ # Unset all the instance variables to force the new data to be pulled
256
+ # from Vault
257
+ self.class.__vault_attributes.each do |attribute, _|
258
+ self.instance_variable_set("@#{attribute}", nil)
259
+ end
260
+
261
+ self.__vault_load_attributes!
262
+ end
263
+ end
73
264
  end
74
265
  end
75
266
  end