diff options
author | Petr Spacek <pspacek@redhat.com> | 2014-10-19 17:04:40 +0200 |
---|---|---|
committer | Martin Kosek <mkosek@redhat.com> | 2014-10-21 12:23:03 +0200 |
commit | 276e69de874f269f6e9089aebb650a5e0814a626 (patch) | |
tree | 829b68e2044ba4fd102b8eedf304f9b036f4c583 /daemons/dnssec | |
parent | 5556b7f50e2939d0c61d852f2b0dcd82ba2fcf9c (diff) | |
download | freeipa-276e69de874f269f6e9089aebb650a5e0814a626.tar.gz freeipa-276e69de874f269f6e9089aebb650a5e0814a626.tar.xz freeipa-276e69de874f269f6e9089aebb650a5e0814a626.zip |
DNSSEC: add ipa dnssec daemons
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 <jcholast@redhat.com>
Reviewed-By: David Kupka <dkupka@redhat.com>
Diffstat (limited to 'daemons/dnssec')
-rwxr-xr-x | daemons/dnssec/ipa-dnskeysync-replica | 164 | ||||
-rwxr-xr-x | daemons/dnssec/ipa-dnskeysyncd | 106 | ||||
-rw-r--r-- | daemons/dnssec/ipa-dnskeysyncd.service | 15 | ||||
-rwxr-xr-x | daemons/dnssec/ipa-ods-exporter | 501 | ||||
-rw-r--r-- | daemons/dnssec/ipa-ods-exporter.service | 15 | ||||
-rw-r--r-- | daemons/dnssec/ipa-ods-exporter.socket | 5 |
6 files changed, 806 insertions, 0 deletions
diff --git a/daemons/dnssec/ipa-dnskeysync-replica b/daemons/dnssec/ipa-dnskeysync-replica new file mode 100755 index 000000000..4d3e660d7 --- /dev/null +++ b/daemons/dnssec/ipa-dnskeysync-replica @@ -0,0 +1,164 @@ +#!/usr/bin/python +# +# Copyright (C) 2014 FreeIPA Contributors see COPYING for license +# +""" +Download keys from LDAP to local HSM. + +This program should be run only on replicas, not on DNSSEC masters. +""" + +from binascii import hexlify +from datetime import datetime +import dns.dnssec +import fcntl +import logging +import os +from pprint import pprint +import subprocess +import socket +import sys +import systemd.daemon +import systemd.journal +import time + +import ipalib +from ipapython.dn import DN +from ipapython.ipa_log_manager import root_logger, standard_logging_setup +from ipapython import ipaldap +from ipapython import ipautil +from ipaserver.plugins.ldap2 import ldap2 +from ipaplatform.paths import paths + +from ipapython.dnssec.abshsm import sync_pkcs11_metadata, ldap2p11helper_api_params, wrappingmech_name2id +from ipapython.dnssec.ldapkeydb import LdapKeyDB +from ipapython.dnssec.localhsm import LocalHSM +import _ipap11helper + +DAEMONNAME = 'ipa-dnskeysyncd' +PRINCIPAL = None # not initialized yet +WORKDIR = '/tmp' + +def hex_set(s): + out = set() + for i in s: + out.add("0x%s" % hexlify(i)) + return out + +def update_metadata_set(log, source_set, target_set): + """sync metadata from source key set to target key set + + Keys not present in both sets are left intact.""" + log = log.getChild('sync_metadata') + matching_keys = set(source_set.keys()).intersection(set(target_set.keys())) + log.info("keys in local HSM & LDAP: %s", hex_set(matching_keys)) + for key_id in matching_keys: + sync_pkcs11_metadata(log, source_set[key_id], target_set[key_id]) + + +def find_unwrapping_key(log, localhsm, wrapping_key_uri): + wrap_keys = localhsm.find_keys(uri=wrapping_key_uri) + # find usable unwrapping key with matching ID + for key_id, key in wrap_keys.iteritems(): + unwrap_keys = localhsm.find_keys(id=key_id, cka_unwrap=True) + if len(unwrap_keys) > 0: + return unwrap_keys.popitem()[1] + +def ldap2replica_master_keys_sync(log, ldapkeydb, localhsm): + ## LDAP -> replica master key synchronization + # import new master keys from LDAP + new_keys = set(ldapkeydb.master_keys.keys()) \ + - set(localhsm.master_keys.keys()) + log.debug("master keys in local HSM: %s", hex_set(localhsm.master_keys.keys())) + log.debug("master keys in LDAP HSM: %s", hex_set(ldapkeydb.master_keys.keys())) + log.debug("new master keys in LDAP HSM: %s", hex_set(new_keys)) + for mkey_id in new_keys: + mkey_ldap = ldapkeydb.master_keys[mkey_id] + for wrapped_ldap in mkey_ldap.wrapped_entries: + unwrapping_key = find_unwrapping_key(log, localhsm, + wrapped_ldap.single_value['ipaWrappingKey']) + if unwrapping_key: + break + + # TODO: Could it happen in normal cases? + assert unwrapping_key is not None, "Local HSM does not contain suitable unwrapping key for master key 0x%s" % hexlify(mkey_id) + + params = ldap2p11helper_api_params(mkey_ldap) + params['data'] = wrapped_ldap.single_value['ipaSecretKey'] + params['unwrapping_key'] = unwrapping_key.handle + params['wrapping_mech'] = wrappingmech_name2id[wrapped_ldap.single_value['ipaWrappingMech']] + log.debug('Importing new master key: 0x%s %s', hexlify(mkey_id), params) + localhsm.p11.import_wrapped_secret_key(**params) + + # synchronize metadata about master keys in LDAP + update_metadata_set(log, ldapkeydb.master_keys, localhsm.master_keys) + +def ldap2replica_zone_keys_sync(log, ldapkeydb, localhsm): + ## LDAP -> replica zone key synchronization + # import new zone keys from LDAP + new_keys = set(ldapkeydb.zone_keypairs.keys()) \ + - set(localhsm.zone_privkeys.keys()) + + log.debug("zone keys in local HSM: %s", hex_set(localhsm.master_keys.keys())) + log.debug("zone keys in LDAP HSM: %s", hex_set(ldapkeydb.master_keys.keys())) + log.debug("new zone keys in LDAP HSM: %s", hex_set(new_keys)) + for zkey_id in new_keys: + zkey_ldap = ldapkeydb.zone_keypairs[zkey_id] + log.debug('Looking for unwrapping key "%s" for zone key 0x%s', + zkey_ldap['ipaWrappingKey'], hexlify(zkey_id)) + unwrapping_key = find_unwrapping_key(log, localhsm, + zkey_ldap['ipaWrappingKey']) + assert unwrapping_key is not None, \ + "Local HSM does not contain suitable unwrapping key for ' \ + 'zone key 0x%s" % hexlify(zkey_id) + + log.debug('Importing zone key pair 0x%s', hexlify(zkey_id)) + localhsm.import_private_key(zkey_ldap, zkey_ldap['ipaPrivateKey'], + unwrapping_key) + localhsm.import_public_key(zkey_ldap, zkey_ldap['ipaPublicKey']) + + # synchronize metadata about zone keys in LDAP & local HSM + update_metadata_set(log, ldapkeydb.master_keys, localhsm.master_keys) + + # delete keys removed from LDAP + deleted_keys = set(localhsm.zone_privkeys.keys()) \ + - set(ldapkeydb.zone_keypairs.keys()) + + for zkey_id in deleted_keys: + localhsm.p11.delete_key(localhsm.zone_pubkeys[zkey_id].handle) + localhsm.p11.delete_key(localhsm.zone_privkeys[zkey_id].handle) + + +# IPA framework initialization +ipalib.api.bootstrap() +ipalib.api.finalize() +standard_logging_setup(verbose=True, debug = True) # debug=ipalib.api.env.debug) +log = root_logger +log.setLevel(level=logging.DEBUG) + +# Kerberos initialization +PRINCIPAL = str('%s/%s' % (DAEMONNAME, ipalib.api.env.host)) +log.debug('Kerberos principal: %s', PRINCIPAL) +ipautil.kinit_hostprincipal(paths.IPA_DNSKEYSYNCD_KEYTAB, WORKDIR, PRINCIPAL) +log.debug('Got TGT') + +# LDAP initialization +ldap = ipalib.api.Backend[ldap2] +# fixme +log.debug('Connecting to LDAP') +ldap.connect(ccache="%s/ccache" % WORKDIR) +log.debug('Connected') + + +### DNSSEC master: key synchronization +ldapkeydb = LdapKeyDB(log, ldap, + DN(ipalib.api.env.container_dnssec_keys, ipalib.api.env.basedn)) + +# TODO: slot number could be configurable +localhsm = LocalHSM(paths.LIBSOFTHSM2_SO, 0, + open(paths.DNSSEC_SOFTHSM_PIN).read()) + +ldap2replica_master_keys_sync(log, ldapkeydb, localhsm) +ldap2replica_zone_keys_sync(log, ldapkeydb, localhsm) + +sys.exit(0) diff --git a/daemons/dnssec/ipa-dnskeysyncd b/daemons/dnssec/ipa-dnskeysyncd new file mode 100755 index 000000000..c7475bd65 --- /dev/null +++ b/daemons/dnssec/ipa-dnskeysyncd @@ -0,0 +1,106 @@ +#!/usr/bin/python +# +# Copyright (C) 2014 FreeIPA Contributors see COPYING for license +# + +import sys +import ldap +import ldapurl +import logging +import os +import signal +import systemd.journal +import time + +from ipalib import api +from ipapython.dn import DN +from ipapython.ipa_log_manager import root_logger, standard_logging_setup +from ipapython import ipaldap +from ipapython import ipautil +from ipaserver.plugins.ldap2 import ldap2 +from ipaplatform.paths import paths + +from ipapython.dnssec.keysyncer import KeySyncer + +DAEMONNAME = 'ipa-dnskeysyncd' +PRINCIPAL = None # not initialized yet +WORKDIR = '/tmp' # private temp +KEYTAB_FB = paths.IPA_DNSKEYSYNCD_KEYTAB + +# Shutdown handler +def commenceShutdown(signum, stack): + # Declare the needed global variables + global watcher_running, ldap_connection, log + log.info('Signal %s received: Shutting down!', signum) + + # We are no longer running + watcher_running = False + + # Tear down the server connection + if ldap_connection: + ldap_connection.close_db() + del ldap_connection + + # Shutdown + sys.exit(0) + + +os.umask(007) + +# Global state +watcher_running = True +ldap_connection = False + +# Signal handlers +signal.signal(signal.SIGTERM, commenceShutdown) +signal.signal(signal.SIGINT, commenceShutdown) + +# IPA framework initialization +api.bootstrap() +api.finalize() +standard_logging_setup(verbose=True, debug=api.env.debug) +log = root_logger +#log.addHandler(systemd.journal.JournalHandler()) + +# Kerberos initialization +PRINCIPAL = str('%s/%s' % (DAEMONNAME, api.env.host)) +log.debug('Kerberos principal: %s', PRINCIPAL) +ipautil.kinit_hostprincipal(KEYTAB_FB, WORKDIR, PRINCIPAL) + +# LDAP initialization +basedn = DN(api.env.container_dns, api.env.basedn) +ldap_url = ldapurl.LDAPUrl(api.env.ldap_uri) +ldap_url.dn = str(basedn) +ldap_url.scope = ldapurl.LDAP_SCOPE_SUBTREE +ldap_url.filterstr = '(|(objectClass=idnsZone)(objectClass=idnsSecKey)(objectClass=ipk11PublicKey))' +log.debug('LDAP URL: %s', ldap_url.unparse()) + +# Real work +while watcher_running: + # Prepare the LDAP server connection (triggers the connection as well) + ldap_connection = KeySyncer(ldap_url.initializeUrl(), ipa_api=api) + + # Now we login to the LDAP server + try: + log.info('LDAP bind...') + ldap_connection.sasl_interactive_bind_s("", ipaldap.SASL_GSSAPI) + except ldap.INVALID_CREDENTIALS, e: + log.exception('Login to LDAP server failed: %s', e) + sys.exit(1) + except ldap.SERVER_DOWN, e: + log.exception('LDAP server is down, going to retry: %s', e) + time.sleep(5) + continue + + # Commence the syncing + log.info('Commencing sync process') + ldap_search = ldap_connection.syncrepl_search( + ldap_url.dn, + ldap_url.scope, + mode='refreshAndPersist', + attrlist=ldap_url.attrs, + filterstr=ldap_url.filterstr + ) + + while ldap_connection.syncrepl_poll(all=1, msgid=ldap_search): + pass diff --git a/daemons/dnssec/ipa-dnskeysyncd.service b/daemons/dnssec/ipa-dnskeysyncd.service new file mode 100644 index 000000000..ecd38a593 --- /dev/null +++ b/daemons/dnssec/ipa-dnskeysyncd.service @@ -0,0 +1,15 @@ +[Unit] +Description=IPA key daemon + +[Service] +EnvironmentFile=/etc/sysconfig/ipa-dnskeysyncd +ExecStart=/usr/libexec/ipa/ipa-dnskeysyncd +User=ods +Group=named +SupplementaryGroups=ods +PrivateTmp=yes +Restart=on-failure +RestartSec=60s + +[Install] +WantedBy=multi-user.target diff --git a/daemons/dnssec/ipa-ods-exporter b/daemons/dnssec/ipa-ods-exporter new file mode 100755 index 000000000..4ae0d99b5 --- /dev/null +++ b/daemons/dnssec/ipa-ods-exporter @@ -0,0 +1,501 @@ +#!/usr/bin/python +# +# Copyright (C) 2014 FreeIPA Contributors see COPYING for license +# +""" +This is FreeIPA's replacement for signerd from OpenDNSSEC suite version 1.4.x. + +This program uses the same socket and protocol as original signerd and should +be activated via systemd socket activation using "ods-signer" command line +utility. + +Purpose of this replacement is to upload keys generated by OpenDNSSEC to LDAP. +""" + +from binascii import hexlify +from datetime import datetime +import dns.dnssec +import fcntl +import logging +import os +import subprocess +import socket +import sys +import systemd.daemon +import systemd.journal +import sqlite3 +import time + +import ipalib +from ipapython.dn import DN +from ipapython.ipa_log_manager import root_logger, standard_logging_setup +from ipapython import ipaldap +from ipapython import ipautil +from ipaserver.plugins.ldap2 import ldap2 +from ipaplatform.paths import paths + +from ipapython.dnssec.abshsm import sync_pkcs11_metadata, wrappingmech_name2id +from ipapython.dnssec.ldapkeydb import LdapKeyDB +from ipapython.dnssec.localhsm import LocalHSM +import _ipap11helper + +DAEMONNAME = 'ipa-ods-exporter' +PRINCIPAL = None # not initialized yet +WORKDIR = os.path.join(paths.VAR_OPENDNSSEC_DIR ,'tmp') +KEYTAB_FB = paths.IPA_ODS_EXPORTER_KEYTAB + +ODS_SE_MAXLINE = 1024 # from ODS common/config.h +ODS_DB_LOCK_PATH = "%s%s" % (paths.OPENDNSSEC_KASP_DB, '.our_lock') + +# TODO: MECH_RSA_OAEP +SECRETKEY_WRAPPING_MECH = 'rsaPkcs' +PRIVKEY_WRAPPING_MECH = 'aesKeyWrapPad' + +# DNSKEY flag constants +dnskey_flag_by_value = { + 0x0001: 'SEP', + 0x0080: 'REVOKE', + 0x0100: 'ZONE' +} + +def dnskey_flags_to_text_set(flags): + """Convert a DNSKEY flags value to set texts + @rtype: set([string])""" + + flags_set = set() + mask = 0x1 + while mask <= 0x8000: + if flags & mask: + text = dnskey_flag_by_value.get(mask) + if not text: + text = hex(mask) + flags_set.add(text) + mask <<= 1 + return flags_set + +def datetime2ldap(dt): + return dt.strftime(ipalib.constants.LDAP_GENERALIZED_TIME_FORMAT) + +def sql2datetime(sql_time): + return datetime.strptime(sql_time, "%Y-%m-%d %H:%M:%S") + +def sql2datetimes(row): + row2key_map = {'generate': 'idnsSecKeyCreated', + 'publish': 'idnsSecKeyPublish', + 'active': 'idnsSecKeyActivate', + 'retire': 'idnsSecKeyInactive', + 'dead': 'idnsSecKeyDelete'} + times = {} + for column, key in row2key_map.iteritems(): + if row[column] is not None: + times[key] = sql2datetime(row[column]) + return times + +def sql2ldap_algorithm(sql_algorithm): + return {"idnsSecAlgorithm": dns.dnssec.algorithm_to_text(sql_algorithm)} + +def sql2ldap_flags(sql_flags): + dns_flags = dnskey_flags_to_text_set(sql_flags) + ldap_flags = {} + for flag in dns_flags: + attr = 'idnsSecKey%s' % flag + ldap_flags[attr] = 'TRUE' + return ldap_flags + +def sql2ldap_keyid(sql_keyid): + assert len(sql_keyid) % 2 == 0 + assert len(sql_keyid) > 0 + # TODO: this is huge hack. BIND has some problems with % notation in URIs. + # Workaround: OpenDNSSEC uses same value for ID also for label (but in hex). + uri = "pkcs11:object=%s" % sql_keyid + #uri += '%'.join(sql_keyid[i:i+2] for i in range(0, len(sql_keyid), 2)) + return {"idnsSecKeyRef": uri} + +class ods_db_lock(object): + def __enter__(self): + self.f = open(ODS_DB_LOCK_PATH, 'w') + fcntl.lockf(self.f, fcntl.LOCK_EX) + + def __exit__(self, *args): + fcntl.lockf(self.f, fcntl.LOCK_UN) + self.f.close() + +def get_ldap_zone(ldap, dns_base, name): + zone_names = ["%s." % name, name] + + # find zone object: name can optionally end with period + ldap_zone = None + for zone_name in zone_names: + zone_base = DN("idnsname=%s" % zone_name, dns_base) + try: + ldap_zone = ldap.get_entry(dn=zone_base, + attrs_list=["idnsname"]) + break + except ipalib.errors.NotFound: + continue + + assert ldap_zone is not None, 'DNS zone "%s" should exist in LDAP' % name + + return ldap_zone + +def get_ldap_keys_dn(zone_dn): + """Container DN""" + return DN("cn=keys", zone_dn) + +def get_ldap_keys(ldap, zone_dn): + """Keys objects""" + keys_dn = get_ldap_keys_dn(zone_dn) + ldap_filter = ldap.make_filter_from_attr('objectClass', 'idnsSecKey') + ldap_keys = ldap.get_entries(base_dn=keys_dn, filter=ldap_filter) + + return ldap_keys + +def get_ods_keys(zone_name): + # Open DB directly and read key timestamps etc. + with ods_db_lock(): + db = sqlite3.connect(paths.OPENDNSSEC_KASP_DB, + isolation_level="EXCLUSIVE") + db.row_factory = sqlite3.Row + db.execute('BEGIN') + + # get zone ID + cur = db.execute("SELECT id FROM zones WHERE LOWER(name)=LOWER(?)", + (zone_name,)) + rows = cur.fetchall() + 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.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'])) + log.debug("%s", key_data) + 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' + + key_data.update(sql2ldap_algorithm(row['algorithm'])) + key_id = "%s-%s-%s" % (key_type, + datetime2ldap(key_data['idnsSecKeyCreated']), + row['HSMkey_id']) + + key_data.update(sql2ldap_keyid(row['HSMkey_id'])) + keys[key_id] = key_data + + return keys + +def sync_set_metadata_2ldap(log, source_set, target_set): + """sync metadata from source key set to target key set in LDAP + + Keys not present in both sets are left intact.""" + log = log.getChild('sync_set_metadata_2ldap') + matching_keys = set(source_set.keys()).intersection(set(target_set.keys())) + log.info("keys in local HSM & LDAP: %s", hex_set(matching_keys)) + for key_id in matching_keys: + sync_pkcs11_metadata(log, source_set[key_id], target_set[key_id]) + +def ldap2master_replica_keys_sync(log, ldapkeydb, localhsm): + """LDAP=>master's local HSM replica key synchronization""" + # import new replica keys from LDAP + log = log.getChild('ldap2master_replica') + log.debug("replica pub keys in LDAP: %s", hex_set(ldapkeydb.replica_pubkeys_wrap)) + log.debug("replica pub keys in SoftHSM: %s", hex_set(localhsm.replica_pubkeys_wrap)) + new_replica_keys = set(ldapkeydb.replica_pubkeys_wrap.keys()) \ + - set(localhsm.replica_pubkeys_wrap.keys()) + log.info("new replica keys in LDAP: %s", hex_set(new_replica_keys)) + for key_id in new_replica_keys: + new_key_ldap = ldapkeydb.replica_pubkeys_wrap[key_id] + log.error('label=%s, id=%s, data=%s', + new_key_ldap['ipk11label'], + hexlify(new_key_ldap['ipk11id']), + hexlify(new_key_ldap['ipapublickey'])) + localhsm.import_public_key(new_key_ldap, new_key_ldap['ipapublickey']) + + # set CKA_WRAP = FALSE for all replica keys removed from LDAP + removed_replica_keys = set(localhsm.replica_pubkeys_wrap.keys()) \ + - set(ldapkeydb.replica_pubkeys_wrap.keys()) + log.info("obsolete replica keys in local HSM: %s", + hex_set(removed_replica_keys)) + for key_id in removed_replica_keys: + localhsm.replica_pubkeys_wrap[key_id]['ipk11wrap'] = False + + # synchronize replica key attributes from LDAP to local HSM + sync_set_metadata_2ldap(log, localhsm.replica_pubkeys_wrap, + ldapkeydb.replica_pubkeys_wrap) + +def master2ldap_master_keys_sync(log, ldapkeydb, localhsm): + ## master key -> LDAP synchronization + # export new master keys to LDAP + new_master_keys = set(localhsm.master_keys.keys()) \ + - set(ldapkeydb.master_keys.keys()) + log.debug("master keys in local HSM: %s", hex_set(localhsm.master_keys.keys())) + log.debug("master keys in LDAP HSM: %s", hex_set(ldapkeydb.master_keys.keys())) + log.debug("new master keys in local HSM: %s", hex_set(new_master_keys)) + for mkey_id in new_master_keys: + mkey = localhsm.master_keys[mkey_id] + ldapkeydb.import_master_key(mkey) + + # re-fill cache with keys we just added + ldapkeydb.flush() + log.debug('master keys in LDAP after flush: %s', hex_set(ldapkeydb.master_keys)) + + # synchronize master key metadata to LDAP + for mkey_id, mkey_local in localhsm.master_keys.iteritems(): + log.debug('synchronizing master key metadata: 0x%s', hexlify(mkey_id)) + sync_pkcs11_metadata(log, mkey_local, ldapkeydb.master_keys[mkey_id]) + + # re-wrap all master keys in LDAP with new replica keys (as necessary) + enabled_replica_key_ids = set(localhsm.replica_pubkeys_wrap.keys()) + log.debug('enabled replica key ids: %s', hex_set(enabled_replica_key_ids)) + + for mkey_id, mkey_ldap in ldapkeydb.master_keys.iteritems(): + log.debug('processing master key data: 0x%s', hexlify(mkey_id)) + + # check that all active replicas have own copy of master key + used_replica_keys = set() + for wrapped_entry in mkey_ldap.wrapped_entries: + matching_keys = localhsm.find_keys( + uri=wrapped_entry.single_value['ipaWrappingKey']) + for matching_key in matching_keys.itervalues(): + assert matching_key['ipk11label'].startswith(u'dnssec-replica:'), \ + 'Wrapped key "%s" refers to PKCS#11 URI "%s" which is ' \ + 'not a know DNSSEC replica key: label "%s" does not start ' \ + 'with "dnssec-replica:" prefix' % (wrapped_entry.dn, + wrapped_entry['ipaWrappingKey'], + matching_key['ipk11label']) + used_replica_keys.add(matching_key['ipk11id']) + + new_replica_keys = enabled_replica_key_ids - used_replica_keys + log.debug('master key 0x%s is not wrapped with replica keys %s', + hexlify(mkey_id), hex_set(new_replica_keys)) + + # wrap master key with new replica keys + mkey_local = localhsm.find_keys(id=mkey_id).popitem()[1] + for replica_key_id in new_replica_keys: + log.info('adding master key 0x%s wrapped with replica key 0x%s' % ( + hexlify(mkey_id), hexlify(replica_key_id))) + replica_key = localhsm.replica_pubkeys_wrap[replica_key_id] + keydata = localhsm.p11.export_wrapped_key(mkey_local.handle, + replica_key.handle, _ipap11helper.MECH_RSA_PKCS) + mkey_ldap.add_wrapped_data(keydata, SECRETKEY_WRAPPING_MECH, + replica_key_id) + + ldapkeydb.flush() + +def master2ldap_zone_keys_sync(log, ldapkeydb, localhsm): + # synchroniza zone keys + log = log.getChild('master2ldap_zone_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)) + + new_keys = set(pubkeys_local) - set(keypairs_ldap) + log.debug("new zone keys in local HSM: %s", hex_set(new_keys)) + mkey = localhsm.active_master_key + # wrap each new zone key pair with selected master key + for zkey_id in new_keys: + pubkey = pubkeys_local[zkey_id] + pubkey_data = localhsm.p11.export_public_key(pubkey.handle) + + privkey = privkeys_local[zkey_id] + privkey_data = localhsm.p11.export_wrapped_key(privkey.handle, + wrapping_key=mkey.handle, + wrapping_mech=wrappingmech_name2id[PRIVKEY_WRAPPING_MECH]) + ldapkeydb.import_zone_key(pubkey, pubkey_data, privkey, privkey_data, + PRIVKEY_WRAPPING_MECH, mkey['ipk11id']) + + sync_set_metadata_2ldap(log, pubkeys_local, keypairs_ldap) + sync_set_metadata_2ldap(log, privkeys_local, keypairs_ldap) + ldapkeydb.flush() + + +def hex_set(s): + out = set() + for i in s: + out.add("0x%s" % hexlify(i)) + return out + +def receive_zone_name(log): + fds = systemd.daemon.listen_fds() + if len(fds) != 1: + raise KeyError('Exactly one socket is expected.') + + sck = socket.fromfd(fds[0], socket.AF_UNIX, socket.SOCK_STREAM) + + conn, addr = sck.accept() + log.debug('accepted new connection %s', repr(conn)) + + # this implements cmdhandler_handle_cmd() logic + cmd = conn.recv(ODS_SE_MAXLINE) + cmd = cmd.strip() + + try: + if cmd == 'ipa-hsm-update': + msg = 'HSM synchronization finished, exiting.' + conn.send('%s\n' % msg) + log.info(msg) + sys.exit(0) + + elif not cmd.startswith('update '): + conn.send('Command "%s" is not supported by IPA; ' \ + 'HSM synchronization was finished and the command ' \ + 'will be ignored.\n' % cmd) + log.info('Ignoring unsupported command "%s".', cmd) + sys.exit(0) + + else: + zone_name = cmd2ods_zone_name(cmd) + conn.send('Update request for zone "%s" queued.\n' % zone_name) + log.info('Processing command: "%s"', cmd) + + finally: + # Reply & close connection early. + # This is necessary to let Enforcer to unlock the ODS DB. + conn.shutdown(socket.SHUT_RDWR) + conn.close() + + return zone_name + +def cmd2ods_zone_name(cmd): + # ODS stores zone name without trailing period + zone_name = cmd[7:].strip() + if len(zone_name) > 1 and zone_name[-1] == '.': + zone_name = zone_name[:-1] + + return zone_name + +log = logging.getLogger('root') +# this service is socket-activated +log.addHandler(systemd.journal.JournalHandler()) +log.setLevel(level=logging.DEBUG) + +if len(sys.argv) != 1: + print __doc__ + sys.exit(1) + +# IPA framework initialization +ipalib.api.bootstrap() +ipalib.api.finalize() + +# Kerberos initialization +PRINCIPAL = str('%s/%s' % (DAEMONNAME, ipalib.api.env.host)) +log.debug('Kerberos principal: %s', PRINCIPAL) +ipautil.kinit_hostprincipal(paths.IPA_ODS_EXPORTER_KEYTAB, WORKDIR, PRINCIPAL) +log.debug('Got TGT') + +# LDAP initialization +dns_dn = DN(ipalib.api.env.container_dns, ipalib.api.env.basedn) +ldap = ipalib.api.Backend[ldap2] +# fixme +log.debug('Connecting to LDAP') +ldap.connect(ccache="%s/ccache" % WORKDIR) +log.debug('Connected') + + +### DNSSEC master: key synchronization +ldapkeydb = LdapKeyDB(log, ldap, DN(ipalib.api.env.container_dnssec_keys, + ipalib.api.env.basedn)) +localhsm = LocalHSM(paths.LIBSOFTHSM2_SO, 0, + open(paths.DNSSEC_SOFTHSM_PIN).read()) + +ldap2master_replica_keys_sync(log, ldapkeydb, localhsm) +master2ldap_master_keys_sync(log, ldapkeydb, localhsm) +master2ldap_zone_keys_sync(log, ldapkeydb, localhsm) + + +### DNSSEC master: DNSSEC key metadata upload +# command receive is delayed so the command will stay in socket queue until +# the problem with LDAP server or HSM is fixed +try: + zone_name = receive_zone_name(log) + +# Handle cases where somebody ran the program without systemd. +except KeyError as e: + print 'HSM (key material) sychronization is finished but ' \ + 'this program should be socket-activated by OpenDNSSEC.' + print 'Use "ods-signer" command line utility to synchronize ' \ + 'DNS zone keys and metadata.' + print 'Error: %s' % e + sys.exit(0) + +ods_keys = get_ods_keys(zone_name) +ods_keys_id = set(ods_keys.keys()) + +ldap_zone = get_ldap_zone(ldap, dns_dn, zone_name) +zone_dn = ldap_zone.dn + +keys_dn = get_ldap_keys_dn(zone_dn) +try: + ldap_keys = get_ldap_keys(ldap, zone_dn) +except ipalib.errors.NotFound: + # cn=keys container does not exist, create it + ldap_keys = [] + ldap_keys_container = ldap.make_entry(keys_dn, + objectClass=['nsContainer']) + try: + ldap.add_entry(ldap_keys_container) + except ipalib.errors.DuplicateEntry: + # ldap.get_entries() does not distinguish non-existent base DN + # from empty result set so addition can fail because container + # itself exists already + pass + +ldap_keys_dict = {} +for ldap_key in ldap_keys: + cn = ldap_key['cn'][0] + ldap_keys_dict[cn] = ldap_key + +ldap_keys = ldap_keys_dict # shorthand +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) +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) + 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) +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) + 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) +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) + ldap_key.update(ods_key) + try: + ldap.update_entry(ldap_key) + except ipalib.errors.EmptyModlist: + continue + +log.debug('Done') diff --git a/daemons/dnssec/ipa-ods-exporter.service b/daemons/dnssec/ipa-ods-exporter.service new file mode 100644 index 000000000..0d917b8d3 --- /dev/null +++ b/daemons/dnssec/ipa-ods-exporter.service @@ -0,0 +1,15 @@ +[Unit] +Description=IPA OpenDNSSEC Signer replacement +Wants=ipa-ods-exporter.socket +After=ipa-ods-exporter.socket + +[Service] +EnvironmentFile=/etc/sysconfig/ipa-ods-exporter +ExecStart=/usr/libexec/ipa/ipa-ods-exporter +User=ods +PrivateTmp=yes +Restart=on-failure +RestartSec=60s + +[Install] +WantedBy=multi-user.target diff --git a/daemons/dnssec/ipa-ods-exporter.socket b/daemons/dnssec/ipa-ods-exporter.socket new file mode 100644 index 000000000..1499f1823 --- /dev/null +++ b/daemons/dnssec/ipa-ods-exporter.socket @@ -0,0 +1,5 @@ +[Socket] +ListenStream=/var/run/opendnssec/engine.sock + +[Install] +WantedBy=sockets.target |