Centralize enable/disable of the ACME service

The initial implementation of ACME in dogtag and IPA required
that ACME be manually enabled on each CA.

dogtag added a REST API that can be access directly or through
the `pki acme` CLI tool to enable or disable the service.

It also abstracted the database connection and introduced the
concept of a realm which defines the DIT for ACME users and
groups, the URL and the identity. This is configured in realm.conf.

A new group was created, Enterprise ACME Administrators, that
controls the users allowed to modify ACME configuration.

The IPA RA is added to this group for the ipa-acme-manage tool
to authenticate to the API to enable/disable ACME.

Related dogtag installation documentation:
https://github.com/dogtagpki/pki/blob/master/docs/installation/acme/Configuring_ACME_Database.md
https://github.com/dogtagpki/pki/blob/master/docs/installation/acme/Configuring_ACME_Realm.md
https://github.com/dogtagpki/pki/blob/master/docs/installation/acme/Installing_PKI_ACME_Responder.md

ACME REST API:
https://github.com/dogtagpki/pki/wiki/PKI-ACME-Enable-REST-API

https://pagure.io/freeipa/issue/8524

Signed-off-by: Rob Crittenden <rcritten@redhat.com>
Reviewed-By: Fraser Tweedale <ftweedal@redhat.com>
Reviewed-By: Christian Heimes <cheimes@redhat.com>
Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com>
Reviewed-By: Rob Crittenden <rcritten@redhat.com>
Reviewed-By: Mohammad Rizwan <myusuf@redhat.com>
This commit is contained in:
Rob Crittenden 2020-10-06 15:17:15 -04:00
parent e13d058a06
commit c0d55ce6de
11 changed files with 109 additions and 32 deletions

View File

@ -106,6 +106,7 @@ dist_app_DATA = \
pki-acme-database.conf.template \ pki-acme-database.conf.template \
pki-acme-engine.conf.template \ pki-acme-engine.conf.template \
pki-acme-issuer.conf.template \ pki-acme-issuer.conf.template \
pki-acme-realm.conf.template \
ldbm-tuning.ldif \ ldbm-tuning.ldif \
$(NULL) $(NULL)

View File

@ -1,2 +1,11 @@
# Parameters read by ACMEEngineConfigFileSource, i.e. these are
# expected to be in the file pointed to by the 'filename' directive
# above.
#
# IPA only sets the values it uses.
#
# Whether to enable the ACME service:
enabled=false enabled=false
wildcard=false
# Whether to accept wildcard DNS identifiers:
policy.wildcard=false

View File

@ -1,5 +1,5 @@
class=org.dogtagpki.acme.issuer.PKIIssuer class=org.dogtagpki.acme.issuer.PKIIssuer
url=https://$FQDN:8443 url=https://$FQDN:8443
profile=acmeServerCert profile=acmeIPAServerCert
username=$USER username=$USER
password=$PASSWORD password=$PASSWORD

View File

@ -0,0 +1,8 @@
authType=BasicAuth
class=org.dogtagpki.acme.realm.DSRealm
groupsDN=ou=groups,o=ipaca
usersDN=ou=people,o=ipaca
url=ldaps://$FQDN:636
configFile=/etc/pki/pki-tomcat/ca/CS.cfg
username=$USER
password=$PASSWORD

View File

@ -7,7 +7,7 @@ app_DATA = \
caIPAserviceCert.UPGRADE.cfg \ caIPAserviceCert.UPGRADE.cfg \
IECUserRoles.cfg \ IECUserRoles.cfg \
KDCs_PKINIT_Certs.cfg \ KDCs_PKINIT_Certs.cfg \
acmeServerCert.cfg \ acmeIPAServerCert.cfg \
$(NULL) $(NULL)
EXTRA_DIST = \ EXTRA_DIST = \

View File

@ -1,4 +1,4 @@
profileId=acmeServerCert profileId=acmeIPAServerCert
classId=caEnrollImpl classId=caEnrollImpl
desc=ACME profile for use in IPA deployments desc=ACME profile for use in IPA deployments
visible=true visible=true

View File

