- Resolves: RHEL-67912 Add DNS over TLS Support, require bind 32:9.18.33-2 and bind-dyndb-ldap 11.11-1 Signed-off-by: Thomas Woerner <twoerner@redhat.com>
1448 lines
58 KiB
Diff
1448 lines
58 KiB
Diff
From 46ef43b2a68139c991883633137c0061f20222a7 Mon Sep 17 00:00:00 2001
|
|
From: Antonio Torres <antorres@redhat.com>
|
|
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 <antorres@redhat.com>
|
|
Reviewed-By: Francisco Trivino <ftrivino@redhat.com>
|
|
Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com>
|
|
Reviewed-By: Varun Mylaraiah <mvarun@redhat.com>
|
|
Reviewed-By: Pavel Brezina <pbrezina@redhat.com>
|
|
---
|
|
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 <antorres@redhat.com>
|
|
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 <antorres@redhat.com>
|
|
Reviewed-By: Francisco Trivino <ftrivino@redhat.com>
|
|
Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com>
|
|
Reviewed-By: Varun Mylaraiah <mvarun@redhat.com>
|
|
Reviewed-By: Pavel Brezina <pbrezina@redhat.com>
|
|
---
|
|
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 <antorres@redhat.com>
|
|
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 <antorres@redhat.com>
|
|
Reviewed-By: Francisco Trivino <ftrivino@redhat.com>
|
|
Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com>
|
|
Reviewed-By: Varun Mylaraiah <mvarun@redhat.com>
|
|
Reviewed-By: Pavel Brezina <pbrezina@redhat.com>
|
|
---
|
|
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
|
|
|