Forcing HTTPS with .htaccess
Every piece of data exchanged between a browser and your server over plain HTTP travels in clear text. Login credentials, personal information, cookies, and session tokens are all visible to anyone who can observe the network traffic. Forcing HTTPS ensures that all communication is encrypted, protecting both your users and your site's integrity.
This guide explains why HTTPS matters, shows you how to redirect all HTTP traffic to HTTPS using .htaccess, addresses the common pitfalls when your server sits behind a load balancer or proxy, and covers how to combine your redirect with HSTS headers for maximum security.
Why HTTPS Matters
HTTPS (HTTP over TLS/SSL) encrypts the connection between the browser and the server. Without it, every request and response passes through the network as readable text, including form submissions, API calls, and authentication cookies.
Security
On an unencrypted connection, anyone positioned between the user and the server can read or modify the traffic. This includes:
- Public Wi-Fi operators at coffee shops, airports, and hotels.
- Internet service providers who may inject ads or tracking scripts.
- Attackers performing man-in-the-middle attacks on shared networks.
With HTTPS, the traffic is encrypted end-to-end. Even if someone intercepts it, they see only unreadable ciphertext.
SEO
Google has used HTTPS as a ranking signal since 2014. Sites served over HTTPS receive a ranking boost compared to their HTTP equivalents. Google Search Console also flags HTTP pages and mixed content as issues that can affect your site's visibility.
Additionally, referral data is lost when a user clicks from an HTTPS site to an HTTP site. The Referer header is stripped for security reasons, meaning your analytics will show the traffic as "direct" instead of attributing it to the referring site.
Browser Trust
Modern browsers actively warn users about HTTP sites. Chrome labels HTTP pages as "Not Secure" in the address bar. Firefox, Edge, and Safari display similar warnings. For any page that contains a form (even a simple search box), these warnings become more prominent and alarming.
These warnings erode user trust. Visitors seeing "Not Secure" are less likely to submit forms, make purchases, or engage with your content.
Modern Web Features
Many modern web APIs are only available on secure origins. Features that require HTTPS include:
- Service Workers and Progressive Web Apps (PWAs)
- Geolocation API
- Camera and microphone access (getUserMedia)
- Push notifications
- HTTP/2 (virtually all browsers require HTTPS for HTTP/2)
- Clipboard API
- Payment Request API
If your site serves any content over HTTP, these features are unavailable to your users.
HTTPS requires a valid SSL/TLS certificate installed on your server. Free certificates are available from Let's Encrypt. Most hosting providers also offer free SSL through cPanel AutoSSL or similar tools. Make sure your certificate is properly configured before setting up the redirect, otherwise visitors will see a certificate error instead of your site.
RewriteCond HTTPS Redirect
The standard method for forcing HTTPS in .htaccess uses mod_rewrite to detect insecure requests and redirect them to the secure equivalent.
Basic HTTPS Redirect
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
</IfModule>
Let's break down each line:
RewriteCond %{HTTPS} offchecks theHTTPSserver variable. It isoffwhen the connection is plain HTTP andonwhen TLS is active. This condition ensures the rule only fires for insecure requests.RewriteRule ^(.*)$matches any URL path.https://%{HTTP_HOST}%{REQUEST_URI}constructs the redirect destination using the same hostname and full request URI, but with thehttps://scheme.[R=301,L]sends a 301 Permanent Redirect and stops processing further rules.
The result:
| Request | Redirects To |
|---|---|
http://example.com/ | https://example.com/ |
http://example.com/about | https://example.com/about |
http://example.com/blog?page=2 | https://example.com/blog?page=2 |
https://example.com/about | No redirect (already HTTPS) |
Why 301 and Not 302
A 301 Permanent Redirect tells browsers and search engines that the HTTP version of every URL has permanently moved to HTTPS. This has two important effects:
- Browsers cache the redirect. After the first visit, the browser goes directly to HTTPS without hitting the HTTP version first, eliminating the redirect overhead on subsequent visits.
- Search engines transfer ranking signals. A 301 tells crawlers to index the HTTPS version and transfer all link equity from the old HTTP URLs.
During initial testing, use R=302 (temporary redirect) until you have confirmed everything works correctly. Once verified, switch to R=301. This prevents browsers from caching a potentially incorrect permanent redirect that is difficult to undo.
# Testing phase
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=302,L]
# Production
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
Preserving the Query String
The %{REQUEST_URI} variable includes the URL path but not the query string. However, by default, Apache automatically appends the original query string to the redirected URL when using RewriteRule. So http://example.com/search?q=test correctly redirects to https://example.com/search?q=test without any extra configuration.
Excluding SSL Certificate Validation Paths
If you use Let's Encrypt, cPanel AutoSSL, or similar certificate authorities that validate domain ownership via HTTP, their validation requests must not be redirected to HTTPS. The validation process specifically requires an HTTP response.
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{HTTPS} off
# Allow Let's Encrypt / ACME challenge requests over HTTP
RewriteCond %{REQUEST_URI} !^/\.well-known/acme-challenge/ [NC]
# Allow cPanel AutoSSL validation over HTTP
RewriteCond %{REQUEST_URI} !^/\.well-known/cpanel-dcv/[\w-]+$ [NC]
# Allow Comodo/Sectigo validation over HTTP
RewriteCond %{REQUEST_URI} !^/\.well-known/pki-validation/[A-F0-9]{32}\.txt(?:\ Comodo\ DCV)?$ [NC]
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
</IfModule>
Each RewriteCond exclusion prevents the redirect for a specific validation path. The conditions are joined with implicit AND logic, so the redirect only fires when the request is HTTP and the URI does not match any of the validation paths.
If you do not exclude these validation paths and your certificate expires, the automatic renewal process will fail. The certificate authority sends an HTTP request to verify your domain, but the redirect sends it to HTTPS, which may be using the expired certificate, causing the validation to fail in a loop.
Common Mistake: Using SERVER_NAME Instead of HTTP_HOST
Wrong approach:
RewriteRule ^(.*)$ https://%{SERVER_NAME}/$1 [R=301,L]
%{SERVER_NAME} returns the value configured in the Apache virtual host, which may not match what the user actually typed. If your server responds to multiple domain names or if the ServerName directive is not set, this can redirect to the wrong domain.
Correct approach:
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
%{HTTP_HOST} returns the hostname the user actually requested, preserving the exact domain they typed. %{REQUEST_URI} includes the full path with the leading slash, so no extra slash is needed in the substitution.
Handling Load Balancers and Proxies
In many modern hosting setups, your Apache server does not receive traffic directly from the internet. Instead, a load balancer, reverse proxy, or CDN (like Cloudflare, AWS ELB, Nginx, or Varnish) sits between the user and Apache.
In these architectures, the user connects to the proxy over HTTPS, but the proxy connects to Apache over plain HTTP. From Apache's perspective, every request is HTTP, even though the user's connection is fully encrypted.
User ──HTTPS──▶ Load Balancer ──HTTP──▶ Apache
(terminates SSL) (sees HTTP only)
This creates a critical problem: the %{HTTPS} variable is always off, so the redirect rule fires on every request, creating an infinite redirect loop.
1. User requests https://example.com/about
2. Proxy forwards to Apache over HTTP
3. Apache sees HTTPS=off, redirects to https://example.com/about
4. Browser follows redirect to https://example.com/about
5. Proxy forwards to Apache over HTTP
6. Apache sees HTTPS=off, redirects again...
→ ERR_TOO_MANY_REDIRECTS
The X-Forwarded-Proto Header
Most proxies and load balancers set a special header called X-Forwarded-Proto (or sometimes X-Forwarded-Ssl or X-Forwarded-Scheme) that tells the backend server what protocol the original client used.
X-Forwarded-Proto: https
You can check this header in your rewrite condition instead of %{HTTPS}:
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{HTTP:X-Forwarded-Proto} !https
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
</IfModule>
The syntax %{HTTP:X-Forwarded-Proto} reads the value of the X-Forwarded-Proto request header. The condition !https matches when the header is either absent or set to anything other than https.
Supporting Both Direct and Proxied Connections
If your server sometimes receives traffic directly and sometimes through a proxy (common during migrations or in hybrid setups), you can check both conditions:
<IfModule mod_rewrite.c>
RewriteEngine On
# Check if direct HTTPS connection
RewriteCond %{HTTPS} off
# AND check if proxy says it's not HTTPS either
RewriteCond %{HTTP:X-Forwarded-Proto} !https
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
</IfModule>
Both conditions use AND logic (the default). The redirect only fires when %{HTTPS} is off and the X-Forwarded-Proto header is not https. This means:
| Scenario | %{HTTPS} | X-Forwarded-Proto | Redirect? |
|---|---|---|---|
| Direct HTTP | off | (absent) | Yes |
| Direct HTTPS | on | (absent) | No |
| Behind proxy, user on HTTP | off | http | Yes |
| Behind proxy, user on HTTPS | off | https | No |
Provider-Specific Headers
Different providers use different headers. Here are the most common ones:
| Provider / Software | Header | Value when HTTPS |
|---|---|---|
| Most standard proxies | X-Forwarded-Proto | https |
| Cloudflare | X-Forwarded-Proto | https |
| AWS ELB/ALB | X-Forwarded-Proto | https |
| Nginx (custom config) | X-Forwarded-Ssl | on |
| Azure | X-Forwarded-Proto | https |
| Heroku | X-Forwarded-Proto | https |
For Nginx setups that use X-Forwarded-Ssl:
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{HTTP:X-Forwarded-Ssl} !on
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
</IfModule>
The X-Forwarded-Proto header can be spoofed by clients if your Apache server is directly exposed to the internet (without a proxy). An attacker could send a request with X-Forwarded-Proto: https to bypass the redirect. Only trust this header when you are certain that all traffic reaches Apache through a trusted proxy that sets the header correctly. If you are behind a trusted proxy, configure Apache to only accept forwarded headers from the proxy's IP address.
Combining with HSTS
The HTTPS redirect alone has a vulnerability: the very first request a user makes to your site is over HTTP. Even though it gets redirected immediately, that initial unencrypted request can be intercepted. An attacker could capture the HTTP request and respond with a malicious page before the redirect happens (known as an SSL stripping attack).
HTTP Strict Transport Security (HSTS) solves this problem. It is a response header that tells the browser: "From now on, always connect to this domain over HTTPS, even if the user types http:// or clicks an HTTP link."
Basic HSTS Header
<IfModule mod_headers.c>
Header always set Strict-Transport-Security "max-age=31536000" env=HTTPS
</IfModule>
max-age=31536000tells the browser to remember this policy for one year (31,536,000 seconds). For the next year, the browser will automatically convert any HTTP request to this domain into HTTPS before sending it, without contacting the server first.env=HTTPSensures the header is only sent on HTTPS responses. Sending an HSTS header over HTTP is meaningless (and technically incorrect) because the connection is not secure, so the browser has no reason to trust it.
HSTS with Subdomain Coverage
If your site uses subdomains that should also be forced to HTTPS, add the includeSubDomains directive:
<IfModule mod_headers.c>
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains" env=HTTPS
</IfModule>
This applies the HTTPS-only policy to every subdomain: www.example.com, api.example.com, blog.example.com, and so on.
Only enable includeSubDomains if all your subdomains support HTTPS. If even one subdomain lacks a valid certificate, it becomes completely inaccessible to browsers that have received the HSTS header. There is no way to "undo" this for users until the max-age expires.
HSTS Preloading
For the ultimate protection, you can submit your domain to the HSTS preload list, a list built into browsers that forces HTTPS from the very first connection, before any HTTP request is ever made.
<IfModule mod_headers.c>
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" env=HTTPS
</IfModule>
The preload directive signals that you want your domain added to the browser preload list. After adding this header, you must submit your domain at hstspreload.org.
Requirements for preload eligibility:
- Valid HTTPS certificate.
- All HTTP traffic redirected to HTTPS.
- HSTS header with
max-ageof at least one year. - HSTS header must include
includeSubDomains. - HSTS header must include
preload. - The redirect must come from the same host (no intermediate redirects).
HSTS preloading is extremely difficult to reverse. Once your domain is on the preload list, it is hardcoded into browser releases. Removing it requires submitting a removal request and waiting for multiple browser release cycles, which can take months. Only preload your domain if you are absolutely certain that HTTPS will always be available for your domain and all its subdomains.
Gradual HSTS Rollout
If you are enabling HSTS for the first time, start with a short max-age and increase it gradually as you gain confidence:
<IfModule mod_headers.c>
Header always set Strict-Transport-Security "max-age=300" env=HTTPS
</IfModule>
<IfModule mod_headers.c>
Header always set Strict-Transport-Security "max-age=604800" env=HTTPS
</IfModule>
<IfModule mod_headers.c>
Header always set Strict-Transport-Security "max-age=2592000" env=HTTPS
</IfModule>
<IfModule mod_headers.c>
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains" env=HTTPS
</IfModule>
If something goes wrong during the short-period phases, the browser forgets the policy quickly and you can fall back to HTTP while you fix the issue.
Complete Production Configuration
Here is a full, production-ready HTTPS enforcement setup combining the redirect with HSTS:
# === Force HTTPS ===
<IfModule mod_rewrite.c>
RewriteEngine On
# Skip Let's Encrypt certificate validation
RewriteCond %{REQUEST_URI} !^/\.well-known/acme-challenge/ [NC]
# Check for direct HTTP or proxy-reported HTTP
RewriteCond %{HTTPS} off
RewriteCond %{HTTP:X-Forwarded-Proto} !https
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
</IfModule>
# === HSTS Header ===
<IfModule mod_headers.c>
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains" env=HTTPS
</IfModule>
This configuration handles direct connections, proxied connections, certificate renewal, and browser-side HTTPS enforcement through HSTS. Together, the redirect and the HSTS header close the gap that either one alone would leave open: the redirect catches existing HTTP requests, and HSTS prevents future HTTP requests from ever being sent.