From 33ae81893cadecd638b48b79f30f01ee8759623c Mon Sep 17 00:00:00 2001 From: unkin-agent Date: Sat, 19 Sep 2026 15:54:23 +1000 Subject: [PATCH] Install certmanager and sshsignhost from RPM certmanager and sshsignhost are now Go binaries released as RPMs, and their packaged paths collide with the venv install these helper classes manage. - Drop the pyvenv, pip, rendered script and /usr/local/bin symlink resources - Delete the now-unused Python script templates - Keep rendering /opt//config.yaml, unchanged ownership and mode - Nest certmanager's output_path under vault:, where the binary reads it - Drop output_path from sshsignhost's config; the binary has no such key - Pin certmanager and sshsignhost to 0.1.0 on the puppet master role - Point sshsignhost at the sshca mount and signhost role Depends on certmanager-0.1.0 and sshsignhost-0.1.0 in the rpm-internal repo. --- hieradata/roles/infra/puppet/master.yaml | 12 ++- .../profiles/manifests/helpers/certmanager.pp | 77 +++---------- .../profiles/manifests/helpers/sshsignhost.pp | 77 +++---------- .../templates/helpers/certmanager.erb | 102 ------------------ .../helpers/certmanager_config.yaml.erb | 2 +- .../templates/helpers/sshsignhost.erb | 83 -------------- .../helpers/sshsignhost_config.yaml.erb | 1 - 7 files changed, 38 insertions(+), 316 deletions(-) delete mode 100644 site/profiles/templates/helpers/certmanager.erb delete mode 100644 site/profiles/templates/helpers/sshsignhost.erb diff --git a/hieradata/roles/infra/puppet/master.yaml b/hieradata/roles/infra/puppet/master.yaml index 7673440..62ebff6 100644 --- a/hieradata/roles/infra/puppet/master.yaml +++ b/hieradata/roles/infra/puppet/master.yaml @@ -47,12 +47,18 @@ profiles::helpers::certmanager::vault_config: profiles::helpers::sshsignhost::vault_config: addr: 'https://vault.service.consul:8200' - mount_point: 'ssh-host-signer' + mount_point: 'sshca' approle_path: 'approle' - role_name: 'hostrole' - output_path: '/tmp/sshsignhost' + role_name: 'signhost' role_id: "%{lookup('sshsignhost::role_id')}" +# Vault signing helpers, delivered as RPMs from the rpm-internal repo. +profiles::packages::include: + certmanager: + ensure: '0.1.0' + sshsignhost: + ensure: '0.1.0' + profiles::puppet::server::agent_server: 'puppet.query.consul' profiles::puppet::server::report_server: 'puppet.query.consul' profiles::puppet::server::ca_server: 'puppetca.query.consul' diff --git a/site/profiles/manifests/helpers/certmanager.pp b/site/profiles/manifests/helpers/certmanager.pp index 41d1730..7e150bb 100644 --- a/site/profiles/manifests/helpers/certmanager.pp +++ b/site/profiles/manifests/helpers/certmanager.pp @@ -1,77 +1,28 @@ # profiles::helpers::certmanager # -# wrapper class for python, pip and venv +# renders the config.yaml read by the certmanager binary (RPM-installed) class profiles::helpers::certmanager ( String $script_name = 'certmanager', Stdlib::AbsolutePath $base_path = "/opt/${script_name}", - Stdlib::AbsolutePath $venv_path = "${base_path}/venv", Stdlib::AbsolutePath $config_path = "${base_path}/config.yaml", Hash $vault_config = {}, String $owner = 'root', String $group = 'root', - Boolean $systempkgs = false, - String $version = 'system', - Array[String[1]] $packages = ['requests', 'pyyaml'], ){ - if $::facts['python3_version'] { + file { $base_path: + ensure => directory, + mode => '0755', + owner => $owner, + group => $group, + } - $python_version = $version ? { - 'system' => $::facts['python3_version'], - default => $version, - } - - # ensure the base_path exists - file { $base_path: - ensure => directory, - mode => '0755', - owner => $owner, - group => $group, - } - - # create a venv - python::pyvenv { $venv_path : - ensure => present, - version => $python_version, - systempkgs => $systempkgs, - venv_dir => $venv_path, - owner => $owner, - group => $group, - require => File[$base_path], - } - - # install the required pip packages - $packages.each |String $package| { - python::pip { "${venv_path}_${package}": - ensure => present, - pkgname => $package, - virtualenv => $venv_path, - } - } - - # create the script from a template - file { "${base_path}/${script_name}": - ensure => file, - mode => '0755', - content => template("profiles/helpers/${script_name}.erb"), - require => Python::Pyvenv[$venv_path], - } - - # create the config from a template - file { $config_path: - ensure => file, - mode => '0660', - owner => 'puppet', - group => 'root', - content => Sensitive(template("profiles/helpers/${script_name}_config.yaml.erb")), - require => Python::Pyvenv[$venv_path], - } - - # create symbolic link in $PATH - file { "/usr/local/bin/${script_name}": - ensure => 'link', - target => "${base_path}/${script_name}", - require => File["${base_path}/${script_name}"], - } + file { $config_path: + ensure => file, + mode => '0660', + owner => 'puppet', + group => 'root', + content => Sensitive(template("profiles/helpers/${script_name}_config.yaml.erb")), + require => File[$base_path], } } diff --git a/site/profiles/manifests/helpers/sshsignhost.pp b/site/profiles/manifests/helpers/sshsignhost.pp index c27678c..1781774 100644 --- a/site/profiles/manifests/helpers/sshsignhost.pp +++ b/site/profiles/manifests/helpers/sshsignhost.pp @@ -1,77 +1,28 @@ # profiles::helpers::sshsignhost # -# wrapper class for python, pip and venv +# renders the config.yaml read by the sshsignhost binary (RPM-installed) class profiles::helpers::sshsignhost ( String $script_name = 'sshsignhost', Stdlib::AbsolutePath $base_path = "/opt/${script_name}", - Stdlib::AbsolutePath $venv_path = "${base_path}/venv", Stdlib::AbsolutePath $config_path = "${base_path}/config.yaml", Hash $vault_config = {}, String $owner = 'root', String $group = 'root', - Boolean $systempkgs = false, - String $version = 'system', - Array[String[1]] $packages = ['requests', 'pyyaml'], ){ - if $::facts['python3_version'] { + file { $base_path: + ensure => directory, + mode => '0755', + owner => $owner, + group => $group, + } - $python_version = $version ? { - 'system' => $::facts['python3_version'], - default => $version, - } - - # ensure the base_path exists - file { $base_path: - ensure => directory, - mode => '0755', - owner => $owner, - group => $group, - } - - # create a venv - python::pyvenv { $venv_path : - ensure => present, - version => $python_version, - systempkgs => $systempkgs, - venv_dir => $venv_path, - owner => $owner, - group => $group, - require => File[$base_path], - } - - # install the required pip packages - $packages.each |String $package| { - python::pip { "${venv_path}_${package}": - ensure => present, - pkgname => $package, - virtualenv => $venv_path, - } - } - - # create the script from a template - file { "${base_path}/${script_name}": - ensure => file, - mode => '0755', - content => template("profiles/helpers/${script_name}.erb"), - require => Python::Pyvenv[$venv_path], - } - - # create the config from a template - file { $config_path: - ensure => file, - mode => '0660', - owner => 'puppet', - group => 'root', - content => Sensitive(template("profiles/helpers/${script_name}_config.yaml.erb")), - require => Python::Pyvenv[$venv_path], - } - - # create symbolic link in $PATH - file { "/usr/local/bin/${script_name}": - ensure => 'link', - target => "${base_path}/${script_name}", - require => File["${base_path}/${script_name}"], - } + file { $config_path: + ensure => file, + mode => '0660', + owner => 'puppet', + group => 'root', + content => Sensitive(template("profiles/helpers/${script_name}_config.yaml.erb")), + require => File[$base_path], } } diff --git a/site/profiles/templates/helpers/certmanager.erb b/site/profiles/templates/helpers/certmanager.erb deleted file mode 100644 index 7266fde..0000000 --- a/site/profiles/templates/helpers/certmanager.erb +++ /dev/null @@ -1,102 +0,0 @@ -#!<%= @venv_path %>/bin/python - -import argparse -import requests -import json -import os -import yaml -from zipfile import ZipFile - -# remove this after certs are generated everywhere -requests.packages.urllib3.disable_warnings() - -def load_config(config_path): - with open(config_path, 'r') as file: - config = yaml.safe_load(file) - return config['vault'] - -def authenticate_approle(vault_config): - url = f"{vault_config['addr']}/v1/auth/{vault_config['approle_path']}/login" - payload = { - "role_id": vault_config['role_id'], - } - response = requests.post(url, json=payload, verify=False) - if response.status_code == 200: - auth_response = response.json() - return auth_response['auth']['client_token'] - else: - print(f"Error authenticating with AppRole: {response.text}") - return None - -def request_certificate(common_name, alt_names, ip_sans, expiry_days, vault_config): - # Authenticate using AppRole and get a token - client_token = authenticate_approle(vault_config) - if not client_token: - print("Failed to authenticate with Vault using AppRole.") - return None - - url = f"{vault_config['addr']}/v1/{vault_config['mount_point']}/issue/{vault_config['role_name']}" - headers = {'X-Vault-Token': client_token} - payload = { - "common_name": common_name, - "alt_names": ",".join(alt_names), - "ip_sans": ",".join(ip_sans), - "ttl": f"{expiry_days}d" - } - response = requests.post(url, headers=headers, json=payload, verify=False) - if response.status_code == 200: - return response.json() - else: - print(f"Error requesting certificate: {response.text}") - return None - -def save_cert_files(certificate_response, common_name, compress, config, json_output): - base_path = config.get('output_path', '.') - cert_dir = os.path.join(base_path, common_name) - if json_output: - import json - output = { - 'certificate': certificate_response['data']['certificate'], - 'private_key': certificate_response['data']['private_key'], - 'full_chain': certificate_response['data']['issuing_ca'] + "\n" + certificate_response['data']['certificate'], - } - print(json.dumps(output)) - elif not compress: - os.makedirs(cert_dir, exist_ok=True) - with open(os.path.join(cert_dir, "certificate.crt"), "w") as cert_file: - cert_file.write(certificate_response['data']['certificate']) - with open(os.path.join(cert_dir, "private.key"), "w") as key_file: - key_file.write(certificate_response['data']['private_key']) - with open(os.path.join(cert_dir, "full_chain.crt"), "w") as full_chain_file: - full_chain_file.write(certificate_response['data']['issuing_ca'] + "\n" + certificate_response['data']['certificate']) - else: - zip_name = f"{os.path.join(base_path, common_name)}.zip" - with ZipFile(zip_name, 'w') as zipf: - zipf.writestr("certificate.crt", certificate_response['data']['certificate']) - zipf.writestr("private.key", certificate_response['data']['private_key']) - zipf.writestr("full_chain.crt", certificate_response['data']['issuing_ca'] + "\n" + certificate_response['data']['certificate']) - -def main(config_file): - config = load_config(config_file) - parser = argparse.ArgumentParser(description='Request and retrieve a certificate from Vault.') - parser.add_argument('common_name', type=str, help='Common Name for the certificate') - parser.add_argument('-a', '--alt-names', type=str, default='', help='Comma-separated alternative names for the certificate') - parser.add_argument('-i', '--ip-sans', type=str, default='', help='Comma-separated IP Subject Alternative Names for the certificate') - parser.add_argument('-e', '--expiry-days', type=int, default=365, help='Validity of the certificate in days (default: 365)') - parser.add_argument('-c', '--compress', action='store_true', help='Compress the certificate, key, and full chain into a zip file') - parser.add_argument('--json', action='store_true', help='Output results in JSON format') - args = parser.parse_args() - alt_names = [name.strip() for name in args.alt_names.split(',') if name] - ip_sans = [ip.strip() for ip in args.ip_sans.split(',') if ip] - certificate_response = request_certificate(args.common_name, alt_names, ip_sans, args.expiry_days, config) - if certificate_response: - if args.json: - save_cert_files(certificate_response, args.common_name, args.compress, config, True) - else: - save_cert_files(certificate_response, args.common_name, args.compress, config, False) - else: - print("Failed to obtain certificate.") - -if __name__ == "__main__": - config_file = '<%= @config_path %>' - main(config_file) diff --git a/site/profiles/templates/helpers/certmanager_config.yaml.erb b/site/profiles/templates/helpers/certmanager_config.yaml.erb index 1b3e1ed..855a206 100644 --- a/site/profiles/templates/helpers/certmanager_config.yaml.erb +++ b/site/profiles/templates/helpers/certmanager_config.yaml.erb @@ -4,4 +4,4 @@ vault: approle_path: '<%= @vault_config['approle_path'] %>' mount_point: '<%= @vault_config['mount_point'] %>' role_name: '<%= @vault_config['role_name'] %>' -output_path: '<%= @vault_config['output_path'] %>' + output_path: '<%= @vault_config['output_path'] %>' diff --git a/site/profiles/templates/helpers/sshsignhost.erb b/site/profiles/templates/helpers/sshsignhost.erb deleted file mode 100644 index f12a6b9..0000000 --- a/site/profiles/templates/helpers/sshsignhost.erb +++ /dev/null @@ -1,83 +0,0 @@ -#!<%= @venv_path %>/bin/python -import argparse -import requests -import json -import yaml - -# remove this after certs are generated everywhere -requests.packages.urllib3.disable_warnings() - -def load_config(config_path): - with open(config_path, 'r') as file: - config = yaml.safe_load(file) - return config['vault'] - -def authenticate_approle(vault_config): - url = f"{vault_config['addr']}/v1/auth/{vault_config['approle_path']}/login" - payload = { - "role_id": vault_config['role_id'], - } - response = requests.post(url, json=payload, verify=False) - if response.status_code == 200: - auth_response = response.json() - return auth_response['auth']['client_token'] - else: - print(f"Error authenticating with AppRole: {response.text}") - return None - -def sign_ssh_certificate(vault_config, public_key, valid_principals, ttl): - # Authenticate using AppRole and get a token - client_token = authenticate_approle(vault_config) - if not client_token: - print("Failed to authenticate with Vault using AppRole.") - return None - - # Prepare the SSH certificate signing request - url = f"{vault_config['addr']}/v1/{vault_config['mount_point']}/sign/{vault_config['role_name']}" - headers = {'X-Vault-Token': client_token} - payload = { - "cert_type": "host", - "public_key": public_key, - "valid_principals": valid_principals, - "ttl": ttl - } - - # Request the SSH certificate signing - response = requests.post(url, headers=headers, json=payload, verify=False) - if response.status_code == 200: - return response.json() - else: - print(f"Error requesting certificate: {response.text}") - return None - -def main(config_file): - config = load_config(config_file) - parser = argparse.ArgumentParser(description='Sign SSH host certificate using Vault.') - parser.add_argument('--public_key', required=True, help='SSH public key as a string') - parser.add_argument('--valid_principals', required=True, help='Comma-separated list of valid principals') - parser.add_argument('--ttl', default='87600h', help='Time-to-live for the certificate (default: 87600h)') - parser.add_argument('--json', action='store_true', help='Output the resulting certificate as JSON') - - args = parser.parse_args() - - # Load configuration - config = load_config(config_file) - - # Sign SSH certificate - response = sign_ssh_certificate(config, args.public_key, args.valid_principals, args.ttl) - - if response and 'data' in response and 'signed_key' in response['data']: - if args.json: - output = { - 'signed_key': response['data']['signed_key'], - } - print(json.dumps(output)) - else: - print(response['data']['signed_key']) - else: - print("Error: The response does not contain the expected data.") - exit(1) - -if __name__ == "__main__": - config_file = '<%= @config_path %>' - main(config_file) diff --git a/site/profiles/templates/helpers/sshsignhost_config.yaml.erb b/site/profiles/templates/helpers/sshsignhost_config.yaml.erb index 1b3e1ed..8bf7f1f 100644 --- a/site/profiles/templates/helpers/sshsignhost_config.yaml.erb +++ b/site/profiles/templates/helpers/sshsignhost_config.yaml.erb @@ -4,4 +4,3 @@ vault: approle_path: '<%= @vault_config['approle_path'] %>' mount_point: '<%= @vault_config['mount_point'] %>' role_name: '<%= @vault_config['role_name'] %>' -output_path: '<%= @vault_config['output_path'] %>'