Skip to content

Commit abbf629

Browse files
authored
(GH-69) Add code and tests for new requires confine_to_keys options (#75)
Adding confine_to_keys option just like modules are doing in order to reduce the number of unnecessary calls to the secrets vault. This helps resolve the rate limiting issue and slowness people are experiencing
1 parent d1c4836 commit abbf629

File tree

3 files changed

+89
-10
lines changed

3 files changed

+89
-10
lines changed

README.md

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ In the above example the api_versions hash is important. It is pinning both of
5757

5858
This module contains a Hiera 5 backend that allows you to securely retrieve secrets from Azure key vault and use them in hiera.
5959

60-
Add a new entry to the `hierarchy` hash in `hiera.yaml` referencing the vault name and API versions:
60+
Add a new entry to the `hierarchy` hash in `hiera.yaml` providing the following required lookup options:
6161

6262
```yaml
6363
- name: 'Azure Key Vault Secrets'
@@ -67,6 +67,10 @@ Add a new entry to the `hierarchy` hash in `hiera.yaml` referencing the vault na
6767
vault_api_version: '2016-10-01'
6868
metadata_api_version: '2018-04-02'
6969
key_replacement_token: '-'
70+
confine_to_keys:
71+
- '^azure_.*'
72+
- '^.*_password$'
73+
- '^password.*'
7074
```
7175
7276
To retrieve a secret in puppet code you can use the `lookup` function:
@@ -96,9 +100,35 @@ Alternatively a custom trusted fact can be included [in the certificate request]
96100
vault_api_version: '2016-10-01'
97101
metadata_api_version: '2018-04-02'
98102
key_replacement_token: '-'
103+
confine_to_keys:
104+
- '^azure_.*'
105+
- '^.*_password$'
106+
- '^password.*'
99107
```
100108

101-
### A note on secret keys
109+
### What is confine_to_keys?
110+
111+
By design, hiera will traverse the configured heiarchy for a given key until one is found. This means that there can be a potentially large number of web requests against azure key vault. In order to improve performance and prevent hitting the Azure KeyVault rate limits ( ex: currently there is a maximum of 2,000 lookups every 10 seconds allowed against a key vault), the confine_to_keys allows you to provide an array of regexs that help avoid making a remote call.
112+
113+
As an example, if you defined your confine_to_keys as shown below, hiera will only make a web request to get the secret in azure key vault when the key being lookedup matches atleast one of the provided regular expressions in the confine_to_keys array.
114+
115+
```yaml
116+
- name: 'Azure Key Vault Secrets from trusted fact'
117+
lookup_key: azure_key_vault::lookup
118+
options:
119+
vault_name: "%{trusted.extensions.pp_environment}"
120+
vault_api_version: '2016-10-01'
121+
metadata_api_version: '2018-04-02'
122+
key_replacement_token: '-'
123+
confine_to_keys:
124+
- '^azure_.*'
125+
- '^.*_password$'
126+
- '^password.*'
127+
```
128+
129+
**NOTE: The confine_to_keys is very important to make you sure get right. As a best practice, come up with some conventions to avoid having a large number of regexs you have to add/update/remove and also test. The above example provides a great starting point.**
130+
131+
### What is key_replacement_token?
102132

103133
KeyVault secret names can only contain the characters `0-9`, `a-z`, `A-Z`, and `-`.
104134

lib/puppet/functions/azure_key_vault/lookup.rb

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,26 @@
33
Puppet::Functions.create_function(:'azure_key_vault::lookup') do
44
dispatch :lookup_key do
55
param 'Variant[String, Numeric]', :secret_name
6-
param 'Struct[{vault_name => String, vault_api_version => String, metadata_api_version => String, Optional[key_replacement_token] => String}]', :options
6+
param 'Struct[{vault_name => String, vault_api_version => String, metadata_api_version => String, confine_to_keys => Array[Regexp], Optional[key_replacement_token] => String}]', :options
77
param 'Puppet::LookupContext', :context
88
end
99

1010
def lookup_key(secret_name, options, context)
1111
# This is a reserved key name in hiera
1212
return context.not_found if secret_name == 'lookup_options'
13+
14+
confine_keys = options['confine_to_keys']
15+
if confine_keys
16+
raise ArgumentError, 'confine_to_keys must be an array' unless confine_keys.is_a?(Array)
17+
18+
regex_key_match = Regexp.union(confine_keys)
19+
20+
unless secret_name[regex_key_match] == secret_name
21+
context.explain { "Skipping azure_key_vault backend because secret_name '#{secret_name}' does not match confine_to_keys" }
22+
context.not_found
23+
end
24+
end
25+
1326
normalized_secret_name = TragicCode::Azure.normalize_object_name(secret_name, options['key_replacement_token'] || '-')
1427
context.explain { " Using normalized KeyVault secret key for lookup: #{normalized_secret_name}" }
1528
return context.cached_value(normalized_secret_name) if context.cache_has_key(normalized_secret_name)

spec/functions/azure_key_vault_lookup_spec.rb

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
'vault_name' => 'vault_name',
77
'vault_api_version' => 'vault_api_version',
88
'metadata_api_version' => 'metadata_api_version',
9+
'confine_to_keys' => [%r{^.*sensitive_azure.*}],
910
}
1011
end
1112
let(:lookup_context) do
@@ -28,14 +29,14 @@
2829
end
2930
it 'validates the :options hash' do
3031
is_expected.to run.with_params(
31-
'secret_name', { 'key1' => 'value1' }, lookup_context
32+
'profile::windows::sqlserver::sensitive_azure_sql_user_password', { 'key1' => 'value1' }, lookup_context
3233
).and_raise_error(ArgumentError)
3334
end
3435
it 'uses the cache' do
35-
expect(lookup_context).to receive(:cache_has_key).with('secret-name').and_return(true)
36-
expect(lookup_context).to receive(:cached_value).with('secret-name').and_return('value')
36+
expect(lookup_context).to receive(:cache_has_key).with('profile--windows--sqlserver--sensitive-azure-sql-user-password').and_return(true)
37+
expect(lookup_context).to receive(:cached_value).with('profile--windows--sqlserver--sensitive-azure-sql-user-password').and_return('value')
3738
is_expected.to run.with_params(
38-
'secret_name', options, lookup_context
39+
'profile::windows::sqlserver::sensitive_azure_sql_user_password', options, lookup_context
3940
).and_return('value')
4041
end
4142
it 'caches the access token after a cache miss' do
@@ -48,7 +49,7 @@
4849
expect(TragicCode::Azure).to receive(:get_secret).and_return(secret_value)
4950
expect(lookup_context).to receive(:cache).and_return(secret_value).ordered
5051
is_expected.to run.with_params(
51-
'secret_name', options, lookup_context
52+
'profile::windows::sqlserver::sensitive_azure_sql_user_password', options, lookup_context
5253
).and_return(secret_value)
5354
end
5455

@@ -60,14 +61,49 @@
6061
end
6162

6263
it 'uses - as the default key_replacement_token' do
63-
secret_name = 'profile::windows::sqlserver::sensitive_sql_user_password'
64+
secret_name = 'profile::windows::sqlserver::sensitive_azure_sql_user_password'
6465
access_token_value = 'access_value'
6566
secret_value = 'secret_value'
6667
expect(TragicCode::Azure).to receive(:normalize_object_name).with(secret_name, '-')
6768
expect(TragicCode::Azure).to receive(:get_access_token).and_return(access_token_value)
6869
expect(TragicCode::Azure).to receive(:get_secret).and_return(secret_value)
6970
is_expected.to run.with_params(
70-
'profile::windows::sqlserver::sensitive_sql_user_password', options, lookup_context
71+
'profile::windows::sqlserver::sensitive_azure_sql_user_password', options, lookup_context
7172
).and_return(secret_value)
7273
end
74+
75+
it 'errors when confine_to_keys is no array' do
76+
is_expected.to run.with_params(
77+
'profile::windows::sqlserver::sensitive_azure_sql_user_password', options.merge({ 'confine_to_keys' => '^vault.*$' }), lookup_context
78+
).and_raise_error(ArgumentError, %r{'confine_to_keys' expects an Array value}i)
79+
end
80+
81+
it 'errors when passing invalid regexes' do
82+
is_expected.to run.with_params(
83+
'profile::windows::sqlserver::sensitive_azure_sql_user_password', options.merge({ 'confine_to_keys' => ['['] }), lookup_context
84+
).and_raise_error(ArgumentError, %r{'confine_to_keys' index 0 expects a Regexp value}i)
85+
end
86+
87+
it 'returns the key if regex matches confine_to_keys' do
88+
access_token_value = 'access_value'
89+
secret_value = 'secret_value'
90+
expect(TragicCode::Azure).to receive(:get_access_token).and_return(access_token_value)
91+
expect(TragicCode::Azure).to receive(:get_secret).and_return(secret_value)
92+
is_expected.to run.with_params(
93+
'profile::windows::sqlserver::sensitive_azure_sql_user_password', options.merge({ 'confine_to_keys' => [%r{^.*sensitive_azure.*}] }), lookup_context
94+
).and_return(secret_value)
95+
end
96+
97+
it 'does not return the key if regex does not match confine_to_keys' do
98+
access_token_value = 'access_value'
99+
secret_value = 'secret_value'
100+
101+
expect(lookup_context).to receive(:not_found)
102+
expect(TragicCode::Azure).to receive(:get_access_token).and_return(access_token_value)
103+
expect(TragicCode::Azure).to receive(:get_secret).and_return(secret_value)
104+
105+
is_expected.to run.with_params(
106+
'profile::windows::sqlserver::sensitive_sql_user_password', options.merge({ 'confine_to_keys' => [%r{^sensitive_azure.*$}] }), lookup_context
107+
)
108+
end
73109
end

0 commit comments

Comments
 (0)