mirror of
https://salsa.debian.org/freeipa-team/freeipa.git
synced 2024-12-26 00:41:25 -06:00
a347c11650
All Python scripts are now generated from a template with a dynamic shebang. ipatests/i18n.py is no longer an executable script with shebang. The module is not executed as script directly, but rather as $(PYTHON) ipatests/i18n.py Fixes: https://pagure.io/freeipa/issue/7680 All Python scripts are now template files with a dynamic shebang line. Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com>
784 lines
29 KiB
Plaintext
784 lines
29 KiB
Plaintext
@PYTHONSHEBANG@
|
|
#
|
|
# 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.
|
|
|
|
Alternativelly, it can be called directly and a command can be supplied as
|
|
first command line argument.
|
|
|
|
Purpose of this replacement is to upload keys generated by OpenDNSSEC to LDAP.
|
|
"""
|
|
from __future__ import print_function
|
|
|
|
from datetime import datetime
|
|
import logging
|
|
import os
|
|
import socket
|
|
import select
|
|
import sys
|
|
import sqlite3
|
|
import traceback
|
|
|
|
import dateutil.tz
|
|
import dns.dnssec
|
|
from gssapi.exceptions import GSSError
|
|
import six
|
|
import systemd.daemon
|
|
import systemd.journal
|
|
|
|
import ipalib
|
|
from ipalib.constants import SOFTHSM_DNSSEC_TOKEN_LABEL
|
|
from ipalib.install.kinit import kinit_keytab
|
|
from ipapython.dn import DN
|
|
from ipapython import ipaldap
|
|
from ipaplatform.paths import paths
|
|
from ipaserver.dnssec.abshsm import sync_pkcs11_metadata, wrappingmech_name2id
|
|
from ipaserver.dnssec.ldapkeydb import LdapKeyDB, str_hexlify
|
|
from ipaserver.dnssec.localhsm import LocalHSM
|
|
|
|
logger = logging.getLogger(os.path.basename(__file__))
|
|
|
|
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')
|
|
|
|
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',
|
|
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):
|
|
"""Convert SQL date format from local time zone into UTC."""
|
|
localtz = dateutil.tz.tzlocal()
|
|
localtime = datetime.strptime(sql_time, "%Y-%m-%d %H:%M:%S").replace(
|
|
tzinfo=localtz)
|
|
utctz = dateutil.tz.gettz('UTC')
|
|
return localtime.astimezone(utctz)
|
|
|
|
def sql2datetimes(row):
|
|
row2key_map = {'generate': 'idnsSecKeyCreated',
|
|
'publish': 'idnsSecKeyPublish',
|
|
'active': 'idnsSecKeyActivate',
|
|
'retire': 'idnsSecKeyInactive',
|
|
'dead': 'idnsSecKeyDelete'}
|
|
times = {}
|
|
for column, key in row2key_map.items():
|
|
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}
|
|
|
|
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:
|
|
raise ValueError("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
|
|
|
|
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
|
|
|
|
if ldap_zone is None:
|
|
raise ipalib.errors.NotFound(
|
|
reason='DNS zone "%s" not found 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):
|
|
# get zone ID
|
|
cur = db.execute("SELECT id FROM zones WHERE LOWER(name)=LOWER(?)",
|
|
(zone_name,))
|
|
rows = cur.fetchall()
|
|
if len(rows) != 1:
|
|
raise ValueError("exactly one DNS zone should exist in ODS DB")
|
|
zone_id = rows[0][0]
|
|
|
|
# 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 = sql2ldap_flags(row['keytype'])
|
|
if key_data.get('idnsSecKeyZONE') != 'TRUE':
|
|
raise ValueError("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']),
|
|
row['HSMkey_id']
|
|
)
|
|
|
|
key_data.update(sql2ldap_keyid(row['HSMkey_id']))
|
|
keys[key_id] = key_data
|
|
logger.debug("key %s metadata: %s", key_id, key_data)
|
|
|
|
return keys
|
|
|
|
def sync_set_metadata_2ldap(name, 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."""
|
|
matching_keys = set(source_set.keys()).intersection(set(target_set.keys()))
|
|
logger.info("%s: keys in local HSM & LDAP: %s",
|
|
name, hex_set(matching_keys))
|
|
for key_id in matching_keys:
|
|
sync_pkcs11_metadata(name, source_set[key_id], target_set[key_id])
|
|
|
|
def ldap2master_replica_keys_sync(ldapkeydb, localhsm):
|
|
"""LDAP=>master's local HSM replica key synchronization"""
|
|
# import new replica keys from LDAP
|
|
logger.debug("replica pub keys in LDAP: %s",
|
|
hex_set(ldapkeydb.replica_pubkeys_wrap))
|
|
logger.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())
|
|
logger.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]
|
|
logger.debug('label=%s, id=%s, data=%s',
|
|
new_key_ldap['ipk11label'],
|
|
str_hexlify(new_key_ldap['ipk11id']),
|
|
str_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())
|
|
logger.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('ldap2master_replica',
|
|
localhsm.replica_pubkeys_wrap,
|
|
ldapkeydb.replica_pubkeys_wrap)
|
|
|
|
def master2ldap_master_keys_sync(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())
|
|
logger.debug("master keys in local HSM: %s",
|
|
hex_set(localhsm.master_keys.keys()))
|
|
logger.debug("master keys in LDAP HSM: %s",
|
|
hex_set(ldapkeydb.master_keys.keys()))
|
|
logger.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()
|
|
logger.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.items():
|
|
logger.debug('synchronizing master key metadata: 0x%s',
|
|
str_hexlify(mkey_id))
|
|
sync_pkcs11_metadata('master2ldap_master', 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())
|
|
logger.debug('enabled replica key ids: %s',
|
|
hex_set(enabled_replica_key_ids))
|
|
|
|
for mkey_id, mkey_ldap in ldapkeydb.master_keys.items():
|
|
logger.debug('processing master key data: 0x%s',
|
|
str_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.values():
|
|
label = matching_key['ipk11label']
|
|
if not label.startswith(u'dnssec-replica:'):
|
|
raise ValueError(
|
|
"Wrapped key '%s' refers to PKCS#11 URI '%s' which "
|
|
"is not a known DNSSEC replica key: label '%s' "
|
|
"does not start with 'dnssec-replica:' prefix" % (
|
|
wrapped_entry.dn,
|
|
wrapped_entry['ipaWrappingKey'],
|
|
label
|
|
)
|
|
)
|
|
used_replica_keys.add(matching_key['ipk11id'])
|
|
|
|
new_replica_keys = enabled_replica_key_ids - used_replica_keys
|
|
logger.debug('master key 0x%s is not wrapped with replica keys %s',
|
|
str_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:
|
|
logger.info('adding master key 0x%s wrapped with replica key 0x%s',
|
|
str_hexlify(mkey_id), str_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,
|
|
wrappingmech_name2id[SECRETKEY_WRAPPING_MECH])
|
|
mkey_ldap.add_wrapped_data(keydata, SECRETKEY_WRAPPING_MECH,
|
|
replica_key_id)
|
|
|
|
ldapkeydb.flush()
|
|
|
|
def master2ldap_zone_keys_sync(ldapkeydb, localhsm):
|
|
"""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()."""
|
|
keypairs_ldap = ldapkeydb.zone_keypairs
|
|
logger.debug("zone keys in LDAP: %s", hex_set(keypairs_ldap))
|
|
|
|
pubkeys_local = localhsm.zone_pubkeys
|
|
privkeys_local = localhsm.zone_privkeys
|
|
logger.debug("zone keys in local HSM: %s", hex_set(privkeys_local))
|
|
|
|
if set(pubkeys_local) != set(privkeys_local):
|
|
raise ValueError(
|
|
"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)
|
|
logger.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('master2ldap_zone_keys', pubkeys_local, keypairs_ldap)
|
|
sync_set_metadata_2ldap('master2ldap_zone_keys', privkeys_local, keypairs_ldap)
|
|
ldapkeydb.flush()
|
|
|
|
def master2ldap_zone_keys_purge(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
|
|
logger.debug("zone keys in LDAP: %s", hex_set(keypairs_ldap))
|
|
|
|
pubkeys_local = localhsm.zone_pubkeys
|
|
privkeys_local = localhsm.zone_privkeys
|
|
logger.debug("zone keys in local HSM: %s", hex_set(privkeys_local))
|
|
if set(pubkeys_local) != set(privkeys_local):
|
|
raise ValueError(
|
|
"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)
|
|
logger.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()
|
|
for i in s:
|
|
out.add("0x%s" % str_hexlify(i))
|
|
return out
|
|
|
|
|
|
def receive_systemd_command():
|
|
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)
|
|
timeout = 1 # give the socket a bit of time
|
|
rlist, _wlist, _xlist = select.select([sck], [], [], timeout)
|
|
if not rlist:
|
|
logger.critical(
|
|
'socket activation did not return a readable socket with a '
|
|
'command.'
|
|
)
|
|
sys.exit(1)
|
|
|
|
logger.debug('accepting new connection')
|
|
conn, _addr = sck.accept()
|
|
logger.debug('accepted new connection %s', repr(conn))
|
|
|
|
# this implements cmdhandler_handle_cmd() logic
|
|
cmd = conn.recv(ODS_SE_MAXLINE).strip()
|
|
# ODS uses an ASCII protocol, the rest of the code expects str
|
|
if six.PY3:
|
|
cmd = cmd.decode('ascii')
|
|
logger.debug('received command "%s" from systemd socket', cmd)
|
|
return cmd, conn
|
|
|
|
def parse_command(cmd):
|
|
"""Parse command to (exit code, message, zone_name) tuple.
|
|
|
|
Exit code None means that execution should continue.
|
|
"""
|
|
if cmd == 'ipa-hsm-update':
|
|
return (
|
|
0,
|
|
'HSM synchronization finished, skipping zone synchronization.',
|
|
None,
|
|
cmd
|
|
)
|
|
|
|
elif cmd == 'ipa-full-update':
|
|
return (
|
|
None,
|
|
'Synchronization of all zones was finished.',
|
|
None,
|
|
cmd
|
|
)
|
|
|
|
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
|
|
)
|
|
|
|
|
|
def send_systemd_reply(conn, reply):
|
|
# Reply & close connection early.
|
|
# This is necessary to let Enforcer to unlock the ODS DB.
|
|
if six.PY3:
|
|
reply = reply.encode('ascii')
|
|
conn.send(reply + b'\n')
|
|
conn.shutdown(socket.SHUT_RDWR)
|
|
conn.close()
|
|
|
|
def cmd2ods_zone_name(cmd):
|
|
# ODS stores zone name without trailing period
|
|
zone_name = cmd.split(' ', 1)[1].strip()
|
|
if len(zone_name) > 1 and zone_name[-1] == '.':
|
|
zone_name = zone_name[:-1]
|
|
|
|
return zone_name
|
|
|
|
def sync_zone(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!"""
|
|
logger.debug('%s: synchronizing zone "%s"', zone_name, zone_name)
|
|
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
|
|
logger.info('%s: new key metadata from ODS: %s', zone_name, new_keys_id)
|
|
for key_id in new_keys_id:
|
|
cn = "cn=%s" % key_id
|
|
key_dn = DN(cn, keys_dn)
|
|
logger.debug('%s: adding key metadata "%s" to LDAP', zone_name, 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
|
|
logger.info('%s: deleted key metadata in LDAP: %s',
|
|
zone_name, deleted_keys_id)
|
|
for key_id in deleted_keys_id:
|
|
cn = "cn=%s" % key_id
|
|
key_dn = DN(cn, keys_dn)
|
|
logger.debug('%s: deleting key metadata "%s" from LDAP',
|
|
zone_name, key_dn)
|
|
ldap.delete_entry(key_dn)
|
|
|
|
update_keys_id = ldap_keys_id.intersection(ods_keys_id)
|
|
logger.info('%s: key metadata in LDAP & ODS: %s',
|
|
zone_name, update_keys_id)
|
|
for key_id in update_keys_id:
|
|
ldap_key = ldap_keys[key_id]
|
|
ods_key = ods_keys[key_id]
|
|
logger.debug('%s: updating key metadata "%s" in LDAP',
|
|
zone_name, ldap_key.dn)
|
|
ldap_key.update(ods_key)
|
|
try:
|
|
ldap.update_entry(ldap_key)
|
|
except ipalib.errors.EmptyModlist:
|
|
continue
|
|
|
|
def cleanup_ldap_zone(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!"""
|
|
logger.debug('%s: cleaning up key metadata from zone "%s"',
|
|
zone_name, 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
|
|
logger.debug('%s: %s', zone_name, str(ex))
|
|
return
|
|
|
|
for ldap_key in ldap_keys:
|
|
logger.debug('%s: deleting key metadata "%s"', zone_name, ldap_key.dn)
|
|
ldap.delete_entry(ldap_key)
|
|
|
|
|
|
# this service is usually socket-activated
|
|
root_logger = logging.getLogger()
|
|
root_logger.addHandler(systemd.journal.JournalHandler())
|
|
root_logger.setLevel(level=logging.DEBUG)
|
|
|
|
if len(sys.argv) > 2:
|
|
print(__doc__)
|
|
sys.exit(1)
|
|
# program was likely invoked from console, log to it
|
|
elif len(sys.argv) == 2:
|
|
console = logging.StreamHandler()
|
|
root_logger.addHandler(console)
|
|
|
|
# IPA framework initialization
|
|
ipalib.api.bootstrap(context='dns', confdir=paths.ETC_IPA, in_server=True)
|
|
ipalib.api.finalize()
|
|
|
|
# Kerberos initialization
|
|
PRINCIPAL = str('%s/%s' % (DAEMONNAME, ipalib.api.env.host))
|
|
logger.debug('Kerberos principal: %s', PRINCIPAL)
|
|
ccache_name = paths.IPA_ODS_EXPORTER_CCACHE
|
|
|
|
try:
|
|
kinit_keytab(PRINCIPAL, paths.IPA_ODS_EXPORTER_KEYTAB, ccache_name,
|
|
attempts=5)
|
|
except GSSError as e:
|
|
logger.critical('Kerberos authentication failed: %s', e)
|
|
sys.exit(1)
|
|
|
|
os.environ['KRB5CCNAME'] = ccache_name
|
|
logger.debug('Got TGT')
|
|
|
|
# LDAP initialization
|
|
dns_dn = DN(ipalib.api.env.container_dns, ipalib.api.env.basedn)
|
|
ldap = ipaldap.LDAPClient(ipalib.api.env.ldap_uri)
|
|
logger.debug('Connecting to LDAP')
|
|
ldap.gssapi_bind()
|
|
logger.debug('Connected')
|
|
|
|
|
|
### DNSSEC master: key material upload & synchronization (but not deletion)
|
|
ldapkeydb = LdapKeyDB(ldap, DN(('cn', 'keys'),
|
|
('cn', 'sec'),
|
|
ipalib.api.env.container_dns,
|
|
ipalib.api.env.basedn))
|
|
localhsm = LocalHSM(paths.LIBSOFTHSM2_SO, SOFTHSM_DNSSEC_TOKEN_LABEL,
|
|
open(paths.DNSSEC_SOFTHSM_PIN).read())
|
|
|
|
ldap2master_replica_keys_sync(ldapkeydb, localhsm)
|
|
master2ldap_master_keys_sync(ldapkeydb, localhsm)
|
|
master2ldap_zone_keys_sync(ldapkeydb, localhsm)
|
|
|
|
|
|
### 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:
|
|
cmd, conn = receive_systemd_command()
|
|
if len(sys.argv) != 1:
|
|
logger.critical('No additional parameters are accepted when '
|
|
'socket activation is used.')
|
|
sys.exit(1)
|
|
# Handle cases where somebody ran the program without systemd.
|
|
except KeyError as e:
|
|
if len(sys.argv) != 2:
|
|
print(__doc__)
|
|
print('ERROR: Exactly one parameter or socket activation is required.')
|
|
sys.exit(1)
|
|
conn = None
|
|
cmd = sys.argv[1]
|
|
|
|
exitcode, msg, zone_name, cmd = parse_command(cmd)
|
|
|
|
if exitcode is not None:
|
|
if conn:
|
|
send_systemd_reply(conn, msg)
|
|
logger.info("%s", msg)
|
|
sys.exit(exitcode)
|
|
else:
|
|
logger.debug("%s", msg)
|
|
|
|
# Open DB directly and read key timestamps etc.
|
|
db = None
|
|
try:
|
|
# LOCK WARNING:
|
|
# ods-enforcerd is holding kasp.db.our_lock when processing all zones and
|
|
# the lock is unlocked only after all calls to ods-signer are finished,
|
|
# i.e. when ods-enforcerd receives reply from each ods-signer call.
|
|
#
|
|
# Consequently, ipa-ods-exporter (ods-signerd implementation) must not
|
|
# request kasp.db.our_lock to prevent deadlocks.
|
|
# SQLite transaction isolation should suffice.
|
|
# Beware: Reply can be sent back only after DB is unlocked and closed
|
|
# otherwise ods-enforcerd will fail.
|
|
|
|
db = sqlite3.connect(paths.OPENDNSSEC_KASP_DB)
|
|
db.row_factory = sqlite3.Row
|
|
db.execute('BEGIN')
|
|
|
|
if zone_name is not None:
|
|
# only one zone should be processed
|
|
if cmd == 'update':
|
|
sync_zone(ldap, dns_dn, zone_name)
|
|
elif cmd == 'ldap-cleanup':
|
|
cleanup_ldap_zone(ldap, dns_dn, zone_name)
|
|
else:
|
|
# process all zones
|
|
for zone_row in db.execute("SELECT name FROM zones"):
|
|
sync_zone(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(ldapkeydb, localhsm)
|
|
|
|
except Exception as ex:
|
|
msg = "ipa-ods-exporter exception: %s" % traceback.format_exc(ex)
|
|
logger.exception("%s", ex)
|
|
raise ex
|
|
|
|
finally:
|
|
try:
|
|
if db:
|
|
db.close()
|
|
finally:
|
|
if conn:
|
|
send_systemd_reply(conn, msg)
|
|
|
|
logger.debug('Done')
|