diff options
author | Martin Basti <mbasti@redhat.com> | 2014-10-16 15:43:29 +0200 |
---|---|---|
committer | Martin Kosek <mkosek@redhat.com> | 2014-10-21 12:18:55 +0200 |
commit | 52acc54f9efbac863e06d130f7e2110f3ee71db9 (patch) | |
tree | 716aa6c179ace02f80aca0f0e7672a21045c1fe9 | |
parent | 3c7bc2a4fdef3bafea469d4b633faf72cc316d35 (diff) | |
download | freeipa-52acc54f9efbac863e06d130f7e2110f3ee71db9.tar.gz freeipa-52acc54f9efbac863e06d130f7e2110f3ee71db9.tar.xz freeipa-52acc54f9efbac863e06d130f7e2110f3ee71db9.zip |
DNSSEC: DNS key synchronization daemon
Tickets:
https://fedorahosted.org/freeipa/ticket/3801
https://fedorahosted.org/freeipa/ticket/4417
Design:
https://fedorahosted.org/bind-dyndb-ldap/wiki/BIND9/Design/DNSSEC
Reviewed-By: Jan Cholasta <jcholast@redhat.com>
Reviewed-By: David Kupka <dkupka@redhat.com>
-rw-r--r-- | install/share/Makefile.am | 3 | ||||
-rw-r--r-- | install/share/dnssec.ldif | 11 | ||||
-rw-r--r-- | install/share/uuid.ldif (renamed from install/share/uuid-ipauniqueid.ldif) | 12 | ||||
-rw-r--r-- | install/updates/20-uuid.update | 11 | ||||
-rw-r--r-- | install/updates/Makefile.am | 1 | ||||
-rw-r--r-- | ipapython/dnsutil.py | 3 | ||||
-rw-r--r-- | ipaserver/install/dnskeysyncinstance.py | 484 | ||||
-rw-r--r-- | ipaserver/install/dsinstance.py | 2 |
8 files changed, 525 insertions, 2 deletions
diff --git a/install/share/Makefile.am b/install/share/Makefile.am index 7833d3295..43eaddc0b 100644 --- a/install/share/Makefile.am +++ b/install/share/Makefile.am @@ -33,6 +33,7 @@ app_DATA = \ replica-acis.ldif \ ds-nfiles.ldif \ dns.ldif \ + dnssec.ldif \ kerberos.ldif \ indices.ldif \ bind.named.conf.template \ @@ -63,7 +64,7 @@ app_DATA = \ managed-entries.ldif \ user_private_groups.ldif \ host_nis_groups.ldif \ - uuid-ipauniqueid.ldif \ + uuid.ldif \ modrdn-krbprinc.ldif \ entryusn.ldif \ root-autobind.ldif \ diff --git a/install/share/dnssec.ldif b/install/share/dnssec.ldif new file mode 100644 index 000000000..27442f366 --- /dev/null +++ b/install/share/dnssec.ldif @@ -0,0 +1,11 @@ +dn: cn=sec,cn=dns,$SUFFIX +changetype: add +objectClass: nsContainer +objectClass: top +cn: sec + +dn: cn=keys,cn=sec,cn=dns,$SUFFIX +changetype: add +objectClass: nsContainer +objectClass: top +cn: keys diff --git a/install/share/uuid-ipauniqueid.ldif b/install/share/uuid.ldif index c8d08cd9b..129246354 100644 --- a/install/share/uuid-ipauniqueid.ldif +++ b/install/share/uuid.ldif @@ -9,3 +9,15 @@ ipaUuidMagicRegen: autogenerate ipaUuidFilter: (|(objectclass=ipaObject)(objectclass=ipaAssociation)) ipaUuidScope: $SUFFIX ipaUuidEnforce: TRUE + +# add plugin configuration for ipk11UniqueId +dn: cn=IPK11 Unique IDs,cn=IPA UUID,cn=plugins,cn=config +changetype: add +objectclass: top +objectclass: extensibleObject +cn: IPK11 Unique IDs +ipaUuidAttr: ipk11UniqueID +ipaUuidMagicRegen: autogenerate +ipaUuidFilter: (objectclass=ipk11Object) +ipaUuidScope: $SUFFIX +ipaUuidEnforce: FALSE diff --git a/install/updates/20-uuid.update b/install/updates/20-uuid.update new file mode 100644 index 000000000..0f1184605 --- /dev/null +++ b/install/updates/20-uuid.update @@ -0,0 +1,11 @@ + +# add plugin configuration for ipk11UniqueId +dn: cn=IPK11 Unique IDs,cn=IPA UUID,cn=plugins,cn=config +default: objectclass: top +default: objectclass: extensibleObject +default: cn: IPK11 Unique IDs +default: ipaUuidAttr: ipk11UniqueID +default: ipaUuidMagicRegen: autogenerate +default: ipaUuidFilter: (objectclass=ipk11Object) +default: ipaUuidScope: $SUFFIX +default: ipaUuidEnforce: FALSE diff --git a/install/updates/Makefile.am b/install/updates/Makefile.am index 8bd0ee5e9..e62a64cea 100644 --- a/install/updates/Makefile.am +++ b/install/updates/Makefile.am @@ -19,6 +19,7 @@ app_DATA = \ 20-syncrepl.update \ 20-user_private_groups.update \ 20-winsync_index.update \ + 20-uuid.update \ 21-replicas_container.update \ 21-ca_renewal_container.update \ 21-certstore_container.update \ diff --git a/ipapython/dnsutil.py b/ipapython/dnsutil.py index 3602d22cd..d7841fe25 100644 --- a/ipapython/dnsutil.py +++ b/ipapython/dnsutil.py @@ -62,6 +62,9 @@ class DNSName(dns.name.Name): #method named by RFC 3490 and python standard library return str(self).decode('ascii') # must be unicode string + def canonicalize(self): + return DNSName(super(DNSName, self).canonicalize()) + def concatenate(self, other): return DNSName(super(DNSName, self).concatenate(other)) diff --git a/ipaserver/install/dnskeysyncinstance.py b/ipaserver/install/dnskeysyncinstance.py new file mode 100644 index 000000000..1dd9a0983 --- /dev/null +++ b/ipaserver/install/dnskeysyncinstance.py @@ -0,0 +1,484 @@ +# +# Copyright (C) 2014 FreeIPA Contributors see COPYING for license +# + +from ipapython.dnsutil import DNSName + +import service +import installutils +import os +import pwd +import grp +import random +import shutil +import stat + +import ldap +import _ipap11helper + +from ipapython.ipa_log_manager import * +from ipapython.dn import DN +from ipapython import ipaldap +from ipapython import sysrestore, ipautil +from ipaplatform import services +from ipaplatform.paths import paths +from ipalib import errors, api +from ipalib.constants import CACERT +from ipaserver.install.bindinstance import dns_container_exists + +softhsm_token_label = u'ipaDNSSEC' +softhsm_slot = 0 +replica_keylabel_template = u"dnssec-replica:%s" + +def check_inst(): + if not os.path.exists(paths.DNSSEC_KEYFROMLABEL): + print ("Please install the 'bind-pkcs11-utils' package and start " + "the installation again") + return False + return True + +def dnssec_container_exists(fqdn, suffix, dm_password=None, ldapi=False, + realm=None, autobind=ipaldap.AUTOBIND_DISABLED): + """ + Test whether the dns container exists. + """ + assert isinstance(suffix, DN) + try: + # At install time we may need to use LDAPI to avoid chicken/egg + # issues with SSL certs and truting CAs + if ldapi: + conn = ipaldap.IPAdmin(host=fqdn, ldapi=True, realm=realm) + else: + conn = ipaldap.IPAdmin(host=fqdn, port=636, cacert=CACERT) + + conn.do_bind(dm_password, autobind=autobind) + except ldap.SERVER_DOWN: + raise RuntimeError('LDAP server on %s is not responding. Is IPA installed?' % fqdn) + + ret = conn.entry_exists(DN(('cn', 'sec'), ('cn', 'dns'), suffix)) + conn.unbind() + + return ret + + +class DNSKeySyncInstance(service.Service): + def __init__(self, fstore=None, dm_password=None, logger=root_logger, + ldapi=False): + service.Service.__init__( + self, "ipa-dnskeysyncd", + service_desc="DNS key synchronization service", + dm_password=dm_password, + ldapi=ldapi + ) + self.dm_password = dm_password + 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 + if fstore: + self.fstore = fstore + else: + self.fstore = sysrestore.FileStore(paths.SYSRESTORE) + + suffix = ipautil.dn_attribute_property('_suffix') + + 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) + self.backup_state("enabled", self.is_enabled()) + self.backup_state("running", self.is_running()) + try: + self.stop() + except: + pass + + # get a connection to the DS + self.ldap_connect() + # checking status step must be first + self.step("checking status", self.__check_dnssec_status) + 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 __check_dnssec_status(self): + named = services.knownservices.named + ods_enforcerd = services.knownservices.ods_enforcerd + + try: + self.named_uid = pwd.getpwnam(named.get_user_name()).pw_uid + except KeyError: + raise RuntimeError("Named UID not found") + + try: + self.named_gid = grp.getgrnam(named.get_group_name()).gr_gid + except KeyError: + raise RuntimeError("Named GID not found") + + try: + self.ods_uid = pwd.getpwnam(ods_enforcerd.get_user_name()).pw_uid + except KeyError: + raise RuntimeError("OpenDNSSEC UID not found") + + try: + self.ods_gid = grp.getgrnam(ods_enforcerd.get_group_name()).gr_gid + except KeyError: + raise RuntimeError("OpenDNSSEC GID not found") + + if not dns_container_exists( + self.fqdn, self.suffix, realm=self.realm, ldapi=True, + dm_password=self.dm_password, autobind=ipaldap.AUTOBIND_AUTO + ): + raise RuntimeError("DNS container does not exist") + + def __setup_dnssec_containers(self): + """ + Setup LDAP containers for DNSSEC + """ + if dnssec_container_exists(self.fqdn, self.suffix, ldapi=True, + dm_password=self.dm_password, + realm=self.realm, + autobind=ipaldap.AUTOBIND_AUTO): + + 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, 0770) + # 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() + + # 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, 0770 | 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, 0770) + # 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, 0400) + + # 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 = self.admin_conn + 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 xrange(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, private_key_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, 0770 | 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, 0770 | 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, self.dm_password, + self.suffix, self.extra_config) + except errors.DuplicateEntry: + self.logger.error("DNSKeySync service already exists") + self.enable() + + def __setup_principal(self): + assert self.ods_gid is not None + dnssynckey_principal = "ipa-dnskeysyncd/" + self.fqdn + "@" + self.realm + installutils.kadmin_addprinc(dnssynckey_principal) + + # Store the keytab on disk + installutils.create_keytab(paths.IPA_DNSKEYSYNCD_KEYTAB, dnssynckey_principal) + p = self.move_service(dnssynckey_principal) + if p is None: + # the service has already been moved, perhaps we're doing a DNS reinstall + dnssynckey_principal_dn = DN( + ('krbprincipalname', dnssynckey_principal), + ('cn', 'services'), ('cn', 'accounts'), self.suffix) + else: + dnssynckey_principal_dn = p + + # Make sure access is strictly reserved to the named user + os.chown(paths.IPA_DNSKEYSYNCD_KEYTAB, 0, self.ods_gid) + os.chmod(paths.IPA_DNSKEYSYNCD_KEYTAB, 0440) + + dns_group = DN(('cn', 'DNS Servers'), ('cn', 'privileges'), + ('cn', 'pbac'), self.suffix) + mod = [(ldap.MOD_ADD, 'member', dnssynckey_principal_dn)] + + try: + self.admin_conn.modify_s(dns_group, mod) + except ldap.TYPE_OR_VALUE_EXISTS: + pass + except Exception, 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: + self.admin_conn.modify_s(dnssynckey_principal_dn, mod) + except Exception, 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 not self.is_configured(): + return + + self.print_msg("Unconfiguring %s" % self.service_name) + + running = self.restore_state("running") + enabled = self.restore_state("enabled") + + if running is not None: + self.stop() + + for f in [paths.SYSCONFIG_NAMED]: + try: + self.fstore.restore_file(f) + except ValueError, error: + self.logger.debug(error) + pass + + # 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 + + if enabled is not None and not enabled: + self.disable() + + if running is not None and running: + self.start() diff --git a/ipaserver/install/dsinstance.py b/ipaserver/install/dsinstance.py index 877d653c8..da5353471 100644 --- a/ipaserver/install/dsinstance.py +++ b/ipaserver/install/dsinstance.py @@ -537,7 +537,7 @@ class DsInstance(service.Service): def __config_uuid_module(self): self._ldap_mod("uuid-conf.ldif") - self._ldap_mod("uuid-ipauniqueid.ldif", self.sub_dict) + self._ldap_mod("uuid.ldif", self.sub_dict) def __config_modrdn_module(self): self._ldap_mod("modrdn-conf.ldif") |