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 +4 -4
- data/README.md +116 -32
- data/lib/vault/encrypted_model.rb +215 -24
- data/lib/vault/rails.rb +183 -21
- data/lib/vault/rails/configurable.rb +98 -0
- data/lib/vault/rails/errors.rb +19 -0
- data/lib/vault/rails/serializer.rb +33 -0
- data/lib/vault/rails/version.rb +1 -1
- data/spec/dummy/app/models/person.rb +16 -0
- data/spec/dummy/config/initializers/vault.rb +2 -1
- data/spec/dummy/db/development.sqlite3 +0 -0
- data/spec/dummy/db/migrate/20150428220101_create_people.rb +4 -0
- data/spec/dummy/db/schema.rb +6 -2
- data/spec/dummy/db/test.sqlite3 +0 -0
- data/spec/dummy/lib/binary_serializer.rb +12 -0
- data/spec/dummy/log/development.log +15591 -0
- data/spec/integration/rails_spec.rb +230 -6
- data/spec/support/vault_server.rb +14 -21
- data/spec/unit/encrypted_model_spec.rb +45 -0
- data/spec/unit/rails_spec.rb +14 -19
- metadata +29 -9
- data/lib/vault/rails/testing.rb +0 -73
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2372e9033c1ad9acaed9ba40d0a16ecaf9255389
|
4
|
+
data.tar.gz: 1ad8eba93c1e7214da702473a92729131902ccb5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
#
|
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
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
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
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
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
|
-
|
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
|
-
|
174
|
+
# Allow renewal of token leases
|
175
|
+
path "auth/token/renew/*" {
|
176
|
+
policy = "write"
|
177
|
+
}
|
92
178
|
|
93
|
-
|
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
|
-
|
31
|
-
|
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}_#{
|
41
|
+
key = options[:key] || "#{Vault::Rails.application}_#{table_name}_#{attribute}"
|
34
42
|
|
35
|
-
|
36
|
-
|
37
|
-
value = instance_variable_get(:@#{column})
|
38
|
-
return value if !value.nil?
|
43
|
+
# Sanity check options!
|
44
|
+
_vault_validate_options!(options)
|
39
45
|
|
40
|
-
|
41
|
-
|
46
|
+
# Get the serializer if one was given.
|
47
|
+
serializer = options[:serialize]
|
42
48
|
|
43
|
-
|
44
|
-
|
45
|
-
|
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
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
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
|
-
|
54
|
-
|
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
|
-
|
102
|
+
end
|
57
103
|
|
58
|
-
|
59
|
-
|
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
|
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
|