# # Copyright (C) 2014 FreeIPA Contributors see COPYING for license # from __future__ import print_function import os import pwd import grp import random import shutil import stat import ldap from ipapython import p11helper as _ipap11helper from ipapython.dnsutil import DNSName from ipaserver.install import service from ipaserver.install import installutils from ipapython.ipa_log_manager import root_logger from ipapython.dn import DN from ipapython import ipautil from ipaplatform.constants import constants from ipaplatform.paths import paths from ipalib import errors, api from ipaserver.install.bindinstance import dns_container_exists softhsm_token_label = u'ipaDNSSEC' softhsm_slot = 0 replica_keylabel_template = u"dnssec-replica:%s" def dnssec_container_exists(suffix): """ Test whether the dns container exists. """ assert isinstance(suffix, DN) return api.Backend.ldap2.entry_exists( DN(('cn', 'sec'), ('cn', 'dns'), suffix)) def remove_replica_public_keys(hostname): keysyncd = DNSKeySyncInstance() keysyncd.remove_replica_public_keys(hostname) class DNSKeySyncInstance(service.Service): def __init__(self, fstore=None, logger=root_logger): super(DNSKeySyncInstance, self).__init__( "ipa-dnskeysyncd", service_desc="DNS key synchronization service", fstore=fstore, service_prefix=u'ipa-dnskeysyncd', keytab=paths.IPA_DNSKEYSYNCD_KEYTAB ) self.logger = logger self.extra_config = [u'dnssecVersion 1', ] # DNSSEC enabled self.named_uid = None self.named_gid = None self.ods_uid = None self.ods_gid = None suffix = ipautil.dn_attribute_property('_suffix') def set_dyndb_ldap_workdir_permissions(self): """ Setting up correct permissions to allow write/read access for daemons """ if self.named_uid is None: self.named_uid = self.__get_named_uid() if self.named_gid is None: self.named_gid = self.__get_named_gid() if not os.path.exists(paths.BIND_LDAP_DNS_IPA_WORKDIR): os.mkdir(paths.BIND_LDAP_DNS_IPA_WORKDIR, 0o770) # dnssec daemons require to have access into the directory os.chmod(paths.BIND_LDAP_DNS_IPA_WORKDIR, 0o770) os.chown(paths.BIND_LDAP_DNS_IPA_WORKDIR, self.named_uid, self.named_gid) def remove_replica_public_keys(self, replica_fqdn): ldap = api.Backend.ldap2 dn_base = DN(('cn', 'keys'), ('cn', 'sec'), ('cn', 'dns'), api.env.basedn) keylabel = replica_keylabel_template % DNSName(replica_fqdn).\ make_absolute().canonicalize().ToASCII() # get old keys from LDAP search_kw = { 'objectclass': u"ipaPublicKeyObject", 'ipk11Label': keylabel, 'ipk11Wrap': True, } filter = ldap.make_filter(search_kw, rules=ldap.MATCH_ALL) entries, _truncated = ldap.find_entries(filter=filter, base_dn=dn_base) for entry in entries: ldap.delete_entry(entry) def start_dnskeysyncd(self): print("Restarting ipa-dnskeysyncd") self.__start() def create_instance(self, fqdn, realm_name): self.fqdn = fqdn self.realm = realm_name self.suffix = ipautil.realm_to_suffix(self.realm) try: self.stop() except Exception: pass # checking status step must be first self.step("checking status", self.__check_dnssec_status) self.step("setting up bind-dyndb-ldap working directory", self.set_dyndb_ldap_workdir_permissions) self.step("setting up kerberos principal", self.__setup_principal) self.step("setting up SoftHSM", self.__setup_softhsm) self.step("adding DNSSEC containers", self.__setup_dnssec_containers) self.step("creating replica keys", self.__setup_replica_keys) self.step("configuring ipa-dnskeysyncd to start on boot", self.__enable) # we need restart named after setting up this service self.start_creation() def __get_named_uid(self): try: return pwd.getpwnam(constants.NAMED_USER).pw_uid except KeyError: raise RuntimeError("Named UID not found") def __get_named_gid(self): try: return grp.getgrnam(constants.NAMED_GROUP).gr_gid except KeyError: raise RuntimeError("Named GID not found") def __check_dnssec_status(self): self.named_uid = self.__get_named_uid() self.named_gid = self.__get_named_gid() try: self.ods_uid = pwd.getpwnam(constants.ODS_USER).pw_uid except KeyError: raise RuntimeError("OpenDNSSEC UID not found") try: self.ods_gid = grp.getgrnam(constants.ODS_GROUP).gr_gid except KeyError: raise RuntimeError("OpenDNSSEC GID not found") if not dns_container_exists(self.suffix): raise RuntimeError("DNS container does not exist") # ready to be installed, storing a state is required to run uninstall self.backup_state("configured", True) def __setup_dnssec_containers(self): """ Setup LDAP containers for DNSSEC """ if dnssec_container_exists(self.suffix): self.logger.info("DNSSEC container exists (step skipped)") return self._ldap_mod("dnssec.ldif", {'SUFFIX': self.suffix, }) def __setup_softhsm(self): assert self.ods_uid is not None assert self.named_gid is not None token_dir_exists = os.path.exists(paths.DNSSEC_TOKENS_DIR) # create dnssec directory if not os.path.exists(paths.IPA_DNSSEC_DIR): self.logger.debug("Creating %s directory", paths.IPA_DNSSEC_DIR) os.mkdir(paths.IPA_DNSSEC_DIR) os.chmod(paths.IPA_DNSSEC_DIR, 0o770) # chown ods:named os.chown(paths.IPA_DNSSEC_DIR, self.ods_uid, self.named_gid) # setup softhsm2 config file softhsm_conf_txt = ("# SoftHSM v2 configuration file \n" "# File generated by IPA instalation\n" "directories.tokendir = %(tokens_dir)s\n" "objectstore.backend = file") % { 'tokens_dir': paths.DNSSEC_TOKENS_DIR } self.logger.debug("Creating new softhsm config file") named_fd = open(paths.DNSSEC_SOFTHSM2_CONF, 'w') named_fd.seek(0) named_fd.truncate(0) named_fd.write(softhsm_conf_txt) named_fd.close() os.chmod(paths.DNSSEC_SOFTHSM2_CONF, 0o644) # setting up named to use softhsm2 if not self.fstore.has_file(paths.SYSCONFIG_NAMED): self.fstore.backup_file(paths.SYSCONFIG_NAMED) # setting up named and ipa-dnskeysyncd to use our softhsm2 config for sysconfig in [paths.SYSCONFIG_NAMED, paths.SYSCONFIG_IPA_DNSKEYSYNCD]: installutils.set_directive(sysconfig, 'SOFTHSM2_CONF', paths.DNSSEC_SOFTHSM2_CONF, quotes=False, separator='=') if (token_dir_exists and os.path.exists(paths.DNSSEC_SOFTHSM_PIN) and os.path.exists(paths.DNSSEC_SOFTHSM_PIN_SO)): # there is initialized softhsm return # remove old tokens if token_dir_exists: self.logger.debug('Removing old tokens directory %s', paths.DNSSEC_TOKENS_DIR) shutil.rmtree(paths.DNSSEC_TOKENS_DIR) # create tokens subdirectory self.logger.debug('Creating tokens %s directory', paths.DNSSEC_TOKENS_DIR) # sticky bit is required by daemon os.mkdir(paths.DNSSEC_TOKENS_DIR) os.chmod(paths.DNSSEC_TOKENS_DIR, 0o770 | stat.S_ISGID) # chown to ods:named os.chown(paths.DNSSEC_TOKENS_DIR, self.ods_uid, self.named_gid) # generate PINs for softhsm allowed_chars = u'123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' pin_length = 30 # Bind allows max 32 bytes including ending '\0' pin = ipautil.ipa_generate_password(allowed_chars, pin_length) pin_so = ipautil.ipa_generate_password(allowed_chars, pin_length) self.logger.debug("Saving user PIN to %s", paths.DNSSEC_SOFTHSM_PIN) named_fd = open(paths.DNSSEC_SOFTHSM_PIN, 'w') named_fd.seek(0) named_fd.truncate(0) named_fd.write(pin) named_fd.close() os.chmod(paths.DNSSEC_SOFTHSM_PIN, 0o770) # chown to ods:named os.chown(paths.DNSSEC_SOFTHSM_PIN, self.ods_uid, self.named_gid) self.logger.debug("Saving SO PIN to %s", paths.DNSSEC_SOFTHSM_PIN_SO) named_fd = open(paths.DNSSEC_SOFTHSM_PIN_SO, 'w') named_fd.seek(0) named_fd.truncate(0) named_fd.write(pin_so) named_fd.close() # owner must be root os.chmod(paths.DNSSEC_SOFTHSM_PIN_SO, 0o400) # initialize SoftHSM command = [ paths.SOFTHSM2_UTIL, '--init-token', '--slot', str(softhsm_slot), '--label', softhsm_token_label, '--pin', pin, '--so-pin', pin_so, ] self.logger.debug("Initializing tokens") os.environ["SOFTHSM2_CONF"] = paths.DNSSEC_SOFTHSM2_CONF ipautil.run(command, nolog=(pin, pin_so,)) def __setup_replica_keys(self): keylabel = replica_keylabel_template % DNSName(self.fqdn).\ make_absolute().canonicalize().ToASCII() ldap = api.Backend.ldap2 dn_base = DN(('cn', 'keys'), ('cn', 'sec'), ('cn', 'dns'), api.env.basedn) with open(paths.DNSSEC_SOFTHSM_PIN, "r") as f: pin = f.read() os.environ["SOFTHSM2_CONF"] = paths.DNSSEC_SOFTHSM2_CONF p11 = _ipap11helper.P11_Helper(softhsm_slot, pin, paths.LIBSOFTHSM2_SO) try: # generate replica keypair self.logger.debug("Creating replica's key pair") key_id = None while True: # check if key with this ID exist in softHSM # id is 16 Bytes long key_id = "".join(chr(random.randint(0, 255)) for _ in range(0, 16)) replica_pubkey_dn = DN(('ipk11UniqueId', 'autogenerate'), dn_base) pub_keys = p11.find_keys(_ipap11helper.KEY_CLASS_PUBLIC_KEY, label=keylabel, id=key_id) if pub_keys: # key with id exists continue priv_keys = p11.find_keys(_ipap11helper.KEY_CLASS_PRIVATE_KEY, label=keylabel, id=key_id) if not priv_keys: break # we found unique id public_key_handle, _privkey_handle = p11.generate_replica_key_pair( keylabel, key_id, pub_cka_verify=False, pub_cka_verify_recover=False, pub_cka_wrap=True, priv_cka_unwrap=True, priv_cka_sensitive=True, priv_cka_extractable=False) # export public key public_key_blob = p11.export_public_key(public_key_handle) # save key to LDAP replica_pubkey_objectclass = [ 'ipk11Object', 'ipk11PublicKey', 'ipaPublicKeyObject', 'top' ] kw = { 'objectclass': replica_pubkey_objectclass, 'ipk11UniqueId': [u'autogenerate'], 'ipk11Label': [keylabel], 'ipaPublicKey': [public_key_blob], 'ipk11Id': [key_id], 'ipk11Wrap': [True], 'ipk11Verify': [False], 'ipk11VerifyRecover': [False], } self.logger.debug("Storing replica public key to LDAP, %s", replica_pubkey_dn) entry = ldap.make_entry(replica_pubkey_dn, **kw) ldap.add_entry(entry) self.logger.debug("Replica public key stored") self.logger.debug("Setting CKA_WRAP=False for old replica keys") # first create new keys, we don't want disable keys before, we # have new keys in softhsm and LDAP # get replica pub keys with CKA_WRAP=True replica_pub_keys = p11.find_keys(_ipap11helper.KEY_CLASS_PUBLIC_KEY, label=keylabel, cka_wrap=True) # old keys in softHSM for handle in replica_pub_keys: # don't disable wrapping for new key # compare IDs not handle if key_id != p11.get_attribute(handle, _ipap11helper.CKA_ID): p11.set_attribute(handle, _ipap11helper.CKA_WRAP, False) # get old keys from LDAP search_kw = { 'objectclass': u"ipaPublicKeyObject", 'ipk11Label': keylabel, 'ipk11Wrap': True, } filter = ldap.make_filter(search_kw, rules=ldap.MATCH_ALL) entries, _truncated = ldap.find_entries(filter=filter, base_dn=dn_base) for entry in entries: # don't disable wrapping for new key if entry.single_value['ipk11Id'] != key_id: entry['ipk11Wrap'] = [False] ldap.update_entry(entry) finally: p11.finalize() # change tokens mod/owner self.logger.debug("Changing ownership of token files") for (root, dirs, files) in os.walk(paths.DNSSEC_TOKENS_DIR): for directory in dirs: dir_path = os.path.join(root, directory) os.chmod(dir_path, 0o770 | stat.S_ISGID) # chown to ods:named os.chown(dir_path, self.ods_uid, self.named_gid) for filename in files: file_path = os.path.join(root, filename) os.chmod(file_path, 0o770 | stat.S_ISGID) # chown to ods:named os.chown(file_path, self.ods_uid, self.named_gid) def __enable(self): try: self.ldap_enable('DNSKeySync', self.fqdn, None, self.suffix, self.extra_config) except errors.DuplicateEntry: self.logger.error("DNSKeySync service already exists") def __setup_principal(self): assert self.ods_gid is not None installutils.remove_keytab(self.keytab) installutils.kadmin_addprinc(self.principal) # Store the keytab on disk installutils.create_keytab(self.keytab, self.principal) p = self.move_service(self.principal) if p is None: # the service has already been moved, perhaps we're doing a DNS reinstall dnssynckey_principal_dn = DN( ('krbprincipalname', self.principal), ('cn', 'services'), ('cn', 'accounts'), self.suffix) else: dnssynckey_principal_dn = p # Make sure access is strictly reserved to the named user os.chown(self.keytab, 0, self.ods_gid) os.chmod(self.keytab, 0o440) dns_group = DN(('cn', 'DNS Servers'), ('cn', 'privileges'), ('cn', 'pbac'), self.suffix) mod = [(ldap.MOD_ADD, 'member', dnssynckey_principal_dn)] try: api.Backend.ldap2.modify_s(dns_group, mod) except ldap.TYPE_OR_VALUE_EXISTS: pass except Exception as e: self.logger.critical("Could not modify principal's %s entry: %s" % (dnssynckey_principal_dn, str(e))) raise # bind-dyndb-ldap persistent search feature requires both size and time # limit-free connection mod = [(ldap.MOD_REPLACE, 'nsTimeLimit', '-1'), (ldap.MOD_REPLACE, 'nsSizeLimit', '-1'), (ldap.MOD_REPLACE, 'nsIdleTimeout', '-1'), (ldap.MOD_REPLACE, 'nsLookThroughLimit', '-1')] try: api.Backend.ldap2.modify_s(dnssynckey_principal_dn, mod) except Exception as e: self.logger.critical("Could not set principal's %s LDAP limits: %s" % (dnssynckey_principal_dn, str(e))) raise def __start(self): try: self.restart() except Exception as e: print("Failed to start ipa-dnskeysyncd") self.logger.debug("Failed to start ipa-dnskeysyncd: %s", e) def uninstall(self): if self.is_configured(): self.print_msg("Unconfiguring %s" % self.service_name) # Just eat states self.restore_state("running") self.restore_state("enabled") self.restore_state("configured") # stop and disable service (IPA service, we do not need it anymore) self.stop() self.disable() for f in [paths.SYSCONFIG_NAMED]: try: self.fstore.restore_file(f) except ValueError as error: self.logger.debug(error) # remove softhsm pin, to make sure new installation will generate # new token database # do not delete *so pin*, user can need it to get token data try: os.remove(paths.DNSSEC_SOFTHSM_PIN) except Exception: pass installutils.remove_keytab(self.keytab)