DNSSEC: allow to disable/replace DNSSEC key master

This commit allows to replace or disable DNSSEC key master

Replacing DNSSEC master requires to copy kasp.db file manually by user

ipa-dns-install:
--disable-dnssec-master  DNSSEC master will be disabled
--dnssec-master --kasp-db=FILE  This configure new DNSSEC master server,  kasp.db from old server is required for sucessful replacement
--force Skip checks

https://fedorahosted.org/freeipa/ticket/4657

Reviewed-By: Petr Spacek <pspacek@redhat.com>
This commit is contained in:
Martin Basti 2015-05-13 14:45:32 +02:00 committed by Tomas Babej
parent b258bcee83
commit e151492560
7 changed files with 309 additions and 22 deletions

View File

@ -61,6 +61,15 @@ def parse_options():
help="DNS zone manager e-mail address. Defaults to hostmaster@DOMAIN")
parser.add_option("-U", "--unattended", dest="unattended", action="store_true",
default=False, help="unattended installation never prompts the user")
parser.add_option("--disable-dnssec-master", dest="disable_dnssec_master",
action="store_true", default=False, help="Disable the "
"DNSSEC master on this server")
parser.add_option("--kasp-db", dest="kasp_db_file", type="string",
metavar="FILE", action="store", help="Copy OpenDNSSEC "
"metadata from the specified file (will not create a new "
"kasp.db file)")
parser.add_option("--force", dest="force", action="store_true",
help="Force install")
options, args = parser.parse_args()
safe_options = parser.get_safe_opts(options)
@ -74,6 +83,9 @@ def parse_options():
if not options.forwarders and not options.no_forwarders:
parser.error("You must specify at least one --forwarder option or --no-forwarders option")
if options.kasp_db_file and not ipautil.file_exists(options.kasp_db_file):
parser.error("File %s does not exist" % options.kasp_db_file)
if options.dm_password:
print ("WARNING: Option -p/--ds-password is deprecated "
"and should not be used anymore.")

View File

@ -90,6 +90,7 @@ class BasePathNamespace(object):
ETC_OPENDNSSEC_DIR = "/etc/opendnssec"
OPENDNSSEC_CONF_FILE = "/etc/opendnssec/conf.xml"
OPENDNSSEC_KASP_FILE = "/etc/opendnssec/kasp.xml"
OPENDNSSEC_ZONELIST_FILE = "/etc/opendnssec/zonelist.xml"
OPENLDAP_LDAP_CONF = "/etc/openldap/ldap.conf"
PAM_LDAP_CONF = "/etc/pam_ldap.conf"
PASSWD = "/etc/passwd"
@ -276,6 +277,7 @@ class BasePathNamespace(object):
SYSRESTORE_INDEX = "/var/lib/ipa-client/sysrestore/sysrestore.index"
IPA_BACKUP_DIR = "/var/lib/ipa/backup"
IPA_DNSSEC_DIR = "/var/lib/ipa/dnssec"
IPA_KASP_DB_BACKUP = "/var/lib/ipa/ipa-kasp.db.backup"
DNSSEC_TOKENS_DIR = "/var/lib/ipa/dnssec/tokens"
DNSSEC_SOFTHSM_PIN = "/var/lib/ipa/dnssec/softhsm_pin"
IPA_CA_CSR = "/var/lib/ipa/ca.csr"

View File

