diff options
author | Martin Basti <mbasti@redhat.com> | 2015-05-13 14:45:32 +0200 |
---|---|---|
committer | Tomas Babej <tbabej@redhat.com> | 2015-07-07 08:37:15 +0200 |
commit | e151492560db25fa13c2a3edf5e2139dc6629047 (patch) | |
tree | 1cfb5a1a48dd522e265d425695122858a9366288 | |
parent | b258bcee8337063259aa38b4387b9bb5721fb380 (diff) | |
download | freeipa-e151492560db25fa13c2a3edf5e2139dc6629047.tar.gz freeipa-e151492560db25fa13c2a3edf5e2139dc6629047.tar.xz freeipa-e151492560db25fa13c2a3edf5e2139dc6629047.zip |
DNSSEC: allow to disable/replace DNSSEC key master
This commit allows to replace or disable DNSSEC key master
Replacing DNSSEC master requires to copy kasp.db file manually by user
ipa-dns-install:
--disable-dnssec-master DNSSEC master will be disabled
--dnssec-master --kasp-db=FILE This configure new DNSSEC master server, kasp.db from old server is required for sucessful replacement
--force Skip checks
https://fedorahosted.org/freeipa/ticket/4657
Reviewed-By: Petr Spacek <pspacek@redhat.com>
-rwxr-xr-x | install/tools/ipa-dns-install | 12 | ||||
-rw-r--r-- | ipaplatform/base/paths.py | 2 | ||||
-rw-r--r-- | ipaserver/install/dns.py | 154 | ||||
-rw-r--r-- | ipaserver/install/odsexporterinstance.py | 12 | ||||
-rw-r--r-- | ipaserver/install/opendnssecinstance.py | 98 | ||||
-rw-r--r-- | ipaserver/install/server/install.py | 23 | ||||
-rw-r--r-- | ipaserver/install/server/replicainstall.py | 30 |
7 files changed, 309 insertions, 22 deletions
diff --git a/install/tools/ipa-dns-install b/install/tools/ipa-dns-install index fd9311657..d82799201 100755 --- a/install/tools/ipa-dns-install +++ b/install/tools/ipa-dns-install @@ -61,6 +61,15 @@ def parse_options(): help="DNS zone manager e-mail address. Defaults to hostmaster@DOMAIN") parser.add_option("-U", "--unattended", dest="unattended", action="store_true", default=False, help="unattended installation never prompts the user") + parser.add_option("--disable-dnssec-master", dest="disable_dnssec_master", + action="store_true", default=False, help="Disable the " + "DNSSEC master on this server") + parser.add_option("--kasp-db", dest="kasp_db_file", type="string", + metavar="FILE", action="store", help="Copy OpenDNSSEC " + "metadata from the specified file (will not create a new " + "kasp.db file)") + parser.add_option("--force", dest="force", action="store_true", + help="Force install") options, args = parser.parse_args() safe_options = parser.get_safe_opts(options) @@ -74,6 +83,9 @@ def parse_options(): if not options.forwarders and not options.no_forwarders: parser.error("You must specify at least one --forwarder option or --no-forwarders option") + if options.kasp_db_file and not ipautil.file_exists(options.kasp_db_file): + parser.error("File %s does not exist" % options.kasp_db_file) + if options.dm_password: print ("WARNING: Option -p/--ds-password is deprecated " "and should not be used anymore.") diff --git a/ipaplatform/base/paths.py b/ipaplatform/base/paths.py index ff80eab98..9fef3e7a1 100644 --- a/ipaplatform/base/paths.py +++ b/ipaplatform/base/paths.py @@ -90,6 +90,7 @@ class BasePathNamespace(object): ETC_OPENDNSSEC_DIR = "/etc/opendnssec" OPENDNSSEC_CONF_FILE = "/etc/opendnssec/conf.xml" OPENDNSSEC_KASP_FILE = "/etc/opendnssec/kasp.xml" + OPENDNSSEC_ZONELIST_FILE = "/etc/opendnssec/zonelist.xml" OPENLDAP_LDAP_CONF = "/etc/openldap/ldap.conf" PAM_LDAP_CONF = "/etc/pam_ldap.conf" PASSWD = "/etc/passwd" @@ -276,6 +277,7 @@ class BasePathNamespace(object): SYSRESTORE_INDEX = "/var/lib/ipa-client/sysrestore/sysrestore.index" IPA_BACKUP_DIR = "/var/lib/ipa/backup" IPA_DNSSEC_DIR = "/var/lib/ipa/dnssec" + IPA_KASP_DB_BACKUP = "/var/lib/ipa/ipa-kasp.db.backup" DNSSEC_TOKENS_DIR = "/var/lib/ipa/dnssec/tokens" DNSSEC_SOFTHSM_PIN = "/var/lib/ipa/dnssec/softhsm_pin" IPA_CA_CSR = "/var/lib/ipa/ca.csr" diff --git a/ipaserver/install/dns.py b/ipaserver/install/dns.py index 8d9570d68..1382382b5 100644 --- a/ipaserver/install/dns.py +++ b/ipaserver/install/dns.py @@ -4,10 +4,15 @@ import sys +from subprocess import CalledProcessError + from ipalib import api +from ipalib import errors from ipaplatform.paths import paths +from ipaplatform import services from ipapython import ipautil from ipapython import sysrestore +from ipapython.dn import DN from ipapython.ipa_log_manager import root_logger from ipapython.ipaldap import AUTOBIND_ENABLED from ipapython.ipautil import user_input @@ -23,6 +28,67 @@ ip_addresses = [] dns_forwarders = [] reverse_zones = [] +NEW_MASTER_MARK = 'NEW_DNSSEC_MASTER' + + +def _find_dnssec_enabled_zones(conn): + search_kw = {'idnssecinlinesigning': True} + dnssec_enabled_filter = conn.make_filter(search_kw) + dn = DN('cn=dns', api.env.basedn) + try: + entries, truncated = conn.find_entries( + base_dn=dn, filter=dnssec_enabled_filter, attrs_list=['idnsname']) + except errors.NotFound: + return [] + else: + return [entry.single_value['idnsname'] for entry in entries + if 'idnsname' in entry] + + +def _is_master(): + # test if server is DNSSEC key master + masters = opendnssecinstance.get_dnssec_key_masters(api.Backend.ldap2) + if api.env.host not in masters: + raise RuntimeError("Current server is not DNSSEC key master") + + +def _disable_dnssec(): + fstore = sysrestore.FileStore(paths.SYSRESTORE) + + ods = opendnssecinstance.OpenDNSSECInstance( + fstore, ldapi=True, autobind=AUTOBIND_ENABLED) + ods.realm = api.env.realm + + ods_exporter = odsexporterinstance.ODSExporterInstance(fstore, ldapi=True) + ods_exporter.realm = api.env.realm + + # unconfigure services first + ods.uninstall() # needs keytab to flush the latest ods database + ods_exporter.uninstall() + + ods.ldap_connect() + ods.ldap_disable('DNSSEC', api.env.host, api.env.basedn) + + ods_exporter.ldap_connect() + ods_exporter.ldap_disable('DNSKeyExporter', api.env.host, api.env.basedn) + ods_exporter.remove_service() + + ods.ldap_disconnect() + ods_exporter.ldap_disconnect() + + conn = api.Backend.ldap2 + dn = DN(('cn', 'DNSSEC'), ('cn', api.env.host), ('cn', 'masters'), + ('cn', 'ipa'), ('cn', 'etc'), api.env.basedn) + try: + entry = conn.get_entry(dn) + except errors.NotFound: + pass + else: + ipa_config = entry.get('ipaConfigString', []) + if opendnssecinstance.KEYMASTER in ipa_config: + ipa_config.remove(opendnssecinstance.KEYMASTER) + conn.update_entry(entry) + def install_check(standalone, replica, options, hostname): global ip_addresses @@ -41,14 +107,22 @@ def install_check(standalone, replica, options, hostname): print " * Configure ipa-ods-exporter (required by DNSSEC key master)" print " * Configure OpenDNSSEC (required by DNSSEC key master)" print " * Generate DNSSEC master key (required by DNSSEC key master)" + elif options.disable_dnssec_master: + print " * Unconfigure ipa-ods-exporter" + print " * Unconfigure OpenDNSSEC" + print "" + print "No new zones will be signed without DNSSEC key master IPA server." + print "" + print ("Please copy file from %s after uninstallation. This file is needed " + "on new DNSSEC key " % paths.IPA_KASP_DB_BACKUP) + print "master server" print "" print "NOTE: DNSSEC zone signing is not enabled by default" print "" if options.dnssec_master: print "DNSSEC support is experimental!" print "" - print "Plan carefully, current version doesn't allow you to move DNSSEC" - print "key master to different server and master cannot be uninstalled" + print "Plan carefully, replacing DNSSEC key master is not recommended" print "" print "" print "To accept the default shown in brackets, press the Enter key." @@ -59,22 +133,79 @@ def install_check(standalone, replica, options, hostname): "Do you want to setup this IPA server as DNSSEC key master?", False)): sys.exit("Aborted") + elif (options.disable_dnssec_master and not options.unattended and not + ipautil.user_input( + "Do you want to disable current DNSSEC key master?", + False)): + sys.exit("Aborted") # Check bind packages are installed if not (bindinstance.check_inst(options.unattended) and dnskeysyncinstance.check_inst()): sys.exit("Aborting installation.") - if options.dnssec_master: + if options.disable_dnssec_master: + _is_master() + + if options.disable_dnssec_master or options.dnssec_master: + dnssec_zones = _find_dnssec_enabled_zones(api.Backend.ldap2) + + if options.disable_dnssec_master: + if dnssec_zones and not options.force: + raise RuntimeError( + "Cannot disable DNSSEC key master, DNSSEC signing is still " + "enabled for following zone(s): %s\n" + "Use --force option to skip this check." % + ", ".join([str(zone) for zone in dnssec_zones])) + elif options.dnssec_master: # check opendnssec packages are installed if not opendnssecinstance.check_inst(): sys.exit("Aborting installation") + if options.kasp_db_file: + dnskeysyncd = services.service('ipa-dnskeysyncd') + + if not dnskeysyncd.is_installed(): + raise RuntimeError("ipa-dnskeysyncd is not configured on this " + "server, you cannot reuse OpenDNSSEC " + "database (kasp.db file)") + + # check if replica can be the DNSSEC master + named = services.knownservices.named + ods_enforcerd = services.knownservices.ods_enforcerd + cmd = [paths.IPA_DNSKEYSYNCD_REPLICA] + environment = { + "SOFTHSM2_CONF": paths.DNSSEC_SOFTHSM2_CONF, + } + + # stop dnskeysyncd before test + dnskeysyncd_running = dnskeysyncd.is_running() + dnskeysyncd.stop() + try: + ipautil.run(cmd, env=environment, + runas=ods_enforcerd.get_user_name(), + suplementary_groups=[named.get_group_name()]) + except CalledProcessError as e: + root_logger.debug("%s", e) + raise RuntimeError("IPA server cannot be the new DNSSEC master " + "(some keys are missing)") + finally: + if dnskeysyncd_running: + dnskeysyncd.start() + elif dnssec_zones and not options.force: + # some zones have --dnssec=true, make sure a user really want to + # install new database + raise RuntimeError( + "DNSSEC is enabled for following zone(s): %s\n" + "Please use option --kasp-db to keep current OpenDNSSEC " + "database or use --force option to skip this check." % + ", ".join([str(zone) for zone in dnssec_zones])) + fstore = sysrestore.FileStore(paths.SYSRESTORE) if options.dnssec_master: ods = opendnssecinstance.OpenDNSSECInstance( - fstore, ldapi=True, autobind=AUTOBIND_ENABLED) + fstore, ldapi=True) ods.realm = api.env.realm dnssec_masters = ods.get_masters() # we can reinstall current server if it is dnssec master @@ -126,6 +257,11 @@ def install(standalone, replica, options): global dns_forwarders global reverse_zones + local_dnskeysyncd_dn = DN(('cn', 'DNSKeySync'), ('cn', api.env.host), + ('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'), + api.env.basedn) + conn = api.Backend.ldap2 + fstore = sysrestore.FileStore(paths.SYSRESTORE) conf_ntp = ntpinstance.NTPInstance(fstore).is_enabled() @@ -149,13 +285,15 @@ def install(standalone, replica, options): dnskeysyncd = dnskeysyncinstance.DNSKeySyncInstance(fstore, ldapi=True) dnskeysyncd.create_instance(api.env.host, api.env.realm) if options.dnssec_master: - ods = opendnssecinstance.OpenDNSSECInstance(fstore, ldapi=True, - autobind=AUTOBIND_ENABLED) + ods = opendnssecinstance.OpenDNSSECInstance(fstore, ldapi=True) ods_exporter = odsexporterinstance.ODSExporterInstance( - fstore, ldapi=True, autobind=AUTOBIND_ENABLED) + fstore, ldapi=True) ods_exporter.create_instance(api.env.host, api.env.realm) - ods.create_instance(api.env.host, api.env.realm) + ods.create_instance(api.env.host, api.env.realm, + kasp_db_file=options.kasp_db_file) + elif options.disable_dnssec_master: + _disable_dnssec() dnskeysyncd.start_dnskeysyncd() bind.start_named() diff --git a/ipaserver/install/odsexporterinstance.py b/ipaserver/install/odsexporterinstance.py index 5b6245bc4..c37095cfc 100644 --- a/ipaserver/install/odsexporterinstance.py +++ b/ipaserver/install/odsexporterinstance.py @@ -15,12 +15,12 @@ from ipapython.dn import DN from ipapython import sysrestore, ipautil, ipaldap from ipaplatform.paths import paths from ipaplatform import services -from ipalib import errors +from ipalib import errors, api class ODSExporterInstance(service.Service): def __init__(self, fstore=None, dm_password=None, ldapi=False, - start_tls=False, autobind=ipaldap.AUTOBIND_DISABLED): + start_tls=False, autobind=ipaldap.AUTOBIND_ENABLED): service.Service.__init__( self, "ipa-ods-exporter", service_desc="IPA OpenDNSSEC exporter daemon", @@ -150,6 +150,14 @@ class ODSExporterInstance(service.Service): def __start(self): self.start() + def remove_service(self): + dns_exporter_principal = ("ipa-ods-exporter/%s@%s" % (self.fqdn, + self.realm)) + try: + api.Command.service_del(dns_exporter_principal) + except errors.NotFound: + pass + def uninstall(self): if not self.is_configured(): return diff --git a/ipaserver/install/opendnssecinstance.py b/ipaserver/install/opendnssecinstance.py index 538475985..d68691fa3 100644 --- a/ipaserver/install/opendnssecinstance.py +++ b/ipaserver/install/opendnssecinstance.py @@ -9,6 +9,9 @@ import os import pwd import grp import stat +import shutil + +from subprocess import CalledProcessError import _ipap11helper @@ -31,7 +34,7 @@ def get_dnssec_key_masters(conn): """ assert conn is not None - dn = DN(('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'), api.env.basedn) + dn = DN(api.env.container_masters, api.env.basedn) filter_attrs = { u'cn': u'DNSSEC', @@ -62,7 +65,7 @@ def check_inst(): class OpenDNSSECInstance(service.Service): def __init__(self, fstore=None, dm_password=None, ldapi=False, - start_tls=False, autobind=ipaldap.AUTOBIND_DISABLED): + start_tls=False, autobind=ipaldap.AUTOBIND_ENABLED): service.Service.__init__( self, "ods-enforcerd", service_desc="OpenDNSSEC enforcer daemon", @@ -94,12 +97,14 @@ class OpenDNSSECInstance(service.Service): self.ldap_connect() return get_dnssec_key_masters(self.admin_conn) - def create_instance(self, fqdn, realm_name, generate_master_key=True): + def create_instance(self, fqdn, realm_name, generate_master_key=True, + kasp_db_file=None): 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) + self.kasp_db_file = kasp_db_file try: self.stop() @@ -152,6 +157,21 @@ class OpenDNSSECInstance(service.Service): except errors.DuplicateEntry: root_logger.error("DNSSEC service already exists") + # add the KEYMASTER identifier into ipaConfigString + # this is needed for the re-enabled DNSSEC master + dn = DN(('cn', 'DNSSEC'), ('cn', self.fqdn), api.env.container_masters, + api.env.basedn) + try: + entry = self.admin_conn.get_entry(dn, ['ipaConfigString']) + except errors.NotFound as e: + root_logger.error( + "DNSSEC service entry not found in the LDAP (%s)", e) + else: + config = entry.setdefault('ipaConfigString', []) + if KEYMASTER not in config: + config.append(KEYMASTER) + self.admin_conn.update_entry(entry) + def __setup_conf_files(self): if not self.fstore.has_file(paths.OPENDNSSEC_CONF_FILE): self.fstore.backup_file(paths.OPENDNSSEC_CONF_FILE) @@ -250,7 +270,7 @@ class OpenDNSSECInstance(service.Service): def __setup_dnssec(self): # run once only - if self.get_state("KASP_DB_configured"): + if self.get_state("KASP_DB_configured") and not self.kasp_db_file: root_logger.debug("Already configured, skipping step") return @@ -259,13 +279,33 @@ class OpenDNSSECInstance(service.Service): if not self.fstore.has_file(paths.OPENDNSSEC_KASP_DB): self.fstore.backup_file(paths.OPENDNSSEC_KASP_DB) - command = [ - paths.ODS_KSMUTIL, - 'setup' - ] + if self.kasp_db_file: + # copy user specified kasp.db to proper location and set proper + # privileges + shutil.copy(self.kasp_db_file, paths.OPENDNSSEC_KASP_DB) + os.chown(paths.OPENDNSSEC_KASP_DB, self.ods_uid, self.ods_gid) + os.chmod(paths.OPENDNSSEC_KASP_DB, 0660) + + # regenerate zonelist.xml + ods_enforcerd = services.knownservices.ods_enforcerd + cmd = [paths.ODS_KSMUTIL, 'zonelist', 'export'] + stdout, stderr, retcode = ipautil.run(cmd, + runas=ods_enforcerd.get_user_name()) + with open(paths.OPENDNSSEC_ZONELIST_FILE, 'w') as zonelistf: + zonelistf.write(stdout) + os.chown(paths.OPENDNSSEC_ZONELIST_FILE, + self.ods_uid, self.ods_gid) + os.chmod(paths.OPENDNSSEC_ZONELIST_FILE, 0660) - ods_enforcerd = services.knownservices.ods_enforcerd - ipautil.run(command, stdin="y", runas=ods_enforcerd.get_user_name()) + else: + # initialize new 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 @@ -286,6 +326,44 @@ class OpenDNSSECInstance(service.Service): running = self.restore_state("running") enabled = self.restore_state("enabled") + # stop DNSSEC services before backing up kasp.db + try: + self.stop() + except Exception: + pass + + ods_exporter = services.service('ipa-ods-exporter') + try: + ods_exporter.stop() + except Exception: + pass + + # remove directive from ipa-dnskeysyncd, this server is not DNSSEC + # master anymore + installutils.set_directive(paths.SYSCONFIG_IPA_DNSKEYSYNCD, + 'ISMASTER', None, + quotes=False, separator='=') + + if ipautil.file_exists(paths.OPENDNSSEC_KASP_DB): + + # force to export data + ods_enforcerd = services.knownservices.ods_enforcerd + cmd = [paths.IPA_ODS_EXPORTER, 'ipa-full-update'] + try: + ipautil.run(cmd, runas=ods_enforcerd.get_user_name()) + except CalledProcessError: + root_logger.debug("OpenDNSSEC database has not been updated") + + try: + shutil.copy(paths.OPENDNSSEC_KASP_DB, + paths.IPA_KASP_DB_BACKUP) + except IOError as e: + root_logger.error( + "Unable to backup OpenDNSSEC database: %s", e) + else: + root_logger.info("OpenDNSSEC database backed up in %s", + paths.IPA_KASP_DB_BACKUP) + for f in [paths.OPENDNSSEC_CONF_FILE, paths.OPENDNSSEC_KASP_FILE, paths.OPENDNSSEC_KASP_DB, paths.SYSCONFIG_ODS]: try: diff --git a/ipaserver/install/server/install.py b/ipaserver/install/server/install.py index 72376357b..b9bf3f34b 100644 --- a/ipaserver/install/server/install.py +++ b/ipaserver/install/server/install.py @@ -1326,6 +1326,25 @@ class ServerDNS(common.Installable, core.Group, core.Composite): description="Setup server to be DNSSEC key master", ) + disable_dnssec_master = Knob( + bool, False, + initializable=False, + description="Disable the DNSSEC master on this server", + ) + + kasp_db_file = Knob( + str, None, + initializable=False, + description="Copy OpenDNSSEC metadata from the specified file (will " + "not create a new kasp.db file)", + ) + + force = Knob( + bool, False, + initializable=False, + description="Force install", + ) + zonemgr = Knob( str, None, description=("DNS zone manager e-mail address. Defaults to " @@ -1614,7 +1633,6 @@ class Server(common.Installable, common.Interactive, core.Composite): self.ca_cert_files = self.ca.ca_cert_files self.subject = self.ca.subject self.ca_signing_algorithm = self.ca.ca_signing_algorithm - self.setup_dns = self.dns.setup_dns self.forwarders = self.dns.forwarders self.no_forwarders = self.dns.no_forwarders @@ -1622,6 +1640,9 @@ class Server(common.Installable, common.Interactive, core.Composite): self.no_reverse = self.dns.no_reverse self.no_dnssec_validation = self.dns.no_dnssec_validation self.dnssec_master = self.dns.dnssec_master + self.disable_dnssec_master = self.dns.disable_dnssec_master + self.kasp_db_file = self.dns.kasp_db_file + self.force = self.dns.force self.zonemgr = self.dns.zonemgr self.no_host_dns = self.dns.no_host_dns self.no_dns_sshfp = self.dns.no_dns_sshfp diff --git a/ipaserver/install/server/replicainstall.py b/ipaserver/install/server/replicainstall.py index b2dc3dd75..a78eeb331 100644 --- a/ipaserver/install/server/replicainstall.py +++ b/ipaserver/install/server/replicainstall.py @@ -691,6 +691,31 @@ class ReplicaDNS(common.Installable, core.Group, core.Composite): description="Disable DNSSEC validation", ) + dnssec_master = Knob( + bool, False, + initializable=False, + description="Setup server to be DNSSEC key master", + ) + + disable_dnssec_master = Knob( + bool, False, + initializable=False, + description="Disable the DNSSEC master on this server", + ) + + force = Knob( + bool, False, + initializable=False, + description="Force install", + ) + + kasp_db_file = Knob( + str, None, + initializable=False, + description="Copy OpenDNSSEC metadata from the specified file (will " + "not create a new kasp.db file)", + ) + no_host_dns = Knob( bool, False, description="Do not use DNS for hostname lookup during installation", @@ -839,7 +864,10 @@ class Replica(common.Installable, common.Interactive, core.Composite): self.reverse_zones = self.dns.reverse_zones self.no_reverse = self.dns.no_reverse self.no_dnssec_validation = self.dns.no_dnssec_validation - self.dnssec_master = False + self.dnssec_master = self.dns.dnssec_master + self.disable_dnssec_master = self.dns.disable_dnssec_master + self.kasp_db_file = self.dns.kasp_db_file + self.force = self.dns.force self.zonemgr = None self.no_host_dns = self.dns.no_host_dns self.no_dns_sshfp = self.dns.no_dns_sshfp |