How to Harden Web Servers: Security Headers, CSP and HSTS on RHEL 7
HTTP security headers are one of the most cost-effective controls available to web server administrators. They instruct the browser to enforce strict content handling rules, preventing common attacks such as cross-site scripting (XSS), clickjacking, MIME-type confusion, and protocol downgrade attacks — without requiring any changes to application code. This tutorial covers adding a comprehensive set of security headers to both Apache and Nginx on RHEL 7, configuring a Content Security Policy, enabling HTTP Strict Transport Security with preload support, and validating the result using the same methodology employed by tools like securityheaders.com.
Prerequisites
- RHEL 7 server with Apache (httpd) or Nginx installed
- TLS/SSL certificate already configured — HSTS requires HTTPS
- Root or sudo access
- Basic understanding of Apache or Nginx virtual host configuration
- mod_headers enabled for Apache (included in httpd package on RHEL 7)
Step 1: Install and Enable Apache or Nginx
Install Apache if not already present:
sudo yum install -y httpd mod_ssl
sudo systemctl enable httpd
sudo systemctl start httpd
For Nginx, enable the EPEL repository first:
sudo yum install -y epel-release
sudo yum install -y nginx
sudo systemctl enable nginx
sudo systemctl start nginx
Verify mod_headers is loaded in Apache:
httpd -M 2>/dev/null | grep headers
# Expected: headers_module (shared)
Step 2: Configure Security Headers in Apache
Edit your virtual host configuration file. Security headers for HTTPS virtual hosts should be placed inside the <VirtualHost *:443> block. The Header always set directive ensures headers are added even for error responses (4xx, 5xx):
sudo vi /etc/httpd/conf.d/ssl.conf
Add the following block inside the SSL virtual host:
<VirtualHost *:443>
ServerName example.com
DocumentRoot /var/www/html
SSLEngine on
SSLCertificateFile /etc/pki/tls/certs/example.com.crt
SSLCertificateKeyFile /etc/pki/tls/private/example.com.key
# HTTP Strict Transport Security — 1 year, include subdomains, preload
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
# Prevent clickjacking
Header always set X-Frame-Options "SAMEORIGIN"
# Prevent MIME-type sniffing
Header always set X-Content-Type-Options "nosniff"
# Control referrer information sent with requests
Header always set Referrer-Policy "strict-origin-when-cross-origin"
# Disable legacy XSS auditor (modern recommendation: rely on CSP)
Header always set X-XSS-Protection "0"
# Content Security Policy (start with report-only during testing)
Header always set Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self';"
# Permissions Policy (formerly Feature-Policy)
Header always set Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=()"
</VirtualHost>
Redirect all HTTP traffic to HTTPS in the port 80 virtual host:
<VirtualHost *:80>
ServerName example.com
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
Redirect permanent / https://example.com/
</VirtualHost>
Test the Apache configuration and reload:
sudo apachectl configtest
sudo systemctl reload httpd
Step 3: Configure Security Headers in Nginx
For Nginx, security headers are added with the add_header directive. Edit your server block configuration:
sudo vi /etc/nginx/conf.d/example.conf
Configure the HTTPS server block:
server {
listen 443 ssl http2;
server_name example.com;
root /usr/share/nginx/html;
ssl_certificate /etc/pki/tls/certs/example.com.crt;
ssl_certificate_key /etc/pki/tls/private/example.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
# HSTS
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
# Clickjacking protection
add_header X-Frame-Options "SAMEORIGIN" always;
# MIME sniffing protection
add_header X-Content-Type-Options "nosniff" always;
# Referrer Policy
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Disable legacy XSS auditor
add_header X-XSS-Protection "0" always;
# Content Security Policy
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self';" always;
# Permissions Policy
add_header Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=()" always;
}
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}
Test and reload Nginx:
sudo nginx -t
sudo systemctl reload nginx
Step 4: Understanding Content-Security-Policy Directives
Content Security Policy is a declarative whitelist that tells the browser which sources are trusted for each content type. A well-crafted CSP is the single most effective defence against XSS attacks. Key directives explained:
default-src 'self'— fallback for all content types; only allow same-origin resourcesscript-src 'self'— only execute scripts served from your own domain; blocks inline scripts and eval()style-src 'self' 'unsafe-inline'— allow same-origin stylesheets;'unsafe-inline'is needed if inline styles are used (work to remove this over time)img-src 'self' data:— images from same origin and data URIs (used by CSS background images)font-src 'self'— web fonts only from your own serverconnect-src 'self'— XMLHttpRequest, WebSocket, and fetch() calls restricted to same originframe-ancestors 'none'— the CSP equivalent of X-Frame-Options DENY; no page may embed yours in a framebase-uri 'self'— prevents base tag injection attacks that redirect relative URLsform-action 'self'— form submissions only to same-origin endpoints
When adding external CDN resources, add them explicitly rather than opening the policy broadly:
script-src 'self' https://cdn.jsdelivr.net https://ajax.googleapis.com;
Use Content-Security-Policy-Report-Only mode during development to log violations without blocking:
# Apache:
Header always set Content-Security-Policy-Report-Only "default-src 'self'; report-uri /csp-report-endpoint"
# Nginx:
add_header Content-Security-Policy-Report-Only "default-src 'self'; report-uri /csp-report-endpoint" always;
Step 5: HTTP Strict Transport Security Deep Dive
HSTS instructs the browser to always use HTTPS for your domain, even if a user types http:// or clicks an HTTP link. The browser caches this instruction for the duration specified by max-age:
max-age=31536000— 1 year (minimum required for HSTS preload submission)includeSubDomains— applies the policy to all subdomains; only add this when all subdomains serve HTTPSpreload— indicates willingness to be included in browser preload lists; requires prior submission to hstspreload.org
Important: Only add preload when you are absolutely certain all current and future subdomains will support HTTPS. Preloading is very difficult to reverse. Start with a shorter max-age during testing:
# Testing phase (5 minutes):
Header always set Strict-Transport-Security "max-age=300"
# Production phase (1 year + subdomains):
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
Step 6: X-Frame-Options and Permissions-Policy
X-Frame-Options: SAMEORIGIN prevents your pages from being embedded in iframes on other domains, mitigating clickjacking attacks. For stricter control, use DENY to block all framing including from your own domain.
The Permissions-Policy header (formerly Feature-Policy) controls browser API access. Disable APIs your site does not use:
# Comprehensive Permissions-Policy example:
Header always set Permissions-Policy "accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), display-capture=(), document-domain=(), encrypted-media=(), fullscreen=(self), geolocation=(), gyroscope=(), interest-cohort=(), magnetometer=(), microphone=(), midi=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), xr-spatial-tracking=()"
Step 7: Validate Security Headers
Test your headers using curl from the command line:
curl -s -D - https://example.com -o /dev/null | grep -i "strict|x-frame|x-content|referrer|content-security|permissions"
Expected output:
strict-transport-security: max-age=31536000; includeSubDomains; preload
x-frame-options: SAMEORIGIN
x-content-type-options: nosniff
referrer-policy: strict-origin-when-cross-origin
content-security-policy: default-src 'self'; script-src 'self'; ...
permissions-policy: geolocation=(), microphone=(), camera=(), payment=()
For a comprehensive grade using the securityheaders.com methodology, replicate their checks by verifying the presence and correct values of all six critical headers: Content-Security-Policy, Strict-Transport-Security, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, and Permissions-Policy. Missing any of these results in a grade reduction. Use Mozilla Observatory for an automated online scan:
# Check via Mozilla Observatory API:
curl "https://http-observatory.security.mozilla.org/api/v1/analyze?host=example.com"
-X POST -d "hidden=true"
Conclusion
Implementing HTTP security headers on RHEL 7 is one of the highest-value, lowest-effort security improvements available for web servers. The headers covered in this guide collectively defend against XSS, clickjacking, MIME confusion, protocol downgrade, and unwanted browser API access. Start by deploying in report-only mode for CSP to observe violations without breaking functionality, progressively tighten the policy, then switch to enforcement mode. For existing applications that use inline scripts or third-party resources, iteratively refine the script-src and connect-src directives until all legitimate functionality is explicitly whitelisted and the policy enforces strict defaults everywhere else.