From ec73de969f55b7a005b6401029f87fe6a225a417 Mon Sep 17 00:00:00 2001 From: Alexander Bokovoy Date: Tue, 10 Mar 2020 23:00:55 +0200 Subject: [PATCH] Secure AJP connector between Dogtag and Apache proxy AJP implementation in Tomcat is vulnerable to CVE-2020-1938 if used without shared secret. Set up a shared secret between localhost connector and Apache mod_proxy_ajp pass-through. For existing secured AJP pass-through make sure the option used for configuration on the tomcat side is up to date. Tomcat 9.0.31.0 deprecated 'requiredSecret' option name in favor of 'secret'. Details can be found at https://tomcat.apache.org/migration-9.html#Upgrading_9.0.x Fixes: https://pagure.io/freeipa/issue/8221 Signed-off-by: Alexander Bokovoy Reviewed-By: Christian Heimes Reviewed-By: Florence Blanc-Renaud Reviewed-By: Rob Crittenden --- install/share/ipa-pki-proxy.conf.template | 12 ++-- ipaplatform/base/paths.py | 2 + ipaplatform/debian/paths.py | 1 + ipaserver/install/cainstance.py | 2 + ipaserver/install/dogtaginstance.py | 80 +++++++++++++++++++++++ ipaserver/install/krainstance.py | 2 - ipaserver/install/server/upgrade.py | 8 +++ 7 files changed, 99 insertions(+), 8 deletions(-) diff --git a/install/share/ipa-pki-proxy.conf.template b/install/share/ipa-pki-proxy.conf.template index 04460197e..0710641c3 100644 --- a/install/share/ipa-pki-proxy.conf.template +++ b/install/share/ipa-pki-proxy.conf.template @@ -1,4 +1,4 @@ -# VERSION 13 - DO NOT REMOVE THIS LINE +# VERSION 14 - DO NOT REMOVE THIS LINE ProxyRequests Off @@ -6,7 +6,7 @@ ProxyRequests Off SSLOptions +StdEnvVars +ExportCertData +StrictRequire +OptRenegotiate SSLVerifyClient none - ProxyPassMatch ajp://localhost:$DOGTAG_PORT + ProxyPassMatch ajp://localhost:$DOGTAG_PORT $DOGTAG_AJP_SECRET ProxyPassReverse ajp://localhost:$DOGTAG_PORT @@ -14,7 +14,7 @@ ProxyRequests Off SSLOptions +StdEnvVars +ExportCertData +StrictRequire +OptRenegotiate SSLVerifyClient none - ProxyPassMatch ajp://localhost:$DOGTAG_PORT + ProxyPassMatch ajp://localhost:$DOGTAG_PORT $DOGTAG_AJP_SECRET ProxyPassReverse ajp://localhost:$DOGTAG_PORT @@ -22,7 +22,7 @@ ProxyRequests Off SSLOptions +StdEnvVars +ExportCertData +StrictRequire +OptRenegotiate SSLVerifyClient require - ProxyPassMatch ajp://localhost:$DOGTAG_PORT + ProxyPassMatch ajp://localhost:$DOGTAG_PORT $DOGTAG_AJP_SECRET ProxyPassReverse ajp://localhost:$DOGTAG_PORT @@ -30,7 +30,7 @@ ProxyRequests Off SSLOptions +StdEnvVars +ExportCertData +StrictRequire +OptRenegotiate SSLVerifyClient optional - ProxyPassMatch ajp://localhost:$DOGTAG_PORT + ProxyPassMatch ajp://localhost:$DOGTAG_PORT $DOGTAG_AJP_SECRET ProxyPassReverse ajp://localhost:$DOGTAG_PORT @@ -38,7 +38,7 @@ ProxyRequests Off SSLOptions +StdEnvVars +ExportCertData +StrictRequire +OptRenegotiate SSLVerifyClient optional - ProxyPassMatch ajp://localhost:$DOGTAG_PORT + ProxyPassMatch ajp://localhost:$DOGTAG_PORT $DOGTAG_AJP_SECRET ProxyPassReverse ajp://localhost:$DOGTAG_PORT diff --git a/ipaplatform/base/paths.py b/ipaplatform/base/paths.py index 5f0335f6f..c03c44296 100644 --- a/ipaplatform/base/paths.py +++ b/ipaplatform/base/paths.py @@ -108,6 +108,7 @@ class BasePathNamespace: PKI_TOMCAT_ALIAS_DIR = "/etc/pki/pki-tomcat/alias" PKI_TOMCAT_ALIAS_PWDFILE_TXT = "/etc/pki/pki-tomcat/alias/pwdfile.txt" PKI_TOMCAT_PASSWORD_CONF = "/etc/pki/pki-tomcat/password.conf" + PKI_TOMCAT_SERVER_XML = "/etc/pki/pki-tomcat/server.xml" ETC_REDHAT_RELEASE = "/etc/redhat-release" RESOLV_CONF = "/etc/resolv.conf" SAMBA_KEYTAB = "/etc/samba/samba.keytab" @@ -339,6 +340,7 @@ class BasePathNamespace: KRB5KDC_LOG = "/var/log/krb5kdc.log" MESSAGES = "/var/log/messages" VAR_LOG_PKI_DIR = "/var/log/pki/" + BIN_TOMCAT = "/usr/sbin/tomcat" TOMCAT_TOPLEVEL_DIR = "/var/log/pki/pki-tomcat" TOMCAT_CA_DIR = "/var/log/pki/pki-tomcat/ca" TOMCAT_CA_ARCHIVE_DIR = "/var/log/pki/pki-tomcat/ca/archive" diff --git a/ipaplatform/debian/paths.py b/ipaplatform/debian/paths.py index 764b5a281..1014c6ae3 100644 --- a/ipaplatform/debian/paths.py +++ b/ipaplatform/debian/paths.py @@ -59,6 +59,7 @@ class DebianPathNamespace(BasePathNamespace): SYSCONFIG_PKI = "/etc/dogtag/" SYSCONFIG_PKI_TOMCAT = "/etc/default/pki-tomcat" SYSCONFIG_PKI_TOMCAT_PKI_TOMCAT_DIR = "/etc/dogtag/tomcat/pki-tomcat" + BIN_TOMCAT = "/usr/share/tomcat9/bin/version.sh" SYSTEMD_SYSTEM_HTTPD_D_DIR = "/etc/systemd/system/apache2.service.d/" SYSTEMD_SYSTEM_HTTPD_IPA_CONF = "/etc/systemd/system/apache2.service.d/ipa.conf" DNSSEC_TRUSTED_KEY = "/etc/bind/trusted-key.key" diff --git a/ipaserver/install/cainstance.py b/ipaserver/install/cainstance.py index fe63229af..2836bdd25 100644 --- a/ipaserver/install/cainstance.py +++ b/ipaserver/install/cainstance.py @@ -401,6 +401,7 @@ class CAInstance(DogtagInstance): self.step("configuring certificate server instance", self.__spawn_instance) self.step("Add ipa-pki-wait-running", self.add_ipa_wait) + self.step("secure AJP connector", self.secure_ajp_connector) self.step("reindex attributes", self.reindex_task) self.step("exporting Dogtag certificate store pin", self.create_certstore_passwdfile) @@ -452,6 +453,7 @@ class CAInstance(DogtagInstance): self.step("configure certificate renewals", self.configure_renewal) self.step("Configure HTTP to proxy connections", self.http_proxy) + # This restart is needed for ACL reload in CA, do not remove it self.step("restarting certificate server", self.restart_instance) self.step("updating IPA configuration", update_ipa_conf) self.step("enabling CA instance", self.__enable_instance) diff --git a/ipaserver/install/dogtaginstance.py b/ipaserver/install/dogtaginstance.py index 18a24f02f..efcf81509 100644 --- a/ipaserver/install/dogtaginstance.py +++ b/ipaserver/install/dogtaginstance.py @@ -28,6 +28,9 @@ import os import shutil import traceback import dbus +import re +import pwd +import lxml.etree from configparser import DEFAULTSECT, ConfigParser, RawConfigParser @@ -42,6 +45,7 @@ from ipalib.constants import CA_DBUS_TIMEOUT, IPA_CA_RECORD, RENEWAL_CA_NAME from ipaplatform import services from ipaplatform.constants import constants from ipaplatform.paths import paths +from ipaplatform.tasks import tasks from ipapython import directivesetter from ipapython import ipaldap from ipapython import ipautil @@ -158,6 +162,7 @@ class DogtagInstance(service.Service): self.pki_config_override = None self.ca_subject = None self.subject_base = None + self.ajp_secret = None def is_installed(self): """ @@ -275,6 +280,72 @@ class DogtagInstance(service.Service): logger.critical("failed to uninstall %s instance %s", self.subsystem, e) + def __is_newer_tomcat_version(self, default=None): + try: + result = ipautil.run([paths.BIN_TOMCAT, "version"], + capture_output=True) + sn = re.search( + r'Server number:\s+([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)', + result.output) + if sn is None: + logger.info("tomcat version cannot be parsed, " + "default to pre-%s", default) + return False + v = tasks.parse_ipa_version(sn.group(1)) + if v >= tasks.parse_ipa_version(default): + return True + except ipautil.CalledProcessError as e: + logger.info( + "failed to discover tomcat version, " + "default to pre-%s, error: %s", + default, str(e)) + return False + + def secure_ajp_connector(self): + """ Update AJP connector to use a password protection """ + + server_xml = lxml.etree.parse(paths.PKI_TOMCAT_SERVER_XML) + doc = server_xml.getroot() + + # no AJP connector means no need to update anything + connectors = doc.xpath('//Connector[@port="8009"]') + if len(connectors) == 0: + return + + # AJP connector is set on port 8009. Use non-greedy search to find it + connector = connectors[0] + + # Detect tomcat version and choose the right option name + # pre-9.0.31.0 uses 'requiredSecret' + # 9.0.31.0 or later uses 'secret' + secretattr = 'requiredSecret' + oldattr = 'requiredSecret' + if self.__is_newer_tomcat_version('9.0.31.0'): + secretattr = 'secret' + + rewrite = True + if secretattr in connector.attrib: + # secret is already in place + # Perhaps, we need to synchronize it with Apache configuration + self.ajp_secret = connector.attrib[secretattr] + rewrite = False + else: + if oldattr in connector.attrib: + self.ajp_secret = connector.attrib[oldattr] + connector.attrib[secretattr] = self.ajp_secret + del connector.attrib[oldattr] + else: + # Generate password, don't use special chars to not break XML + self.ajp_secret = ipautil.ipa_generate_password(special=None) + connector.attrib[secretattr] = self.ajp_secret + + if rewrite: + pent = pwd.getpwnam(constants.PKI_USER) + with open(paths.PKI_TOMCAT_SERVER_XML, "wb") as fd: + server_xml.write(fd, pretty_print=True, encoding="utf-8") + os.fchmod(fd.fileno(), 0o660) + os.fchown(fd.fileno(), pent.pw_uid, pent.pw_gid) + def http_proxy(self): """ Update the http proxy file """ template_filename = ( @@ -284,11 +355,20 @@ class DogtagInstance(service.Service): DOGTAG_PORT=8009, CLONE='' if self.clone else '#', FQDN=self.fqdn, + DOGTAG_AJP_SECRET='', ) + if self.ajp_secret: + sub_dict['DOGTAG_AJP_SECRET'] = "secret={}".format(self.ajp_secret) template = ipautil.template_file(template_filename, sub_dict) with open(paths.HTTPD_IPA_PKI_PROXY_CONF, "w") as fd: fd.write(template) os.fchmod(fd.fileno(), 0o640) + # Restart httpd + http_service = services.knownservices.httpd + logger.debug("Restarting %s to apply AJP changes", + http_service.service_name) + http_service.restart() + logger.debug("%s successfully restarted", http_service.service_name) def configure_certmonger_renewal_helpers(self): """ diff --git a/ipaserver/install/krainstance.py b/ipaserver/install/krainstance.py index 09efdcad9..99a53861e 100644 --- a/ipaserver/install/krainstance.py +++ b/ipaserver/install/krainstance.py @@ -133,8 +133,6 @@ class KRAInstance(DogtagInstance): self.step("configure certmonger for renewals", self.configure_certmonger_renewal_helpers) self.step("configure certificate renewals", self.configure_renewal) - self.step("configure HTTP to proxy connections", - self.http_proxy) if not self.clone: self.step("add vault container", self.__add_vault_container) self.step("apply LDAP updates", self.__apply_updates) diff --git a/ipaserver/install/server/upgrade.py b/ipaserver/install/server/upgrade.py index 503e271da..b4ef31199 100644 --- a/ipaserver/install/server/upgrade.py +++ b/ipaserver/install/server/upgrade.py @@ -1966,6 +1966,14 @@ def upgrade_configuration(): os.path.join(paths.USR_SHARE_IPA_DIR, "ipa-kdc-proxy.conf.template")) if ca.is_configured(): + # Handle upgrade of AJP connector configuration + ca.secure_ajp_connector() + if ca.ajp_secret: + sub_dict['DOGTAG_AJP_SECRET'] = "secret={}".format( + ca.ajp_secret) + else: + sub_dict['DOGTAG_AJP_SECRET'] = '' + upgrade_file( sub_dict, paths.HTTPD_IPA_PKI_PROXY_CONF,