Your Apache Traffic Server instance is running, proxying requests to your backend, and caching content effectively, but every request is travelling over plain HTTP. Browsers flag the connection as “Not Secure,” users see warnings before they even read your content, and any cookies or form data are readable to anyone on the same network path.
The fix is SSL/TLS termination at the proxy layer. Instead of configuring a certificate on every backend server, you configure it once on ATS. Clients connect to ATS over HTTPS; ATS decrypts the traffic, applies its caching and routing logic, then forwards plain HTTP to your backend over a private internal connection. One certificate. One place to manage it. Every backend benefits automatically.
In this tutorial, you will configure Apache Traffic Server on Ubuntu to terminate SSL connections on port 443, set up your certificate (either a free one from Let’s Encrypt using Certbot, or an existing certificate you already own), redirect all HTTP traffic to HTTPS, and set up automatic certificate renewal so the setup stays valid indefinitely without manual intervention.
What Is SSL Termination?
SSL termination means the proxy handles the TLS handshake and decryption on behalf of your backend servers. The encrypted connection “terminates” at the proxy.
This is different from SSL passthrough, where the proxy forwards encrypted traffic directly to the backend without reading it. Passthrough is simpler but you lose the ability to cache, inspect, or modify the response at the proxy level which defeats much of the purpose of running ATS.
With SSL termination at ATS:
- Certificate management is centralized. Renew one cert instead of updating every backend.
- Backends stay simple. They can serve plain HTTP on an internal interface with no certificate configuration.
- Caching still works. ATS decrypts the response, can cache it, and serves the cached copy over HTTPS to the next client.
- TLS processing happens once. The edge handles the expensive handshake; your backends only see cleartext HTTP.
In ATS, SSL termination is built in. You tell ATS to listen on a port with the ssl flag, point it at your certificate files, and ATS takes care of TLS negotiation for every connection.
Prerequisites
Before you begin, make sure you have:
- A server running Ubuntu 22.04 LTS or later
- Apache Traffic Server installed and running as a reverse proxy. If you have not set this up yet, start with Install and Configure Apache Traffic Server as a Caching Reverse Proxy on Ubuntu
- Ports 80 and 443 open in your firewall
sudoaccess on the server- A domain name pointing to your server’s public IP, required if you plan to use Let’s Encrypt; not needed if you already have a certificate
You also need to ensure that nothing else is binding to port 443 before you start.
Step 1: Prepare Your SSL Certificate
ATS needs two files to terminate TLS: a certificate file (your cert plus any intermediate CA certs, in PEM format) and a private key file. How you get those files depends on your situation. Pick the option that applies to you.
Option A: Obtain a Free Certificate with Let’s Encrypt (Certbot)
Use this option if you do not already have a certificate and want a free, publicly trusted one from Let’s Encrypt. Your domain must have a public DNS A record pointing to this server.
Certbot is the official tool for requesting and renewing Let’s Encrypt certificates. There is no native Certbot plugin for ATS, but you do not need one. You can use Certbot’s standalone mode to get the certificate files and then point ATS at them.
Install Certbot:
sudo apt update
sudo apt install certbot -y
In standalone mode, Certbot spins up its own temporary HTTP server to respond to Let’s Encrypt’s domain verification challenge. Since ATS is already listening on port 80, you need to stop it briefly:
sudo systemctl stop trafficserver
Now request the certificate. Replace example.com with your actual domain:
sudo certbot certonly --standalone -d example.com -d www.example.com
Certbot will ask for your email address (used for renewal reminders and expiry notices) and prompt you to accept the terms of service. After a few seconds, you will see a success message and the paths to your certificate files:
Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/example.com/fullchain.pem
Key is saved at: /etc/letsencrypt/live/example.com/privkey.pem
Start ATS again:
sudo systemctl start trafficserver
If you cannot stop ATS, use the webroot method instead: serve the
.well-known/acme-challenge/path from a directory via your backend, then runcertbot certonly --webroot -w /var/www/html -d example.com. Add a remap rule in ATS pointing that path to your backend’s challenge directory.
Once Certbot finishes, move on to Step 2 to copy the certificate files into ATS.
Option B: Use an Existing Certificate
Use this option if you already have a certificate from a commercial CA, your organisation’s internal PKI, or a wildcard cert you purchased. You should have two files on hand:
- A certificate file containing your certificate and any required intermediate CA certificates, concatenated together in PEM format (this is often called the “full chain” or “bundle” file)
- A private key file in PEM format, unencrypted (ATS cannot use password-protected key files)
If your CA gave you separate files (leaf cert, intermediate cert, root cert), concatenate them in the correct order, leaf first, then intermediates:
cat your-domain.crt intermediate.crt > fullchain.pem
If your private key is password-protected, decrypt it first:
openssl rsa -in encrypted.key -out server.key
Once you have the two files ready, proceed to Step 2.
Step 2: Copy the Certificate Files to ATS
ATS needs to read the certificate and private key at startup. Create a dedicated directory inside the ATS config tree:
sudo mkdir -p /etc/trafficserver/ssl
If you used Option A (Certbot), copy the files from the Let’s Encrypt live directory:
sudo cp /etc/letsencrypt/live/example.com/fullchain.pem /etc/trafficserver/ssl/server.pem
sudo cp /etc/letsencrypt/live/example.com/privkey.pem /etc/trafficserver/ssl/server.key
Always use fullchain.pem, not cert.pem. The full chain file includes your certificate plus the Let’s Encrypt intermediate certificates, which older clients need to validate the chain of trust. Using only the leaf cert (cert.pem) causes validation failures on some clients.
If you used Option B (existing certificate), copy your own files instead:
sudo cp /path/to/fullchain.pem /etc/trafficserver/ssl/server.pem
sudo cp /path/to/server.key /etc/trafficserver/ssl/server.key
In both cases, lock down the private key so only root can read it:
sudo chmod 600 /etc/trafficserver/ssl/server.key
sudo chown -R root:root /etc/trafficserver/ssl/
Step 3: Register the Certificate with ATS
ATS uses a file called ssl_multicert.config to map TLS certificates to incoming connections. This file supports SNI, so a single ATS instance can serve different certificates for different domains on the same port, but for now, you will configure one certificate for all connections.
Open the file:
sudo nano /etc/trafficserver/ssl_multicert.config
Add this line:
dest_ip=* ssl_cert_name=/etc/trafficserver/ssl/server.pem ssl_key_name=/etc/trafficserver/ssl/server.key
dest_ip=* means this certificate applies to any incoming TLS connection that does not match a more specific rule. When you later add domains with separate certificates, you will add additional lines with ssl_fqdn matching for SNI.
Save and close the file.
Step 4: Configure ATS to Listen on Port 443
Open the main configuration file:
sudo nano /etc/trafficserver/records.config
Find the proxy.config.http.server_ports line. It currently looks something like this:
CONFIG proxy.config.http.server_ports STRING 80
Update it to add port 443 with the ssl flag:
CONFIG proxy.config.http.server_ports STRING 80 443:ssl
The :ssl suffix is mandatory. Without it, ATS will listen on port 443 but speak plain HTTP, which will confuse every client that tries to connect with TLS.
Also tell ATS where to find the certificate and key files:
CONFIG proxy.config.ssl.server.cert.path STRING /etc/trafficserver/ssl
CONFIG proxy.config.ssl.server.private_key.path STRING /etc/trafficserver/ssl
While you have the file open, apply modern TLS settings. Disable the old, insecure protocol versions:
CONFIG proxy.config.ssl.TLSv1 INT 0
CONFIG proxy.config.ssl.TLSv1_1 INT 0
CONFIG proxy.config.ssl.TLSv1_2 INT 1
CONFIG proxy.config.ssl.TLSv1_3 INT 1
And set a strong cipher suite. This follows Mozilla’s “Intermediate” compatibility profile, secure against known attacks while still supporting a wide range of modern clients:
CONFIG proxy.config.ssl.server.cipher_suite STRING ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305
Save the file. Port configuration changes require a full restart, a config reload is not enough:
sudo systemctl restart trafficserver
Verify ATS is now listening on both ports:
sudo ss -tlnp | grep traffic
You should see entries for both port 80 and port 443.
Step 5: Add HTTPS Remap Rules
Your existing remap.config maps http://example.com/ to your backend. ATS treats HTTP and HTTPS as separate protocols, so you need an explicit rule for the HTTPS version as well.
Open the remap file:
sudo nano /etc/trafficserver/remap.config
Add an HTTPS mapping alongside your existing HTTP one:
map http://example.com/ http://127.0.0.1:8080/
map https://example.com/ http://127.0.0.1:8080/
Notice that the backend URL for the HTTPS rule still uses http://, this is correct. ATS decrypts the incoming HTTPS connection and forwards plain HTTP to your backend. The backend does not need any SSL configuration.
Reload the configuration:
sudo traffic_ctl config reload
Test that HTTPS is working:
curl -I https://example.com/
You should get a 200 OK response with the Via header from ATS confirming it handled the request. If you see a certificate error, verify that your server.pem file contains the full certificate chain and not just the leaf certificate.
Step 6: Redirect HTTP to HTTPS
With both ports active, HTTP traffic still reaches port 80 successfully. You want to automatically redirect those requests to HTTPS. In ATS, this is done by replacing the HTTP map rule with a redirect rule.
Open remap.config:
sudo nano /etc/trafficserver/remap.config
Change the HTTP entry from a proxy map to a redirect:
redirect http://example.com/ https://example.com/
map https://example.com/ http://127.0.0.1:8080/
The redirect directive makes ATS respond with a 302 redirect. For a permanent 301 redirect, which search engines and browsers cache, removing the redirect hop on future visits, check your ATS version’s documentation. In ATS 9.x and later, you can use redirect_temporary for 302 and redirect_permanent is not a built-in keyword, so a header_rewrite rule is the cleaner way to issue a 301.
For most setups, the 302 is fine during initial testing. Once you confirm everything works correctly, move to a 301 for production.
Reload after saving:
sudo traffic_ctl config reload
Test the redirect:
curl -I http://example.com/
Expected output:
HTTP/1.1 302 Found
Location: https://example.com/
Step 7: Automate Certificate Renewal
Certificate renewal works differently depending on which option you chose in Step 1.
Option A: Automate Renewal with Certbot
Let’s Encrypt certificates expire after 90 days. Certbot handles renewal automatically via a systemd timer that runs twice a day. Check if it is active:
sudo systemctl status certbot.timer
The timer calls certbot renew, which checks all certificates and renews any within 30 days of expiry. The problem is that after renewal, the new certificate files land in /etc/letsencrypt/live/, but ATS still has the old files in /etc/trafficserver/ssl/. You need to copy the new files over and reload ATS after every renewal.
Certbot solves this with deploy hooks: scripts that run automatically after a successful renewal. Create one now:
sudo nano /etc/letsencrypt/renewal-hooks/deploy/reload-ats.sh
Add the following:
#!/bin/bash
cp /etc/letsencrypt/live/example.com/fullchain.pem /etc/trafficserver/ssl/server.pem
cp /etc/letsencrypt/live/example.com/privkey.pem /etc/trafficserver/ssl/server.key
chmod 600 /etc/trafficserver/ssl/server.key
traffic_ctl config reload
Make the script executable:
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-ats.sh
Every time Certbot successfully renews your certificate, it runs this script automatically copying the fresh certificate and reloading ATS with zero downtime.
Run a dry-run renewal to verify the whole pipeline works:
sudo certbot renew --dry-run
If you see All simulated renewals succeeded, your automatic renewal is working correctly and your certificate will never expire as long as the server is reachable.
Option B: Renewal for Existing Certificates
If you are using a certificate from a commercial CA or an internal PKI, renewal depends on your issuer’s process. The key point is that every time you receive a new certificate, you must copy it into ATS and reload.
To avoid forgetting this step, create a small helper script you can run whenever you update the certificate:
sudo nano /usr/local/bin/ats-cert-reload.sh
Add the following, adjusting the source paths to where your CA delivers renewed files:
#!/bin/bash
set -e
cp /path/to/new/fullchain.pem /etc/trafficserver/ssl/server.pem
cp /path/to/new/server.key /etc/trafficserver/ssl/server.key
chmod 600 /etc/trafficserver/ssl/server.key
traffic_ctl config reload
echo "ATS certificate updated and reloaded."
Make it executable:
sudo chmod +x /usr/local/bin/ats-cert-reload.sh
Run this script every time you renew your certificate. If your CA supports ACME-compatible renewal tools other than Certbot (such as acme.sh), those tools also support deploy hooks, wire this script up there in the same way as the Certbot deploy hook above.
Track your expiry date. Commercial certificates can have lifetimes of one to two years. Set a calendar reminder 30 days before expiry so you have time to renew and deploy without a last-minute scramble.
Common Mistakes and Troubleshooting
ATS is not listening on port 443 after restart
Confirm that proxy.config.http.server_ports in records.config includes 443:ssl (with the colon and the word ssl). Then verify with sudo ss -tlnp | grep 443. If the port is still absent, check the ATS error log for startup errors:
sudo journalctl -u trafficserver --since "5 minutes ago"
Browser shows “ERR_SSL_PROTOCOL_ERROR”
This usually means ATS is listening on port 443 but speaking plain HTTP instead of TLS. The :ssl flag is missing from proxy.config.http.server_ports, or the service was reloaded rather than restarted after the change.
“No remap entry found” on HTTPS requests
You added only the http:// map rule and forgot the https:// version. Every scheme ATS handles needs its own remap entry. Add map https://example.com/ http://127.0.0.1:8080/ to remap.config and reload.
Certificate chain errors: “unable to get local issuer certificate”
Your server.pem contains only the leaf certificate, not the full chain. If you used Certbot, make sure you copied fullchain.pem and not cert.pem. If you used your own certificate, concatenate the leaf cert and intermediate cert(s) into a single file (leaf first) and copy that into /etc/trafficserver/ssl/server.pem, then reload.
ATS cannot read the key file
ATS may run as a non-root user depending on your package version. Check what user the process runs as:
ps aux | grep traffic_server
If it is not root, grant that user read access to the SSL directory:
sudo chown -R root:<ats-user> /etc/trafficserver/ssl/
sudo chmod 640 /etc/trafficserver/ssl/server.key
Certbot deploy hook does not run after renewal
Make sure the script is executable (chmod +x) and located specifically in /etc/letsencrypt/renewal-hooks/deploy/, not in pre/ or post/. Deploy hooks only run after a successful renewal, not during a dry run. Test with sudo certbot renew --dry-run first, then trigger a real renewal with a soon-to-expire test certificate to confirm the hook fires.
Best Practices
Always use fullchain.pem, never cert.pem. The intermediate certificates in the chain are required by clients that do not have the Let’s Encrypt intermediate cached. Skipping them causes intermittent failures that are hard to reproduce.
Disable TLS 1.0 and 1.1 from day one. No modern client needs them. Both versions have known vulnerabilities. The configuration in Step 4 disables them, do not re-enable them unless you have a very specific compatibility requirement and understand the risk.
Test your configuration with SSL Labs. After setup, submit your domain at SSL Labs Server Test. It grades your TLS configuration and highlights issues like weak cipher suites, missing HSTS headers, or chain problems. Aim for at least an A rating.
Add HSTS after confirming HTTPS works. HTTP Strict Transport Security tells browsers to always use HTTPS for your domain, even if the user types http://. Add it via the header_rewrite plugin. Set a short max-age (300 seconds) while testing, then increase to 31536000 (one year) in production. Warning: once a browser has cached a long HSTS header, you cannot easily roll back to HTTP, so verify your HTTPS setup is solid first.
Rotate private keys on renewal when possible. By default, Certbot reuses the existing private key on renewal for continuity. You can force a new key with --reuse-key=false added to your renewal configuration if your security policy requires it.
Keep ATS updated. TLS-related vulnerabilities appear regularly. Follow the Apache Traffic Server mailing list and apply security releases promptly.
Conclusion
You have configured Apache Traffic Server to terminate TLS on port 443, set up your certificate whether from Let’s Encrypt via Certbot or from an existing certificate you already owned, redirected HTTP traffic to HTTPS, and wired up automatic certificate renewal so ATS picks up new files without downtime.
Your HTTPS setup is now centralized at ATS. Adding a new backend service requires only a new remap rule, no certificate management on the backend at all. And because ATS still caches and proxies after termination, you keep the performance benefits of a caching reverse proxy on top of a secure connection.
From here, some worthwhile next steps:
- OCSP stapling: enable with
proxy.config.ssl.ocsp.enabled INT 1inrecords.config, it bundles revocation status into the TLS handshake, saving clients a round trip and speeding up SSL negotiation - SNI-based multi-domain certificates: add additional entries to
ssl_multicert.configwithssl_fqdn=other.example.comto serve different certificates per domain from the same ATS instance on the same port - The
header_rewriteplugin: addStrict-Transport-Security,X-Content-Type-Options, andX-Frame-Optionsheaders to all proxied responses in one place, without touching individual backends - Mutual TLS (mTLS): ATS supports verifying client certificates if your API requires clients to authenticate themselves, useful for machine-to-machine communication in zero-trust architectures