Skip to content

Commit

Permalink
Use single Custodia instance in installers
Browse files Browse the repository at this point in the history
Installers now pass a single CustodiaInstance object around, instead of
creating new instances on demand. In case of replica promotion with CA,
the instance gets all secrets from a master with CA present. Before, an
installer created multiple instances and may have requested CA key
material from a different machine than DM password hash.

In case of Domain Level 1 and replica promotion, the CustodiaInstance no
longer adds the keys to the local instance and waits for replication to
other replica. Instead the installer directly uploads the new public
keys to the remote 389-DS instance.

Without promotion, new Custodia public keys are still added to local
389-DS over LDAPI.

Fixes: https://pagure.io/freeipa/issue/7518
Signed-off-by: Christian Heimes <cheimes@redhat.com>
Reviewed-By: Simo Sorce <ssorce@redhat.com>
  • Loading branch information
tiran committed Apr 26, 2018
1 parent 84e60e5 commit 994f71a
Show file tree
Hide file tree
Showing 8 changed files with 155 additions and 74 deletions.
13 changes: 11 additions & 2 deletions install/tools/ipa-ca-install
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ from ipaserver.install.installutils import create_replica_config
from ipaserver.install.installutils import check_creds, ReplicaConfig
from ipaserver.install import dsinstance, ca
from ipaserver.install import cainstance, service
from ipaserver.install import custodiainstance
from ipapython import version
from ipalib import api
from ipalib.constants import DOMAIN_LEVEL_0
Expand Down Expand Up @@ -219,13 +220,17 @@ def install_replica(safe_options, options, filename):
options.domain_name = config.domain_name
options.dm_password = config.dirman_password
options.host_name = config.host_name
options.ca_host_name = config.ca_host_name
if os.path.exists(cafile):
options.ca_cert_file = cafile
else:
options.ca_cert_file = None

ca.install_check(True, config, options)
ca.install(True, config, options)

custodia = custodiainstance.get_custodia_instance(
options, custodiainstance.CustodiaModes.CA_PEER)
ca.install(True, config, options, custodia=custodia)


def install_master(safe_options, options):
Expand Down Expand Up @@ -263,13 +268,17 @@ def install_master(safe_options, options):
"Continue to configure the CA with these values?", False):
sys.exit("Installation aborted")

ca.install(True, None, options)
# No CA peer available yet.
custodia = custodiainstance.get_custodia_instance(
options, custodiainstance.CustodiaModes.STANDALONE)
ca.install(True, None, options, custodia=custodia)

# Run ipa-certupdate to add the new CA certificate to
# certificate databases on this server.
logger.info("Updating certificate databases.")
ipa_certupdate.run_with_args(api)


def install(safe_options, options, filename):
options.promote = False

Expand Down
26 changes: 12 additions & 14 deletions ipaserver/install/ca.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,7 @@
from ipaserver.install import sysupgrade
from ipapython.install import typing
from ipapython.install.core import group, knob, extend_knob
from ipaserver.install import (cainstance,
custodiainstance,
dsinstance,
bindinstance)
from ipaserver.install import cainstance, bindinstance, dsinstance
from ipapython import ipautil, certdb
from ipapython.admintool import ScriptError
from ipaplatform import services
Expand Down Expand Up @@ -240,12 +237,12 @@ def install_check(standalone, replica_config, options):
"cannot continue." % (subject, db.secdir))


def install(standalone, replica_config, options):
install_step_0(standalone, replica_config, options)
install_step_1(standalone, replica_config, options)
def install(standalone, replica_config, options, custodia):
install_step_0(standalone, replica_config, options, custodia=custodia)
install_step_1(standalone, replica_config, options, custodia=custodia)


def install_step_0(standalone, replica_config, options):
def install_step_0(standalone, replica_config, options, custodia):
realm_name = options.realm_name
dm_password = options.dm_password
host_name = options.host_name
Expand Down Expand Up @@ -278,9 +275,6 @@ def install_step_0(standalone, replica_config, options):
else:
cafile = os.path.join(replica_config.dir, 'cacert.p12')
if options.promote:
custodia = custodiainstance.CustodiaInstance(
replica_config.host_name,
replica_config.realm_name)
custodia.get_ca_keys(
replica_config.ca_host_name,
cafile,
Expand All @@ -307,7 +301,9 @@ def install_step_0(standalone, replica_config, options):
'certmap.conf', 'subject_base', str(subject_base))
dsinstance.write_certmap_conf(realm_name, ca_subject)

ca = cainstance.CAInstance(realm_name, host_name=host_name)
ca = cainstance.CAInstance(
realm=realm_name, host_name=host_name, custodia=custodia
)
ca.configure_instance(host_name, dm_password, dm_password,
subject_base=subject_base,
ca_subject=ca_subject,
Expand All @@ -326,7 +322,7 @@ def install_step_0(standalone, replica_config, options):
use_ldaps=standalone)


def install_step_1(standalone, replica_config, options):
def install_step_1(standalone, replica_config, options, custodia):
if replica_config is not None and not replica_config.setup_ca:
return

