diff --git a/Gemfile b/Gemfile index 7904c15fa5..d78e86d278 100644 --- a/Gemfile +++ b/Gemfile @@ -22,7 +22,7 @@ group :development do gem "voxpupuli-puppet-lint-plugins", '~> 4.0', require: false gem "facterdb", '~> 1.18', require: false gem "metadata-json-lint", '~> 3.0', require: false - gem "puppetlabs_spec_helper", '~> 6.0', require: false + gem "puppetlabs_spec_helper", '~> 7.0', require: false gem "rspec-puppet-facts", '~> 2.0', require: false gem "codecov", '~> 0.2', require: false gem "dependency_checker", '~> 1.0.0', require: false diff --git a/README.md b/README.md index d6894a6ac9..3b6d7c537e 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,28 @@ include apt ### Add GPG keys +You can fetch GPG keys via HTTP, Puppet URI, or local filesystem. The key can be in GPG binary format, or ASCII armored, but the filename should have the appropriate extension (`.gpg` for keys in binary format; or `.asc` for ASCII armored keys). + +#### Fetch via HTTP + +```puppet +apt::keyring { 'puppetlabs-keyring.gpg': + source => 'https://apt.puppetlabs.com/keyring.gpg', +} +``` + +#### Fetch via Puppet URI + +```puppet +apt::keyring { 'puppetlabs-keyring.gpg': + source => 'puppet:///modules/my_module/local_puppetlabs-keyring.gpg', +} +``` + +Alternatively `apt::key` can be used. + +**Warning** `apt::key` is deprecated in the latest Debian and Ubuntu releases. Please use apt::keyring instead. + **Warning:** Using short key IDs presents a serious security issue, potentially leaving you open to collision attacks. We recommend you always use full fingerprints to identify your GPG keys. This module allows short keys, but issues a security warning if you use them. Declare the `apt::key` defined type: @@ -184,6 +206,22 @@ apt::source { 'puppetlabs': } ``` +### Adding name and source to the key parameter of apt::source, which then manages modern apt gpg keyrings + +The `name` parameter of key hash should contain the filename with extension (such as `puppetlabs.gpg`). + +```puppet +apt::source { 'puppetlabs': + comment => 'Puppet8', + location => 'https://apt.puppetlabs.com/', + repos => 'puppet8', + key => { + 'name' => 'puppetlabs.gpg', + 'source' => 'https://apt.puppetlabs.com/keyring.gpg', + }, +} +``` + ### Configure Apt from Hiera diff --git a/REFERENCE.md b/REFERENCE.md index 842ca67463..f4e31a20cf 100644 --- a/REFERENCE.md +++ b/REFERENCE.md @@ -20,6 +20,7 @@ * [`apt::conf`](#apt--conf): Specifies a custom Apt configuration file. * [`apt::key`](#apt--key): Manages the GPG keys that Apt uses to authenticate packages. +* [`apt::keyring`](#apt--keyring): Manage GPG keyrings for apt repositories * [`apt::mark`](#apt--mark): Manages apt-mark settings * [`apt::pin`](#apt--pin): Manages Apt pins. Does not trigger an apt-get update run. * [`apt::ppa`](#apt--ppa): Manages PPA repositories using `add-apt-repository`. Not supported on Debian. @@ -73,6 +74,7 @@ The following parameters are available in the `apt` class: * [`proxy_defaults`](#-apt--proxy_defaults) * [`sources`](#-apt--sources) * [`keys`](#-apt--keys) +* [`keyrings`](#-apt--keyrings) * [`ppas`](#-apt--ppas) * [`pins`](#-apt--pins) * [`settings`](#-apt--settings) @@ -239,6 +241,14 @@ Creates new `apt::key` resources. Valid options: a hash to be passed to the crea Default value: `$apt::params::keys` +##### `keyrings` + +Data type: `Hash` + +Hash of `apt::keyring` resources. + +Default value: `{}` + ##### `ppas` Data type: `Hash` @@ -624,6 +634,101 @@ Passes additional options to `apt-key adv --keyserver-options`. Default value: `$apt::key_options` +### `apt::keyring` + +Manage GPG keyrings for apt repositories + +#### Examples + +##### Download the puppetlabs apt keyring + +```puppet +apt::keyring { 'puppetlabs-keyring.gpg': + source => 'https://apt.puppetlabs.com/keyring.gpg', +} +``` + +##### Deploy the apt source and associated keyring file + +```puppet +apt::source { 'puppet8-release': + location => 'http://apt.puppetlabs.com', + repos => 'puppet8', + key => { + name => 'puppetlabs-keyring.gpg', + source => 'https://apt.puppetlabs.com/keyring.gpg' + } +} +``` + +#### Parameters + +The following parameters are available in the `apt::keyring` defined type: + +* [`keyring_dir`](#-apt--keyring--keyring_dir) +* [`keyring_filename`](#-apt--keyring--keyring_filename) +* [`keyring_file`](#-apt--keyring--keyring_file) +* [`keyring_file_mode`](#-apt--keyring--keyring_file_mode) +* [`source`](#-apt--keyring--source) +* [`content`](#-apt--keyring--content) +* [`ensure`](#-apt--keyring--ensure) + +##### `keyring_dir` + +Data type: `Stdlib::Absolutepath` + +Path to the directory where the keyring will be stored. + +Default value: `'/etc/apt/keyrings'` + +##### `keyring_filename` + +Data type: `String[1]` + +Optional filename for the keyring. It should also contain extension along with the filename. + +Default value: `$name` + +##### `keyring_file` + +Data type: `Stdlib::Absolutepath` + +File path of the keyring. + +Default value: `"${keyring_dir}/${keyring_filename}"` + +##### `keyring_file_mode` + +Data type: `Stdlib::Filemode` + +File permissions of the keyring. + +Default value: `'0644'` + +##### `source` + +Data type: `Optional[Stdlib::Filesource]` + +Source of the keyring file. Mutually exclusive with 'content'. + +Default value: `undef` + +##### `content` + +Data type: `Optional[String[1]]` + +Content of the keyring file. Mutually exclusive with 'source'. + +Default value: `undef` + +##### `ensure` + +Data type: `Enum['present','absent']` + +Ensure presence or absence of the resource. + +Default value: `'present'` + ### `apt::mark` Manages apt-mark settings @@ -925,6 +1030,20 @@ apt::source { 'puppetlabs': } ``` +##### Download key behaviour to handle modern apt gpg keyrings. The `name` parameter in the key hash should be given with + +```puppet +extension. Absence of extension will result in file formation with just name and no extension. +apt::source { 'puppetlabs': + location => 'http://apt.puppetlabs.com', + comment => 'Puppet8', + key => { + 'name' => 'puppetlabs.gpg', + 'source' => 'https://apt.puppetlabs.com/keyring.gpg', + }, +} +``` + #### Parameters The following parameters are available in the `apt::source` defined type: @@ -1001,9 +1120,12 @@ Default value: `{}` Data type: `Optional[Variant[String, Hash]]` -Creates a declaration of the apt::key defined type. Valid options: a string to be passed to the `id` parameter of the `apt::key` -defined type, or a hash of `parameter => value` pairs to be passed to `apt::key`'s `id`, `server`, `content`, `source`, `weak_ssl`, -and/or `options` parameters. +Creates an `apt::keyring` in `/etc/apt/keyrings` (or anywhere on disk given `filename`) Valid options: + * a hash of `parameter => value` pairs to be passed to `file`: `name` (title), `content`, `source`, `filename` + +The following inputs are valid for the (deprecated) `apt::key` defined type. Valid options: + * a string to be passed to the `id` parameter of the `apt::key` defined type + * a hash of `parameter => value` pairs to be passed to `apt::key`: `id`, `server`, `content`, `source`, `weak_ssl`, `options` Default value: `undef` @@ -1012,6 +1134,7 @@ Default value: `undef` Data type: `Optional[Stdlib::AbsolutePath]` Absolute path to a file containing the PGP keyring used to sign this repository. Value is used to set signed-by on the source entry. +This is not necessary if the key is installed with `key` param above. See https://wiki.debian.org/DebianRepository/UseThirdParty for details. Default value: `undef` @@ -1030,8 +1153,8 @@ Default value: `undef` Data type: `Optional[String]` Tells Apt to only download information for specified architectures. Valid options: a string containing one or more architecture names, -separated by commas (e.g., 'i386' or 'i386,alpha,powerpc'). Default: undef (if unspecified, Apt downloads information for all architectures -defined in the Apt::Architectures option). +separated by commas (e.g., 'i386' or 'i386,alpha,powerpc'). +(if unspecified, Apt downloads information for all architectures defined in the Apt::Architectures option) Default value: `undef` diff --git a/manifests/init.pp b/manifests/init.pp index b30b418d72..d263a61d08 100644 --- a/manifests/init.pp +++ b/manifests/init.pp @@ -88,6 +88,9 @@ # @param keys # Creates new `apt::key` resources. Valid options: a hash to be passed to the create_resources function linked above. # +# @param keyrings +# Hash of `apt::keyring` resources. +# # @param ppas # Creates new `apt::ppa` resources. Valid options: a hash to be passed to the create_resources function linked above. # @@ -159,6 +162,7 @@ Apt::Proxy $proxy = $apt::params::proxy, Hash $sources = $apt::params::sources, Hash $keys = $apt::params::keys, + Hash $keyrings = {}, Hash $ppas = $apt::params::ppas, Hash $pins = $apt::params::pins, Hash $settings = $apt::params::settings, @@ -347,6 +351,12 @@ if $keys { create_resources('apt::key', $keys) } + # manage keyrings if present + $keyrings.each |$key, $data| { + apt::keyring { $key: + * => $data, + } + } # manage ppas if present if $ppas { create_resources('apt::ppa', $ppas) diff --git a/manifests/keyring.pp b/manifests/keyring.pp new file mode 100644 index 0000000000..83af558257 --- /dev/null +++ b/manifests/keyring.pp @@ -0,0 +1,74 @@ +# @summary Manage GPG keyrings for apt repositories +# +# @example Download the puppetlabs apt keyring +# apt::keyring { 'puppetlabs-keyring.gpg': +# source => 'https://apt.puppetlabs.com/keyring.gpg', +# } +# @example Deploy the apt source and associated keyring file +# apt::source { 'puppet8-release': +# location => 'http://apt.puppetlabs.com', +# repos => 'puppet8', +# key => { +# name => 'puppetlabs-keyring.gpg', +# source => 'https://apt.puppetlabs.com/keyring.gpg' +# } +# } +# +# @param keyring_dir +# Path to the directory where the keyring will be stored. +# +# @param keyring_filename +# Optional filename for the keyring. It should also contain extension along with the filename. +# +# @param keyring_file +# File path of the keyring. +# +# @param keyring_file_mode +# File permissions of the keyring. +# +# @param source +# Source of the keyring file. Mutually exclusive with 'content'. +# +# @param content +# Content of the keyring file. Mutually exclusive with 'source'. +# +# @param ensure +# Ensure presence or absence of the resource. +# +define apt::keyring ( + Stdlib::Absolutepath $keyring_dir = '/etc/apt/keyrings', + String[1] $keyring_filename = $name, + Stdlib::Absolutepath $keyring_file = "${keyring_dir}/${keyring_filename}", + Stdlib::Filemode $keyring_file_mode = '0644', + Optional[Stdlib::Filesource] $source = undef, + Optional[String[1]] $content = undef, + Enum['present','absent'] $ensure = 'present', +) { + ensure_resource('file', $keyring_dir, { ensure => 'directory', mode => '0755', }) + if $source and $content { + fail("Parameters 'source' and 'content' are mutually exclusive") + } elsif ! $source and ! $content { + fail("One of 'source' or 'content' parameters are required") + } + + case $ensure { + 'present': { + file { $keyring_file: + ensure => 'file', + mode => $keyring_file_mode, + owner => 'root', + group => 'root', + source => $source, + content => $content, + } + } + 'absent': { + file { $keyring_file: + ensure => $ensure, + } + } + default: { + fail("Invalid 'ensure' value '${ensure}' for apt::keyring") + } + } +} diff --git a/manifests/source.pp b/manifests/source.pp index dac455fcaf..e1793cea48 100644 --- a/manifests/source.pp +++ b/manifests/source.pp @@ -10,6 +10,17 @@ # }, # } # +# @example Download key behaviour to handle modern apt gpg keyrings. The `name` parameter in the key hash should be given with +# extension. Absence of extension will result in file formation with just name and no extension. +# apt::source { 'puppetlabs': +# location => 'http://apt.puppetlabs.com', +# comment => 'Puppet8', +# key => { +# 'name' => 'puppetlabs.gpg', +# 'source' => 'https://apt.puppetlabs.com/keyring.gpg', +# }, +# } +# # @param location # Required, unless ensure is set to 'absent'. Specifies an Apt repository. Valid options: a string containing a repository URL. # @@ -35,12 +46,16 @@ # Specifies whether to request the distribution's uncompiled source code. Default false. # # @param key -# Creates a declaration of the apt::key defined type. Valid options: a string to be passed to the `id` parameter of the `apt::key` -# defined type, or a hash of `parameter => value` pairs to be passed to `apt::key`'s `id`, `server`, `content`, `source`, `weak_ssl`, -# and/or `options` parameters. +# Creates an `apt::keyring` in `/etc/apt/keyrings` (or anywhere on disk given `filename`) Valid options: +# * a hash of `parameter => value` pairs to be passed to `file`: `name` (title), `content`, `source`, `filename` +# +# The following inputs are valid for the (deprecated) `apt::key` defined type. Valid options: +# * a string to be passed to the `id` parameter of the `apt::key` defined type +# * a hash of `parameter => value` pairs to be passed to `apt::key`: `id`, `server`, `content`, `source`, `weak_ssl`, `options` # # @param keyring # Absolute path to a file containing the PGP keyring used to sign this repository. Value is used to set signed-by on the source entry. +# This is not necessary if the key is installed with `key` param above. # See https://wiki.debian.org/DebianRepository/UseThirdParty for details. # # @param pin @@ -49,8 +64,8 @@ # # @param architecture # Tells Apt to only download information for specified architectures. Valid options: a string containing one or more architecture names, -# separated by commas (e.g., 'i386' or 'i386,alpha,powerpc'). Default: undef (if unspecified, Apt downloads information for all architectures -# defined in the Apt::Architectures option). +# separated by commas (e.g., 'i386' or 'i386,alpha,powerpc'). +# (if unspecified, Apt downloads information for all architectures defined in the Apt::Architectures option) # # @param allow_unsigned # Specifies whether to authenticate packages from this release, even if the Release file is not signed or the signature can't be checked. @@ -122,19 +137,68 @@ $includes = $apt::include_defaults + $include - if $key and $keyring { - fail('parameters key and keyring are mutualy exclusive') - } - - if $key { + if $keyring { + if $key { + fail('parameters key and keyring are mutually exclusive') + } else { + $_list_keyring = $keyring + } + } elsif $key { if $key =~ Hash { - unless $key['id'] { - fail('key hash must contain at least an id entry') + unless $key['name'] or $key['id'] { + fail('key hash must contain a key name (for apt::keyring) or an id (for apt::key)') + } + if $key['id'] { + # defaults like keyserver are only relevant to apt::key + $_key = merge($apt::source_key_defaults, $key) + } else { + $_key = $key } - $_key = $apt::source_key_defaults + $key } else { $_key = { 'id' => assert_type(String[1], $key) } } + if $_key['ensure'] { + $_key_ensure = $_key['ensure'] + } else { + $_key_ensure = $ensure + } + + # Old keyserver keys handled by apt-key + if $_key =~ Hash and $_key['id'] { + # We do not want to remove keys when the source is absent. + if $ensure == 'present' { + apt::key { "Add key: ${$_key['id']} from Apt::Source ${title}": + ensure => $_key_ensure, + id => $_key['id'], + server => $_key['server'], + content => $_key['content'], + source => $_key['source'], + options => $_key['options'], + weak_ssl => $_key['weak_ssl'], + before => $_before, + } + } + $_list_keyring = undef + } + # Modern apt keyrings + elsif $_key =~ Hash and $_key['name'] { + apt::keyring { $_key['name']: + ensure => $_key_ensure, + content => $_key['content'], + source => $_key['source'], + keyring_filename => $_key['filename'], + before => $_before, + } + # TODO replace this block with a reference to the apt::keyring's final filename/full_path + if $_key['filename'] { + $_list_keyring = $_key['filename'] + } else { + $_list_keyring = "/etc/apt/keyrings/${_key['name']}" + } + } + } else { + # No `key` nor `keyring` provided + $_list_keyring = undef } $header = epp('apt/_header.epp') @@ -152,7 +216,7 @@ 'arch' => $_architecture, 'trusted' => $allow_unsigned ? { true => 'yes', false => undef }, 'allow-insecure' => $allow_insecure ? { true => 'yes', false => undef }, - 'signed-by' => $keyring, + 'signed-by' => $_list_keyring, 'check-valid-until' => $check_valid_until? { true => undef, false => 'false' }, }, ), @@ -184,26 +248,4 @@ } create_resources('apt::pin', { "${name}" => $_pin }) } - - # We do not want to remove keys when the source is absent. - if $key and ($ensure == 'present') { - if $_key =~ Hash { - if $_key['ensure'] != undef { - $_ensure = $_key['ensure'] - } else { - $_ensure = $ensure - } - - apt::key { "Add key: ${$_key['id']} from Apt::Source ${title}": - ensure => $_ensure, - id => $_key['id'], - server => $_key['server'], - content => $_key['content'], - source => $_key['source'], - options => $_key['options'], - weak_ssl => $_key['weak_ssl'], - before => $_before, - } - } - } } diff --git a/spec/acceptance/apt_keyring_spec.rb b/spec/acceptance/apt_keyring_spec.rb new file mode 100644 index 0000000000..0104464ff4 --- /dev/null +++ b/spec/acceptance/apt_keyring_spec.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require 'spec_helper_acceptance' + +PUPPETLABS_KEYRING_CHECK_COMMAND = 'gpg --import /etc/apt/keyrings/puppetlabs-keyring.gpg && gpg --list-keys | grep -F -A 1 \'pub rsa4096 2019-04-08 [SC] [expires: 2025-04-06]\'' \ +'| grep \'D6811ED3ADEEB8441AF5AA8F4528B6CD9E61EF26\'' + +describe 'apt::keyring' do + context 'when using default values and source specified explicitly' do + keyring_pp = <<-MANIFEST + apt::keyring { 'puppetlabs-keyring.gpg': + source => 'https://apt.puppetlabs.com/keyring.gpg', + } + MANIFEST + + it 'applies idempotently' do + retry_on_error_matching do + idempotent_apply(keyring_pp) + end + end + + it 'expects file content to be present and correct' do + retry_on_error_matching do + run_shell(PUPPETLABS_KEYRING_CHECK_COMMAND.to_s) + end + end + end +end diff --git a/spec/defines/keyring_spec.rb b/spec/defines/keyring_spec.rb new file mode 100644 index 0000000000..6b3c65e1ef --- /dev/null +++ b/spec/defines/keyring_spec.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'apt::keyring' do + let(:title) { 'namevar' } + let(:params) do + { + source: 'http://apt.puppetlabs.com/pubkey.gpg', + } + end + + on_supported_os.each do |os, os_facts| + context "on #{os}" do + let(:facts) { os_facts } + + it { is_expected.to compile } + end + end +end