2015-05-08 12:39:29 -05:00
|
|
|
# Copyright (C) 2015 FreeIPa Project Contributors, see 'COPYING' for license.
|
|
|
|
|
2018-04-05 02:21:16 -05:00
|
|
|
from __future__ import print_function, absolute_import
|
2017-06-27 08:09:01 -05:00
|
|
|
|
2018-04-26 05:06:36 -05:00
|
|
|
import enum
|
2017-05-24 09:35:07 -05:00
|
|
|
import logging
|
|
|
|
|
2018-06-22 03:00:24 -05:00
|
|
|
from ipalib import api
|
2017-03-31 10:22:45 -05:00
|
|
|
from ipaserver.secrets.kem import IPAKEMKeys, KEMLdap
|
2016-11-22 10:55:10 -06:00
|
|
|
from ipaserver.secrets.client import CustodiaClient
|
2015-05-08 12:39:29 -05:00
|
|
|
from ipaplatform.paths import paths
|
2016-03-22 03:40:43 -05:00
|
|
|
from ipaplatform.constants import constants
|
2016-05-03 08:53:12 -05:00
|
|
|
from ipaserver.install.service import SimpleServiceInstance
|
2015-05-08 12:39:29 -05:00
|
|
|
from ipapython import ipautil
|
2018-11-29 07:49:43 -06:00
|
|
|
from ipapython import ipaldap
|
2018-04-30 01:25:23 -05:00
|
|
|
from ipapython.certdb import NSSDatabase
|
2015-05-08 12:39:29 -05:00
|
|
|
from ipaserver.install import installutils
|
2015-11-27 09:21:02 -06:00
|
|
|
from ipaserver.install import ldapupdate
|
2015-11-03 11:33:17 -06:00
|
|
|
from ipaserver.install import sysupgrade
|
2017-01-06 07:19:12 -06:00
|
|
|
from base64 import b64decode
|
2015-08-07 10:44:59 -05:00
|
|
|
from jwcrypto.common import json_decode
|
2015-05-08 12:39:29 -05:00
|
|
|
import os
|
2016-08-08 08:05:52 -05:00
|
|
|
import stat
|
2017-03-31 10:22:45 -05:00
|
|
|
import time
|
2016-03-22 03:40:43 -05:00
|
|
|
import pwd
|
2015-05-08 12:39:29 -05:00
|
|
|
|
2017-05-24 09:35:07 -05:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
2015-05-08 12:39:29 -05:00
|
|
|
|
2018-04-26 05:06:36 -05:00
|
|
|
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'
|
2018-09-10 08:54:46 -05:00
|
|
|
# local instance (first master)
|
|
|
|
FIRST_MASTER = 'Custodia on first master'
|
2018-04-26 05:06:36 -05:00
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
*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
|
|
|
|
|
2018-09-10 08:50:10 -05:00
|
|
|
For replicas, the instance will upload new keys and retrieve secrets
|
|
|
|
to the same host. Therefore it uses *ca_host_name* instead of
|
2018-04-26 05:06:36 -05:00
|
|
|
*master_host_name* to create a replica with CA.
|
|
|
|
"""
|
|
|
|
assert isinstance(mode, CustodiaModes)
|
2018-05-02 10:43:09 -05:00
|
|
|
logger.debug(
|
2018-04-26 05:06:36 -05:00
|
|
|
"Custodia client for '%r' with promotion %s.",
|
2018-09-10 08:54:46 -05:00
|
|
|
mode, 'yes' if mode != CustodiaModes.FIRST_MASTER else 'no'
|
2018-04-26 05:06:36 -05:00
|
|
|
)
|
2018-09-10 08:50:10 -05:00
|
|
|
if mode == CustodiaModes.CA_PEER:
|
|
|
|
# In case we install replica with CA, prefer CA host as source for
|
|
|
|
# all Custodia secret material.
|
|
|
|
custodia_peer = config.ca_host_name
|
|
|
|
elif mode == CustodiaModes.KRA_PEER:
|
|
|
|
custodia_peer = config.kra_host_name
|
|
|
|
elif mode == CustodiaModes.MASTER_PEER:
|
|
|
|
custodia_peer = config.master_host_name
|
2018-09-10 08:54:46 -05:00
|
|
|
elif mode == CustodiaModes.FIRST_MASTER:
|
2018-06-07 11:17:20 -05:00
|
|
|
custodia_peer = None
|
2018-09-10 08:50:10 -05:00
|
|
|
else:
|
2018-11-09 04:41:14 -06:00
|
|
|
raise RuntimeError("Unknown custodia mode %s" % mode)
|
2018-04-26 05:06:36 -05:00
|
|
|
|
2018-06-07 11:17:20 -05:00
|
|
|
if custodia_peer is None:
|
2018-04-26 05:06:36 -05:00
|
|
|
# use ldapi with local dirsrv instance
|
2018-05-02 10:43:09 -05:00
|
|
|
logger.debug("Custodia uses LDAPI.")
|
2018-04-26 05:06:36 -05:00
|
|
|
else:
|
2018-06-07 11:17:20 -05:00
|
|
|
logger.info("Custodia uses '%s' as master peer.", custodia_peer)
|
2018-04-26 05:06:36 -05:00
|
|
|
|
|
|
|
return CustodiaInstance(
|
|
|
|
host_name=config.host_name,
|
|
|
|
realm=config.realm_name,
|
2018-06-07 11:17:20 -05:00
|
|
|
custodia_peer=custodia_peer
|
2018-04-26 05:06:36 -05:00
|
|
|
)
|
|
|
|
|
|
|
|
|
2015-05-08 12:39:29 -05:00
|
|
|
class CustodiaInstance(SimpleServiceInstance):
|
2018-06-07 11:17:20 -05:00
|
|
|
def __init__(self, host_name=None, realm=None, custodia_peer=None):
|
2015-05-08 12:39:29 -05:00
|
|
|
super(CustodiaInstance, self).__init__("ipa-custodia")
|
|
|
|
self.config_file = paths.IPA_CUSTODIA_CONF
|
2017-11-08 08:15:30 -06:00
|
|
|
self.server_keys = paths.IPA_CUSTODIA_KEYS
|
2018-06-07 11:17:20 -05:00
|
|
|
self.custodia_peer = custodia_peer
|
2015-06-11 14:45:38 -05:00
|
|
|
self.fqdn = host_name
|
|
|
|
self.realm = realm
|
2015-05-08 12:39:29 -05:00
|
|
|
|
2018-06-07 11:17:20 -05:00
|
|
|
@property
|
|
|
|
def ldap_uri(self):
|
|
|
|
if self.custodia_peer is None:
|
2018-11-29 07:49:43 -06:00
|
|
|
return ipaldap.realm_to_ldapi_uri(self.realm)
|
2018-06-07 11:17:20 -05:00
|
|
|
else:
|
|
|
|
return "ldap://{}".format(self.custodia_peer)
|
|
|
|
|
2015-05-08 12:39:29 -05:00
|
|
|
def __config_file(self):
|
|
|
|
template_file = os.path.basename(self.config_file) + '.template'
|
2016-11-22 09:06:45 -06:00
|
|
|
template = os.path.join(paths.USR_SHARE_IPA_DIR, template_file)
|
2016-03-22 03:40:43 -05:00
|
|
|
httpd_info = pwd.getpwnam(constants.HTTPD_USER)
|
2017-11-08 08:15:30 -06:00
|
|
|
sub_dict = dict(
|
|
|
|
IPA_CUSTODIA_CONF_DIR=paths.IPA_CUSTODIA_CONF_DIR,
|
|
|
|
IPA_CUSTODIA_KEYS=paths.IPA_CUSTODIA_KEYS,
|
|
|
|
IPA_CUSTODIA_SOCKET=paths.IPA_CUSTODIA_SOCKET,
|
|
|
|
IPA_CUSTODIA_AUDIT_LOG=paths.IPA_CUSTODIA_AUDIT_LOG,
|
2018-11-29 07:49:43 -06:00
|
|
|
LDAP_URI=ipaldap.realm_to_ldapi_uri(self.realm),
|
2017-11-08 08:15:30 -06:00
|
|
|
UID=httpd_info.pw_uid,
|
|
|
|
GID=httpd_info.pw_gid
|
|
|
|
)
|
2015-05-08 12:39:29 -05:00
|
|
|
conf = ipautil.template_file(template, sub_dict)
|
2017-11-08 08:15:30 -06:00
|
|
|
with open(self.config_file, "w") as f:
|
|
|
|
f.write(conf)
|
|
|
|
ipautil.flush_sync(f)
|
2015-05-08 12:39:29 -05:00
|
|
|
|
2016-10-06 10:35:04 -05:00
|
|
|
def create_instance(self):
|
2018-06-07 11:17:20 -05:00
|
|
|
if self.ldap_uri.startswith('ldapi://'):
|
2018-04-26 05:06:36 -05:00
|
|
|
# local case, ensure container exists
|
|
|
|
self.step("Making sure custodia container exists",
|
|
|
|
self.__create_container)
|
|
|
|
|
2015-05-08 12:39:29 -05:00
|
|
|
self.step("Generating ipa-custodia config file", self.__config_file)
|
|
|
|
self.step("Generating ipa-custodia keys", self.__gen_keys)
|
2018-04-26 05:06:36 -05:00
|
|
|
super(CustodiaInstance, self).create_instance(
|
|
|
|
gensvc_name='KEYS',
|
|
|
|
fqdn=self.fqdn,
|
|
|
|
ldap_suffix=ipautil.realm_to_suffix(self.realm),
|
|
|
|
realm=self.realm
|
|
|
|
)
|
2015-11-03 11:33:17 -06:00
|
|
|
sysupgrade.set_upgrade_state('custodia', 'installed', True)
|
2015-05-08 12:39:29 -05:00
|
|
|
|
2017-11-16 10:01:55 -06:00
|
|
|
def uninstall(self):
|
|
|
|
super(CustodiaInstance, self).uninstall()
|
|
|
|
keystore = IPAKEMKeys({
|
|
|
|
'server_keys': self.server_keys,
|
|
|
|
'ldap_uri': self.ldap_uri
|
|
|
|
})
|
2017-12-18 06:52:10 -06:00
|
|
|
keystore.remove_server_keys_file()
|
2017-11-16 10:01:55 -06:00
|
|
|
installutils.remove_file(self.config_file)
|
|
|
|
sysupgrade.set_upgrade_state('custodia', 'installed', False)
|
|
|
|
|
2015-05-08 12:39:29 -05:00
|
|
|
def __gen_keys(self):
|
2017-11-16 10:01:55 -06:00
|
|
|
keystore = IPAKEMKeys({
|
|
|
|
'server_keys': self.server_keys,
|
|
|
|
'ldap_uri': self.ldap_uri
|
|
|
|
})
|
|
|
|
keystore.generate_server_keys()
|
2015-05-08 12:39:29 -05:00
|
|
|
|
2015-06-11 14:45:38 -05:00
|
|
|
def upgrade_instance(self):
|
2018-01-31 02:57:26 -06:00
|
|
|
installed = sysupgrade.get_upgrade_state("custodia", "installed")
|
|
|
|
if installed:
|
|
|
|
if (not os.path.isfile(self.server_keys)
|
|
|
|
or not os.path.isfile(self.config_file)):
|
|
|
|
logger.warning(
|
|
|
|
"Custodia server keys or config are missing, forcing "
|
|
|
|
"reinstallation of ipa-custodia."
|
|
|
|
)
|
|
|
|
installed = False
|
|
|
|
|
|
|
|
if not installed:
|
2017-05-24 09:35:07 -05:00
|
|
|
logger.info("Custodia service is being configured")
|
2015-11-03 11:33:17 -06:00
|
|
|
self.create_instance()
|
2016-11-24 02:55:27 -06:00
|
|
|
else:
|
|
|
|
old_config = open(self.config_file).read()
|
|
|
|
self.__config_file()
|
|
|
|
new_config = open(self.config_file).read()
|
|
|
|
if new_config != old_config:
|
2017-05-24 09:35:07 -05:00
|
|
|
logger.info("Restarting Custodia")
|
2016-11-24 02:55:27 -06:00
|
|
|
self.restart()
|
|
|
|
|
2016-08-08 08:05:52 -05:00
|
|
|
mode = os.stat(self.server_keys).st_mode
|
|
|
|
if stat.S_IMODE(mode) != 0o600:
|
2017-05-24 09:35:07 -05:00
|
|
|
logger.info("Secure server.keys mode")
|
2016-08-08 08:05:52 -05:00
|
|
|
os.chmod(self.server_keys, 0o600)
|
2015-05-08 12:39:29 -05:00
|
|
|
|
2015-11-27 09:21:02 -06:00
|
|
|
def __create_container(self):
|
|
|
|
"""
|
|
|
|
Runs the custodia update file to ensure custodia container is present.
|
|
|
|
"""
|
|
|
|
|
|
|
|
sub_dict = {
|
|
|
|
'SUFFIX': self.suffix,
|
|
|
|
}
|
|
|
|
|
2016-10-06 10:35:04 -05:00
|
|
|
updater = ldapupdate.LDAPUpdate(sub_dict=sub_dict)
|
2015-11-27 09:21:02 -06:00
|
|
|
updater.update([os.path.join(paths.UPDATES_DIR, '73-custodia.update')])
|
|
|
|
|
2018-06-07 11:17:20 -05:00
|
|
|
def import_ra_key(self):
|
|
|
|
cli = self._get_custodia_client()
|
2017-01-13 02:08:42 -06:00
|
|
|
# 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)
|
2015-06-11 14:45:38 -05:00
|
|
|
cli.fetch_key('ra/ipaCert')
|
|
|
|
|
2018-06-07 11:17:20 -05:00
|
|
|
def import_dm_password(self):
|
|
|
|
cli = self._get_custodia_client()
|
2015-06-11 14:45:38 -05:00
|
|
|
cli.fetch_key('dm/DMHash')
|
|
|
|
|
2018-06-22 03:00:24 -05:00
|
|
|
def _wait_keys(self):
|
|
|
|
timeout = api.env.replication_wait_timeout
|
2017-03-31 10:22:45 -05:00
|
|
|
deadline = int(time.time()) + timeout
|
2018-10-30 16:31:42 -05:00
|
|
|
logger.debug("Waiting up to %s seconds to see our keys "
|
|
|
|
"appear on host %s", timeout, self.ldap_uri)
|
2017-03-31 10:22:45 -05:00
|
|
|
|
2018-06-07 11:17:20 -05:00
|
|
|
konn = KEMLdap(self.ldap_uri)
|
2017-03-31 10:22:45 -05:00
|
|
|
saved_e = None
|
|
|
|
while True:
|
|
|
|
try:
|
|
|
|
return konn.check_host_keys(self.fqdn)
|
|
|
|
except Exception as e:
|
2018-04-26 05:06:36 -05:00
|
|
|
# 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,
|
2018-06-07 11:17:20 -05:00
|
|
|
print(
|
|
|
|
" Waiting for keys to appear on host: {}, please "
|
|
|
|
"wait until this has completed.".format(
|
|
|
|
self.ldap_uri)
|
|
|
|
)
|
2017-03-31 10:22:45 -05:00
|
|
|
# log only once for the same error
|
|
|
|
if not isinstance(e, type(saved_e)):
|
2017-05-24 09:35:07 -05:00
|
|
|
logger.debug(
|
|
|
|
"Transient error getting keys: '%s'", e)
|
2017-03-31 10:22:45 -05:00
|
|
|
saved_e = e
|
|
|
|
if int(time.time()) > deadline:
|
|
|
|
raise RuntimeError("Timed out trying to obtain keys.")
|
|
|
|
time.sleep(1)
|
|
|
|
|
2018-06-07 11:17:20 -05:00
|
|
|
def _get_custodia_client(self):
|
|
|
|
if self.custodia_peer is None:
|
|
|
|
raise ValueError("Can't replicate secrets without Custodia peer")
|
2017-06-23 03:48:41 -05:00
|
|
|
# Before we attempt to fetch keys from this host, make sure our public
|
|
|
|
# keys have been replicated there.
|
2018-06-07 11:17:20 -05:00
|
|
|
self._wait_keys()
|
2017-06-23 03:48:41 -05:00
|
|
|
|
2018-04-26 05:06:36 -05:00
|
|
|
return CustodiaClient(
|
|
|
|
client_service='host@{}'.format(self.fqdn),
|
|
|
|
keyfile=self.server_keys, keytab=paths.KRB5_KEYTAB,
|
2018-06-07 11:17:20 -05:00
|
|
|
server=self.custodia_peer, realm=self.realm
|
2018-04-26 05:06:36 -05:00
|
|
|
)
|
2017-06-23 03:48:41 -05:00
|
|
|
|
2018-06-07 11:17:20 -05:00
|
|
|
def _get_keys(self, cacerts_file, cacerts_pwd, data):
|
2018-04-17 00:10:48 -05:00
|
|
|
# Fetch all needed certs one by one, then combine them in a single
|
|
|
|
# PKCS12 file
|
2015-08-25 14:42:25 -05:00
|
|
|
prefix = data['prefix']
|
|
|
|
certlist = data['list']
|
2018-06-07 11:17:20 -05:00
|
|
|
cli = self._get_custodia_client()
|
2015-08-07 10:44:59 -05:00
|
|
|
|
2018-04-17 00:10:48 -05:00
|
|
|
with NSSDatabase(None) as tmpdb:
|
|
|
|
tmpdb.create_db()
|
2015-08-07 10:44:59 -05:00
|
|
|
# Cert file password
|
2018-04-17 00:10:48 -05:00
|
|
|
crtpwfile = os.path.join(tmpdb.secdir, 'crtpwfile')
|
2015-08-07 10:44:59 -05:00
|
|
|
with open(crtpwfile, 'w+') as f:
|
|
|
|
f.write(cacerts_pwd)
|
|
|
|
|
|
|
|
for nickname in certlist:
|
2015-08-25 14:42:25 -05:00
|
|
|
value = cli.fetch_key(os.path.join(prefix, nickname), False)
|
2015-08-07 10:44:59 -05:00
|
|
|
v = json_decode(value)
|
2018-04-17 00:10:48 -05:00
|
|
|
pk12pwfile = os.path.join(tmpdb.secdir, 'pk12pwfile')
|
2015-08-07 10:44:59 -05:00
|
|
|
with open(pk12pwfile, 'w+') as f:
|
|
|
|
f.write(v['export password'])
|
2018-04-17 00:10:48 -05:00
|
|
|
pk12file = os.path.join(tmpdb.secdir, 'pk12file')
|
2017-07-31 09:53:06 -05:00
|
|
|
with open(pk12file, 'wb') as f:
|
2015-08-07 10:44:59 -05:00
|
|
|
f.write(b64decode(v['pkcs12 data']))
|
2018-04-17 00:10:48 -05:00
|
|
|
tmpdb.run_pk12util([
|
|
|
|
'-k', tmpdb.pwd_file,
|
|
|
|
'-n', nickname,
|
|
|
|
'-i', pk12file,
|
|
|
|
'-w', pk12pwfile
|
|
|
|
])
|
|
|
|
|
2018-04-30 01:25:23 -05:00
|
|
|
# Add CA certificates
|
|
|
|
self.export_ca_certs_nssdb(tmpdb, True)
|
2016-08-23 03:39:08 -05:00
|
|
|
|
2015-08-07 10:44:59 -05:00
|
|
|
# Now that we gathered all certs, re-export
|
2018-04-17 00:10:48 -05:00
|
|
|
ipautil.run([
|
|
|
|
paths.PKCS12EXPORT,
|
|
|
|
'-d', tmpdb.secdir,
|
|
|
|
'-p', tmpdb.pwd_file,
|
|
|
|
'-w', crtpwfile,
|
|
|
|
'-o', cacerts_file
|
|
|
|
])
|
2015-08-07 10:44:59 -05:00
|
|
|
|
2018-06-07 11:17:20 -05:00
|
|
|
def get_ca_keys(self, cacerts_file, cacerts_pwd):
|
2015-08-25 14:42:25 -05:00
|
|
|
certlist = ['caSigningCert cert-pki-ca',
|
|
|
|
'ocspSigningCert cert-pki-ca',
|
|
|
|
'auditSigningCert cert-pki-ca',
|
|
|
|
'subsystemCert cert-pki-ca']
|
|
|
|
data = {'prefix': 'ca',
|
|
|
|
'list': certlist}
|
2018-06-07 11:17:20 -05:00
|
|
|
self._get_keys(cacerts_file, cacerts_pwd, data)
|
2015-08-25 14:42:25 -05:00
|
|
|
|
2018-06-07 11:17:20 -05:00
|
|
|
def get_kra_keys(self, cacerts_file, cacerts_pwd):
|
2015-08-25 14:42:25 -05:00
|
|
|
certlist = ['auditSigningCert cert-pki-kra',
|
|
|
|
'storageCert cert-pki-kra',
|
|
|
|
'subsystemCert cert-pki-ca',
|
|
|
|
'transportCert cert-pki-kra']
|
|
|
|
data = {'prefix': 'ca',
|
|
|
|
'list': certlist}
|
2018-06-07 11:17:20 -05:00
|
|
|
self._get_keys(cacerts_file, cacerts_pwd, data)
|
2015-08-25 14:42:25 -05:00
|
|
|
|
2015-05-08 12:39:29 -05:00
|
|
|
def __start(self):
|
|
|
|
super(CustodiaInstance, self).__start()
|
|
|
|
|
|
|
|
def __enable(self):
|
|
|
|
super(CustodiaInstance, self).__enable()
|