From abf4418c4612e5354e85c00a461bf537d8deff9c Mon Sep 17 00:00:00 2001 From: Martin Basti Date: Oct 21 2014 10:18:55 +0000 Subject: DNSSEC: opendnssec services 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 Reviewed-By: David Kupka --- diff --git a/install/share/Makefile.am b/install/share/Makefile.am index 43eaddc..878d886 100644 --- a/install/share/Makefile.am +++ b/install/share/Makefile.am @@ -56,6 +56,8 @@ app_DATA = \ memberof-task.ldif \ memberof-conf.ldif \ nis.uldif \ + opendnssec_conf.template \ + opendnssec_kasp.template \ unique-attributes.ldif \ schema_compat.uldif \ ldapi.ldif \ diff --git a/install/share/opendnssec_conf.template b/install/share/opendnssec_conf.template new file mode 100644 index 0000000..c407326 --- /dev/null +++ b/install/share/opendnssec_conf.template @@ -0,0 +1,46 @@ + + + + + + + + $SOFTHSM_LIB + $TOKEN_LABEL + $PIN + + + + + + + + local0 + + + /etc/opendnssec/kasp.xml + /etc/opendnssec/zonelist.xml + + + + + + + ods + ods + + + $KASP_DB + PT3600S + + + + + + + + diff --git a/install/share/opendnssec_kasp.template b/install/share/opendnssec_kasp.template new file mode 100644 index 0000000..cad9f7c --- /dev/null +++ b/install/share/opendnssec_kasp.template @@ -0,0 +1,150 @@ + + + + + + + + A default policy that will amaze you and your friends + + PT2H + P3D + + P14D + P14D + + PT12H + PT3600S + + + + + + + P100D + + 1 + 5 + + + + + + + + PT3600S + PT3600S + PT3600S + + P14D + + + + 8 + P1Y + SoftHSM + + + + + 8 + P90D + SoftHSM + + + + + + PT43200S + + PT3600S + PT3600S + unixtime + + + + + PT9999S + + PT3600S + + + PT172800S + PT10800S + + + + + + + Quick turnaround policy for lab work + + PT10M + PT30M + + PT1H + PT1H + + PT1M + PT3600S + + + + + + + + + PT300S + PT360S + PT360S + + P14D + + + + 8 + P1Y + SoftHSM + + + + + 8 + PT4H + SoftHSM + + + + + + PT300S + + PT300S + PT300S + unixtime + + + + + PT9999S + + PT3600S + + + PT172800S + PT10800S + + + + + diff --git a/ipapython/p11helper.py b/ipapython/p11helper.py new file mode 100644 index 0000000..f084855 --- /dev/null +++ b/ipapython/p11helper.py @@ -0,0 +1,40 @@ +#!/usr/bin/python +# +# Copyright (C) 2014 FreeIPA Contributors see COPYING for license +# + +import _ipap11helper +import random + +def generate_master_key(p11, keylabel=u"dnssec-master", key_length=16, + disable_old_keys=True): + assert isinstance(p11, _ipap11helper.P11_Helper) + + key_id = None + while True: + # check if key with this ID exist in LDAP or softHSM + # id is 16 Bytes long + key_id = "".join(chr(random.randint(0, 255)) for _ in xrange(0, 16)) + keys = p11.find_keys(_ipap11helper.KEY_CLASS_SECRET_KEY, + label=keylabel, + id=key_id) + if not keys: + break # we found unique id + + p11.generate_master_key(keylabel, + key_id, + key_length=key_length, + cka_wrap=True, + cka_unwrap=True) + + if disable_old_keys: + # set CKA_WRAP=False for old master keys + master_keys = p11.find_keys(_ipap11helper.KEY_CLASS_SECRET_KEY, + label=keylabel, + cka_wrap=True) + + for handle in master_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) diff --git a/ipaserver/install/odsexporterinstance.py b/ipaserver/install/odsexporterinstance.py new file mode 100644 index 0000000..57b1451 --- /dev/null +++ b/ipaserver/install/odsexporterinstance.py @@ -0,0 +1,179 @@ +# +# Copyright (C) 2014 FreeIPA Contributors see COPYING for license +# + +import service +import installutils +import os +import pwd +import grp + +import ldap + +from ipapython.ipa_log_manager import * +from ipapython.dn import DN +from ipapython import sysrestore, ipautil, ipaldap +from ipaplatform.paths import paths +from ipaplatform import services +from ipalib import errors + + +class ODSExporterInstance(service.Service): + def __init__(self, fstore=None, dm_password=None): + service.Service.__init__( + self, "ipa-ods-exporter", + service_desc="IPA OpenDNSSEC exporter daemon", + dm_password=dm_password, + ldapi=False, + autobind=ipaldap.AUTOBIND_DISABLED + ) + self.dm_password = dm_password + self.ods_uid = None + self.ods_gid = None + self.enable_if_exists = False + + if fstore: + self.fstore = fstore + else: + self.fstore = sysrestore.FileStore(paths.SYSRESTORE) + + suffix = ipautil.dn_attribute_property('_suffix') + + def create_instance(self, fqdn, realm_name): + self.backup_state("enabled", self.is_enabled()) + self.backup_state("running", self.is_running()) + self.fqdn = fqdn + self.realm = realm_name + self.suffix = ipautil.realm_to_suffix(self.realm) + + 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 DNS Key Exporter", self.__setup_key_exporter) + self.step("setting up kerberos principal", self.__setup_principal) + self.step("disabling default signer daemon", self.__disable_signerd) + self.step("starting DNS Key Exporter", self.__start) + self.step("configuring DNS Key Exporter to start on boot", self.__enable) + self.start_creation() + + def __check_dnssec_status(self): + ods_enforcerd = services.knownservices.ods_enforcerd + + 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") + + def __enable(self): + + try: + self.ldap_enable('DNSKeyExporter', self.fqdn, self.dm_password, + self.suffix) + except errors.DuplicateEntry: + root_logger.error("DNSKeyExporter service already exists") + self.enable() + + def __setup_key_exporter(self): + installutils.set_directive(paths.SYSOCNFIG_IPA_ODS_EXPORTER, + 'SOFTHSM2_CONF', + paths.DNSSEC_SOFTHSM2_CONF, + quotes=False, separator='=') + + def __setup_principal(self): + assert self.ods_uid is not None + dns_exporter_principal = "ipa-ods-exporter/" + self.fqdn + "@" + self.realm + installutils.kadmin_addprinc(dns_exporter_principal) + + # Store the keytab on disk + installutils.create_keytab(paths.IPA_ODS_EXPORTER_KEYTAB, dns_exporter_principal) + p = self.move_service(dns_exporter_principal) + if p is None: + # the service has already been moved, perhaps we're doing a DNS reinstall + dns_exporter_principal_dn = DN( + ('krbprincipalname', dns_exporter_principal), + ('cn', 'services'), ('cn', 'accounts'), self.suffix) + else: + dns_exporter_principal_dn = p + + # Make sure access is strictly reserved to the ods user + os.chmod(paths.IPA_ODS_EXPORTER_KEYTAB, 0440) + os.chown(paths.IPA_ODS_EXPORTER_KEYTAB, 0, self.ods_gid) + + dns_group = DN(('cn', 'DNS Servers'), ('cn', 'privileges'), + ('cn', 'pbac'), self.suffix) + mod = [(ldap.MOD_ADD, 'member', dns_exporter_principal_dn)] + + try: + self.admin_conn.modify_s(dns_group, mod) + except ldap.TYPE_OR_VALUE_EXISTS: + pass + except Exception, e: + root_logger.critical("Could not modify principal's %s entry: %s" + % (dns_exporter_principal_dn, str(e))) + raise + + # 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(dns_exporter_principal_dn, mod) + except Exception, e: + root_logger.critical("Could not set principal's %s LDAP limits: %s" + % (dns_exporter_principal_dn, str(e))) + raise + + def __disable_signerd(self): + signerd_service = services.knownservices.ods_signerd + + self.backup_state("singerd_running", signerd_service.is_running()) + self.backup_state("singerd_enabled", signerd_service.is_enabled()) + + # disable default opendnssec signer daemon + signerd_service.stop() + signerd_service.mask() + + def __start(self): + self.start() + + 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 enabled is not None and not enabled: + self.disable() + + if running is not None and running: + self.start() + + # restore state of dnssec default signer daemon + signerd_enabled = self.restore_state("singerd_enabled") + signerd_running = self.restore_state("singerd_runnning") + signerd_service = services.knownservices.ods_signerd + + signerd_service.unmask() + + # service was stopped and disabled by setup + if signerd_enabled: + signerd_service.enable() + + if signerd_running: + signerd_service.start() diff --git a/ipaserver/install/opendnssecinstance.py b/ipaserver/install/opendnssecinstance.py new file mode 100644 index 0000000..0d2fb00 --- /dev/null +++ b/ipaserver/install/opendnssecinstance.py @@ -0,0 +1,299 @@ +# +# Copyright (C) 2014 FreeIPA Contributors see COPYING for license +# + +import random + +import service +import os +import pwd +import grp +import stat + +import _ipap11helper + +import installutils +from ipapython.ipa_log_manager import * +from ipapython.dn import DN +from ipapython import sysrestore, ipautil, ipaldap, p11helper +from ipaplatform import services +from ipaplatform.paths import paths +from ipalib import errors, api +from ipaserver.install import dnskeysyncinstance + +KEYMASTER = u'dnssecKeyMaster' +softhsm_slot = 0 + + +def get_dnssec_key_masters(conn): + """ + :return: list of active dnssec key masters + """ + assert conn is not None + + dn = DN(('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'), api.env.basedn) + + filter_attrs = { + u'cn': u'DNSSEC', + u'objectclass': u'ipaConfigObject', + u'ipaConfigString': [KEYMASTER, u'enabledService'], + } + only_masters_f = conn.make_filter(filter_attrs, rules=conn.MATCH_ALL) + + try: + entries = conn.find_entries(filter=only_masters_f, base_dn=dn) + except errors.NotFound: + return [] + + keymasters_list = [] + for entry in entries[0]: + keymasters_list.append(str(entry.dn[1].value)) + + return keymasters_list + + +def check_inst(): + if not os.path.exists(paths.ODS_KSMUTIL): + print ("Please install the 'opendnssec' package and start " + "the installation again") + return False + return True + + +class OpenDNSSECInstance(service.Service): + def __init__(self, fstore=None, dm_password=None): + service.Service.__init__( + self, "ods-enforcerd", + service_desc="OpenDNSSEC enforcer daemon", + dm_password=dm_password, + ldapi=False, + autobind=ipaldap.AUTOBIND_DISABLED + ) + self.dm_password = dm_password + self.ods_uid = None + self.ods_gid = None + self.conf_file_dict = { + 'SOFTHSM_LIB': paths.LIBSOFTHSM2_SO, + 'TOKEN_LABEL': dnskeysyncinstance.softhsm_token_label, + 'KASP_DB': paths.OPENDNSSEC_KASP_DB, + } + self.kasp_file_dict = {} + self.extra_config = [KEYMASTER] + + if fstore: + self.fstore = fstore + else: + self.fstore = sysrestore.FileStore(paths.SYSRESTORE) + + suffix = ipautil.dn_attribute_property('_suffix') + + def get_masters(self): + if not self.admin_conn: + self.ldap_connect() + return get_dnssec_key_masters(self.admin_conn) + + def create_instance(self, fqdn, realm_name, generate_master_key=True): + self.backup_state("enabled", self.is_enabled()) + self.backup_state("running", self.is_running()) + self.fqdn = fqdn + self.realm = realm_name + self.suffix = ipautil.realm_to_suffix(self.realm) + + try: + self.stop() + except Exception: + pass + + # get a connection to the DS + if not self.admin_conn: + self.ldap_connect() + # checking status must be first + self.step("checking status", self.__check_dnssec_status) + self.step("setting up configuration files", self.__setup_conf_files) + self.step("setting up ownership and file mode bits", self.__setup_ownership_file_modes) + if generate_master_key: + self.step("generating master key", self.__generate_master_key) + self.step("setting up OpenDNSSEC", self.__setup_dnssec) + self.step("setting up ipa-dnskeysyncd", self.__setup_dnskeysyncd) + self.step("starting OpenDNSSEC enforcer", self.__start) + self.step("configuring OpenDNSSEC enforcer to start on boot", self.__enable) + 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") + + def __enable(self): + try: + self.ldap_enable('DNSSEC', self.fqdn, self.dm_password, + self.suffix, self.extra_config) + except errors.DuplicateEntry: + root_logger.error("DNSSEC service already exists") + self.enable() + + def __setup_conf_files(self): + if not self.fstore.has_file(paths.OPENDNSSEC_CONF_FILE): + self.fstore.backup_file(paths.OPENDNSSEC_CONF_FILE) + + if not self.fstore.has_file(paths.OPENDNSSEC_KASP_FILE): + self.fstore.backup_file(paths.OPENDNSSEC_KASP_FILE) + + pin_fd = open(paths.DNSSEC_SOFTHSM_PIN, "r") + pin = pin_fd.read() + pin_fd.close() + + # add pin to template + sub_conf_dict = self.conf_file_dict + sub_conf_dict['PIN'] = pin + + ods_conf_txt = ipautil.template_file( + ipautil.SHARE_DIR + "opendnssec_conf.template", sub_conf_dict) + ods_conf_fd = open(paths.OPENDNSSEC_CONF_FILE, 'w') + ods_conf_fd.seek(0) + ods_conf_fd.truncate(0) + ods_conf_fd.write(ods_conf_txt) + ods_conf_fd.close() + + ods_kasp_txt = ipautil.template_file( + ipautil.SHARE_DIR + "opendnssec_kasp.template", self.kasp_file_dict) + ods_kasp_fd = open(paths.OPENDNSSEC_KASP_FILE, 'w') + ods_kasp_fd.seek(0) + ods_kasp_fd.truncate(0) + ods_kasp_fd.write(ods_kasp_txt) + ods_kasp_fd.close() + + if not self.fstore.has_file(paths.SYSCONFIG_ODS): + self.fstore.backup_file(paths.SYSCONFIG_ODS) + + installutils.set_directive(paths.SYSCONFIG_ODS, + 'SOFTHSM2_CONF', + paths.DNSSEC_SOFTHSM2_CONF, + quotes=False, separator='=') + + def __setup_ownership_file_modes(self): + assert self.ods_uid is not None + assert self.ods_gid is not None + + # workarounds for packaging bugs in opendnssec-1.4.5-2.fc20.x86_64 + # https://bugzilla.redhat.com/show_bug.cgi?id=1098188 + for (root, dirs, files) in os.walk(paths.ETC_OPENDNSSEC_DIR): + for directory in dirs: + dir_path = os.path.join(root, directory) + os.chmod(dir_path, 0770) + # chown to root:ods + os.chown(dir_path, 0, self.ods_gid) + for filename in files: + file_path = os.path.join(root, filename) + os.chmod(file_path, 0660) + # chown to root:ods + os.chown(file_path, 0, self.ods_gid) + + for (root, dirs, files) in os.walk(paths.VAR_OPENDNSSEC_DIR): + for directory in dirs: + dir_path = os.path.join(root, directory) + os.chmod(dir_path, 0770) + # chown to ods:ods + os.chown(dir_path, self.ods_uid, self.ods_gid) + for filename in files: + file_path = os.path.join(root, filename) + os.chmod(file_path, 0660) + # chown to ods:ods + os.chown(file_path, self.ods_uid, self.ods_gid) + + def __generate_master_key(self): + + 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 master key + root_logger.debug("Creating master key") + p11helper.generate_master_key(p11) + + # change tokens mod/owner + root_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) + os.chown(dir_path, self.ods_uid, self.named_gid) # chown to ods:named + for filename in files: + file_path = os.path.join(root, filename) + os.chmod(file_path, 0770 | stat.S_ISGID) + os.chown(file_path, self.ods_uid, self.named_gid) # chown to ods:named + + finally: + p11.finalize() + + def __setup_dnssec(self): + # run once only + if self.get_state("KASP_DB_configured"): + root_logger.debug("Already configured, skipping step") + + self.backup_state("KASP_DB_configured", True) + + if not self.fstore.has_file(paths.OPENDNSSEC_KASP_DB): + self.fstore.backup_file(paths.OPENDNSSEC_KASP_DB) + + command = [ + paths.ODS_KSMUTIL, + 'setup' + ] + + ods_enforcerd = services.knownservices.ods_enforcerd + ipautil.run(command, stdin="y", runas=ods_enforcerd.get_user_name()) + + def __setup_dnskeysyncd(self): + # set up dnskeysyncd this is DNSSEC master + installutils.set_directive(paths.SYSCONFIG_IPA_DNSKEYSYNCD, + 'ISMASTER', + '1', + quotes=False, separator='=') + + def __start(self): + self.restart() # needed to reload conf files + + 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") + + for f in [paths.OPENDNSSEC_CONF_FILE, paths.OPENDNSSEC_KASP_FILE, + paths.OPENDNSSEC_KASP_DB, paths.SYSCONFIG_ODS]: + try: + self.fstore.restore_file(f) + except ValueError, error: + root_logger.debug(error) + pass + + if enabled is not None and not enabled: + self.disable() + + if running is not None and running: + self.start()