URL: https://github.com/freeipa/freeipa/pull/5268 Author: abbra Title: #5268: [Backport][ipa-4-8] [EPN] SMTP client enhancements Action: opened
PR body: """ This PR was opened automatically because PR #5257 was pushed to master and backport to ipa-4-8 is required. """ To pull the PR as Git branch: git remote add ghfreeipa https://github.com/freeipa/freeipa git fetch ghfreeipa pull/5268/head:pr5268 git checkout pr5268
From 51a61cae7d29d98543c82009cea711b76f2c31a5 Mon Sep 17 00:00:00 2001 From: Stanislav Levin <s...@altlinux.org> Date: Mon, 30 Sep 2019 14:59:25 +0300 Subject: [PATCH 1/6] ipatests: Respect platform's openssl dir There are different build configurations of OpenSSL from one distro to another. For example, Debian: '--openssldir=/usr/lib/ssl', Fedora: '--openssldir=/etc/pki/tls', openSUSE: '--openssldir=/etc/ssl', ALTLinux: '--openssldir=/var/lib/ssl'. Signed-off-by: Stanislav Levin <s...@altlinux.org> --- ipaplatform/base/paths.py | 3 + ipaplatform/debian/paths.py | 3 + ipaplatform/suse/paths.py | 3 + ipatests/test_integration/test_cert.py | 92 ++++++++++++------- ipatests/test_integration/test_epn.py | 55 +++++++++-- .../test_replica_promotion.py | 9 +- 6 files changed, 119 insertions(+), 46 deletions(-) diff --git a/ipaplatform/base/paths.py b/ipaplatform/base/paths.py index 0c5494612d8..024d9b167fe 100644 --- a/ipaplatform/base/paths.py +++ b/ipaplatform/base/paths.py @@ -208,6 +208,9 @@ class BasePathNamespace: ODS_ENFORCER = "/usr/sbin/ods-enforcer" ODS_ENFORCER_DB_SETUP = "/usr/sbin/ods-enforcer-db-setup" OPENSSL = "/usr/bin/openssl" + OPENSSL_DIR = "/etc/pki/tls" + OPENSSL_CERTS_DIR = "/etc/pki/tls/certs" + OPENSSL_PRIVATE_DIR = "/etc/pki/tls/private" PK12UTIL = "/usr/bin/pk12util" SOFTHSM2_UTIL = "/usr/bin/softhsm2-util" SSLGET = "/usr/bin/sslget" diff --git a/ipaplatform/debian/paths.py b/ipaplatform/debian/paths.py index c97007acead..e9ba639ee98 100644 --- a/ipaplatform/debian/paths.py +++ b/ipaplatform/debian/paths.py @@ -43,6 +43,9 @@ class DebianPathNamespace(BasePathNamespace): NAMED_MANAGED_KEYS_DIR = "/var/cache/bind/dynamic" CHRONY_CONF = "/etc/chrony/chrony.conf" OPENLDAP_LDAP_CONF = "/etc/ldap/ldap.conf" + OPENSSL_DIR = "/usr/lib/ssl" + OPENSSL_CERTS_DIR = "/usr/lib/ssl/certs" + OPENSSL_PRIVATE_DIR = "/usr/lib/ssl/private" ETC_DEBIAN_VERSION = "/etc/debian_version" # Old versions of freeipa wrote all trusted certificates to a single # file, which is not supported by ca-certificates. diff --git a/ipaplatform/suse/paths.py b/ipaplatform/suse/paths.py index e5baf30b8bb..383f191db03 100644 --- a/ipaplatform/suse/paths.py +++ b/ipaplatform/suse/paths.py @@ -29,6 +29,9 @@ class SusePathNamespace(BasePathNamespace): NAMED_CUSTOM_OPTIONS_CONF = "/etc/named.d/ipa-options-ext.conf" NAMED_VAR_DIR = "/var/lib/named" NAMED_MANAGED_KEYS_DIR = "/var/lib/named/dyn" + OPENSSL_DIR = "/etc/ssl" + OPENSSL_CERTS_DIR = "/etc/ssl/certs" + OPENSSL_PRIVATE_DIR = "/etc/ssl/private" IPA_P11_KIT = "/etc/pki/trust/ipa.p11-kit" # Those files are only here to be able to configure them, we copy those in # rpm spec to fillupdir diff --git a/ipatests/test_integration/test_cert.py b/ipatests/test_integration/test_cert.py index 2ac32a72f33..d84c7f1fc7f 100644 --- a/ipatests/test_integration/test_cert.py +++ b/ipatests/test_integration/test_cert.py @@ -6,6 +6,8 @@ Module provides tests which testing ability of various certificate related scenarios. """ +import os + import ipaddress import pytest import random @@ -78,11 +80,13 @@ def test_cacert_file_appear_with_option_F(self): related: https://pagure.io/freeipa/issue/8105 """ - cmd_arg = ['ipa-getcert', 'request', - '-f', '/etc/pki/tls/certs/test.pem', - '-k', '/etc/pki/tls/private/test.key', - '-K', 'test/%s' % self.clients[0].hostname, - '-F', '/etc/pki/tls/test.CA'] + cmd_arg = [ + "ipa-getcert", "request", + "-f", os.path.join(paths.OPENSSL_CERTS_DIR, "test.pem"), + "-k", os.path.join(paths.OPENSSL_PRIVATE_DIR, "test.key"), + "-K", "test/%s" % self.clients[0].hostname, + "-F", os.path.join(paths.OPENSSL_DIR, "test.CA"), + ] result = self.clients[0].run_command(cmd_arg) request_id = re.findall(r'\d+', result.stdout_text) @@ -90,13 +94,15 @@ def test_cacert_file_appear_with_option_F(self): status = tasks.wait_for_request(self.clients[0], request_id[0], 50) assert status == "MONITORING" - self.clients[0].run_command(['ls', '-l', '/etc/pki/tls/test.CA']) + self.clients[0].run_command( + ["ls", "-l", os.path.join(paths.OPENSSL_DIR, "test.CA")] + ) def test_ipa_getcert_san_aci(self): """Test for DNS and IP SAN extensions + ACIs """ hostname = self.clients[0].hostname - certfile = '/etc/pki/tls/certs/test2.pem' + certfile = os.path.join(paths.OPENSSL_CERTS_DIR, "test2.pem") tasks.kinit_admin(self.master) @@ -117,7 +123,7 @@ def test_ipa_getcert_san_aci(self): cmd_arg = [ 'ipa-getcert', 'request', '-v', '-w', '-f', certfile, - '-k', '/etc/pki/tls/private/test2.key', + '-k', os.path.join(paths.OPENSSL_PRIVATE_DIR, "test2.key"), '-K', f'test/{hostname}', '-D', hostname, '-A', self.clients[0].ip, @@ -182,9 +188,11 @@ def test_subca_certs(self): self.master.run_command(["ipa", "ca-disable", "mysubca"]) self.master.run_command(["ipa", "ca-del", "mysubca"]) self.master.run_command( - ["rm", "-fv", "/etc/pki/tls/private/test.key"] + ["rm", "-fv", os.path.join(paths.OPENSSL_PRIVATE_DIR, "test.key")] + ) + self.master.run_command( + ["rm", "-fv", os.path.join(paths.OPENSSL_CERTS_DIR, "test.pem")] ) - self.master.run_command(["rm", "-fv", "/etc/pki/tls/certs/test.pem"]) def test_getcert_list_profile_using_subca(self, test_subca_certs): """ @@ -199,10 +207,8 @@ def test_getcert_list_profile_using_subca(self, test_subca_certs): "ipa", "-I", "test-request", - "-k", - "/etc/pki/tls/private/test.key", - "-f", - "/etc/pki/tls/certs/test.pem", + "-k", os.path.join(paths.OPENSSL_PRIVATE_DIR, "test.key"), + "-f", os.path.join(paths.OPENSSL_CERTS_DIR, "test.pem"), "-D", self.master.hostname, "-K", @@ -245,12 +251,21 @@ def request_cert(self): string.ascii_lowercase ) for i in range(10) ) - self.master.run_command([ - 'ipa-getcert', 'request', - '-f', '/etc/pki/tls/certs/{}.pem'.format(self.request_id), - '-k', '/etc/pki/tls/private/{}.key'.format(self.request_id), - '-I', self.request_id, - '-K', 'test/{}'.format(self.master.hostname)]) + self.master.run_command( + [ + 'ipa-getcert', 'request', + '-f', + os.path.join( + paths.OPENSSL_CERTS_DIR, f"{self.request_id}.pem", + ), + '-k', + os.path.join( + paths.OPENSSL_PRIVATE_DIR, f"{self.request_id}.key" + ), + '-I', self.request_id, + '-K', 'test/{}'.format(self.master.hostname) + ] + ) status = tasks.wait_for_request(self.master, self.request_id, 100) assert status == "MONITORING" @@ -260,16 +275,20 @@ def request_cert(self): '-i', self.request_id]) self.master.run_command( [ - 'rm', - '-rf', - '/etc/pki/tls/certs/{}.pem'.format(self.request_id) + "rm", + "-rf", + os.path.join( + paths.OPENSSL_CERTS_DIR, f"{self.request_id}.pem" + ), ] ) self.master.run_command( [ - 'rm', - '-rf', - '/etc/pki/tls/private/{}.key'.format(self.request_id) + "rm", + "-rf", + os.path.join( + paths.OPENSSL_PRIVATE_DIR, f"{self.request_id}.key" + ), ] ) @@ -283,7 +302,7 @@ def test_certmonger_rekey_keysize(self, request_cert): related: https://bugzilla.redhat.com/show_bug.cgi?id=1249165 """ certdata = self.master.get_file_contents( - '/etc/pki/tls/certs/{}.pem'.format(self.request_id) + os.path.join(paths.OPENSSL_CERTS_DIR, f"{self.request_id}.pem") ) cert = x509.load_pem_x509_certificate( certdata, default_backend() @@ -299,7 +318,7 @@ def test_certmonger_rekey_keysize(self, request_cert): assert status == "MONITORING" certdata = self.master.get_file_contents( - '/etc/pki/tls/certs/{}.pem'.format(self.request_id) + os.path.join(paths.OPENSSL_CERTS_DIR, f"{self.request_id}.pem") ) cert = x509.load_pem_x509_certificate( certdata, default_backend() @@ -352,11 +371,14 @@ def test_rekey_keytype_DSA(self): related: https://bugzilla.redhat.com/show_bug.cgi?id=1249165 """ - result = self.master.run_command([ - 'ipa-getcert', 'request', - '-f', '/etc/pki/tls/certs/test_dsa.pem', - '-k', '/etc/pki/tls/private/test_dsa.key', - '-K', 'test/{}'.format(self.master.hostname)]) + result = self.master.run_command( + [ + 'ipa-getcert', 'request', + '-f', os.path.join(paths.OPENSSL_CERTS_DIR, "test_dsa.pem"), + '-k', os.path.join(paths.OPENSSL_PRIVATE_DIR, "test_dsa.key"), + '-K', 'test/{}'.format(self.master.hostname), + ] + ) req_id = re.findall(r'\d+', result.stdout_text) status = tasks.wait_for_request(self.master, req_id[0], 100) assert status == "MONITORING" @@ -369,7 +391,9 @@ def test_rekey_keytype_DSA(self): time.sleep(100) # look for keytpe as DSA in request file self.master.run_command([ - 'grep', 'DSA', '/var/lib/certmonger/requests/{}'.format(req_id[0]) + 'grep', + 'DSA', + os.path.join(paths.CERTMONGER_REQUESTS_DIR, req_id[0]), ]) err_msg = 'Unable to create enrollment request: Invalid Request' diff --git a/ipatests/test_integration/test_epn.py b/ipatests/test_integration/test_epn.py index c66d316a62c..3a79929696f 100644 --- a/ipatests/test_integration/test_epn.py +++ b/ipatests/test_integration/test_epn.py @@ -35,6 +35,7 @@ from subprocess import CalledProcessError +from ipaplatform.paths import paths from ipatests.test_integration.base import IntegrationTest from ipatests.pytest_ipa.integration import tasks @@ -108,11 +109,17 @@ def configure_starttls(host): Depends on configure_postfix() being executed first. """ - host.run_command(r'rm -f /etc/pki/tls/private/postfix.key') - host.run_command(r'rm -f /etc/pki/tls/certs/postfix.pem') + host.run_command( + ["rm", "-f", os.path.join(paths.OPENSSL_PRIVATE_DIR, "postfix.key")] + ) + host.run_command( + ["rm", "-f", os.path.join(paths.OPENSSL_CERTS_DIR, "postfix.pem")] + ) host.run_command(["ipa-getcert", "request", - "-f", "/etc/pki/tls/certs/postfix.pem", - "-k", "/etc/pki/tls/private/postfix.key", + "-f", + os.path.join(paths.OPENSSL_CERTS_DIR, "postfix.pem"), + "-k", + os.path.join(paths.OPENSSL_PRIVATE_DIR, "postfix.key"), "-K", "smtp/%s" % host.hostname, "-D", host.hostname, "-O", "postfix", @@ -123,8 +130,18 @@ def configure_starttls(host): ]) postconf(host, 'smtpd_tls_loglevel = 1') postconf(host, 'smtpd_tls_auth_only = yes') - postconf(host, 'smtpd_tls_key_file = /etc/pki/tls/private/postfix.key') - postconf(host, 'smtpd_tls_cert_file = /etc/pki/tls/certs/postfix.pem') + postconf( + host, + "smtpd_tls_key_file = {}".format( + os.path.join(paths.OPENSSL_PRIVATE_DIR, "postfix.key") + ) + ) + postconf( + host, + "smtpd_tls_cert_file = {}".format( + os.path.join(paths.OPENSSL_CERTS_DIR, "postfix.pem") + ) + ) postconf(host, 'smtpd_tls_received_header = yes') postconf(host, 'smtpd_tls_session_cache_timeout = 3600s') @@ -246,10 +263,28 @@ def uninstall(cls, mh): tasks.uninstall_packages(cls.clients[0], EPN_PKG) tasks.uninstall_packages(cls.clients[0], ["postfix"]) cls.master.run_command(r'rm -f /etc/postfix/smtp.keytab') - cls.master.run_command(r'getcert stop-tracking -f ' - '/etc/pki/tls/certs/postfix.pem') - cls.master.run_command(r'rm -f /etc/pki/tls/private/postfix.key') - cls.master.run_command(r'rm -f /etc/pki/tls/certs/postfix.pem') + cls.master.run_command( + [ + "getcert", + "stop-tracking", + "-f", + os.path.join(paths.OPENSSL_CERTS_DIR, "postfix.pem"), + ] + ) + cls.master.run_command( + [ + "rm", + "-f", + os.path.join(paths.OPENSSL_PRIVATE_DIR, "postfix.key"), + ] + ) + cls.master.run_command( + [ + "rm", + "-f", + os.path.join(paths.OPENSSL_CERTS_DIR, "postfix.pem"), + ] + ) @pytest.mark.skip_if_platform( "debian", reason="Cannot check installed packages using RPM" diff --git a/ipatests/test_integration/test_replica_promotion.py b/ipatests/test_integration/test_replica_promotion.py index c1019aa5573..5071fcbfe75 100644 --- a/ipatests/test_integration/test_replica_promotion.py +++ b/ipatests/test_integration/test_replica_promotion.py @@ -4,6 +4,7 @@ from __future__ import absolute_import +import os import time import re import textwrap @@ -626,8 +627,12 @@ def test_sign_with_subca_on_replica(self): master = self.master replica = self.replicas[0] - TEST_KEY_FILE = '/etc/pki/tls/private/test_subca.key' - TEST_CRT_FILE = '/etc/pki/tls/private/test_subca.crt' + TEST_KEY_FILE = os.path.join( + paths.OPENSSL_PRIVATE_DIR, 'test_subca.key' + ) + TEST_CRT_FILE = os.path.join( + paths.OPENSSL_PRIVATE_DIR, 'test_subca.crt' + ) caacl_cmd = [ 'ipa', 'caacl-add-ca', 'hosts_services_caIPAserviceCert', From aea98c8b41691744787ce954bd8b315a20ef2ced Mon Sep 17 00:00:00 2001 From: Stanislav Levin <s...@altlinux.org> Date: Thu, 12 Nov 2020 14:45:50 +0300 Subject: [PATCH 2/6] EPN: Don't downgrade security If an administrator requests `smtp_security=starttls`, but SMTP server disables STARTTLS, then EPN downgrade security to `none`, which means plain text. Administrator doesn't expect such behavior. Fixes: https://pagure.io/freeipa/issue/8578 Signed-off-by: Stanislav Levin <s...@altlinux.org> --- ipaclient/install/ipa_epn.py | 12 +--- ipatests/test_integration/test_epn.py | 86 ++++++++++++++++++++++----- 2 files changed, 74 insertions(+), 24 deletions(-) diff --git a/ipaclient/install/ipa_epn.py b/ipaclient/install/ipa_epn.py index 88c926e88fc..17b0420a338 100644 --- a/ipaclient/install/ipa_epn.py +++ b/ipaclient/install/ipa_epn.py @@ -685,20 +685,14 @@ def _connect(self): e, ) - if ( - self._conn.has_extn("STARTTLS") - and self._security_protocol.lower() == "starttls" - ): + if self._security_protocol.lower() == "starttls": try: self._conn.starttls() self._conn.ehlo() except smtplib.SMTPException as e: - logger.error( + raise RuntimeError( "IPA-EPN: Unable to create an encrypted session to " - "%s:%s: %s", - self._smtp_hostname, - self._smtp_port, - e, + "%s:%s: %s" % (self._smtp_hostname, self._smtp_port, e) ) if self._username and self._password: diff --git a/ipatests/test_integration/test_epn.py b/ipatests/test_integration/test_epn.py index 3a79929696f..b9cde8fe680 100644 --- a/ipatests/test_integration/test_epn.py +++ b/ipatests/test_integration/test_epn.py @@ -43,6 +43,25 @@ EPN_PKG = ["*ipa-client-epn"] +STARTTLS_EPN_CONF = textwrap.dedent( + """\ + [global] + smtp_user={user} + smtp_password={password} + smtp_security=starttls + """ +) + +SSL_EPN_CONF = textwrap.dedent( + """\ + [global] + smtp_user={user} + smtp_password={password} + smtp_port=465 + smtp_security=ssl + """ +) + def datetime_to_generalized_time(dt): """Convert datetime to LDAP_GENERALIZED_TIME_FORMAT @@ -93,6 +112,11 @@ def configure_postfix(host, realm): postconf(host, 'broken_sasl_auth_clients = yes') postconf(host, 'smtpd_sasl_authenticated_header = yes') postconf(host, 'smtpd_sasl_local_domain = %s' % realm) + # TLS will not be used + postconf(host, 'smtpd_tls_security_level = none') + + # disable procmail if exists, make use of default local(8) delivery agent + postconf(host, "mailbox_command=") host.run_command(["systemctl", "restart", "saslauthd"]) @@ -144,6 +168,8 @@ def configure_starttls(host): ) postconf(host, 'smtpd_tls_received_header = yes') postconf(host, 'smtpd_tls_session_cache_timeout = 3600s') + # announce STARTTLS support to remote SMTP clients, not require + postconf(host, 'smtpd_tls_security_level = may') host.run_command(["systemctl", "restart", "postfix"]) @@ -319,6 +345,43 @@ def test_EPN_connection_refused(self): stderr_text assert rc > 0 + def test_EPN_no_security_downgrade_starttls(self): + """Configure postfix without starttls and test no auth happens + """ + epn_conf = STARTTLS_EPN_CONF.format( + user=self.master.config.admin_name, + password=self.master.config.admin_password, + ) + self.master.put_file_contents('/etc/ipa/epn.conf', epn_conf) + + (unused, stderr_text, rc) = self._check_epn_output( + self.master, mailtest=True, + raiseonerr=False, validatejson=False + ) + expected_msg = "IPA-EPN: Unable to create an encrypted session to" + assert expected_msg in stderr_text + assert rc > 0 + + def test_EPN_no_security_downgrade_tls(self): + """Configure postfix without tls and test no auth happens + """ + epn_conf = SSL_EPN_CONF.format( + user=self.master.config.admin_name, + password=self.master.config.admin_password, + ) + self.master.put_file_contents('/etc/ipa/epn.conf', epn_conf) + + (unused, stderr_text, rc) = self._check_epn_output( + self.master, mailtest=True, + raiseonerr=False, validatejson=False + ) + expected_msg = ( + "IPA-EPN: Could not connect to the configured SMTP " + "server" + ) + assert expected_msg in stderr_text + assert rc > 0 + def test_EPN_smoketest_1(self): """No users except admin. Check --dry-run output. With the default configuration, the result should be an empty list. @@ -611,13 +674,10 @@ def test_mailtest_dry_run(self): def test_EPN_starttls(self, cleanupmail): """Configure with starttls and test delivery """ - epn_conf = textwrap.dedent(''' - [global] - smtp_user={user} - smtp_password={password} - smtp_security=starttls - '''.format(user=self.master.config.admin_name, - password=self.master.config.admin_password)) + epn_conf = STARTTLS_EPN_CONF.format( + user=self.master.config.admin_name, + password=self.master.config.admin_password, + ) self.master.put_file_contents('/etc/ipa/epn.conf', epn_conf) configure_starttls(self.master) @@ -629,14 +689,10 @@ def test_EPN_starttls(self, cleanupmail): def test_EPN_ssl(self, cleanupmail): """Configure with ssl and test delivery """ - epn_conf = textwrap.dedent(''' - [global] - smtp_user={user} - smtp_password={password} - smtp_port=465 - smtp_security=ssl - '''.format(user=self.master.config.admin_name, - password=self.master.config.admin_password)) + epn_conf = SSL_EPN_CONF.format( + user=self.master.config.admin_name, + password=self.master.config.admin_password, + ) self.master.put_file_contents('/etc/ipa/epn.conf', epn_conf) configure_ssl(self.master) From 23a9f75de62d13202ae512fa5656d992e4e43d68 Mon Sep 17 00:00:00 2001 From: Stanislav Levin <s...@altlinux.org> Date: Thu, 12 Nov 2020 18:35:14 +0300 Subject: [PATCH 3/6] test_epn: Standardize EPN configs for deduplication Signed-off-by: Stanislav Levin <s...@altlinux.org> --- ipatests/test_integration/test_epn.py | 71 ++++++++++++++------------- 1 file changed, 38 insertions(+), 33 deletions(-) diff --git a/ipatests/test_integration/test_epn.py b/ipatests/test_integration/test_epn.py index b9cde8fe680..afc8489b6fe 100644 --- a/ipatests/test_integration/test_epn.py +++ b/ipatests/test_integration/test_epn.py @@ -43,20 +43,27 @@ EPN_PKG = ["*ipa-client-epn"] -STARTTLS_EPN_CONF = textwrap.dedent( +DEFAULT_EPN_CONF = textwrap.dedent( """\ [global] + """ +) + +USER_EPN_CONF = DEFAULT_EPN_CONF + textwrap.dedent( + """\ smtp_user={user} smtp_password={password} + """ +) + +STARTTLS_EPN_CONF = USER_EPN_CONF + textwrap.dedent( + """\ smtp_security=starttls """ ) -SSL_EPN_CONF = textwrap.dedent( +SSL_EPN_CONF = USER_EPN_CONF + textwrap.dedent( """\ - [global] - smtp_user={user} - smtp_password={password} smtp_port=465 smtp_security=ssl """ @@ -387,10 +394,7 @@ def test_EPN_smoketest_1(self): With the default configuration, the result should be an empty list. Also check behavior on master and client alike. """ - epn_conf = textwrap.dedent(''' - [global] - ''') - self.master.put_file_contents('/etc/ipa/epn.conf', epn_conf) + self.master.put_file_contents('/etc/ipa/epn.conf', DEFAULT_EPN_CONF) # check EPN on client (LDAP+GSSAPI) (stdout_text, unused, _unused) = self._check_epn_output( self.clients[0], dry_run=True @@ -609,12 +613,10 @@ def test_EPN_nbdays_input_4(self): def test_EPN_authenticated(self, cleanupmail): """Enable authentication and test that mail is delivered """ - epn_conf = textwrap.dedent(''' - [global] - smtp_user={user} - smtp_password={password} - '''.format(user=self.master.config.admin_name, - password=self.master.config.admin_password)) + epn_conf = USER_EPN_CONF.format( + user=self.master.config.admin_name, + password=self.master.config.admin_password, + ) self.master.put_file_contents('/etc/ipa/epn.conf', epn_conf) tasks.ipa_epn(self.master) @@ -648,14 +650,18 @@ def test_mailtest(self, cleanupmail): Using a non-expired user here, user2, to receive the result. """ - epn_conf = textwrap.dedent(''' - [global] - smtp_user={user} - smtp_password={password} - smtp_admin=user2@{domain} - '''.format(user=self.master.config.admin_name, - password=self.master.config.admin_password, - domain=self.master.domain.name)) + epn_conf = ( + USER_EPN_CONF + + textwrap.dedent( + """\ + smtp_admin=user2@{domain} + """ + ) + ).format( + user=self.master.config.admin_name, + password=self.master.config.admin_password, + domain=self.master.domain.name, + ) self.master.put_file_contents('/etc/ipa/epn.conf', epn_conf) tasks.ipa_epn(self.master, mailtest=True) @@ -704,19 +710,21 @@ def test_EPN_ssl(self, cleanupmail): def test_EPN_delay_config(self, cleanupmail): """Test the smtp_delay configuration option """ - epn_conf = textwrap.dedent(''' - [global] + epn_conf = DEFAULT_EPN_CONF + textwrap.dedent( + """\ smtp_delay=A - ''') + """ + ) self.master.put_file_contents('/etc/ipa/epn.conf', epn_conf) result = tasks.ipa_epn(self.master, raiseonerr=False) assert "could not convert string to float: 'A'" in result.stderr_text - epn_conf = textwrap.dedent(''' - [global] + epn_conf = DEFAULT_EPN_CONF + textwrap.dedent( + """\ smtp_delay=-1 - ''') + """ + ) self.master.put_file_contents('/etc/ipa/epn.conf', epn_conf) result = tasks.ipa_epn(self.master, raiseonerr=False) assert "smtp_delay cannot be less than zero" in result.stderr_text @@ -726,10 +734,7 @@ def test_EPN_admin(self): It also doesn't by default have an e-mail address Check --dry-run output. """ - epn_conf = textwrap.dedent(''' - [global] - ''') - self.master.put_file_contents('/etc/ipa/epn.conf', epn_conf) + self.master.put_file_contents('/etc/ipa/epn.conf', DEFAULT_EPN_CONF) self.master.run_command( ['ipa', 'user-mod', 'admin', '--password-expiration', datetime_to_generalized_time( From 662876b3b14ed691470000e95b5ee718a624e224 Mon Sep 17 00:00:00 2001 From: Stanislav Levin <s...@altlinux.org> Date: Thu, 12 Nov 2020 18:52:51 +0300 Subject: [PATCH 4/6] EPN: Enable certificate validation and hostname checking https://pagure.io/freeipa/issue/8579 Signed-off-by: Stanislav Levin <s...@altlinux.org> --- ipaclient/install/ipa_epn.py | 23 ++++++++++++++++++++--- ipatests/test_integration/test_epn.py | 25 ++++++++++++++++++------- 2 files changed, 38 insertions(+), 10 deletions(-) diff --git a/ipaclient/install/ipa_epn.py b/ipaclient/install/ipa_epn.py index 17b0420a338..79fa8f5a01f 100644 --- a/ipaclient/install/ipa_epn.py +++ b/ipaclient/install/ipa_epn.py @@ -29,6 +29,7 @@ import pwd import logging import smtplib +import ssl import time from collections import deque @@ -205,6 +206,7 @@ class EPN(admintool.AdminTool): def __init__(self, options, args): super(EPN, self).__init__(options, args) self._conn = None + self._ssl_context = None self._expiring_password_user_list = EPNUserList() self._ldap_data = [] self._date_ranges = [] @@ -291,12 +293,15 @@ def run(self): logger.error("IPA client is not configured on this system.") raise admintool.ScriptError() + # tasks required privileges self._get_krb5_ticket() self._read_configuration() self._validate_configuration() self._parse_configuration() self._get_connection() self._read_ipa_configuration() + self._create_ssl_context() + drop_privileges() if self.options.mailtest: self._gentestdata() @@ -316,6 +321,7 @@ def run(self): smtp_timeout=api.env.smtp_timeout, smtp_username=api.env.smtp_user, smtp_password=api.env.smtp_password, + ssl_context=self._ssl_context, x_mailer=self.command_name, msg_subtype=api.env.msg_subtype, msg_charset=api.env.msg_charset, @@ -457,6 +463,14 @@ def _get_connection(self): return self._conn + def _create_ssl_context(self): + """Create SSL context. + This must be done before the dropping priviliges to allow + read in the smtp client's certificate and private key if specified. + """ + if api.env.smtp_security.lower() in ("starttls", "ssl"): + self._ssl_context = ssl.create_default_context() + def _fetch_data_from_ldap(self, date_range): """Run a LDAP query to fetch a list of user entries whose passwords would expire in the near future. Store in self._ldap_data. @@ -603,15 +617,15 @@ def __init__( smtp_timeout=60, smtp_username=None, smtp_password=None, + ssl_context=None, ): - # We only support "none" (cleartext) for now. - # Future values: "ssl", "starttls" self._security_protocol = security_protocol self._smtp_hostname = smtp_hostname self._smtp_port = smtp_port self._smtp_timeout = smtp_timeout self._username = smtp_username self._password = smtp_password + self._ssl_context = ssl_context # This should not be touched self._conn = None @@ -664,6 +678,7 @@ def _connect(self): host=self._smtp_hostname, port=self._smtp_port, timeout=self._smtp_timeout, + context=self._ssl_context, ) except (socketerror, smtplib.SMTPException) as e: msg = \ @@ -687,7 +702,7 @@ def _connect(self): if self._security_protocol.lower() == "starttls": try: - self._conn.starttls() + self._conn.starttls(context=self._ssl_context) self._conn.ehlo() except smtplib.SMTPException as e: raise RuntimeError( @@ -743,6 +758,7 @@ def __init__( smtp_timeout=60, smtp_username=None, smtp_password=None, + ssl_context=None, x_mailer=None, msg_subtype="plain", msg_charset="utf8", @@ -766,6 +782,7 @@ def __init__( smtp_timeout=smtp_timeout, smtp_username=smtp_username, smtp_password=smtp_password, + ssl_context=ssl_context, ) def cleanup(self): diff --git a/ipatests/test_integration/test_epn.py b/ipatests/test_integration/test_epn.py index afc8489b6fe..2594e5b58d3 100644 --- a/ipatests/test_integration/test_epn.py +++ b/ipatests/test_integration/test_epn.py @@ -37,6 +37,7 @@ from ipaplatform.paths import paths from ipatests.test_integration.base import IntegrationTest +from ipatests.pytest_ipa.integration.firewall import Firewall from ipatests.pytest_ipa.integration import tasks logger = logging.getLogger(__name__) @@ -58,12 +59,14 @@ STARTTLS_EPN_CONF = USER_EPN_CONF + textwrap.dedent( """\ + smtp_server={server} smtp_security=starttls """ ) SSL_EPN_CONF = USER_EPN_CONF + textwrap.dedent( """\ + smtp_server={server} smtp_port=465 smtp_security=ssl """ @@ -125,6 +128,9 @@ def configure_postfix(host, realm): # disable procmail if exists, make use of default local(8) delivery agent postconf(host, "mailbox_command=") + # listen on all active interfaces + postconf(host, "inet_interfaces = all") + host.run_command(["systemctl", "restart", "saslauthd"]) result = host.run_command(["postconf", "mydestination"]) @@ -264,6 +270,7 @@ def install(cls, mh): # doesn't know about. # - Adds a class variable, pkg, containing the package name of # the downloaded *ipa-client-epn rpm. + hosts = [cls.master, cls.clients[0]] tasks.uninstall_packages(cls.clients[0],EPN_PKG) pkgdir = tasks.download_packages(cls.clients[0], EPN_PKG) pkg = cls.clients[0].run_command(r'ls -1 {}'.format(pkgdir)) @@ -273,20 +280,20 @@ def install(cls, mh): '/tmp']) cls.clients[0].run_command(r'rm -rf {}'.format(pkgdir)) - tasks.install_packages(cls.master, EPN_PKG) - tasks.install_packages(cls.master, ["postfix"]) - tasks.install_packages(cls.clients[0], EPN_PKG) - tasks.install_packages(cls.clients[0], ["postfix"]) - for host in (cls.master, cls.clients[0]): + for host in hosts: + tasks.install_packages(host, EPN_PKG + ["postfix"]) try: tasks.install_packages(host, ["cyrus-sasl"]) except Exception: # the package is likely already installed pass + tasks.install_master(cls.master, setup_dns=True) tasks.install_client(cls.master, cls.clients[0]) - configure_postfix(cls.master, cls.master.domain.realm) - configure_postfix(cls.clients[0], cls.master.domain.realm) + for host in hosts: + configure_postfix(host, cls.master.domain.realm) + Firewall(host).enable_services(["smtp", "smtps"]) + @classmethod def uninstall(cls, mh): @@ -356,6 +363,7 @@ def test_EPN_no_security_downgrade_starttls(self): """Configure postfix without starttls and test no auth happens """ epn_conf = STARTTLS_EPN_CONF.format( + server=self.master.hostname, user=self.master.config.admin_name, password=self.master.config.admin_password, ) @@ -373,6 +381,7 @@ def test_EPN_no_security_downgrade_tls(self): """Configure postfix without tls and test no auth happens """ epn_conf = SSL_EPN_CONF.format( + server=self.master.hostname, user=self.master.config.admin_name, password=self.master.config.admin_password, ) @@ -681,6 +690,7 @@ def test_EPN_starttls(self, cleanupmail): """Configure with starttls and test delivery """ epn_conf = STARTTLS_EPN_CONF.format( + server=self.master.hostname, user=self.master.config.admin_name, password=self.master.config.admin_password, ) @@ -696,6 +706,7 @@ def test_EPN_ssl(self, cleanupmail): """Configure with ssl and test delivery """ epn_conf = SSL_EPN_CONF.format( + server=self.master.hostname, user=self.master.config.admin_name, password=self.master.config.admin_password, ) From 801e1da6070d4d9e43cb95837e5466ef4aea58f1 Mon Sep 17 00:00:00 2001 From: Stanislav Levin <s...@altlinux.org> Date: Thu, 12 Nov 2020 19:21:05 +0300 Subject: [PATCH 5/6] EPN: Allow authentication by SMTP client's certificate SMTP server may ask or require client's certificate for verification. To support this the underlying Python's functionality is used [0]. Added 3 new options(corresponds to `load_cert_chain`): - smtp_client_cert - the path to a single file in PEM format containing the certificate. - smtp_client_key - the path to a file containing the private key in. - smtp_client_key_pass - the password for decrypting the private key. [0]: https://docs.python.org/3/library/ssl.html#ssl.SSLContext.load_cert_chain Fixes: https://pagure.io/freeipa/issue/8580 Signed-off-by: Stanislav Levin <s...@altlinux.org> --- client/man/epn.conf.5 | 9 ++ client/share/epn.conf | 17 ++++ ipaclient/install/ipa_epn.py | 9 ++ ipatests/test_integration/test_epn.py | 141 ++++++++++++++++++-------- 4 files changed, 136 insertions(+), 40 deletions(-) diff --git a/client/man/epn.conf.5 b/client/man/epn.conf.5 index df1f0156c85..60508d292bd 100644 --- a/client/man/epn.conf.5 +++ b/client/man/epn.conf.5 @@ -60,6 +60,15 @@ Specifies the id of the user to authenticate with the SMTP server. Default None. .B smtp_password <password> Specifies the password for the authorized user. Default None. .TP +.B smtp_client_cert <certificate> +Specifies the path to a single file in PEM format containing the certificate. Default None. +.TP +.B smtp_client_key <private key> +Specifies the path to a file containing the private key in. Otherwise the private key will be taken from certfile as well. Default None. +.TP +.B smtp_client_key_pass <private key password> +Specifies the password for decrypting the private key. Default None. +.TP .B smtp_timeout <seconds> Specifies the number of seconds to wait for SMTP to respond. Default 60. .TP diff --git a/client/share/epn.conf b/client/share/epn.conf index e3645801cbb..fa0bc299cf1 100644 --- a/client/share/epn.conf +++ b/client/share/epn.conf @@ -23,6 +23,23 @@ smtp_port = 25 # Default None (empty value). # smtp_password = +# Specifies the path to a single file in PEM format containing the certificate. +# https://docs.python.org/3/library/ssl.html#ssl.SSLContext.load_cert_chain +# Default None (empty value). +# smtp_client_cert = + +# Specifies the path to a file containing the private key in. Otherwise the +# private key will be taken from certfile as well. +# https://docs.python.org/3/library/ssl.html#ssl.SSLContext.load_cert_chain +# Default None (empty value). +# smtp_client_key = + +# Specifies the password for decrypting the private key. It will be ignored if +# the private key is not encrypted and no password is needed. +# https://docs.python.org/3/library/ssl.html#ssl.SSLContext.load_cert_chain +# Default None (empty value). +# smtp_client_key_pass = + # Specifies the number of seconds to wait for SMTP to respond. smtp_timeout = 60 diff --git a/ipaclient/install/ipa_epn.py b/ipaclient/install/ipa_epn.py index 79fa8f5a01f..14bf686f72d 100644 --- a/ipaclient/install/ipa_epn.py +++ b/ipaclient/install/ipa_epn.py @@ -56,6 +56,9 @@ "smtp_port": 25, "smtp_user": None, "smtp_password": None, + "smtp_client_cert": None, + "smtp_client_key": None, + "smtp_client_key_pass": None, "smtp_timeout": 60, "smtp_security": "none", "smtp_admin": "root@localhost", @@ -470,6 +473,12 @@ def _create_ssl_context(self): """ if api.env.smtp_security.lower() in ("starttls", "ssl"): self._ssl_context = ssl.create_default_context() + if api.env.smtp_client_cert: + self._ssl_context.load_cert_chain( + certfile=api.env.smtp_client_cert, + keyfile=api.env.smtp_client_key, + password=str(api.env.smtp_client_key_pass), + ) def _fetch_data_from_ldap(self, date_range): """Run a LDAP query to fetch a list of user entries whose passwords diff --git a/ipatests/test_integration/test_epn.py b/ipatests/test_integration/test_epn.py index 2594e5b58d3..8b1b11c340e 100644 --- a/ipatests/test_integration/test_epn.py +++ b/ipatests/test_integration/test_epn.py @@ -44,6 +44,13 @@ EPN_PKG = ["*ipa-client-epn"] +SMTP_CLIENT_CERT = os.path.join(paths.OPENSSL_CERTS_DIR, "smtp_client.pem") +SMTP_CLIENT_KEY = os.path.join(paths.OPENSSL_PRIVATE_DIR, "smtp_client.key") +SMTP_CLIENT_KEY_PASS = "Secret123" + +SMTPD_KEY = os.path.join(paths.OPENSSL_PRIVATE_DIR, "postfix.key") +SMTPD_CERT = os.path.join(paths.OPENSSL_CERTS_DIR, "postfix.pem") + DEFAULT_EPN_CONF = textwrap.dedent( """\ [global] @@ -72,6 +79,13 @@ """ ) +CLIENT_CERT_EPN_CONF = textwrap.dedent( + """\ + smtp_client_cert={client_cert} + smtp_client_key={client_key} + smtp_client_key_pass={client_key_pass} + """ +) def datetime_to_generalized_time(dt): """Convert datetime to LDAP_GENERALIZED_TIME_FORMAT @@ -146,17 +160,10 @@ def configure_starttls(host): Depends on configure_postfix() being executed first. """ - host.run_command( - ["rm", "-f", os.path.join(paths.OPENSSL_PRIVATE_DIR, "postfix.key")] - ) - host.run_command( - ["rm", "-f", os.path.join(paths.OPENSSL_CERTS_DIR, "postfix.pem")] - ) + host.run_command(["rm", "-f", SMTPD_KEY, SMTPD_CERT]) host.run_command(["ipa-getcert", "request", - "-f", - os.path.join(paths.OPENSSL_CERTS_DIR, "postfix.pem"), - "-k", - os.path.join(paths.OPENSSL_PRIVATE_DIR, "postfix.key"), + "-f", SMTPD_CERT, + "-k", SMTPD_KEY, "-K", "smtp/%s" % host.hostname, "-D", host.hostname, "-O", "postfix", @@ -167,18 +174,8 @@ def configure_starttls(host): ]) postconf(host, 'smtpd_tls_loglevel = 1') postconf(host, 'smtpd_tls_auth_only = yes') - postconf( - host, - "smtpd_tls_key_file = {}".format( - os.path.join(paths.OPENSSL_PRIVATE_DIR, "postfix.key") - ) - ) - postconf( - host, - "smtpd_tls_cert_file = {}".format( - os.path.join(paths.OPENSSL_CERTS_DIR, "postfix.pem") - ) - ) + postconf(host, "smtpd_tls_key_file = {}".format(SMTPD_KEY)) + postconf(host, "smtpd_tls_cert_file = {}".format(SMTPD_CERT)) postconf(host, 'smtpd_tls_received_header = yes') postconf(host, 'smtpd_tls_session_cache_timeout = 3600s') # announce STARTTLS support to remote SMTP clients, not require @@ -187,6 +184,33 @@ def configure_starttls(host): host.run_command(["systemctl", "restart", "postfix"]) +def configure_ssl_client_cert(host): + """Obtain a TLS cert for the SMTP client and configure postfix for client + certificate verification. + + Depends on configure_starttls(). + """ + host.run_command(["rm", "-f", SMTP_CLIENT_KEY, SMTP_CLIENT_CERT]) + + host.run_command(["ipa-getcert", "request", + "-f", SMTP_CLIENT_CERT, + "-k", SMTP_CLIENT_KEY, + "-K", "smtp_client/%s" % host.hostname, + "-D", host.hostname, + "-P", "Secret123", + "-w", + ]) + + # mandatory TLS encryption + postconf(host, "smtpd_tls_security_level = encrypt") + # require a trusted remote SMTP client certificate + postconf(host, "smtpd_tls_req_ccert = yes") + # CA certificates of root CAs trusted to sign remote SMTP client cert + postconf(host, f"smtpd_tls_CAfile = {paths.IPA_CA_CRT}") + + host.run_command(["systemctl", "restart", "postfix"]) + + def configure_ssl(host): """Enable the ssl listener on port 465. """ @@ -303,26 +327,18 @@ def uninstall(cls, mh): tasks.uninstall_packages(cls.clients[0], EPN_PKG) tasks.uninstall_packages(cls.clients[0], ["postfix"]) cls.master.run_command(r'rm -f /etc/postfix/smtp.keytab') - cls.master.run_command( - [ - "getcert", - "stop-tracking", - "-f", - os.path.join(paths.OPENSSL_CERTS_DIR, "postfix.pem"), - ] - ) - cls.master.run_command( - [ - "rm", - "-f", - os.path.join(paths.OPENSSL_PRIVATE_DIR, "postfix.key"), - ] - ) + + for cert in [SMTPD_CERT, SMTP_CLIENT_CERT]: + cls.master.run_command(["getcert", "stop-tracking", "-f", cert]) + cls.master.run_command( [ "rm", "-f", - os.path.join(paths.OPENSSL_CERTS_DIR, "postfix.pem"), + SMTPD_CERT, + SMTPD_KEY, + SMTP_CLIENT_CERT, + SMTP_CLIENT_KEY, ] ) @@ -342,7 +358,7 @@ def test_EPN_config_file(self): assert epn_conf in cmd1.stdout_text assert epn_template in cmd1.stdout_text cmd2 = self.master.run_command(["sha256sum", epn_conf]) - ck = "192481b52fb591112afd7b55b12a44c6618fdbc7e05a3b1866fd67ec579c51df" + ck = "9977d846539d4945900bd04bae25bf746ac75fb561d3769014002db04e1790b8" assert cmd2.stdout_text.find(ck) == 0 def test_EPN_connection_refused(self): @@ -433,8 +449,10 @@ def cleanupusers(self): @pytest.fixture def cleanupmail(self): """Cleanup any existing mail that has been sent.""" + cmd = ["rm", "-f"] for i in range(30): - self.master.run_command(["rm", "-f", "/var/mail/user%d" % i]) + cmd.append("/var/mail/user%d" % i) + self.master.run_command(cmd) def test_EPN_smoketest_2(self, cleanupusers): """Add a user without password. @@ -718,6 +736,49 @@ def test_EPN_ssl(self, cleanupmail): validate_mail(self.master, i, "Hi test user,\nYour login entry user%d is going" % i) + def test_EPN_ssl_client_cert(self, cleanupmail): + """Configure with ssl + client certificate and test delivery + """ + epn_conf = (SSL_EPN_CONF + CLIENT_CERT_EPN_CONF).format( + server=self.master.hostname, + user=self.master.config.admin_name, + password=self.master.config.admin_password, + client_cert=SMTP_CLIENT_CERT, + client_key=SMTP_CLIENT_KEY, + client_key_pass=SMTP_CLIENT_KEY_PASS, + ) + self.master.put_file_contents('/etc/ipa/epn.conf', epn_conf) + configure_ssl_client_cert(self.master) + + tasks.ipa_epn(self.master) + for i in self.notify_ttls: + validate_mail( + self.master, + i, + "Hi test user,\nYour login entry user%d is going" % i + ) + + def test_EPN_starttls_client_cert(self, cleanupmail): + """Configure with starttls + client certificate and test delivery + """ + epn_conf = (STARTTLS_EPN_CONF + CLIENT_CERT_EPN_CONF).format( + server=self.master.hostname, + user=self.master.config.admin_name, + password=self.master.config.admin_password, + client_cert=SMTP_CLIENT_CERT, + client_key=SMTP_CLIENT_KEY, + client_key_pass=SMTP_CLIENT_KEY_PASS, + ) + self.master.put_file_contents('/etc/ipa/epn.conf', epn_conf) + + tasks.ipa_epn(self.master) + for i in self.notify_ttls: + validate_mail( + self.master, + i, + "Hi test user,\nYour login entry user%d is going" % i + ) + def test_EPN_delay_config(self, cleanupmail): """Test the smtp_delay configuration option """ From de86f1d00010e7bd43d0e2b8d78ab10b9f7fa99f Mon Sep 17 00:00:00 2001 From: Stanislav Levin <s...@altlinux.org> Date: Fri, 13 Nov 2020 11:58:52 +0300 Subject: [PATCH 6/6] ipatests: Collect EPN log for debugging Signed-off-by: Stanislav Levin <s...@altlinux.org> --- ipatests/pytest_ipa/integration/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ipatests/pytest_ipa/integration/__init__.py b/ipatests/pytest_ipa/integration/__init__.py index ab7f9114e7a..8fbf5247cf9 100644 --- a/ipatests/pytest_ipa/integration/__init__.py +++ b/ipatests/pytest_ipa/integration/__init__.py @@ -62,6 +62,8 @@ # IPA backup and restore logs paths.IPARESTORE_LOG, paths.IPABACKUP_LOG, + # EPN log + paths.IPAEPN_LOG, # kerberos related logs paths.KADMIND_LOG, paths.KRB5KDC_LOG,
_______________________________________________ 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