Configure Apache Tomcat 11 with automated SSL certificate management using Let's Encrypt and Certbot. Set up HTTPS connector, Java keystore integration, and automatic certificate renewal for production environments.
Prerequisites
- Domain name pointing to your server
- Ports 80 and 443 accessible
- Sudo access
- At least 2GB RAM
What this solves
This tutorial configures Apache Tomcat 11 with automated SSL certificates from Let's Encrypt using Certbot. You'll set up HTTPS connectivity with Java keystore integration and automatic certificate renewal. This approach provides production-ready SSL termination directly in Tomcat without requiring a reverse proxy.
Prerequisites
You need a domain name pointing to your server and ports 80 and 443 open in your firewall. Ensure you have sudo access and at least 2GB RAM available for Tomcat operations.
Step-by-step installation
Update system packages
Start by updating your package manager to ensure you get the latest security updates and dependencies.
sudo apt update && sudo apt upgrade -y
Install Java 17 and required packages
Tomcat 11 requires Java 17 or higher. Install OpenJDK 17 along with wget and other required tools for certificate management.
sudo apt install -y openjdk-17-jdk wget curl unzip
Create Tomcat user
Create a dedicated system user for running Tomcat services. This follows security best practices by avoiding root execution.
sudo useradd -m -U -d /opt/tomcat -s /bin/false tomcat
Download and install Apache Tomcat 11
Download the latest stable Tomcat 11 release and extract it to the system directory.
cd /tmp
wget https://archive.apache.org/dist/tomcat/tomcat-11/v11.0.0/bin/apache-tomcat-11.0.0.tar.gz
sudo tar xf apache-tomcat-11.0.0.tar.gz -C /opt/tomcat --strip-components=1
sudo chown -R tomcat:tomcat /opt/tomcat
sudo chmod +x /opt/tomcat/bin/*.sh
Install Certbot for Let's Encrypt
Install Certbot to handle SSL certificate generation and renewal from Let's Encrypt authority.
sudo apt install -y certbot
Create systemd service file
Configure Tomcat as a systemd service for automatic startup and service management.
[Unit]
Description=Apache Tomcat Web Application Container
After=network.target
[Service]
Type=forking
Environment=JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
Environment=CATALINA_PID=/opt/tomcat/temp/tomcat.pid
Environment=CATALINA_HOME=/opt/tomcat
Environment=CATALINA_BASE=/opt/tomcat
Environment='CATALINA_OPTS=-Xms512M -Xmx1024M -server -XX:+UseParallelGC'
Environment='JAVA_OPTS=-Djava.awt.headless=true -Djava.security.egd=file:/dev/./urandom'
ExecStart=/opt/tomcat/bin/startup.sh
ExecStop=/opt/tomcat/bin/shutdown.sh
User=tomcat
Group=tomcat
UMask=0007
RestartSec=10
Restart=always
[Install]
WantedBy=multi-user.target
Start and enable Tomcat
Reload systemd configuration and start Tomcat to verify the basic installation works before adding SSL.
sudo systemctl daemon-reload
sudo systemctl enable --now tomcat
sudo systemctl status tomcat
Generate initial SSL certificate
Use Certbot standalone mode to generate the initial SSL certificate. Replace example.com with your actual domain name.
sudo systemctl stop tomcat
sudo certbot certonly --standalone -d example.com
sudo systemctl start tomcat
Create keystore conversion script
Create a script to convert Let's Encrypt PEM certificates into Java keystore format that Tomcat requires.
#!/bin/bash
Configuration
DOMAIN="example.com"
KEYSTORE_PATH="/opt/tomcat/conf/keystore.p12"
KEYSTORE_PASSWORD="changeit"
CERT_PATH="/etc/letsencrypt/live/${DOMAIN}"
Remove existing keystore
rm -f "$KEYSTORE_PATH"
Convert certificates to PKCS12 format
openssl pkcs12 -export \
-in "${CERT_PATH}/fullchain.pem" \
-inkey "${CERT_PATH}/privkey.pem" \
-out "$KEYSTORE_PATH" \
-name tomcat \
-password "pass:${KEYSTORE_PASSWORD}"
Set proper ownership and permissions
chown tomcat:tomcat "$KEYSTORE_PATH"
chmod 600 "$KEYSTORE_PATH"
echo "Certificate conversion completed"
Restart Tomcat to load new certificates
systemctl restart tomcat
Make conversion script executable
Set proper permissions on the certificate conversion script for automated execution.
sudo chmod +x /opt/tomcat/bin/convert-certificates.sh
sudo chown root:root /opt/tomcat/bin/convert-certificates.sh
Run initial certificate conversion
Execute the conversion script to create the initial Java keystore from Let's Encrypt certificates.
sudo /opt/tomcat/bin/convert-certificates.sh
Configure Tomcat SSL connector
Edit the Tomcat server configuration to add HTTPS connector with the SSL keystore. This enables SSL termination directly in Tomcat.
Configure automatic certificate renewal
Create a systemd timer and service for automatic certificate renewal. This ensures SSL certificates stay valid without manual intervention.
[Unit]
Description=Certbot Renewal
[Service]
Type=oneshot
ExecStart=/usr/bin/certbot renew --quiet --deploy-hook "/opt/tomcat/bin/convert-certificates.sh"
User=root
Create renewal timer
Configure the systemd timer to run certificate renewal checks twice daily. Let's Encrypt recommends frequent checks with automatic renewal.
[Unit]
Description=Run certbot twice daily
Requires=certbot-renewal.service
[Timer]
OnCalendar=--* 00,12:00:00
RandomizedDelaySec=3600
Persistent=true
[Install]
WantedBy=timers.target
Enable automatic renewal
Enable and start the systemd timer for automatic certificate renewal monitoring.
sudo systemctl daemon-reload
sudo systemctl enable --now certbot-renewal.timer
sudo systemctl status certbot-renewal.timer
Configure firewall rules
Open the required ports for HTTP and HTTPS traffic. Port 8080 and 8443 are Tomcat's default ports.
sudo ufw allow 8080/tcp
sudo ufw allow 8443/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
Restart Tomcat with SSL configuration
Restart Tomcat to load the new SSL connector configuration and verify HTTPS is working.
sudo systemctl restart tomcat
sudo systemctl status tomcat
Configure production security hardening
Update SSL protocols and ciphers
Configure strong SSL protocols and cipher suites for enhanced security. This configuration disables weak protocols and ciphers.
Configure HTTP to HTTPS redirect
Add security constraints to redirect all HTTP traffic to HTTPS automatically. This ensures encrypted connections for all users.
Protected Context
/*
CONFIDENTIAL
Configure security headers
Add HTTP security headers to protect against common web vulnerabilities. Create a custom valve for header injection.
httpHeaderSecurity
org.apache.catalina.filters.HttpHeaderSecurityFilter
hstsMaxAgeSeconds
31536000
hstsIncludeSubdomains
true
Verify your setup
Test that Tomcat is running with proper SSL configuration and automatic renewal is working.
# Check Tomcat service status
sudo systemctl status tomcat
Test HTTP connection (should redirect to HTTPS)
curl -I http://example.com:8080
Test HTTPS connection
curl -I https://example.com:8443
Verify SSL certificate details
openssl s_client -connect example.com:8443 -servername example.com
Check certificate renewal timer
sudo systemctl status certbot-renewal.timer
Test certificate renewal (dry run)
sudo certbot renew --dry-run
Check keystore contents
keytool -list -v -keystore /opt/tomcat/conf/keystore.p12 -storetype PKCS12
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| Certificate generation fails | Port 80 blocked or domain not pointing to server | Check DNS records and firewall settings for port 80 |
| Tomcat won't start with SSL | Incorrect keystore password or path | Verify keystore path and password in server.xml match conversion script |
| Browser shows certificate error | Keystore not generated or corrupted | Run sudo /opt/tomcat/bin/convert-certificates.sh and restart Tomcat |
| HTTP doesn't redirect to HTTPS | Missing security constraints in web.xml | Add transport-guarantee CONFIDENTIAL setting to web.xml |
| Certificate renewal fails | Tomcat blocking port 80 during renewal | Modify renewal script to stop Tomcat temporarily or use webroot method |
| HTTPS connection refused | Firewall blocking port 8443 | Open port 8443 in firewall configuration |
Monitor SSL certificate status
Set up monitoring to track certificate expiration and renewal status. This helps prevent unexpected certificate expiration issues.
#!/bin/bash
DOMAIN="example.com"
PORT="8443"
WARN_DAYS=30
Get certificate expiration date
EXP_DATE=$(echo | openssl s_client -connect ${DOMAIN}:${PORT} -servername ${DOMAIN} 2>/dev/null | openssl x509 -noout -enddate | cut -d= -f2)
EXP_EPOCH=$(date -d "$EXP_DATE" +%s)
CURRENT_EPOCH=$(date +%s)
DAYS_LEFT=$(( (EXP_EPOCH - CURRENT_EPOCH) / 86400 ))
echo "Certificate for ${DOMAIN} expires in ${DAYS_LEFT} days"
if [ $DAYS_LEFT -lt $WARN_DAYS ]; then
echo "WARNING: Certificate expires soon!"
exit 1
fi
echo "Certificate is valid"
For comprehensive SSL and application monitoring, consider implementing automated monitoring solutions like those discussed in Tomcat JMX monitoring with Grafana.
Next steps
- Monitor Tomcat performance with JMX and Grafana dashboards
- Configure Tomcat database connection pooling with HikariCP optimization
- Configure NGINX SSL termination with Certbot for Let's Encrypt certificates
- Setup Tomcat clustering with HAProxy load balancing for high availability
- Configure Tomcat application security hardening with authentication and authorization
Running this in production?
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
# Global variables
DOMAIN=""
KEYSTORE_PASSWORD="$(openssl rand -base64 32)"
JAVA_HOME=""
# Usage function
usage() {
echo "Usage: $0 <domain>"
echo "Example: $0 example.com"
exit 1
}
# Error handling
cleanup() {
echo -e "${RED}[ERROR] Installation failed. Cleaning up...${NC}"
systemctl stop tomcat 2>/dev/null || true
systemctl disable tomcat 2>/dev/null || true
rm -f /etc/systemd/system/tomcat.service
userdel -r tomcat 2>/dev/null || true
rm -rf /opt/tomcat
systemctl daemon-reload
}
trap cleanup ERR
# Logging functions
log_info() {
echo -e "${GREEN}$1${NC}"
}
log_warn() {
echo -e "${YELLOW}$1${NC}"
}
log_error() {
echo -e "${RED}$1${NC}"
}
# Check if running as root
check_root() {
if [[ $EUID -ne 0 ]]; then
log_error "This script must be run as root"
exit 1
fi
}
# Detect distribution
detect_distro() {
if [ -f /etc/os-release ]; then
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_INSTALL="apt install -y"
PKG_UPDATE="apt update && apt upgrade -y"
JAVA_HOME="/usr/lib/jvm/java-17-openjdk-amd64"
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_INSTALL="dnf install -y"
PKG_UPDATE="dnf update -y"
JAVA_HOME="/usr/lib/jvm/java-17-openjdk"
;;
amzn)
PKG_MGR="yum"
PKG_INSTALL="yum install -y"
PKG_UPDATE="yum update -y"
JAVA_HOME="/usr/lib/jvm/java-17-openjdk"
;;
*)
log_error "Unsupported distro: $ID"
exit 1
;;
esac
else
log_error "Cannot detect distribution"
exit 1
fi
}
# Validate domain
validate_domain() {
if [[ ! "$DOMAIN" =~ ^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$ ]]; then
log_error "Invalid domain format: $DOMAIN"
exit 1
fi
}
# Check prerequisites
check_prerequisites() {
log_info "[1/10] Checking prerequisites..."
# Check available memory
local mem_kb=$(grep MemTotal /proc/meminfo | awk '{print $2}')
local mem_gb=$((mem_kb / 1024 / 1024))
if [[ $mem_gb -lt 2 ]]; then
log_warn "Warning: Less than 2GB RAM available. Tomcat may run slowly."
fi
# Check if ports are available
if netstat -tuln 2>/dev/null | grep -q ":80 \|:443 "; then
log_error "Ports 80 or 443 are already in use"
exit 1
fi
# Test domain resolution
if ! dig +short "$DOMAIN" >/dev/null 2>&1 && ! nslookup "$DOMAIN" >/dev/null 2>&1; then
log_warn "Warning: Domain $DOMAIN may not resolve to this server"
fi
}
# Update system packages
update_system() {
log_info "[2/10] Updating system packages..."
$PKG_UPDATE
}
# Install required packages
install_packages() {
log_info "[3/10] Installing Java 17 and required packages..."
if [[ "$PKG_MGR" == "apt" ]]; then
$PKG_INSTALL openjdk-17-jdk wget curl unzip certbot openssl
else
$PKG_INSTALL java-17-openjdk-devel wget curl unzip certbot openssl
fi
}
# Create tomcat user
create_tomcat_user() {
log_info "[4/10] Creating Tomcat user..."
if ! id tomcat >/dev/null 2>&1; then
useradd -m -U -d /opt/tomcat -s /bin/false tomcat
fi
}
# Install Tomcat
install_tomcat() {
log_info "[5/10] Installing Apache Tomcat 11..."
cd /tmp
if [[ ! -f apache-tomcat-11.0.0.tar.gz ]]; then
wget https://archive.apache.org/dist/tomcat/tomcat-11/v11.0.0/bin/apache-tomcat-11.0.0.tar.gz
fi
mkdir -p /opt/tomcat
tar xf apache-tomcat-11.0.0.tar.gz -C /opt/tomcat --strip-components=1
chown -R tomcat:tomcat /opt/tomcat
chmod +x /opt/tomcat/bin/*.sh
find /opt/tomcat -type d -exec chmod 755 {} \;
find /opt/tomcat -type f -exec chmod 644 {} \;
chmod +x /opt/tomcat/bin/*.sh
}
# Create systemd service
create_systemd_service() {
log_info "[6/10] Creating systemd service..."
cat > /etc/systemd/system/tomcat.service << EOF
[Unit]
Description=Apache Tomcat Web Application Container
After=network.target
[Service]
Type=forking
Environment=JAVA_HOME=$JAVA_HOME
Environment=CATALINA_PID=/opt/tomcat/temp/tomcat.pid
Environment=CATALINA_HOME=/opt/tomcat
Environment=CATALINA_BASE=/opt/tomcat
Environment='CATALINA_OPTS=-Xms512M -Xmx1024M -server -XX:+UseParallelGC'
Environment='JAVA_OPTS=-Djava.awt.headless=true -Djava.security.egd=file:/dev/./urandom'
ExecStart=/opt/tomcat/bin/startup.sh
ExecStop=/opt/tomcat/bin/shutdown.sh
User=tomcat
Group=tomcat
UMask=0007
RestartSec=10
Restart=always
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable tomcat
}
# Generate SSL certificate
generate_ssl_cert() {
log_info "[7/10] Generating SSL certificate..."
systemctl stop tomcat 2>/dev/null || true
# Configure firewall if needed
if command -v ufw >/dev/null 2>&1; then
ufw allow 80/tcp 2>/dev/null || true
ufw allow 443/tcp 2>/dev/null || true
elif command -v firewall-cmd >/dev/null 2>&1; then
firewall-cmd --permanent --add-service=http 2>/dev/null || true
firewall-cmd --permanent --add-service=https 2>/dev/null || true
firewall-cmd --reload 2>/dev/null || true
fi
certbot certonly --standalone --non-interactive --agree-tos --register-unsafely-without-email -d "$DOMAIN"
systemctl start tomcat
}
# Create certificate conversion script
create_cert_script() {
log_info "[8/10] Creating certificate conversion script..."
cat > /opt/tomcat/bin/convert-cert.sh << 'EOF'
#!/bin/bash
set -euo pipefail
DOMAIN="DOMAIN_PLACEHOLDER"
KEYSTORE_PATH="/opt/tomcat/conf/keystore.p12"
KEYSTORE_PASSWORD="PASSWORD_PLACEHOLDER"
CERT_PATH="/etc/letsencrypt/live/${DOMAIN}"
if [[ ! -d "$CERT_PATH" ]]; then
echo "Certificate path not found: $CERT_PATH"
exit 1
fi
rm -f "$KEYSTORE_PATH"
openssl pkcs12 -export \
-in "${CERT_PATH}/fullchain.pem" \
-inkey "${CERT_PATH}/privkey.pem" \
-out "$KEYSTORE_PATH" \
-name tomcat \
-password "pass:${KEYSTORE_PASSWORD}"
chown tomcat:tomcat "$KEYSTORE_PATH"
chmod 600 "$KEYSTORE_PATH"
echo "Certificate conversion completed"
systemctl restart tomcat
EOF
sed -i "s/DOMAIN_PLACEHOLDER/$DOMAIN/g" /opt/tomcat/bin/convert-cert.sh
sed -i "s/PASSWORD_PLACEHOLDER/$KEYSTORE_PASSWORD/g" /opt/tomcat/bin/convert-cert.sh
chmod 755 /opt/tomcat/bin/convert-cert.sh
chown root:root /opt/tomcat/bin/convert-cert.sh
}
# Configure SSL in Tomcat
configure_ssl() {
log_info "[9/10] Configuring SSL in Tomcat..."
# Run certificate conversion
/opt/tomcat/bin/convert-cert.sh
# Backup original server.xml
cp /opt/tomcat/conf/server.xml /opt/tomcat/conf/server.xml.backup
# Configure HTTPS connector
sed -i '/<Service name="Catalina">/a\
<Connector port="443" protocol="org.apache.coyote.http11.Http11NioProtocol"\
maxThreads="150" SSLEnabled="true">\
<SSLHostConfig>\
<Certificate certificateKeystoreFile="conf/keystore.p12"\
certificateKeystorePassword="'$KEYSTORE_PASSWORD'"\
type="RSA" />\
</SSLHostConfig>\
</Connector>' /opt/tomcat/conf/server.xml
# Add HTTP to HTTPS redirect
sed -i '/<\/Host>/i\
<Valve className="org.apache.catalina.valves.rewrite.RewriteValve" />' /opt/tomcat/conf/server.xml
# Create rewrite configuration for HTTP to HTTPS redirect
cat > /opt/tomcat/conf/rewrite.config << EOF
RewriteCond %{SERVER_PORT} 8080
RewriteRule ^(.*)$ https://$DOMAIN\$1 [R=301,L]
EOF
chown tomcat:tomcat /opt/tomcat/conf/rewrite.config
chmod 644 /opt/tomcat/conf/rewrite.config
}
# Setup certificate auto-renewal
setup_auto_renewal() {
log_info "[10/10] Setting up automatic certificate renewal..."
# Create renewal hook
cat > /etc/letsencrypt/renewal-hooks/deploy/tomcat-restart.sh << EOF
#!/bin/bash
/opt/tomcat/bin/convert-cert.sh
EOF
chmod 755 /etc/letsencrypt/renewal-hooks/deploy/tomcat-restart.sh
# Test renewal
certbot renew --dry-run
systemctl restart tomcat
}
# Verification
verify_installation() {
log_info "Verifying installation..."
# Check Tomcat service
if systemctl is-active --quiet tomcat; then
log_info "✓ Tomcat service is running"
else
log_error "✗ Tomcat service is not running"
systemctl status tomcat
exit 1
fi
# Check SSL certificate
if [[ -f "/etc/letsencrypt/live/$DOMAIN/fullchain.pem" ]]; then
log_info "✓ SSL certificate exists"
else
log_error "✗ SSL certificate not found"
exit 1
fi
# Check keystore
if [[ -f "/opt/tomcat/conf/keystore.p12" ]]; then
log_info "✓ Java keystore created"
else
log_error "✗ Java keystore not found"
exit 1
fi
log_info ""
log_info "Installation completed successfully!"
log_info "Your Tomcat server is accessible at:"
log_info "HTTP: http://$DOMAIN:8080 (redirects to HTTPS)"
log_info "HTTPS: https://$DOMAIN:443"
log_info ""
log_info "Keystore password: $KEYSTORE_PASSWORD"
log_info "Save this password for future reference!"
}
# Main execution
main() {
if [[ $# -ne 1 ]]; then
usage
fi
DOMAIN="$1"
check_root
detect_distro
validate_domain
check_prerequisites
update_system
install_packages
create_tomcat_user
install_tomcat
create_systemd_service
generate_ssl_cert
create_cert_script
configure_ssl
setup_auto_renewal
verify_installation
}
main "$@"
Review the script before running. Execute with: bash install.sh