Expand All @@ -335,7 +331,9 @@ def install_step_1(standalone, replica_config, options):
subject_base = options._subject_base
basedn = ipautil.realm_to_suffix(realm_name)

ca = cainstance.CAInstance(realm_name, host_name=host_name)
ca = cainstance.CAInstance(
realm=realm_name, host_name=host_name, custodia=custodia
)

ca.stop('pki-tomcat')

Expand Down
9 changes: 4 additions & 5 deletions ipaserver/install/cainstance.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@
from ipaserver.secrets.kem import IPAKEMKeys

from ipaserver.install import certs
from ipaserver.install import custodiainstance
from ipaserver.install import dsinstance
from ipaserver.install import installutils
from ipaserver.install import ldapupdate
Expand Down Expand Up @@ -298,7 +297,7 @@ class CAInstance(DogtagInstance):
'caSigningCert cert-pki-ca')
server_cert_name = 'Server-Cert cert-pki-ca'

def __init__(self, realm=None, host_name=None):
def __init__(self, realm=None, host_name=None, custodia=None):
super(CAInstance, self).__init__(
realm=realm,
subsystem="CA",
Expand All @@ -323,6 +322,8 @@ def __init__(self, realm=None, host_name=None):
self.no_db_setup = False
self.keytab = os.path.join(
paths.PKI_TOMCAT, self.service_prefix + '.keytab')
# Custodia instance for RA key retrieval
self._custodia = custodia

def configure_instance(self, host_name, dm_password, admin_password,
pkcs12_info=None, master_host=None, csr_file=None,
Expand Down Expand Up @@ -761,9 +762,7 @@ def import_ra_cert(self, rafile, password=''):
self.configure_agent_renewal()

def __import_ra_key(self):
custodia = custodiainstance.CustodiaInstance(host_name=self.fqdn,
realm=self.realm)
custodia.import_ra_key(self.master_host)
self._custodia.import_ra_key(self.master_host)
self.__set_ra_cert_perms()

self.configure_agent_renewal()
Expand Down
136 changes: 103 additions & 33 deletions ipaserver/install/custodiainstance.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import print_function, absolute_import

import enum
import logging

from ipalib import api
Expand All @@ -26,6 +27,77 @@
logger = logging.getLogger(__name__)


class CustodiaModes(enum.Enum):
# peer must have a CA
CA_PEER = 'Custodia CA peer'
# peer must have a CA, KRA preferred
KRA_PEER = 'Custodia KRA peer'
# any master will do
MASTER_PEER = 'Custodia master peer'
# standalone / local instance
STANDALONE = 'Custodia standalone'


def get_custodia_instance(config, mode):
"""Create Custodia instance
:param config: configuration/installer object
:param mode: CustodiaModes member
:return: CustodiaInstance object
The config object must have the following attribute
*host_name*
FQDN of the new replica/master
*realm_name*
Kerberos realm
*promote*
True, when instance will be promoted from client to replica
*master_host_name* (for *CustodiaModes.MASTER_PEER*)
hostname of a master (may not have a CA)
*ca_host_name* (for *CustodiaModes.CA_PEER*)
hostname of a master with CA
*kra_host_name* (for *CustodiaModes.KRA_PEER*)
hostname of a master with KRA or CA
For promotion, the instance will upload new keys and retrieve secrets
from the same host. Therefore it uses *ca_host_name* instead of
*master_host_name* to create a replica with CA.
"""
assert isinstance(mode, CustodiaModes)
logger.info(
"Custodia client for '%r' with promotion %s.",
mode, 'yes' if config.promote else 'no'
)
if config.promote:
if mode == CustodiaModes.CA_PEER:
# In case we install replica with CA, prefer CA host as source for
# all Custodia secret material.
custodia_master = config.ca_host_name
elif mode == CustodiaModes.KRA_PEER:
custodia_master = config.kra_host_name
elif mode == CustodiaModes.MASTER_PEER:
custodia_master = config.master_host_name
elif mode == CustodiaModes.STANDALONE:
custodia_master = None
else:
custodia_master = None

if custodia_master is None:
# use ldapi with local dirsrv instance
logger.info("Custodia uses LDAPI.")
ldap_uri = None
else:
logger.info("Custodia uses '%s' as master peer.", custodia_master)
ldap_uri = 'ldap://{}'.format(custodia_master)

return CustodiaInstance(
host_name=config.host_name,
realm=config.realm_name,
ldap_uri=ldap_uri
)


class CustodiaInstance(SimpleServiceInstance):
def __init__(self, host_name=None, realm=None, ldap_uri=None):
super(CustodiaInstance, self).__init__("ipa-custodia")
Expand Down Expand Up @@ -54,14 +126,19 @@ def __config_file(self):
ipautil.flush_sync(f)

def create_instance(self):
suffix = ipautil.realm_to_suffix(self.realm)
if self.ldap_uri is None or self.ldap_uri.startswith('ldapi://'):
# local case, ensure container exists
self.step("Making sure custodia container exists",
self.__create_container)

self.step("Generating ipa-custodia config file", self.__config_file)
self.step("Making sure custodia container exists", self.__create_container)
self.step("Generating ipa-custodia keys", self.__gen_keys)
super(CustodiaInstance, self).create_instance(gensvc_name='KEYS',
fqdn=self.fqdn,
ldap_suffix=suffix,
realm=self.realm)
super(CustodiaInstance, self).create_instance(
gensvc_name='KEYS',
fqdn=self.fqdn,
ldap_suffix=ipautil.realm_to_suffix(self.realm),
realm=self.realm
)
sysupgrade.set_upgrade_state('custodia', 'installed', True)

def uninstall(self):
Expand Down Expand Up @@ -108,18 +185,6 @@ def upgrade_instance(self):
logger.info("Secure server.keys mode")
os.chmod(self.server_keys, 0o600)

def create_replica(self, master_host_name):
suffix = ipautil.realm_to_suffix(self.realm)
self.ldap_uri = 'ldap://%s' % master_host_name
self.master_host_name = master_host_name

self.step("Generating ipa-custodia config file", self.__config_file)
self.step("Generating ipa-custodia keys", self.__gen_keys)
super(CustodiaInstance, self).create_instance(gensvc_name='KEYS',
fqdn=self.fqdn,
ldap_suffix=suffix,
realm=self.realm)

def __create_container(self):
"""
Runs the custodia update file to ensure custodia container is present.
Expand All @@ -133,33 +198,35 @@ def __create_container(self):
updater.update([os.path.join(paths.UPDATES_DIR, '73-custodia.update')])

def import_ra_key(self, master_host_name):
cli = self.__CustodiaClient(server=master_host_name)
cli = self._get_custodia_client(server=master_host_name)
# please note that ipaCert part has to stay here for historical
# reasons (old servers expect you to ask for ra/ipaCert during
# replication as they store the RA agent cert in an NSS database
# with this nickname)
cli.fetch_key('ra/ipaCert')

def import_dm_password(self, master_host_name):
cli = self.__CustodiaClient(server=master_host_name)
cli = self._get_custodia_client(server=master_host_name)
cli.fetch_key('dm/DMHash')

def __wait_keys(self, host, timeout=300):
def _wait_keys(self, host, timeout=300):
ldap_uri = 'ldap://%s' % host
deadline = int(time.time()) + timeout
logger.info("Waiting up to %s seconds to see our keys "
"appear on host: %s", timeout, host)
# FIXME: Change once there's better way to show this mesage
# in installer output
print("Waiting for keys to appear on host: {}, "
"please wait until this has completed.".format(host))

konn = KEMLdap(ldap_uri)
saved_e = None
while True:
try:
return konn.check_host_keys(self.fqdn)
except Exception as e:
# Print message to console only once for first error.
if saved_e is None:
# FIXME: Change once there's better way to show this
# message in installer output,
print(" Waiting for keys to appear on host: {}, please "
"wait until this has completed.".format(host))
# log only once for the same error
if not isinstance(e, type(saved_e)):
logger.debug(
Expand All @@ -169,20 +236,23 @@ def __wait_keys(self, host, timeout=300):
raise RuntimeError("Timed out trying to obtain keys.")
time.sleep(1)

def __CustodiaClient(self, server):
def _get_custodia_client(self, server):
# Before we attempt to fetch keys from this host, make sure our public
# keys have been replicated there.
self.__wait_keys(server)
self._wait_keys(server)

return CustodiaClient('host@%s' % self.fqdn, self.server_keys,
paths.KRB5_KEYTAB, server, realm=self.realm)
return CustodiaClient(
client_service='host@{}'.format(self.fqdn),
keyfile=self.server_keys, keytab=paths.KRB5_KEYTAB,
server=server, realm=self.realm
)

def __get_keys(self, ca_host, cacerts_file, cacerts_pwd, data):
def _get_keys(self, ca_host, cacerts_file, cacerts_pwd, data):
# Fetch all needed certs one by one, then combine them in a single
# PKCS12 file
prefix = data['prefix']
certlist = data['list']
cli = self.__CustodiaClient(server=ca_host)
cli = self._get_custodia_client(server=ca_host)

with NSSDatabase(None) as tmpdb:
tmpdb.create_db()
Expand Down Expand Up @@ -235,7 +305,7 @@ def get_ca_keys(self, ca_host, cacerts_file, cacerts_pwd):
'subsystemCert cert-pki-ca']
data = {'prefix': 'ca',
'list': certlist}
self.__get_keys(ca_host, cacerts_file, cacerts_pwd, data)
self._get_keys(ca_host, cacerts_file, cacerts_pwd, data)

def get_kra_keys(self, ca_host, cacerts_file, cacerts_pwd):
certlist = ['auditSigningCert cert-pki-kra',
Expand All @@ -244,7 +314,7 @@ def get_kra_keys(self, ca_host, cacerts_file, cacerts_pwd):
'transportCert cert-pki-kra']
data = {'prefix': 'ca',
'list': certlist}
self.__get_keys(ca_host, cacerts_file, cacerts_pwd, data)
self._get_keys(ca_host, cacerts_file, cacerts_pwd, data)

def __start(self):
super(CustodiaInstance, self).__start()
Expand Down

0 comments on commit 994f71a

Please sign in to comment.