I recently encountered a puzzling situation where Apache 2.4 was sending HSTS (HTTP Strict Transport Security) headers even though:
- I wasn't using SSL/TLS at all
mod_headers
andmod_ssl
weren't enabled- No HSTS directives existed in any config files
$ curl -I http://django.dev
HTTP/1.1 200 OK
Date: Mon, 15 Mar 2021 14:28:51 GMT
Server: Apache/2.4.29 (Ubuntu)
Strict-Transport-Security: max-age=31536000; includeSubDomains
After extensive debugging, I discovered several potential sources:
- Browser cache: Modern browsers cache HSTS policies
- System-wide headers: Some Linux distros enable security headers by default
- Residual configs: Previous SSL configurations might leave traces
For Apache 2.4, explicitly disable HSTS by adding this to your VirtualHost:
<VirtualHost *:80>
ServerName django.dev
# ... other configs ...
# Disable HSTS explicitly
Header always unset Strict-Transport-Security
# Alternative if above doesn't work
Header set Strict-Transport-Security "max-age=0;"
</VirtualHost>
After making changes:
$ sudo apachectl configtest
$ sudo systemctl reload apache2
$ curl -I http://django.dev | grep -i strict-transport
# Should return empty
If you still see HSTS behavior in browsers:
- Chrome/Edge: Visit
chrome://net-internals/#hsts
and delete domain - Firefox: Clear HSTS state in
about:config
- Safari: Reset all website data
Recently while setting up a development environment using Vagrant with Ubuntu 18.04 and Apache 2.4.29, I encountered an unexpected HSTS header being sent in responses to my virtual host requests. Here's what my configuration looked like:
ServerName django.dev
ServerAlias www.django.dev
ServerAdmin webmaster@localhost
DocumentRoot /var/www/
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
Despite having no SSL configuration and mod_ssl being disabled, Apache was still sending this header:
Strict-Transport-Security: max-age=31536000; includeSubDomains
After extensive debugging, I discovered several potential sources for HSTS headers in Apache:
- Browser preload lists (especially for .dev domains)
- Apache modules like mod_headers or mod_security
- Global Apache configuration files
- Browser cache from previous HTTPS visits
The key revelation was that Google has included all .dev domains in Chrome's HSTS preload list since 2017. This means:
- Browsers automatically redirect .dev to HTTPS
- They enforce HSTS even without server headers
- This behavior persists even when using local DNS
Option 1: Change your development domain
Use something other than .dev (like .test or .localhost):
ServerName django.test
ServerAlias www.django.test
Option 2: Force HTTP in your VirtualHost
Add header modification:
# ... existing config ...
Header always set Strict-Transport-Security "max-age=0;"
Option 3: Clear browser HSTS cache
For Chrome:
- Visit chrome://net-internals/#hsts
- Delete domain security policies for your .dev domain
Use curl to test without browser interference:
curl -I http://django.dev
Should return no HSTS header after implementing the solutions.
When setting up new development environments:
- Avoid using .dev, .foo, or other TLDs in browser preload lists
- Use .test (RFC 2606 reserved) or .localhost
- Document your domain choice in project README
# Recommended development domain format
ServerName projectname.localhost
ServerAlias www.projectname.localhost