@ -4,10 +4,15 @@
import sys
from subprocess import CalledProcessError
from ipalib import api
from ipalib import errors
from ipaplatform.paths import paths
from ipaplatform import services
from ipapython import ipautil
from ipapython import sysrestore
from ipapython.dn import DN
from ipapython.ipa_log_manager import root_logger
from ipapython.ipaldap import AUTOBIND_ENABLED
from ipapython.ipautil import user_input
@ -23,6 +28,67 @@ ip_addresses = []
dns_forwarders = []
reverse_zones = []
NEW_MASTER_MARK = 'NEW_DNSSEC_MASTER'
def _find_dnssec_enabled_zones(conn):
search_kw = {'idnssecinlinesigning': True}
dnssec_enabled_filter = conn.make_filter(search_kw)
dn = DN('cn=dns', api.env.basedn)
try:
entries, truncated = conn.find_entries(
base_dn=dn, filter=dnssec_enabled_filter, attrs_list=['idnsname'])
except errors.NotFound:
return []
else:
return [entry.single_value['idnsname'] for entry in entries
if 'idnsname' in entry]
def _is_master():
# test if server is DNSSEC key master
masters = opendnssecinstance.get_dnssec_key_masters(api.Backend.ldap2)
if api.env.host not in masters:
raise RuntimeError("Current server is not DNSSEC key master")
def _disable_dnssec():
fstore = sysrestore.FileStore(paths.SYSRESTORE)
ods = opendnssecinstance.OpenDNSSECInstance(
fstore, ldapi=True, autobind=AUTOBIND_ENABLED)
ods.realm = api.env.realm
ods_exporter = odsexporterinstance.ODSExporterInstance(fstore, ldapi=True)
ods_exporter.realm = api.env.realm
# unconfigure services first
ods.uninstall() # needs keytab to flush the latest ods database
ods_exporter.uninstall()
ods.ldap_connect()
ods.ldap_disable('DNSSEC', api.env.host, api.env.basedn)
ods_exporter.ldap_connect()
ods_exporter.ldap_disable('DNSKeyExporter', api.env.host, api.env.basedn)
ods_exporter.remove_service()
ods.ldap_disconnect()
ods_exporter.ldap_disconnect()
conn = api.Backend.ldap2
dn = DN(('cn', 'DNSSEC'), ('cn', api.env.host), ('cn', 'masters'),
('cn', 'ipa'), ('cn', 'etc'), api.env.basedn)
try:
entry = conn.get_entry(dn)
except errors.NotFound:
pass
else:
ipa_config = entry.get('ipaConfigString', [])
if opendnssecinstance.KEYMASTER in ipa_config:
ipa_config.remove(opendnssecinstance.KEYMASTER)
conn.update_entry(entry)
def install_check(standalone, replica, options, hostname):
global ip_addresses
@ -41,14 +107,22 @@ def install_check(standalone, replica, options, hostname):
print " * Configure ipa-ods-exporter (required by DNSSEC key master)"
print " * Configure OpenDNSSEC (required by DNSSEC key master)"
print " * Generate DNSSEC master key (required by DNSSEC key master)"
elif options.disable_dnssec_master:
print " * Unconfigure ipa-ods-exporter"
print " * Unconfigure OpenDNSSEC"
print ""
print "No new zones will be signed without DNSSEC key master IPA server."
print ""
print ("Please copy file from %s after uninstallation. This file is needed "
"on new DNSSEC key " % paths.IPA_KASP_DB_BACKUP)
print "master server"
print ""
print "NOTE: DNSSEC zone signing is not enabled by default"
print ""
if options.dnssec_master:
print "DNSSEC support is experimental!"
print ""
print "Plan carefully, current version doesn't allow you to move DNSSEC"
print "key master to different server and master cannot be uninstalled"
print "Plan carefully, replacing DNSSEC key master is not recommended"
print ""
print ""
print "To accept the default shown in brackets, press the Enter key."
@ -59,22 +133,79 @@ def install_check(standalone, replica, options, hostname):
"Do you want to setup this IPA server as DNSSEC key master?",
False)):
sys.exit("Aborted")
elif (options.disable_dnssec_master and not options.unattended and not
ipautil.user_input(
"Do you want to disable current DNSSEC key master?",
False)):
sys.exit("Aborted")
# Check bind packages are installed
if not (bindinstance.check_inst(options.unattended) and
dnskeysyncinstance.check_inst()):
sys.exit("Aborting installation.")
if options.dnssec_master:
if options.disable_dnssec_master:
_is_master()
if options.disable_dnssec_master or options.dnssec_master:
dnssec_zones = _find_dnssec_enabled_zones(api.Backend.ldap2)
if options.disable_dnssec_master:
if dnssec_zones and not options.force:
raise RuntimeError(
"Cannot disable DNSSEC key master, DNSSEC signing is still "
"enabled for following zone(s): %s\n"
"Use --force option to skip this check." %
", ".join([str(zone) for zone in dnssec_zones]))
elif options.dnssec_master:
# check opendnssec packages are installed
if not opendnssecinstance.check_inst():
sys.exit("Aborting installation")
if options.kasp_db_file:
dnskeysyncd = services.service('ipa-dnskeysyncd')
if not dnskeysyncd.is_installed():
raise RuntimeError("ipa-dnskeysyncd is not configured on this "
"server, you cannot reuse OpenDNSSEC "
"database (kasp.db file)")
# check if replica can be the DNSSEC master
named = services.knownservices.named
ods_enforcerd = services.knownservices.ods_enforcerd
cmd = [paths.IPA_DNSKEYSYNCD_REPLICA]
environment = {
"SOFTHSM2_CONF": paths.DNSSEC_SOFTHSM2_CONF,
}
# stop dnskeysyncd before test
dnskeysyncd_running = dnskeysyncd.is_running()
dnskeysyncd.stop()
try:
ipautil.run(cmd, env=environment,
runas=ods_enforcerd.get_user_name(),
suplementary_groups=[named.get_group_name()])
except CalledProcessError as e:
root_logger.debug("%s", e)
raise RuntimeError("IPA server cannot be the new DNSSEC master "
"(some keys are missing)")
finally:
if dnskeysyncd_running:
dnskeysyncd.start()
elif dnssec_zones and not options.force:
# some zones have --dnssec=true, make sure a user really want to
# install new database
raise RuntimeError(
"DNSSEC is enabled for following zone(s): %s\n"
"Please use option --kasp-db to keep current OpenDNSSEC "
"database or use --force option to skip this check." %
", ".join([str(zone) for zone in dnssec_zones]))
fstore = sysrestore.FileStore(paths.SYSRESTORE)
if options.dnssec_master:
ods = opendnssecinstance.OpenDNSSECInstance(
fstore, ldapi=True, autobind=AUTOBIND_ENABLED)
fstore, ldapi=True)
ods.realm = api.env.realm
dnssec_masters = ods.get_masters()
# we can reinstall current server if it is dnssec master
@ -126,6 +257,11 @@ def install(standalone, replica, options):
global dns_forwarders
global reverse_zones
local_dnskeysyncd_dn = DN(('cn', 'DNSKeySync'), ('cn', api.env.host),
('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'),
api.env.basedn)
conn = api.Backend.ldap2
fstore = sysrestore.FileStore(paths.SYSRESTORE)
conf_ntp = ntpinstance.NTPInstance(fstore).is_enabled()
@ -149,13 +285,15 @@ def install(standalone, replica, options):
dnskeysyncd = dnskeysyncinstance.DNSKeySyncInstance(fstore, ldapi=True)
dnskeysyncd.create_instance(api.env.host, api.env.realm)
if options.dnssec_master:
ods = opendnssecinstance.OpenDNSSECInstance(fstore, ldapi=True,
autobind=AUTOBIND_ENABLED)
ods = opendnssecinstance.OpenDNSSECInstance(fstore, ldapi=True)
ods_exporter = odsexporterinstance.ODSExporterInstance(
fstore, ldapi=True, autobind=AUTOBIND_ENABLED)
fstore, ldapi=True)
ods_exporter.create_instance(api.env.host, api.env.realm)
ods.create_instance(api.env.host, api.env.realm)
ods.create_instance(api.env.host, api.env.realm,
kasp_db_file=options.kasp_db_file)
elif options.disable_dnssec_master:
_disable_dnssec()
dnskeysyncd.start_dnskeysyncd()
bind.start_named()

