This patch allows to disable DNSSEC key master on IPA server, or replace current DNSSEC key master with another IPA server.

Only for master branch.

https://fedorahosted.org/freeipa/ticket/4657

Patches attached.

--
Martin Basti

From 68ce33509c3ea12a2af9401e6856ab14a812ddd0 Mon Sep 17 00:00:00 2001
From: Martin Basti <mba...@redhat.com>
Date: Wed, 20 May 2015 17:49:08 +0200
Subject: [PATCH 2/2] DNSSEC: update message

https://fedorahosted.org/freeipa/ticket/4657
---
 install/tools/ipa-replica-manage | 1 +
 1 file changed, 1 insertion(+)

diff --git a/install/tools/ipa-replica-manage b/install/tools/ipa-replica-manage
index 4f92c0c927b3267eeb466a06d3283823c05437b0..f19c13e4af3746474e915b16c600e548c16b2f72 100755
--- a/install/tools/ipa-replica-manage
+++ b/install/tools/ipa-replica-manage
@@ -701,6 +701,7 @@ def del_master(realm, hostname, options):
             dnssec_masters = opendnssecinstance.get_dnssec_key_masters(delrepl.conn)
             if hostname in dnssec_masters:
                 print "Replica is active DNSSEC key master. Uninstall could break your DNS system."
+                print "Please disable or replace DNSSEC key master first."
                 sys.exit("Deletion aborted")
 
         # Pick CA renewal master
-- 
2.1.0

From 3fb190e9b687cd0d49c0331521d687279e1c7f1a Mon Sep 17 00:00:00 2001
From: Martin Basti <mba...@redhat.com>
Date: Wed, 13 May 2015 14:45:32 +0200
Subject: [PATCH 1/2] 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
--replace-dnssec-master=IPA_SERVER  DNSSEC master will be replaced, by
IPA_SERVER (required to rerun ipa-dns-install wit appropriate options).
--dnssec-master --kasp-db=FILE  This configure new DNSSEC master server,  kasp.db from old server is required

https://fedorahosted.org/freeipa/ticket/4657
---
 install/tools/ipa-dns-install            |  28 ++++-
 ipaplatform/base/paths.py                |   1 +
 ipaserver/install/dns.py                 | 195 +++++++++++++++++++++++++++++--
 ipaserver/install/odsexporterinstance.py |  12 +-
 ipaserver/install/opendnssecinstance.py  |  52 +++++++--
 5 files changed, 268 insertions(+), 20 deletions(-)

diff --git a/install/tools/ipa-dns-install b/install/tools/ipa-dns-install
index fd9311657e813988310db2be604ca68d26936af5..711749ccfc6851338095cb20dfc40b2ff930c747 100755
--- a/install/tools/ipa-dns-install
+++ b/install/tools/ipa-dns-install
@@ -61,6 +61,17 @@ 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 DNSSEC "
+                      "master on this server")
+    parser.add_option("--replace-dnssec-master", dest="replace_dnssec_master",
+                      type="string", metavar="IPA_DNS_SERVER_HOSTNAME",
+                      action="store", help="Replace current DNSSEC master "
+                      "with specified IPA server")
+    parser.add_option("--kasp-db", dest="kasp_db_file", type="string",
+                      metavar="FILE", action="store", help="Do not create new "
+                      "kasp.db database for DNSSEC metadata, but copy metadata "
+                      "from the specified file")
 
     options, args = parser.parse_args()
     safe_options = parser.get_safe_opts(options)
@@ -70,10 +81,17 @@ def parse_options():
     elif options.reverse_zones and options.no_reverse:
         parser.error("You cannot specify a --reverse-zone option together with --no-reverse")
 
+    if options.disable_dnssec_master and options.replace_dnssec_master:
+        parser.error("You cannot specify a --disable-dnssec-master option "
+                     "together with --replace-dnssec-master")
+
     if options.unattended:
         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.")
@@ -106,8 +124,14 @@ def main():
 
     options.setup_ca = False
 