@ -127,6 +127,7 @@ class BasePathNamespace:
PKI_ACME_DATABASE_CONF = "/etc/pki/pki-tomcat/acme/database.conf" PKI_ACME_DATABASE_CONF = "/etc/pki/pki-tomcat/acme/database.conf"
PKI_ACME_ENGINE_CONF = "/etc/pki/pki-tomcat/acme/engine.conf" PKI_ACME_ENGINE_CONF = "/etc/pki/pki-tomcat/acme/engine.conf"
PKI_ACME_ISSUER_CONF = "/etc/pki/pki-tomcat/acme/issuer.conf" PKI_ACME_ISSUER_CONF = "/etc/pki/pki-tomcat/acme/issuer.conf"
PKI_ACME_REALM_CONF = "/etc/pki/pki-tomcat/acme/realm.conf"
ETC_REDHAT_RELEASE = "/etc/redhat-release" ETC_REDHAT_RELEASE = "/etc/redhat-release"
RESOLV_CONF = "/etc/resolv.conf" RESOLV_CONF = "/etc/resolv.conf"
SAMBA_KEYTAB = "/etc/samba/samba.keytab" SAMBA_KEYTAB = "/etc/samba/samba.keytab"

View File

@ -56,8 +56,8 @@ INCLUDED_PROFILES = {
Profile(u'KDCs_PKINIT_Certs', Profile(u'KDCs_PKINIT_Certs',
u'Profile for PKINIT support by KDCs', u'Profile for PKINIT support by KDCs',
False), False),
Profile(u'acmeServerCert', Profile(u'acmeIPAServerCert',
u'ACME service certificate profile', u'ACME IPA service certificate profile',
False), False),
} }

View File

@ -71,7 +71,7 @@ ADMIN_GROUPS = [
'Security Domain Administrators' 'Security Domain Administrators'
] ]
ACME_AGENT_GROUP = 'ACME Agents' ACME_AGENT_GROUP = 'Enterprise ACME Administrators'
PROFILES_DN = DN(('ou', 'certificateProfiles'), ('ou', 'ca'), ('o', 'ipaca')) PROFILES_DN = DN(('ou', 'certificateProfiles'), ('ou', 'ca'), ('o', 'ipaca'))
@ -768,6 +768,12 @@ class CAInstance(DogtagInstance):
self.basedn) self.basedn)
conn.add_entry_to_group(user_dn, group_dn, 'uniqueMember') conn.add_entry_to_group(user_dn, group_dn, 'uniqueMember')
group_dn = DN(('cn', ACME_AGENT_GROUP), ('ou', 'groups'),
self.basedn)
conn.add_entry_to_group(user_dn, group_dn, 'uniqueMember')
conn.disconnect()
def __get_ca_chain(self): def __get_ca_chain(self):
try: try:
return dogtag.get_ca_certchain(ca_host=self.fqdn) return dogtag.get_ca_certchain(ca_host=self.fqdn)
@ -1479,6 +1485,8 @@ class CAInstance(DogtagInstance):
logger.debug('ACME service is already deployed') logger.debug('ACME service is already deployed')
return False return False
self._ldap_mod('/usr/share/pki/acme/database/ds/schema.ldif')
configure_acme_acls() configure_acme_acls()
# create ACME agent group (if not exist already) and user # create ACME agent group (if not exist already) and user
@ -1510,6 +1518,7 @@ class CAInstance(DogtagInstance):
('pki-acme-database.conf.template', paths.PKI_ACME_DATABASE_CONF), ('pki-acme-database.conf.template', paths.PKI_ACME_DATABASE_CONF),
('pki-acme-engine.conf.template', paths.PKI_ACME_ENGINE_CONF), ('pki-acme-engine.conf.template', paths.PKI_ACME_ENGINE_CONF),
('pki-acme-issuer.conf.template', paths.PKI_ACME_ISSUER_CONF), ('pki-acme-issuer.conf.template', paths.PKI_ACME_ISSUER_CONF),
('pki-acme-realm.conf.template', paths.PKI_ACME_REALM_CONF),
] ]
sub_dict = dict( sub_dict = dict(
FQDN=self.fqdn, FQDN=self.fqdn,
@ -1732,6 +1741,11 @@ def ensure_acme_containers():
DN(('ou', 'orders'), ou_acme), DN(('ou', 'orders'), ou_acme),
DN(('ou', 'authorizations'), ou_acme), DN(('ou', 'authorizations'), ou_acme),
DN(('ou', 'challenges'), ou_acme), DN(('ou', 'challenges'), ou_acme),
DN(('ou', 'certificates'), ou_acme),
]
extensible_rdns = [
DN(('ou', 'config'), ou_acme),
] ]
for rdn in rdns: for rdn in rdns:
@ -1741,6 +1755,13 @@ def ensure_acme_containers():
ou=[rdn[0][0].value], ou=[rdn[0][0].value],
) )
for rdn in extensible_rdns:
ensure_entry(
DN(rdn, ('o', 'ipaca')),
objectclass=['top', 'organizationalUnit', 'extensibleObject'],
ou=[rdn[0][0].value],
)
def ensure_entry(dn, **attrs): def ensure_entry(dn, **attrs):
"""Ensure an entry exists. """Ensure an entry exists.

