TLSCertificateReloadListener.java
/*
* 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.text.SimpleDateFormat;
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.
*/
public class TLSCertificateReloadListener implements LifecycleListener {
private static final Log log = LogFactory.getLog(TLSCertificateReloadListener.class);
private static final StringManager sm = StringManager.getManager(TLSCertificateReloadListener.class);
private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX");
// 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(),
dateFormat.format(expiringCertificate.getNotAfter())));
}
}
} catch (IllegalArgumentException iae) {
log.error(sm.getString("tlsCertRenewalListener.reloadFailed", connector,
sslHostConfig.getHostName()), iae);
}
}
}
}
}
}
}