-    dns_installer.install_check(True, False, options, hostname=api.env.host)
-    dns_installer.install(True, False, options)
+    if options.disable_dnssec_master:
+        dns_installer.disable_dnssec_master(options.unattended)
+    elif options.replace_dnssec_master:
+        dns_installer.replace_dnssec_master(options.replace_dnssec_master,
+                                            options.unattended)
+    else:
+        dns_installer.install_check(True, False, options, hostname=api.env.host)
+        dns_installer.install(True, False, options)
 
     # Restart http instance to make sure that python-dns has the right resolver
     # https://bugzilla.redhat.com/show_bug.cgi?id=800368
diff --git a/ipaplatform/base/paths.py b/ipaplatform/base/paths.py
index 9ba87523b5619188f02bdad6c23d2446a2c4b0f2..aac7a2b651d81b9653721bdc95747a2d7b1eab64 100644
--- a/ipaplatform/base/paths.py
+++ b/ipaplatform/base/paths.py
@@ -143,6 +143,7 @@ class BasePathNamespace(object):
     KRA_AGENT_PEM = "/etc/httpd/alias/kra-agent.pem"
     CACERT_P12 = "/root/cacert.p12"
     ROOT_IPA_CSR = "/root/ipa.csr"
+    ROOT_IPA_KASP_DB_BACKUP = "/root/ipa-kasp.db.backup"
     ROOT_TMP_CA_P12 = "/root/tmp-ca.p12"
     NAMED_PID = "/run/named/named.pid"
     IP = "/sbin/ip"
diff --git a/ipaserver/install/dns.py b/ipaserver/install/dns.py
index 8d9570d68c5c84116839371f3ce8e73d71da2db5..75f7002efe6fc56e6fc125f9157833112ef5a770 100644
--- a/ipaserver/install/dns.py
+++ b/ipaserver/install/dns.py
@@ -5,9 +5,11 @@
 import sys
 
 from ipalib import api
+from ipalib import errors
 from ipaplatform.paths import paths
 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 +25,152 @@ 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)
+    ods_exporter.realm = api.env.realm
+
+    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.uninstall()
+    ods_exporter.uninstall()
+
+    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 disable_dnssec_master(unattended):
+    print "=============================================================================="
+    print "This program will disable DNSSEC key master on current IPA server."
+    print ""
+    print "No new zones will be signed without DNSSEC key master IPA server."
+    print ""
+    print "This includes:"
+    print "  * Unconfigure ipa-ods-exporter"
+    print "  * Unconfigure OpenDNSSEC"
+    print ""
+
+    if (not unattended and not ipautil.user_input(
+            "Do you want to disable current DNSSEC key master?",
+            False)):
+        raise RuntimeError("Cancelled by user.")
+
+    # test if server is DNSSEC key master
+    _is_master()
+
+    dnssec_zones = _find_dnssec_enabled_zones(api.Backend.ldap2)
+    if dnssec_zones:
+        raise RuntimeError("Cannot disable DNSSEC key master, DNSSEC signing "
+                           "is still enabled for following zone(s): %s." %
+                           ", ".join(dnssec_zones))
+    _disable_dnssec()
+
+
+def replace_dnssec_master(new_master, unattended):
+    print "=============================================================================="
+    print "This program will disable DNSSEC key master on current IPA server "
+    print "and mark specified server to be new DNSSEC key master."
+    print ""
+    print "BE CAREFUL, replacing DNSSEC key masters is not recommended, "
+    print "it may break your DNSSEC signatures."
+    print ""
+    print "Replacing DNSSEC key master requires manual configuration, please "
+    print "read documentation."
+    print ("Please copy file from %s after uninstallation. This file is needed "
+           "on new DNSSEC key " % paths.ROOT_IPA_KASP_DB_BACKUP)
+    print "master server"
+    print ""
+    print "This includes:"
+    print ("  * Check the IPA server %s if can be new DNSSEC key master"
+           % new_master)
+    print "  * Unconfigure ipa-ods-exporter"
+    print "  * Unconfigure OpenDNSSEC"
+    print ""
+
+    if (not unattended and not ipautil.user_input(
+            "Do you want to replace current DNSSEC key master?",
+            False)):
+        raise RuntimeError("Cancelled by user.")
+
+    # test if server is DNSSEC key master
+    _is_master()
+
+    # detect if new DNSSEC key master has ipa-dnskeysyncd installed to be sure
+    # the DNSSEC keys are there and we will not lose them
+    dn = DN(('cn', new_master), ('cn', 'masters'), ('cn', 'ipa'),
+            ('cn', 'etc'), api.env.basedn)
+    dn_keysync = DN(('cn', 'DNSKeySync'), dn)
+
+    conn = api.Backend.ldap2
+
+    try:
+        conn.get_entry(dn)
+    except errors.NotFound:
+        raise RuntimeError("Could not find IPA server %s" % new_master)
+
+    try:
+        entry = conn.get_entry(dn_keysync)
+    except errors.NotFound:
+        raise RuntimeError("IPA server %s has not enabled or installed service"
+                           " ipa-dnskeysyncd installed." % new_master)
+    else:
+        ipa_config = entry.get('ipaConfigString', [])
+        if NEW_MASTER_MARK in ipa_config:
+            # new server was already marked to be DNSSEC master
+            pass
+        else:
+            ipa_config.append(NEW_MASTER_MARK)
+            conn.update_entry(entry)
+
+    _disable_dnssec()
+
 
 def install_check(standalone, replica, options, hostname):
     global ip_addresses
