summaryrefslogtreecommitdiffstats
path: root/ipapython/dnssec/bindmgr.py
diff options
context:
space:
mode:
authorPetr Spacek <pspacek@redhat.com>2014-10-19 17:04:40 +0200
committerMartin Kosek <mkosek@redhat.com>2014-10-21 12:23:03 +0200
commit276e69de874f269f6e9089aebb650a5e0814a626 (patch)
tree829b68e2044ba4fd102b8eedf304f9b036f4c583 /ipapython/dnssec/bindmgr.py
parent5556b7f50e2939d0c61d852f2b0dcd82ba2fcf9c (diff)
downloadfreeipa-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 'ipapython/dnssec/bindmgr.py')
-rw-r--r--ipapython/dnssec/bindmgr.py176
1 files changed, 176 insertions, 0 deletions
diff --git a/ipapython/dnssec/bindmgr.py b/ipapython/dnssec/bindmgr.py
new file mode 100644
index 000000000..55765e16b
--- /dev/null
+++ b/ipapython/dnssec/bindmgr.py
@@ -0,0 +1,176 @@
+#!/usr/bin/python
+#
+# Copyright (C) 2014 FreeIPA Contributors see COPYING for license
+#
+
+from datetime import datetime
+import dns.name
+import errno
+import os
+import logging
+import shutil
+import stat
+import subprocess
+
+from ipalib import api
+import ipalib.constants
+from ipapython.dn import DN
+from ipapython import ipa_log_manager, ipautil
+from ipaplatform.paths import paths
+
+from temp import TemporaryDirectory
+
+time_bindfmt = '%Y%m%d%H%M%S'
+
+# this daemon should run under ods:named user:group
+# user has to be ods because ODSMgr.py sends signal to ods-enforcerd
+FILE_PERM = (stat.S_IRUSR | stat.S_IRGRP | stat.S_IWGRP | stat.S_IWUSR)
+DIR_PERM = (stat.S_IRWXU | stat.S_IRWXG)
+
+class BINDMgr(object):
+ """BIND key manager. It does LDAP->BIND key files synchronization.
+
+ One LDAP object with idnsSecKey object class will produce
+ single pair of BIND key files.
+ """
+ def __init__(self, api):
+ self.api = api
+ self.log = ipa_log_manager.log_mgr.get_logger(self)
+ self.ldap_keys = {}
+ self.modified_zones = set()
+
+ def notify_zone(self, zone):
+ cmd = ['rndc', 'sign', zone.to_text()]
+ output = ipautil.run(cmd)[0]
+ self.log.info(output)
+
+ def dn2zone_name(self, dn):
+ """cn=KSK-20140813162153Z-cede9e182fc4af76c4bddbc19123a565,cn=keys,idnsname=test,cn=dns,dc=ipa,dc=example"""
+ # verify that metadata object is under DNS sub-tree
+ dn = DN(dn)
+ container = DN(self.api.env.container_dns, self.api.env.basedn)
+ idx = dn.rfind(container)
+ assert idx != -1, 'Metadata object %s is not inside %s' % (dn, container)
+ assert len(dn[idx - 1]) == 1, 'Multi-valued RDN as zone name is not supported'
+ return dns.name.from_text(dn[idx - 1]['idnsname'])
+
+ def time_ldap2bindfmt(self, str_val):
+ dt = datetime.strptime(str_val, ipalib.constants.LDAP_GENERALIZED_TIME_FORMAT)
+ return dt.strftime(time_bindfmt)
+
+ def dates2params(self, ldap_attrs):
+ attr2param = {'idnsseckeypublish': '-P',
+ 'idnsseckeyactivate': '-A',
+ 'idnsseckeyinactive': '-I',
+ 'idnsseckeydelete': '-D'}
+
+ params = []
+ for attr, param in attr2param.items():
+ 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]))
+
+ return params
+
+ def ldap_event(self, op, uuid, attrs):
+ """Record single LDAP event - key addition, deletion or modification.
+
+ Change is only recorded to memory.
+ self.sync() has to be called to synchronize change to BIND."""
+ assert op == 'add' or op == 'del' or op == 'mod'
+ zone = self.dn2zone_name(attrs['dn'])
+ self.modified_zones.add(zone)
+ zone_keys = self.ldap_keys.setdefault(zone, {})
+ if op == 'add':
+ self.log.info('Key metadata %s added to zone %s' % (attrs['dn'], zone))
+ zone_keys[uuid] = attrs
+
+ elif op == 'del':
+ self.log.info('Key metadata %s deleted from zone %s' % (attrs['dn'], zone))
+ zone_keys.pop(uuid)
+
+ elif op == 'mod':
+ self.log.info('Key metadata %s updated in zone %s' % (attrs['dn'], zone))
+ zone_keys[uuid] = attrs
+
+ def install_key(self, zone, uuid, attrs, workdir):
+ """Run dnssec-keyfromlabel on given LDAP object.
+ :returns: base file name of output files, e.g. Kaaa.test.+008+19719"""
+ self.log.info('attrs: %s', attrs)
+ assert attrs.get('idnsseckeyzone', ['FALSE'])[0] == 'TRUE', \
+ 'object %s is not a DNS zone key' % attrs['dn']
+
+ uri = "%s;pin-source=%s" % (attrs['idnsSecKeyRef'][0], paths.DNSSEC_SOFTHSM_PIN)
+ cmd = [paths.DNSSEC_KEYFROMLABEL, '-K', workdir, '-a', attrs['idnsSecAlgorithm'][0], '-l', uri]
+ cmd += self.dates2params(attrs)
+ if attrs.get('idnsSecKeySep', ['FALSE'])[0].upper() == 'TRUE':
+ cmd += ['-f', 'KSK']
+ if attrs.get('idnsSecKeyRevoke', ['FALSE'])[0].upper() == 'TRUE':
+ cmd += ['-R', datetime.now().strftime(time_bindfmt)]
+ cmd.append(zone.to_text())
+
+ # keys has to be readable by ODS & named
+ basename = ipautil.run(cmd)[0].strip()
+ private_fn = "%s/%s.private" % (workdir, basename)
+ os.chmod(private_fn, FILE_PERM)
+ # this is useful mainly for debugging
+ with open("%s/%s.uuid" % (workdir, basename), 'w') as uuid_file:
+ uuid_file.write(uuid)
+ with open("%s/%s.dn" % (workdir, basename), 'w') as dn_file:
+ dn_file.write(attrs['dn'])
+
+ def sync_zone(self, zone):
+ self.log.info('Synchronizing zone %s' % zone)
+ zone_path = os.path.join(paths.BIND_LDAP_DNS_ZONE_WORKDIR,
+ zone.to_text(omit_final_dot=True))
+ try:
+ os.makedirs(zone_path)
+ except OSError as e:
+ if e.errno != errno.EEXIST:
+ raise e
+
+ # fix HSM permissions
+ # TODO: move out
+ for prefix, dirs, files in os.walk(paths.DNSSEC_TOKENS_DIR, topdown=True):
+ for name in dirs:
+ fpath = os.path.join(prefix, name)
+ self.log.debug('Fixing directory permissions: %s', fpath)
+ os.chmod(fpath, DIR_PERM | stat.S_ISGID)
+ for name in files:
+ fpath = os.path.join(prefix, name)
+ self.log.debug('Fixing file permissions: %s', fpath)
+ os.chmod(fpath, FILE_PERM)
+ # TODO: move out
+
+ with TemporaryDirectory(zone_path) as tempdir:
+ for uuid, attrs in self.ldap_keys[zone].items():
+ self.install_key(zone, uuid, attrs, tempdir)
+ # keys were generated in a temporary directory, swap directories
+ target_dir = "%s/keys" % zone_path
+ try:
+ shutil.rmtree(target_dir)
+ except OSError as e:
+ if e.errno != errno.ENOENT:
+ raise e
+ shutil.move(tempdir, target_dir)
+ os.chmod(target_dir, DIR_PERM)
+
+ self.notify_zone(zone)
+
+ def sync(self):
+ """Synchronize list of zones in LDAP with BIND."""
+ self.log.debug('Key metadata in LDAP: %s' % self.ldap_keys)
+ for zone in self.modified_zones:
+ self.sync_zone(zone)
+
+ self.modified_zones = set()
+
+ def diff_zl(self, s1, s2):
+ """Compute zones present in s1 but not present in s2.
+
+ Returns: List of (uuid, name) tuples with zones present only in s1."""
+ s1_extra = s1.uuids - s2.uuids
+ removed = [(uuid, name) for (uuid, name) in s1.mapping.items()
+ if uuid in s1_extra]
+ return removed