Use the pki tool to bootstrap certificates during installation

We previously called out to certmonger to have it directly
obtain certificates from the CA. Instead use the CA-generated
/root/ca-admin.p12 certificate to authenticate using the pki
tool to generate the IPA RA certificate.

Then use that certificate to issue the DS, HTTP and KDC
certificates.

Fixes: https://pagure.io/freeipa/issue/9738

Signed-off-by: Rob Crittenden <rcritten@redhat.com>
Reviewed-By: Fraser Tweedale <ftweedal@redhat.com>
Reviewed-By: Florence Blanc-Renaud <frenaud@redhat.com>
Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com>
Reviewed-By: Rafael Guterres Jeffman <rjeffman@redhat.com>
Reviewed-By: Thomas Woerner <twoerner@redhat.com>
This commit is contained in:
Rob Crittenden
2025-11-19 16:50:37 -05:00
parent d018361c6f
commit cdc8054453
7 changed files with 289 additions and 129 deletions
+1
View File
@@ -107,6 +107,7 @@ dist_app_DATA = \
pki-acme-issuer.conf.template \
pki-acme-realm.conf.template \
ldbm-tuning.ldif \
openssl_cnf.template \
$(NULL)
kdcproxyconfdir = $(IPA_SYSCONF_DIR)/kdcproxy
+36
View File
@@ -0,0 +1,36 @@
[req]
distinguished_name = req_distinguished_name
attributes = req_attrs
req_extensions = req_ext
prompt = no
[req_distinguished_name]
O = ${REALM}
CN = ${FQDN}
[req_attrs]
friendlyName = ASN1:BMPString:${NICKNAME}
[req_ext]
subjectAltName = @alt_names
basicConstraints = critical, CA:false
subjectKeyIdentifier = hash
1.3.6.1.4.1.311.20.2 = ASN1:BMPString:${PROFILE}
[alt_names]
DNS.1 = ${FQDN}
${DNS_2}
otherName.1 = 1.3.6.1.4.1.311.20.2.3;UTF8:${SERVICE}/${FQDN}@${REALM}
otherName.2 = 1.3.6.1.5.2.2;SEQUENCE:princ_name
[princ_name]
realm=EXP:0,GeneralString:${REALM}
principal_name=EXP:1,SEQUENCE:principal_seq
[principal_seq]
name_type=EXP:0,INTEGER:1
name_string=EXP:1,SEQUENCE:principals
[principals]
princ1=GeneralString:${SERVICE}
princ2=GeneralString:${FQDN}
+60 -74
View File
@@ -49,7 +49,10 @@ from ipaplatform.tasks import tasks
from ipapython import directivesetter
from ipapython import dogtag
from ipapython import ipautil
from ipapython.certdb import get_ca_nickname
from ipapython.certdb import (
get_ca_nickname,
IPA_CA_TRUST_FLAGS,
EMPTY_TRUST_FLAGS)
from ipapython.dn import DN, RDN
from ipapython.ipa_log_manager import standard_logging_setup
from ipaserver.secrets.kem import IPAKEMKeys
@@ -934,88 +937,71 @@ class CAInstance(DogtagInstance):
in a usual deployment would be used in the UI to handle
administrative duties. IPA does not use this certificate
except as a bootstrap to generate the RA.
To do this it bends over backwards a bit by modifying the
way typical certificates are retrieved using certmonger by
forcing it to call dogtag-submit directly.
"""
with tempfile.TemporaryDirectory() as tmpdir:
tmpdb = certs.CertDB(self.realm, nssdir=tmpdir)
chain_file = os.path.join(tmpdir, "chain.pem")
# create a temp PEM file storing the CA chain
chain_file = tempfile.NamedTemporaryFile(
mode="w", dir=paths.VAR_LIB_IPA, delete=False)
chain_file.close()
chain = self.__get_ca_chain()
data = base64.b64decode(chain)
ipautil.run(
[paths.OPENSSL,
"pkcs7",
"-inform",
"DER",
"-print_certs",
"-out", chain_file,
], stdin=data, capture_output=False)
chain = self.__get_ca_chain()
data = base64.b64decode(chain)
ipautil.run(
[paths.OPENSSL,
"pkcs7",
"-inform",
"DER",
"-print_certs",
"-out", chain_file.name,
], stdin=data, capture_output=False)
tmpdb.create_noise_file()
tmpdb.create_passwd_file()
tmpdb.create_certdbs()
tmpdb.load_cacert(chain_file, IPA_CA_TRUST_FLAGS)
# CA agent cert in PEM form
agent_cert = tempfile.NamedTemporaryFile(
mode="w", dir=paths.VAR_LIB_IPA, delete=False)
agent_cert.close()
tmpdb.import_pkcs12(
paths.DOGTAG_ADMIN_P12, pkcs12_passwd=self.dm_password)
# CA agent key in PEM form
agent_key = tempfile.NamedTemporaryFile(
mode="w", dir=paths.VAR_LIB_IPA, delete=False)
agent_key.close()
certs.install_pem_from_p12(paths.DOGTAG_ADMIN_P12,
self.dm_password,
agent_cert.name)
certs.install_key_from_p12(paths.DOGTAG_ADMIN_P12,
self.dm_password,
agent_key.name)
agent_args = [paths.CERTMONGER_DOGTAG_SUBMIT,
"--cafile", chain_file.name,
"--ee-url", 'http://%s:8080/ca/ee/ca/' % self.fqdn,
"--agent-url",
'https://%s:8443/ca/agent/ca/' % self.fqdn,
"--certfile", agent_cert.name,
"--keyfile", agent_key.name, ]
helper = " ".join(agent_args)
# configure certmonger renew agent to use temporary agent cert
old_helper = certmonger.modify_ca_helper(
ipalib.constants.RENEWAL_CA_NAME, helper)
try:
# The certificate must be requested using caSubsystemCert profile
# because this profile does not require agent authentication
reqId = certmonger.request_and_wait_for_cert(
certpath=(paths.RA_AGENT_PEM, paths.RA_AGENT_KEY),
principal='host/%s' % self.fqdn,
subject=str(DN(('CN', 'IPA RA'), self.subject_base)),
ca=ipalib.constants.RENEWAL_CA_NAME,
profile=ipalib.constants.RA_AGENT_PROFILE,
pre_command='renew_ra_cert_pre',
post_command='renew_ra_cert',
storage="FILE",
resubmit_timeout=api.env.certmonger_wait_timeout
ipautil.run(
[paths.CERTUTIL,
"-d", tmpdb.secdir,
"-R", "-s", str(DN(('CN', 'IPA RA'), self.subject_base)),
"-g", "2048",
"-z", os.path.join(tmpdb.secdir, tmpdb.noise_fname),
"-f", tmpdb.passwd_fname,
"-o", os.path.join(tmpdb.secdir, "csr"),
"-a",]
)
self._set_ra_cert_perms()
self.requestId = str(reqId)
tmpdb.pki_issue_certificate(
service=None,
profile="caSubsystemCert",
subject="CN=IPA RA",
keyfile=None,
certfile=paths.RA_AGENT_PEM,
key_passwd_file=None,
use_admin=True)
self.ra_cert = x509.load_certificate_from_file(
paths.RA_AGENT_PEM)
finally:
# we can restore the helper parameters
certmonger.modify_ca_helper(
ipalib.constants.RENEWAL_CA_NAME, old_helper)
# remove any temporary files
for f in (chain_file, agent_cert, agent_key):
try:
os.remove(f.name)
except OSError:
pass
tmpdb.add_cert(self.ra_cert, 'IPA RA', EMPTY_TRUST_FLAGS)
pk12_pwdfile = ipautil.write_tmp_file(self.dm_password)
tmpdb.export_pkcs12(
os.path.join(tmpdb.secdir, "ra.p12"),
pk12_pwdfile.name,
'IPA RA')
certs.install_key_from_p12(
os.path.join(tmpdb.secdir, "ra.p12"),
self.dm_password, paths.RA_AGENT_KEY)
self._set_ra_cert_perms()
update_people_entry(self.ra_cert)
certmonger.start_tracking(
certpath=(paths.RA_AGENT_PEM, paths.RA_AGENT_KEY),
ca=ipalib.constants.RENEWAL_CA_NAME,
profile=ipalib.constants.RA_AGENT_PROFILE,
pre_command='renew_ra_cert_pre',
post_command='renew_ra_cert',
storage='FILE',
)
def prepare_crl_publish_dir(self):
"""
+117
View File
@@ -727,6 +727,123 @@ class CertDB:
"""
self.nssdb.convert_db()
def pki_issue_certificate(self, service, profile, subject,
keyfile, certfile, key_passwd_file=None,
dns_2_san='', use_admin=False):
"""Use openssl to generate a CSR and submit it using the pki
cli tool.
There are effective two modes for this depending on the value
of use_admin. When use_admin is True we are issuing the RA
certificate using the CA admin certificate. When it is False
we are using the RA agent certificate to issue certificates for
services during installation.
use_admin = True
- service and profile can be None
- fetch the CA chain manually
- the request needs to be manually approved
"""
def get_string(instring, key):
start = instring.find(key)
if start == -1:
raise RuntimeError(
"Unable to find %s in output" % key)
return instring[start + len(key):].split()[0]
if use_admin:
nickname = 'ipa-ca-agent'
else:
ipautil.run(
[paths.OPENSSL, 'pkcs12', '-export',
'-in', paths.RA_AGENT_PEM,
'-out', os.path.join(self.secdir, 'agent.p12'),
'-inkey', paths.RA_AGENT_KEY,
'-name', 'IPA RA',
'-password', 'file:{}'.format(self.passwd_fname),
'-keypbe', 'AES-256-CBC',
'-certpbe', 'AES-256-CBC',
'-macalg', 'sha384',]
)
with open(self.passwd_fname, "r") as fd:
key_password = fd.read()
self.import_pkcs12(
os.path.join(self.secdir, 'agent.p12'),
pkcs12_passwd=key_password
)
if service == "krbtgt":
host = api.env.realm
else:
host = api.env.host
template = os.path.join(
paths.USR_SHARE_IPA_DIR, "openssl_cnf.template")
sub_dict = dict(
REALM=api.env.realm,
FQDN=host,
NICKNAME="Server-Cert",
PROFILE=profile,
SERVICE=service,
DNS_2=dns_2_san,
)
conf = ipautil.template_file(template, sub_dict)
destfile = os.path.join(self.secdir, "openssl.cnf")
with open(destfile, 'w') as f:
os.fchmod(f.fileno(), 0o600)
f.write(conf)
args = ["openssl", "req", "-new",
"-out", os.path.join(self.secdir, "csr"),
"-keyout", keyfile,
"-config", destfile]
if key_passwd_file:
args.extend(["-passout", "file:{}".format(key_passwd_file)])
else:
args.extend(["-nodes"])
result = ipautil.run(args, capture_output=True)
nickname = 'IPA RA'
result = ipautil.run(
["pki", "-d", self.secdir,
"-C", self.passwd_fname,
"-n", nickname,
"ca-cert-request-submit",
"--profile", profile,
"--subject", subject,
"--csr", os.path.join(self.secdir, "csr")],
capture_output=True)
if use_admin:
request_id = get_string(result.output, 'Request ID:')
status = get_string(result.output, 'Request Status:')
if status != "pending":
raise RuntimeError(
"The certificate submission was not successful")
result = ipautil.run(
["pki", "-C", self.passwd_fname,
"-d", self.secdir,
"-n", "ipa-ca-agent",
"ca-cert-request-approve",
"--force", request_id],
capture_output=True)
serial_number = get_string(result.output, 'Certificate ID:')
status = get_string(result.output, 'Operation Result:')
if status != "success":
raise RuntimeError(
"The certificate submission was not successful")
# The profile auto-issues so no need to approve it
result = ipautil.run(
["pki",
"-d", self.secdir,
"ca-cert-export",
"--output-file", certfile,
"--output-format", "pem",
serial_number],
capture_output=True)
class _CrossProcessLock:
_DATETIME_FORMAT = '%Y%m%d%H%M%S%f'
+30 -19
View File
@@ -855,19 +855,34 @@ class DsInstance(service.Service):
# rewrite the pin file with current password
dsdb.create_pin_file()
if self.master_fqdn is None:
ca_args = [
paths.CERTMONGER_DOGTAG_SUBMIT,
'--ee-url', 'https://%s:8443/ca/ee/ca' % self.fqdn,
'--certfile', paths.RA_AGENT_PEM,
'--keyfile', paths.RA_AGENT_KEY,
'--cafile', paths.IPA_CA_CRT,
'--agent-submit'
]
helper = " ".join(ca_args)
prev_helper = certmonger.modify_ca_helper('IPA', helper)
with tempfile.TemporaryDirectory() as tmpdir:
tmpdb = certs.CertDB(api.env.realm, nssdir=tmpdir)
tmpdb.create_from_cacert()
keyfile = os.path.join(tmpdb.secdir, "key.pem")
certfile = os.path.join(tmpdb.secdir, "cert.pem")
tmpdb.pki_issue_certificate(
"ldap", dogtag.DEFAULT_PROFILE,
str(DN(('CN', self.fqdn), self.subject_base)),
keyfile, certfile
)
ipautil.run(
[paths.OPENSSL, 'pkcs12', '-export',
'-in', certfile,
'-out', os.path.join(tmpdb.secdir, 'server.p12'),
'-inkey', keyfile,
'-password', 'file:{}'.format(tmpdb.passwd_fname),
'-name', 'Server-Cert',
'-keypbe', 'AES-256-CBC',
'-certpbe', 'AES-256-CBC',
'-macalg', 'sha384',]
)
with open(tmpdb.passwd_fname, "r") as fd:
key_password = fd.read()
dsdb.import_pkcs12(
os.path.join(tmpdb.secdir, 'server.p12'), key_password
)
else:
prev_helper = None
try:
cmd = 'restart_dirsrv %s' % self.serverid
certmonger.request_and_wait_for_cert(
certpath=dirname,
@@ -882,18 +897,14 @@ class DsInstance(service.Service):
post_command=cmd,
resubmit_timeout=api.env.certmonger_wait_timeout
)
finally:
if prev_helper is not None:
certmonger.modify_ca_helper('IPA', prev_helper)
# restart_dirsrv in the request above restarts DS, reconnect ldap2
api.Backend.ldap2.disconnect()
api.Backend.ldap2.connect()
self.cert = dsdb.get_cert_from_db(self.nickname)
if prev_helper is not None:
self.add_cert_to_service()
self.add_cert_to_service()
if self.master_fqdn is None:
self.start_tracking_certificates(self.serverid)
self.cacert_name = dsdb.cacert_name
+15 -34
View File
@@ -25,6 +25,7 @@ import os
import glob
import shlex
import shutil
import tempfile
from augeas import Augeas
import dbus
@@ -337,22 +338,21 @@ class HTTPInstance(service.Service):
self.start_tracking_certificates()
self.add_cert_to_service()
else:
if not self.promote:
ca_args = [
paths.CERTMONGER_DOGTAG_SUBMIT,
'--ee-url', 'https://%s:8443/ca/ee/ca' % self.fqdn,
'--certfile', paths.RA_AGENT_PEM,
'--keyfile', paths.RA_AGENT_KEY,
'--cafile', paths.IPA_CA_CRT,
'--agent-submit'
]
helper = " ".join(ca_args)
prev_helper = certmonger.modify_ca_helper('IPA', helper)
with tempfile.TemporaryDirectory() as tmpdir:
tmpdb = certs.CertDB(api.env.realm, nssdir=tmpdir)
tmpdb.create_from_cacert()
dns_2 = f"DNS.2={IPA_CA_RECORD}.{api.env.domain}"
tmpdb.pki_issue_certificate(
"HTTP", dogtag.DEFAULT_PROFILE,
str(DN(('CN', self.fqdn), self.subject_base)),
paths.HTTPD_KEY_FILE, paths.HTTPD_CERT_FILE,
key_passwd_file, dns_2_san=dns_2
)
self.start_tracking_certificates()
else:
prev_helper = None
try:
# In migration case, if CA server is older version it may not
# have codepaths to support the ipa-ca.$DOMAIN dnsName in HTTP
# cert. Therefore if request fails, try again without the
@@ -376,28 +376,9 @@ class HTTPInstance(service.Service):
args['dns'] = [self.fqdn] # remove ipa-ca.$DOMAIN
args['stop_tracking_on_error'] = False
certmonger.request_and_wait_for_cert(**args)
finally:
if prev_helper is not None:
certmonger.modify_ca_helper('IPA', prev_helper)
self.cert = x509.load_certificate_from_file(
paths.HTTPD_CERT_FILE
)
if prev_helper is not None:
self.add_cert_to_service()
with open(paths.HTTPD_KEY_FILE, 'rb') as f:
priv_key = x509.load_pem_private_key(
f.read(), pkey_passwd, backend=x509.default_backend())
# Verify we have a valid server cert
if (priv_key.public_key().public_numbers()
!= self.cert.public_key().public_numbers()):
raise RuntimeError(
"The public key of the issued HTTPD service certificate "
"does not match its private key.")
sysupgrade.set_upgrade_state('ssl.conf', 'migrated_to_mod_ssl', True)
self.cert = x509.load_certificate_from_file(paths.HTTPD_CERT_FILE)
self.add_cert_to_service()
def configure_mod_ssl_certs(self):
"""Configure the mod_ssl certificate directives"""
+30 -2
View File
@@ -23,6 +23,7 @@ from __future__ import print_function
import logging
import os
import socket
import tempfile
import dbus
import dns.name
@@ -197,11 +198,14 @@ class KrbInstance(service.Service):
self.step("starting the KDC", self.__start_instance)
self.step("configuring KDC to start on boot", self.__enable)
def create_instance(self, realm_name, host_name, domain_name, admin_password, master_password, setup_pkinit=False, pkcs12_info=None, subject_base=None):
def create_instance(self, realm_name, host_name, domain_name,
admin_password, master_password, setup_pkinit=False,
pkcs12_info=None, subject_base=None, promote=False):
self.master_password = master_password
self.pkcs12_info = pkcs12_info
self.subject_base = subject_base
self.config_pkinit = setup_pkinit
self.promote = promote
self.__common_setup(realm_name, host_name, domain_name, admin_password)
@@ -234,6 +238,7 @@ class KrbInstance(service.Service):
self.subject_base = subject_base
self.master_fqdn = master_fqdn
self.config_pkinit = setup_pkinit
self.promote = True
self.__common_setup(realm_name, host_name, domain_name, admin_password)
@@ -442,6 +447,26 @@ class KrbInstance(service.Service):
timeout=api.env.replication_wait_timeout
)
def _get_certificate(self):
with tempfile.TemporaryDirectory() as tmpdir:
tmpdb = certs.CertDB(api.env.realm, nssdir=tmpdir)
tmpdb.create_from_cacert()
tmpdb.pki_issue_certificate(
"krbtgt", KDC_PROFILE,
str(DN(('CN', self.fqdn), self.subject_base)),
paths.KDC_KEY, paths.KDC_CERT
)
os.chmod(paths.KDC_CERT, 0o644)
self.cert = x509.load_certificate_from_file(paths.KDC_CERT)
certmonger.start_tracking(
certpath=(paths.KDC_CERT, paths.KDC_KEY),
post_command='renew_kdc_cert',
dns=[self.fqdn],
storage='FILE',
profile=KDC_PROFILE,
)
def _call_certmonger(self, certmonger_ca='IPA'):
subject = str(DN(('cn', self.fqdn), self.subject_base))
krbtgt = "krbtgt/" + self.realm + "@" + self.realm
@@ -538,7 +563,10 @@ class KrbInstance(service.Service):
def issue_ipa_ca_signed_pkinit_certs(self):
try:
self._call_certmonger()
if self.promote:
self._call_certmonger()
else:
self._get_certificate()
self._install_pkinit_ca_bundle()
self.pkinit_enable()
except RuntimeError as e: