diff --git a/README.md b/README.md index 3ff8444..3964232 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,26 @@ quadlets::quadlet { "centos.network": } ``` +### Manage podman secrets with puppet +The expected format for the title is 'username:secretname'. This allows manage the same secret for multiple +users. To manage secrets for non root users, they need to have an active session (either enable linger or being logged in !). +```puppet +# a secret for the root user: +quadlets_secret{ 'root:secretname': + secret => 'donotforget!', +} +# a secret for the xyz user, with labels: +quadlets_secret{ 'xyz:secretname': + secret => 'ensuretorember', + labels => { 'label1' => 'one', 'label2' => 'two' }, +} + +# ensure to remove a secret: +quadlets_secret{ 'root:secretname': + ensure => 'absent' +} +``` + ### Hiera Representation Of User setup and Quadlet deployment ```yaml diff --git a/REFERENCE.md b/REFERENCE.md index 68a5b37..e1b0a47 100644 --- a/REFERENCE.md +++ b/REFERENCE.md @@ -21,6 +21,10 @@ * [`quadlets::quadlet`](#quadlets--quadlet): Generate and manage podman quadlet definitions (podman > 4.4.0) * [`quadlets::user`](#quadlets--user): Generate and manage podman quadlet user +### Resource types + +* [`quadlets_secret`](#quadlets_secret): Type for managing podman secrets (per user) + ### Data types * [`Quadlets::Auth`](#Quadlets--Auth): custom datatype to specify username and password @@ -70,6 +74,7 @@ The following parameters are available in the `quadlets` class: * [`purge_quadlet_dir`](#-quadlets--purge_quadlet_dir) * [`quadlets_hash`](#-quadlets--quadlets_hash) * [`users_hash`](#-quadlets--users_hash) +* [`secrets_hash`](#-quadlets--secrets_hash) ##### `manage_package` @@ -195,6 +200,14 @@ a `Hash` of `quadlets::users` to deploy Default value: `{}` +##### `secrets_hash` + +Data type: `Stdlib::CreateResources` + +a `Hash` of `quadlets_secrets` to deploy + +Default value: `{}` + ## Defined types ### `quadlets::quadlet` @@ -636,6 +649,78 @@ Define additional parameters to be used to create the user. Default value: `{}` +## Resource types + +### `quadlets_secret` + +Type for managing podman secrets (per user) + +#### Examples + +##### Define a secret mysecret for user blah + +```puppet +quadlets_secret{ 'blah:mysecret': + secret => '***secret***', +} +``` + +#### Properties + +The following properties are available in the `quadlets_secret` type. + +##### `ensure` + +Valid values: `present`, `absent` + +The basic property that the resource should be in. + +Default value: `present` + +##### `labels` + +secret labels to set + +Default value: `{}` + +##### `secret` + +the secret himself + +#### Parameters + +The following parameters are available in the `quadlets_secret` type. + +* [`doptions`](#-quadlets_secret--doptions) +* [`driver`](#-quadlets_secret--driver) +* [`name`](#-quadlets_secret--name) +* [`provider`](#-quadlets_secret--provider) + +##### `doptions` + +driver options used for secret creation + +Default value: `{}` + +##### `driver` + +driver to be used for secret creation + +Default value: `file` + +##### `name` + +Valid values: `%r{^\S+:\S+}` + +namevar + +combination of user:secretname of the secret to administrate + +##### `provider` + +The specific backend to use for this `quadlets_secret` resource. You will seldom need to specify this --- Puppet will +usually discover the appropriate provider for your platform. + ## Data types ### `Quadlets::Auth` diff --git a/lib/puppet/provider/quadlets_secret/podman.rb b/lib/puppet/provider/quadlets_secret/podman.rb new file mode 100644 index 0000000..b488b72 --- /dev/null +++ b/lib/puppet/provider/quadlets_secret/podman.rb @@ -0,0 +1,105 @@ +# frozen_string_literal: true + +# +# This file contains a provider for the resource type `libvirt_nwfilter`, +# + +require 'json' + +Puppet::Type.type(:quadlets_secret).provide(:podman) do + desc "@summary provider for the resource type `quadlets_secret`, + which manages quadlet secrets + using the podman command." + + commands podman: 'podman' + + def should_user + @should_user || @should_user = resource[:name].split(':')[0] + end + + def should_secretname + @should_secretname || @should_secretname = resource[:name].split(':')[1] + end + + def hash_to_array(inhash) + res = [] + if inhash + inhash.keys.sort.each do |k| + res << "#{k}=#{inhash[k]}" + end + end + res + end + + def run_podman(args, env = {}) + user_info = Etc.getpwnam(should_user) + runenv = { + cwd: user_info.dir, + failonfail: true, + uid: user_info.uid, + gid: user_info.gid, + combine: false, + custom_environment: env.merge({ 'HOME' => user_info.dir, 'XDG_RUNTIME_DIR' => "/run/user/#{user_info.uid}" }) + } + # test if user is logged in or lingering: + raise Puppet::Error, "user #{should_user} is not logged in, consider enable linger." unless File.directory?("/run/user/#{user_info.uid}") + + execute([command('podman')] + args, runenv) + end + + def create_secret(replace) + args = ['secret', 'create', should_secretname, '--env', 'psecret'] + + if replace + args << "-d=#{@result['Spec']['Driver']['Name']}" + hash_to_array(@result['Spec']['Driver']['Options']).each do |d| + args << "--driver-opts=#{d}" + end + args << '--replace' + else + args << "-d=#{@resource[:driver]}" + hash_to_array(resource[:doptions]).each do |d| + args << "--driver-opts=#{d}" + end + end + + resource[:labels]&.split('|')&.each do |l| + args << "-l=#{l}" + end + + run_podman(args, { 'psecret' => @resource[:secret] }) + end + + def create + create_secret(false) + end + + def destroy + run_podman(['secret', 'rm', should_secretname]) + end + + def labels=(_label) + create_secret(true) + end + + def labels + hash_to_array(@result['Spec']['Labels']).join('|') if @result + end + + def secret=(_secret) + create_secret(true) + end + + def secret + @result['SecretData'] if @result + end + + def exists? + begin + res = run_podman(['secret', 'inspect', should_secretname, '--showsecret']) + rescue Puppet::ExecutionFailure + return false + end + @result = JSON.parse(res)[0] + end +end diff --git a/lib/puppet/type/quadlets_secret.rb b/lib/puppet/type/quadlets_secret.rb new file mode 100644 index 0000000..984ecc5 --- /dev/null +++ b/lib/puppet/type/quadlets_secret.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +Puppet::Type.newtype(:quadlets_secret) do + desc <<~DESC + Type for managing podman secrets (per user) + + @example Define a secret mysecret for user blah + quadlets_secret{ 'blah:mysecret': + secret => '***secret***', + } + DESC + + ensurable + + newparam(:name, namevar: true) do + desc 'combination of user:secretname of the secret to administrate' + + newvalues(%r{^\S+:\S+}) + end + + newparam(:driver) do + desc 'driver to be used for secret creation' + defaultto 'file' + end + + newparam(:doptions) do + desc 'driver options used for secret creation' + defaultto({}) + + validate do |value| + raise ArgumentError, 'needs to be a Hash' unless value.is_a?(Hash) + end + end + + newproperty(:labels) do + desc 'secret labels to set' + defaultto({}) + + validate do |value| + raise ArgumentError, 'needs to be a Hash' unless value.is_a?(Hash) + end + + munge do |value| + res = [] + value.keys.sort.each do |k| + res << "#{k}=#{value[k]}" + end + return res.join('|') + end + end + + newproperty(:secret) do + desc 'the secret himself' + + validate do |value| + raise ArgumentError, 'needs to be a string' unless value.is_a?(String) + end + + def should_to_s(_value) + '*****' + end + + def is_to_s(_value) + '*****' + end + end + + autorequire(:class) { 'quadlets' } + + autorequire(:user) do + [self[:name].split(':')[0]] + end + + autorequire(:loginctl_user) do + [self[:name].split(':')[0]] + end + + autorequire(:package) do + ['podman'] + end +end diff --git a/manifests/init.pp b/manifests/init.pp index 92af5e3..46a106b 100644 --- a/manifests/init.pp +++ b/manifests/init.pp @@ -26,6 +26,9 @@ # @param quadlets_hash a `Hash` of `quadlets::quadlet` to deploy # @param users_hash a `Hash` of `quadlets::users` to deploy # +# @param secrets_hash +# a `Hash` of `quadlets_secrets` to deploy +# # @example Set up Podman for quadlets # include quadlets # @@ -46,6 +49,7 @@ Boolean $purge_quadlet_dir = false, Stdlib::CreateResources $quadlets_hash = {}, Stdlib::CreateResources $users_hash = {}, + Stdlib::CreateResources $secrets_hash = {}, ) { $quadlet_dir = '/etc/containers/systemd' $quadlet_system_user_dir = '/etc/containers/systemd/users' @@ -68,4 +72,10 @@ * => $_v, } } + + $secrets_hash.each |$_n, $_v| { + quadlets_secret { $_n: + * => $_v, + } + } } diff --git a/spec/acceptance/quadlets_secret_spec.rb b/spec/acceptance/quadlets_secret_spec.rb new file mode 100644 index 0000000..1db9324 --- /dev/null +++ b/spec/acceptance/quadlets_secret_spec.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +require 'spec_helper_acceptance' + +describe 'quadlets_secret' do + context 'with a selection of secrets for root and auser' do + it_behaves_like 'an idempotent resource' do + let(:manifest) do + <<-PUPPET + include quadlets + + quadlets_secret{'root:asecret': + secret => 'whoknows', + } + + quadlets_secret{'root:anothersecret': + secret => 'justguess', + labels => { + label1 => 'one', + label2 => 'two', + }, + } + + quadlets_secret{'root:withpath': + secret => 'Idonottellyou;)', + doptions => { + path => '/tmp/whithpathsecret', + }, + } + PUPPET + end + end + + it 'root:asecret exists' do + result = command('podman secret ls --filter Name=asecret -n --format "{{.Name}}"') + expect(result.stdout.strip).to eq('asecret') + end + + it 'root:anothersecret has labels' do + result = command('podman secret inspect anothersecret --format "{{.Spec.Labels}}"') + expect(result.stdout.strip).to eq('map[label1:one label2:two]') + end + + describe user('auser') do + it { is_expected.to exist } + end + + describe 'directories for secret with path set' do + describe file('/tmp/whithpathsecret') do + it { is_expected.to be_directory } + it { is_expected.to be_owned_by 'root' } + it { is_expected.to be_grouped_into 'root' } + end + end + + describe 'file for secret with path set' do + describe file('/tmp/whithpathsecret/secretsdata.json') do + it { is_expected.to be_file } + it { is_expected.to be_owned_by 'root' } + it { is_expected.to be_grouped_into 'root' } + end + end + end +end diff --git a/spec/classes/init_spec.rb b/spec/classes/init_spec.rb index 5c789da..1631d11 100644 --- a/spec/classes/init_spec.rb +++ b/spec/classes/init_spec.rb @@ -236,6 +236,18 @@ it { is_expected.to contain_quadlets__user('starmer').with_manage_user(true) } it { is_expected.to have_quadlets__user_resource_count(2) } end + + context 'with secrets defined' do + let(:params) do + { secrets_hash: { + 'root:secret1': { + secret: '** hidden **', + } + } } + end + + it { is_expected.to contain_quadlets_secret('root:secret1').with_secret('** hidden **') } + end end end end diff --git a/spec/types/quadlets_secret_spec.rb b/spec/types/quadlets_secret_spec.rb new file mode 100644 index 0000000..b8b64a6 --- /dev/null +++ b/spec/types/quadlets_secret_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'quadlets_secret' do + let(:title) { 'root:asecret' } + + context 'with default provider' do + it { is_expected.to be_valid_type.with_properties('ensure') } + it { is_expected.to be_valid_type.with_parameters('driver') } + it { is_expected.to be_valid_type.with_parameters('doptions') } + it { is_expected.to be_valid_type.with_properties('labels') } + it { is_expected.to be_valid_type.with_properties('secret') } + end +end