Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ee726e2220 | |||
| 33ae81893c |
@@ -47,12 +47,18 @@ profiles::helpers::certmanager::vault_config:
|
|||||||
|
|
||||||
profiles::helpers::sshsignhost::vault_config:
|
profiles::helpers::sshsignhost::vault_config:
|
||||||
addr: 'https://vault.service.consul:8200'
|
addr: 'https://vault.service.consul:8200'
|
||||||
mount_point: 'ssh-host-signer'
|
mount_point: 'sshca'
|
||||||
approle_path: 'approle'
|
approle_path: 'approle'
|
||||||
role_name: 'hostrole'
|
role_name: 'signhost'
|
||||||
output_path: '/tmp/sshsignhost'
|
|
||||||
role_id: "%{lookup('sshsignhost::role_id')}"
|
role_id: "%{lookup('sshsignhost::role_id')}"
|
||||||
|
|
||||||
|
# Vault signing helpers, delivered as RPMs from the rpm-internal repo.
|
||||||
|
profiles::packages::include:
|
||||||
|
certmanager:
|
||||||
|
ensure: '0.2.0'
|
||||||
|
sshsignhost:
|
||||||
|
ensure: '0.1.0'
|
||||||
|
|
||||||
profiles::puppet::server::agent_server: 'puppet.query.consul'
|
profiles::puppet::server::agent_server: 'puppet.query.consul'
|
||||||
profiles::puppet::server::report_server: 'puppet.query.consul'
|
profiles::puppet::server::report_server: 'puppet.query.consul'
|
||||||
profiles::puppet::server::ca_server: 'puppetca.query.consul'
|
profiles::puppet::server::ca_server: 'puppetca.query.consul'
|
||||||
|
|||||||
@@ -1,77 +1,28 @@
|
|||||||
# profiles::helpers::certmanager
|
# 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 (
|
class profiles::helpers::certmanager (
|
||||||
String $script_name = 'certmanager',
|
String $script_name = 'certmanager',
|
||||||
Stdlib::AbsolutePath $base_path = "/opt/${script_name}",
|
Stdlib::AbsolutePath $base_path = "/opt/${script_name}",
|
||||||
Stdlib::AbsolutePath $venv_path = "${base_path}/venv",
|
|
||||||
Stdlib::AbsolutePath $config_path = "${base_path}/config.yaml",
|
Stdlib::AbsolutePath $config_path = "${base_path}/config.yaml",
|
||||||
Hash $vault_config = {},
|
Hash $vault_config = {},
|
||||||
String $owner = 'root',
|
String $owner = 'root',
|
||||||
String $group = '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 ? {
|
file { $config_path:
|
||||||
'system' => $::facts['python3_version'],
|
ensure => file,
|
||||||
default => $version,
|
mode => '0660',
|
||||||
}
|
owner => 'puppet',
|
||||||
|
group => 'root',
|
||||||
# ensure the base_path exists
|
content => Sensitive(template("profiles/helpers/${script_name}_config.yaml.erb")),
|
||||||
file { $base_path:
|
require => 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}"],
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,77 +1,28 @@
|
|||||||
# profiles::helpers::sshsignhost
|
# 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 (
|
class profiles::helpers::sshsignhost (
|
||||||
String $script_name = 'sshsignhost',
|
String $script_name = 'sshsignhost',
|
||||||
Stdlib::AbsolutePath $base_path = "/opt/${script_name}",
|
Stdlib::AbsolutePath $base_path = "/opt/${script_name}",
|
||||||
Stdlib::AbsolutePath $venv_path = "${base_path}/venv",
|
|
||||||
Stdlib::AbsolutePath $config_path = "${base_path}/config.yaml",
|
Stdlib::AbsolutePath $config_path = "${base_path}/config.yaml",
|
||||||
Hash $vault_config = {},
|
Hash $vault_config = {},
|
||||||
String $owner = 'root',
|
String $owner = 'root',
|
||||||
String $group = '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 ? {
|
file { $config_path:
|
||||||
'system' => $::facts['python3_version'],
|
ensure => file,
|
||||||
default => $version,
|
mode => '0660',
|
||||||
}
|
owner => 'puppet',
|
||||||
|
group => 'root',
|
||||||
# ensure the base_path exists
|
content => Sensitive(template("profiles/helpers/${script_name}_config.yaml.erb")),
|
||||||
file { $base_path:
|
require => 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}"],
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
|
||||||
@@ -4,4 +4,4 @@ vault:
|
|||||||
approle_path: '<%= @vault_config['approle_path'] %>'
|
approle_path: '<%= @vault_config['approle_path'] %>'
|
||||||
mount_point: '<%= @vault_config['mount_point'] %>'
|
mount_point: '<%= @vault_config['mount_point'] %>'
|
||||||
role_name: '<%= @vault_config['role_name'] %>'
|
role_name: '<%= @vault_config['role_name'] %>'
|
||||||
output_path: '<%= @vault_config['output_path'] %>'
|
output_path: '<%= @vault_config['output_path'] %>'
|
||||||
|
|||||||
@@ -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)
|
|
||||||
@@ -4,4 +4,3 @@ vault:
|
|||||||
approle_path: '<%= @vault_config['approle_path'] %>'
|
approle_path: '<%= @vault_config['approle_path'] %>'
|
||||||
mount_point: '<%= @vault_config['mount_point'] %>'
|
mount_point: '<%= @vault_config['mount_point'] %>'
|
||||||
role_name: '<%= @vault_config['role_name'] %>'
|
role_name: '<%= @vault_config['role_name'] %>'
|
||||||
output_path: '<%= @vault_config['output_path'] %>'
|
|
||||||
|
|||||||
Reference in New Issue
Block a user