Merge pull request 'feat: add certmanager helper' (#118) from neoloc/certmanager into develop

Reviewed-on: unkinben/puppet-prod#118
This commit is contained in:
Ben Vincent 2024-02-19 19:53:36 +09:30
commit 4cdba982fe
6 changed files with 160 additions and 0 deletions

View File

@ -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]

View File

@ -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')}"

View File

@ -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}"],
}
}
}

View File

@ -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,

View File

@ -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)

View File

@ -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'] %>'