Hello,

this patch set fixes key rotation in DNSSEC.

You can use attached template files for OpenDNSSEC config to shorten time
intervals between key rotations.

Please let me know if you have any questions, I'm all ears!

-- 
Petr^2 Spacek
From ef4c0e6ef2a5238ef2afe54e2f912480385abadd Mon Sep 17 00:00:00 2001
From: Petr Spacek <pspa...@redhat.com>
Date: Thu, 26 Nov 2015 14:56:00 +0100
Subject: [PATCH] DNSSEC: Improve error reporting from ipa-ods-exporter

https://fedorahosted.org/freeipa/ticket/5348
---
 daemons/dnssec/ipa-ods-exporter | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/daemons/dnssec/ipa-ods-exporter b/daemons/dnssec/ipa-ods-exporter
index 3fb01cac58c60e9e6f07db7e639d5fa5c3fa2548..28c529b7ea01947794ca707e94e6eac515f14e42 100755
--- a/daemons/dnssec/ipa-ods-exporter
+++ b/daemons/dnssec/ipa-ods-exporter
@@ -32,6 +32,7 @@ import systemd.daemon
 import systemd.journal
 import sqlite3
 import time
+import traceback
 
 import ipalib
 from ipapython.dn import DN
@@ -575,7 +576,8 @@ try:
             sync_zone(log, ldap, dns_dn, zone_row['name'])
 
 except Exception as ex:
-    msg = "ipa-ods-exporter exception: %s" % ex
+    msg = "ipa-ods-exporter exception: %s" % traceback.format_exc(ex)
+    log.exception(ex)
     raise ex
 
 finally:
-- 
2.5.0

From a4e57fe1d9f7d8faee921ea481748f031b89c0cb Mon Sep 17 00:00:00 2001
From: Petr Spacek <pspa...@redhat.com>
Date: Tue, 24 Nov 2015 12:49:40 +0100
Subject: [PATCH] DNSSEC: Make sure that current state in OpenDNSSEC matches
 key state in LDAP

Previously we published timestamps of planned state changes in LDAP.
This led to situations where state transition in OpenDNSSEC was blocked
by an additional condition (or unavailability of OpenDNSSEC) but BIND
actually did the transition as planned.

Additionally key state mapping was incorrect for KSK so sometimes KSK
was not used for signing when it should.

Example (for code without this fix):
- Add a zone and let OpenDNSSEC to generate keys.
- Wait until keys are in state "published" and next state is "inactive".
- Shutdown OpenDNSSEC or break replication from DNSSEC key master.
- See that keys on DNS replicas will transition to state "inactive" even
  though it should not happen because OpenDNSSEC is not available
  (i.e. new keys may not be available).
- End result is that affected zone will not be signed anymore, even
  though it should stay signed with the old keys.

https://fedorahosted.org/freeipa/ticket/5348
---
 daemons/dnssec/ipa-ods-exporter | 105 ++++++++++++++++++++++++++++++++++++----
 1 file changed, 95 insertions(+), 10 deletions(-)

diff --git a/daemons/dnssec/ipa-ods-exporter b/daemons/dnssec/ipa-ods-exporter
index 28c529b7ea01947794ca707e94e6eac515f14e42..bdafa79b11a80bb0af8df89cc378bb052d87c5ad 100755
--- a/daemons/dnssec/ipa-ods-exporter
+++ b/daemons/dnssec/ipa-ods-exporter
@@ -57,6 +57,14 @@ ODS_DB_LOCK_PATH = "%s%s" % (paths.OPENDNSSEC_KASP_DB, '.our_lock')
 SECRETKEY_WRAPPING_MECH = 'rsaPkcsOaep'
 PRIVKEY_WRAPPING_MECH = 'aesKeyWrapPad'
 
