Skip to content

Forcing HTTPS with .htaccess

Learn how to enforce HTTPS encryption on your Apache site using .htaccess with redirect rules, handle load balancer/proxy setups, and implement HSTS for maximum security against SSL stripping attacks.

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.

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} off checks the HTTPS server variable. It is off when the connection is plain HTTP and on when 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 the https:// 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:

  1. 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.
  2. Search engines transfer ranking signals. A 301 tells crawlers to index the HTTPS version and transfer all link equity from the old HTTP URLs.

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.

HTTPS redirect with certificate validation exclusionsapache
<IfModule mod_rewrite.c>
  RewriteEngine On
  RewriteCond %{HTTPS} off

  RewriteCond %{REQUEST_URI} !^/\.well-known/acme-challenge/ [NC]

  RewriteCond %{REQUEST_URI} !^/\.well-known/cpanel-dcv/[\w-]+$ [NC]

  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.

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}:

HTTPS redirect behind a proxyapache
<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:

HTTPS redirect supporting both direct and proxied connectionsapache
<IfModule mod_rewrite.c>
  RewriteEngine On

  RewriteCond %{HTTPS} off

  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>

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=31536000 tells 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=HTTPS ensures 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.

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-age of 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).

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:

Week 1: Test with 5 minutesapache
<IfModule mod_headers.c>
  Header always set Strict-Transport-Security "max-age=300" env=HTTPS
</IfModule>
Week 2: Extend to 1 weekapache
<IfModule mod_headers.c>
  Header always set Strict-Transport-Security "max-age=604800" env=HTTPS
</IfModule>
Week 3: Extend to 1 monthapache
<IfModule mod_headers.c>
  Header always set Strict-Transport-Security "max-age=2592000" env=HTTPS
</IfModule>
Final: Full 1 yearapache
<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:

Complete HTTPS enforcement with HSTSapache
<IfModule mod_rewrite.c>
  RewriteEngine On

  RewriteCond %{REQUEST_URI} !^/\.well-known/acme-challenge/ [NC]

  RewriteCond %{HTTPS} off
  RewriteCond %{HTTP:X-Forwarded-Proto} !https

  RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
</IfModule>

<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.

Navigation