diff --git a/0052-Add-DNS-over-TLS-support.patch b/0052-Add-DNS-over-TLS-support.patch new file mode 100644 index 0000000..6264f5c --- /dev/null +++ b/0052-Add-DNS-over-TLS-support.patch @@ -0,0 +1,1447 @@ +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 + diff --git a/freeipa.spec b/freeipa.spec index 5eb7170..2cb71d7 100644 --- a/freeipa.spec +++ b/freeipa.spec @@ -85,7 +85,8 @@ # Fix for TLS 1.3 PHA, RHBZ#1775158 %global httpd_version 2.4.37-21 -%global bind_version 9.11.20-6 +%global bind_name bind9.18 +%global bind_version 9.18.29-2 # support for passkey %global sssd_version 2.9.5 @@ -135,6 +136,7 @@ %global httpd_version 2.4.41-9 # Fix for RHBZ#2117342 +%global bind_name bind %if 0%{?fedora} < 37 %global bind_version 9.11.24-1 %else @@ -224,7 +226,7 @@ Name: %{package_name} Version: %{IPA_VERSION} -Release: 10%{?rc_version:.%rc_version}%{?dist} +Release: 11%{?rc_version:.%rc_version}%{?dist} Summary: The Identity, Policy and Audit system License: GPL-3.0-or-later @@ -299,6 +301,7 @@ Patch0048: 0048-ipatests-restart-dirsrv-after-time-jumps.patch Patch0049: 0049-ipa-otpd-do-not-pass-OIDC-client-secret-if-there-is-.patch Patch0050: 0050-Migrate-Keycloak-tests-to-JDK-21-and-Keycloak-26.patch Patch0051: 0051-Apply-certmonger_timeout-to-start_tracking-and-reque.patch +Patch0052: 0052-Add-DNS-over-TLS-support.patch Patch1001: 1001-Change-branding-to-IPA-and-Identity-Management.patch %endif %endif @@ -648,14 +651,14 @@ If you are installing an IPA server, you need to install this package. Summary: IPA integrated DNS server with support for automatic DNSSEC signing BuildArch: noarch Requires: %{name}-server = %{version}-%{release} -Requires: bind-dyndb-ldap >= 11.2-2 -Requires: bind >= %{bind_version} -Requires: bind-utils >= %{bind_version} +Requires: bind-dyndb-ldap >= 11.11-1 +Requires: %{bind_name} >= %{bind_version} +Requires: %{bind_name}-utils >= %{bind_version} # bind-dnssec-utils is required by the OpenDNSSec integration # https://pagure.io/freeipa/issue/9026 -Requires: bind-dnssec-utils >= %{bind_version} +Requires: %{bind_name}-dnssec-utils >= %{bind_version} %if %{with bind_pkcs11} -Requires: bind-pkcs11 >= %{bind_version} +Requires: %{bind_name}-pkcs11 >= %{bind_version} %else Requires: softhsm >= %{softhsm_version} Requires: openssl-pkcs11 >= %{openssl_pkcs11_version} @@ -663,6 +666,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} @@ -677,6 +681,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} @@ -749,7 +762,7 @@ Requires: sssd-idp >= %{sssd_version} Requires: sssd-krb5 >= %{sssd_version} Requires: certmonger >= %{certmonger_version} Requires: nss-tools >= %{nss_version} -Requires: bind-utils +Requires: %{bind_name}-utils Requires: oddjob-mkhomedir Requires: libsss_autofs Requires: autofs @@ -757,6 +770,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 @@ -798,6 +812,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 @@ -1745,6 +1767,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 @@ -1804,6 +1830,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 @@ -1914,6 +1944,9 @@ fi %endif %changelog +* Tue Feb 11 2025 Thomas Woerner - 4.12.2-11 +- Resolves: RHEL-67913 Add DNS over TLS Support, Require bind9.18 32:9.18.29-2 and new bind-dyndb-ldap 11.11-1 + * Tue Jan 28 2025 Florence Blanc-Renaud - 4.12.2-10 - Resolves: RHEL-73022 A slow HSM can cause IPA server installation to fail setting up certificate tracking