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-engine.conf.template \
pki-acme-issuer.conf.template \
pki-acme-realm.conf.template \
ldbm-tuning.ldif \
$(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
wildcard=false
# Whether to accept wildcard DNS identifiers:
policy.wildcard=false

View File

@ -1,5 +1,5 @@
class=org.dogtagpki.acme.issuer.PKIIssuer
url=https://$FQDN:8443
profile=acmeServerCert
profile=acmeIPAServerCert
username=$USER
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 \
IECUserRoles.cfg \
KDCs_PKINIT_Certs.cfg \
acmeServerCert.cfg \
acmeIPAServerCert.cfg \
$(NULL)
EXTRA_DIST = \

View File

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

View File

@ -127,6 +127,7 @@ class BasePathNamespace:
PKI_ACME_DATABASE_CONF = "/etc/pki/pki-tomcat/acme/database.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_REALM_CONF = "/etc/pki/pki-tomcat/acme/realm.conf"
ETC_REDHAT_RELEASE = "/etc/redhat-release"
RESOLV_CONF = "/etc/resolv.conf"
SAMBA_KEYTAB = "/etc/samba/samba.keytab"

View File

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

View File

@ -71,7 +71,7 @@ ADMIN_GROUPS = [
'Security Domain Administrators'
]
ACME_AGENT_GROUP = 'ACME Agents'
ACME_AGENT_GROUP = 'Enterprise ACME Administrators'
PROFILES_DN = DN(('ou', 'certificateProfiles'), ('ou', 'ca'), ('o', 'ipaca'))
@ -768,6 +768,12 @@ class CAInstance(DogtagInstance):
self.basedn)
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):
try:
return dogtag.get_ca_certchain(ca_host=self.fqdn)
@ -1479,6 +1485,8 @@ class CAInstance(DogtagInstance):
logger.debug('ACME service is already deployed')
return False
self._ldap_mod('/usr/share/pki/acme/database/ds/schema.ldif')
configure_acme_acls()
# 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-engine.conf.template', paths.PKI_ACME_ENGINE_CONF),
('pki-acme-issuer.conf.template', paths.PKI_ACME_ISSUER_CONF),
('pki-acme-realm.conf.template', paths.PKI_ACME_REALM_CONF),
]
sub_dict = dict(
FQDN=self.fqdn,
@ -1732,6 +1741,11 @@ def ensure_acme_containers():
DN(('ou', 'orders'), ou_acme),
DN(('ou', 'authorizations'), ou_acme),
DN(('ou', 'challenges'), ou_acme),
DN(('ou', 'certificates'), ou_acme),
]
extensible_rdns = [
DN(('ou', 'config'), ou_acme),
]
for rdn in rdns:
@ -1741,6 +1755,13 @@ def ensure_acme_containers():
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):
"""Ensure an entry exists.

View File

@ -3,13 +3,16 @@
#
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 ipapython.admintool import AdminTool
from ipapython.directivesetter import DirectiveSetter
from ipapython import cookie, dogtag
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.
#
@ -20,6 +23,49 @@ from ipalib.facts import is_ipa_configured
# 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):
ENABLE = 'enable'
DISABLE = 'disable'
@ -52,28 +98,17 @@ class IPAACMEManage(AdminTool):
print("CA is not installed on this server.")
return 1
if self.command == Command.ENABLE:
directive = 'enabled'
value = 'true'
elif self.command == Command.DISABLE:
directive = 'enabled'
value = 'false'
else:
raise RuntimeError('programmer error: unhandled enum case')
api.bootstrap(in_server=True, confdir=paths.ETC_IPA)
api.finalize()
api.Backend.ldap2.connect()
with DirectiveSetter(
paths.PKI_ACME_ENGINE_CONF,
separator='=',
quotes=False,
) as ds:
ds.set(directive, value)
# Work around a limitation in PKI ACME service file watching
# 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.
state = acme_state(api)
with state as ca_api:
if self.command == Command.ENABLE:
ca_api.enable()
elif self.command == Command.DISABLE:
ca_api.disable()
else:
raise RuntimeError('programmer error: unhandled enum case')
return 0

View File

@ -1112,7 +1112,9 @@ def ca_upgrade_schema(ca):
return False
# 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 [
'/usr/share/pki/acme/conf/database/ds/schema.ldif',
'/usr/share/pki/acme/conf/database/ldap/schema.ldif',
'/usr/share/pki/acme/database/ldap/schema.ldif',
]: