Files
puppet-prod/modules/libs/lib/facter/enc_direct_facts.rb
T
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

96 lines
2.7 KiB
Ruby

# frozen_string_literal: true
require 'facter'
require 'yaml'
require 'net/http'
require 'openssl'
require 'uri'
require 'fileutils'
# 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
return {} unless File.exist?(CACHE_FILE)
cache_data = YAML.safe_load(File.read(CACHE_FILE)) || {}
timestamp = cache_data.fetch('timestamp', 0)
return cache_data if Time.now.to_i - timestamp < CACHE_TTL
{}
end
def self.write_cache(enc_data)
FileUtils.mkdir_p(File.dirname(CACHE_FILE))
cache_data = enc_data.merge({ 'timestamp' => Time.now.to_i })
File.write(CACHE_FILE, cache_data.to_yaml)
end
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)
YAML.safe_load(response.body) || {}
end
def self.retrieve_enc_data
return @enc_data if @enc_data
@enc_data = fetch_from_encapi
write_cache(@enc_data)
@enc_data
end
def self.fetch_enc_data
retrieve_enc_data
rescue StandardError => e
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 encapi is unreachable.'
end
def self.enc_role
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 encapi ENC response')
end
end
Facter.add('enc_role') do
setcode { EncapiENC.enc_role }
end
Facter.add('enc_env') do
setcode { EncapiENC.enc_env }
end