6 changes: 5 additions & 1 deletion .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ AllCops:
Metrics/LineLength:
Description: People have wide screens, use them.
Max: 200
GetText/DecorateString:
Description: We don't want to decorate test output.
Exclude:
- spec/*
RSpec/BeforeAfterAll:
Description: Beware of using after(:all) as it may cause state to leak between tests.
A necessary evil in acceptance testing.
Expand All @@ -33,7 +37,7 @@ Style/BlockDelimiters:
EnforcedStyle: braces_for_chaining
Style/ClassAndModuleChildren:
Description: Compact style reduces the required amount of indentation.
EnforcedStyle: compact
EnforcedStyle: nested
Style/EmptyElse:
Description: Enforce against empty else clauses, but allow `nil` for clarity.
EnforcedStyle: empty
Expand Down
13 changes: 10 additions & 3 deletions .sync.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,24 @@ Gemfile:
ref: '20ee04ba1234e9e83eb2ffb5056e23d641c7a018'
condition: "Gem::Version.new(RUBY_VERSION.dup) >= Gem::Version.new('2.2.2')"
- gem: 'puppet-strings'
- gem: 'webmock'

Rakefile:
requires:
- 'puppet-strings/tasks'

spec/spec_helper.rb:
mock_with: ':rspec'
spec_overrides: |
require 'webmock/rspec'
require 'puppet_x/tragiccode/azure'
WebMock.disable_net_connect!
.gitlab-ci.yml:
delete: true

.travis.yml:
extras:
- env: CHECK=build DEPLOY_TO_FORGE=yes
.rubocop.yml:
default_configs:
Style/ClassAndModuleChildren:
Description: Compact style reduces the required amount of indentation.
EnforcedStyle: nested
9 changes: 5 additions & 4 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,23 @@ script:
- 'bundle exec rake $CHECK'
bundler_args: --without system_tests
rvm:
- 2.4.4
- 2.5.0
env:
global:
- BEAKER_PUPPET_COLLECTION=puppet5 PUPPET_GEM_VERSION="~> 5.0"
- BEAKER_PUPPET_COLLECTION=puppet6 PUPPET_GEM_VERSION="~> 6.0"
matrix:
fast_finish: true
include:
-
env: CHECK="syntax lint metadata_lint check:symlinks check:git_ignore check:dot_underscore check:test_file rubocop"
-
env: CHECK=parallel_spec
-
env: PUPPET_GEM_VERSION="~> 5.0" CHECK=parallel_spec
rvm: 2.4.4
-
env: PUPPET_GEM_VERSION="~> 4.0" CHECK=parallel_spec
rvm: 2.1.9
-
env: CHECK=build DEPLOY_TO_FORGE=yes
branches:
only:
- master
Expand Down
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@

All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org).

## [0.4.0](https://github.com/tragiccode/tragiccode-azure_key_vault/tree/0.4.0) (2018-10-24)

[Full Changelog](https://github.com/tragiccode/tragiccode-azure_key_vault/compare/0.3.0...0.4.0)

### Added

- Add a Hiera backend [\#13](https://github.com/TraGicCode/tragiccode-azure_key_vault/pull/13) ([hbuckle](https://github.com/hbuckle))

### UNCATEGORIZED PRS; GO LABEL THEM

- \(GH-20\) Update pdk template to latest [\#22](https://github.com/TraGicCode/tragiccode-azure_key_vault/pull/22) ([TraGicCode](https://github.com/TraGicCode))
- \(GH-14\) Adding tags to metadata.json [\#18](https://github.com/TraGicCode/tragiccode-azure_key_vault/pull/18) ([TraGicCode](https://github.com/TraGicCode))
- \(GH-15\) Fix forge link for reference.md [\#16](https://github.com/TraGicCode/tragiccode-azure_key_vault/pull/16) ([TraGicCode](https://github.com/TraGicCode))

## [0.3.0](https://github.com/tragiccode/tragiccode-azure_key_vault/tree/0.3.0) (2018-09-26)

[Full Changelog](https://github.com/tragiccode/tragiccode-azure_key_vault/compare/0.2.0...0.3.0)
Expand Down
23 changes: 8 additions & 15 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -1,22 +1,15 @@
source ENV['GEM_SOURCE'] || 'https://rubygems.org'

def location_for(place_or_version, fake_version = nil)
if place_or_version =~ %r{\A(git[:@][^#]*)#(.*)}
[fake_version, { git: Regexp.last_match(1), branch: Regexp.last_match(2), require: false }].compact
elsif place_or_version =~ %r{\Afile:\/\/(.*)}
['>= 0', { path: File.expand_path(Regexp.last_match(1)), require: false }]
else
[place_or_version, { require: false }]
end
end
git_url_regex = %r{\A(?<url>(https?|git)[:@][^#]*)(#(?<branch>.*))?}
file_url_regex = %r{\Afile:\/\/(?<path>.*)}

def gem_type(place_or_version)
if place_or_version =~ %r{\Agit[:@]}
:git
elsif !place_or_version.nil? && place_or_version.start_with?('file:')
:file
if place_or_version && (git_url = place_or_version.match(git_url_regex))
[fake_version, { git: git_url[:url], branch: git_url[:branch], require: false }].compact
elsif place_or_version && (file_url = place_or_version.match(file_url_regex))
['>= 0', { path: File.expand_path(file_url[:path]), require: false }]
else
:gem
[place_or_version, { require: false }]
end
end

Expand All @@ -35,10 +28,10 @@ group :development do
gem "puppet-module-win-dev-r#{minor_version}", require: false, platforms: [:mswin, :mingw, :x64_mingw]
gem "github_changelog_generator", require: false, git: 'https://github.com/skywinder/github-changelog-generator', ref: '20ee04ba1234e9e83eb2ffb5056e23d641c7a018' if Gem::Version.new(RUBY_VERSION.dup) >= Gem::Version.new('2.2.2')
gem "puppet-strings", require: false
gem "webmock", require: false
end

puppet_version = ENV['PUPPET_GEM_VERSION']
puppet_type = gem_type(puppet_version)
facter_version = ENV['FACTER_GEM_VERSION']
hiera_version = ENV['HIERA_GEM_VERSION']

Expand Down
54 changes: 50 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@

## Description

Secure secrets management is essential and critical in order to protect data in the cloud. Key Vault is Microsoft Azure's solution to make this happen. This module allows you to easily fetch secrets securely on the puppet server and embed them into catalogs during compilation time.
Secure secrets management is essential and critical in order to protect data in the cloud. Key Vault is Microsoft Azure's solution to make this happen.
This module provides a Puppet function and a Hiera backend that allows you to easily fetch secrets securely on the puppet server and embed them into catalogs during compilation time.

## Setup

Expand All @@ -27,7 +28,7 @@ The module requires the following:
* Puppet Server running on a machine with Managed Service Identity ( MSI ) and assigned the appropriate permissions
to pull secrets from the vault. To learn more or get help with this please visit https://docs.microsoft.com/en-us/azure/active-directory/managed-service-identity/tutorial-windows-vm-access-nonaad

## How it works
## How the function works

This module contains a Puppet 4 function that allows you to securely retrieve secrets from Azure Key Vault. In order to get started simply call the function in your manifests passing in the required parameters:

Expand All @@ -45,9 +46,52 @@ In the above example the api_versions hash is important. It is pinning both of
* Instance Metadata Service Versions ( https://docs.microsoft.com/en-us/azure/virtual-machines/windows/instance-metadata-service )
* Vault Versions ( TBD )

## How the hiera backend works

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

Add a new entry to the `hierarchy` hash in `hiera.yaml` referencing the vault name and API versions:

```yaml
- name: 'Azure Key Vault Secrets'
lookup_key: azure_key_vault::lookup
options:
vault_name: production-vault
vault_api_version: '2016-10-01'
metadata_api_version: '2018-02-01'
```
To retrieve a secret in puppet code you can use the `lookup` function:

```puppet
notify { 'lookup':
message => lookup('important-secret'),
}
```

This function can also be used in hiera files, for example to set class parameters:

```yaml
some_class::password: "%{lookup('important-secret')}"
```

You can use a fact to specify different vaults for different groups of nodes. It is
recommended to use a trusted fact such as trusted.extensions.pp_environment as these facts
cannot be altered.
Alternatively a custom trusted fact can be included [in the certificate request](https://puppet.com/docs/puppet/latest/ssl_attributes_extensions.html)]

```yaml
- name: 'Azure Key Vault Secrets from trusted fact'
lookup_key: azure_key_vault::lookup
options:
vault_name: "%{trusted.extensions.pp_environment}"
vault_api_version: '2016-10-01'
metadata_api_version: '2018-02-01'
```

## How it's secure by default

In order to prevent accidental leakage of your secrets throughout all of the locations puppet stores information the returned value of the `azure_key_vault::secret` function is a string wrapped in a Sensitive data type. Lets look at an example of what this means and why it's important. Below is an example of pulling a secret and trying to output the value in a notice function.
In order to prevent accidental leakage of your secrets throughout all of the locations puppet stores information the returned value of the `azure_key_vault::secret` function & Hiera backend return a string wrapped in a Sensitive data type. Lets look at an example of what this means and why it's important. Below is an example of pulling a secret and trying to output the value in a notice function.

```puppet
$secret = azure_key_vault::secret('production-vault', 'important-secret', {
Expand Down Expand Up @@ -134,9 +178,11 @@ $admin_password_secret = azure_key_vault::secret('production-vault', 'admin-pass
'067e89990f0a4a50a7bd854b40a56089')
```

**NOTE: Retrieving a specific version of a secret is currently not available via the hiera backend**

## Reference

See [REFERENCE.md](REFERENCE.md)
See [REFERENCE.md](https://github.com/tragiccode/tragiccode-azure_key_vault/blob/master/REFERENCE.md)

## Development

Expand Down
31 changes: 31 additions & 0 deletions REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,41 @@

**Functions**

* [`azure_key_vault::lookup`](#azure_key_vaultlookup):
* [`azure_key_vault::secret`](#azure_key_vaultsecret): Retrieves secrets from Azure's Key Vault.

## Functions

### azure_key_vault::lookup

Type: Ruby 4.x API

The azure_key_vault::lookup function.

#### `azure_key_vault::lookup(Variant[String, Numeric] $secret_name, Struct[{vault_name => String, vault_api_version => String, metadata_api_version => String}] $options, Puppet::LookupContext $context)`

The azure_key_vault::lookup function.

Returns: `Any`

##### `secret_name`

Data type: `Variant[String, Numeric]`



##### `options`

Data type: `Struct[{vault_name => String, vault_api_version => String, metadata_api_version => String}]`



##### `context`

Data type: `Puppet::LookupContext`



### azure_key_vault::secret

Type: Ruby 4.x API
Expand Down
8 changes: 8 additions & 0 deletions appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ environment:
PUPPET_GEM_VERSION: ~> 5.0
RUBY_VERSION: 24-x64
CHECK: parallel_spec
-
PUPPET_GEM_VERSION: ~> 6.0
RUBY_VERSION: 25
CHECK: parallel_spec
-
PUPPET_GEM_VERSION: ~> 6.0
RUBY_VERSION: 25-x64
CHECK: parallel_spec
matrix:
fast_finish: true
install:
Expand Down
35 changes: 35 additions & 0 deletions lib/puppet/functions/azure_key_vault/lookup.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
require_relative '../../../puppet_x/tragiccode/azure'

Puppet::Functions.create_function(:'azure_key_vault::lookup') do
dispatch :lookup_key do
param 'Variant[String, Numeric]', :secret_name
param 'Struct[{vault_name => String, vault_api_version => String, metadata_api_version => String}]', :options
param 'Puppet::LookupContext', :context
end

def lookup_key(secret_name, options, context)
# This is a reserved key name in hiera
return context.not_found if secret_name == 'lookup_options'
return context.cached_value(secret_name) if context.cache_has_key(secret_name)
access_token = if context.cache_has_key('access_token')
context.cached_value('access_token')
else
TragicCode::Azure.get_access_token(options['metadata_api_version'])
end
begin
secret_value = TragicCode::Azure.get_secret(
options['vault_name'],
secret_name,
options['vault_api_version'],
access_token,
'',
)
rescue RuntimeError => e
Puppet.warning(e.message)
secret_value = nil
end
context.not_found if secret_value.nil?
return if secret_value.nil?
context.cache(secret_name, secret_value)
end
end
23 changes: 10 additions & 13 deletions lib/puppet/functions/azure_key_vault/secret.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
require 'open-uri'
require 'json'
require_relative '../../../puppet_x/tragiccode/azure'

# Retrieves secrets from Azure's Key Vault.
Puppet::Functions.create_function(:'azure_key_vault::secret') do
Expand All @@ -20,17 +19,15 @@ def secret(vault_name, secret_name, api_versions_hash, secret_version = '')
Puppet.debug("secret_name: #{secret_name}")
Puppet.debug("secret_version: #{secret_version}")
Puppet.debug("metadata_api_version: #{api_versions_hash['metadata_api_version']}")
Puppet.debug("api_version_vault: #{api_versions_hash['vault_api_version']}")

secret_url = "https://#{vault_name}.vault.azure.net/secrets/#{secret_name}#{secret_version.empty? ? secret_version : "/#{secret_version}"}?api-version=#{api_versions_hash['vault_api_version']}"
Puppet.debug("Generated Secrets Url: #{secret_url}")

# Get MSI's Access-Token
get_access_token = open("http://169.254.169.254/metadata/identity/oauth2/token?api-version=#{api_versions_hash['metadata_api_version']}&resource=https%3A%2F%2Fvault.azure.net",
'Metadata' => 'true')
access_token = JSON.parse(get_access_token.string)['access_token']
get_secret = open(secret_url, 'Authorization' => "Bearer #{access_token}")
secret_value = JSON.parse(get_secret.string)['value']
Puppet.debug("vault_api_version: #{api_versions_hash['vault_api_version']}")
access_token = TragicCode::Azure.get_access_token(api_versions_hash['metadata_api_version'])
secret_value = TragicCode::Azure.get_secret(
vault_name,
secret_name,
api_versions_hash['vault_api_version'],
access_token,
secret_version,
)
Puppet::Pops::Types::PSensitiveType::Sensitive.new(secret_value)
end
end
30 changes: 30 additions & 0 deletions lib/puppet_x/tragiccode/azure.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
require 'net/http'
require 'json'

module TragicCode
# Azure API functions
class Azure
def self.get_access_token(api_version)
uri = URI("http://169.254.169.254/metadata/identity/oauth2/token?api-version=#{api_version}&resource=https%3A%2F%2Fvault.azure.net")
req = Net::HTTP::Get.new(uri)
req['Metadata'] = 'true'
res = Net::HTTP.start(uri.hostname, uri.port) do |http|
http.request(req)
end
raise res.body unless res.is_a?(Net::HTTPSuccess)
JSON.parse(res.body)['access_token']
end

def self.get_secret(vault_name, secret_name, vault_api_version, access_token, secret_version)
version_parameter = secret_version.empty? ? secret_version : "/#{secret_version}"
uri = URI("https://#{vault_name}.vault.azure.net/secrets/#{secret_name}#{version_parameter}?api-version=#{vault_api_version}")
req = Net::HTTP::Get.new(uri)
req['Authorization'] = "Bearer #{access_token}"
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(req)
end
raise res.body unless res.is_a?(Net::HTTPSuccess)
JSON.parse(res.body)['value']
end
end
end
Loading