This is an automated email from the ASF dual-hosted git repository.
markt pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
The following commit(s) were added to refs/heads/9.0.x by this push:
new 7d388dbd3e Fix BZ 65770 - add a listener to reload TLS certificates
7d388dbd3e is described below
commit 7d388dbd3e43fd9c1406a464505449c0b770b12f
Author: Mark Thomas <[email protected]>
AuthorDate: Mon Sep 25 15:46:27 2023 +0100
Fix BZ 65770 - add a listener to reload TLS certificates
The listener is intended to be used in environments where a 3rd party
component (e.g. certbot) is responsible for regularly renewing the
certificate.
https://bz.apache.org/bugzilla/show_bug.cgi?id=65770
---
.../catalina/security/LocalStrings.properties | 4 +
.../security/TLSCertificateReloadListener.java | 176 +++++++++++++++++++++
java/org/apache/coyote/ProtocolHandler.java | 13 ++
.../org/apache/coyote/ajp/AbstractAjpProtocol.java | 6 +
.../coyote/http11/AbstractHttp11Protocol.java | 6 +
java/org/apache/tomcat/util/net/SSLHostConfig.java | 26 +++
webapps/docs/changelog.xml | 6 +
webapps/docs/config/listeners.xml | 37 +++++
8 files changed, 274 insertions(+)
diff --git a/java/org/apache/catalina/security/LocalStrings.properties
b/java/org/apache/catalina/security/LocalStrings.properties
index f1e1aa689c..b2f6cdf023 100644
--- a/java/org/apache/catalina/security/LocalStrings.properties
+++ b/java/org/apache/catalina/security/LocalStrings.properties
@@ -22,3 +22,7 @@ SecurityListener.checkUserWarning=Start attempted while
running as user [{0}]. R
SecurityUtil.doAsPrivilege=An exception occurs when running the
PrivilegedExceptionAction block.
listener.notServer=This listener must only be nested within Server elements,
but is in [{0}].
+
+tlsCertRenewalListener.notRenewed=[{0}], TLS virtual host [{1}] with name
[{2}] that expires on [{3}] is overdue for renewal
+tlsCertRenewalListener.reloadFailed=[{0}], TLS virtual host [{1}] reload of
TLS configuration failed
+tlsCertRenewalListener.reloadSuccess=[{0}], TLS virtual host [{1}] reloaded
TLS configuration
\ No newline at end of file
diff --git
a/java/org/apache/catalina/security/TLSCertificateReloadListener.java
b/java/org/apache/catalina/security/TLSCertificateReloadListener.java
new file mode 100644
index 0000000000..d9fe20f462
--- /dev/null
+++ b/java/org/apache/catalina/security/TLSCertificateReloadListener.java
@@ -0,0 +1,176 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.catalina.security;
+
+import java.security.cert.X509Certificate;
+import java.util.Calendar;
+import java.util.Set;
+
+import org.apache.catalina.Lifecycle;
+import org.apache.catalina.LifecycleEvent;
+import org.apache.catalina.LifecycleListener;
+import org.apache.catalina.Server;
+import org.apache.catalina.Service;
+import org.apache.catalina.connector.Connector;
+import org.apache.juli.logging.Log;
+import org.apache.juli.logging.LogFactory;
+import org.apache.tomcat.util.net.SSLHostConfig;
+import org.apache.tomcat.util.res.StringManager;
+
+/**
+ * A {@link LifecycleListener} that may be used to monitor the expiration
dates of TLS certificates and trigger
+ * automatic reloading of the TLS configuration a set number of days before
the TLS certificate expires.
+ * <p>
+ * This listener assumes there is some other process (certbot, cloud
infrastructure, etc) that renews the certificate on
+ * a regular basis and replaces the current certificate with the new one.
+ * <p>
+ * This listener does <b>NOT</b> re-read the Tomcat configuration from
server.xml. If you make changes to server.xml you
+ * must restart the Tomcat process to pick up those changes.
+ * <p>
+ */
+public class TLSCertificateReloadListener implements LifecycleListener {
+
+ private static final Log log =
LogFactory.getLog(TLSCertificateReloadListener.class);
+ private static final StringManager sm =
StringManager.getManager(TLSCertificateReloadListener.class);
+
+ // Configuration
+ private int checkPeriod = 24 * 60 * 60;
+ private int daysBefore = 14;
+
+ // State
+ private Calendar nextCheck = Calendar.getInstance();
+
+
+ /**
+ * Get the time, in seconds, between reloading checks.
+ * <p>
+ * The periodic process for {@code LifecycleListener} typically runs much
more frequently than this listener
+ * requires. This attribute controls the period between checks.
+ * <p>
+ * If not specified, a default of 86,400 seconds (24 hours) is used.
+ *
+ * @return The time, in seconds, between reloading checks
+ */
+ public int getCheckPeriod() {
+ return checkPeriod;
+ }
+
+
+ /**
+ * Set the time, in seconds, between reloading checks.
+ *
+ * @param checkPeriod The new time, in seconds, between reloading checks
+ */
+ public void setCheckPeriod(int checkPeriod) {
+ this.checkPeriod = checkPeriod;
+ }
+
+
+ /**
+ * Get the number of days before the expiry of a TLS certificate that it
is expected that the new certificate will
+ * be in place and the reloading can be triggered.
+ * <p>
+ * If not specified, a default of 14 days is used.
+ *
+ * @return The number of days before the expiry of a TLS certificate that
the reloading will be triggered
+ */
+ public int getDaysBefore() {
+ return daysBefore;
+ }
+
+
+ /**
+ * Set the number of days before the expiry of a TLS certificate that it
is expected that the new certificate will
+ * be in place and the reloading can be triggered.
+ *
+ * @param daysBefore the number of days before the expiry of the current
certificate that reloading will be
+ * triggered
+ */
+ public void setDaysBefore(int daysBefore) {
+ this.daysBefore = daysBefore;
+ }
+
+
+ @Override
+ public void lifecycleEvent(LifecycleEvent event) {
+ if (event.getType().equals(Lifecycle.PERIODIC_EVENT)) {
+ Server server;
+ if (event.getSource() instanceof Server) {
+ server = (Server) event.getSource();
+ } else {
+ return;
+ }
+ checkCertificatesForRenewal(server);
+ } else if (event.getType().equals(Lifecycle.BEFORE_INIT_EVENT)) {
+ // This is the earliest event in Lifecycle
+ if (!(event.getLifecycle() instanceof Server)) {
+ log.warn(sm.getString("listener.notServer",
event.getLifecycle().getClass().getSimpleName()));
+ }
+ }
+ }
+
+
+ private void checkCertificatesForRenewal(Server server) {
+ // Only run the check once every checkPeriod (seconds)
+ Calendar calendar = Calendar.getInstance();
+ if (calendar.compareTo(nextCheck) > 0) {
+ nextCheck.add(Calendar.SECOND, getCheckPeriod());
+ } else {
+ return;
+ }
+
+ /*
+ * Advance current date by "daysBefore". Any certificates that expire
before this time should have been renewed
+ * by now so reloading the associated SSLHostConfig should pick up the
new certificate.
+ */
+ calendar.add(Calendar.DAY_OF_MONTH, getDaysBefore());
+
+ // Check all of the certificates
+ Service[] services = server.findServices();
+ for (Service service : services) {
+ Connector[] connectors = service.findConnectors();
+ for (Connector connector : connectors) {
+ SSLHostConfig[] sslHostConfigs =
connector.findSslHostConfigs();
+ for (SSLHostConfig sslHostConfig : sslHostConfigs) {
+ if
(!sslHostConfig.certificatesExpiringBefore(calendar.getTime()).isEmpty()) {
+ // One or more certificates is due to expire and
should have been renewed
+ // Reload the configuration
+ try {
+
connector.getProtocolHandler().addSslHostConfig(sslHostConfig, true);
+ // Now check again
+ Set<X509Certificate> expiringCertificates =
+
sslHostConfig.certificatesExpiringBefore(calendar.getTime());
+
log.info(sm.getString("tlsCertRenewalListener.reloadSuccess", connector,
+ sslHostConfig.getHostName()));
+ if (!expiringCertificates.isEmpty()) {
+ for (X509Certificate expiringCertificate :
expiringCertificates) {
+
log.warn(sm.getString("tlsCertRenewalListener.notRenewed", connector,
+ sslHostConfig.getHostName(),
+
expiringCertificate.getSubjectX500Principal().getName(),
+
expiringCertificate.getNotAfter()));
+ }
+ }
+ } catch (IllegalArgumentException iae) {
+
log.error(sm.getString("tlsCertRenewalListener.reloadFailed", connector,
+ sslHostConfig.getHostName()), iae);
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/java/org/apache/coyote/ProtocolHandler.java
b/java/org/apache/coyote/ProtocolHandler.java
index 7e7dfdb819..55de6e494f 100644
--- a/java/org/apache/coyote/ProtocolHandler.java
+++ b/java/org/apache/coyote/ProtocolHandler.java
@@ -173,6 +173,19 @@ public interface ProtocolHandler {
void addSslHostConfig(SSLHostConfig sslHostConfig);
+ /**
+ * Add a new SSL configuration for a virtual host.
+ *
+ * @param sslHostConfig the configuration
+ * @param replace If {@code true} replacement of an existing
configuration is permitted, otherwise any such
+ * attempted replacement will trigger an exception
+ *
+ * @throws IllegalArgumentException If the host name is not valid or if a
configuration has already been provided
+ * for that host and replacement is
not allowed
+ */
+ void addSslHostConfig(SSLHostConfig sslHostConfig, boolean replace);
+
+
/**
* Find all configured SSL virtual host configurations which will be used
by SNI.
*
diff --git a/java/org/apache/coyote/ajp/AbstractAjpProtocol.java
b/java/org/apache/coyote/ajp/AbstractAjpProtocol.java
index ff64a1cd0b..8241a7b964 100644
--- a/java/org/apache/coyote/ajp/AbstractAjpProtocol.java
+++ b/java/org/apache/coyote/ajp/AbstractAjpProtocol.java
@@ -231,6 +231,12 @@ public abstract class AbstractAjpProtocol<S> extends
AbstractProtocol<S> {
}
+ @Override
+ public void addSslHostConfig(SSLHostConfig sslHostConfig, boolean replace)
{
+ getLog().warn(sm.getString("ajpprotocol.noSSL",
sslHostConfig.getHostName()));
+ }
+
+
@Override
public SSLHostConfig[] findSslHostConfigs() {
return new SSLHostConfig[0];
diff --git a/java/org/apache/coyote/http11/AbstractHttp11Protocol.java
b/java/org/apache/coyote/http11/AbstractHttp11Protocol.java
index 00e4873cd8..cc6988d308 100644
--- a/java/org/apache/coyote/http11/AbstractHttp11Protocol.java
+++ b/java/org/apache/coyote/http11/AbstractHttp11Protocol.java
@@ -793,6 +793,12 @@ public abstract class AbstractHttp11Protocol<S> extends
AbstractProtocol<S> {
}
+ @Override
+ public void addSslHostConfig(SSLHostConfig sslHostConfig, boolean replace)
{
+ getEndpoint().addSslHostConfig(sslHostConfig, replace);
+ }
+
+
@Override
public SSLHostConfig[] findSslHostConfigs() {
return getEndpoint().findSslHostConfigs();
diff --git a/java/org/apache/tomcat/util/net/SSLHostConfig.java
b/java/org/apache/tomcat/util/net/SSLHostConfig.java
index ae2252529a..b2d944ee9b 100644
--- a/java/org/apache/tomcat/util/net/SSLHostConfig.java
+++ b/java/org/apache/tomcat/util/net/SSLHostConfig.java
@@ -22,6 +22,8 @@ import java.io.IOException;
import java.io.Serializable;
import java.security.KeyStore;
import java.security.UnrecoverableKeyException;
+import java.security.cert.X509Certificate;
+import java.util.Date;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
@@ -884,6 +886,30 @@ public class SSLHostConfig implements Serializable {
// --------------------------------------------------------- Support
methods
+ public Set<X509Certificate> certificatesExpiringBefore(Date date) {
+ Set<X509Certificate> result = new HashSet<>();
+ Set<SSLHostConfigCertificate> sslHostConfigCertificates =
getCertificates();
+ for (SSLHostConfigCertificate sslHostConfigCertificate :
sslHostConfigCertificates) {
+ SSLContext sslContext = sslHostConfigCertificate.getSslContext();
+ if (sslContext != null) {
+ String alias =
sslHostConfigCertificate.getCertificateKeyAlias();
+ if (alias == null) {
+ alias = SSLUtilBase.DEFAULT_KEY_ALIAS;
+ }
+ X509Certificate[] certificates =
sslContext.getCertificateChain(alias);
+ if (certificates != null && certificates.length > 0) {
+ X509Certificate certificate = certificates[0];
+ Date expirationDate = certificate.getNotAfter();
+ if (date.after(expirationDate)) {
+ result.add(certificate);
+ }
+ }
+ }
+ }
+ return result;
+ }
+
+
public static String adjustRelativePath(String path) throws
FileNotFoundException {
// Empty or null path can't point to anything useful. The assumption is
// that the value is deliberately empty / null so leave it that way.
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index 0e6dd3e952..8a55546f20 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -107,6 +107,12 @@
<section name="Tomcat 9.0.81 (remm)" rtext="in development">
<subsection name="Catalina">
<changelog>
+ <add>
+ <bug>65770</bug>: Provide a lifecycle listener that will automatically
+ reload TLS configurations a set time before the certificate is due to
+ expire. This is intended to be used with third-party tools that
+ regularly renew TLS certificates. (markt)
+ </add>
<fix>
Fix handling of an error reading a context descriptor on deployment.
(remm)
diff --git a/webapps/docs/config/listeners.xml
b/webapps/docs/config/listeners.xml
index 1ad5c8f24a..b6201c2463 100644
--- a/webapps/docs/config/listeners.xml
+++ b/webapps/docs/config/listeners.xml
@@ -492,6 +492,43 @@
</subsection>
+ <subsection name="TLS configuration reload listener -
org.apache.catalina.security.TLSCertificateReloadListener">
+
+ <p>This listener may be used to monitor the expiration dates of TLS
+ certificates and trigger automatic reloading of the TLS configuration a set
+ number of days before the TLS certificate expires.</p>
+
+ <p>This listener assumes there is some other process (certbot, cloud
+ infrastructure, etc) that renews the certificate on a regular basis and
+ replaces the current certificate with the new one.</p>
+
+ <p>This listener does <b>NOT</b> re-read the Tomcat configuration from
+ server.xml. If you make changes to server.xml you must restart the Tomcat
+ process to pick up those changes.</p>
+
+ <p>This listener must only be nested within <a
href="server.html">Server</a>
+ elements.</p>
+
+ <attributes>
+
+ <attribute name="checkPeriod" required="false">
+ <p>The time, in seconds, between reloading checks. The periodic process
+ for <code>LifecycleListener</code> typically runs much more frequently
+ than this listener requires. This attribute controls the period between
+ checks. If not specified, a default of 86,400 seconds (24 hours) is
+ used.</p>
+ </attribute>
+
+ <attribute name="daysBefore" required="false">
+ <p>The number of days before the expiry of a TLS certificate that it is
+ expected that the new certificate will be in place and the reloading
can
+ be triggered. If not specified, a default of 14 days is used.</p>
+ </attribute>
+
+ </attributes>
+
+ </subsection>
+
<subsection name="UserConfig - org.apache.catalina.startup.UserConfig">
<p>The <strong>UserConfig</strong> provides feature of User Web
Applications.
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]