diff --git a/daemons/dnssec/ipa-ods-exporter.in b/daemons/dnssec/ipa-ods-exporter.in index 7c0553999..dd8606221 100644 --- a/daemons/dnssec/ipa-ods-exporter.in +++ b/daemons/dnssec/ipa-ods-exporter.in @@ -22,7 +22,6 @@ import os import socket import select import sys -import sqlite3 import traceback import dateutil.tz @@ -42,6 +41,8 @@ from ipaserver.dnssec.abshsm import sync_pkcs11_metadata, wrappingmech_name2id from ipaserver.dnssec.ldapkeydb import LdapKeyDB, str_hexlify from ipaserver.dnssec.localhsm import LocalHSM +from ipaserver.dnssec import opendnssec + logger = logging.getLogger(os.path.basename(__file__)) DAEMONNAME = 'ipa-ods-exporter' @@ -233,26 +234,19 @@ def get_ldap_keys(ldap, zone_dn): 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() + rows = db.get_zone_id(zone_name) if len(rows) != 1: raise ValueError("exactly one DNS zone should exist in ODS DB") - zone_id = rows[0][0] + zone_id = rows[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,)) + rows = db.get_keys_for_zone(zone_id) keys = {} - for row in cur: + for row in rows: key_data = sql2ldap_flags(row['keytype']) if key_data.get('idnsSecKeyZONE') != 'TRUE': raise ValueError("unexpected key type 0x%x" % row['keytype']) @@ -483,11 +477,13 @@ def receive_systemd_command(): sys.exit(1) logger.debug('accepting new connection') - conn, _addr = sck.accept() + conn_tmp, _addr = sck.accept() + conn = opendnssec.ODSSignerConn(conn_tmp) logger.debug('accepted new connection %s', repr(conn)) # this implements cmdhandler_handle_cmd() logic - cmd = conn.recv(ODS_SE_MAXLINE).strip() + cmd = conn.read_cmd() + # ODS uses an ASCII protocol, the rest of the code expects str if six.PY3: cmd = cmd.decode('ascii') @@ -548,9 +544,7 @@ def send_systemd_reply(conn, reply): # 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() + conn.send_reply_and_close(reply) def cmd2ods_zone_name(cmd): # ODS stores zone name without trailing period @@ -566,7 +560,11 @@ def sync_zone(ldap, dns_dn, zone_name): 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) + try: + ods_keys = get_ods_keys(zone_name) + except ValueError as e: + logger.error(str(e)) + return ods_keys_id = set(ods_keys.keys()) ldap_zone = get_ldap_zone(ldap, dns_dn, zone_name) @@ -724,6 +722,14 @@ except KeyError as e: cmd = sys.argv[1] exitcode, msg, zone_name, cmd = parse_command(cmd) +if exitcode: + logger.debug("parse_command returned exitcode: %d", exitcode) +if msg: + logger.debug("parse_command returned msg: %s", msg) +if zone_name: + logger.debug("parse_command returned zone_name: %s", zone_name) +if cmd: + logger.debug("parse_command returned cmd: %s", cmd) if exitcode is not None: if conn: @@ -747,9 +753,7 @@ try: # 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') + db = opendnssec.ODSDBConnection() if zone_name is not None: # only one zone should be processed @@ -759,8 +763,8 @@ try: 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']) + for zone_name in db.get_zones(): + sync_zone(ldap, dns_dn, zone_name) ### DNSSEC master: DNSSEC key material purging # references to old key material were removed above in sync_zone() diff --git a/ipaplatform/base/tasks.py b/ipaplatform/base/tasks.py index d36039aa2..6a4ec71dc 100644 --- a/ipaplatform/base/tasks.py +++ b/ipaplatform/base/tasks.py @@ -298,6 +298,36 @@ class BaseTaskNamespace: cmd = [paths.ODS_ENFORCER_DB_SETUP] return ipautil.run(cmd, stdin="y", runas=constants.ODS_USER) + def run_ods_notify(self, **kwargs): + """Notify ods-enforcerd to reload its conf.""" + if paths.ODS_KSMUTIL is not None and os.path.exists(paths.ODS_KSMUTIL): + # OpenDNSSEC 1.4 + cmd = [paths.ODS_KSMUTIL, 'notify'] + else: + # OpenDNSSEC 2.x + cmd = [paths.ODS_ENFORCER, 'flush'] + + # run commands as ODS user + if os.geteuid() == 0: + kwargs['runas'] = constants.ODS_USER + + return ipautil.run(cmd, **kwargs) + + def run_ods_policy_import(self, **kwargs): + """Run OpenDNSSEC manager command to import policy.""" + # This step is needed with OpenDNSSEC 2.1 only + if paths.ODS_KSMUTIL is not None and os.path.exists(paths.ODS_KSMUTIL): + # OpenDNSSEC 1.4 + return + + # OpenDNSSEC 2.x + cmd = [paths.ODS_ENFORCER, 'policy', 'import'] + + # run commands as ODS user + if os.geteuid() == 0: + kwargs['runas'] = constants.ODS_USER + ipautil.run(cmd, **kwargs) + def run_ods_manager(self, params, **kwargs): """Run OpenDNSSEC manager command (ksmutil, enforcer) diff --git a/ipaserver/dnssec/_ods14.py b/ipaserver/dnssec/_ods14.py new file mode 100644 index 000000000..4382ad8a0 --- /dev/null +++ b/ipaserver/dnssec/_ods14.py @@ -0,0 +1,45 @@ +# +# Copyright (C) 2020 FreeIPA Contributors see COPYING for license +# + +import socket + +from ipaserver.dnssec._odsbase import AbstractODSDBConnection +from ipaserver.dnssec._odsbase import AbstractODSSignerConn +from ipaserver.dnssec._odsbase import ODS_SE_MAXLINE + + +class ODSDBConnection(AbstractODSDBConnection): + def get_zones(self): + cur = self._db.execute("SELECT name from zones") + rows = cur.fetchall() + return [row['name'] for row in rows] + + def get_zone_id(self, zone_name): + cur = self._db.execute( + "SELECT id FROM zones WHERE LOWER(name)=LOWER(?)", + (zone_name,)) + rows = cur.fetchall() + return [row[0] for row in rows] + + def get_keys_for_zone(self, zone_id): + cur = self._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,)) + for row in cur: + yield row + + +class ODSSignerConn(AbstractODSSignerConn): + def read_cmd(self): + cmd = self._conn.recv(ODS_SE_MAXLINE).strip() + return cmd + + def send_reply_and_close(self, reply): + self._conn.send(reply + b'\n') + self._conn.shutdown(socket.SHUT_RDWR) + self._conn.close() diff --git a/ipaserver/dnssec/_ods21.py b/ipaserver/dnssec/_ods21.py new file mode 100644 index 000000000..d00f3757a --- /dev/null +++ b/ipaserver/dnssec/_ods21.py @@ -0,0 +1,67 @@ +# +# Copyright (C) 2020 FreeIPA Contributors see COPYING for license +# + +from datetime import datetime + +from ipaserver.dnssec._odsbase import AbstractODSDBConnection +from ipaserver.dnssec._odsbase import AbstractODSSignerConn +from ipaserver.dnssec._odsbase import ODS_SE_MAXLINE + +CLIENT_OPC_STDOUT = 0 +CLIENT_OPC_EXIT = 4 + + +class ODSDBConnection(AbstractODSDBConnection): + def get_zones(self): + cur = self._db.execute("SELECT name from zone") + rows = cur.fetchall() + return [row['name'] for row in rows] + + def get_zone_id(self, zone_name): + cur = self._db.execute( + "SELECT id FROM zone WHERE LOWER(name)=LOWER(?)", + (zone_name,)) + rows = cur.fetchall() + return [row[0] for row in rows] + + def get_keys_for_zone(self, zone_id): + cur = self._db.execute( + "SELECT hsmk.locator, hsmk.inception, hsmk.algorithm, " + "hsmk.keyType, hsmk.state " + "FROM hsmKey AS hsmk " + "JOIN keyData AS kd ON hsmk.id = kd.hsmKeyId " + "WHERE kd.zoneId = ?", (zone_id,)) + for row in cur: + key = dict() + key['HSMkey_id'] = row['locator'] + key['generate'] = str(datetime.fromtimestamp(row['inception'])) + key['algorithm'] = row['algorithm'] + key['publish'] = key['generate'] + key['active'] = None + key['retire'] = None + key['dead'] = None + if row['keyType'] == 2: + key['keytype'] = 256 + elif row['keyType'] == 1: + key['keytype'] = 257 + key['state'] = row['state'] + yield key + + +class ODSSignerConn(AbstractODSSignerConn): + def read_cmd(self): + msg = self._conn.recv(ODS_SE_MAXLINE) + _opc = int(msg[0]) + msglen = int(msg[1]) << 8 + int(msg[2]) + cmd = msg[3:msglen - 1].strip() + return cmd + + def send_reply_and_close(self, reply): + prefix = bytearray([CLIENT_OPC_STDOUT, len(reply) >> 8, + len(reply) & 255]) + self._conn.sendall(prefix + reply) + # 2nd message: CLIENT_OPC_EXIT, then len, msg len, exit code + prefix = bytearray([CLIENT_OPC_EXIT, 0, 1, 0]) + self._conn.sendall(prefix) + self._conn.close() diff --git a/ipaserver/dnssec/_odsbase.py b/ipaserver/dnssec/_odsbase.py new file mode 100644 index 000000000..198ea8842 --- /dev/null +++ b/ipaserver/dnssec/_odsbase.py @@ -0,0 +1,52 @@ +# +# Copyright (C) 2020 FreeIPA Contributors see COPYING for license +# + +import six +import abc +import sqlite3 +from ipaplatform.paths import paths + +ODS_SE_MAXLINE = 1024 # from ODS common/config.h + + +@six.add_metaclass(abc.ABCMeta) +class AbstractODSDBConnection(): + """Abstract class representing the Connection to ODS database.""" + def __init__(self): + """Creates a connection to the kasp database.""" + self._db = sqlite3.connect(paths.OPENDNSSEC_KASP_DB) + self._db.row_factory = sqlite3.Row + self._db.execute('BEGIN') + + @abc.abstractmethod + def get_zones(self): + """Returns a list of zone names.""" + + @abc.abstractmethod + def get_zone_id(self, zone_name): + """Returns a list of zone ids for the given zone_name.""" + + @abc.abstractmethod + def get_keys_for_zone(self, zone_id): + """Returns a list of keys for the given zone_id.""" + + def close(self): + """Closes the connection to the kasp database.""" + self._db.close() + + +@six.add_metaclass(abc.ABCMeta) +class AbstractODSSignerConn(): + """Abstract class representing the Connection to ods-signer.""" + def __init__(self, conn): + """Initializes the object with a socket conn.""" + self._conn = conn + + @abc.abstractmethod + def read_cmd(self): + """Reads the next command on the connection.""" + + @abc.abstractmethod + def send_reply_and_close(self, reply): + """Sends the reply on the connection.""" diff --git a/ipaserver/dnssec/odsmgr.py b/ipaserver/dnssec/odsmgr.py index 9c736efd8..506265d7d 100644 --- a/ipaserver/dnssec/odsmgr.py +++ b/ipaserver/dnssec/odsmgr.py @@ -159,27 +159,48 @@ class ODSMgr: def add_ods_zone(self, uuid, name): zone_path = '%s%s' % (ENTRYUUID_PREFIX, uuid) + if name != dns.name.root: + name = name.relativize(dns.name.root) cmd = ['zone', 'add', '--zone', str(name), '--input', zone_path] - output = self.ksmutil(cmd) - logger.info('%s', output) - self.notify_enforcer() + output = None + try: + output = self.ksmutil(cmd) + except ipautil.CalledProcessError as e: + # Zone already exists in HSM + if e.returncode == 1 \ + and str(e.output).endswith(str(name) + ' already exists!'): + # Just return + return + if output is not None: + logger.info('%s', output) + self.notify_enforcer() def del_ods_zone(self, name): # ods-ksmutil blows up if zone name has period at the end - name = name.relativize(dns.name.root) + if name != dns.name.root: + name = name.relativize(dns.name.root) # detect if name is root zone if name == dns.name.empty: name = dns.name.root cmd = ['zone', 'delete', '--zone', str(name)] - output = self.ksmutil(cmd) - logger.info('%s', output) - self.notify_enforcer() - self.cleanup_signer(name) + output = None + try: + output = self.ksmutil(cmd) + except ipautil.CalledProcessError as e: + # Zone already doesn't exist in HSM + if e.returncode == 1 \ + and str(e.output).endswith(str(name) + ' not found!'): + # Just cleanup signer, no need to notify enforcer + self.cleanup_signer(name) + return + if output is not None: + logger.info('%s', output) + self.notify_enforcer() + self.cleanup_signer(name) def notify_enforcer(self): - cmd = ['notify'] - output = self.ksmutil(cmd) - logger.info('%s', output) + result = tasks.run_ods_notify(capture_output=True) + logger.info('%s', result.output) def cleanup_signer(self, zone_name): cmd = ['ods-signer', 'ldap-cleanup', str(zone_name)] diff --git a/ipaserver/dnssec/opendnssec.py b/ipaserver/dnssec/opendnssec.py new file mode 100644 index 000000000..d0b806150 --- /dev/null +++ b/ipaserver/dnssec/opendnssec.py @@ -0,0 +1,12 @@ +# +# Copyright (C) 2020 FreeIPA Contributors see COPYING for license +# + +import os +from ipaplatform.paths import paths + +# pylint: disable=unused-import +if paths.ODS_KSMUTIL is not None and os.path.exists(paths.ODS_KSMUTIL): + from ._ods14 import ODSDBConnection, ODSSignerConn +else: + from ._ods21 import ODSDBConnection, ODSSignerConn diff --git a/ipaserver/install/opendnssecinstance.py b/ipaserver/install/opendnssecinstance.py index 6354521b4..5a3cbf40b 100644 --- a/ipaserver/install/opendnssecinstance.py +++ b/ipaserver/install/opendnssecinstance.py @@ -314,6 +314,7 @@ class OpenDNSSECInstance(service.Service): def __start(self): self.restart() # needed to reload conf files + tasks.run_ods_policy_import() def uninstall(self): if not self.is_configured():