From 46ef43b2a68139c991883633137c0061f20222a7 Mon Sep 17 00:00:00 2001 From: Antonio Torres Date: Mon, 29 Apr 2024 11:36:52 +0200 Subject: [PATCH 1/4] Add DNS over TLS support Add DNS over TLS support using Unbound as a local resolver. This includes new options on both server and client side. * `--dns-over-tls`: enable DNS over TLS support. This option is present on both client and server. It deploys Unbound and configures BIND on the server to receive DoT requests. * `--dot-forwarder`: the upstream DNS server with DoT support. It must be specified in the format `1.2.3.4#dns.server.test` * `--dns-over-tls-key` and `--dns-over-tls-cert`: in case user prefers to have the DoT certificate in BIND generated by themselves. If these are empty, IPA CA is used instead to request a new certificate. Signed-off-by: Antonio Torres Reviewed-By: Francisco Trivino Reviewed-By: Alexander Bokovoy Reviewed-By: Varun Mylaraiah Reviewed-By: Pavel Brezina --- client/man/ipa-client-install.1 | 3 + client/share/Makefile.am | 1 + client/share/unbound.conf.template | 10 ++ install/share/bind.named.conf.template | 4 + install/tools/ipa-dns-install.in | 50 +++++- install/tools/man/ipa-dns-install.1 | 16 ++ install/tools/man/ipa-replica-install.1 | 16 ++ install/tools/man/ipa-server-install.1 | 16 ++ ipaclient/install/client.py | 89 +++++++++- ipaplatform/base/paths.py | 4 + ipaplatform/base/services.py | 2 +- ipapython/ipautil.py | 13 ++ ipaserver/install/bindinstance.py | 48 +++++- ipaserver/install/dns.py | 182 ++++++++++++++++++++- ipaserver/install/server/__init__.py | 60 ++++++- ipaserver/install/server/install.py | 24 ++- ipaserver/install/server/replicainstall.py | 2 + 17 files changed, 507 insertions(+), 33 deletions(-) create mode 100644 client/share/unbound.conf.template diff --git a/client/man/ipa-client-install.1 b/client/man/ipa-client-install.1 index 725b11422..e6f641254 100644 --- a/client/man/ipa-client-install.1 +++ b/client/man/ipa-client-install.1 @@ -201,6 +201,9 @@ Use \fIIP_ADDRESS\fR in DNS A/AAAA record for this host. May be specified multip .TP \fB\-\-all\-ip\-addresses\fR Create DNS A/AAAA record for each IP address on this host. +.TP +\fB\-\-dns\-over\-tls\fR +Configure DNS over TLS. .SS "SSSD OPTIONS" .TP diff --git a/client/share/Makefile.am b/client/share/Makefile.am index bf631a22f..52f3c4dd4 100644 --- a/client/share/Makefile.am +++ b/client/share/Makefile.am @@ -5,6 +5,7 @@ dist_app_DATA = \ freeipa.template \ sshd_ipa.conf.template \ ssh_ipa.conf.template \ + unbound.conf.template \ $(NULL) epnconfdir = $(IPA_SYSCONF_DIR) diff --git a/client/share/unbound.conf.template b/client/share/unbound.conf.template new file mode 100644 index 000000000..166036f65 --- /dev/null +++ b/client/share/unbound.conf.template @@ -0,0 +1,10 @@ +server: + tls-cert-bundle: $TLS_CERT_BUNDLE_PATH + tls-upstream: yes + interface: 127.0.0.55 + log-servfail: yes +forward-zone: + name: "." + forward-tls-upstream: yes + forward-first: no + $FORWARD_ADDRS diff --git a/install/share/bind.named.conf.template b/install/share/bind.named.conf.template index 01b77c5ae..b64a6a4b0 100644 --- a/install/share/bind.named.conf.template +++ b/install/share/bind.named.conf.template @@ -21,6 +21,8 @@ options { managed-keys-directory "$MANAGED_KEYS_DIR"; + $NAMED_DNS_OVER_TLS_OPTIONS_CONF + /* user customizations of options */ include "$NAMED_CUSTOM_OPTIONS_CONF"; @@ -49,6 +51,8 @@ ${NAMED_ZONE_COMMENT}}; include "$RFC1912_ZONES"; include "$ROOT_KEY"; +$NAMED_DNS_OVER_TLS_CONF + /* user customization */ include "$NAMED_CUSTOM_CONF"; diff --git a/install/tools/ipa-dns-install.in b/install/tools/ipa-dns-install.in index f1a90e7ac..0b0cd2be5 100644 --- a/install/tools/ipa-dns-install.in +++ b/install/tools/ipa-dns-install.in @@ -27,6 +27,7 @@ import sys from ipaserver.install import bindinstance from ipaserver.install import installutils +from ipaplatform import services from ipapython import version from ipalib import api from ipaplatform.paths import paths @@ -56,6 +57,24 @@ def parse_options(): parser.add_option("--auto-forwarders", dest="auto_forwarders", action="store_true", default=False, help="Use DNS forwarders configured in /etc/resolv.conf") + parser.add_option("--dns-over-tls", dest="dns_over_tls", + help="Configure DNS over TLS", default=False, + action="store_true") + parser.add_option("--dot-forwarder", dest="dot_forwarders", + action="append", + default=[], + help="Add a DNS over TLS forwarder. " + "This option can be used multiple times") + parser.add_option("--dns-over-tls-cert", dest="dns_over_tls_cert", + help="Certificate to use for DNS over TLS. " + "If empty, a new certificate will be requested " + "from IPA CA") + parser.add_option("--dns-over-tls-key", dest="dns_over_tls_key", + help="Key for certificate specified " + "in --dns-over-tls-cert") + parser.add_option("--dns-policy", dest="dns_policy", + choices=("relaxed", "enforced"), default="relaxed", + help="Policy for encrypted DNS enforcement") parser.add_option("--forward-policy", dest="forward_policy", choices=("first", "only"), default=None, help="DNS forwarding policy for global forwarders") @@ -102,12 +121,37 @@ def parse_options(): elif options.auto_reverse and options.no_reverse: parser.error("You cannot specify a --auto-reverse option together with --no-reverse") + if options.dns_policy == "enforced" and not options.dns_over_tls: + parser.error("If encrypted DNS policy is enforced, " + "--dns-over-tls must be specified.") + + unbound = services.knownservices["unbound"] + if options.dns_over_tls and not unbound.is_installed(): + parser.error( + "To enable DNS over TLS, package ipa-server-encrypted-dns " + "must be installed." + ) + + if not options.dns_over_tls and options.dot_forwarders: + parser.error("You cannot specify a " + "--dot-forwarder option without --dns-over-tls") + elif options.dns_over_tls and not options.dot_forwarders: + parser.error( + "You must specify --dot-forwarder " + "when enabling DNS over TLS") + elif bool(options.dns_over_tls_key) != bool(options.dns_over_tls_cert): + parser.error( + "You cannot specify a --dns-over-tls-key option " + "without the --dns-over-tls-cert option and vice versa") + if options.unattended: if (not options.forwarders - and not options.no_forwarders - and not options.auto_forwarders): + and not options.no_forwarders + and not options.auto_forwarders + and not options.dot_forwarders): parser.error("You must specify at least one option: " - "--forwarder or --no-forwarders or --auto-forwarders") + "--forwarder or --no-forwarders or --auto-forwarders" + " or --dot-forwarder") if options.kasp_db_file and not os.path.isfile(options.kasp_db_file): parser.error("File %s does not exist" % options.kasp_db_file) diff --git a/install/tools/man/ipa-dns-install.1 b/install/tools/man/ipa-dns-install.1 index 029001eca..6008d2028 100644 --- a/install/tools/man/ipa-dns-install.1 +++ b/install/tools/man/ipa-dns-install.1 @@ -69,6 +69,22 @@ Allow creatin of (reverse) zone even if the zone is already resolvable. Using th .TP \fB\-U\fR, \fB\-\-unattended\fR An unattended installation that will never prompt for user input +.TP +\fB\-\-dns\-over\-tls\fR +Configure DNS over TLS. +.TP +\fB\-\-dot\-forwarder\fR=\fIIP_ADDRESS#HOSTNAME\fR +Add a DNS-over-TLS-enabled forwarder in the format of ip#hostname, e.g.: dns.example.com#1.2.3.4. This option can be used multiple times. +.TP +\fB\-\-dns\-over\-tls\-cert\fR=\fIFILE\fR +Certificate to use for DNS over TLS. If empty, a new certificate will be requested from IPA CA. +.TP +\fB\-\-dns\-over\-tls\-key\fR=\fIFILE\fR +Key for the certificate specified in --dns-over-tls-key. +.TP +\fB\-\-dns\-policy\fR=\fIrelaxed|enforced\fR +Encrypted DNS policy. If enforced, DNS communications will only be allowed through the configured encrypted DNS methods. If relaxed, +unencrypted DNS queries will be allowed. .SH "DEPRECATED OPTIONS" .TP \fB\-p\fR \fIDM_PASSWORD\fR, \fB\-\-ds\-password\fR=\fIDM_PASSWORD\fR diff --git a/install/tools/man/ipa-replica-install.1 b/install/tools/man/ipa-replica-install.1 index 3b1d25ba6..c55d21253 100644 --- a/install/tools/man/ipa-replica-install.1 +++ b/install/tools/man/ipa-replica-install.1 @@ -223,6 +223,22 @@ Do not automatically create DNS SSHFP records. .TP \fB\-\-no\-dnssec\-validation\fR Disable DNSSEC validation on this server. +.TP +\fB\-\-dns\-over\-tls\fR +Configure DNS over TLS. +.TP +\fB\-\-dot\-forwarder\fR=\fIIP_ADDRESS#HOSTNAME\fR +Add a DNS-over-TLS-enabled forwarder in the format of ip#hostname, e.g.: dns.example.com#1.2.3.4. This option can be used multiple times. +.TP +\fB\-\-dns\-over\-tls\-cert\fR=\fIFILE\fR +Certificate to use for DNS over TLS. If empty, a new certificate will be requested from IPA CA. +.TP +\fB\-\-dns\-over\-tls\-key\fR=\fIFILE\fR +Key for the certificate specified in --dns-over-tls-key. +.TP +\fB\-\-dns\-policy\fR=\fIrelaxed|enforced\fR +Encrypted DNS policy. If enforced, DNS communications will only be allowed through the configured encrypted DNS methods. If relaxed, +unencrypted DNS queries will be allowed. .SS "SID GENERATION OPTIONS" .TP diff --git a/install/tools/man/ipa-server-install.1 b/install/tools/man/ipa-server-install.1 index 215a77d6b..84d82531c 100644 --- a/install/tools/man/ipa-server-install.1 +++ b/install/tools/man/ipa-server-install.1 @@ -252,6 +252,22 @@ Disable DNSSEC validation on this server. .TP \fB\-\-allow\-zone\-overlap\fR Allow creation of (reverse) zone even if the zone is already resolvable. Using this option is discouraged as it result in later problems with domain name resolution. +.TP +\fB\-\-dns\-over\-tls\fR +Configure DNS over TLS. +.TP +\fB\-\-dot\-forwarder\fR=\fIIP_ADDRESS#HOSTNAME\fR +Add a DNS-over-TLS-enabled forwarder in the format of ip#hostname, e.g.: dns.example.com#1.2.3.4. This option can be used multiple times. +.TP +\fB\-\-dns\-over\-tls\-cert\fR=\fIFILE\fR +Certificate to use for DNS over TLS. If empty, a new certificate will be requested from IPA CA. +.TP +\fB\-\-dns\-over\-tls\-key\fR=\fIFILE\fR +Key for the certificate specified in --dns-over-tls-key. +.TP +\fB\-\-dns\-policy\fR=\fIrelaxed|enforced\fR +Encrypted DNS policy. If enforced, DNS communications will only be allowed through the configured encrypted DNS methods. If relaxed, +unencrypted DNS queries will be allowed. .SS "SID GENERATION OPTIONS" .TP diff --git a/ipaclient/install/client.py b/ipaclient/install/client.py index 47a371f62..9e4d3bbe7 100644 --- a/ipaclient/install/client.py +++ b/ipaclient/install/client.py @@ -1002,6 +1002,11 @@ def configure_sssd_conf( if options.dns_updates: domain.set_option('dyndns_update', True) + if options.dns_over_tls: + server_ip = str(list(dnsutil.resolve_ip_addresses( + cli_server[0]))[0]) + domain.set_option('dyndns_server', 'dns+tls://{}:853#{}' + .format(server_ip, cli_server[0])) if options.all_ip_addresses: domain.set_option('dyndns_iface', '*') else: @@ -1409,8 +1414,9 @@ def get_local_ipaddresses(iface=None): return ips -def do_nsupdate(update_txt): +def do_nsupdate(update_txt, options, server): logger.debug("Writing nsupdate commands to %s:", UPDATE_FILE) + logger.debug("%s", update_txt) with open(UPDATE_FILE, "w") as f: @@ -1418,12 +1424,20 @@ def do_nsupdate(update_txt): result = False try: - ipautil.run([paths.NSUPDATE, '-g', UPDATE_FILE]) + if options.dns_over_tls: + ipautil.run([paths.NSUPDATE, '-p', '853', '-S', + '-H', server, '-g', UPDATE_FILE]) + else: + ipautil.run([paths.NSUPDATE, '-g', UPDATE_FILE]) result = True except CalledProcessError as e: logger.debug('nsupdate (GSS-TSIG) failed: %s', str(e)) try: - ipautil.run([paths.NSUPDATE, UPDATE_FILE]) + if options.dns_over_tls: + ipautil.run([paths.NSUPDATE, '-p', '853', '-S', + '-H', server, '-g', UPDATE_FILE]) + else: + ipautil.run([paths.NSUPDATE, UPDATE_FILE]) try: sssdconfig = SSSDConfig.SSSDConfig() sssdconfig.import_config() @@ -1525,6 +1539,8 @@ def update_dns(server, hostname, options): no_matching_interface_for_ip_address_warning(update_ips) update_txt = "debug\n" + if options.dns_over_tls: + update_txt += "server %s 853" % server update_txt += ipautil.template_str(DELETE_TEMPLATE_A, dict(HOSTNAME=hostname)) update_txt += ipautil.template_str(DELETE_TEMPLATE_AAAA, @@ -1538,7 +1554,7 @@ def update_dns(server, hostname, options): template = ADD_TEMPLATE_AAAA update_txt += ipautil.template_str(template, sub_dict) - if not do_nsupdate(update_txt): + if not do_nsupdate(update_txt, options, server): logger.error("Failed to update DNS records.") verify_dns_update(hostname, update_ips) @@ -1654,6 +1670,54 @@ def client_dns(server, hostname, options): hostname, ex) dns_ok = False + # Setup DNS over TLS + if options.dns_over_tls: + # setup and enable Unbound as resolver + server_ip = str(list(dnsutil.resolve_ip_addresses(server))[0]) + forward_addr = "forward-addr: %s#%s" % (server_ip, server) + ipautil.copy_template_file( + paths.UNBOUND_CONF_SRC, + paths.UNBOUND_CONF, + dict( + TLS_CERT_BUNDLE_PATH=os.path.join( + paths.OPENSSL_CERTS_DIR, "ca-bundle.crt"), + FORWARD_ADDRS=forward_addr + ) + ) + sr = services.knownservices["systemd-resolved"] + if sr.is_running(): + sr.stop() + sr.disable() + + nm = services.knownservices["NetworkManager"] + if nm.is_enabled(): + with open(paths.NETWORK_MANAGER_IPA_CONF, "w") as f: + dns_none = [ + "# auto-generated by IPA installer", + "[main]", + "dns=none\n" + ] + f.write("\n".join(dns_none)) + nm.reload_or_restart() + + # Overwrite resolv.conf to point to Unbound + cfg = [ + "# auto-generated by IPA installer", + "search .", + "nameserver 127.0.0.55\n" + ] + fstore = sysrestore.FileStore(paths.IPA_CLIENT_SYSRESTORE) + fstore.backup_file(paths.RESOLV_CONF) + with open(paths.RESOLV_CONF, 'w') as f: + f.write('\n'.join(cfg)) + os.chmod(paths.RESOLV_CONF, 0o644) + + services.knownservices.unbound.enable() + services.knownservices.unbound.restart() + logger.info("DNS encryption support was enabled. " + "Unbound is configured to listen on 127.0.0.55:53 and " + "forward to upstream DoT servers.") + if ( options.dns_updates or options.all_ip_addresses or options.ip_addresses or not dns_ok @@ -1661,6 +1725,7 @@ def client_dns(server, hostname, options): update_dns(server, hostname, options) + def check_ip_addresses(options): if options.ip_addresses: for ip in options.ip_addresses: @@ -1672,7 +1737,7 @@ def check_ip_addresses(options): return True -def update_ssh_keys(hostname, ssh_dir, create_sshfp): +def update_ssh_keys(hostname, ssh_dir, options, server): if not os.path.isdir(ssh_dir): return @@ -1718,10 +1783,12 @@ def update_ssh_keys(hostname, ssh_dir, create_sshfp): logger.warning("Failed to upload host SSH public keys.") return - if create_sshfp: + if options.create_sshfp: ttl = 1200 update_txt = 'debug\n' + if options.dns_over_tls: + update_txt += "server %s 853" % server update_txt += 'update delete %s. IN SSHFP\nshow\nsend\n' % hostname for pubkey in pubkeys: sshfp = pubkey.fingerprint_dns_sha1() @@ -1734,7 +1801,7 @@ def update_ssh_keys(hostname, ssh_dir, create_sshfp): hostname, ttl, sshfp) update_txt += 'show\nsend\n' - if not do_nsupdate(update_txt): + if not do_nsupdate(update_txt, options, server): logger.warning("Could not update DNS SSHFP records.") @@ -3163,7 +3230,7 @@ def _install(options, tdict): if not options.on_master: client_dns(cli_server[0], hostname, options) - update_ssh_keys(hostname, paths.SSH_CONFIG_DIR, options.create_sshfp) + update_ssh_keys(hostname, paths.SSH_CONFIG_DIR, options, cli_server[0]) try: os.remove(CCACHE_FILE) @@ -3988,6 +4055,12 @@ class ClientInstallInterface(hostname_.HostNameInstallInterface, if value < 1: raise ValueError("expects an integer greater than 0.") + dns_over_tls = knob( + None, + description="Configure DNS over TLS", + ) + dns_over_tls = enroll_only(dns_over_tls) + request_cert = knob( None, deprecated=True, diff --git a/ipaplatform/base/paths.py b/ipaplatform/base/paths.py index b339d2202..b2da94992 100644 --- a/ipaplatform/base/paths.py +++ b/ipaplatform/base/paths.py @@ -102,6 +102,8 @@ class BasePathNamespace: NAMED_ROOT_KEY = "/etc/named.root.key" NAMED_MANAGED_KEYS_DIR = "/var/named/dynamic" NAMED_CRYPTO_POLICY_FILE = None + UNBOUND_CONF_SRC = '/usr/share/ipa/client/unbound.conf.template' + UNBOUND_CONF = "/etc/unbound/conf.d/zzz-ipa.conf" NSLCD_CONF = "/etc/nslcd.conf" NSS_LDAP_CONF = "/etc/nss_ldap.conf" NSSWITCH_CONF = "/etc/nsswitch.conf" @@ -225,6 +227,8 @@ class BasePathNamespace: OPENSSL_DIR = "/etc/pki/tls" OPENSSL_CERTS_DIR = "/etc/pki/tls/certs" OPENSSL_PRIVATE_DIR = "/etc/pki/tls/private" + BIND_DNS_OVER_TLS_CRT = "/etc/pki/tls/certs/bind_dot.crt" + BIND_DNS_OVER_TLS_KEY = "/etc/pki/tls/private/bind_dot.key" PK12UTIL = "/usr/bin/pk12util" SOFTHSM2_UTIL = "/usr/bin/softhsm2-util" SSLGET = "/usr/bin/sslget" diff --git a/ipaplatform/base/services.py b/ipaplatform/base/services.py index 275422e30..fcb626aa3 100644 --- a/ipaplatform/base/services.py +++ b/ipaplatform/base/services.py @@ -53,7 +53,7 @@ wellknownservices = [ 'named', 'ods_enforcerd', 'ods_signerd', 'gssproxy', 'nfs-utils', 'sssd', 'NetworkManager', 'ipa-custodia', 'ipa-dnskeysyncd', 'ipa-otpd', 'ipa-ods-exporter', - 'systemd-resolved', + 'systemd-resolved', 'unbound', ] # The common ports for these services. This is used to wait for the diff --git a/ipapython/ipautil.py b/ipapython/ipautil.py index c237d59fb..681f60655 100644 --- a/ipapython/ipautil.py +++ b/ipapython/ipautil.py @@ -266,6 +266,19 @@ class CheckedIPAddressLoopback(CheckedIPAddress): file=sys.stderr) +class IPAddressDoTForwarder(str): + """IPv4 or IPv6 address with added hostname as needed for DNS over TLS + configuration. Example: 1.2.3.4#dns.hostname.test + """ + def __init__(self, addr): + addr_split = addr.split("#") + if len(addr_split) != 2 or not valid_ip(addr_split[0]): + raise ValueError( + "DoT forwarder must be in the format " + "of '1.2.3.4#dns.example.test'." + ) + + def valid_ip(addr): return netaddr.valid_ipv4(addr) or netaddr.valid_ipv6(addr) diff --git a/ipaserver/install/bindinstance.py b/ipaserver/install/bindinstance.py index 939f5f21f..4f4ab9bbc 100644 --- a/ipaserver/install/bindinstance.py +++ b/ipaserver/install/bindinstance.py @@ -28,6 +28,7 @@ import re import shutil import sys import time +import textwrap import ldap import six @@ -50,7 +51,7 @@ from ipapython.admintool import ScriptError import ipalib from ipalib import api, errors from ipalib.constants import IPA_CA_RECORD -from ipalib.install import dnsforwarders +from ipalib.install import dnsforwarders, certmonger from ipaplatform import services from ipaplatform.tasks import tasks from ipaplatform.constants import constants @@ -668,14 +669,20 @@ class BindInstance(service.Service): def setup(self, fqdn, ip_addresses, realm_name, domain_name, forwarders, forward_policy, reverse_zones, zonemgr=None, - no_dnssec_validation=False): + no_dnssec_validation=False, dns_over_tls=False, + dns_over_tls_cert=None, dns_over_tls_key=None, + dns_policy=None): """Setup bindinstance for installation """ self.setup_templating( fqdn=fqdn, realm_name=realm_name, domain_name=domain_name, - no_dnssec_validation=no_dnssec_validation + no_dnssec_validation=no_dnssec_validation, + dns_over_tls=dns_over_tls, + dns_over_tls_cert=dns_over_tls_cert, + dns_over_tls_key=dns_over_tls_key, + dns_policy=dns_policy ) self.ip_addresses = ip_addresses self.forwarders = forwarders @@ -688,7 +695,9 @@ class BindInstance(service.Service): self.zonemgr = normalize_zonemgr(zonemgr) def setup_templating( - self, fqdn, realm_name, domain_name, no_dnssec_validation=None + self, fqdn, realm_name, domain_name, no_dnssec_validation=None, + dns_over_tls=None, dns_over_tls_cert=None, dns_over_tls_key=None, + dns_policy=None ): """Setup bindinstance for templating """ @@ -698,6 +707,10 @@ class BindInstance(service.Service): self.host = fqdn.split(".")[0] self.suffix = ipautil.realm_to_suffix(self.realm) self.no_dnssec_validation = no_dnssec_validation + self.dns_over_tls = dns_over_tls + self.dns_over_tls_cert = dns_over_tls_cert + self.dns_over_tls_key = dns_over_tls_key + self.dns_policy = dns_policy self._setup_sub_dict() @property @@ -872,6 +885,24 @@ class BindInstance(service.Service): else: crypto_policy = "// not available" + if self.dns_over_tls: + named_tls_conf = textwrap.dedent("""\ + tls local-tls {{ + \tkey-file "{}"; + \tcert-file "{}"; + }}; + """).format(self.dns_over_tls_key, self.dns_over_tls_cert) + unencrypted_iface = ("127.0.0.1" if self.dns_policy == "enforced" + else "any") + named_tls_options = textwrap.dedent("""\ + \tlisten-on { %s; }; + \tlisten-on tls local-tls { any; }; + \tlisten-on-v6 tls local-tls { any; }; + """ % unencrypted_iface) + else: + named_tls_options = "" + named_tls_conf = "" + self.sub_dict = dict( FQDN=self.fqdn, SERVER_ID=ipaldap.realm_to_serverid(self.realm), @@ -891,6 +922,8 @@ class BindInstance(service.Service): NAMED_DATA_DIR=constants.NAMED_DATA_DIR, NAMED_ZONE_COMMENT=constants.NAMED_ZONE_COMMENT, NAMED_DNSSEC_VALIDATION=self._get_dnssec_validation(), + NAMED_DNS_OVER_TLS_OPTIONS_CONF=named_tls_options, + NAMED_DNS_OVER_TLS_CONF=named_tls_conf, ) def __setup_dns_container(self): @@ -1344,6 +1377,11 @@ class BindInstance(service.Service): self.named_conflict.unmask() + certmonger.stop_tracking(certfile=paths.BIND_DNS_OVER_TLS_CRT) + certmonger.stop_tracking(certfile=paths.BIND_DNS_OVER_TLS_KEY) + services.knownservices.unbound.disable() + services.knownservices.unbound.stop() + ipautil.remove_file(paths.NAMED_CONF_BAK) ipautil.remove_file(paths.NAMED_CUSTOM_CONF) ipautil.remove_file(paths.NAMED_CUSTOM_OPTIONS_CONF) @@ -1357,6 +1395,8 @@ class BindInstance(service.Service): pass except ValueError: pass + ipautil.remove_file(paths.BIND_DNS_OVER_TLS_CRT) + ipautil.remove_file(paths.BIND_DNS_OVER_TLS_KEY) ipautil.remove_keytab(self.keytab) ipautil.remove_ccache(run_as=self.service_user) diff --git a/ipaserver/install/dns.py b/ipaserver/install/dns.py index 47d79af9c..29ca0d2ff 100644 --- a/ipaserver/install/dns.py +++ b/ipaserver/install/dns.py @@ -20,14 +20,18 @@ from subprocess import CalledProcessError from ipalib import api from ipalib import errors from ipalib import util -from ipalib.install import hostname, sysrestore +from ipalib import x509 +from ipalib.install import hostname, sysrestore, certmonger from ipalib.install.service import enroll_only, prepare_only from ipalib.install import dnsforwarders +from ipalib.constants import FQDN from ipaplatform.paths import paths from ipaplatform.constants import constants from ipaplatform import services +from ipapython import admintool from ipapython import ipautil from ipapython import dnsutil +from ipapython.certdb import EXTERNAL_CA_TRUST_FLAGS from ipapython.dn import DN from ipapython.dnsutil import check_zone_overlap from ipapython.install import typing @@ -37,7 +41,9 @@ from ipapython.ipautil import user_input from ipaserver.install.installutils import get_server_ip_address from ipaserver.install.installutils import read_dns_forwarders from ipaserver.install.installutils import update_hosts_file +from ipaserver.install.installutils import default_subject_base from ipaserver.install import bindinstance +from ipaserver.install import certs from ipaserver.install import dnskeysyncinstance from ipaserver.install import odsexporterinstance from ipaserver.install import opendnssecinstance @@ -108,6 +114,73 @@ def _disable_dnssec(): conn.update_entry(entry) +def _setup_dns_over_tls(options): + if os.path.isfile(paths.IPA_CA_CRT) and not options.dns_over_tls_cert: + # request certificate for DNS over TLS, using IPA CA + cert = paths.BIND_DNS_OVER_TLS_CRT + key = paths.BIND_DNS_OVER_TLS_KEY + certmonger.request_and_wait_for_cert( + certpath=(cert, key), + principal='DNS/%s@%s' % (FQDN, api.env.realm), + subject=str(DN(('CN', FQDN), default_subject_base(api.env.realm))), + storage="FILE" + ) + constants.NAMED_USER.chown(cert, gid=constants.NAMED_GROUP.gid) + constants.NAMED_USER.chown(key, gid=constants.NAMED_GROUP.gid) + + # setup and enable Unbound as resolver + forward_addrs = ["# forward-addr: specify here forwarders"] + if options.dot_forwarders: + forward_addrs = ["forward-addr: %s" % fw + for fw in options.dot_forwarders] + ipautil.copy_template_file( + paths.UNBOUND_CONF_SRC, + paths.UNBOUND_CONF, + dict( + TLS_CERT_BUNDLE_PATH=os.path.join( + paths.OPENSSL_CERTS_DIR, "ca-bundle.crt"), + FORWARD_ADDRS="\n".join(forward_addrs) + ) + ) + + sr = services.knownservices["systemd-resolved"] + if sr.is_running(): + sr.stop() + sr.disable() + + api.Command.dnsserver_mod( + FQDN, + idnsforwarders="127.0.0.55", + idnsforwardpolicy="first" + ) + + nm = services.knownservices["NetworkManager"] + if nm.is_enabled(): + with open(paths.NETWORK_MANAGER_IPA_CONF, "w") as f: + dns_none = [ + "# auto-generated by IPA installer", + "[main]", + "dns=none\n" + ] + f.write("\n".join(dns_none)) + nm.reload_or_restart() + + # Overwrite resolv.conf to point to IPA + cfg = [ + "# auto-generated by IPA installer", + "search .", + "nameserver 127.0.0.1\n" + ] + fstore = sysrestore.FileStore(paths.SYSRESTORE) + fstore.backup_file(paths.RESOLV_CONF) + with open(paths.RESOLV_CONF, 'w') as f: + f.write('\n'.join(cfg)) + os.chmod(paths.RESOLV_CONF, 0o644) + + services.knownservices.unbound.enable() + services.knownservices.unbound.restart() + + def package_check(exception): if not os.path.isfile(paths.IPA_DNS_INSTALL): raise exception( @@ -287,9 +360,14 @@ def install_check(standalone, api, replica, options, hostname): if options.no_forwarders: options.forwarders = [] - elif options.forwarders or options.auto_forwarders: + elif (options.forwarders + or options.dot_forwarders or options.auto_forwarders): if not options.forwarders: - options.forwarders = [] + if options.dot_forwarders: + options.forwarders = [fw.split("#")[0] + for fw in options.dot_forwarders] + else: + options.forwarders = [] if options.auto_forwarders: options.forwarders.extend(dnsforwarders.get_nameservers()) elif standalone or not replica: @@ -330,11 +408,46 @@ def install(standalone, replica, options, api=api): # otherwise this is done by server/replica installer update_hosts_file(ip_addresses, api.env.host, fstore) + if os.path.isfile(paths.IPA_CA_CRT) and not options.dns_over_tls_cert: + dot_cert = paths.BIND_DNS_OVER_TLS_CRT + dot_key = paths.BIND_DNS_OVER_TLS_KEY + elif options.dns_over_tls_cert and options.dns_over_tls_key: + # Check certificate validity first + with certs.NSSDatabase() as tmpdb: + tmpdb.create_db() + ca_certs = x509.load_certificate_list_from_file( + options.dns_over_tls_cert) + nicknames = [] + for ca_cert in ca_certs: + nicknames.append(str(DN(ca_cert.subject))) + tmpdb.add_cert( + ca_cert, str(DN(ca_cert.subject)), EXTERNAL_CA_TRUST_FLAGS) + try: + for nick in nicknames: + tmpdb.verify_ca_cert_validity(nick) + except ValueError as e: + raise admintool.ScriptError( + "Not a valid CA certificate: %s" % e) + dot_cert = options.dns_over_tls_cert + dot_key = options.dns_over_tls_key + else: + raise RuntimeError( + "Certificate for DNS over TLS not specified " + "and IPA CA is not present." + ) + + if not options.forwarders and options.dot_forwarders: + options.forwaders = [fw.split("#")[0] for fw in options.dot_forwarders] + bind = bindinstance.BindInstance(fstore, api=api) bind.setup(api.env.host, ip_addresses, api.env.realm, api.env.domain, options.forwarders, options.forward_policy, reverse_zones, zonemgr=options.zonemgr, - no_dnssec_validation=options.no_dnssec_validation) + no_dnssec_validation=options.no_dnssec_validation, + dns_over_tls=options.dns_over_tls, + dns_over_tls_cert=dot_cert, + dns_over_tls_key=dot_key, + dns_policy=options.dns_policy) if standalone and not options.unattended: print("") @@ -343,6 +456,11 @@ def install(standalone, replica, options, api=api): print("") bind.create_instance() + + if options.dns_over_tls: + print("Setting up DNS over TLS") + _setup_dns_over_tls(options) + print("Restarting the web server to pick up resolv.conf changes") services.knownservices.httpd.restart(capture_output=True) @@ -370,6 +488,12 @@ def install(standalone, replica, options, api=api): bind.update_system_records() if standalone: + if options.dns_over_tls and options.dns_policy == "enforced": + dns_port = "853" + elif options.dns_over_tls: + dns_port = "53, 853" + else: + dns_port = "53" print("==============================================================================") print("Setup complete") print("") @@ -378,14 +502,22 @@ def install(standalone, replica, options, api=api): print("") print("\tYou must make sure these network ports are open:") print("\t\tTCP Ports:") - print("\t\t * 53: bind") + print(f"\t\t * {dns_port}: bind") print("\t\tUDP Ports:") - print("\t\t * 53: bind") + print(f"\t\t * {dns_port}: bind") elif not standalone and replica: print("") bind.check_global_configuration() print("") + if options.dns_over_tls: + policy = "enforced" if options.dns_policy == "enforced" else "relaxed" + print("") + print(("DNS encryption support was enabled " + "with policy '{}'.".format(policy))) + print(("Unbound is configured to listen on 127.0.0.55:53 and " + "forward to upstream DoT servers.")) + def uninstall_check(options): # test if server is DNSSEC key master @@ -424,6 +556,10 @@ class DNSForwardPolicy(enum.Enum): FIRST = 'first' +class EncryptedDNSPolicy(enum.Enum): + RELAXED = 'relaxed' + ENFORCED = 'enforced' + @group class DNSInstallInterface(hostname.HostNameInstallInterface): """ @@ -536,6 +672,40 @@ class DNSInstallInterface(hostname.HostNameInstallInterface): ) no_dnssec_validation = enroll_only(no_dnssec_validation) + dns_over_tls = knob( + None, + description="Configure DNS over TLS", + ) + dns_over_tls = enroll_only(dns_over_tls) + + dot_forwarders = knob( + typing.List[ipautil.IPAddressDoTForwarder], None, + description=("Add a DNS over TLS forwarder. " + "This option can be used multiple times"), + cli_names='--dot-forwarder', + ) + dot_forwarders = enroll_only(dot_forwarders) + + dns_over_tls_cert = knob( + str, None, + description=("Certificate to use for DNS over TLS. " + "If empty, a new certificate will be " + "requested from IPA CA"), + ) + dns_over_tls_cert = enroll_only(dns_over_tls_cert) + + dns_over_tls_key = knob( + str, None, + description="Key for certificate specified in --dns-over-tls-cert", + ) + dns_over_tls_key = enroll_only(dns_over_tls_key) + + dns_policy = knob( + EncryptedDNSPolicy, 'relaxed', + description=("Encrypted DNS policy"), + ) + dns_policy = enroll_only(dns_policy) + dnssec_master = False disable_dnssec_master = False kasp_db_file = None diff --git a/ipaserver/install/server/__init__.py b/ipaserver/install/server/__init__.py index 857b08f9f..c6a88585a 100644 --- a/ipaserver/install/server/__init__.py +++ b/ipaserver/install/server/__init__.py @@ -21,6 +21,7 @@ from ipalib.install.service import (enroll_only, from ipapython.install import typing from ipapython.install.core import group, knob, extend_knob from ipapython.install.common import step +from ipaplatform import services from .install import validate_admin_password, validate_dm_password from .install import get_min_idstart @@ -442,6 +443,18 @@ class ServerInstallInterface(ServerCertificateInstallInterface, raise RuntimeError( "You cannot specify a --no-dnssec-validation option " "without the --setup-dns option") + if self.dot_forwarders: + raise RuntimeError( + "You cannot specify a --dot-forwarder option " + "without the --setup-dns option") + if self.dns_over_tls_cert: + raise RuntimeError( + "You cannot specify a --dns-over-tls-cert option " + "without the --setup-dns option") + if self.dns_over_tls_key: + raise RuntimeError( + "You cannot specify a --dns-over-tls-key option " + "without the --setup-dns option") elif self.forwarders and self.no_forwarders: raise RuntimeError( "You cannot specify a --forwarder option together with " @@ -458,7 +471,32 @@ class ServerInstallInterface(ServerCertificateInstallInterface, raise RuntimeError( "You cannot specify a --auto-reverse option together with " "--no-reverse") - + elif self.dot_forwarders and not self.dns_over_tls: + raise RuntimeError( + "You cannot specify a --dot-forwarder option " + "without the --dns-over-tls option") + elif (self.dns_over_tls + and not services.knownservices["unbound"].is_installed()): + raise RuntimeError( + "To enable DNS over TLS, package ipa-server-encrypted-dns " + "must be installed." + ) + elif self.dns_policy == "enforced" and not self.dns_over_tls: + raise RuntimeError( + "You cannot specify a --dns-policy option " + "without the --dns-over-tls option") + elif self.dns_over_tls_cert and not self.dns_over_tls: + raise RuntimeError( + "You cannot specify a --dns-over-tls-cert option " + "without the --dns-over-tls option") + elif self.dns_over_tls_key and not self.dns_over_tls: + raise RuntimeError( + "You cannot specify a --dns-over-tls-key option " + "without the --dns-over-tls option") + elif bool(self.dns_over_tls_key) != bool(self.dns_over_tls_cert): + raise RuntimeError( + "You cannot specify a --dns-over-tls-key option " + "without the --dns-over-tls-cert option and vice versa") if not self.setup_adtrust: if self.add_agents: raise RuntimeError( @@ -504,12 +542,18 @@ class ServerInstallInterface(ServerCertificateInstallInterface, "In unattended mode you need to provide at least -r, " "-p and -a options") if self.setup_dns: - if (not self.forwarders and - not self.no_forwarders and - not self.auto_forwarders): + if (not self.forwarders + and not self.no_forwarders + and not self.auto_forwarders + and not self.dot_forwarders): raise RuntimeError( "You must specify at least one of --forwarder, " - "--auto-forwarders, or --no-forwarders options") + "--auto-forwarders, --dot-forwarder or " + "--no-forwarders options") + elif self.dns_over_tls and not self.dot_forwarders: + raise RuntimeError( + "You must specify --dot-forwarder " + "when enabling DNS over TLS") any_ignore_option_true = any( [self.ignore_topology_disconnect, self.ignore_last_of_role]) @@ -541,10 +585,12 @@ class ServerInstallInterface(ServerCertificateInstallInterface, if self.setup_dns: if (not self.forwarders and not self.no_forwarders and - not self.auto_forwarders): + not self.auto_forwarders + and not self.dot_forwarders): raise RuntimeError( "You must specify at least one of --forwarder, " - "--auto-forwarders, or --no-forwarders options") + "--auto-forwarders, --dot-forwarder, " + "or --no-forwarders options") ServerMasterInstallInterface = installs_master(ServerInstallInterface) diff --git a/ipaserver/install/server/install.py b/ipaserver/install/server/install.py index c39c807a9..4354683f1 100644 --- a/ipaserver/install/server/install.py +++ b/ipaserver/install/server/install.py @@ -1018,6 +1018,10 @@ def install(installer): if options.setup_dns: dns.install(False, False, options) + elif options.dns_over_tls: + service.print_msg("Warning: --dns-over-tls option " + "specified without --setup-dns, ignoring") + options.dns_over_tls = False # Always call adtrust installer to configure SID generation # if --setup-adtrust is not specified, only the SID part is executed @@ -1089,12 +1093,16 @@ def install(installer): print("\t\t * 80, 443: HTTP/HTTPS") print("\t\t * 389, 636: LDAP/LDAPS") print("\t\t * 88, 464: kerberos") - if options.setup_dns: - print("\t\t * 53: bind") + if options.dns_over_tls and options.dns_policy == "enforced": + dns_port = "853" + elif options.dns_over_tls: + dns_port = "53, 853" + else: + dns_port = "53" + print(f"\t\t * {dns_port}: bind") print("\t\tUDP Ports:") print("\t\t * 88, 464: kerberos") - if options.setup_dns: - print("\t\t * 53: bind") + print(f"\t\t * {dns_port}: bind") if not options.no_ntp: print("\t\t * 123: ntp") print("") @@ -1109,6 +1117,14 @@ def install(installer): print("\t and servers for correct operation. You should consider " "enabling chronyd.") + if options.dns_over_tls: + policy = "enforced" if options.dns_policy == "enforced" else "relaxed" + print("") + print(("DNS encryption support was enabled " + "with policy '{}'.".format(policy))) + print(("Unbound is configured to listen on 127.0.0.55:53 and " + "forward to upstream DoT servers.")) + print("") if setup_ca and not options.token_name: print(("Be sure to back up the CA certificates stored in " + diff --git a/ipaserver/install/server/replicainstall.py b/ipaserver/install/server/replicainstall.py index eeaaacb65..1f2c81f85 100644 --- a/ipaserver/install/server/replicainstall.py +++ b/ipaserver/install/server/replicainstall.py @@ -722,6 +722,8 @@ def ensure_enrolled(installer): args.extend(("--ntp-server", server)) if installer.ntp_pool: args.extend(("--ntp-pool", installer.ntp_pool)) + if installer.dns_over_tls and not installer.setup_dns: + args.append("--dns-over-tls") try: # Call client install script -- 2.48.1 From 186d5f65dc57dba3bb027fa4c5c4cb1603ce305a Mon Sep 17 00:00:00 2001 From: Antonio Torres Date: Wed, 11 Dec 2024 13:38:28 +0100 Subject: [PATCH 2/4] ipatests: add tests for DNS over TLS Signed-off-by: Antonio Torres Reviewed-By: Francisco Trivino Reviewed-By: Alexander Bokovoy Reviewed-By: Varun Mylaraiah Reviewed-By: Pavel Brezina --- ipatests/test_integration/test_edns.py | 257 +++++++++++++++++++++++++ 1 file changed, 257 insertions(+) create mode 100644 ipatests/test_integration/test_edns.py diff --git a/ipatests/test_integration/test_edns.py b/ipatests/test_integration/test_edns.py new file mode 100644 index 000000000..b42570ffa --- /dev/null +++ b/ipatests/test_integration/test_edns.py @@ -0,0 +1,257 @@ +# +# Copyright (C) 2024 FreeIPA Contributors see COPYING for license +# +"""This covers tests for DNS over TLS related feature""" + +from __future__ import absolute_import +import textwrap + +from ipatests.pytest_ipa.integration import tasks +from ipatests.test_integration.base import IntegrationTest +from ipatests.test_integration.test_dns import TestDNS +from ipatests.pytest_ipa.integration.firewall import Firewall +from ipaplatform.paths import paths + + +class TestDNSOverTLS(IntegrationTest): + """Tests for DNS over TLS feature.""" + + topology = 'line' + num_replicas = 1 + num_clients = 1 + + @classmethod + def install(cls, mh): + Firewall(cls.master).enable_service("dns-over-tls") + Firewall(cls.replicas[0]).enable_service("dns-over-tls") + tasks.install_packages(cls.master, ['*ipa-server-encrypted-dns']) + tasks.install_packages(cls.replicas[0], ['*ipa-server-encrypted-dns']) + tasks.install_packages(cls.clients[0], ['*ipa-client-encrypted-dns']) + + def test_install_dnsovertls_invalid_ca(self): + """ + This test checks that the installers throws an error + when invalid cert is specified. + """ + bad_ca_cnf = textwrap.dedent(""" + [ req ] + x509_extensions = v3_ca + [ v3_ca ] + basicConstraints = critical,CA:false + """) + self.master.put_file_contents("/bad_ca.cnf", bad_ca_cnf) + self.master.run_command(["openssl", "req", "-newkey", "rsa:2048", + "-nodes", "-keyout", + "/etc/pki/tls/certs/privkey-invalid.pem", + "-x509", "-days", "36500", "-out", + "/etc/pki/tls/certs/certificate-invalid.pem", + "-subj", + ("/C=ES/ST=Andalucia/L=Sevilla/O=CompanyName/" + "OU=IT/CN=www.example.com/" + "emailAddress=email@example.com"), + "-config", "/bad_ca.cnf"]) + args = [ + "--dns-over-tls", + "--dot-forwarder", "1.1.1.1#cloudflare-dns.com", + "--dns-over-tls-cert", + "/etc/pki/tls/certs/certificate-invalid.pem", + "--dns-over-tls-key", + "/etc/pki/tls/certs/privkey-invalid.pem" + ] + res = tasks.install_master(self.master, extra_args=args, + raiseonerr=False) + assert "Not a valid CA certificate: " in res.stderr_text + tasks.uninstall_master(self.master) + + def test_install_dnsovertls_without_setup_dns_master(self): + """ + This test installs an IPA server using the --dns-over-tls option + without using setup-dns option, and captures warnings that appear. + """ + self.master.run_command(["ipa-server-install", "--uninstall", "-U"]) + args = [ + "--dns-over-tls", + ] + res = tasks.install_master( + self.master, extra_args=args, setup_dns=False) + assert ("Warning: --dns-over-tls option specified without " + "--setup-dns, ignoring") in res.stdout_text + tasks.uninstall_master(self.master) + + def test_install_dnsovertls_master(self): + """ + This tests installs IPA server with --dns-over-tls option. + """ + args = [ + "--dns-over-tls", + "--dot-forwarder", "1.1.1.1#cloudflare-dns.com", + ] + return tasks.install_master(self.master, extra_args=args) + + def test_install_dnsovertls_client(self): + """ + This tests installs IPA client with --dns-over-tls option. + """ + self.clients[0].put_file_contents( + paths.RESOLV_CONF, + "nameserver %s" % self.master.ip + ) + args = [ + "--dns-over-tls" + ] + return tasks.install_client(self.master, + self.clients[0], + nameservers=None, + extra_args=args) + + def test_install_dnsovertls_replica(self): + """ + This tests installs IPA replica with --dns-over-tls option. + """ + args = [ + "--dns-over-tls", + "--dot-forwarder", "1.1.1.1#cloudflare-dns.com", + ] + return tasks.install_replica(self.master, self.replicas[0], + setup_dns=True, extra_args=args) + + def test_queries_encrypted(self): + """ + This test performs queries from each of the hosts + and ensures they were routed to 1.1.1.1#853 (eDNS). + """ + unbound_log_cfg = textwrap.dedent(""" + server: + verbosity: 3 + log-queries: yes + """) + # Test servers first (querying to local Unbound) + for server in [self.master, self.replicas[0]]: + server.put_file_contents("/etc/unbound/conf.d/log.conf", + unbound_log_cfg) + server.run_command(["systemctl", "restart", "unbound"]) + server.run_command(["journalctl", "--flush", "--rotate", + "--vacuum-time=1s"]) + server.run_command(["dig", "freeipa.org"]) + server.run_command(["journalctl", "-u", "unbound", + "--grep=1.1.1.1#853"]) + server.run_command(["journalctl", "--flush", "--rotate", + "--vacuum-time=1s"]) + # Now, test the client (redirects query to master) + self.clients[0].run_command(["dig", "redhat.com"]) + self.master.run_command(["journalctl", "-u", "unbound", + "--grep=1.1.1.1#853"]) + + def test_uninstall_all(self): + """ + This test ensures that all hosts can be uninstalled correctly. + """ + tasks.uninstall_client(self.clients[0]) + tasks.uninstall_replica(self.master, self.replicas[0]) + tasks.uninstall_master(self.master) + + def test_install_dnsovertls_master_external_ca(self): + """ + This test ensures that IPA server can be installed + with DoT using an external CA. + """ + self.master.run_command(["openssl", "req", "-newkey", "rsa:2048", + "-nodes", "-keyout", + "/etc/pki/tls/certs/privkey.pem", "-x509", + "-days", "36500", "-out", + "/etc/pki/tls/certs/certificate.pem", "-subj", + ("/C=ES/ST=Andalucia/L=Sevilla/O=CompanyName/" + "OU=IT/CN={}/" + "emailAddress=email@example.com") + .format(self.master.hostname)]) + self.master.run_command(["chown", "named:named", + "/etc/pki/tls/certs/privkey.pem", + "/etc/pki/tls/certs/certificate.pem"]) + args = [ + "--dns-over-tls", + "--dot-forwarder", "1.1.1.1#cloudflare-dns.com", + "--dns-over-tls-cert", "/etc/pki/tls/certs/certificate.pem", + "--dns-over-tls-key", "/etc/pki/tls/certs/privkey.pem" + ] + return tasks.install_master(self.master, extra_args=args) + + def test_enrollments_external_ca(self): + """ + Test that replicas and clients can be deployed when the master + uses an external CA. + """ + tasks.copy_files(self.master, self.clients[0], + ["/etc/pki/tls/certs/certificate.pem"]) + self.clients[0].run_command(["mv", + "/etc/pki/tls/certs/certificate.pem", + "/etc/pki/ca-trust/source/anchors/"]) + self.clients[0].run_command(["update-ca-trust", "extract"]) + self.test_install_dnsovertls_client() + self.test_install_dnsovertls_replica() + self.test_queries_encrypted() + + def test_install_dnsovertls_with_invalid_ipaddress_master(self): + """ + This test installs an IPA server using the --dns-over-tls + option with an invalid IP address. + """ + args = [ + "--dns-over-tls", + "--dot-forwarder", "198.168.0.0.1#example-dns.test", + ] + res = tasks.install_master(self.master, extra_args=args, + raiseonerr=False) + assert ("--dot-forwarder invalid: DoT forwarder must be in " + "the format of '1.2.3.4#dns.example.test'") in res.stderr_text + tasks.uninstall_master(self.master) + + def test_validate_DoT_options_master(self): + """ + Tests that DoT options are displayed correctly on master. + """ + cmdout = self.master.run_command( + ['ipa-server-install', '--help']) + assert '''--dot-forwarder=DOT_FORWARDERS + Add a DNS over TLS forwarder. This option can be used + multiple times''' in cmdout.stdout_text # noqa: E501 + assert '''--dns-over-tls-cert=DNS_OVER_TLS_CERT + Certificate to use for DNS over TLS. If empty, a new + certificate will be requested from IPA CA''' in cmdout.stdout_text # noqa: E501 + assert '''--dns-over-tls-key=DNS_OVER_TLS_KEY + Key for certificate specified in --dns-over-tls-cert''' in cmdout.stdout_text # noqa: E501 + assert '''--dns-over-tls Configure DNS over TLS''' in cmdout.stdout_text # noqa: E501 + + def test_validate_DoT_options_replica(self): + """ + Tests that DoT options are displayed correctly on replica. + """ + cmdout = self.replicas[0].run_command( + ['ipa-server-install', '--help']) + assert '''--dot-forwarder=DOT_FORWARDERS + Add a DNS over TLS forwarder. This option can be used + multiple times''' in cmdout.stdout_text + assert '''--dns-over-tls-cert=DNS_OVER_TLS_CERT + Certificate to use for DNS over TLS. If empty, a new + certificate will be requested from IPA CA''' in cmdout.stdout_text # noqa: E501 + assert '''--dns-over-tls-key=DNS_OVER_TLS_KEY + Key for certificate specified in --dns-over-tls-cert''' in cmdout.stdout_text # noqa: E501 + assert '''--dns-over-tls Configure DNS over TLS''' in cmdout.stdout_text # noqa: E501 + + def test_validate_DoT_options_client(self): + """ + Tests that DoT options are displayed correctly on client. + """ + cmdout = self.clients[0].run_command( + ['ipa-client-install', '--help']) + assert '''--dns-over-tls Configure DNS over TLS''' in cmdout.stdout_text # noqa: E501 + + +class TestDNS_DoT(TestDNS): + @classmethod + def install(cls, mh): + tasks.install_packages(cls.master, ['*ipa-server-encrypted-dns']) + args = [ + "--dns-over-tls", + "--dot-forwarder", "1.1.1.1#cloudflare-dns.com" + ] + tasks.install_master(cls.master, extra_args=args) -- 2.48.1 From a32b8fda893ae00bcd8efd91339dc8dbe35fc3cd Mon Sep 17 00:00:00 2001 From: Antonio Torres Date: Wed, 11 Dec 2024 13:35:29 +0100 Subject: [PATCH 3/4] spec: add unbound requirement and template file Signed-off-by: Antonio Torres Reviewed-By: Francisco Trivino Reviewed-By: Alexander Bokovoy Reviewed-By: Varun Mylaraiah Reviewed-By: Pavel Brezina --- freeipa.spec.in | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/freeipa.spec.in b/freeipa.spec.in index 4b91aa96f..b539f51f8 100755 --- a/freeipa.spec.in +++ b/freeipa.spec.in @@ -628,6 +628,7 @@ Requires: openssl-pkcs11 >= %{openssl_pkcs11_version} # See https://bugzilla.redhat.com/show_bug.cgi?id=1825812 # RHEL 8.3+ and Fedora 32+ have 2.1 Requires: opendnssec >= 2.1.6-5 +Recommends: %{name}-server-encrypted-dns %{?systemd_requires} Provides: %{alt_name}-server-dns = %{version} @@ -642,6 +643,15 @@ IPA integrated DNS server with support for automatic DNSSEC signing. Integrated DNS server is BIND 9. OpenDNSSEC provides key management. +%package server-encrypted-dns +Summary: support for encrypted DNS in IPA integrated DNS server +Requires: %{name}-client-encrypted-dns + +%description server-encrypted-dns +Provides support for enabling DNS over TLS in the IPA integrated DNS +server. + + %package server-trust-ad Summary: Virtual package to install packages required for Active Directory trusts Requires: %{name}-server = %{version}-%{release} @@ -722,6 +732,7 @@ Requires: libnfsidmap Requires: (nfs-utils or nfsv4-client-utils) Requires: sssd-tools >= %{sssd_version} Requires(post): policycoreutils +Recommends: %{name}-client-encrypted-dns # https://pagure.io/freeipa/issue/8530 Recommends: libsss_sudo @@ -763,6 +774,14 @@ If your network uses IPA for authentication, this package should be installed on every client machine. This package provides command-line tools for IPA administrators. +%package client-encrypted-dns +Summary: Enable encrypted DNS support for clients +Requires: unbound + +%description client-encrypted-dns +This package enables support for installing clients with encrypted DNS +via DNS over TLS. + %package client-samba Summary: Tools to configure Samba on IPA client Group: System Environment/Base @@ -1724,6 +1743,10 @@ fi %attr(644,root,root) %{_unitdir}/ipa-ods-exporter.socket %attr(644,root,root) %{_unitdir}/ipa-ods-exporter.service +%files server-encrypted-dns +%doc README.md Contributors.txt +%license COPYING + %files server-trust-ad %doc README.md Contributors.txt %license COPYING @@ -1783,6 +1806,10 @@ fi %attr(600,root,root) %config(noreplace) %{_sysconfdir}/ipa/epn.conf %attr(644,root,root) %config(noreplace) %{_sysconfdir}/ipa/epn/expire_msg.template +%files client-encrypted-dns +%doc README.md Contributors.txt +%license COPYING + %files -n python3-ipaclient %doc README.md Contributors.txt %license COPYING -- 2.48.1