URL: https://github.com/freeipa/freeipa/pull/5125 Author: tiran Title: #5125: Add systemd-resolved support Action: opened
PR body: """ Fedora 33 switched to systemd-resolved - [X] Add helpers to get forwarders from resolve1 D-BUS API - [X] Configure NetworkManager to use systemd-resolved - [X] Use new API for auto-forwarders - [X] Configure systemd-resolved to use IPA's BIND server - [ ] Update DNS resolver configuration in ``ipa-server-upgrade`` - [ ] Add IPA's DNSSEC keys to systemd-resolved - [ ] Enable DNSSEC support of systemd-resolved See: https://pagure.io/freeipa/issue/8275 """ To pull the PR as Git branch: git remote add ghfreeipa https://github.com/freeipa/freeipa git fetch ghfreeipa pull/5125/head:pr5125 git checkout pr5125
From 54baea6b59238b9e968a3a51c36feacf3b0f8d91 Mon Sep 17 00:00:00 2001 From: Christian Heimes <chei...@redhat.com> Date: Tue, 22 Sep 2020 13:26:36 +0200 Subject: [PATCH 1/5] Add helpers for resolve1 and nameservers detect_resolve1_resolv_conf() detects if systemd-resolved is enabled and manages /etc/resolv.conf. get_resolve1_nameservers() gets upstream DNS servers from systemd-resolved's D-Bus interface. get_dnspython_nameservers() gets upstream DNS servers from /etc/resolv.conf via dns.python. get_nameservers() gets a list of unique, non-loopback DNS server IP addresses. Also fixes setup.py to include D-Bus for ipalib instead of ipapython. See: https://pagure.io/freeipa/issue/8275 Signed-off-by: Christian Heimes <chei...@redhat.com> --- ipalib/install/dnsforwarders.py | 157 ++++++++++++++++++++++++++++++++ ipalib/setup.py | 2 +- ipapython/setup.py | 1 - 3 files changed, 158 insertions(+), 2 deletions(-) create mode 100644 ipalib/install/dnsforwarders.py diff --git a/ipalib/install/dnsforwarders.py b/ipalib/install/dnsforwarders.py new file mode 100644 index 0000000000..e181a2314e --- /dev/null +++ b/ipalib/install/dnsforwarders.py @@ -0,0 +1,157 @@ +# +# Copyright (C) 2020 FreeIPA Contributors see COPYING for license +# +"""DNS forwarder and systemd-resolve1 helpers +""" +import ipaddress +import logging +import os +import socket + +import dbus + +from ipaplatform.paths import paths +try: + from ipapython.dnsutil import get_ipa_resolver +except ImportError: + from dns.resolver import get_default_resolver as get_ipa_resolver + + +logger = logging.getLogger(__name__) + + +_SYSTEMD_RESOLV_CONF = { + "/run/systemd/resolve/stub-resolv.conf", + "/run/systemd/resolve/resolv.conf", + "/lib/systemd/resolv.conf", + "/usr/lib/systemd/resolv.conf", +} + +_DBUS_RESOLVE1_NAME = "org.freedesktop.resolve1" +_DBUS_RESOLVE1_PATH = "/org/freedesktop/resolve1" +_DBUS_RESOLVE1_MANAGER_IF = "org.freedesktop.resolve1.Manager" +_DBUS_PROPERTY_IF = "org.freedesktop.DBus.Properties" + +# netlink interface index for resolve1 global settings and loopback +IFINDEX_GLOBAL = 0 +IFINDEX_LOOPBACK = 1 + + +def detect_resolve1_resolv_conf(): + """Detect if /etc/resolv.conf is managed by systemd-resolved + + See man(5) NetworkManager.conf + """ + try: + dest = os.readlink(paths.RESOLV_CONF) + except OSError: + # not a link + return False + # convert path relative to /etc/resolv.conf to abs path + dest = os.path.normpath( + os.path.join(os.path.dirname(paths.RESOLV_CONF), dest) + ) + return dest in _SYSTEMD_RESOLV_CONF + + +def get_resolve1_nameservers(*, with_ifindex=False): + """Get list of DNS nameservers from systemd-resolved + + :return: list of tuples (ifindex, ipaddress_obj) + """ + bus = dbus.SystemBus() + try: + resolve1 = bus.get_object(_DBUS_RESOLVE1_NAME, _DBUS_RESOLVE1_PATH) + prop_if = dbus.Interface(resolve1, _DBUS_PROPERTY_IF) + dns_prop = prop_if.Get(_DBUS_RESOLVE1_MANAGER_IF, "DNSEx") + finally: + bus.close() + + results = [] + for ifindex, af, dns_arr, port, sniname in dns_prop: + if port not in {0, 53} or sniname: + # non-default port, non-standard port, or SNI name configuration + # for DNS over TLS, e.g. 1.2.3.4:9953#example.com + continue + # convert packed format to IPAddress object (like inet_ntop) + if af == socket.AF_INET: + dnsip = ipaddress.IPv4Address(bytes(dns_arr)) + elif af == socket.AF_INET6: + dnsip = ipaddress.IPv6Address(bytes(dns_arr)) + else: + # neither IPv4 nor IPv6 + continue + if with_ifindex: + # netlink interface index, see socket.if_nameindex() + ifindex = int(ifindex) + results.append((ifindex, dnsip)) + else: + results.append(dnsip) + + return results + + +def get_dnspython_nameservers(*, with_ifindex=False): + """Get list of DNS nameservers from dnspython + + On Linux dnspython parses /etc/resolv.conf for us + + :return: list of tuples (ifindex, ipaddress_obj) + """ + results = [] + for nameserver in get_ipa_resolver().nameservers: + nameserver = ipaddress.ip_address(nameserver) + if with_ifindex: + results.append((IFINDEX_GLOBAL, nameserver)) + else: + results.append(nameserver) + return results + + +def get_nameservers(): + """Get list of unique, non-loopback DNS nameservers + + :return: list of strings + """ + if detect_resolve1_resolv_conf(): + logger.debug( + "systemd-resolved detected, fetching nameservers from D-Bus" + ) + nameservers = get_resolve1_nameservers(with_ifindex=True) + else: + logger.debug( + "systemd-resolved not detected, parsing %s", paths.RESOLV_CONF + ) + nameservers = get_dnspython_nameservers(with_ifindex=True) + + logger.debug("Detected nameservers: %r", nameservers) + + result = [] + seen = set() + for ifindex, ip in nameservers: + # unique entries + if ip in seen: + continue + seen.add(ip) + # skip loopback + if ifindex == IFINDEX_LOOPBACK or ip.is_loopback: + continue + result.append(str(ip)) + + logger.debug("Use nameservers %r", result) + + return result + + +if __name__ == "__main__": + from pprint import pprint + + print("systemd-resolved detected:", detect_resolve1_resolv_conf()) + print("dnspython nameservers:") + pprint(get_dnspython_nameservers()) + print("resolve1 nameservers:") + try: + pprint(get_resolve1_nameservers()) + except Exception as e: + print(e) + print("nameservers:", get_nameservers()) diff --git a/ipalib/setup.py b/ipalib/setup.py index 32f55c2f0a..68a321cece 100644 --- a/ipalib/setup.py +++ b/ipalib/setup.py @@ -44,6 +44,6 @@ "six", ], extras_require={ - "install": ["ipaplatform"], + "install": ["dbus-python"], # for certmonger and resolve1 }, ) diff --git a/ipapython/setup.py b/ipapython/setup.py index 228e2040ee..ea55f5c729 100644 --- a/ipapython/setup.py +++ b/ipapython/setup.py @@ -46,7 +46,6 @@ "six", ], extras_require={ - "install": ["dbus-python"], # for certmonger "ldap": ["python-ldap"], # ipapython.ipaldap # CheckedIPAddress.get_matching_interface "netifaces": ["netifaces"], From 393b7b53912fa660df5daf5dfcfe642fac9fc57d Mon Sep 17 00:00:00 2001 From: Christian Heimes <chei...@redhat.com> Date: Tue, 22 Sep 2020 13:30:30 +0200 Subject: [PATCH 2/5] Configure NetworkManager to use systemd-resolved zzz-ipa.conf now enables NetworkManager's systemd-resolved plugin when systemd-resolved is detected. See: https://pagure.io/freeipa/issue/8275 Signed-off-by: Christian Heimes <chei...@redhat.com> --- ipaplatform/base/tasks.py | 4 +++- ipaplatform/redhat/tasks.py | 14 ++++++++++++-- ipaserver/install/bindinstance.py | 5 ++++- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/ipaplatform/base/tasks.py b/ipaplatform/base/tasks.py index ad1e90d398..ae3c47ea63 100644 --- a/ipaplatform/base/tasks.py +++ b/ipaplatform/base/tasks.py @@ -309,11 +309,13 @@ def systemd_daemon_reload(self): """Tell systemd to reload config files""" raise NotImplementedError - def configure_dns_resolver(self, nameservers, searchdomains, fstore=None): + def configure_dns_resolver(self, nameservers, searchdomains, *, + resolve1_enabled=False, fstore=None): """Configure global DNS resolver (e.g. /etc/resolv.conf) :param nameservers: list of IP addresses :param searchdomains: list of search domaons + :param resolve1_enabled: is systemd-resolved enabled? :param fstore: optional file store for backup """ raise NotImplementedError diff --git a/ipaplatform/redhat/tasks.py b/ipaplatform/redhat/tasks.py index 3e1d427705..6cf8a48e4a 100644 --- a/ipaplatform/redhat/tasks.py +++ b/ipaplatform/redhat/tasks.py @@ -44,6 +44,7 @@ from pyasn1.error import PyAsn1Error from ipapython import directivesetter +from ipapython import dnsutil from ipapython import ipautil import ipapython.errors @@ -66,7 +67,7 @@ NM_IPA_CONF = textwrap.dedent(""" # auto-generated by IPA installer [main] - dns=default + dns={dnsprocessing} [global-dns] searches={searches} @@ -613,7 +614,8 @@ def setup_httpd_logging(self): 'TransferLog', 'logs/access_log', False) - def configure_dns_resolver(self, nameservers, searchdomains, fstore=None): + def configure_dns_resolver(self, nameservers, searchdomains, *, + resolve1_enabled=False, fstore=None): """Configure global DNS resolver (e.g. /etc/resolv.conf) :param nameservers: list of IP addresses @@ -639,7 +641,15 @@ def configure_dns_resolver(self, nameservers, searchdomains, fstore=None): # a new resolv.conf. The file is prefixed with ``zzz`` to # make it the last file. Global dns options do not stack and last # man standing wins. + if resolve1_enabled: + # push DNS configuration to systemd-resolved + dnsprocessing = "systemd-resolved" + else: + # update /etc/resolv.conf + dnsprocessing = "default" + cfg = NM_IPA_CONF.format( + dnsprocessing=dnsprocessing, servers=','.join(nameservers), searches=','.join(searchdomains) ) diff --git a/ipaserver/install/bindinstance.py b/ipaserver/install/bindinstance.py index 10a2583e5c..70760c35cc 100644 --- a/ipaserver/install/bindinstance.py +++ b/ipaserver/install/bindinstance.py @@ -51,6 +51,7 @@ import ipalib from ipalib import api, errors from ipalib.constants import IPA_CA_RECORD +from ipalib.install import dnsforwarders from ipaplatform import services from ipaplatform.tasks import tasks from ipaplatform.constants import constants @@ -1120,6 +1121,7 @@ def __setup_server_configuration(self): def __setup_resolv_conf(self): searchdomains = [self.domain] nameservers = [] + resolve1_enabled = dnsforwarders.detect_resolve1_resolv_conf() for ip_address in self.ip_addresses: if ip_address.version == 4: @@ -1129,7 +1131,8 @@ def __setup_resolv_conf(self): try: tasks.configure_dns_resolver( - nameservers, searchdomains, fstore=self.fstore + nameservers, searchdomains, + resolve1_enabled=resolve1_enabled, fstore=self.fstore ) except IOError as e: logger.error('Could not update DNS config: %s', e) From 53ad3e4833ed1b35b9bab29400c77a8023cffe18 Mon Sep 17 00:00:00 2001 From: Christian Heimes <chei...@redhat.com> Date: Tue, 22 Sep 2020 13:32:45 +0200 Subject: [PATCH 3/5] Use new API for auto-forwarders Auto-forwarders and manual configuration now use the new API to get a list of DNS servers. Manual installer refuses loopback, too. See: https://pagure.io/freeipa/issue/8275 Signed-off-by: Christian Heimes <chei...@redhat.com> --- ipaserver/install/dns.py | 3 ++- ipaserver/install/installutils.py | 22 +++++++++++++++++----- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/ipaserver/install/dns.py b/ipaserver/install/dns.py index f7d793e1de..9cd8978720 100644 --- a/ipaserver/install/dns.py +++ b/ipaserver/install/dns.py @@ -22,6 +22,7 @@ from ipalib import util from ipalib.install import hostname, sysrestore from ipalib.install.service import enroll_only, prepare_only +from ipalib.install import dnsforwarders from ipaplatform.paths import paths from ipaplatform.constants import constants from ipaplatform import services @@ -290,7 +291,7 @@ def install_check(standalone, api, replica, options, hostname): if not options.forwarders: options.forwarders = [] if options.auto_forwarders: - options.forwarders += dnsutil.get_ipa_resolver().nameservers + options.forwarders.extend(dnsforwarders.get_nameservers()) elif standalone or not replica: options.forwarders = read_dns_forwarders() diff --git a/ipaserver/install/installutils.py b/ipaserver/install/installutils.py index 6be7669c28..dde4b4974d 100644 --- a/ipaserver/install/installutils.py +++ b/ipaserver/install/installutils.py @@ -54,8 +54,9 @@ from ipalib.constants import MAXHOSTNAMELEN from ipalib.util import validate_hostname from ipalib import api, errors, x509 +from ipalib.install import dnsforwarders from ipapython.dn import DN -from ipapython.dnsutil import resolve, get_ipa_resolver +from ipapython.dnsutil import resolve from ipaserver.install import certs, service, sysupgrade from ipaplatform import services from ipaplatform.paths import paths @@ -289,13 +290,19 @@ def read_dns_forwarders(): if ipautil.user_input("Do you want to configure DNS forwarders?", True): print( "Following DNS servers are configured in /etc/resolv.conf: %s" - % ", ".join(get_ipa_resolver().nameservers) + % ", ".join(dnsforwarders.get_dnspython_nameservers()) ) + if dnsforwarders.detect_resolve1_resolv_conf(): + print( + "The following DNS servers are configured in " + "systemd-resolved: %s" % ", ".join( + dnsforwarders.get_resolve1_nameservers()) + ) if ipautil.user_input("Do you want to configure these servers as DNS " "forwarders?", True): - addrs = get_ipa_resolver().nameservers[:] - print("All DNS servers from /etc/resolv.conf were added. You can " - "enter additional addresses now:") + addrs = dnsforwarders.get_nameservers() + print("All detected DNS servers were added. You can enter " + "additional addresses now:") while True: ip = ipautil.user_input("Enter an IP address for a DNS forwarder, " "or press Enter to skip", allow_empty=True) @@ -308,6 +315,11 @@ def read_dns_forwarders(): print("DNS forwarder %s not added." % ip) continue + if ip_parsed.is_loopback(): + print("Error: %s is a loopback address" % ip) + print("DNS forwarder %s not added." % ip) + continue + print("DNS forwarder %s added. You may add another." % ip) addrs.append(str(ip_parsed)) From 20365549228e1d724bba56d4faa7c667cd9434dd Mon Sep 17 00:00:00 2001 From: Christian Heimes <chei...@redhat.com> Date: Tue, 22 Sep 2020 13:54:37 +0200 Subject: [PATCH 4/5] Configure systemd-resolved to use IPA's BIND IPA installer now instructs systemd-resolved to use IPA's BIND DNS server as primary DNS server. Fixes: https://pagure.io/freeipa/issue/8275 Signed-off-by: Christian Heimes <chei...@redhat.com> --- ipaplatform/base/paths.py | 1 + ipaplatform/base/services.py | 3 ++- ipaplatform/base/tasks.py | 35 ++++++++++++++++++++++++++++++++++- ipaplatform/redhat/tasks.py | 25 ++++++++++++++++--------- 4 files changed, 53 insertions(+), 11 deletions(-) diff --git a/ipaplatform/base/paths.py b/ipaplatform/base/paths.py index 6310869450..a9eb2e3957 100644 --- a/ipaplatform/base/paths.py +++ b/ipaplatform/base/paths.py @@ -387,6 +387,7 @@ class BasePathNamespace: CERTMONGER = '/usr/sbin/certmonger' NETWORK_MANAGER_CONFIG_DIR = '/etc/NetworkManager/conf.d' NETWORK_MANAGER_IPA_CONF = '/etc/NetworkManager/conf.d/zzz-ipa.conf' + SYSTEMD_RESOLVED_IPA_CONF = '/etc/systemd/resolved.conf.d/zzz-ipa.conf' IPA_CUSTODIA_CONF_DIR = '/etc/ipa/custodia' IPA_CUSTODIA_CONF = '/etc/ipa/custodia/custodia.conf' IPA_CUSTODIA_KEYS = '/etc/ipa/custodia/server.keys' diff --git a/ipaplatform/base/services.py b/ipaplatform/base/services.py index ac1a3c89bd..a17daa23c1 100644 --- a/ipaplatform/base/services.py +++ b/ipaplatform/base/services.py @@ -57,7 +57,8 @@ 'rpcidmapd', 'pki_tomcatd', 'chronyd', 'domainname', 'named', 'ods_enforcerd', 'ods_signerd', 'gssproxy', 'nfs-utils', 'sssd', 'NetworkManager', 'ipa-custodia', - 'ipa-dnskeysyncd', 'ipa-otpd', 'ipa-ods-exporter' + 'ipa-dnskeysyncd', 'ipa-otpd', 'ipa-ods-exporter', + 'systemd-resolved', ] # The common ports for these services. This is used to wait for the diff --git a/ipaplatform/base/tasks.py b/ipaplatform/base/tasks.py index ae3c47ea63..a39857e869 100644 --- a/ipaplatform/base/tasks.py +++ b/ipaplatform/base/tasks.py @@ -24,7 +24,9 @@ from __future__ import absolute_import +import os import logging +import textwrap from pkg_resources import parse_version @@ -35,6 +37,17 @@ logger = logging.getLogger(__name__) +# TODO: Add other masters as FallbackDNS ? +RESOLVE1_IPA_CONF = textwrap.dedent(""" + # auto-generated by IPA installer + [Resolve] + # use local BIND instance + DNS=127.0.0.1 + # make local BIND default DNS server, add search suffixes + Domains=~. {searchdomains} +""") + + class BaseTaskNamespace: def restore_context(self, filepath, force=False): @@ -318,7 +331,21 @@ def configure_dns_resolver(self, nameservers, searchdomains, *, :param resolve1_enabled: is systemd-resolved enabled? :param fstore: optional file store for backup """ - raise NotImplementedError + if resolve1_enabled: + # break circular import + from ipaplatform.services import knownservices + + confd = os.path.dirname(paths.SYSTEMD_RESOLVED_IPA_CONF) + os.makedirs(confd, exist_ok=True) + + cfg = RESOLVE1_IPA_CONF.format( + searchdomains=" ".join(searchdomains) + ) + with open(paths.SYSTEMD_RESOLVED_IPA_CONF, "w") as f: + os.fchmod(f.fileno(), 0o644) + f.write(cfg) + + knownservices["systemd-resolved"].reload_or_restart() def unconfigure_dns_resolver(self, fstore=None): """Unconfigure global DNS resolver (e.g. /etc/resolv.conf) @@ -328,6 +355,12 @@ def unconfigure_dns_resolver(self, fstore=None): if fstore is not None and fstore.has_file(paths.RESOLV_CONF): fstore.restore_file(paths.RESOLV_CONF) + if os.path.isfile(paths.SYSTEMD_RESOLVED_IPA_CONF): + # break circular import + from ipaplatform.services import knownservices + + os.unlink(paths.SYSTEMD_RESOLVED_IPA_CONF) + knownservices["systemd-resolved"].reload_or_restart() def configure_pkcs11_modules(self, fstore): """Disable p11-kit modules diff --git a/ipaplatform/redhat/tasks.py b/ipaplatform/redhat/tasks.py index 6cf8a48e4a..2b92c649bf 100644 --- a/ipaplatform/redhat/tasks.py +++ b/ipaplatform/redhat/tasks.py @@ -44,7 +44,6 @@ from pyasn1.error import PyAsn1Error from ipapython import directivesetter -from ipapython import dnsutil from ipapython import ipautil import ipapython.errors @@ -625,6 +624,13 @@ def configure_dns_resolver(self, nameservers, searchdomains, *, assert nameservers and isinstance(nameservers, list) assert searchdomains and isinstance(searchdomains, list) + super().configure_dns_resolver( + nameservers=nameservers, + searchdomains=searchdomains, + resolve1_enabled=resolve1_enabled, + fstore=fstore + ) + # break circular import from ipaplatform.services import knownservices @@ -632,7 +638,8 @@ def configure_dns_resolver(self, nameservers, searchdomains, *, fstore.backup_file(paths.RESOLV_CONF) nm = knownservices['NetworkManager'] - if nm.is_enabled(): + nm_enabled = nm.is_enabled() + if nm_enabled: logger.debug( "Network Manager is enabled, write %s", paths.NETWORK_MANAGER_IPA_CONF @@ -658,11 +665,13 @@ def configure_dns_resolver(self, nameservers, searchdomains, *, f.write(cfg) # reload NetworkManager nm.reload_or_restart() - else: - # no NM running, fall back to /etc/resolv.conf + + if not resolve1_enabled and not nm_enabled: + # no NM running, no systemd-resolved detected + # fall back to /etc/resolv.conf logger.debug( - "Network Manager is not enabled, write %s directly.", - paths.RESOLV_CONF + "Neither Network Manager nor systemd-resolved are enabled, " + "write %s directly.", paths.RESOLV_CONF ) cfg = [ "# auto-generated by IPA installer", @@ -678,12 +687,10 @@ def unconfigure_dns_resolver(self, fstore=None): :param fstore: optional file store for restore """ + super().unconfigure_dns_resolver(fstore=fstore) # break circular import from ipaplatform.services import knownservices - if fstore is not None and fstore.has_file(paths.RESOLV_CONF): - fstore.restore_file(paths.RESOLV_CONF) - nm = knownservices['NetworkManager'] if os.path.isfile(paths.NETWORK_MANAGER_IPA_CONF): os.unlink(paths.NETWORK_MANAGER_IPA_CONF) From ba63d907ab54c36bee4e82f4d122cfc3b967c2aa Mon Sep 17 00:00:00 2001 From: Christian Heimes <chei...@redhat.com> Date: Tue, 22 Sep 2020 13:58:57 +0200 Subject: [PATCH 5/5] Temp commit: Azure Fedora 33 Signed-off-by: Christian Heimes <chei...@redhat.com> --- ipatests/azure/Dockerfiles/Dockerfile.build.fedora | 2 +- ipatests/azure/templates/variables-fedora.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ipatests/azure/Dockerfiles/Dockerfile.build.fedora b/ipatests/azure/Dockerfiles/Dockerfile.build.fedora index 674bf60ed7..139d89a2af 100644 --- a/ipatests/azure/Dockerfiles/Dockerfile.build.fedora +++ b/ipatests/azure/Dockerfiles/Dockerfile.build.fedora @@ -1,4 +1,4 @@ -FROM fedora:32 +FROM fedora:33 MAINTAINER [FreeIPA Developers freeipa-devel@lists.fedorahosted.org] ENV container=docker LANG=en_US.utf8 LANGUAGE=en_US.utf8 LC_ALL=en_US.utf8 diff --git a/ipatests/azure/templates/variables-fedora.yml b/ipatests/azure/templates/variables-fedora.yml index 3e8f878c22..4b630d0905 100644 --- a/ipatests/azure/templates/variables-fedora.yml +++ b/ipatests/azure/templates/variables-fedora.yml @@ -1,7 +1,7 @@ variables: IPA_PLATFORM: fedora # the Docker public image to build IPA packages (rpms) - DOCKER_BUILD_IMAGE: 'fedora:32' + DOCKER_BUILD_IMAGE: 'fedora:33' # the Dockerfile to build Docker image for running IPA tests DOCKER_DOCKERFILE: ${{ format('Dockerfile.build.{0}', variables.IPA_PLATFORM) }}
_______________________________________________ FreeIPA-devel mailing list -- freeipa-devel@lists.fedorahosted.org To unsubscribe send an email to freeipa-devel-le...@lists.fedorahosted.org Fedora Code of Conduct: https://docs.fedoraproject.org/en-US/project/code-of-conduct/ List Guidelines: https://fedoraproject.org/wiki/Mailing_list_guidelines List Archives: https://lists.fedorahosted.org/archives/list/freeipa-devel@lists.fedorahosted.org