+# Constants from OpenDNSSEC's enforcer/ksm/include/ksm/ksm.h
+KSM_STATE_PUBLISH    = 2
+KSM_STATE_READY      = 3
+KSM_STATE_ACTIVE     = 4
+KSM_STATE_RETIRE     = 5
+KSM_STATE_DEAD       = 6
+KSM_STATE_KEYPUBLISH = 10
+
 # DNSKEY flag constants
 dnskey_flag_by_value = {
     0x0001: 'SEP',
@@ -122,6 +130,77 @@ def sql2ldap_keyid(sql_keyid):
     #uri += '%'.join(sql_keyid[i:i+2] for i in range(0, len(sql_keyid), 2))
     return {"idnsSecKeyRef": uri}
 
+def ods2bind_timestamps(key_state, key_type, ods_times):
+    """Transform (timestamps and key states) from ODS to set of BIND timestamps
+    with equivalent meaning. At the same time, remove timestamps
+    for future/planned state transitions to prevent ODS & BIND
+    from desynchronizing.
+
+    OpenDNSSEC database may contain timestamps for state transitions planned
+    in the future, but timestamp itself is not sufficient information because
+    there could be some additional condition which is guaded by OpenDNSSEC
+    itself.
+
+    BIND works directly with timestamps without any additional conditions.
+    This difference causes problem when state transition planned in OpenDNSSEC
+    does not happen as originally planned for some reason.
+
+    At the same time, this difference causes problem when OpenDNSSEC on DNSSEC
+    key master and BIND instances on replicas are not synchronized. This
+    happens when DNSSEC key master is down, or a replication is down. Even
+    a temporary desynchronization could cause DNSSEC validation failures
+    which could have huge impact.
+
+    To prevent this problem, this function removes all timestamps corresponding
+    to future state transitions. As a result, BIND will not do state transition
+    until it happens in OpenDNSSEC first and until the change is replicated.
+
+    Also, timestamp mapping depends on key type and is not 1:1.
+    For detailed description of the mapping please see
+    https://fedorahosted.org/bind-dyndb-ldap/wiki/BIND9/Design/DNSSEC/OpenDNSSEC2BINDKeyStates
+    """
+    bind_times = {}
+    # idnsSecKeyCreated is equivalent to SQL column 'created'
+    bind_times['idnsSecKeyCreated'] = ods_times['idnsSecKeyCreated']
+
+    # set of key states where publishing in DNS zone is desired is taken from
+    # opendnssec/enforcer/ksm/ksm_request.c:KsmRequestIssueKeys()
+    # TODO: support for RFC 5011, requires OpenDNSSEC v1.4.8+
+    if ('idnsSecKeyPublish' in ods_times and
+        key_state in {KSM_STATE_PUBLISH, KSM_STATE_READY, KSM_STATE_ACTIVE,
+                      KSM_STATE_RETIRE, KSM_STATE_KEYPUBLISH}):
+        bind_times['idnsSecKeyPublish'] = ods_times['idnsSecKeyPublish']
+
+    # ZSK and KSK handling differs in enforcerd, see
+    # opendnssec/enforcer/enforcerd/enforcer.c:commKeyConfig()
+    if key_type == 'ZSK':
+        # idnsSecKeyActivate cannot be set before the key reaches ACTIVE state
+        if ('idnsSecKeyActivate' in ods_times and
+            key_state in {KSM_STATE_ACTIVE, KSM_STATE_RETIRE, KSM_STATE_DEAD}):
+                bind_times['idnsSecKeyActivate'] = ods_times['idnsSecKeyActivate']
+
+        # idnsSecKeyInactive cannot be set before the key reaches RETIRE state
+        if ('idnsSecKeyInactive' in ods_times and
+            key_state in {KSM_STATE_RETIRE, KSM_STATE_DEAD}):
+                bind_times['idnsSecKeyInactive'] = ods_times['idnsSecKeyInactive']
+
+    elif key_type == 'KSK':
+        # KSK is special: it is used for signing as long as it is in zone
+        if ('idnsSecKeyPublish' in ods_times and
+            key_state in {KSM_STATE_PUBLISH, KSM_STATE_READY, KSM_STATE_ACTIVE,
+                          KSM_STATE_RETIRE, KSM_STATE_KEYPUBLISH}):
+            bind_times['idnsSecKeyActivate'] = ods_times['idnsSecKeyPublish']
+        # idnsSecKeyInactive is ignored for KSK on purpose
+
+    else:
+        assert False, "unsupported key type %s" % key_type
+
+    # idnsSecKeyDelete is relevant only in DEAD state
+    if 'idnsSecKeyDelete' in ods_times and key_state == KSM_STATE_DEAD:
+        bind_times['idnsSecKeyDelete'] = ods_times['idnsSecKeyDelete']
+
+    return bind_times
+
 class ods_db_lock(object):
     def __enter__(self):
         self.f = open(ODS_DB_LOCK_PATH, 'w')
@@ -172,25 +251,31 @@ def get_ods_keys(zone_name):
     assert len(rows) == 1, "exactly one DNS zone should exist in ODS DB"
     zone_id = rows[0][0]
 
-    # get all keys for given zone ID
-    cur = db.execute("SELECT kp.HSMkey_id, kp.generate, kp.algorithm, dnsk.publish, dnsk.active, dnsk.retire, dnsk.dead, dnsk.keytype "
-             "FROM keypairs AS kp JOIN dnsseckeys AS dnsk ON kp.id = dnsk.keypair_id "
-             "WHERE dnsk.zone_id = ?", (zone_id,))
+    # get relevant keys for given zone ID:
+    # ignore keys which were generated but not used yet
+    # key state check is using constants from
+    # OpenDNSSEC's enforcer/ksm/include/ksm/ksm.h
+    # WARNING! OpenDNSSEC version 1 and 2 are using different constants!
+    cur = db.execute("SELECT kp.HSMkey_id, kp.generate, kp.algorithm, "
+                     "dnsk.publish, dnsk.active, dnsk.retire, dnsk.dead, "
+                     "dnsk.keytype, dnsk.state "
+                     "FROM keypairs AS kp "
+                     "JOIN dnsseckeys AS dnsk ON kp.id = dnsk.keypair_id "
+                     "WHERE dnsk.zone_id = ?", (zone_id,))
     keys = {}
     for row in cur:
-        key_data = sql2datetimes(row)
-        if 'idnsSecKeyDelete' in key_data \
-            and key_data['idnsSecKeyDelete'] > datetime.now():
-                continue  # ignore deleted keys
-
-        key_data.update(sql2ldap_flags(row['keytype']))
+        key_data = sql2ldap_flags(row['keytype'])
         assert key_data.get('idnsSecKeyZONE', None) == 'TRUE', \
                 'unexpected key type 0x%x' % row['keytype']
         if key_data.get('idnsSecKeySEP', 'FALSE') == 'TRUE':
             key_type = 'KSK'
         else:
             key_type = 'ZSK'
 
+        # transform key state to timestamps for BIND with equivalent semantics
+        ods_times = sql2datetimes(row)
+        key_data.update(ods2bind_timestamps(row['state'], key_type, ods_times))
+
         key_data.update(sql2ldap_algorithm(row['algorithm']))
         key_id = "%s-%s-%s" % (key_type,
                                datetime2ldap(key_data['idnsSecKeyCreated']),
-- 
2.5.0

From b3b30032249309afd53b9ce60cc92ab0efb91dfd Mon Sep 17 00:00:00 2001
From: Petr Spacek <pspa...@redhat.com>
Date: Thu, 26 Nov 2015 15:19:03 +0100
Subject: [PATCH] DNSSEC: Make sure that current key state in LDAP matches key
 state in BIND

We have to explicitly specify "none" value to prevent dnssec-keyfromlabel
utility from using current time for keys without "publish" and "activate"
timestamps.

Previously this lead to situation where key was in (intermediate) state
"generated" in OpenDNSSEC but BIND started to use this key for signing.

https://fedorahosted.org/freeipa/ticket/5348
---
 ipapython/dnssec/bindmgr.py | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/ipapython/dnssec/bindmgr.py b/ipapython/dnssec/bindmgr.py
index a0a9f2eb28ddb9dce8d1d71ab10afe6a0e0146ec..2112db7ffe9fac6ef8b5f26224d05864c9290146 100644
--- a/ipapython/dnssec/bindmgr.py
+++ b/ipapython/dnssec/bindmgr.py
@@ -58,17 +58,21 @@ class BINDMgr(object):
         return dt.strftime(time_bindfmt)
 
     def dates2params(self, ldap_attrs):
+        """Convert LDAP timestamps to list of parameters suitable
+        for dnssec-keyfromlabel utility"""
         attr2param = {'idnsseckeypublish': '-P',
                 'idnsseckeyactivate': '-A',
                 'idnsseckeyinactive': '-I',
                 'idnsseckeydelete': '-D'}
 
         params = []
         for attr, param in attr2param.items():
+            params.append(param)
             if attr in ldap_attrs:
-                params.append(param)
                 assert len(ldap_attrs[attr]) == 1, 'Timestamp %s is expected to be single-valued' % attr
                 params.append(self.time_ldap2bindfmt(ldap_attrs[attr][0]))
+            else:
+                params.append('none')
 
         return params
 
-- 
2.5.0

From 1be2bba4affb8a454170cc428374243a605b2691 Mon Sep 17 00:00:00 2001
From: Petr Spacek <pspa...@redhat.com>
Date: Wed, 2 Dec 2015 12:58:23 +0100
Subject: [PATCH] DNSSEC: remove obsolete TODO note

https://fedorahosted.org/freeipa/ticket/5348
---
 ipapython/dnssec/ldapkeydb.py | 1 -
 1 file changed, 1 deletion(-)

diff --git a/ipapython/dnssec/ldapkeydb.py b/ipapython/dnssec/ldapkeydb.py
index 8069993758b534f86311e800115e797fb28e7f70..c78424617ef8cedf8c591e5f0da0d27138c19960 100644
--- a/ipapython/dnssec/ldapkeydb.py
+++ b/ipapython/dnssec/ldapkeydb.py
@@ -182,7 +182,6 @@ class MasterKey(Key):
         # TODO: replace this with 'autogenerate' to prevent collisions
         uuid_rdn = DN('ipk11UniqueId=%s' % uuid.uuid1())
         entry_dn = DN(uuid_rdn, self.ldapkeydb.base_dn)
-        # TODO: add ipaWrappingMech attribute
         entry = self.ldap.make_entry(entry_dn,
                    objectClass=['ipaSecretKeyObject', 'ipk11Object'],
                    ipaSecretKey=data,
-- 
2.5.0

From ad3c908d70aa211a0b54de7de90bfcb7fc58372f Mon Sep 17 00:00:00 2001
From: Petr Spacek <pspa...@redhat.com>
Date: Tue, 15 Dec 2015 14:13:23 +0100
Subject: [PATCH] DNSSEC: add debug mode to ldapkeydb.py

ldapkeydb.py can be executed directly now. In that case it will print
out key metadata as obtained using IPA LDAP API.

Kerberos credential cache has to be filled with principal posessing
appropriate access rights before the script is execured.

https://fedorahosted.org/freeipa/ticket/5348
---
 ipapython/dnssec/ldapkeydb.py | 50 ++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 49 insertions(+), 1 deletion(-)

diff --git a/ipapython/dnssec/ldapkeydb.py b/ipapython/dnssec/ldapkeydb.py
index c78424617ef8cedf8c591e5f0da0d27138c19960..fa6869c84a9b799120e7292b552f6553d3b51dcb 100644
--- a/ipapython/dnssec/ldapkeydb.py
+++ b/ipapython/dnssec/ldapkeydb.py
@@ -4,6 +4,8 @@
 
 from binascii import hexlify
 import collections
+import logging
+from pprint import pprint
 import sys
 import time
 
@@ -137,7 +139,14 @@ class Key(collections.MutableMapping):
         return len(self.entry)
 
     def __str__(self):
-        return str(self.entry)
+        return repr(self)
+
+    def __repr__(self):
+        sanitized = dict(self.entry)
+        for attr in ['ipaPrivateKey', 'ipaPublicKey', 'ipk11publickeyinfo']:
+            if attr in sanitized:
+                del sanitized[attr]
+        return repr(sanitized)
 
     def _cleanup_key(self):
         """remove default values from LDAP entry"""
@@ -348,3 +357,42 @@ class LdapKeyDB(AbstractHSM):
                 '(&(objectClass=ipk11PrivateKey)(objectClass=ipaPrivateKeyObject)(objectClass=ipk11PublicKey)(objectClass=ipaPublicKeyObject))'))
 
         return self.cache_zone_keypairs
+
+if __name__ == '__main__':
+    log = logging.getLogger('root')
+
+    # IPA framework initialization
+    ipalib.api.bootstrap(in_server=True, log=None)  # no logging to file
+    ipalib.api.finalize()
+
+    # LDAP initialization
+    dns_dn = DN(ipalib.api.env.container_dns, ipalib.api.env.basedn)
+    ldap = ipaldap.LDAPClient(ipalib.api.env.ldap_uri)
+    log.debug('Connecting to LDAP')
+    # GSSAPI will be used, used has to be kinited already
+    ldap.gssapi_bind()
+    log.debug('Connected')
+
+    ldapkeydb = LdapKeyDB(log, ldap, DN(('cn', 'keys'), ('cn', 'sec'),
+                          ipalib.api.env.container_dns,
+                          ipalib.api.env.basedn))
+
+    print('replica public keys: CKA_WRAP = TRUE')
+    print('====================================')
+    for pubkey_id, pubkey in ldapkeydb.replica_pubkeys_wrap.items():
+        print(hexlify(pubkey_id))
+        pprint(pubkey)
+
+    print('')
+    print('master keys')
+    print('===========')
+    for mkey_id, mkey in ldapkeydb.master_keys.items():
+        print(hexlify(mkey_id))
+        pprint(mkey)
+
+    print('')
+    print('zone key pairs')
+    print('==============')
+    for key_id, key in ldapkeydb.zone_keypairs.items():
+        print(hexlify(key_id))
+        pprint(key)
-- 
2.5.0

From dc417e05d6f318a4511bdafef5e166974662aed8 Mon Sep 17 00:00:00 2001
From: Petr Spacek <pspa...@redhat.com>
Date: Tue, 15 Dec 2015 14:16:52 +0100
Subject: [PATCH] DNSSEC: logging improvements in ldapkeydb.py

https://fedorahosted.org/freeipa/ticket/5348
---
 daemons/dnssec/ipa-ods-exporter | 17 +++++++++++------
 1 file changed, 11 insertions(+), 6 deletions(-)

diff --git a/daemons/dnssec/ipa-ods-exporter b/daemons/dnssec/ipa-ods-exporter
index bdafa79b11a80bb0af8df89cc378bb052d87c5ad..dd1c16355a19033d1b11157e8f0d1349a2d15cf5 100755
--- a/daemons/dnssec/ipa-ods-exporter
+++ b/daemons/dnssec/ipa-ods-exporter
@@ -491,6 +491,11 @@ def cmd2ods_zone_name(cmd):
     return zone_name
 
 def sync_zone(log, ldap, dns_dn, zone_name):
+    """synchronize metadata about zone keys for single DNS zone
+    
+    Key material has to be synchronized elsewhere.
+    Keep in mind that keys could be shared among multiple zones!"""
+    log.getChild("%s.%s" % (__name__, zone_name))
     log.debug('synchronizing zone "%s"', zone_name)
     ods_keys = get_ods_keys(zone_name)
     ods_keys_id = set(ods_keys.keys())
@@ -523,30 +528,30 @@ def sync_zone(log, ldap, dns_dn, zone_name):
     ldap_keys_id = set(ldap_keys.keys())
 
     new_keys_id = ods_keys_id - ldap_keys_id
-    log.info('new keys from ODS: %s', new_keys_id)
+    log.info('new key metadata from ODS: %s', new_keys_id)
     for key_id in new_keys_id:
         cn = "cn=%s" % key_id
         key_dn = DN(cn, keys_dn)
-        log.debug('adding key "%s" to LDAP', key_dn)
+        log.debug('adding key metadata "%s" to LDAP', key_dn)
         ldap_key = ldap.make_entry(key_dn,
                                    objectClass=['idnsSecKey'],
                                    **ods_keys[key_id])
         ldap.add_entry(ldap_key)
 
     deleted_keys_id = ldap_keys_id - ods_keys_id
-    log.info('deleted keys in LDAP: %s', deleted_keys_id)
+    log.info('deleted key metadata in LDAP: %s', deleted_keys_id)
     for key_id in deleted_keys_id:
         cn = "cn=%s" % key_id
         key_dn = DN(cn, keys_dn)
-        log.debug('deleting key "%s" from LDAP', key_dn)
+        log.debug('deleting key metadata "%s" from LDAP', key_dn)
         ldap.delete_entry(key_dn)
 
     update_keys_id = ldap_keys_id.intersection(ods_keys_id)
-    log.info('keys in LDAP & ODS: %s', update_keys_id)
+    log.info('key metadata in LDAP & ODS: %s', update_keys_id)
     for key_id in update_keys_id:
         ldap_key = ldap_keys[key_id]
         ods_key = ods_keys[key_id]
-        log.debug('updating key "%s" in LDAP', ldap_key.dn)
+        log.debug('updating key metadata "%s" in LDAP', ldap_key.dn)
         ldap_key.update(ods_key)
         try:
             ldap.update_entry(ldap_key)
-- 
2.5.0

From 196ac0bd87c0e15afff686c4af241839cecf4673 Mon Sep 17 00:00:00 2001
From: Petr Spacek <pspa...@redhat.com>
Date: Tue, 15 Dec 2015 15:22:45 +0100
Subject: [PATCH] DNSSEC: remove keys purged by OpenDNSSEC from master HSM from
 LDAP

Key purging has to be only only after key metadata purging so
ipa-dnskeysyncd on replices does not fail while dereferencing
non-existing keys.

https://fedorahosted.org/freeipa/ticket/5334
---
 daemons/dnssec/ipa-ods-exporter | 38 ++++++++++++++++++++++--
 ipapython/dnssec/ldapkeydb.py   | 66 ++++++++++++++++++++++++++++++++++-------
 2 files changed, 90 insertions(+), 14 deletions(-)

diff --git a/daemons/dnssec/ipa-ods-exporter b/daemons/dnssec/ipa-ods-exporter
index dd1c16355a19033d1b11157e8f0d1349a2d15cf5..79953a180fdde574bd3b8e25a8f400a40eb1e5ff 100755
--- a/daemons/dnssec/ipa-ods-exporter
+++ b/daemons/dnssec/ipa-ods-exporter
@@ -387,7 +387,10 @@ def master2ldap_master_keys_sync(log, ldapkeydb, localhsm):
     ldapkeydb.flush()
 
 def master2ldap_zone_keys_sync(log, ldapkeydb, localhsm):
-    # synchroniza zone keys
+    """add and update zone key material from local HSM to LDAP
+
+    No key material will be removed, only new keys will be added or updated.
+    Key removal is hanled by master2ldap_zone_keys_purge()."""
     log = log.getChild('master2ldap_zone_keys')
     keypairs_ldap = ldapkeydb.zone_keypairs
     log.debug("zone keys in LDAP: %s", hex_set(keypairs_ldap))
@@ -420,6 +423,30 @@ def master2ldap_zone_keys_sync(log, ldapkeydb, localhsm):
     sync_set_metadata_2ldap(log, privkeys_local, keypairs_ldap)
     ldapkeydb.flush()
 
+def master2ldap_zone_keys_purge(log, ldapkeydb, localhsm):
+    """purge removed key material from LDAP (but not metadata)
+
+    Keys which are present in LDAP but not in local HSM will be removed.
+    Key metadata must be removed first so references to removed key material
+    are removed before actually removing the keys."""
+    keypairs_ldap = ldapkeydb.zone_keypairs
+    log.debug("zone keys in LDAP: %s", hex_set(keypairs_ldap))
+
+    pubkeys_local = localhsm.zone_pubkeys
+    privkeys_local = localhsm.zone_privkeys
+    log.debug("zone keys in local HSM: %s", hex_set(privkeys_local))
+    assert set(pubkeys_local) == set(privkeys_local), \
+            "IDs of private and public keys for DNS zones in local HSM does " \
+            "not match to key pairs: %s vs. %s" % \
+            (hex_set(pubkeys_local), hex_set(privkeys_local))
+
+    deleted_key_ids = set(keypairs_ldap) - set(pubkeys_local)
+    log.debug("zone keys deleted from local HSM but present in LDAP: %s",
+            hex_set(deleted_key_ids))
+    for zkey_id in deleted_key_ids:
+        ldapkeydb.schedule_key_deletion(zkey_id)
+    ldapkeydb.flush()
+
 
 def hex_set(s):
     out = set()
@@ -599,7 +626,7 @@ ldap.gssapi_bind()
 log.debug('Connected')
 
 
-### DNSSEC master: key synchronization
+### DNSSEC master: key material upload & synchronization (but not deletion)
 ldapkeydb = LdapKeyDB(log, ldap, DN(('cn', 'keys'), ('cn', 'sec'),
                                     ipalib.api.env.container_dns,
                                     ipalib.api.env.basedn))
@@ -611,7 +638,7 @@ master2ldap_master_keys_sync(log, ldapkeydb, localhsm)
 master2ldap_zone_keys_sync(log, ldapkeydb, localhsm)
 
 
-### DNSSEC master: DNSSEC key metadata upload
+### DNSSEC master: DNSSEC key metadata upload & synchronization & deletion
 # command receive is delayed so the command will stay in socket queue until
 # the problem with LDAP server or HSM is fixed
 try:
@@ -665,6 +692,11 @@ try:
         for zone_row in db.execute("SELECT name FROM zones"):
             sync_zone(log, ldap, dns_dn, zone_row['name'])
 
+    ### DNSSEC master: DNSSEC key material purging
+    # references to old key material were removed above in sync_zone()
+    # so now we can purge old key material from LDAP
+    master2ldap_zone_keys_purge(log, ldapkeydb, localhsm)
+
 except Exception as ex:
     msg = "ipa-ods-exporter exception: %s" % traceback.format_exc(ex)
     log.exception(ex)
diff --git a/ipapython/dnssec/ldapkeydb.py b/ipapython/dnssec/ldapkeydb.py
index fa6869c84a9b799120e7292b552f6553d3b51dcb..aa3630e01032ac2ba3a65473fbb49d9cf494c388 100644
--- a/ipapython/dnssec/ldapkeydb.py
+++ b/ipapython/dnssec/ldapkeydb.py
@@ -105,6 +105,28 @@ def get_default_attrs(object_classes):
         result.update(defaults[cls])
     return result
 
+
+class KeyDeleter(object):
+    """placeholder for to-be-deleted object in cache"""
+    def __init__(self, entry, ldap):
+        self.entry = entry
+        self.ldap = ldap
+        self.log = ldap.log.getChild(__name__)
+
+    def _update_key(self):
+        """remove key metadata object from LDAP
+
+        After calling this, the python object is no longer valid and has to
+        be deleted.
+        """
+        self.log.debug('deleting key id 0x%s DN %s from LDAP',
+                       hexlify(self.entry.single_value['ipk11id']),
+                       self.entry.dn)
+        self.ldap.delete_entry(self.entry)
+        self.entry = None
+        self.ldap = None
+
+
 class Key(collections.MutableMapping):
     """abstraction to hide LDAP entry weirdnesses:
         - non-normalized attribute names
@@ -156,6 +178,23 @@ class Key(collections.MutableMapping):
             if self.get(attr, empty) == default_attrs[attr]:
                 del self[attr]
 
+    def _update_key(self):
+        """remove default values from LDAP entry and write back changes"""
+        self._cleanup_key()
+
+        try:
+            self.ldap.update_entry(self.entry)
+        except ipalib.errors.EmptyModlist:
+            pass
+
+    def prepare_deletion(self):
+        """create placeholder for a deleted key
+
+        This placeholder needs to be put into key cache. Object will be
+        actually deleted when _update_key() on the new object is called."""
+        return KeyDeleter(self.entry, self.ldap)
+
+
 class ReplicaKey(Key):
     # TODO: object class assert
     def __init__(self, entry, ldap, ldapkeydb):
@@ -242,21 +281,26 @@ class LdapKeyDB(AbstractHSM):
         self._update_keys()
         return keys
 
-    def _update_key(self, key):
-        """remove default values from LDAP entry and write back changes"""
-        key._cleanup_key()
-
-        try:
-            self.ldap.update_entry(key.entry)
-        except ipalib.errors.EmptyModlist:
-            pass
-
     def _update_keys(self):
         for cache in [self.cache_masterkeys, self.cache_replica_pubkeys_wrap,
-                self.cache_zone_keypairs]:
+                      self.cache_zone_keypairs]:
             if cache:
                 for key in cache.values():
-                    self._update_key(key)
+                    key._update_key()
+
+    def schedule_key_deletion(self, keyid):
+        """schedule key deletion from LDAP and from in-memory cache
+
+        Keys will be actually deleted when flush() is called."""
+        matched_already = False
+        for keyset in [self.zone_keypairs, self.replica_pubkeys_wrap,
+                       self.master_keys]:
+            if keyid in keyset:
+                assert not matched_already, \
+                    "key %s is in more than one keyset" % hexlify(keyid)
+                matched_already = True
+                # this replaces object in LdapKeyDB cache with placeholder
+                keyset[keyid] = keyset[keyid].prepare_deletion()
 
     def flush(self):
         """write back content of caches to LDAP"""
-- 
2.5.0

From 62024ed8cd21e2688952037f22a4c59b7a2ffaa2 Mon Sep 17 00:00:00 2001
From: Petr Spacek <pspa...@redhat.com>
Date: Sun, 20 Dec 2015 18:36:48 +0100
Subject: [PATCH] DNSSEC: ipa-dnskeysyncd: Skip zones with old DNSSEC metadata
 in LDAP

This filtering is useful in cases where LDAP contains DNS zones which
have old metadata objects and DNSSEC disabled. Such zones must be
ignored to prevent errors while calling dnssec-keyfromlabel or rndc.

https://fedorahosted.org/freeipa/ticket/5348
---
 ipapython/dnssec/bindmgr.py   | 16 +++++++++++++---
 ipapython/dnssec/keysyncer.py | 24 ++++++++++++++++++------
 2 files changed, 31 insertions(+), 9 deletions(-)

diff --git a/ipapython/dnssec/bindmgr.py b/ipapython/dnssec/bindmgr.py
index 2112db7ffe9fac6ef8b5f26224d05864c9290146..e9e5f89ba5ec8d8dea92c99faceabe04c8aee97d 100644
--- a/ipapython/dnssec/bindmgr.py
+++ b/ipapython/dnssec/bindmgr.py
@@ -192,10 +192,20 @@ class BINDMgr(object):
 
         self.notify_zone(zone)
 
-    def sync(self):
-        """Synchronize list of zones in LDAP with BIND."""
+    def sync(self, dnssec_zones):
+        """Synchronize list of zones in LDAP with BIND.
+
+        dnssec_zones lists zones which should be processed. All other zones
+        will be ignored even though they were modified using ldap_event().
+
+        This filter is useful in cases where LDAP contains DNS zones which
+        have old metadata objects and DNSSEC disabled. Such zones must be
+        ignored to prevent errors while calling dnssec-keyfromlabel or rndc.
+        """
         self.log.debug('Key metadata in LDAP: %s' % self.ldap_keys)
-        for zone in self.modified_zones:
+        self.log.debug('Zones modified but skipped during bindmgr.sync: %s',
+                       self.modified_zones - dnssec_zones)
+        for zone in self.modified_zones.intersection(dnssec_zones):
             self.sync_zone(zone)
 
         self.modified_zones = set()
diff --git a/ipapython/dnssec/keysyncer.py b/ipapython/dnssec/keysyncer.py
index 426dd940a8a613319f33cbd11710779428f7e51c..e0c6df3a6215543f923a7723e47d7c324e9c4b64 100644
--- a/ipapython/dnssec/keysyncer.py
+++ b/ipapython/dnssec/keysyncer.py
@@ -6,6 +6,8 @@ import logging
 import ldap.dn
 import os
 
+import dns.name
+
 from ipaplatform.paths import paths
 from ipapython import ipautil
 
@@ -33,6 +35,7 @@ class KeySyncer(SyncReplConsumer):
 
         self.bindmgr = BINDMgr(self.api)
         self.init_done = False
+        self.dnssec_zones = set()
         SyncReplConsumer.__init__(self, *args, **kwargs)
 
     def _get_objclass(self, attrs):
@@ -112,40 +115,49 @@ class KeySyncer(SyncReplConsumer):
         self.ods_sync()
         self.hsm_replica_sync()
         self.hsm_master_sync()
-        self.bindmgr.sync()
+        self.bindmgr.sync(self.dnssec_zones)
 
     # idnsSecKey wrapper
     # Assumption: metadata points to the same key blob all the time,
     # i.e. it is not necessary to re-download blobs because of change in DNSSEC
     # metadata - DNSSEC flags or timestamps.
     def key_meta_add(self, uuid, dn, newattrs):
         self.hsm_replica_sync()
         self.bindmgr.ldap_event('add', uuid, newattrs)
-        self.bindmgr_sync()
+        self.bindmgr_sync(self.dnssec_zones)
 
     def key_meta_del(self, uuid, dn, oldattrs):
         self.bindmgr.ldap_event('del', uuid, oldattrs)
-        self.bindmgr_sync()
+        self.bindmgr_sync(self.dnssec_zones)
         self.hsm_replica_sync()
 
     def key_metadata_sync(self, uuid, dn, oldattrs, newattrs):
         self.bindmgr.ldap_event('mod', uuid, newattrs)
-        self.bindmgr_sync()
+        self.bindmgr_sync(self.dnssec_zones)
 
-    def bindmgr_sync(self):
+    def bindmgr_sync(self, dnssec_zones):
         if self.init_done:
-            self.bindmgr.sync()
+            self.bindmgr.sync(dnssec_zones)
 
     # idnsZone wrapper
     def zone_add(self, uuid, dn, newattrs):
+        zone = dns.name.from_text(newattrs['idnsname'][0])
+        if self.__is_dnssec_enabled(newattrs):
+            self.dnssec_zones.add(zone)
+        else:
+            self.dnssec_zones.discard(zone)
+
         if not self.ismaster:
             return
 
         if self.__is_dnssec_enabled(newattrs):
             self.odsmgr.ldap_event('add', uuid, newattrs)
         self.ods_sync()
 
     def zone_del(self, uuid, dn, oldattrs):
+        zone = dns.name.from_text(oldattrs['idnsname'][0])
+        self.dnssec_zones.discard(zone)
+
         if not self.ismaster:
             return
 
-- 
2.5.0

From 9e0a609b886217572bcfc7a60270c61268e50934 Mon Sep 17 00:00:00 2001
From: Petr Spacek <pspa...@redhat.com>
Date: Sun, 20 Dec 2015 19:19:28 +0100
Subject: [PATCH] DNSSEC: ipa-ods-exporter: add ldap-cleanup command

Command "ldap-cleanup <zone name>" will remove all key metadata from
LDAP. This can be used manually in sequence like:
ldap-cleanup <zone name>
update <zone name>
to delete all key metadata from LDAP and re-export them from OpenDNSSEC.

ldap-cleanup command should be called when disabling DNSSEC on a DNS
zone to remove stale key metadata from LDAP.

https://fedorahosted.org/freeipa/ticket/5348
---
 daemons/dnssec/ipa-ods-exporter | 60 ++++++++++++++++++++++++++++++++---------
 1 file changed, 48 insertions(+), 12 deletions(-)

diff --git a/daemons/dnssec/ipa-ods-exporter b/daemons/dnssec/ipa-ods-exporter
index 79953a180fdde574bd3b8e25a8f400a40eb1e5ff..d654982f558c96f854c7ff28a1b64189ccbfd70c 100755
--- a/daemons/dnssec/ipa-ods-exporter
+++ b/daemons/dnssec/ipa-ods-exporter
@@ -227,7 +227,9 @@ def get_ldap_zone(ldap, dns_base, name):
         except ipalib.errors.NotFound:
             continue
 
-    assert ldap_zone is not None, 'DNS zone "%s" should exist in LDAP' % name
+    if ldap_zone is None:
+        raise ipalib.errors.NotFound(
+            reason='DNS zone "%s" not found in LDAP' % name)
 
     return ldap_zone
 
@@ -482,25 +484,37 @@ def parse_command(cmd):
     if cmd == 'ipa-hsm-update':
         return (0,
                 'HSM synchronization finished, skipping zone synchronization.',
-                None)
+                None,
+                cmd)
 
     elif cmd == 'ipa-full-update':
         return (None,
                 'Synchronization of all zones was finished.',
-                None)
+                None,
+                cmd)
 
-    elif not cmd.startswith('update '):
+    elif cmd.startswith('ldap-cleanup '):
+        zone_name = cmd2ods_zone_name(cmd)
+        return (None,
+                'Zone "%s" metadata will be removed from LDAP.\n' % zone_name,
+                zone_name,
+                'ldap-cleanup')
+
+    elif cmd.startswith('update '):
+        zone_name = cmd2ods_zone_name(cmd)
+        return (None,
+                'Zone "%s" metadata will be updated in LDAP.\n' % zone_name,
+                zone_name,
+                'update')
+
+    else:
         return (0,
                 'Command "%s" is not supported by IPA; '
                 'HSM synchronization was finished and the command '
                 'will be ignored.' % cmd,
+                None,
                 None)
 
-    else:
-        zone_name = cmd2ods_zone_name(cmd)
-        return (None,
-                'Zone was "%s" updated.\n' % zone_name,
-                zone_name)
 
 def send_systemd_reply(conn, reply):
         # Reply & close connection early.
@@ -511,7 +525,7 @@ def send_systemd_reply(conn, reply):
 
 def cmd2ods_zone_name(cmd):
     # ODS stores zone name without trailing period
-    zone_name = cmd[7:].strip()
+    zone_name = cmd.split(' ', 1)[1].strip()
     if len(zone_name) > 1 and zone_name[-1] == '.':
         zone_name = zone_name[:-1]
 
@@ -585,6 +599,25 @@ def sync_zone(log, ldap, dns_dn, zone_name):
         except ipalib.errors.EmptyModlist:
             continue
 
+def cleanup_ldap_zone(log, ldap, dns_dn, zone_name):
+    """delete all key metadata about zone keys for single DNS zone
+
+    Key material has to be synchronized elsewhere.
+    Keep in mind that keys could be shared among multiple zones!"""
+    log = log.getChild("%s.%s" % (__name__, zone_name))
+    log.debug('cleaning up key metadata from zone "%s"', zone_name)
+
+    try:
+        ldap_zone = get_ldap_zone(ldap, dns_dn, zone_name)
+        ldap_keys = get_ldap_keys(ldap, ldap_zone.dn)
+    except ipalib.errors.NotFound as ex:
+        # zone or cn=keys container does not exist, we are done
+        log.debug(str(ex))
+        return
+
+    for ldap_key in ldap_keys:
+        log.debug('deleting key metadata "%s"', ldap_key.dn)
+        ldap.delete_entry(ldap_key)
 
 log = logging.getLogger('root')
 # this service is usually socket-activated
@@ -656,7 +689,7 @@ except KeyError as e:
     conn = None
     cmd = sys.argv[1]
 
-exitcode, msg, zone_name = parse_command(cmd)
+exitcode, msg, zone_name, cmd = parse_command(cmd)
 
 if exitcode is not None:
     if conn:
@@ -686,7 +719,10 @@ try:
 
     if zone_name is not None:
         # only one zone should be processed
-        sync_zone(log, ldap, dns_dn, zone_name)
+        if cmd == 'update':
+            sync_zone(log, ldap, dns_dn, zone_name)
+        elif cmd == 'ldap-cleanup':
+            cleanup_ldap_zone(log, ldap, dns_dn, zone_name)
     else:
         # process all zones
         for zone_row in db.execute("SELECT name FROM zones"):
-- 
2.5.0

From da394b0837f403c74a283f34bd5388c2e34b0961 Mon Sep 17 00:00:00 2001
From: Petr Spacek <pspa...@redhat.com>
Date: Sun, 20 Dec 2015 19:35:55 +0100
Subject: [PATCH] DNSSEC: ipa-dnskeysyncd: call ods-signer ldap-cleanup on zone
 removal

Command "ldap-cleanup <zone name>" is called to remove all key metadata from
LDAP. This command is now called when disabling DNSSEC on a DNS zone. The stale
metadata were causing problems when re-enabling DNSSEC on the same zone.

https://fedorahosted.org/freeipa/ticket/5348
---
 ipapython/dnssec/odsmgr.py | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/ipapython/dnssec/odsmgr.py b/ipapython/dnssec/odsmgr.py
index ebcd3aa242531689fee697b6d12d0269e5dba27e..7bbc92c626c9a97bb761687dd974acdbfef40778 100644
--- a/ipapython/dnssec/odsmgr.py
+++ b/ipapython/dnssec/odsmgr.py
@@ -153,12 +153,18 @@ class ODSMgr(object):
         output = self.ksmutil(cmd)
         self.log.info(output)
         self.notify_enforcer()
+        self.cleanup_signer(name)
 
     def notify_enforcer(self):
         cmd = ['notify']
         output = self.ksmutil(cmd)
         self.log.info(output)
 
+    def cleanup_signer(self, zone_name):
+        cmd = ['ods-signer', 'ldap-cleanup', str(zone_name)]
+        output = ipautil.run(cmd, capture_output=True)
+        self.log.info(output)
+
     def ldap_event(self, op, uuid, attrs):
         """Record single LDAP event - zone addition or deletion.
 
-- 
2.5.0

<?xml version="1.0" encoding="UTF-8"?>
<!-- Managed by IPA - do not edit! -->
<Configuration>

	<RepositoryList>

		<Repository name="SoftHSM">
			<Module>$SOFTHSM_LIB</Module>
			<TokenLabel>$TOKEN_LABEL</TokenLabel>
			<PIN>$PIN</PIN>
            <AllowExtraction/>
		</Repository>

	</RepositoryList>

	<Common>
		<Logging>
			<Syslog><Facility>local0</Facility></Syslog>
		</Logging>

		<PolicyFile>/etc/opendnssec/kasp.xml</PolicyFile>
		<ZoneListFile>/etc/opendnssec/zonelist.xml</ZoneListFile>

	<!--
		<ZoneFetchFile>/etc/opendnssec/zonefetch.xml</ZoneFetchFile>
	-->
	</Common>

	<Enforcer>
		<Privileges>
			<User>ods</User>
			<Group>ods</Group>
		</Privileges>

		<Datastore><SQLite>$KASP_DB</SQLite></Datastore>
		<Interval>PT1M</Interval>
		<!-- <ManualKeyGeneration/> -->
		<!-- <RolloverNotification>P14D</RolloverNotification> -->

		<!-- the <DelegationSignerSubmitCommand> will get all current
		     DNSKEYs (as a RRset) on standard input
		-->
		<!-- <DelegationSignerSubmitCommand>/usr/sbin/eppclient</DelegationSignerSubmitCommand> -->
	</Enforcer>

</Configuration>
<?xml version="1.0" encoding="UTF-8"?>

<KASP>

	<Policy name="default">
		<Description>IPA default policy</Description>
		<Signatures>
			<Resign>PT5S</Resign>
			<Refresh>PT10S</Refresh>
			<Validity>
				<Default>PT1M</Default>
				<Denial>PT1M</Denial>
			</Validity>
			<Jitter>PT1S</Jitter>
			<InceptionOffset>P1D</InceptionOffset>
		</Signatures>

		<Denial>
			<NSEC3>
				<!-- <TTL>PT0S</TTL> -->
				<!-- <OptOut/> -->
				<Resalt>P100D</Resalt>
				<Hash>
					<Algorithm>1</Algorithm>
					<Iterations>5</Iterations>
					<Salt length="8"/>
				</Hash>
			</NSEC3>
		</Denial>

		<Keys>
			<!-- Parameters for both KSK and ZSK -->
			<TTL>PT10S</TTL>
			<RetireSafety>PT10S</RetireSafety>
			<PublishSafety>PT10S</PublishSafety>
			<!-- <ShareKeys/> -->
			<Purge>PT2H</Purge>

			<!-- Parameters for KSK only -->
			<KSK>
				<Algorithm length="3072">8</Algorithm>
				<Lifetime>PT1H</Lifetime>
				<Repository>SoftHSM</Repository>
			</KSK>

			<!-- Parameters for ZSK only -->
			<ZSK>
				<Algorithm length="2048">8</Algorithm>
				<Lifetime>PT15M</Lifetime>
				<Repository>SoftHSM</Repository>
				<!-- <ManualRollover/> -->
			</ZSK>
		</Keys>

		<Zone>
			<PropagationDelay>PT10S</PropagationDelay>
			<SOA>
				<TTL>PT10S</TTL>
				<Minimum>PT10S</Minimum>
				<Serial>unixtime</Serial>
			</SOA>
		</Zone>

		<Parent>
			<PropagationDelay>PT10S</PropagationDelay>
			<DS>
				<TTL>PT10S</TTL>
			</DS>
			<SOA>
				<TTL>PT10S</TTL>
				<Minimum>PT10S</Minimum>
			</SOA>
		</Parent>

	</Policy>

</KASP>
-- 
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