Harden Apache HTTP server with modern SSL/TLS configuration, perfect forward secrecy cipher suites, and security headers to protect against common web vulnerabilities and ensure compliance with security standards.
Prerequisites
- Apache HTTP server
- Root or sudo access
- Basic understanding of SSL/TLS concepts
What this solves
Apache's default SSL configuration uses outdated protocols and weak cipher suites that expose your web applications to security vulnerabilities. This tutorial configures Apache with modern TLS protocols, perfect forward secrecy (PFS) cipher suites, and essential security headers including HSTS, CSP, and X-Frame-Options to protect against man-in-the-middle attacks, protocol downgrade attacks, and common web vulnerabilities.
Step-by-step configuration
Update system packages
Start by updating your package manager to ensure you have the latest security patches and Apache modules.
sudo apt update && sudo apt upgrade -y
Install Apache and SSL modules
Install Apache HTTP server with the SSL module and headers module required for security hardening.
sudo apt install -y apache2 apache2-utils
sudo a2enmod ssl
sudo a2enmod headers
sudo a2enmod rewrite
Generate SSL certificate
Create a self-signed certificate for testing or use an existing certificate from Let's Encrypt or a commercial CA.
sudo mkdir -p /etc/apache2/ssl
sudo openssl req -x509 -nodes -days 365 -newkey rsa:4096 \
-keyout /etc/apache2/ssl/apache-selfsigned.key \
-out /etc/apache2/ssl/apache-selfsigned.crt \
-subj "/C=US/ST=State/L=City/O=Organization/OU=OrgUnit/CN=example.com"
Configure SSL security settings
Create a dedicated SSL security configuration file with modern cipher suites and perfect forward secrecy.
# SSL Security Configuration
Disable SSLv2, SSLv3, and TLS 1.0/1.1
SSLProtocol all -SSLv2 -SSLv3 -TLSv1 -TLSv1.1
Perfect Forward Secrecy cipher suites
SSLCipherSuite 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:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
Honor cipher order preference of the server
SSLHonorCipherOrder on
Use secure renegotiation
SSLInsecureRenegotiation off
Disable SSL compression to prevent CRIME attacks
SSLCompression off
OCSP Stapling for better performance
SSLUseStapling on
SSLStaplingCache "shmcb:logs/stapling-cache(150000)"
Modern Diffie-Hellman parameters
SSLOpenSSLConfCmd DHParameters "/etc/ssl/certs/dhparam.pem"
Generate strong Diffie-Hellman parameters
Create custom Diffie-Hellman parameters for perfect forward secrecy. This process takes several minutes.
sudo openssl dhparam -out /etc/ssl/certs/dhparam.pem 2048
sudo openssl dhparam -out /etc/ssl/certs/dhparam.pem 4096Configure security headers
Create a configuration file for essential HTTP security headers including HSTS, CSP, and clickjacking protection.
# HTTP Security Headers
HTTP Strict Transport Security (HSTS)
Force HTTPS for 1 year, include subdomains
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
Prevent clickjacking attacks
Header always set X-Frame-Options "SAMEORIGIN"
Prevent MIME type sniffing
Header always set X-Content-Type-Options "nosniff"
Enable XSS protection
Header always set X-XSS-Protection "1; mode=block"
Referrer Policy - limit information leakage
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Content Security Policy (basic - customize for your application)
Header always set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-src 'none'; object-src 'none';"
Permissions Policy (formerly Feature Policy)
Header always set Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=(), usb=(), accelerometer=(), gyroscope=(), magnetometer=()"
Remove server signature for security
ServerTokens Prod
ServerSignature Off
Hide Apache version
Header always unset Server
Header always set Server "Apache"
Create secure virtual host
Configure a virtual host with SSL hardening and security headers applied.
ServerName example.com
ServerAlias www.example.com
# Redirect all HTTP to HTTPS
RewriteEngine On
RewriteCond %{HTTPS} !=on
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
ServerName example.com
ServerAlias www.example.com
DocumentRoot /var/www/html
# SSL Configuration
SSLEngine on
SSLCertificateFile /etc/apache2/ssl/apache-selfsigned.crt
SSLCertificateKeyFile /etc/apache2/ssl/apache-selfsigned.key
# Apply security configurations
Include /etc/apache2/conf-available/ssl-security.conf
Include /etc/apache2/conf-available/security-headers.conf
# Logging
ErrorLog ${APACHE_LOG_DIR}/secure-site-error.log
CustomLog ${APACHE_LOG_DIR}/secure-site-access.log combined
# Additional security directives
AllowOverride None
Require all granted
Options -Indexes -Includes -ExecCGI
# Deny access to sensitive files
Require all denied
Enable configurations and restart Apache
Enable the security configurations and virtual host, then restart Apache to apply changes.
sudo a2enconf ssl-security
sudo a2enconf security-headers
sudo a2ensite secure-site
sudo a2dissite 000-default
sudo apache2ctl configtest
sudo systemctl restart apache2
Configure firewall rules
Open HTTP and HTTPS ports in the firewall to allow web traffic.
sudo ufw allow 'Apache Full'
sudo ufw --force enable
sudo ufw status
Test SSL configuration and security validation
Verify SSL certificate and protocols
Test the SSL configuration using OpenSSL to ensure proper protocol and cipher suite negotiation.
# Test TLS 1.3 connection
openssl s_client -connect example.com:443 -tls1_3 -servername example.com
Test TLS 1.2 connection
openssl s_client -connect example.com:443 -tls1_2 -servername example.com
Test that weak protocols are disabled (should fail)
openssl s_client -connect example.com:443 -tls1_1 -servername example.com
openssl s_client -connect example.com:443 -ssl3 -servername example.com
Test cipher suites and perfect forward secrecy
Verify that only strong cipher suites with PFS are available.
# Test ECDHE cipher (should work - provides PFS)
openssl s_client -connect example.com:443 -cipher 'ECDHE-RSA-AES256-GCM-SHA384'
Test weak cipher (should fail)
openssl s_client -connect example.com:443 -cipher 'RC4-SHA'
List all available ciphers
nmap --script ssl-enum-ciphers -p 443 example.com
Verify security headers
Check that all security headers are properly configured and transmitted.
# Test security headers
curl -I https://example.com
Test specific headers
curl -s -D- https://example.com | grep -i "strict-transport-security\|x-frame-options\|x-content-type-options\|content-security-policy"
Verify your setup
# Check Apache status and SSL module
sudo systemctl status apache2
sudo apache2ctl -M | grep ssl
Verify SSL certificate
openssl x509 -in /etc/apache2/ssl/apache-selfsigned.crt -text -noout
Test HTTPS redirect
curl -I http://example.com
Check SSL Labs rating (replace with your domain)
Visit: https://www.ssllabs.com/ssltest/analyze.html?d=example.com
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| SSL handshake failures | Weak cipher suites or protocols | Check SSLCipherSuite and SSLProtocol directives |
| Browser certificate warnings | Self-signed certificate | Use Let's Encrypt or commercial CA certificate |
| HSTS not working | Headers module not enabled | sudo a2enmod headers && sudo systemctl restart apache2 |
| Configuration test fails | Syntax errors in config files | sudo apache2ctl configtest to identify issues |
| OCSP stapling errors | Missing intermediate certificates | Add SSLCertificateChainFile directive with chain |
| Perfect Forward Secrecy failing | Missing DH parameters | Ensure /etc/ssl/certs/dhparam.pem exists and is referenced |
Next steps
- Implement Apache WAF with ModSecurity for advanced threat protection
- Configure Apache rate limiting and DDoS protection
- Set up automated SSL certificate renewal with Let's Encrypt
- Configure Apache log monitoring for security analysis
- Configure Apache load balancing with SSL termination
Running this in production?
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Colors for output
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly NC='\033[0m' # No Color
# Global variables
DOMAIN="${1:-example.com}"
DH_BITS="${2:-2048}"
APACHE_DIR=""
SSL_DIR=""
CONF_DIR=""
SERVICE_NAME=""
usage() {
echo "Usage: $0 [domain] [dh_bits]"
echo " domain: Domain name for SSL certificate (default: example.com)"
echo " dh_bits: DH parameter bits - 2048 or 4096 (default: 2048)"
exit 1
}
log_info() {
echo -e "${GREEN}[INFO]${NC} $1"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
cleanup() {
log_error "Script failed. Check logs above for details."
exit 1
}
trap cleanup ERR
check_root() {
if [[ $EUID -ne 0 ]]; then
log_error "This script must be run as root or with sudo"
exit 1
fi
}
detect_distro() {
if [[ ! -f /etc/os-release ]]; then
log_error "/etc/os-release not found. Cannot detect distribution."
exit 1
fi
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_INSTALL="apt install -y"
PKG_UPDATE="apt update && apt upgrade -y"
APACHE_DIR="/etc/apache2"
SSL_DIR="/etc/apache2/ssl"
CONF_DIR="/etc/apache2/conf-available"
SERVICE_NAME="apache2"
;;
almalinux|rocky|centos|rhel|ol)
PKG_MGR="dnf"
PKG_INSTALL="dnf install -y"
PKG_UPDATE="dnf update -y"
APACHE_DIR="/etc/httpd"
SSL_DIR="/etc/httpd/ssl"
CONF_DIR="/etc/httpd/conf.d"
SERVICE_NAME="httpd"
;;
fedora)
PKG_MGR="dnf"
PKG_INSTALL="dnf install -y"
PKG_UPDATE="dnf update -y"
APACHE_DIR="/etc/httpd"
SSL_DIR="/etc/httpd/ssl"
CONF_DIR="/etc/httpd/conf.d"
SERVICE_NAME="httpd"
;;
amzn)
PKG_MGR="yum"
PKG_INSTALL="yum install -y"
PKG_UPDATE="yum update -y"
APACHE_DIR="/etc/httpd"
SSL_DIR="/etc/httpd/ssl"
CONF_DIR="/etc/httpd/conf.d"
SERVICE_NAME="httpd"
;;
*)
log_error "Unsupported distribution: $ID"
exit 1
;;
esac
}
validate_args() {
if [[ "$DH_BITS" != "2048" && "$DH_BITS" != "4096" ]]; then
log_error "DH bits must be 2048 or 4096"
usage
fi
if [[ ! "$DOMAIN" =~ ^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]?\.[a-zA-Z]{2,}$ ]]; then
log_warn "Domain '$DOMAIN' may not be valid. Continuing anyway..."
fi
}
update_system() {
echo "[1/8] Updating system packages..."
$PKG_UPDATE
log_info "System packages updated"
}
install_apache() {
echo "[2/8] Installing Apache and SSL modules..."
if [[ "$PKG_MGR" == "apt" ]]; then
$PKG_INSTALL apache2 apache2-utils openssl
a2enmod ssl headers rewrite
else
$PKG_INSTALL httpd mod_ssl openssl
systemctl enable $SERVICE_NAME
fi
log_info "Apache and SSL modules installed"
}
generate_ssl_cert() {
echo "[3/8] Generating SSL certificate..."
mkdir -p "$SSL_DIR"
chmod 755 "$SSL_DIR"
openssl req -x509 -nodes -days 365 -newkey rsa:4096 \
-keyout "$SSL_DIR/apache-selfsigned.key" \
-out "$SSL_DIR/apache-selfsigned.crt" \
-subj "/C=US/ST=State/L=City/O=Organization/OU=OrgUnit/CN=$DOMAIN"
chmod 600 "$SSL_DIR/apache-selfsigned.key"
chmod 644 "$SSL_DIR/apache-selfsigned.crt"
chown root:root "$SSL_DIR"/*
log_info "SSL certificate generated for $DOMAIN"
}
generate_dhparam() {
echo "[4/8] Generating Diffie-Hellman parameters ($DH_BITS bits)..."
log_warn "This may take several minutes..."
openssl dhparam -out /etc/ssl/certs/dhparam.pem "$DH_BITS"
chmod 644 /etc/ssl/certs/dhparam.pem
chown root:root /etc/ssl/certs/dhparam.pem
log_info "DH parameters generated"
}
configure_ssl_security() {
echo "[5/8] Configuring SSL security settings..."
local ssl_conf=""
if [[ "$PKG_MGR" == "apt" ]]; then
ssl_conf="$CONF_DIR/ssl-security.conf"
else
ssl_conf="$CONF_DIR/ssl-security.conf"
fi
cat > "$ssl_conf" << EOF
# SSL Security Configuration - Modern TLS with Perfect Forward Secrecy
# Disable SSLv2, SSLv3, and TLS 1.0/1.1
SSLProtocol all -SSLv2 -SSLv3 -TLSv1 -TLSv1.1
# Perfect Forward Secrecy cipher suites
SSLCipherSuite 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:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
# Honor cipher order preference of the server
SSLHonorCipherOrder on
# Use secure renegotiation
SSLInsecureRenegotiation off
# Disable SSL compression to prevent CRIME attacks
SSLCompression off
# OCSP Stapling for better performance
SSLUseStapling on
SSLStaplingCache "shmcb:logs/stapling-cache(150000)"
# Modern Diffie-Hellman parameters
SSLOpenSSLConfCmd DHParameters "/etc/ssl/certs/dhparam.pem"
EOF
chmod 644 "$ssl_conf"
chown root:root "$ssl_conf"
if [[ "$PKG_MGR" == "apt" ]]; then
a2enconf ssl-security
fi
log_info "SSL security configuration created"
}
configure_security_headers() {
echo "[6/8] Configuring HTTP security headers..."
local headers_conf=""
if [[ "$PKG_MGR" == "apt" ]]; then
headers_conf="$CONF_DIR/security-headers.conf"
else
headers_conf="$CONF_DIR/security-headers.conf"
fi
cat > "$headers_conf" << EOF
# HTTP Security Headers Configuration
# HTTP Strict Transport Security (HSTS)
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
# Content Security Policy
Header always set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'"
# X-Frame-Options for clickjacking protection
Header always set X-Frame-Options "DENY"
# X-Content-Type-Options
Header always set X-Content-Type-Options "nosniff"
# X-XSS-Protection
Header always set X-XSS-Protection "1; mode=block"
# Referrer Policy
Header always set Referrer-Policy "strict-origin-when-cross-origin"
# Remove server information
ServerTokens Prod
ServerSignature Off
EOF
chmod 644 "$headers_conf"
chown root:root "$headers_conf"
if [[ "$PKG_MGR" == "apt" ]]; then
a2enconf security-headers
fi
log_info "Security headers configuration created"
}
configure_ssl_site() {
echo "[7/8] Configuring SSL virtual host..."
local site_conf=""
if [[ "$PKG_MGR" == "apt" ]]; then
site_conf="/etc/apache2/sites-available/ssl-secure.conf"
else
site_conf="$CONF_DIR/ssl-secure.conf"
fi
cat > "$site_conf" << EOF
<VirtualHost *:443>
ServerName $DOMAIN
DocumentRoot /var/www/html
# SSL Configuration
SSLEngine on
SSLCertificateFile $SSL_DIR/apache-selfsigned.crt
SSLCertificateKeyFile $SSL_DIR/apache-selfsigned.key
# Security headers are included globally
# Logs
ErrorLog \${APACHE_LOG_DIR}/ssl_error.log
CustomLog \${APACHE_LOG_DIR}/ssl_access.log combined
</VirtualHost>
# Redirect HTTP to HTTPS
<VirtualHost *:80>
ServerName $DOMAIN
Redirect permanent / https://$DOMAIN/
</VirtualHost>
EOF
chmod 644 "$site_conf"
chown root:root "$site_conf"
if [[ "$PKG_MGR" == "apt" ]]; then
a2ensite ssl-secure
fi
log_info "SSL virtual host configured"
}
restart_apache() {
echo "[8/8] Starting and verifying Apache configuration..."
# Test configuration
if [[ "$PKG_MGR" == "apt" ]]; then
apache2ctl configtest
else
httpd -t
fi
# Start/restart Apache
systemctl restart "$SERVICE_NAME"
systemctl enable "$SERVICE_NAME"
log_info "Apache restarted successfully"
}
verify_installation() {
echo "Verifying installation..."
# Check if Apache is running
if systemctl is-active --quiet "$SERVICE_NAME"; then
log_info "Apache is running"
else
log_error "Apache is not running"
return 1
fi
# Check SSL certificate
if [[ -f "$SSL_DIR/apache-selfsigned.crt" ]]; then
log_info "SSL certificate exists"
else
log_error "SSL certificate not found"
return 1
fi
# Check DH parameters
if [[ -f "/etc/ssl/certs/dhparam.pem" ]]; then
log_info "DH parameters generated"
else
log_error "DH parameters not found"
return 1
fi
log_info "SSL hardening configuration completed successfully!"
log_warn "Remember to replace the self-signed certificate with a proper CA certificate for production use"
log_warn "Test your SSL configuration at: https://www.ssllabs.com/ssltest/"
}
main() {
check_root
validate_args
detect_distro
log_info "Starting Apache SSL hardening for domain: $DOMAIN"
log_info "Detected distribution: $ID"
update_system
install_apache
generate_ssl_cert
generate_dhparam
configure_ssl_security
configure_security_headers
configure_ssl_site
restart_apache
verify_installation
}
main "$@"
Review the script before running. Execute with: bash install.sh