View File

@ -3,13 +3,16 @@
# #
import enum import enum
import pathlib
from ipalib import api, errors
from ipalib import _
from ipalib.facts import is_ipa_configured
from ipaplatform.paths import paths from ipaplatform.paths import paths
from ipapython.admintool import AdminTool from ipapython.admintool import AdminTool
from ipapython.directivesetter import DirectiveSetter from ipapython import cookie, dogtag
from ipaserver.install import cainstance from ipaserver.install import cainstance
from ipalib.facts import is_ipa_configured
from ipaserver.plugins.dogtag import RestClient
# Manages the FreeIPA ACME service on a per-server basis. # Manages the FreeIPA ACME service on a per-server basis.
# #
@ -20,6 +23,49 @@ from ipalib.facts import is_ipa_configured
# remove this program, or make it a wrapper for the API commands. # remove this program, or make it a wrapper for the API commands.
class acme_state(RestClient):
def _request(self, url):
return dogtag.https_request(
self.ca_host, 8443,
url=url,
cafile=self.ca_cert,
client_certfile=paths.RA_AGENT_PEM,
client_keyfile=paths.RA_AGENT_KEY,
method='POST'
)
def __enter__(self):
status, resp_headers, _unused = self._request('/acme/login')
cookies = cookie.Cookie.parse(resp_headers.get('set-cookie', ''))
if status != 200 or len(cookies) == 0:
raise errors.RemoteRetrieveError(
reason=_('Failed to authenticate to CA REST API')
)
object.__setattr__(self, 'cookie', str(cookies[0]))
return self
def __exit__(self, exc_type, exc_value, traceback):
"""Log out of the REST API"""
headers = dict(Cookie=self.cookie)
status, unused, _unused = self._request('/acme/logout')
object.__setattr__(self, 'cookie', None)
if status != 204:
raise RuntimeError('Failed to logout')
def enable(self):
headers = dict(Cookie=self.cookie)
status, unused, _unused = self._request('/acme/enable')
if status != 200:
raise RuntimeError('Failed to enable ACME')
def disable(self):
headers = dict(Cookie=self.cookie)
status, unused, _unused = self._request('/acme/disable')
if status != 200:
raise RuntimeError('Failed to disble ACME')
class Command(enum.Enum): class Command(enum.Enum):
ENABLE = 'enable' ENABLE = 'enable'
DISABLE = 'disable' DISABLE = 'disable'
@ -52,28 +98,17 @@ class IPAACMEManage(AdminTool):
print("CA is not installed on this server.") print("CA is not installed on this server.")
return 1 return 1
if self.command == Command.ENABLE: api.bootstrap(in_server=True, confdir=paths.ETC_IPA)
directive = 'enabled' api.finalize()
value = 'true' api.Backend.ldap2.connect()
elif self.command == Command.DISABLE:
directive = 'enabled'
value = 'false'
else:
raise RuntimeError('programmer error: unhandled enum case')
with DirectiveSetter( state = acme_state(api)
paths.PKI_ACME_ENGINE_CONF, with state as ca_api:
separator='=', if self.command == Command.ENABLE:
quotes=False, ca_api.enable()
) as ds: elif self.command == Command.DISABLE:
ds.set(directive, value) ca_api.disable()
else:
# Work around a limitation in PKI ACME service file watching raise RuntimeError('programmer error: unhandled enum case')
# where renames (what DirectiveSetter does) are not detected.
# It will be fixed, but keeping the workaround will do no harm.
pathlib.Path(paths.PKI_ACME_ENGINE_CONF).touch()
# Nothing else to do; the Dogtag ACME service monitors engine.conf
# for updates and reconfigures itself as required.
return 0 return 0

View File

@ -1112,7 +1112,9 @@ def ca_upgrade_schema(ca):
return False return False
# ACME schema file moved in pki-server-10.9.0-0.3 # ACME schema file moved in pki-server-10.9.0-0.3
# ACME database connections were abstrated in pki-acme-10.10.0
for path in [ for path in [
'/usr/share/pki/acme/conf/database/ds/schema.ldif',
'/usr/share/pki/acme/conf/database/ldap/schema.ldif', '/usr/share/pki/acme/conf/database/ldap/schema.ldif',
'/usr/share/pki/acme/database/ldap/schema.ldif', '/usr/share/pki/acme/database/ldap/schema.ldif',
]: ]: