From db24ab9357ea63deaf25e8b9c5b3ad2d08a0c82b Mon Sep 17 00:00:00 2001 From: Petr Spacek 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 Reviewed-By: Martin Basti Reviewed-By: Martin Basti --- daemons/dnssec/ipa-ods-exporter | 45 ++++++++++++++++++++++---- ipapython/dnssec/ldapkeydb.py | 72 ++++++++++++++++++++++++++++++++++------- 2 files changed, 99 insertions(+), 18 deletions(-) diff --git a/daemons/dnssec/ipa-ods-exporter b/daemons/dnssec/ipa-ods-exporter index 051fa53a950f7afbea5e9b1e541a9435aa02bc17..2a1cc4315355569b24ec6ef42a68f4d64fee9f4f 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)) @@ -396,10 +399,10 @@ def master2ldap_zone_keys_sync(log, ldapkeydb, localhsm): 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)) + 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))) new_keys = set(pubkeys_local) - set(keypairs_ldap) log.debug("new zone keys in local HSM: %s", hex_set(new_keys)) @@ -420,6 +423,29 @@ 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: + keypairs_ldap[zkey_id].schedule_deletion() + ldapkeydb.flush() def hex_set(s): out = set() @@ -600,7 +626,7 @@ ldap.connect(ccache=ccache_name) 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)) @@ -612,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: @@ -666,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 54a1fba1d2db8f27c9c9b881ff42201365852587..2131508cc6779d1cc99c417a31da866b7fdcf2c2 100644 --- a/ipapython/dnssec/ldapkeydb.py +++ b/ipapython/dnssec/ldapkeydb.py @@ -105,40 +105,56 @@ def get_default_attrs(object_classes): result.update(defaults[cls]) return result + class Key(collections.MutableMapping): """abstraction to hide LDAP entry weirdnesses: - non-normalized attribute names - boolean attributes returned as strings + - planned entry deletion prevents subsequent use of the instance """ def __init__(self, entry, ldap, ldapkeydb): self.entry = entry + self._delentry = None # indicates that object was deleted self.ldap = ldap self.ldapkeydb = ldapkeydb self.log = ldap.log.getChild(__name__) + def __assert_not_deleted(self): + assert self.entry and not self._delentry, ( + "attempt to use to-be-deleted entry %s detected" + % self._delentry.dn) + def __getitem__(self, key): + self.__assert_not_deleted() val = self.entry.single_value[key] if key.lower() in bool_attr_names: val = ldap_bool(val) return val def __setitem__(self, key, value): + self.__assert_not_deleted() self.entry[key] = value def __delitem__(self, key): + self.__assert_not_deleted() del self.entry[key] def __iter__(self): """generates list of ipa names of all PKCS#11 attributes present in the object""" + self.__assert_not_deleted() for ipa_name in self.entry.keys(): lowercase = ipa_name.lower() if lowercase in attrs_name2id: yield lowercase def __len__(self): + self.__assert_not_deleted() return len(self.entry) def __repr__(self): + if self._delentry: + return 'deleted entry: %s' % repr(self._delentry) + sanitized = dict(self.entry) for attr in ['ipaPrivateKey', 'ipaPublicKey', 'ipk11publickeyinfo']: if attr in sanitized: @@ -153,6 +169,49 @@ 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""" + if self._delentry: + self._delete_key() + return + + self._cleanup_key() + + try: + self.ldap.update_entry(self.entry) + except ipalib.errors.EmptyModlist: + pass + + def _delete_key(self): + """remove key metadata entry from LDAP + + After calling this, the python object is no longer valid and all + subsequent method calls on it will fail. + """ + assert not self.entry, ( + "Key._delete_key() called before Key.schedule_deletion()") + assert self._delentry, "Key._delete_key() called more than once" + self.log.debug('deleting key id 0x%s DN %s from LDAP', + hexlify(self._delentry.single_value['ipk11id']), + self._delentry.dn) + self.ldap.delete_entry(self._delentry) + self._delentry = None + self.ldap = None + self.ldapkeydb = None + + def schedule_deletion(self): + """schedule key deletion from LDAP + + Calling schedule_deletion() will make this object incompatible with + normal Key. After that the object must not be read or modified. + Key metadata will be actually deleted when LdapKeyDB.flush() is called. + """ + assert not self._delentry, ( + "Key.schedule_deletion() called more than once") + self._delentry = self.entry + self.entry = None + + class ReplicaKey(Key): # TODO: object class assert def __init__(self, entry, ldap, ldapkeydb): @@ -239,21 +298,12 @@ 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.itervalues(): - self._update_key(key) + key._update_key() def flush(self): """write back content of caches to LDAP""" -- 2.4.3