2014-10-19 10:04:40 -05:00
|
|
|
#
|
|
|
|
# Copyright (C) 2014 FreeIPA Contributors see COPYING for license
|
|
|
|
#
|
|
|
|
|
2018-04-05 02:21:16 -05:00
|
|
|
from __future__ import absolute_import
|
|
|
|
|
2014-10-19 10:04:40 -05:00
|
|
|
from datetime import datetime
|
2017-05-23 11:35:57 -05:00
|
|
|
import logging
|
|
|
|
|
2014-10-19 10:04:40 -05:00
|
|
|
import dns.name
|
|
|
|
import errno
|
|
|
|
import os
|
|
|
|
import shutil
|
|
|
|
import stat
|
|
|
|
|
2017-07-04 09:16:11 -05:00
|
|
|
import six
|
|
|
|
|
2014-10-19 10:04:40 -05:00
|
|
|
import ipalib.constants
|
|
|
|
from ipapython.dn import DN
|
2017-05-23 11:35:57 -05:00
|
|
|
from ipapython import ipautil
|
2014-10-19 10:04:40 -05:00
|
|
|
from ipaplatform.paths import paths
|
|
|
|
|
2016-11-22 10:55:10 -06:00
|
|
|
from ipaserver.dnssec.temp import TemporaryDirectory
|
2014-10-19 10:04:40 -05:00
|
|
|
|
2017-05-23 11:35:57 -05:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
2014-10-19 10:04:40 -05:00
|
|
|
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)
|
|
|
|
|
2018-09-26 04:59:50 -05:00
|
|
|
|
|
|
|
class BINDMgr:
|
2014-10-19 10:04:40 -05:00
|
|
|
"""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.ldap_keys = {}
|
|
|
|
self.modified_zones = set()
|
|
|
|
|
|
|
|
def notify_zone(self, zone):
|
|
|
|
cmd = ['rndc', 'sign', zone.to_text()]
|
2015-11-25 10:17:18 -06:00
|
|
|
result = ipautil.run(cmd, capture_output=True)
|
2017-05-23 11:35:57 -05:00
|
|
|
logger.info('%s', result.output_log)
|
2014-10-19 10:04:40 -05:00
|
|
|
|
|
|
|
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):
|
2017-08-23 07:28:49 -05:00
|
|
|
if isinstance(str_val, bytes):
|
|
|
|
str_val = str_val.decode('utf-8')
|
2017-12-15 09:33:50 -06:00
|
|
|
dt = datetime.strptime(
|
|
|
|
str_val,
|
|
|
|
ipalib.constants.LDAP_GENERALIZED_TIME_FORMAT
|
|
|
|
)
|
2017-08-23 07:28:49 -05:00
|
|
|
return dt.strftime(time_bindfmt).encode('utf-8')
|
2014-10-19 10:04:40 -05:00
|
|
|
|
|
|
|
def dates2params(self, ldap_attrs):
|
2015-11-26 08:19:03 -06:00
|
|
|
"""Convert LDAP timestamps to list of parameters suitable
|
|
|
|
for dnssec-keyfromlabel utility"""
|
2014-10-19 10:04:40 -05:00
|
|
|
attr2param = {'idnsseckeypublish': '-P',
|
|
|
|
'idnsseckeyactivate': '-A',
|
|
|
|
'idnsseckeyinactive': '-I',
|
|
|
|
'idnsseckeydelete': '-D'}
|
|
|
|
|
|
|
|
params = []
|
|
|
|
for attr, param in attr2param.items():
|
2015-11-26 08:19:03 -06:00
|
|
|
params.append(param)
|
2014-10-19 10:04:40 -05:00
|
|
|
if attr in ldap_attrs:
|
|
|
|
assert len(ldap_attrs[attr]) == 1, 'Timestamp %s is expected to be single-valued' % attr
|
|
|
|
params.append(self.time_ldap2bindfmt(ldap_attrs[attr][0]))
|
2015-11-26 08:19:03 -06:00
|
|
|
else:
|
|
|
|
params.append('none')
|
2014-10-19 10:04:40 -05:00
|
|
|
|
|
|
|
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."""
|
2018-07-11 15:30:12 -05:00
|
|
|
assert op in ('add', 'del', 'mod')
|
2014-10-19 10:04:40 -05:00
|
|
|
zone = self.dn2zone_name(attrs['dn'])
|
|
|
|
self.modified_zones.add(zone)
|
|
|
|
zone_keys = self.ldap_keys.setdefault(zone, {})
|
|
|
|
if op == 'add':
|
2017-05-23 11:35:57 -05:00
|
|
|
logger.info('Key metadata %s added to zone %s',
|
|
|
|
attrs['dn'], zone)
|
2014-10-19 10:04:40 -05:00
|
|
|
zone_keys[uuid] = attrs
|
|
|
|
|
|
|
|
elif op == 'del':
|
2017-05-23 11:35:57 -05:00
|
|
|
logger.info('Key metadata %s deleted from zone %s',
|
|
|
|
attrs['dn'], zone)
|
2014-10-19 10:04:40 -05:00
|
|
|
zone_keys.pop(uuid)
|
|
|
|
|
|
|
|
elif op == 'mod':
|
2017-05-23 11:35:57 -05:00
|
|
|
logger.info('Key metadata %s updated in zone %s',
|
|
|
|
attrs['dn'], zone)
|
2014-10-19 10:04:40 -05:00
|
|
|
zone_keys[uuid] = attrs
|
|
|
|
|
|
|
|
def install_key(self, zone, uuid, attrs, workdir):
|
|
|
|
"""Run dnssec-keyfromlabel on given LDAP object.
|
2017-11-15 08:46:33 -06:00
|
|
|
:returns: base file name of output files, e.g. Kaaa.test.+008+19719
|
|
|
|
"""
|
2017-05-23 11:35:57 -05:00
|
|
|
logger.info('attrs: %s', attrs)
|
2017-08-23 07:28:49 -05:00
|
|
|
assert attrs.get('idnsseckeyzone', [b'FALSE'])[0] == b'TRUE', \
|
|
|
|
b'object %s is not a DNS zone key' % attrs['dn']
|
2014-10-19 10:04:40 -05:00
|
|
|
|
2017-11-15 08:46:33 -06:00
|
|
|
uri = b"%s;pin-source=%s" % (
|
|
|
|
attrs['idnsSecKeyRef'][0],
|
|
|
|
paths.DNSSEC_SOFTHSM_PIN.encode('utf-8')
|
|
|
|
)
|
|
|
|
cmd = [
|
|
|
|
paths.DNSSEC_KEYFROMLABEL,
|
|
|
|
'-K', workdir,
|
|
|
|
'-a', attrs['idnsSecAlgorithm'][0],
|
|
|
|
'-l', uri
|
|
|
|
]
|
|
|
|
cmd.extend(self.dates2params(attrs))
|
2017-08-23 07:28:49 -05:00
|
|
|
if attrs.get('idnsSecKeySep', [b'FALSE'])[0].upper() == b'TRUE':
|
2017-11-15 08:46:33 -06:00
|
|
|
cmd.extend(['-f', 'KSK'])
|
2017-08-23 07:28:49 -05:00
|
|
|
if attrs.get('idnsSecKeyRevoke', [b'FALSE'])[0].upper() == b'TRUE':
|
2017-11-15 08:46:33 -06:00
|
|
|
cmd.extend(['-R', datetime.now().strftime(time_bindfmt)])
|
2014-10-19 10:04:40 -05:00
|
|
|
cmd.append(zone.to_text())
|
|
|
|
|
|
|
|
# keys has to be readable by ODS & named
|
2015-11-25 10:17:18 -06:00
|
|
|
result = ipautil.run(cmd, capture_output=True)
|
|
|
|
basename = result.output.strip()
|
2014-10-19 10:04:40 -05:00
|
|
|
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'])
|
|
|
|
|
2014-10-23 07:13:38 -05:00
|
|
|
def get_zone_dir_name(self, zone):
|
|
|
|
"""Escape zone name to form suitable for file-system.
|
|
|
|
|
|
|
|
This method has to be equivalent to zr_get_zone_path()
|
|
|
|
in bind-dyndb-ldap/zone_register.c."""
|
|
|
|
|
|
|
|
if zone == dns.name.root:
|
|
|
|
return "@"
|
|
|
|
|
|
|
|
# strip final (empty) label
|
|
|
|
zone = zone.relativize(dns.name.root)
|
2017-11-15 08:46:33 -06:00
|
|
|
escaped = []
|
2014-10-23 07:13:38 -05:00
|
|
|
for label in zone:
|
|
|
|
for char in label:
|
2017-11-15 08:46:33 -06:00
|
|
|
if six.PY3:
|
|
|
|
char = chr(char)
|
|
|
|
if char.isalnum() or char in "-_":
|
|
|
|
escaped.append(char.lower())
|
2014-10-23 07:13:38 -05:00
|
|
|
else:
|
2017-11-15 08:46:33 -06:00
|
|
|
escaped.append("%%%02X" % ord(char))
|
|
|
|
escaped.append('.')
|
2014-10-23 07:13:38 -05:00
|
|
|
|
|
|
|
# strip trailing period
|
2017-11-15 08:46:33 -06:00
|
|
|
return ''.join(escaped[:-1])
|
2014-10-23 07:13:38 -05:00
|
|
|
|
2014-10-19 10:04:40 -05:00
|
|
|
def sync_zone(self, zone):
|
2017-05-23 11:35:57 -05:00
|
|
|
logger.info('Synchronizing zone %s', zone)
|
2014-10-19 10:04:40 -05:00
|
|
|
zone_path = os.path.join(paths.BIND_LDAP_DNS_ZONE_WORKDIR,
|
2014-10-23 07:13:38 -05:00
|
|
|
self.get_zone_dir_name(zone))
|
2014-10-19 10:04:40 -05:00
|
|
|
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)
|
2017-05-23 11:35:57 -05:00
|
|
|
logger.debug('Fixing directory permissions: %s', fpath)
|
2014-10-19 10:04:40 -05:00
|
|
|
os.chmod(fpath, DIR_PERM | stat.S_ISGID)
|
|
|
|
for name in files:
|
|
|
|
fpath = os.path.join(prefix, name)
|
2017-05-23 11:35:57 -05:00
|
|
|
logger.debug('Fixing file permissions: %s', fpath)
|
2014-10-19 10:04:40 -05:00
|
|
|
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)
|
|
|
|
|
2015-12-20 11:36:48 -06:00
|
|
|
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.
|
|
|
|
"""
|
2017-05-23 11:35:57 -05:00
|
|
|
logger.debug('Key metadata in LDAP: %s', self.ldap_keys)
|
|
|
|
logger.debug('Zones modified but skipped during bindmgr.sync: %s',
|
|
|
|
self.modified_zones - dnssec_zones)
|
2015-12-20 11:36:48 -06:00
|
|
|
for zone in self.modified_zones.intersection(dnssec_zones):
|
2014-10-19 10:04:40 -05:00
|
|
|
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
|