diff --git a/README.md b/README.md index facde01..2863581 100644 --- a/README.md +++ b/README.md @@ -49,8 +49,47 @@ infrastructure is assumed to be up and reachable by your Puppet agents. Install this module as you would in any other; the necessary code will be distributed to Puppet agents via pluginsync. -In your manifests, call the `vault_lookup::lookup()` function using the -Deferred type. For example: +Configure Vault server in Hiera: + +```yaml +vault_lookup::server: https://vault.hostname:8200 +``` +get secret from Vault: + +```puppet +$d = vault_lookup::kv('secret/test') +``` +which will return `Deferred` function (evaluated on agent side). + +When working with `$d` (Deferred object), you can use `vault_lookup::fmt()` function (where you can apply `sprintf` formatting): +```puppet +file { '/path/to/your/file': + content => vault_lookup::fmt('password=%s', {'pass' => $d.unwrap }), +} +``` +Or you could use directly `Deferred` object: + +```puppet +file { '/etc/secrets.conf': + ensure => file, + content => Deferred('inline_epp', + ['PASSWORD=<%= $password.unwrap %>', {'password' => $d}]), +} +``` + +Optionally you can specify which `field` should be retrieved: + +```puppet +$d = vault_lookup::kv('secret/test', {'field' => 'password') +``` + +If needed, you can override Vault server address: + +```puppet +$d = vault_lookup::kv('secret/test', {'vault_addr' => 'https://vault.hostname:8200') +``` + +Internally `vault_lookup::kv` creates a `Deferred` object: ```puppet $d = Deferred('vault_lookup::lookup', ["secret/test", 'https://vault.hostname:8200']) @@ -72,7 +111,7 @@ You can also choose not to specify the Vault URL, and then Puppet will use the set in the service config file for Puppet, on Debian `/etc/default/puppet`, on RedHat `/etc/sysconfig/puppet`: -``` +```puppet $d = Deferred('vault_lookup::lookup', ["secret/test"]) node default { diff --git a/lib/puppet/functions/vault_lookup/fmt.rb b/lib/puppet/functions/vault_lookup/fmt.rb new file mode 100644 index 0000000..cd8aea7 --- /dev/null +++ b/lib/puppet/functions/vault_lookup/fmt.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +Puppet::Functions.create_function('vault_lookup::fmt') do + # @summary Lazy format function + # Will be evaluated on client side - not during catalog compile on server + # @param format Ruby printf syntax + # @param args arguments passed to sprintf function + # @see https://idiosyncratic-ruby.com/49-what-the-format.html + # @example + # vault::fmt("%d + %d = %d", {'x' => 2, 'y' => 2, 'z' => 5}) + # won't work on Puppet 7 + dispatch :fmt do + param 'String', :format + param 'Hash', :args + return_type 'Deferred' + end + + def fmt(format, args) + Puppet::Pops::Types::TypeFactory.deferred.create('sprintf', [format, call_function('vault_lookup::unpack', args)]) + end +end diff --git a/lib/puppet/functions/vault_lookup/kv.rb b/lib/puppet/functions/vault_lookup/kv.rb new file mode 100644 index 0000000..c8a2f6b --- /dev/null +++ b/lib/puppet/functions/vault_lookup/kv.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +Puppet::Functions.create_function('vault_lookup::kv') do + # @summary Fetches secret stored under $key from Vault server using deferred function + # @param key Secret key path in Vault + # @param ops Options passed to Vault + # + # kv2 is prefixed with `"kv/data` + # + # @example + # vault::kv('secret') + # vault::kv('secret', {'field' => 'password'}) + dispatch :kv do + param 'String', :key + optional_param 'Hash', :opts + return_type 'Deferred' + end + + def kv(key, opts = {}) + unless opts.key?('vault_addr') + type_parser = Puppet::Pops::Types::TypeParser.singleton + vault_addr = call_function('lookup', 'vault_lookup::server', type_parser.parse('Optional[String]'), nil, nil) + opts['vault_addr'] = vault_addr if vault_addr + end + + Puppet::Pops::Types::TypeFactory.deferred.create('vault_lookup::lookup', [key, opts]) + end +end diff --git a/lib/puppet/functions/vault_lookup/unpack.rb b/lib/puppet/functions/vault_lookup/unpack.rb new file mode 100644 index 0000000..128460b --- /dev/null +++ b/lib/puppet/functions/vault_lookup/unpack.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +Puppet::Functions.create_function('vault_lookup::unpack') do + # @summary Unwrap values in a Hash + # @param args Hash possibly with Sensitive data + dispatch :unpack do + param 'Hash', :args + return_type 'Hash' + end + + def unpack(args) + args.transform_values do |value| + if value.is_a?(Puppet::Pops::Types::PSensitiveType::Sensitive) + call_function('unwrap', value) + else + value + end + end + end +end diff --git a/spec/functions/fmt_spec.rb b/spec/functions/fmt_spec.rb new file mode 100644 index 0000000..b43e316 --- /dev/null +++ b/spec/functions/fmt_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'vault_lookup::fmt' do + context 'deferred array format' do + it { + exp = Puppet::Pops::Types::TypeFactory.deferred.create('sprintf', ['foo=%d', { 'bar' => 2 }]) + # This kind_of matcher requires https://github.com/puppetlabs/rspec-puppet/pull/24 + expect(subject).to run.with_params( + 'foo=%d', { 'bar' => 2 } + ).and_return(kind_of(Puppet::Pops::Types::PuppetObject)) + + expect(subject).to run.with_params( + 'foo=%d', { 'bar' => 2 } + ).and_return(exp) + } + end +end diff --git a/spec/functions/kv_spec.rb b/spec/functions/kv_spec.rb new file mode 100644 index 0000000..f788744 --- /dev/null +++ b/spec/functions/kv_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'vault_lookup::kv' do + context 'with server address' do + it { + exp = Puppet::Pops::Types::TypeFactory.deferred.create('vault_lookup::lookup', ['kv/data/secret', { 'vault_addr' => 'https://vault.example.com:8200' }]) + # This kind_of matcher requires https://github.com/puppetlabs/rspec-puppet/pull/24 + expect(subject).to run.with_params( + 'kv/data/secret' + ).and_return(kind_of(Puppet::Pops::Types::PuppetObject)) + + expect(subject).to run.with_params( + 'kv/data/secret', { 'vault_addr' => 'https://vault.example.com:8200' } + ).and_return(exp) + } + end + + context 'with lookup key' do + let(:hiera_data) { { 'vault_lookup::server' => 'https://vault:8200' } } + + it { + exp = Puppet::Pops::Types::TypeFactory.deferred.create('vault_lookup::lookup', ['secret', {}]) + + expect(subject).to run.with_params( + 'secret' + ).and_return(exp) + } + end + + context 'with vault_addr key' do + it { + exp = Puppet::Pops::Types::TypeFactory.deferred.create('vault_lookup::lookup', ['secret', { 'vault_addr' => 'https://vault:8200' }]) + + expect(subject).to run.with_params( + 'secret', { 'vault_addr' => 'https://vault:8200' } + ).and_return(exp) + } + end + + context 'with field key' do + it { + exp = Puppet::Pops::Types::TypeFactory.deferred.create('vault_lookup::lookup', ['secret', { 'field' => 'password' }]) + + expect(subject).to run.with_params( + 'secret', { 'field' => 'password' } + ).and_return(exp) + } + end +end diff --git a/spec/functions/unpack_spec.rb b/spec/functions/unpack_spec.rb new file mode 100644 index 0000000..2c1b12f --- /dev/null +++ b/spec/functions/unpack_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'vault_lookup::unpack' do + let(:function) { subject } + + it 'converts sensitive Hash' do + exp = { 'password' => 'p1ssw0rd' } + expect(subject).to run.with_params( + { 'password' => sensitive('p1ssw0rd') } + ).and_return(exp) + end + + it 'does nothing with String type' do + h = { 'foo' => 'bar', 'boo' => 'baz' } + expect(subject).to run.with_params( + { 'foo' => 'bar', 'boo' => 'baz' } + ).and_return(h) + end +end