View File

@ -15,12 +15,12 @@ from ipapython.dn import DN
from ipapython import sysrestore, ipautil, ipaldap
from ipaplatform.paths import paths
from ipaplatform import services
from ipalib import errors
from ipalib import errors, api
class ODSExporterInstance(service.Service):
def __init__(self, fstore=None, dm_password=None, ldapi=False,
start_tls=False, autobind=ipaldap.AUTOBIND_DISABLED):
start_tls=False, autobind=ipaldap.AUTOBIND_ENABLED):
service.Service.__init__(
self, "ipa-ods-exporter",
service_desc="IPA OpenDNSSEC exporter daemon",
@ -150,6 +150,14 @@ class ODSExporterInstance(service.Service):
def __start(self):
self.start()
def remove_service(self):
dns_exporter_principal = ("ipa-ods-exporter/%s@%s" % (self.fqdn,
self.realm))
try:
api.Command.service_del(dns_exporter_principal)
except errors.NotFound:
pass
def uninstall(self):
if not self.is_configured():
return

View File

@ -9,6 +9,9 @@ import os
import pwd
import grp
import stat
import shutil
from subprocess import CalledProcessError
import _ipap11helper
@ -31,7 +34,7 @@ def get_dnssec_key_masters(conn):
"""
assert conn is not None
dn = DN(('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'), api.env.basedn)
dn = DN(api.env.container_masters, api.env.basedn)
filter_attrs = {
u'cn': u'DNSSEC',
@ -62,7 +65,7 @@ def check_inst():
class OpenDNSSECInstance(service.Service):
def __init__(self, fstore=None, dm_password=None, ldapi=False,
start_tls=False, autobind=ipaldap.AUTOBIND_DISABLED):
start_tls=False, autobind=ipaldap.AUTOBIND_ENABLED):
service.Service.__init__(
self, "ods-enforcerd",
service_desc="OpenDNSSEC enforcer daemon",
@ -94,12 +97,14 @@ class OpenDNSSECInstance(service.Service):
self.ldap_connect()
return get_dnssec_key_masters(self.admin_conn)
def create_instance(self, fqdn, realm_name, generate_master_key=True):
def create_instance(self, fqdn, realm_name, generate_master_key=True,
kasp_db_file=None):
self.backup_state("enabled", self.is_enabled())
self.backup_state("running", self.is_running())
self.fqdn = fqdn
self.realm = realm_name
self.suffix = ipautil.realm_to_suffix(self.realm)
self.kasp_db_file = kasp_db_file
try:
self.stop()
@ -152,6 +157,21 @@ class OpenDNSSECInstance(service.Service):
except errors.DuplicateEntry:
root_logger.error("DNSSEC service already exists")
# add the KEYMASTER identifier into ipaConfigString
# this is needed for the re-enabled DNSSEC master
dn = DN(('cn', 'DNSSEC'), ('cn', self.fqdn), api.env.container_masters,
api.env.basedn)
try:
entry = self.admin_conn.get_entry(dn, ['ipaConfigString'])
except errors.NotFound as e:
root_logger.error(
"DNSSEC service entry not found in the LDAP (%s)", e)
else:
config = entry.setdefault('ipaConfigString', [])
if KEYMASTER not in config:
config.append(KEYMASTER)
self.admin_conn.update_entry(entry)
def __setup_conf_files(self):
if not self.fstore.has_file(paths.OPENDNSSEC_CONF_FILE):
self.fstore.backup_file(paths.OPENDNSSEC_CONF_FILE)
@ -250,7 +270,7 @@ class OpenDNSSECInstance(service.Service):
def __setup_dnssec(self):
# run once only
if self.get_state("KASP_DB_configured"):
if self.get_state("KASP_DB_configured") and not self.kasp_db_file:
root_logger.debug("Already configured, skipping step")
return
@ -259,13 +279,33 @@ class OpenDNSSECInstance(service.Service):
if not self.fstore.has_file(paths.OPENDNSSEC_KASP_DB):
self.fstore.backup_file(paths.OPENDNSSEC_KASP_DB)
command = [
paths.ODS_KSMUTIL,
'setup'
]
if self.kasp_db_file:
# copy user specified kasp.db to proper location and set proper
# privileges
shutil.copy(self.kasp_db_file, paths.OPENDNSSEC_KASP_DB)
os.chown(paths.OPENDNSSEC_KASP_DB, self.ods_uid, self.ods_gid)
os.chmod(paths.OPENDNSSEC_KASP_DB, 0660)
ods_enforcerd = services.knownservices.ods_enforcerd
ipautil.run(command, stdin="y", runas=ods_enforcerd.get_user_name())
# regenerate zonelist.xml
ods_enforcerd = services.knownservices.ods_enforcerd
cmd = [paths.ODS_KSMUTIL, 'zonelist', 'export']
stdout, stderr, retcode = ipautil.run(cmd,
runas=ods_enforcerd.get_user_name())
with open(paths.OPENDNSSEC_ZONELIST_FILE, 'w') as zonelistf:
zonelistf.write(stdout)
os.chown(paths.OPENDNSSEC_ZONELIST_FILE,
self.ods_uid, self.ods_gid)
os.chmod(paths.OPENDNSSEC_ZONELIST_FILE, 0660)
else:
# initialize new kasp.db
command = [
paths.ODS_KSMUTIL,
'setup'
]
ods_enforcerd = services.knownservices.ods_enforcerd
ipautil.run(command, stdin="y", runas=ods_enforcerd.get_user_name())
def __setup_dnskeysyncd(self):
# set up dnskeysyncd this is DNSSEC master
@ -286,6 +326,44 @@ class OpenDNSSECInstance(service.Service):
running = self.restore_state("running")
enabled = self.restore_state("enabled")
# stop DNSSEC services before backing up kasp.db
try:
self.stop()
except Exception:
pass
ods_exporter = services.service('ipa-ods-exporter')
try:
ods_exporter.stop()
except Exception:
pass
# remove directive from ipa-dnskeysyncd, this server is not DNSSEC
# master anymore
installutils.set_directive(paths.SYSCONFIG_IPA_DNSKEYSYNCD,
'ISMASTER', None,
quotes=False, separator='=')
if ipautil.file_exists(paths.OPENDNSSEC_KASP_DB):
# force to export data
ods_enforcerd = services.knownservices.ods_enforcerd
cmd = [paths.IPA_ODS_EXPORTER, 'ipa-full-update']
try:
ipautil.run(cmd, runas=ods_enforcerd.get_user_name())
except CalledProcessError:
root_logger.debug("OpenDNSSEC database has not been updated")
try:
shutil.copy(paths.OPENDNSSEC_KASP_DB,
paths.IPA_KASP_DB_BACKUP)
except IOError as e:
root_logger.error(
"Unable to backup OpenDNSSEC database: %s", e)
else:
root_logger.info("OpenDNSSEC database backed up in %s",
paths.IPA_KASP_DB_BACKUP)
for f in [paths.OPENDNSSEC_CONF_FILE, paths.OPENDNSSEC_KASP_FILE,
paths.OPENDNSSEC_KASP_DB, paths.SYSCONFIG_ODS]:
try:

View File

@ -1326,6 +1326,25 @@ class ServerDNS(common.Installable, core.Group, core.Composite):
description="Setup server to be DNSSEC key master",
)
disable_dnssec_master = Knob(
bool, False,
initializable=False,
description="Disable the DNSSEC master on this server",
)
kasp_db_file = Knob(
str, None,
initializable=False,
description="Copy OpenDNSSEC metadata from the specified file (will "
"not create a new kasp.db file)",
)
force = Knob(
bool, False,
initializable=False,
description="Force install",
)
zonemgr = Knob(
str, None,
description=("DNS zone manager e-mail address. Defaults to "
@ -1614,7 +1633,6 @@ class Server(common.Installable, common.Interactive, core.Composite):
self.ca_cert_files = self.ca.ca_cert_files
self.subject = self.ca.subject
self.ca_signing_algorithm = self.ca.ca_signing_algorithm
self.setup_dns = self.dns.setup_dns
self.forwarders = self.dns.forwarders
self.no_forwarders = self.dns.no_forwarders
@ -1622,6 +1640,9 @@ class Server(common.Installable, common.Interactive, core.Composite):
self.no_reverse = self.dns.no_reverse
self.no_dnssec_validation = self.dns.no_dnssec_validation
self.dnssec_master = self.dns.dnssec_master
self.disable_dnssec_master = self.dns.disable_dnssec_master
self.kasp_db_file = self.dns.kasp_db_file
self.force = self.dns.force
self.zonemgr = self.dns.zonemgr
self.no_host_dns = self.dns.no_host_dns
self.no_dns_sshfp = self.dns.no_dns_sshfp

View File

@ -691,6 +691,31 @@ class ReplicaDNS(common.Installable, core.Group, core.Composite):
description="Disable DNSSEC validation",
)
dnssec_master = Knob(
bool, False,
initializable=False,
description="Setup server to be DNSSEC key master",
)
disable_dnssec_master = Knob(
bool, False,
initializable=False,
description="Disable the DNSSEC master on this server",
)
force = Knob(
bool, False,
initializable=False,
description="Force install",
)
kasp_db_file = Knob(
str, None,
initializable=False,
description="Copy OpenDNSSEC metadata from the specified file (will "
"not create a new kasp.db file)",
)
no_host_dns = Knob(
bool, False,
description="Do not use DNS for hostname lookup during installation",
@ -839,7 +864,10 @@ class Replica(common.Installable, common.Interactive, core.Composite):
self.reverse_zones = self.dns.reverse_zones
self.no_reverse = self.dns.no_reverse
self.no_dnssec_validation = self.dns.no_dnssec_validation
self.dnssec_master = False
self.dnssec_master = self.dns.dnssec_master
self.disable_dnssec_master = self.dns.disable_dnssec_master
self.kasp_db_file = self.dns.kasp_db_file
self.force = self.dns.force
self.zonemgr = None
self.no_host_dns = self.dns.no_host_dns
self.no_dns_sshfp = self.dns.no_dns_sshfp