From cf7f2f434dc9da54e093113b79e9cb9f6902a9b4 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Tue, 23 Jun 2026 09:39:16 -0400 Subject: [PATCH] Add function for nodes to request a TLS certificate from confluent Also, make certificate lifetime default configurable as attribute, with 47 as explicit default. --- .../confluent/config/attributes.py | 3 +++ .../discovery/handlers/redfishbmc.py | 5 ++++- .../plugins/hardwaremanagement/redfish.py | 8 +++++++- confluent_server/confluent/selfservice.py | 20 +++++++++++-------- 4 files changed, 26 insertions(+), 10 deletions(-) diff --git a/confluent_server/confluent/config/attributes.py b/confluent_server/confluent/config/attributes.py index 0e02d28a..7f14fdcc 100644 --- a/confluent_server/confluent/config/attributes.py +++ b/confluent_server/confluent/config/attributes.py @@ -666,6 +666,9 @@ node = { 'pubkeys.tls': { 'description': ('Fingerprint of the TLS certificate for service running on host.'), }, + 'pubkeys.tls_lifetime': { + 'description': ('When granting a certificate to the node or related BMC, this is the validity in days. Default is 45 days.'), + }, 'pubkeys.ssh': { 'description': ('Fingerprint of the SSH key of the OS running on the ' 'system.'), diff --git a/confluent_server/confluent/discovery/handlers/redfishbmc.py b/confluent_server/confluent/discovery/handlers/redfishbmc.py index 094f7bf4..e2a9d008 100644 --- a/confluent_server/confluent/discovery/handlers/redfishbmc.py +++ b/confluent_server/confluent/discovery/handlers/redfishbmc.py @@ -327,8 +327,11 @@ class NodeHandler(generic.NodeHandler): nodename, {}).get('hardwaremanagement.method', {}).get('value', 'ipmi') if hwmgt_method != 'redfish': return + tls_lifetime = self.configmanager.get_node_attributes(nodename, 'pubkeys.tls_lifetime') + tls_lifetime = tls_lifetime.get(nodename, {}).get('pubkeys.tls_lifetime', {}).get('value', 45) + tls_lifetime = int(tls_lifetime) proc = await asyncio.create_subprocess_exec( - '/opt/confluent/bin/nodecertutil', nodename, 'signbmccert', '--days', '47' + '/opt/confluent/bin/nodecertutil', nodename, 'signbmccert', '--days', str(tls_lifetime) ) await proc.wait() diff --git a/confluent_server/confluent/plugins/hardwaremanagement/redfish.py b/confluent_server/confluent/plugins/hardwaremanagement/redfish.py index b62f25a6..c38745e8 100644 --- a/confluent_server/confluent/plugins/hardwaremanagement/redfish.py +++ b/confluent_server/confluent/plugins/hardwaremanagement/redfish.py @@ -595,6 +595,12 @@ class IpmiHandler: if self.element[0] == 'sign' and self.op == 'update': csr = await self.ipmicmd.get_bmc_csr() subj, san = util.get_bmc_subject_san(self.cfm, self.node, self.inputdata.get_added_names(self.node)) + try: + tls_lifetime = self.inputdata.get_days(self.node) + except KeyError: + tls_lifetime = self.cfm.get_node_attributes(self.node, 'hardwaremanagement.manager_tls_lifetime') + tls_lifetime = tls_lifetime.get(self.node, {}).get('hardwaremanagement.manager_tls_lifetime', {}).get('value', 45) + tls_lifetime = int(tls_lifetime) with tempfile.NamedTemporaryFile() as tmpfile: tmpfile.write(csr.encode()) tmpfile.flush() @@ -602,7 +608,7 @@ class IpmiHandler: certname = certfile.name certfile.close() await certutil.create_certificate(None, certname, tmpfile.name, subj, san, backdate=False, - days=self.inputdata.get_days(self.node)) + days=tls_lifetime) with open(certname, 'rb') as certf: cert = certf.read() os.unlink(certname) diff --git a/confluent_server/confluent/selfservice.py b/confluent_server/confluent/selfservice.py index 382849de..a8bdff19 100644 --- a/confluent_server/confluent/selfservice.py +++ b/confluent_server/confluent/selfservice.py @@ -18,6 +18,7 @@ import hashlib import crypt import json import os +import tempfile import time import yaml try: @@ -57,14 +58,14 @@ def listdump(input): def principals_to_san(principals): - san = [] + san = set([]) for principal in principals: try: ipaddress.ip_address(principal) - san.append('IP:' + principal) + san.add('IP:' + principal) except ValueError: - san.append('DNS:' + principal) - return san + san.add('DNS:' + principal) + return ','.join(san) async def get_extra_names(nodename, cfg, myip=None, preferadjacent=False, addlocalhost=True): if addlocalhost: @@ -409,7 +410,8 @@ async def handle_request(req, make_response, mimetype): return await make_response(mimetype, 200, 'OK', body=dumper(ncfg)) elif reqpath == '/self/tlscert' and reqbody: csr = reqbody.decode('utf8') - pals = await get_extra_names(nodename, cfg, myip) + pals = await get_extra_names(nodename, cfg, myip, addlocalhost=False) + pals.add(nodename) with tempfile.NamedTemporaryFile() as tmpfile: tmpfile.write(csr.encode()) tmpfile.flush() @@ -417,13 +419,15 @@ async def handle_request(req, make_response, mimetype): certname = certfile.name certfile.close() subj = '/CN={0}'.format(nodename) - + tls_lifetime = cfg.get_node_attributes(nodename, 'pubkeys.tls_lifetime') + tls_lifetime = tls_lifetime.get(nodename, {}).get('pubkeys.tls_lifetime', {}).get('value', 45) + tls_lifetime = int(tls_lifetime) await certutil.create_certificate(None, certname, tmpfile.name, subj, principals_to_san(pals), backdate=False, - days=3650) + days=tls_lifetime) with open(certname, 'rb') as certf: cert = certf.read() os.unlink(certname) - return await make_response('application/x-pem-file', 200, 'OK', body=cert.encode()) + return await make_response('application/x-pem-file', 200, 'OK', body=cert) elif reqpath == '/self/sshcert' and reqbody: if not sshutil.ca_exists(): return await make_response(mimetype, 500, 'Unconfigured', body='CA is not configured on this system (run ...)')