Compare commits

..

7 Commits

Author SHA1 Message Date
unkin-agent f8cee2225b ship journald logs to the k8s log ingest endpoint
ci/woodpecker/pr/ruby-validate Pipeline was successful
ci/woodpecker/pr/yamllint Pipeline was successful
ci/woodpecker/pr/puppet-lint Pipeline was successful
ci/woodpecker/pr/erb-validate Pipeline was successful
ci/woodpecker/pr/bolt-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
2026-09-26 18:34:17 +10:00
unkin-agent cb9f8870bf Trust the sshca host CA alongside the legacy signer (#530)
Catalog compilation moved to the k8s puppetserver compilers, which sign host certificates against the terraform-managed `sshca` mount. Clients only trust the legacy `ssh-host-signer` CA, so every re-signed node (ausyd1nxvm2120 already) presents a certificate nothing accepts, and knownhosts emits no plain host-key fallback.

- Add a second `@cert-authority *` entry for the `sshca` public key to `profiles::ssh::knownhosts::lines`.
- Keep the legacy entry untouched so legacy-signed hosts still verify.

Reviewed-on: #530
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-09-24 22:46:30 +10:00
unkin-agent 734fcb8cf4 Trust the estate CA when fetching ENC facts (#529)
Facter runs under Puppet's vendored Ruby, which reads its own bundled CA file and never the system trust store. The ENC fact now fetches over HTTPS, so every node fails certificate verification and falls back to its cached value.

- set ca_file on the request to the vaultca anchor bundle
- keep VERIFY_PEER on, and fall through to the existing cache path when the anchor is absent

Reviewed-on: #529
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-09-20 23:32:16 +10:00
unkin-agent f933660d3b Fetch agent ENC facts from encapi (#528)
The enc_role and enc_env facts still resolve against Cobbler on every agent, the last Cobbler dependency in the classification path now that the master-side ENC runs encapic-enc.

- Point the fact at https://encapi.k8s.syd1.au.unkin.net
- Rename the module and its messages from Cobbler to encapi

Cache file, TTL and fallback-to-cache failure behaviour are unchanged.

Reviewed-on: #528
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-09-20 23:08:20 +10:00
unkin-agent 3370aff38f Classify puppet masters through encapi (#527)
The VM puppet masters are the last part of the classification path still calling Cobbler; the k8s compilers already classify through encapi and the encapic RPM is installed on all six masters.

- Point `profiles::puppet::server::external_nodes` at `/usr/bin/encapic-enc` for `roles::infra::puppet::master`.
- Drop the stale comment about external_nodes still using cobbler-enc.

`profiles::puppet::cobbler_enc` stays in place so the revert is one hiera line.
Depends on the encapic 0.2.0 install (#525).

Reviewed-on: #527
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-09-20 22:42:43 +10:00
unkin-agent 1a907467e9 Shorten metadata_expire on internal RPM repos (#526)
A package pinned in hieradata right after its RPM lands in artifactapi is invisible to dnf until the host's 1h cached metadata expires, so the first Puppet run after a release cannot find the version.

- Set `metadata_expire` 60s on `rpm-internal`/`rpm-vendor` and their per-release variants for AlmaLinux and Fedora
- Leave upstream mirrors on the 1h default
- Drop the stale expiry note in `profiles::dns::updater`

Reviewed-on: #526
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-09-20 00:55:19 +10:00
unkin-agent cf25a20a92 Install encapic ENC client on puppet masters (#525)
The VM masters classify through the cobbler ENC while the k8s compilers
already use encapi. Install the client ahead of that cutover;
external_nodes still points at cobbler-enc, so classification is unchanged.

- pin the encapic package to 0.2.0 via profiles::packages::include
- add profiles::puppet::encapic managing /etc/encapic/encapic.conf from a
  hiera-driven ENCAPI_URL, ordered after Package['encapic'] so the config is
  written once the RPM that owns the path is installed
- include the class from profiles::puppet::puppetmaster

Requires encapic 0.2.0 in the rpm-internal repo.

Reviewed-on: #525
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-09-20 00:32:40 +10:00
14 changed files with 112 additions and 156 deletions
+2 -1
View File
@@ -367,6 +367,7 @@ ssh::server::options:
profiles::ssh::knownhosts::lines:
- '@cert-authority * ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC1HD97vYxLTniE4qNpGuftUlvmkEXIuX8+7nbENv/IzsGUghEDRtyThjQ7ojNKIsQ7f8wXr0gMcI+fAPfrbcOMHCAoYMomikwL0b3h95SZI40q3CyM+0DMnwiVVDX6C1QxkO2Rv9cszSkCa85NotJhXiUuTBI9BFcRPy+mAhbpAru+bfypYofI0wW97XNTl8Jgwmni5MgutBIQAokFIn5ux8iWxndCH3AqDtmkwC5DfQeQ+wZx7rkwqJEpJffQzrjb1gIM6P9hDCVBBVPh/3o80IJ69rFWrJAZUb+JpG4cXJH0NcSW+wqc3JCT/x3q8VlHwOTXSlNNKtOJCRx73mB8e1XTTy2a9FgpKDDg5XQXWHAViJDz1RTRL9gRefMylRgKz4bXoTuY9kJWM8hPTyUejtukbJThlBJc3OmDxBZBF7F0iqB11pHexok43OCEiANodVa36eWu9/5X032Vm48fZ1/akDPY/NSy3wAn7kwut+A0/JAHFHASrq+1mt9YurkJegI+YHXO6eEWpBIpmI7ORHJbGL4MhkHrxYzVamuP8CkU7tXzsv138+wpOcRHNp9yJY4PT40BZkRf/O3O+jt3pj9Dj8rvgywF2W6hFzywh3Y78upOprRkQlQtHfsI8EyrYI8/hUw2u3H+3yPXh3YjWfqvWVG1BRLRHBV7m90uaw=='
- '@cert-authority * ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDi80G0GtKMdRj4azPwxbJW46NG0y8seSVSnvFm2Ka/nckdUb/3lRlQuPYf1tkbqqFlcXjDM8+u0tzM2kLsgfn0Dpujm1fuoA66yGGweBjtxLFErH5+6+/KND5I9w8LMY2AtVztnBk/CVx8RwgrooABDRZiBH7OOAOpJqqFI7LvEsv61zC0nAqbYJ8uxfx+r4lJ9dUhK1woitEqC4npSRUn5KJK+KAEd7AzcUkZb6TO9A3oRPz1nQ/qU+QNMmVUi+wRj9kJR18mxErugyLLTNcFDBqSdHNun3eUNBUmoS2cAa8ZVscOLzbUGejVK9UY06Q7wu+J34Fzl8CN5tBqRHGZ3ykqGZ6gG+O0Egr9Rm3Obgkujpio2uTh27LhBy72vjgJ8kTFmPlzANTJFpKlsy91usSFPh8WZeIo6VpPLqnC32rjfNkfqHyPAeJ3+rdOkUZoDjMoZc3wxxLZTgMU5ud1w4LxQNRkNiaT/tRRNQF8o+pygkS7xxKduBBMzYdRS1OifaS4gyvP82oOyquWywhyFNtG7ph8FQwc34puM7FyxfaJ0XL/+zAfPMhDoOojvofADJo73R+FgVyer+mCJw4KtWJ4JzZrNTYz1Xl8WlhF8RDzOYfSH46qzJFa9FcRqgP4LUkgzdvcQ+1hBFpvpHtw3vl3DiqtZDDbK9SyrEtdnw=='
profiles::base::groups::local:
admins:
@@ -400,7 +401,7 @@ networking::route_defaults:
# logging:
victorialogs::client::journald::enable: true
victorialogs::client::journald::inserturl: https://vlinsert.service.consul:9428/insert/journald
victorialogs::client::journald::inserturl: https://logs-ingest.k8s.syd1.au.unkin.net/insert/journald
# FIXME these are for the proxmox ceph cluster
profiles::ceph::client::fsid: 7f7f00cb-95de-498c-8dcc-14b54e4e9ca8
+4
View File
@@ -77,6 +77,7 @@ profiles::yum::global::repos:
baseurl: https://artifactapi.k8s.syd1.au.unkin.net/api/v1/local/rpm-internal/
gpgcheck: false
mirrorlist: absent
metadata_expire: '60'
rpm-vendor:
name: rpm-vendor
descr: rpm-vendor repository
@@ -84,6 +85,7 @@ profiles::yum::global::repos:
baseurl: https://artifactapi.k8s.syd1.au.unkin.net/api/v1/local/rpm-vendor/
gpgcheck: false
mirrorlist: absent
metadata_expire: '60'
# Per-release variants, resolved from the host's EL major version so el8
# hosts pull rpm-internal-el8/rpm-vendor-el8, el9 hosts el9, etc.
rpm-internal-release:
@@ -93,6 +95,7 @@ profiles::yum::global::repos:
baseurl: https://artifactapi.k8s.syd1.au.unkin.net/api/v1/local/rpm-internal-el%{facts.os.release.major}/
gpgcheck: false
mirrorlist: absent
metadata_expire: '60'
rpm-vendor-release:
name: rpm-vendor-el%{facts.os.release.major}
descr: rpm-vendor-el%{facts.os.release.major} repository
@@ -100,6 +103,7 @@ profiles::yum::global::repos:
baseurl: https://artifactapi.k8s.syd1.au.unkin.net/api/v1/local/rpm-vendor-el%{facts.os.release.major}/
gpgcheck: false
mirrorlist: absent
metadata_expire: '60'
# Additional repositories - default to absent, roles can override with ensure: present
# FRRouting repositories
+2
View File
@@ -60,6 +60,7 @@ profiles::yum::global::repos:
baseurl: https://artifactapi.k8s.syd1.au.unkin.net/api/v1/local/rpm-internal-f%{facts.os.release.major}/
gpgcheck: false
mirrorlist: absent
metadata_expire: '60'
rpm-vendor:
name: rpm-vendor-f%{facts.os.release.major}
descr: rpm-vendor-f%{facts.os.release.major} repository
@@ -67,3 +68,4 @@ profiles::yum::global::repos:
baseurl: https://artifactapi.k8s.syd1.au.unkin.net/api/v1/local/rpm-vendor-f%{facts.os.release.major}/
gpgcheck: false
mirrorlist: absent
metadata_expire: '60'
+11 -2
View File
@@ -26,6 +26,15 @@ profiles::puppet::cobbler_enc::packages:
- 'requests'
- 'PyYAML'
profiles::puppet::enc::repo: https://git.service.au-syd1.consul/unkinben/puppet-enc.git
# Deep-merged with the entries in roles/infra/puppet.yaml.
profiles::packages::include:
encapic:
ensure: '0.2.0'
profiles::puppet::encapic::encapi_url: https://encapi.k8s.syd1.au.unkin.net
profiles::puppet::server::external_nodes: '/usr/bin/encapic-enc'
profiles::puppet::r10k::r10k_repo: https://git.unkin.net/unkin/puppet-r10k.git
profiles::puppet::g10k::bin_path: '/usr/bin/g10k'
profiles::puppet::g10k::cfg_path: '/etc/puppetlabs/r10k/r10k.yaml'
@@ -47,9 +56,9 @@ profiles::helpers::certmanager::vault_config:
profiles::helpers::sshsignhost::vault_config:
addr: 'https://vault.service.consul:8200'
mount_point: 'sshca'
mount_point: 'ssh-host-signer'
approle_path: 'approle'
role_name: 'signhost'
role_name: 'hostrole'
output_path: '/tmp/sshsignhost'
role_id: "%{lookup('sshsignhost::role_id')}"
+33 -12
View File
@@ -3,13 +3,21 @@
require 'facter'
require 'yaml'
require 'net/http'
require 'openssl'
require 'uri'
require 'fileutils'
# CobblerENC module: Fetches ENC data from Cobbler, caches it, and provides structured facts.
module CobblerENC
# EncapiENC module: Fetches ENC data from encapi, caches it, and provides structured facts.
module EncapiENC
CACHE_FILE = '/var/cache/puppet_enc.yaml'
CACHE_TTL = 7 * 24 * 60 * 60 # 7 days in seconds
# Facter runs under Puppet's vendored ruby, whose OpenSSL trusts only
# /opt/puppetlabs/puppet/ssl/cert.pem and never the system trust store, so the
# estate CA anchor profiles::pki::vaultca installs has to be named explicitly.
CA_BUNDLE_PATHS = [
'/etc/pki/ca-trust/source/anchors/vaultcaroot.pem',
'/usr/local/share/ca-certificates/vaultcaroot.pem'
].freeze
@enc_data = nil # In-memory cache for the ENC response
def self.read_cache
@@ -29,9 +37,22 @@ module CobblerENC
File.write(CACHE_FILE, cache_data.to_yaml)
end
def self.fetch_from_cobbler
uri = URI("http://cobbler.main.unkin.net/cblr/svc/op/puppet/hostname/#{Facter.value(:fqdn) || Facter.value(:hostname)}")
response = Net::HTTP.get_response(uri)
def self.ca_bundle
CA_BUNDLE_PATHS.find { |path| File.exist?(path) }
end
def self.http_client(uri)
client = Net::HTTP.new(uri.host, uri.port)
client.use_ssl = true
client.verify_mode = OpenSSL::SSL::VERIFY_PEER
bundle = ca_bundle
client.ca_file = bundle if bundle
client
end
def self.fetch_from_encapi
uri = URI("https://encapi.k8s.syd1.au.unkin.net/cblr/svc/op/puppet/hostname/#{Facter.value(:fqdn) || Facter.value(:hostname)}")
response = http_client(uri).request(Net::HTTP::Get.new(uri))
raise "Failed to fetch ENC data. HTTP #{response.code}" unless response.is_a?(Net::HTTPSuccess)
@@ -41,7 +62,7 @@ module CobblerENC
def self.retrieve_enc_data
return @enc_data if @enc_data
@enc_data = fetch_from_cobbler
@enc_data = fetch_from_encapi
write_cache(@enc_data)
@enc_data
end
@@ -49,26 +70,26 @@ module CobblerENC
def self.fetch_enc_data
retrieve_enc_data
rescue StandardError => e
Facter.warn("Error retrieving Cobbler ENC data: #{e.message}")
Facter.warn("Error retrieving encapi ENC data: #{e.message}")
@enc_data = read_cache
return @enc_data unless @enc_data.empty?
raise 'No cached ENC data available and Cobbler is down.'
raise 'No cached ENC data available and encapi is unreachable.'
end
def self.enc_role
fetch_enc_data.fetch('classes', {}).keys.first || raise('ENC Role not found in Cobbler ENC response')
fetch_enc_data.fetch('classes', {}).keys.first || raise('ENC Role not found in encapi ENC response')
end
def self.enc_env
fetch_enc_data.fetch('environment', nil) || raise('ENC Environment not found in Cobbler ENC response')
fetch_enc_data.fetch('environment', nil) || raise('ENC Environment not found in encapi ENC response')
end
end
Facter.add('enc_role') do
setcode { CobblerENC.enc_role }
setcode { EncapiENC.enc_role }
end
Facter.add('enc_env') do
setcode { CobblerENC.enc_env }
setcode { EncapiENC.enc_env }
end
+1 -2
View File
@@ -24,8 +24,7 @@ class profiles::dns::updater (
Stdlib::AbsolutePath $config_dir = '/etc/dns-updater',
Stdlib::AbsolutePath $master_basedir = lookup('profiles::dns::master::basedir'),
# dns-updater daemon (replaces the dns-update shell script). 'latest' so hosts
# pick up new releases (e.g. the record filter); rpm-internal metadata_expire
# is 1h so this does not thrash.
# pick up new releases (e.g. the record filter).
String $package_ensure = 'latest',
Stdlib::AbsolutePath $api_socket = '/run/dns-updater/api.sock',
String $resync = '10m',
@@ -7,10 +7,6 @@ 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,
@@ -20,14 +16,6 @@ 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,10 +7,6 @@ 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,
@@ -20,14 +16,6 @@ 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,
+32
View File
@@ -0,0 +1,32 @@
# Class: profiles::puppet::encapic
#
# Manages the configuration for the encapic ENC client. The package itself is
# installed through profiles::packages (pinned in hiera); this class owns the
# config so the encapi endpoint can change without repackaging.
class profiles::puppet::encapic (
Stdlib::HTTPUrl $encapi_url,
Stdlib::AbsolutePath $config_dir = '/etc/encapic',
String $config_name = 'encapic.conf',
String $owner = 'root',
String $group = 'root',
) {
# The RPM ships this file as %config(noreplace), so puppet must write it only
# once the package is present or the install overwrites it.
file { $config_dir:
ensure => directory,
mode => '0755',
owner => $owner,
group => $group,
require => Package['encapic'],
}
file { "${config_dir}/${config_name}":
ensure => file,
mode => '0644',
owner => $owner,
group => $group,
content => "ENCAPI_URL=${encapi_url}\n",
require => File[$config_dir],
}
}
@@ -12,6 +12,7 @@ class profiles::puppet::puppetmaster (
include profiles::puppet::g10k
include profiles::puppet::enc
include profiles::puppet::cobbler_enc
include profiles::puppet::encapic
include profiles::puppet::autosign
include profiles::puppet::gems
include profiles::helpers::certmanager
@@ -1,7 +1,6 @@
#!<%= @venv_path %>/bin/python
import argparse
import sys
import requests
import json
import os
@@ -26,50 +25,14 @@ def authenticate_approle(vault_config):
auth_response = response.json()
return auth_response['auth']['client_token']
else:
print(f"Error authenticating with AppRole: {response.text}", file=sys.stderr)
print(f"Error authenticating with AppRole: {response.text}")
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):
try:
client_token = authenticate(vault_config)
except VaultAuthError as error:
print(f"Failed to authenticate with Vault: {error}", file=sys.stderr)
# 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']}"
@@ -84,7 +47,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}", file=sys.stderr)
print(f"Error requesting certificate: {response.text}")
return None
def save_cert_files(certificate_response, common_name, compress, config, json_output):
@@ -132,8 +95,7 @@ def main(config_file):
else:
save_cert_files(certificate_response, args.common_name, args.compress, config, False)
else:
print("Failed to obtain certificate.", file=sys.stderr)
exit(1)
print("Failed to obtain certificate.")
if __name__ == "__main__":
config_file = '<%= @config_path %>'
@@ -1,14 +1,7 @@
vault:
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'] %>'
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'] %>'
@@ -1,6 +1,5 @@
#!<%= @venv_path %>/bin/python
import argparse
import sys
import requests
import json
import yaml
@@ -23,50 +22,14 @@ def authenticate_approle(vault_config):
auth_response = response.json()
return auth_response['auth']['client_token']
else:
print(f"Error authenticating with AppRole: {response.text}", file=sys.stderr)
print(f"Error authenticating with AppRole: {response.text}")
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):
try:
client_token = authenticate(vault_config)
except VaultAuthError as error:
print(f"Failed to authenticate with Vault: {error}", file=sys.stderr)
# 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
@@ -84,7 +47,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}", file=sys.stderr)
print(f"Error requesting certificate: {response.text}")
return None
def main(config_file):
@@ -112,7 +75,7 @@ def main(config_file):
else:
print(response['data']['signed_key'])
else:
print("Error: The response does not contain the expected data.", file=sys.stderr)
print("Error: The response does not contain the expected data.")
exit(1)
if __name__ == "__main__":
@@ -1,14 +1,7 @@
vault:
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'] %>'
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'] %>'