kms_encrypted 0.3.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -0
- data/LICENSE.txt +1 -1
- data/README.md +40 -13
- data/lib/kms_encrypted/client.rb +69 -0
- data/lib/kms_encrypted/clients/aws.rb +36 -0
- data/lib/kms_encrypted/clients/base.rb +45 -0
- data/lib/kms_encrypted/clients/google.rb +40 -0
- data/lib/kms_encrypted/clients/test.rb +29 -0
- data/lib/kms_encrypted/clients/vault.rb +48 -0
- data/lib/kms_encrypted/database.rb +93 -0
- data/lib/kms_encrypted/log_subscriber.rb +14 -6
- data/lib/kms_encrypted/model.rb +70 -122
- data/lib/kms_encrypted/version.rb +1 -1
- data/lib/kms_encrypted.rb +26 -2
- metadata +13 -15
- data/.gitignore +0 -9
- data/.travis.yml +0 -11
- data/Gemfile +0 -3
- data/Rakefile +0 -11
- data/guides/Amazon.md +0 -262
- data/guides/Google.md +0 -131
- data/guides/Vault.md +0 -143
- data/kms_encrypted.gemspec +0 -34
data/lib/kms_encrypted/model.rb
CHANGED
@@ -1,9 +1,11 @@
|
|
1
1
|
module KmsEncrypted
|
2
2
|
module Model
|
3
|
-
def has_kms_key(
|
4
|
-
key_id ||=
|
3
|
+
def has_kms_key(name: nil, key_id: nil, eager_encrypt: false, version: 1, previous_versions: nil, upgrade_context: false)
|
4
|
+
key_id ||= ENV["KMS_KEY_ID"]
|
5
5
|
|
6
6
|
key_method = name ? "kms_key_#{name}" : "kms_key"
|
7
|
+
key_column = "encrypted_#{key_method}"
|
8
|
+
context_method = name ? "kms_encryption_context_#{name}" : "kms_encryption_context"
|
7
9
|
|
8
10
|
class_eval do
|
9
11
|
class << self
|
@@ -11,143 +13,89 @@ module KmsEncrypted
|
|
11
13
|
@kms_keys ||= {}
|
12
14
|
end unless respond_to?(:kms_keys)
|
13
15
|
end
|
14
|
-
kms_keys[key_method.to_sym] = {
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
16
|
+
kms_keys[key_method.to_sym] = {
|
17
|
+
key_id: key_id,
|
18
|
+
name: name,
|
19
|
+
version: version,
|
20
|
+
previous_versions: previous_versions,
|
21
|
+
upgrade_context: upgrade_context
|
22
|
+
}
|
23
|
+
|
24
|
+
if kms_keys.size == 1
|
25
|
+
after_save :encrypt_kms_keys
|
26
|
+
|
27
|
+
# fetch all keys together so only need to update database once
|
28
|
+
def encrypt_kms_keys
|
29
|
+
updates = {}
|
30
|
+
self.class.kms_keys.each do |key_method, key|
|
31
|
+
instance_var = "@#{key_method}"
|
32
|
+
key_column = "encrypted_#{key_method}"
|
33
|
+
plaintext_key = instance_variable_get(instance_var)
|
34
|
+
|
35
|
+
if !send(key_column) && plaintext_key
|
36
|
+
updates[key_column] = KmsEncrypted::Database.new(self, key_method).encrypt(plaintext_key)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
if updates.any?
|
40
|
+
current_time = current_time_from_proper_timezone
|
41
|
+
timestamp_attributes_for_update_in_model.each do |attr|
|
42
|
+
updates[attr] = current_time
|
43
|
+
end
|
44
|
+
update_columns(updates)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# same pattern as attr_encrypted reload
|
49
|
+
if method_defined?(:reload)
|
50
|
+
alias_method :reload_without_kms_encrypted, :reload
|
51
|
+
def reload(*args, &block)
|
52
|
+
result = reload_without_kms_encrypted(*args, &block)
|
53
|
+
self.class.kms_keys.keys.each do |key_method|
|
54
|
+
instance_variable_set("@#{key_method}", nil)
|
55
|
+
end
|
56
|
+
result
|
23
57
|
end
|
24
|
-
result
|
25
58
|
end
|
26
59
|
end
|
27
60
|
|
28
61
|
define_method(key_method) do
|
29
|
-
raise ArgumentError, "Missing key id" unless key_id
|
30
|
-
|
31
62
|
instance_var = "@#{key_method}"
|
32
63
|
|
33
64
|
unless instance_variable_get(instance_var)
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
key_id: key_id,
|
45
|
-
context: context
|
46
|
-
}
|
47
|
-
ActiveSupport::Notifications.instrument("generate_data_key.kms_encrypted", event) do
|
48
|
-
if key_id == "insecure-test-key"
|
49
|
-
plaintext_key = "00000000000000000000000000000000"
|
50
|
-
encrypted_key = "insecure-data-key-#{rand(1_000_000_000_000)}"
|
51
|
-
elsif key_id.start_with?("projects/")
|
52
|
-
# generate random AES-256 key
|
53
|
-
plaintext_key = OpenSSL::Random.random_bytes(32)
|
54
|
-
|
55
|
-
# encrypt it
|
56
|
-
# load client first to ensure namespace is loaded
|
57
|
-
client = KmsEncrypted.google_client
|
58
|
-
request = ::Google::Apis::CloudkmsV1::EncryptRequest.new(
|
59
|
-
plaintext: plaintext_key,
|
60
|
-
additional_authenticated_data: context.to_json
|
61
|
-
)
|
62
|
-
response = client.encrypt_crypto_key(key_id, request)
|
63
|
-
key_version = response.name
|
64
|
-
|
65
|
-
# shorten key to save space
|
66
|
-
short_key_id = Base64.encode64(key_version.split("/").select.with_index { |_, i| i.odd? }.join("/"))
|
67
|
-
|
68
|
-
# build encrypted key
|
69
|
-
# we reference the key in the field for easy rotation
|
70
|
-
encrypted_key = "$gc$#{short_key_id}$#{[response.ciphertext].pack(default_encoding)}"
|
71
|
-
elsif key_id.start_with?("vault/")
|
72
|
-
# generate random AES-256 key
|
73
|
-
plaintext_key = OpenSSL::Random.random_bytes(32)
|
74
|
-
|
75
|
-
# encrypt it
|
76
|
-
response = KmsEncrypted.vault_client.logical.write(
|
77
|
-
"transit/encrypt/#{key_id.sub("vault/", "")}",
|
78
|
-
plaintext: Base64.encode64(plaintext_key),
|
79
|
-
context: Base64.encode64(context.to_json)
|
80
|
-
)
|
81
|
-
|
82
|
-
encrypted_key = response.data[:ciphertext]
|
83
|
-
else
|
84
|
-
# generate data key from API
|
85
|
-
resp = KmsEncrypted.aws_client.generate_data_key(
|
86
|
-
key_id: key_id,
|
87
|
-
encryption_context: context,
|
88
|
-
key_spec: "AES_256"
|
89
|
-
)
|
90
|
-
plaintext_key = resp.plaintext
|
91
|
-
encrypted_key = [resp.ciphertext_blob].pack(default_encoding)
|
65
|
+
encrypted_key = send(key_column)
|
66
|
+
plaintext_key =
|
67
|
+
if encrypted_key
|
68
|
+
KmsEncrypted::Database.new(self, key_method).decrypt(encrypted_key)
|
69
|
+
else
|
70
|
+
key = SecureRandom.random_bytes(32)
|
71
|
+
|
72
|
+
if eager_encrypt == :fetch_id
|
73
|
+
raise ArgumentError, ":fetch_id only works with Postgres" unless self.class.connection.adapter_name =~ /postg/i
|
74
|
+
self.id ||= self.class.connection.execute("select nextval('#{self.class.sequence_name}')").first["nextval"]
|
92
75
|
end
|
93
|
-
end
|
94
76
|
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
unless instance_variable_get(instance_var)
|
100
|
-
encrypted_key = send(key_column)
|
101
|
-
plaintext_key = nil
|
102
|
-
|
103
|
-
event = {
|
104
|
-
key_id: key_id,
|
105
|
-
context: context
|
106
|
-
}
|
107
|
-
ActiveSupport::Notifications.instrument("decrypt_data_key.kms_encrypted", event) do
|
108
|
-
if encrypted_key.start_with?("insecure-data-key-")
|
109
|
-
plaintext_key = "00000000000000000000000000000000".encode("BINARY")
|
110
|
-
elsif encrypted_key.start_with?("$gc$")
|
111
|
-
_, _, short_key_id, ciphertext = encrypted_key.split("$", 4)
|
112
|
-
|
113
|
-
# restore key, except for cryptoKeyVersion
|
114
|
-
stored_key_id = Base64.decode64(short_key_id).split("/")[0..3]
|
115
|
-
stored_key_id.insert(0, "projects")
|
116
|
-
stored_key_id.insert(2, "locations")
|
117
|
-
stored_key_id.insert(4, "keyRings")
|
118
|
-
stored_key_id.insert(6, "cryptoKeys")
|
119
|
-
stored_key_id = stored_key_id.join("/")
|
120
|
-
|
121
|
-
# load client first to ensure namespace is loaded
|
122
|
-
client = KmsEncrypted.google_client
|
123
|
-
request = ::Google::Apis::CloudkmsV1::DecryptRequest.new(
|
124
|
-
ciphertext: ciphertext.unpack(default_encoding).first,
|
125
|
-
additional_authenticated_data: context.to_json
|
126
|
-
)
|
127
|
-
plaintext_key = client.decrypt_crypto_key(stored_key_id, request).plaintext
|
128
|
-
elsif encrypted_key.start_with?("vault:")
|
129
|
-
response = KmsEncrypted.vault_client.logical.write(
|
130
|
-
"transit/decrypt/#{key_id.sub("vault/", "")}",
|
131
|
-
ciphertext: encrypted_key,
|
132
|
-
context: Base64.encode64(context.to_json)
|
133
|
-
)
|
134
|
-
|
135
|
-
plaintext_key = Base64.decode64(response.data[:plaintext])
|
136
|
-
else
|
137
|
-
plaintext_key = KmsEncrypted.aws_client.decrypt(
|
138
|
-
ciphertext_blob: encrypted_key.unpack(default_encoding).first,
|
139
|
-
encryption_context: context
|
140
|
-
).plaintext
|
77
|
+
if eager_encrypt == true || ([:try, :fetch_id].include?(eager_encrypt) && id)
|
78
|
+
encrypted_key = KmsEncrypted::Database.new(self, key_method).encrypt(key)
|
79
|
+
send("#{key_column}=", encrypted_key)
|
141
80
|
end
|
142
|
-
end
|
143
81
|
|
144
|
-
|
145
|
-
|
82
|
+
key
|
83
|
+
end
|
84
|
+
instance_variable_set(instance_var, plaintext_key)
|
146
85
|
end
|
147
86
|
|
148
87
|
instance_variable_get(instance_var)
|
149
88
|
end
|
150
89
|
|
90
|
+
define_method(context_method) do
|
91
|
+
raise KmsEncrypted::Error, "id needed for encryption context" unless id
|
92
|
+
|
93
|
+
{
|
94
|
+
model_name: model_name.to_s,
|
95
|
+
model_id: id
|
96
|
+
}
|
97
|
+
end
|
98
|
+
|
151
99
|
define_method("rotate_#{key_method}!") do
|
152
100
|
# decrypt
|
153
101
|
plaintext_attributes = {}
|
data/lib/kms_encrypted.rb
CHANGED
@@ -1,12 +1,27 @@
|
|
1
1
|
# dependencies
|
2
2
|
require "active_support"
|
3
|
+
require "base64"
|
4
|
+
require "json"
|
5
|
+
require "securerandom"
|
3
6
|
|
4
7
|
# modules
|
8
|
+
require "kms_encrypted/database"
|
5
9
|
require "kms_encrypted/log_subscriber"
|
6
10
|
require "kms_encrypted/model"
|
7
11
|
require "kms_encrypted/version"
|
8
12
|
|
13
|
+
# clients
|
14
|
+
require "kms_encrypted/client"
|
15
|
+
require "kms_encrypted/clients/base"
|
16
|
+
require "kms_encrypted/clients/aws"
|
17
|
+
require "kms_encrypted/clients/google"
|
18
|
+
require "kms_encrypted/clients/test"
|
19
|
+
require "kms_encrypted/clients/vault"
|
20
|
+
|
9
21
|
module KmsEncrypted
|
22
|
+
class Error < StandardError; end
|
23
|
+
class DecryptionError < Error; end
|
24
|
+
|
10
25
|
class << self
|
11
26
|
attr_writer :aws_client
|
12
27
|
attr_writer :google_client
|
@@ -14,7 +29,7 @@ module KmsEncrypted
|
|
14
29
|
|
15
30
|
def aws_client
|
16
31
|
@aws_client ||= Aws::KMS::Client.new(
|
17
|
-
retry_limit:
|
32
|
+
retry_limit: 1,
|
18
33
|
http_open_timeout: 2,
|
19
34
|
http_read_timeout: 2
|
20
35
|
)
|
@@ -27,12 +42,21 @@ module KmsEncrypted
|
|
27
42
|
client.authorization = ::Google::Auth.get_application_default(
|
28
43
|
"https://www.googleapis.com/auth/cloud-platform"
|
29
44
|
)
|
45
|
+
client.client_options.log_http_requests = false
|
46
|
+
client.client_options.open_timeout_sec = 2
|
47
|
+
client.client_options.read_timeout_sec = 2
|
30
48
|
client
|
31
49
|
end
|
32
50
|
end
|
33
51
|
|
34
52
|
def vault_client
|
35
|
-
@vault_client ||= ::Vault
|
53
|
+
@vault_client ||= ::Vault::Client.new
|
54
|
+
end
|
55
|
+
|
56
|
+
# hash is independent of key, but specific to audit device
|
57
|
+
def context_hash(context, path:)
|
58
|
+
context = Base64.encode64(context.to_json)
|
59
|
+
vault_client.logical.write("sys/audit-hash/#{path}", input: context).data[:hash]
|
36
60
|
end
|
37
61
|
end
|
38
62
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: kms_encrypted
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Kane
|
8
8
|
autorequire:
|
9
|
-
bindir:
|
9
|
+
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-
|
11
|
+
date: 2018-12-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -151,24 +151,22 @@ dependencies:
|
|
151
151
|
- !ruby/object:Gem::Version
|
152
152
|
version: '0'
|
153
153
|
description:
|
154
|
-
email:
|
155
|
-
- andrew@chartkick.com
|
154
|
+
email: andrew@chartkick.com
|
156
155
|
executables: []
|
157
156
|
extensions: []
|
158
157
|
extra_rdoc_files: []
|
159
158
|
files:
|
160
|
-
- ".gitignore"
|
161
|
-
- ".travis.yml"
|
162
159
|
- CHANGELOG.md
|
163
|
-
- Gemfile
|
164
160
|
- LICENSE.txt
|
165
161
|
- README.md
|
166
|
-
- Rakefile
|
167
|
-
- guides/Amazon.md
|
168
|
-
- guides/Google.md
|
169
|
-
- guides/Vault.md
|
170
|
-
- kms_encrypted.gemspec
|
171
162
|
- lib/kms_encrypted.rb
|
163
|
+
- lib/kms_encrypted/client.rb
|
164
|
+
- lib/kms_encrypted/clients/aws.rb
|
165
|
+
- lib/kms_encrypted/clients/base.rb
|
166
|
+
- lib/kms_encrypted/clients/google.rb
|
167
|
+
- lib/kms_encrypted/clients/test.rb
|
168
|
+
- lib/kms_encrypted/clients/vault.rb
|
169
|
+
- lib/kms_encrypted/database.rb
|
172
170
|
- lib/kms_encrypted/log_subscriber.rb
|
173
171
|
- lib/kms_encrypted/model.rb
|
174
172
|
- lib/kms_encrypted/version.rb
|
@@ -184,7 +182,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
184
182
|
requirements:
|
185
183
|
- - ">="
|
186
184
|
- !ruby/object:Gem::Version
|
187
|
-
version: '
|
185
|
+
version: '2.2'
|
188
186
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
189
187
|
requirements:
|
190
188
|
- - ">="
|
@@ -192,7 +190,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
192
190
|
version: '0'
|
193
191
|
requirements: []
|
194
192
|
rubyforge_project:
|
195
|
-
rubygems_version: 2.7.
|
193
|
+
rubygems_version: 2.7.6
|
196
194
|
signing_key:
|
197
195
|
specification_version: 4
|
198
196
|
summary: Simple, secure key management for attr_encrypted
|
data/.gitignore
DELETED
data/.travis.yml
DELETED
data/Gemfile
DELETED
data/Rakefile
DELETED
data/guides/Amazon.md
DELETED
@@ -1,262 +0,0 @@
|
|
1
|
-
# Amazon KMS
|
2
|
-
|
3
|
-
Add this line to your application’s Gemfile:
|
4
|
-
|
5
|
-
```ruby
|
6
|
-
gem 'aws-sdk-kms'
|
7
|
-
gem 'kms_encrypted'
|
8
|
-
```
|
9
|
-
|
10
|
-
Add columns for the encrypted data and the encrypted KMS data keys
|
11
|
-
|
12
|
-
```ruby
|
13
|
-
add_column :users, :encrypted_email, :text
|
14
|
-
add_column :users, :encrypted_email_iv, :text
|
15
|
-
add_column :users, :encrypted_kms_key, :text
|
16
|
-
```
|
17
|
-
|
18
|
-
Create an [Amazon Web Services](https://aws.amazon.com/) account if you don’t have one. KMS works great whether or not you run your infrastructure on AWS.
|
19
|
-
|
20
|
-
Create a [KMS master key](https://console.aws.amazon.com/iam/home#/encryptionKeys) and set it in your environment along with your AWS credentials ([dotenv](https://github.com/bkeepers/dotenv) is great for this)
|
21
|
-
|
22
|
-
```sh
|
23
|
-
KMS_KEY_ID=arn:aws:kms:...
|
24
|
-
AWS_ACCESS_KEY_ID=...
|
25
|
-
AWS_SECRET_ACCESS_KEY=...
|
26
|
-
```
|
27
|
-
|
28
|
-
You can also use the alias
|
29
|
-
|
30
|
-
```sh
|
31
|
-
KMS_KEY_ID=alias/my-alias
|
32
|
-
```
|
33
|
-
|
34
|
-
And update your model
|
35
|
-
|
36
|
-
```ruby
|
37
|
-
class User < ApplicationRecord
|
38
|
-
has_kms_key
|
39
|
-
|
40
|
-
attr_encrypted :email, key: :kms_key
|
41
|
-
end
|
42
|
-
```
|
43
|
-
|
44
|
-
For each encrypted attribute, use the `kms_key` method for its key.
|
45
|
-
|
46
|
-
## Auditing
|
47
|
-
|
48
|
-
[AWS CloudTrail](https://aws.amazon.com/cloudtrail/) logs all decryption calls. However, to know what data is being decrypted, you’ll need to add context.
|
49
|
-
|
50
|
-
Add a `kms_encryption_context` method to your model.
|
51
|
-
|
52
|
-
```ruby
|
53
|
-
class User < ApplicationRecord
|
54
|
-
def kms_encryption_context
|
55
|
-
# some hash
|
56
|
-
end
|
57
|
-
end
|
58
|
-
```
|
59
|
-
|
60
|
-
The context is used as part of the encryption and decryption process, so it must be a value that doesn’t change. Otherwise, you won’t be able to decrypt. Read more about [encryption context here](https://docs.aws.amazon.com/kms/latest/developerguide/encryption-context.html).
|
61
|
-
|
62
|
-
The primary key is a good choice, but auto-generated ids aren’t available until a record is created, and we need to encrypt before this. One solution is to preload the primary key. Here’s what it looks like with Postgres:
|
63
|
-
|
64
|
-
```ruby
|
65
|
-
class User < ApplicationRecord
|
66
|
-
def kms_encryption_context
|
67
|
-
self.id ||= self.class.connection.execute("select nextval('#{self.class.sequence_name}')").first["nextval"]
|
68
|
-
{"Record" => "#{model_name}/#{id}"}
|
69
|
-
end
|
70
|
-
end
|
71
|
-
```
|
72
|
-
|
73
|
-
[Amazon Athena](https://aws.amazon.com/athena/) is great for querying CloudTrail logs. Create a table (thanks to [this post](http://www.1strategy.com/blog/2017/07/25/auditing-aws-activity-with-cloudtrail-and-athena/) for the table structure) with:
|
74
|
-
|
75
|
-
```sql
|
76
|
-
CREATE EXTERNAL TABLE cloudtrail_logs (
|
77
|
-
eventversion STRING,
|
78
|
-
userIdentity STRUCT<
|
79
|
-
type:STRING,
|
80
|
-
principalid:STRING,
|
81
|
-
arn:STRING,
|
82
|
-
accountid:STRING,
|
83
|
-
invokedby:STRING,
|
84
|
-
accesskeyid:STRING,
|
85
|
-
userName:String,
|
86
|
-
sessioncontext:STRUCT<
|
87
|
-
attributes:STRUCT<
|
88
|
-
mfaauthenticated:STRING,
|
89
|
-
creationdate:STRING>,
|
90
|
-
sessionIssuer:STRUCT<
|
91
|
-
type:STRING,
|
92
|
-
principalId:STRING,
|
93
|
-
arn:STRING,
|
94
|
-
accountId:STRING,
|
95
|
-
userName:STRING>>>,
|
96
|
-
eventTime STRING,
|
97
|
-
eventSource STRING,
|
98
|
-
eventName STRING,
|
99
|
-
awsRegion STRING,
|
100
|
-
sourceIpAddress STRING,
|
101
|
-
userAgent STRING,
|
102
|
-
errorCode STRING,
|
103
|
-
errorMessage STRING,
|
104
|
-
requestId STRING,
|
105
|
-
eventId STRING,
|
106
|
-
resources ARRAY<STRUCT<
|
107
|
-
ARN:STRING,
|
108
|
-
accountId:STRING,
|
109
|
-
type:STRING>>,
|
110
|
-
eventType STRING,
|
111
|
-
apiVersion STRING,
|
112
|
-
readOnly BOOLEAN,
|
113
|
-
recipientAccountId STRING,
|
114
|
-
sharedEventID STRING,
|
115
|
-
vpcEndpointId STRING,
|
116
|
-
requestParameters STRING,
|
117
|
-
responseElements STRING,
|
118
|
-
additionalEventData STRING,
|
119
|
-
serviceEventDetails STRING
|
120
|
-
)
|
121
|
-
ROW FORMAT SERDE 'com.amazon.emr.hive.serde.CloudTrailSerde'
|
122
|
-
STORED AS INPUTFORMAT 'com.amazon.emr.cloudtrail.CloudTrailInputFormat'
|
123
|
-
OUTPUTFORMAT 'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat'
|
124
|
-
LOCATION 's3://my-cloudtrail-logs/'
|
125
|
-
```
|
126
|
-
|
127
|
-
Change the last line to point to your CloudTrail log bucket and query away
|
128
|
-
|
129
|
-
```sql
|
130
|
-
SELECT
|
131
|
-
eventTime,
|
132
|
-
userIdentity.userName,
|
133
|
-
requestParameters
|
134
|
-
FROM
|
135
|
-
cloudtrail_logs
|
136
|
-
WHERE
|
137
|
-
eventName = 'Decrypt'
|
138
|
-
AND resources[1].arn = 'arn:aws:kms:...'
|
139
|
-
ORDER BY 1
|
140
|
-
```
|
141
|
-
|
142
|
-
There will also be `GenerateDataKey` events.
|
143
|
-
|
144
|
-
## Alerting
|
145
|
-
|
146
|
-
We recommend setting up alerts on suspicious behavior.
|
147
|
-
|
148
|
-
## Key Rotation
|
149
|
-
|
150
|
-
KMS supports [automatic key rotation](https://docs.aws.amazon.com/kms/latest/developerguide/rotate-keys.html). No action is required in this case.
|
151
|
-
|
152
|
-
To manually rotate keys, replace the old KMS key id with the new key id in your model. Your app does not need the old key id to perform rotation (however, the key must still be enabled in your AWS account).
|
153
|
-
|
154
|
-
```sh
|
155
|
-
KMS_KEY_ID=arn:aws:kms:...
|
156
|
-
```
|
157
|
-
|
158
|
-
and run
|
159
|
-
|
160
|
-
```ruby
|
161
|
-
User.find_each do |user|
|
162
|
-
user.rotate_kms_key!
|
163
|
-
end
|
164
|
-
```
|
165
|
-
|
166
|
-
## IAM Permissions
|
167
|
-
|
168
|
-
A great feature of KMS is the ability to grant encryption and decryption permission separately.
|
169
|
-
|
170
|
-
To encrypt the data, use a policy with:
|
171
|
-
|
172
|
-
```json
|
173
|
-
{
|
174
|
-
"Version": "2012-10-17",
|
175
|
-
"Statement": [
|
176
|
-
{
|
177
|
-
"Sid": "EncryptData",
|
178
|
-
"Effect": "Allow",
|
179
|
-
"Action": "kms:GenerateDataKey",
|
180
|
-
"Resource": "arn:aws:kms:..."
|
181
|
-
}
|
182
|
-
]
|
183
|
-
}
|
184
|
-
```
|
185
|
-
|
186
|
-
If a system can only encrypt, you must clear out existing data and data keys before updates.
|
187
|
-
|
188
|
-
```ruby
|
189
|
-
user.encrypted_email = nil
|
190
|
-
user.encrypted_kms_key = nil
|
191
|
-
# before user.save or user.update
|
192
|
-
```
|
193
|
-
|
194
|
-
To decrypt the data, use a policy with:
|
195
|
-
|
196
|
-
```json
|
197
|
-
{
|
198
|
-
"Version": "2012-10-17",
|
199
|
-
"Statement": [
|
200
|
-
{
|
201
|
-
"Sid": "DecryptData",
|
202
|
-
"Effect": "Allow",
|
203
|
-
"Action": "kms:Decrypt",
|
204
|
-
"Resource": "arn:aws:kms:..."
|
205
|
-
}
|
206
|
-
]
|
207
|
-
}
|
208
|
-
```
|
209
|
-
|
210
|
-
Be extremely selective of systems you allow to decrypt.
|
211
|
-
|
212
|
-
## Testing
|
213
|
-
|
214
|
-
For testing, you can prevent network calls to KMS by setting:
|
215
|
-
|
216
|
-
```sh
|
217
|
-
KMS_KEY_ID=insecure-test-key
|
218
|
-
```
|
219
|
-
|
220
|
-
## Multiple Keys Per Record
|
221
|
-
|
222
|
-
You may want to protect different columns with different data keys (or even master keys).
|
223
|
-
|
224
|
-
To do this, add more columns
|
225
|
-
|
226
|
-
```ruby
|
227
|
-
add_column :users, :encrypted_phone, :text
|
228
|
-
add_column :users, :encrypted_phone_iv, :text
|
229
|
-
add_column :users, :encrypted_kms_key_phone, :text
|
230
|
-
```
|
231
|
-
|
232
|
-
And update your model
|
233
|
-
|
234
|
-
```ruby
|
235
|
-
class User < ApplicationRecord
|
236
|
-
has_kms_key
|
237
|
-
has_kms_key name: :phone, key_id: "..."
|
238
|
-
|
239
|
-
attr_encrypted :email, key: :kms_key
|
240
|
-
attr_encrypted :phone, key: :kms_key_phone
|
241
|
-
end
|
242
|
-
```
|
243
|
-
|
244
|
-
For context, use:
|
245
|
-
|
246
|
-
```ruby
|
247
|
-
class User < ApplicationRecord
|
248
|
-
def kms_encryption_context_phone
|
249
|
-
# some hash
|
250
|
-
end
|
251
|
-
end
|
252
|
-
```
|
253
|
-
|
254
|
-
To rotate keys, use:
|
255
|
-
|
256
|
-
```ruby
|
257
|
-
user.rotate_kms_key_phone!
|
258
|
-
```
|
259
|
-
|
260
|
-
## File Uploads
|
261
|
-
|
262
|
-
While outside the scope of this gem, you can also use KMS for sensitive file uploads. Check out [this guide](https://ankane.org/aws-client-side-encryption) to learn more.
|