Let certmanager and sshsignhost authenticate to Vault by kubernetes auth
ci/woodpecker/pr/ruby-validate Pipeline was successful
ci/woodpecker/pr/puppet-lint Pipeline was successful
ci/woodpecker/pr/bolt-validate Pipeline was successful
ci/woodpecker/pr/yamllint Pipeline was successful
ci/woodpecker/pr/erb-validate Pipeline was successful
ci/woodpecker/pr/epp-validate Pipeline was successful
ci/woodpecker/pr/puppet-validate Pipeline was successful
ci/woodpecker/pr/ruby-check Pipeline was successful
ci/woodpecker/pr/ruby-validate Pipeline was successful
ci/woodpecker/pr/puppet-lint Pipeline was successful
ci/woodpecker/pr/bolt-validate Pipeline was successful
ci/woodpecker/pr/yamllint Pipeline was successful
ci/woodpecker/pr/erb-validate Pipeline was successful
ci/woodpecker/pr/epp-validate Pipeline was successful
ci/woodpecker/pr/puppet-validate Pipeline was successful
ci/woodpecker/pr/ruby-check Pipeline was successful
Both helpers are run server-side by generate() during catalog compilation and only speak AppRole, whose token_bound_cidrs pin them to the six legacy VM masters, so the autoscaled k8s compilers cannot obtain a token and any compile needing a cert or a signed host key fails there. - Add a kubernetes login branch that reads the service account JWT and posts it to auth/<k8s_mount>/login, selected by an auth_method config key - Add auth_method, k8s_mount, k8s_role and jwt_path class parameters, defaulting to approle so the VM masters render and behave as before - Render role_id and approle_path only for the approle case - Report a missing JWT, a rejected login or an unknown auth_method on stderr instead of falling back or raising - Point sshsignhost at the sshca mount and signhost role that Vault actually has Needs terraform-vault #152, already applied.
This commit is contained in:
@@ -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')}"
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 %>'
|
||||
|
||||
@@ -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'] %>'
|
||||
|
||||
@@ -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__":
|
||||
|
||||
@@ -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'] %>'
|
||||
|
||||
Reference in New Issue
Block a user