diff --git a/hieradata/roles/infra/puppet/master.yaml b/hieradata/roles/infra/puppet/master.yaml index 7673440..0517827 100644 --- a/hieradata/roles/infra/puppet/master.yaml +++ b/hieradata/roles/infra/puppet/master.yaml @@ -47,9 +47,9 @@ 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' + role_name: 'signhost' output_path: '/tmp/sshsignhost' role_id: "%{lookup('sshsignhost::role_id')}" diff --git a/site/profiles/manifests/helpers/certmanager.pp b/site/profiles/manifests/helpers/certmanager.pp index 41d1730..1234432 100644 --- a/site/profiles/manifests/helpers/certmanager.pp +++ b/site/profiles/manifests/helpers/certmanager.pp @@ -7,6 +7,10 @@ class profiles::helpers::certmanager ( Stdlib::AbsolutePath $venv_path = "${base_path}/venv", Stdlib::AbsolutePath $config_path = "${base_path}/config.yaml", Hash $vault_config = {}, + Enum['approle','kubernetes'] $auth_method = 'approle', + String[1] $k8s_mount = 'k8s/au/syd1', + String[1] $k8s_role = 'puppet_certmanager', + Stdlib::AbsolutePath $jwt_path = '/var/run/secrets/kubernetes.io/serviceaccount/token', String $owner = 'root', String $group = 'root', Boolean $systempkgs = false, @@ -16,6 +20,14 @@ class profiles::helpers::certmanager ( if $::facts['python3_version'] { + # class parameters supply the auth defaults; $vault_config may override them + $vault_settings = { + 'auth_method' => $auth_method, + 'k8s_mount' => $k8s_mount, + 'k8s_role' => $k8s_role, + 'jwt_path' => $jwt_path, + } + $vault_config + $python_version = $version ? { 'system' => $::facts['python3_version'], default => $version, diff --git a/site/profiles/manifests/helpers/sshsignhost.pp b/site/profiles/manifests/helpers/sshsignhost.pp index c27678c..693ee36 100644 --- a/site/profiles/manifests/helpers/sshsignhost.pp +++ b/site/profiles/manifests/helpers/sshsignhost.pp @@ -7,6 +7,10 @@ class profiles::helpers::sshsignhost ( Stdlib::AbsolutePath $venv_path = "${base_path}/venv", Stdlib::AbsolutePath $config_path = "${base_path}/config.yaml", Hash $vault_config = {}, + Enum['approle','kubernetes'] $auth_method = 'approle', + String[1] $k8s_mount = 'k8s/au/syd1', + String[1] $k8s_role = 'puppet_sshsigner', + Stdlib::AbsolutePath $jwt_path = '/var/run/secrets/kubernetes.io/serviceaccount/token', String $owner = 'root', String $group = 'root', Boolean $systempkgs = false, @@ -16,6 +20,14 @@ class profiles::helpers::sshsignhost ( if $::facts['python3_version'] { + # class parameters supply the auth defaults; $vault_config may override them + $vault_settings = { + 'auth_method' => $auth_method, + 'k8s_mount' => $k8s_mount, + 'k8s_role' => $k8s_role, + 'jwt_path' => $jwt_path, + } + $vault_config + $python_version = $version ? { 'system' => $::facts['python3_version'], default => $version, diff --git a/site/profiles/templates/helpers/certmanager.erb b/site/profiles/templates/helpers/certmanager.erb index 7266fde..41fbfc5 100644 --- a/site/profiles/templates/helpers/certmanager.erb +++ b/site/profiles/templates/helpers/certmanager.erb @@ -1,6 +1,7 @@ #!<%= @venv_path %>/bin/python import argparse +import sys import requests import json import os @@ -25,14 +26,50 @@ def authenticate_approle(vault_config): auth_response = response.json() return auth_response['auth']['client_token'] else: - print(f"Error authenticating with AppRole: {response.text}") + print(f"Error authenticating with AppRole: {response.text}", file=sys.stderr) return None +class VaultAuthError(Exception): + pass + +def authenticate_kubernetes(vault_config): + jwt_path = vault_config.get('jwt_path') or '/var/run/secrets/kubernetes.io/serviceaccount/token' + try: + with open(jwt_path, 'r') as file: + jwt = file.read().strip() + except OSError as error: + raise VaultAuthError(f"cannot read service account token '{jwt_path}': {error}") + if not jwt: + raise VaultAuthError(f"service account token '{jwt_path}' is empty") + url = f"{vault_config['addr']}/v1/auth/{vault_config['k8s_mount']}/login" + payload = { + "role": vault_config['k8s_role'], + "jwt": jwt, + } + response = requests.post(url, json=payload, verify=False) + if response.status_code != 200: + raise VaultAuthError( + f"kubernetes login as role '{vault_config['k8s_role']}' on mount " + f"'{vault_config['k8s_mount']}' was rejected ({response.status_code}): {response.text}" + ) + return response.json()['auth']['client_token'] + +def authenticate(vault_config): + auth_method = vault_config.get('auth_method', 'approle') + if auth_method == 'approle': + client_token = authenticate_approle(vault_config) + if not client_token: + raise VaultAuthError("approle login was rejected") + return client_token + if auth_method == 'kubernetes': + return authenticate_kubernetes(vault_config) + raise VaultAuthError(f"unsupported auth_method '{auth_method}': expected 'approle' or 'kubernetes'") + 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.") + try: + client_token = authenticate(vault_config) + except VaultAuthError as error: + print(f"Failed to authenticate with Vault: {error}", file=sys.stderr) return None url = f"{vault_config['addr']}/v1/{vault_config['mount_point']}/issue/{vault_config['role_name']}" @@ -47,7 +84,7 @@ def request_certificate(common_name, alt_names, ip_sans, expiry_days, vault_conf if response.status_code == 200: return response.json() else: - print(f"Error requesting certificate: {response.text}") + print(f"Error requesting certificate: {response.text}", file=sys.stderr) return None def save_cert_files(certificate_response, common_name, compress, config, json_output): @@ -95,7 +132,8 @@ def main(config_file): else: save_cert_files(certificate_response, args.common_name, args.compress, config, False) else: - print("Failed to obtain certificate.") + print("Failed to obtain certificate.", file=sys.stderr) + exit(1) if __name__ == "__main__": config_file = '<%= @config_path %>' diff --git a/site/profiles/templates/helpers/certmanager_config.yaml.erb b/site/profiles/templates/helpers/certmanager_config.yaml.erb index 1b3e1ed..8f863c3 100644 --- a/site/profiles/templates/helpers/certmanager_config.yaml.erb +++ b/site/profiles/templates/helpers/certmanager_config.yaml.erb @@ -1,7 +1,14 @@ vault: - addr: '<%= @vault_config['addr'] %>' - role_id: '<%= @vault_config['role_id'] %>' - approle_path: '<%= @vault_config['approle_path'] %>' - mount_point: '<%= @vault_config['mount_point'] %>' - role_name: '<%= @vault_config['role_name'] %>' -output_path: '<%= @vault_config['output_path'] %>' + addr: '<%= @vault_settings['addr'] %>' + auth_method: '<%= @vault_settings['auth_method'] %>' +<% if @vault_settings['auth_method'] == 'kubernetes' -%> + k8s_mount: '<%= @vault_settings['k8s_mount'] %>' + k8s_role: '<%= @vault_settings['k8s_role'] %>' + jwt_path: '<%= @vault_settings['jwt_path'] %>' +<% else -%> + role_id: '<%= @vault_settings['role_id'] %>' + approle_path: '<%= @vault_settings['approle_path'] %>' +<% end -%> + mount_point: '<%= @vault_settings['mount_point'] %>' + role_name: '<%= @vault_settings['role_name'] %>' +output_path: '<%= @vault_settings['output_path'] %>' diff --git a/site/profiles/templates/helpers/sshsignhost.erb b/site/profiles/templates/helpers/sshsignhost.erb index f12a6b9..ab0894b 100644 --- a/site/profiles/templates/helpers/sshsignhost.erb +++ b/site/profiles/templates/helpers/sshsignhost.erb @@ -1,5 +1,6 @@ #!<%= @venv_path %>/bin/python import argparse +import sys import requests import json import yaml @@ -22,14 +23,50 @@ def authenticate_approle(vault_config): auth_response = response.json() return auth_response['auth']['client_token'] else: - print(f"Error authenticating with AppRole: {response.text}") + print(f"Error authenticating with AppRole: {response.text}", file=sys.stderr) return None +class VaultAuthError(Exception): + pass + +def authenticate_kubernetes(vault_config): + jwt_path = vault_config.get('jwt_path') or '/var/run/secrets/kubernetes.io/serviceaccount/token' + try: + with open(jwt_path, 'r') as file: + jwt = file.read().strip() + except OSError as error: + raise VaultAuthError(f"cannot read service account token '{jwt_path}': {error}") + if not jwt: + raise VaultAuthError(f"service account token '{jwt_path}' is empty") + url = f"{vault_config['addr']}/v1/auth/{vault_config['k8s_mount']}/login" + payload = { + "role": vault_config['k8s_role'], + "jwt": jwt, + } + response = requests.post(url, json=payload, verify=False) + if response.status_code != 200: + raise VaultAuthError( + f"kubernetes login as role '{vault_config['k8s_role']}' on mount " + f"'{vault_config['k8s_mount']}' was rejected ({response.status_code}): {response.text}" + ) + return response.json()['auth']['client_token'] + +def authenticate(vault_config): + auth_method = vault_config.get('auth_method', 'approle') + if auth_method == 'approle': + client_token = authenticate_approle(vault_config) + if not client_token: + raise VaultAuthError("approle login was rejected") + return client_token + if auth_method == 'kubernetes': + return authenticate_kubernetes(vault_config) + raise VaultAuthError(f"unsupported auth_method '{auth_method}': expected 'approle' or 'kubernetes'") + 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.") + try: + client_token = authenticate(vault_config) + except VaultAuthError as error: + print(f"Failed to authenticate with Vault: {error}", file=sys.stderr) return None # Prepare the SSH certificate signing request @@ -47,7 +84,7 @@ def sign_ssh_certificate(vault_config, public_key, valid_principals, ttl): if response.status_code == 200: return response.json() else: - print(f"Error requesting certificate: {response.text}") + print(f"Error requesting certificate: {response.text}", file=sys.stderr) return None def main(config_file): @@ -75,7 +112,7 @@ def main(config_file): else: print(response['data']['signed_key']) else: - print("Error: The response does not contain the expected data.") + print("Error: The response does not contain the expected data.", file=sys.stderr) exit(1) if __name__ == "__main__": diff --git a/site/profiles/templates/helpers/sshsignhost_config.yaml.erb b/site/profiles/templates/helpers/sshsignhost_config.yaml.erb index 1b3e1ed..8f863c3 100644 --- a/site/profiles/templates/helpers/sshsignhost_config.yaml.erb +++ b/site/profiles/templates/helpers/sshsignhost_config.yaml.erb @@ -1,7 +1,14 @@ vault: - addr: '<%= @vault_config['addr'] %>' - role_id: '<%= @vault_config['role_id'] %>' - approle_path: '<%= @vault_config['approle_path'] %>' - mount_point: '<%= @vault_config['mount_point'] %>' - role_name: '<%= @vault_config['role_name'] %>' -output_path: '<%= @vault_config['output_path'] %>' + addr: '<%= @vault_settings['addr'] %>' + auth_method: '<%= @vault_settings['auth_method'] %>' +<% if @vault_settings['auth_method'] == 'kubernetes' -%> + k8s_mount: '<%= @vault_settings['k8s_mount'] %>' + k8s_role: '<%= @vault_settings['k8s_role'] %>' + jwt_path: '<%= @vault_settings['jwt_path'] %>' +<% else -%> + role_id: '<%= @vault_settings['role_id'] %>' + approle_path: '<%= @vault_settings['approle_path'] %>' +<% end -%> + mount_point: '<%= @vault_settings['mount_point'] %>' + role_name: '<%= @vault_settings['role_name'] %>' +output_path: '<%= @vault_settings['output_path'] %>'