@@ -47,8 +195,7 @@ def install_check(standalone, replica, options, hostname):
         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."
@@ -69,12 +216,30 @@ def install_check(standalone, replica, options, hostname):
         # check opendnssec packages are installed
         if not opendnssecinstance.check_inst():
             sys.exit("Aborting installation")
+        if options.kasp_db_file:
+            local_dnskeysyncd_dn = DN(('cn', 'DNSKeySync'), ('cn', hostname),
+                              ('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'),
+                              api.env.basedn)
+            conn = api.Backend.ldap2
+
+            try:
+                dnskeysync_entry = conn.get_entry(local_dnskeysyncd_dn)
+            except errors.NotFound:
+                raise RuntimeError(
+                    "IPA server has not enabled or installed service "
+                    "ipa-dnskeysyncd")
+            else:
+                ipa_config = dnskeysync_entry.get('ipaConfigString', [])
+                if not NEW_MASTER_MARK in ipa_config:
+                    # server is not marked as to be new DNSSEC key master
+                    sys.exit("Server is not marked to be new DNSSEC master")
+
 
     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 +291,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 +319,24 @@ 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)
+        if options.kasp_db_file:
+            # remove new DNSSEC master mark from dnskeysyncd service
+            # entry must exists
+            dnskeysync_entry = conn.get_entry(local_dnskeysyncd_dn)
+
+            ipa_config = dnskeysync_entry.get('ipaConfigString', [])
+            if NEW_MASTER_MARK in ipa_config:
+                ipa_config.remove(NEW_MASTER_MARK)
+                dnskeysync_entry['ipaConfigString'] = ipa_config
+                conn.update_entry(dnskeysync_entry)
+
 
     dnskeysyncd.start_dnskeysyncd()
     bind.start_named()
diff --git a/ipaserver/install/odsexporterinstance.py b/ipaserver/install/odsexporterinstance.py
index 5b6245bc48803b4c5545299e4386213319ae859a..c37095cfc3bba8c6724f45d23293bdf6f4a200ee 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 5384759858463b25e92b20b65ac633b79519d5dd..fd2cfbbd664edd3bc849c6893191d36762dcc3aa 100644
--- a/ipaserver/install/opendnssecinstance.py
+++ b/ipaserver/install/opendnssecinstance.py
@@ -9,6 +9,7 @@ import os
 import pwd
 import grp
 import stat
+import shutil
 
 import _ipap11helper
 
@@ -62,7 +63,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 +95,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()
@@ -250,7 +253,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 +262,21 @@ 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)
+        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())
+            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 +297,29 @@ 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
+
+        if ipautil.file_exists(paths.OPENDNSSEC_KASP_DB):
+            try:
+                shutil.copy(paths.OPENDNSSEC_KASP_DB,
+                            paths.ROOT_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.ROOT_IPA_KASP_DB_BACKUP)
+
         for f in [paths.OPENDNSSEC_CONF_FILE, paths.OPENDNSSEC_KASP_FILE,
                   paths.OPENDNSSEC_KASP_DB, paths.SYSCONFIG_ODS]:
             try:
-- 
2.1.0

-- 
Manage your subscription for the Freeipa-devel mailing list:
https://www.redhat.com/mailman/listinfo/freeipa-devel
Contribute to FreeIPA: http://www.freeipa.org/page/Contribute/Code

Reply via email to