diff --git a/hieradata/roles/infra/puppet/master.eyaml b/hieradata/roles/infra/puppet/master.eyaml new file mode 100644 index 0000000..ab3e7f1 --- /dev/null +++ b/hieradata/roles/infra/puppet/master.eyaml @@ -0,0 +1,2 @@ +--- +certmanager::vault_token: ENC[PKCS7,MIIBygYJKoZIhvcNAQcDoIIBuzCCAbcCAQAxggEhMIIBHQIBADAFMAACAQEwDQYJKoZIhvcNAQEBBQAEggEAXnyY0VPJZ/EFBzgYBGbTQUpqcHSlGVRisDtoV54LCWM02MBFtIALvBdRovt7qP0rU1EYKObVN2r/AzxG1pOVkQdAb8IcJXochjz+kstxP8z1ZpXENOFmD8PWoqstvppC9r0RrCCXOgDCvffdV+XygKg5/LLBjOcf8cR6hsyGpgIn8xO5L2nrzQFl9/ROb3mh7/0OL3dEqyQXF74rAn3pWq4yjlbWNK0aku5gQOaNfVn2Q7+3nMYwUsGSrN1ikVSKsa4pMbEMf6qN+EqpbVMKFPXvdw+OXBkHbKpqYHHSCPN9bDJeT1icYk61DwJSJ3GFi/zREbdSNgTdZ7yNqnxvwDCBjAYJKoZIhvcNAQcBMB0GCWCGSAFlAwQBKgQQ+d/jLP79UV3MypBSdFteiYBgU539y/m6r2oiYwVeIDzUrPfLdoQpZCCg8mFSYlFiD1ZyhKeq+qLvExmdbL95f9oLF2n9D7bMt+A5iefVWzrK6UcvVJuZ5slU3bqsfhlieIFiV8EMP6N/LuUphWnwuzA5] diff --git a/hieradata/roles/infra/puppet/master.yaml b/hieradata/roles/infra/puppet/master.yaml index f47db83..39e92b4 100644 --- a/hieradata/roles/infra/puppet/master.yaml +++ b/hieradata/roles/infra/puppet/master.yaml @@ -18,3 +18,10 @@ profiles::puppet::gems::puppet: - 'deep_merge' - 'ipaddr' - 'hiera-eyaml' + +profiles::helpers::certmanager::vault_config: + addr: 'https://198.18.17.39:8200' + mount_point: 'pki_int' + role_name: 'unkin-dot-net' + output_path: '/tmp/certmanager' + token: "%{lookup('certmanager::vault_token')}" diff --git a/site/profiles/manifests/helpers/certmanager.pp b/site/profiles/manifests/helpers/certmanager.pp new file mode 100644 index 0000000..860d0ea --- /dev/null +++ b/site/profiles/manifests/helpers/certmanager.pp @@ -0,0 +1,75 @@ +# profiles::helpers::certmanager +# +# wrapper class for python, pip and venv +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'] { + + $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 => '0600', + 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}"], + } + } +} diff --git a/site/profiles/manifests/puppet/puppetmaster.pp b/site/profiles/manifests/puppet/puppetmaster.pp index 627f247..bf7254b 100644 --- a/site/profiles/manifests/puppet/puppetmaster.pp +++ b/site/profiles/manifests/puppet/puppetmaster.pp @@ -30,6 +30,7 @@ class profiles::puppet::puppetmaster ( include profiles::puppet::enc include profiles::puppet::autosign include profiles::puppet::gems + include profiles::helpers::certmanager class { 'puppetdb::master::config': puppetdb_server => $puppetdb_host, diff --git a/site/profiles/templates/helpers/certmanager.erb b/site/profiles/templates/helpers/certmanager.erb new file mode 100644 index 0000000..59c9ae8 --- /dev/null +++ b/site/profiles/templates/helpers/certmanager.erb @@ -0,0 +1,68 @@ +#!/usr/bin/env <%= @venv_path %>/bin/python + +import argparse +import requests +import json +import os +import yaml +from zipfile import ZipFile + +def load_config(config_path): + with open(config_path, 'r') as file: + config = yaml.safe_load(file) + return config['vault'] + +def request_certificate(common_name, alt_names, ip_sans, expiry_days, vault_config): + url = f"{vault_config['addr']}/v1/{vault_config['mount_point']}/issue/{vault_config['role_name']}" + headers = {'X-Vault-Token': vault_config['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): + base_path = config.get('output_path', '.') + cert_dir = os.path.join(base_path, common_name) + if 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') + 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: + save_cert_files(certificate_response, args.common_name, args.compress, config) + 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 new file mode 100644 index 0000000..aea4d18 --- /dev/null +++ b/site/profiles/templates/helpers/certmanager_config.yaml.erb @@ -0,0 +1,7 @@ +vault: + addr: '<%= @vault_config['addr'] %>' + token: '<%= @vault_config['token'] %>' + mount_point: '<%= @vault_config['mount_point'] %>' + role_name: '<%= @vault_config['role_name'] %>' +output_path: '<%= @vault_config['output_path'] %>' +