From d8b354558de83e9fe53ad067d033500f6179c4c2 Mon Sep 17 00:00:00 2001 From: Ben Vincent Date: Fri, 17 Oct 2025 22:46:26 +1100 Subject: [PATCH] feat: add incus auto-client certificate trust (#406) - add fact to export vault public cert from agents - add fact to export list of trusted incus client certs - add method for incus clients to export their client cert to be trusted Reviewed-on: https://git.unkin.net/unkin/puppet-prod/pulls/406 --- hieradata/roles/infra/git/runner.yaml | 1 + modules/incus/lib/facter/incus_trust_list.rb | 28 +++++++++++++ modules/incus/manifests/client.pp | 16 ++++++++ modules/incus/manifests/client_cert.pp | 41 +++++++++++++++++++ modules/incus/manifests/init.pp | 5 +++ modules/libs/lib/facter/vault_cert_content.rb | 11 +++++ .../libs/lib/facter/vault_cert_fingerprint.rb | 23 +++++++++++ 7 files changed, 125 insertions(+) create mode 100644 modules/incus/lib/facter/incus_trust_list.rb create mode 100644 modules/incus/manifests/client.pp create mode 100644 modules/incus/manifests/client_cert.pp create mode 100644 modules/libs/lib/facter/vault_cert_content.rb create mode 100644 modules/libs/lib/facter/vault_cert_fingerprint.rb diff --git a/hieradata/roles/infra/git/runner.yaml b/hieradata/roles/infra/git/runner.yaml index 851c6df..24b0d94 100644 --- a/hieradata/roles/infra/git/runner.yaml +++ b/hieradata/roles/infra/git/runner.yaml @@ -2,6 +2,7 @@ hiera_include: - docker - profiles::gitea::runner + - incus::client docker::version: latest docker::curl_ensure: false diff --git a/modules/incus/lib/facter/incus_trust_list.rb b/modules/incus/lib/facter/incus_trust_list.rb new file mode 100644 index 0000000..e2a45f0 --- /dev/null +++ b/modules/incus/lib/facter/incus_trust_list.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +# lib/facter/incus_trust_list.rb +require 'json' + +Facter.add(:incus_trust_list) do + confine do + # Only run on systems that have incus installed and running + incus_path = Facter::Util::Resolution.which('incus') + incus_path && File.exist?('/var/lib/incus/server.key') + end + + setcode do + incus_path = Facter::Util::Resolution.which('incus') + next {} unless incus_path + + begin + # Run incus config trust list --format=json + trust_output = Facter::Core::Execution.execute("#{incus_path} config trust list --format=json") + next {} if trust_output.empty? + + # Parse the JSON output + JSON.parse(trust_output) + rescue StandardError + {} + end + end +end diff --git a/modules/incus/manifests/client.pp b/modules/incus/manifests/client.pp new file mode 100644 index 0000000..732ccb0 --- /dev/null +++ b/modules/incus/manifests/client.pp @@ -0,0 +1,16 @@ +# incus::client +# +# This class configures a host as an incus client and exports its certificate +# for automatic trust management on incus servers. +# +class incus::client { + + # Export this client's certificate for collection by incus servers + @@incus::client_cert { $facts['networking']['fqdn']: + hostname => $facts['networking']['fqdn'], + certificate => $facts['vault_cert_content'], + fingerprint => $facts['vault_cert_fingerprint'], + tag => 'incus_client', + } + +} \ No newline at end of file diff --git a/modules/incus/manifests/client_cert.pp b/modules/incus/manifests/client_cert.pp new file mode 100644 index 0000000..9222d39 --- /dev/null +++ b/modules/incus/manifests/client_cert.pp @@ -0,0 +1,41 @@ +# Define the exported resource type for incus client certificates +define incus::client_cert ( + String $hostname, + Optional[String] $certificate = undef, + Optional[String] $fingerprint = undef, +) { + + # Only proceed if we have both certificate and fingerprint + if $certificate and $fingerprint { + + $trust_list = $facts['incus_trust_list'] + $existing_client = $trust_list.filter |$client| { $client['name'] == $hostname } + + if $existing_client.empty { + # Add new certificate + exec { "incus_trust_add_${hostname}": + path => ['/bin', '/usr/bin'], + command => "echo '${certificate}' > /tmp/${hostname}.crt && \ + incus config trust add-certificate /tmp/${hostname}.crt --name ${hostname} && \ + rm -f /tmp/${hostname}.crt", + unless => "incus config trust list --format=json | grep '\"name\":\"${hostname}\"'", + } + } else { + # Check if fingerprints are different + $existing_fingerprint = $existing_client[0]['fingerprint'] + + if $existing_fingerprint != $fingerprint { + # Remove existing and add new certificate only if fingerprints differ + exec { "incus_trust_update_${hostname}": + path => ['/bin', '/usr/bin'], + command => "incus config trust remove ${existing_fingerprint} && \ + echo '${certificate}' > /tmp/${hostname}.crt && \ + incus config trust add-certificate /tmp/${hostname}.crt --name ${hostname} && \ + rm -f /tmp/${hostname}.crt", + onlyif => "incus config trust list --format=json | grep '${existing_fingerprint}'", + } + } + # If fingerprints match, do nothing (certificate is already correct) + } + } +} diff --git a/modules/incus/manifests/init.pp b/modules/incus/manifests/init.pp index 5bbac37..bdc1e13 100644 --- a/modules/incus/manifests/init.pp +++ b/modules/incus/manifests/init.pp @@ -92,5 +92,10 @@ class incus ( } } } + + # Collect exported client certificates and manage trust + Incus::Client_cert <<| tag == 'incus_client' |>> { + require => Service['incus'], + } } } diff --git a/modules/libs/lib/facter/vault_cert_content.rb b/modules/libs/lib/facter/vault_cert_content.rb new file mode 100644 index 0000000..25af28c --- /dev/null +++ b/modules/libs/lib/facter/vault_cert_content.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +# lib/facter/vault_cert_content.rb + +Facter.add(:vault_cert_content) do + confine kernel: 'Linux' + setcode do + cert_path = '/etc/pki/tls/vault/certificate.crt' + File.read(cert_path) if File.exist?(cert_path) && File.readable?(cert_path) + end +end diff --git a/modules/libs/lib/facter/vault_cert_fingerprint.rb b/modules/libs/lib/facter/vault_cert_fingerprint.rb new file mode 100644 index 0000000..a828e72 --- /dev/null +++ b/modules/libs/lib/facter/vault_cert_fingerprint.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +# lib/facter/vault_cert_fingerprint.rb + +Facter.add(:vault_cert_fingerprint) do + confine kernel: 'Linux' + setcode do + require 'openssl' + require 'digest' + + cert_path = '/etc/pki/tls/vault/certificate.crt' + if File.exist?(cert_path) && File.readable?(cert_path) + begin + cert_content = File.read(cert_path) + cert = OpenSSL::X509::Certificate.new(cert_content) + # Calculate SHA256 fingerprint like incus does + Digest::SHA256.hexdigest(cert.to_der) + rescue StandardError + nil + end + end + end +end