mirror of
https://salsa.debian.org/freeipa-team/freeipa.git
synced 2024-12-23 15:40:01 -06:00
5509e00a82
ipa-cert-fix man page needs to explain that certmonger may trigger a renewal right after ipa-cert-fix completes because certmonger does not notice the updated certificates. Also add a similar note at the end of ipa-cert-fix. Fixes: https://pagure.io/freeipa/issue/8702 Signed-off-by: Florence Blanc-Renaud <flo@redhat.com> Reviewed-By: Mohammad Rizwan <myusuf@redhat.com> Reviewed-By: Rob Crittenden <rcritten@redhat.com>
436 lines
14 KiB
Python
436 lines
14 KiB
Python
#
|
|
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
|
|
#
|
|
|
|
# ipa-cert-fix performs the following steps:
|
|
#
|
|
# 1. Confirm running as root (AdminTool.validate_options does this)
|
|
#
|
|
# 2. Confirm that DS is up.
|
|
#
|
|
# 3. Determine which of following certs (if any) need renewing
|
|
# - IPA RA
|
|
# - Apache HTTPS
|
|
# - 389 LDAPS
|
|
# - Kerberos KDC (PKINIT)
|
|
#
|
|
# 4. Execute `pki-server cert-fix` with relevant options,
|
|
# including `--extra-cert SERIAL` for each cert from #3.
|
|
#
|
|
# 5. Print details of renewed certificates.
|
|
#
|
|
# 6. Install renewed certs from #3 in relevant places
|
|
#
|
|
# 7. ipactl restart
|
|
|
|
from __future__ import print_function, absolute_import
|
|
|
|
import base64
|
|
from cryptography import x509 as crypto_x509
|
|
from cryptography.hazmat.backends import default_backend
|
|
import datetime
|
|
from enum import Enum
|
|
import logging
|
|
import shutil
|
|
|
|
from ipalib import api
|
|
from ipalib import x509
|
|
from ipalib.facts import is_ipa_configured
|
|
from ipalib.install import certmonger
|
|
from ipaplatform.paths import paths
|
|
from ipapython.admintool import AdminTool
|
|
from ipapython.certdb import NSSDatabase, EMPTY_TRUST_FLAGS
|
|
from ipapython.dn import DN
|
|
from ipapython.ipaldap import realm_to_serverid
|
|
from ipaserver.install import ca, cainstance, dsinstance
|
|
from ipaserver.install.certs import is_ipa_issued_cert
|
|
from ipapython import directivesetter
|
|
from ipapython import ipautil
|
|
|
|
msg = """
|
|
WARNING
|
|
|
|
ipa-cert-fix is intended for recovery when expired certificates
|
|
prevent the normal operation of IPA. It should ONLY be used
|
|
in such scenarios, and backup of the system, especially certificates
|
|
and keys, is STRONGLY RECOMMENDED.
|
|
|
|
"""
|
|
|
|
renewal_note = """
|
|
Note: Monitor the certmonger-initiated renewal of
|
|
certificates after ipa-cert-fix and wait for its completion before
|
|
any other administrative task.
|
|
"""
|
|
|
|
RENEWED_CERT_PATH_TEMPLATE = "/etc/pki/pki-tomcat/certs/{}-renewed.crt"
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
cert_nicknames = {
|
|
'sslserver': 'Server-Cert cert-pki-ca',
|
|
'subsystem': 'subsystemCert cert-pki-ca',
|
|
'ca_ocsp_signing': 'ocspSigningCert cert-pki-ca',
|
|
'ca_audit_signing': 'auditSigningCert cert-pki-ca',
|
|
'kra_transport': 'transportCert cert-pki-kra',
|
|
'kra_storage': 'storageCert cert-pki-kra',
|
|
'kra_audit_signing': 'auditSigningCert cert-pki-kra',
|
|
}
|
|
|
|
class IPACertType(Enum):
|
|
IPARA = "IPA RA"
|
|
HTTPS = "Apache HTTPS"
|
|
LDAPS = "LDAP"
|
|
KDC = "KDC"
|
|
|
|
|
|
class IPACertFix(AdminTool):
|
|
command_name = "ipa-cert-fix"
|
|
usage = "%prog"
|
|
description = "Renew expired certificates."
|
|
|
|
def validate_options(self):
|
|
super(IPACertFix, self).validate_options(needs_root=True)
|
|
|
|
def run(self):
|
|
if not is_ipa_configured():
|
|
print("IPA is not configured.")
|
|
return 2
|
|
|
|
if not cainstance.is_ca_installed_locally():
|
|
print("CA is not installed on this server.")
|
|
return 1
|
|
|
|
try:
|
|
ipautil.run(['pki-server', 'cert-fix', '--help'], raiseonerr=True)
|
|
except ipautil.CalledProcessError:
|
|
print(
|
|
"The 'pki-server cert-fix' command is not available; "
|
|
"cannot proceed."
|
|
)
|
|
return 1
|
|
|
|
api.bootstrap(in_server=True, confdir=paths.ETC_IPA)
|
|
api.finalize()
|
|
|
|
if not dsinstance.is_ds_running(realm_to_serverid(api.env.realm)):
|
|
print(
|
|
"The LDAP server is not running; cannot proceed."
|
|
)
|
|
return 1
|
|
|
|
api.Backend.ldap2.connect() # ensure DS is up
|
|
|
|
subject_base = dsinstance.DsInstance().find_subject_base()
|
|
if not subject_base:
|
|
raise RuntimeError("Cannot determine certificate subject base.")
|
|
|
|
ca_subject_dn = ca.lookup_ca_subject(api, subject_base)
|
|
|
|
now = datetime.datetime.now() + datetime.timedelta(weeks=2)
|
|
certs, extra_certs, non_renewed = expired_certs(now)
|
|
|
|
if not certs and not extra_certs:
|
|
print("Nothing to do.")
|
|
return 0
|
|
|
|
print(msg)
|
|
|
|
print_intentions(certs, extra_certs, non_renewed)
|
|
|
|
response = ipautil.user_input('Enter "yes" to proceed')
|
|
if response.lower() != 'yes':
|
|
print("Not proceeding.")
|
|
return 0
|
|
print("Proceeding.")
|
|
|
|
try:
|
|
fix_certreq_directives(certs)
|
|
run_cert_fix(certs, extra_certs)
|
|
except ipautil.CalledProcessError:
|
|
if any(
|
|
x[0] is IPACertType.LDAPS
|
|
for x in extra_certs + non_renewed
|
|
):
|
|
# The DS cert was expired. This will cause 'pki-server
|
|
# cert-fix' to fail at the final restart, and return nonzero.
|
|
# So this exception *might* be OK to ignore.
|
|
#
|
|
# If 'pki-server cert-fix' has written new certificates
|
|
# corresponding to all the extra_certs, then ignore the
|
|
# CalledProcessError and proceed to installing the IPA-specific
|
|
# certs. Otherwise re-raise.
|
|
if check_renewed_ipa_certs(extra_certs):
|
|
pass
|
|
else:
|
|
raise
|
|
else:
|
|
raise # otherwise re-raise
|
|
|
|
replicate_dogtag_certs(subject_base, ca_subject_dn, certs)
|
|
install_ipa_certs(subject_base, ca_subject_dn, extra_certs)
|
|
|
|
if any(x[0] != 'sslserver' for x in certs) \
|
|
or any(x[0] is IPACertType.IPARA for x in extra_certs):
|
|
# we renewed a "shared" certificate, therefore we must
|
|
# become the renewal master
|
|
print("Becoming renewal master.")
|
|
cainstance.CAInstance().set_renewal_master()
|
|
|
|
print("Restarting IPA")
|
|
ipautil.run(['ipactl', 'restart'], raiseonerr=True)
|
|
|
|
print(renewal_note)
|
|
return 0
|
|
|
|
|
|
def expired_certs(now):
|
|
expired_ipa, non_renew_ipa = expired_ipa_certs(now)
|
|
return expired_dogtag_certs(now), expired_ipa, non_renew_ipa
|
|
|
|
|
|
def expired_dogtag_certs(now):
|
|
"""
|
|
Determine which Dogtag certs are expired, or close to expiry.
|
|
|
|
Return a list of (cert_id, cert) pairs.
|
|
|
|
"""
|
|
certs = []
|
|
db = NSSDatabase(nssdir=paths.PKI_TOMCAT_ALIAS_DIR)
|
|
|
|
for certid, nickname in cert_nicknames.items():
|
|
try:
|
|
cert = db.get_cert(nickname)
|
|
except RuntimeError:
|
|
pass # unfortunately certdb doesn't give us a better exception
|
|
else:
|
|
if cert.not_valid_after <= now:
|
|
certs.append((certid, cert))
|
|
|
|
return certs
|
|
|
|
|
|
def expired_ipa_certs(now):
|
|
"""
|
|
Determine which IPA certs are expired, or close to expiry.
|
|
|
|
Return a list of (IPACertType, cert) pairs.
|
|
|
|
"""
|
|
certs = []
|
|
non_renewed = []
|
|
|
|
# IPA RA
|
|
cert = x509.load_certificate_from_file(paths.RA_AGENT_PEM)
|
|
if cert.not_valid_after <= now:
|
|
certs.append((IPACertType.IPARA, cert))
|
|
|
|
# Apache HTTPD
|
|
cert = x509.load_certificate_from_file(paths.HTTPD_CERT_FILE)
|
|
if cert.not_valid_after <= now:
|
|
if not is_ipa_issued_cert(api, cert):
|
|
non_renewed.append((IPACertType.HTTPS, cert))
|
|
else:
|
|
certs.append((IPACertType.HTTPS, cert))
|
|
|
|
# LDAPS
|
|
serverid = realm_to_serverid(api.env.realm)
|
|
ds = dsinstance.DsInstance(realm_name=api.env.realm)
|
|
ds_dbdir = dsinstance.config_dirname(serverid)
|
|
ds_nickname = ds.get_server_cert_nickname(serverid)
|
|
db = NSSDatabase(nssdir=ds_dbdir)
|
|
cert = db.get_cert(ds_nickname)
|
|
if cert.not_valid_after <= now:
|
|
if not is_ipa_issued_cert(api, cert):
|
|
non_renewed.append((IPACertType.LDAPS, cert))
|
|
else:
|
|
certs.append((IPACertType.LDAPS, cert))
|
|
|
|
# KDC
|
|
cert = x509.load_certificate_from_file(paths.KDC_CERT)
|
|
if cert.not_valid_after <= now:
|
|
if not is_ipa_issued_cert(api, cert):
|
|
non_renewed.append((IPACertType.HTTPS, cert))
|
|
else:
|
|
certs.append((IPACertType.KDC, cert))
|
|
|
|
return certs, non_renewed
|
|
|
|
|
|
def print_intentions(dogtag_certs, ipa_certs, non_renewed):
|
|
print("The following certificates will be renewed:")
|
|
print()
|
|
|
|
for certid, cert in dogtag_certs:
|
|
print_cert_info("Dogtag", certid, cert)
|
|
|
|
for certtype, cert in ipa_certs:
|
|
print_cert_info("IPA", certtype.value, cert)
|
|
|
|
if non_renewed:
|
|
print(
|
|
"The following certificates will NOT be renewed because "
|
|
"they were not issued by the IPA CA:"
|
|
)
|
|
print()
|
|
|
|
for certtype, cert in non_renewed:
|
|
print_cert_info("IPA", certtype.value, cert)
|
|
|
|
|
|
def print_cert_info(context, desc, cert):
|
|
print("{} {} certificate:".format(context, desc))
|
|
print(" Subject: {}".format(DN(cert.subject)))
|
|
print(" Serial: {}".format(cert.serial_number))
|
|
print(" Expires: {}".format(cert.not_valid_after))
|
|
print()
|
|
|
|
|
|
def get_csr_from_certmonger(nickname):
|
|
"""
|
|
Get the csr for the provided nickname by asking certmonger.
|
|
|
|
Returns the csr in ASCII format without the header/footer in a single line
|
|
or None if not found.
|
|
"""
|
|
criteria = {
|
|
'cert-database': paths.PKI_TOMCAT_ALIAS_DIR,
|
|
'cert-nickname': nickname,
|
|
}
|
|
|
|
id = certmonger.get_request_id(criteria)
|
|
if id:
|
|
csr = certmonger.get_request_value(id, "csr")
|
|
if csr:
|
|
try:
|
|
# Make sure the value can be parsed as valid CSR
|
|
csr_obj = crypto_x509.load_pem_x509_csr(
|
|
csr.encode('ascii'), default_backend())
|
|
val = base64.b64encode(csr_obj.public_bytes(x509.Encoding.DER))
|
|
return val.decode('ascii')
|
|
except Exception as e:
|
|
# Fallthrough and return None
|
|
logger.debug("Unable to get CSR from certmonger: %s", e)
|
|
return None
|
|
|
|
|
|
def fix_certreq_directives(certs):
|
|
"""
|
|
For all the certs to be fixed, ensure that the corresponding CSR is found
|
|
in PKI config file, or try to get the CSR from certmonger.
|
|
"""
|
|
directives = {
|
|
'auditSigningCert cert-pki-ca': ('ca.audit_signing.certreq',
|
|
paths.CA_CS_CFG_PATH),
|
|
'ocspSigningCert cert-pki-ca': ('ca.ocsp_signing.certreq',
|
|
paths.CA_CS_CFG_PATH),
|
|
'subsystemCert cert-pki-ca': ('ca.subsystem.certreq',
|
|
paths.CA_CS_CFG_PATH),
|
|
'Server-Cert cert-pki-ca': ('ca.sslserver.certreq',
|
|
paths.CA_CS_CFG_PATH),
|
|
'auditSigningCert cert-pki-kra': ('kra.audit_signing.certreq',
|
|
paths.KRA_CS_CFG_PATH),
|
|
'storageCert cert-pki-kra': ('kra.storage.certreq',
|
|
paths.KRA_CS_CFG_PATH),
|
|
'transportCert cert-pki-kra': ('kra.transport.certreq',
|
|
paths.KRA_CS_CFG_PATH),
|
|
}
|
|
|
|
# pki-server cert-fix needs to find the CSR in the subsystem config file
|
|
# otherwise it will fail
|
|
# For each cert to be fixed, check that the CSR is present or
|
|
# get it from certmonger
|
|
for (certid, _cert) in certs:
|
|
# Check if the directive is set in the config file
|
|
nickname = cert_nicknames[certid]
|
|
(directive, cfg_path) = directives[nickname]
|
|
if directivesetter.get_directive(cfg_path, directive, '=') is None:
|
|
# The CSR is missing, try to get it from certmonger
|
|
csr = get_csr_from_certmonger(nickname)
|
|
if csr:
|
|
# Update the directive
|
|
directivesetter.set_directive(cfg_path, directive, csr,
|
|
quotes=False, separator='=')
|
|
|
|
|
|
def run_cert_fix(certs, extra_certs):
|
|
ldapi_path = (
|
|
paths.SLAPD_INSTANCE_SOCKET_TEMPLATE
|
|
% '-'.join(api.env.realm.split('.'))
|
|
)
|
|
cmd = [
|
|
'pki-server',
|
|
'cert-fix',
|
|
'--ldapi-socket', ldapi_path,
|
|
'--agent-uid', 'ipara',
|
|
]
|
|
for certid, _cert in certs:
|
|
cmd.extend(['--cert', certid])
|
|
for _certtype, cert in extra_certs:
|
|
cmd.extend(['--extra-cert', str(cert.serial_number)])
|
|
ipautil.run(cmd, raiseonerr=True)
|
|
|
|
|
|
def replicate_dogtag_certs(subject_base, ca_subject_dn, certs):
|
|
for certid, _oldcert in certs:
|
|
cert_path = "/etc/pki/pki-tomcat/certs/{}.crt".format(certid)
|
|
cert = x509.load_certificate_from_file(cert_path)
|
|
print_cert_info("Renewed Dogtag", certid, cert)
|
|
replicate_cert(subject_base, ca_subject_dn, cert)
|
|
|
|
|
|
def check_renewed_ipa_certs(certs):
|
|
"""
|
|
Check whether all expected IPA-specific certs (extra_certs) were renewed
|
|
successfully.
|
|
|
|
For now this subroutine just checks that the files that we expect
|
|
``pki-server cert-fix`` to have written do exist and contain an X.509
|
|
certificate.
|
|
|
|
Return ``True`` if everything seems to be as expected, otherwise ``False``.
|
|
|
|
"""
|
|
for _certtype, oldcert in certs:
|
|
cert_path = RENEWED_CERT_PATH_TEMPLATE.format(oldcert.serial_number)
|
|
try:
|
|
x509.load_certificate_from_file(cert_path)
|
|
except (IOError, ValueError):
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
def install_ipa_certs(subject_base, ca_subject_dn, certs):
|
|
"""Print details and install renewed IPA certificates."""
|
|
for certtype, oldcert in certs:
|
|
cert_path = RENEWED_CERT_PATH_TEMPLATE.format(oldcert.serial_number)
|
|
cert = x509.load_certificate_from_file(cert_path)
|
|
print_cert_info("Renewed IPA", certtype.value, cert)
|
|
|
|
if certtype is IPACertType.IPARA:
|
|
shutil.copyfile(cert_path, paths.RA_AGENT_PEM)
|
|
cainstance.update_people_entry(cert)
|
|
replicate_cert(subject_base, ca_subject_dn, cert)
|
|
elif certtype is IPACertType.HTTPS:
|
|
shutil.copyfile(cert_path, paths.HTTPD_CERT_FILE)
|
|
elif certtype is IPACertType.LDAPS:
|
|
serverid = realm_to_serverid(api.env.realm)
|
|
ds = dsinstance.DsInstance(realm_name=api.env.realm)
|
|
ds_dbdir = dsinstance.config_dirname(serverid)
|
|
db = NSSDatabase(nssdir=ds_dbdir)
|
|
ds_nickname = ds.get_server_cert_nickname(serverid)
|
|
db.delete_cert(ds_nickname)
|
|
db.import_pem_cert(ds_nickname, EMPTY_TRUST_FLAGS, cert_path)
|
|
elif certtype is IPACertType.KDC:
|
|
shutil.copyfile(cert_path, paths.KDC_CERT)
|
|
|
|
|
|
def replicate_cert(subject_base, ca_subject_dn, cert):
|
|
nickname = cainstance.get_ca_renewal_nickname(
|
|
subject_base, ca_subject_dn, DN(cert.subject))
|
|
if nickname:
|
|
cainstance.update_ca_renewal_entry(api.Backend.ldap2, nickname, cert)
|