mirror of
https://salsa.debian.org/freeipa-team/freeipa.git
synced 2025-02-25 18:55:28 -06:00
Support OpenDNSSEC 2.1: new ods-signer protocol
The communication between ods-signer and the socket-activated process has changed with OpenDNSSEC 2.1. Adapt ipa-ods-exporter to support also the new protocol. The internal database was also modified. Add a wrapper calling the right code (table names hab=ve changed, as well as table columns). With OpenDNSSEC the policy also needs to be explicitely loaded after ods-enforcer-db-setup has been run, with ods-enforcer policy import The command ods-ksmutil notify must be replace with ods-enforce flush. Related: https://pagure.io/freeipa/issue/8214 Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com> Reviewed-By: Christian Heimes <cheimes@redhat.com>
This commit is contained in:
parent
b857828180
commit
8080bf7b35
@ -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()
|
||||
|
@ -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)
|
||||
|
||||
|
45
ipaserver/dnssec/_ods14.py
Normal file
45
ipaserver/dnssec/_ods14.py
Normal file
@ -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()
|
67
ipaserver/dnssec/_ods21.py
Normal file
67
ipaserver/dnssec/_ods21.py
Normal file
@ -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()
|
52
ipaserver/dnssec/_odsbase.py
Normal file
52
ipaserver/dnssec/_odsbase.py
Normal file
@ -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."""
|
@ -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)]
|
||||
|
12
ipaserver/dnssec/opendnssec.py
Normal file
12
ipaserver/dnssec/opendnssec.py
Normal file
@ -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
|
@ -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():
|
||||
|
Loading…
Reference in New Issue
Block a user