Web Security

HTTPS Migration Guide: Moving from HTTP to HTTPS Without Breaking SEO

A step-by-step technical guide for migrating a website from HTTP to HTTPS using Let's Encrypt, configuring 301 redirects and HSTS, fixing mixed content errors, and preserving search rankings in Google Search Console.

October 15, 20258 min readShipSafer Team

In 2025, running any part of a public-facing web property over HTTP is both a security failure and an SEO disadvantage. Google has used HTTPS as a ranking signal since 2014, Chrome marks HTTP pages as "Not Secure," and HTTP/2 and HTTP/3 effectively require TLS. Despite this, HTTP-to-HTTPS migration remains a source of SEO trauma for teams that execute it incorrectly. This guide covers the migration end-to-end: certificate acquisition, redirect configuration, mixed content remediation, HSTS deployment, and Search Console verification.

Before You Start: Audit Your Current Setup

Before changing anything, document the baseline. You need to know:

  1. How many URLs are indexed: Use Google Search Console (GSC) > Index > Pages to get the current index count. Export the list of indexed URLs.
  2. Current canonical tags: Audit your canonical tags — they should be pointing to HTTP URLs. After migration, they all need to update to HTTPS.
  3. Backlinks: Export your backlink profile from Ahrefs, Moz, or Google Search Console. These links will point to HTTP URLs; after migration, your 301 redirects will handle them, but you should understand the volume.
  4. Internal links: Run a crawl with Screaming Frog or similar to find all internal links. Every internal link pointing to HTTP will need updating after migration (not for SEO — 301s handle link equity — but for performance, since every 301 redirect adds a round trip).
  5. Third-party scripts and resources: Identify all external resources (scripts, stylesheets, fonts, images, iframes). Any that are loaded over HTTP will cause mixed content errors after migration.

Getting a Certificate with Let's Encrypt

Let's Encrypt provides free, automated, and open-source TLS certificates. Certificates are valid for 90 days and must be renewed automatically. The standard toolchain is Certbot.

Installing Certbot:

# Ubuntu/Debian
sudo apt update
sudo apt install certbot python3-certbot-nginx  # or python3-certbot-apache

# CentOS/RHEL
sudo dnf install certbot python3-certbot-nginx

Obtaining a certificate (nginx):

# Stop nginx temporarily to use standalone mode, or use the nginx plugin
sudo certbot --nginx -d example.com -d www.example.com

# For wildcard certificates (requires DNS challenge)
sudo certbot certonly --manual \
  --preferred-challenges dns \
  -d "*.example.com" \
  -d "example.com"

For wildcard certificates, Certbot requires you to add a DNS TXT record to verify domain ownership. Many DNS providers support this via API, which enables automated wildcard cert renewal.

Automatic renewal: Certbot installs a systemd timer or cron job that runs certbot renew twice daily. This is sufficient for most deployments. Verify it is active:

sudo systemctl status certbot.timer
# Or check the cron job
sudo crontab -l | grep certbot

Test renewal without actually renewing:

sudo certbot renew --dry-run

Certificate file locations (Let's Encrypt):

  • Certificate: /etc/letsencrypt/live/example.com/cert.pem
  • Full chain: /etc/letsencrypt/live/example.com/fullchain.pem
  • Private key: /etc/letsencrypt/live/example.com/privkey.pem

Always use fullchain.pem (certificate + intermediate chain) in your web server config, not just cert.pem.

Nginx Configuration

A complete nginx HTTPS configuration with HTTP redirect:

# HTTP — redirect all traffic to HTTPS
server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;

    # Allow Let's Encrypt ACME challenges
    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    # Redirect everything else to HTTPS
    location / {
        return 301 https://$host$request_uri;
    }
}

# HTTPS — main server block
server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name example.com www.example.com;

    # Certificate configuration
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    # Mozilla Modern SSL configuration (TLS 1.2+)
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;
    ssl_session_timeout 1d;
    ssl_session_cache shared:SSL:10m;
    ssl_session_tickets off;

    # OCSP stapling
    ssl_stapling on;
    ssl_stapling_verify on;
    ssl_trusted_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    resolver 8.8.8.8 8.8.4.4 valid=300s;

    # HSTS (see below before enabling)
    # add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

    root /var/www/html;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }
}

Apache Configuration

# /etc/apache2/sites-available/example.com.conf

# HTTP → HTTPS redirect
<VirtualHost *:80>
    ServerName example.com
    ServerAlias www.example.com

    # ACME challenge
    Alias /.well-known/acme-challenge/ /var/www/certbot/.well-known/acme-challenge/
    <Directory "/var/www/certbot/.well-known/acme-challenge/">
        Options None
        AllowOverride None
        Require all granted
    </Directory>

    RewriteEngine On
    RewriteCond %{REQUEST_URI} !^/.well-known/acme-challenge/
    RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
</VirtualHost>

# HTTPS
<VirtualHost *:443>
    ServerName example.com
    ServerAlias www.example.com

    SSLEngine on
    SSLCertificateFile /etc/letsencrypt/live/example.com/fullchain.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem

    # Mozilla Modern configuration
    SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1
    SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:...
    SSLHonorCipherOrder off
    SSLSessionTickets off

    DocumentRoot /var/www/html
</VirtualHost>

HTTP Strict Transport Security (HSTS)

HSTS tells browsers to only connect to your site over HTTPS for a specified period, even if the user types http:// or clicks an HTTP link. Once active, browsers will refuse HTTP connections to your site without even sending a request.

Deploy HSTS in stages — getting this wrong can lock users out of your site:

Stage 1 — Short max-age (test for 1-2 weeks):

Strict-Transport-Security: max-age=300

This tells browsers to cache the HSTS policy for 5 minutes. During this phase, verify no users are reporting access issues.

Stage 2 — Increase max-age (run for a few weeks):

Strict-Transport-Security: max-age=86400

24-hour caching. Continue monitoring for issues.

Stage 3 — Production HSTS:

Strict-Transport-Security: max-age=63072000; includeSubDomains

Two-year max-age. The includeSubDomains directive means all subdomains must also be served over HTTPS. Only add this if all subdomains are HTTPS-capable.

Stage 4 — HSTS Preloading (optional but recommended):

Strict-Transport-Security: max-age=63072000; includeSubDomains; preload

The preload flag indicates your site wants to be included in browser preload lists. Submit at hstspreload.org. This bakes HTTPS-only behavior into the browser itself — users get HSTS protection even on their first visit, before the header has been served. Warning: preloading is difficult to reverse. Only preload if you are confident you will maintain HTTPS indefinitely.

Fixing Mixed Content

Mixed content is the single most common migration issue. It occurs when an HTTPS page loads resources (scripts, stylesheets, images, iframes) over HTTP. Mixed content blocks or warnings will appear in the browser console.

Active mixed content (scripts, stylesheets, iframes loaded over HTTP): Browsers block these entirely in modern versions.

Passive mixed content (images, audio, video loaded over HTTP): Browsers may display a warning but still load the resource.

Finding mixed content:

  1. Open Chrome DevTools Console — blocked mixed content appears as errors.
  2. Use Screaming Frog with HTTPS crawl configured to flag HTTP resources.
  3. Use the browser extension "HTTPS Everywhere" or "Mixed Content Checker" for site-wide scanning.

Fixing mixed content — in your codebase:

Search for hardcoded http:// references:

# Find HTTP references in template/component files
grep -r "http://" src/ --include="*.tsx" --include="*.ts" --include="*.html" | grep -v "https://"

Replace hardcoded HTTP asset URLs with HTTPS equivalents, or use protocol-relative URLs (//cdn.example.com/asset.js) — though note protocol-relative URLs are deprecated in modern usage; prefer explicit https://.

For assets on your own CDN or server, use relative URLs where possible:

<!-- Instead of this -->
<img src="http://cdn.example.com/logo.png" />

<!-- Use this -->
<img src="/images/logo.png" />
<!-- Or this -->
<img src="https://cdn.example.com/logo.png" />

Content-Security-Policy as a mixed content detector: Add the following header in report-only mode during migration to get a consolidated report of all mixed content:

Content-Security-Policy-Report-Only: upgrade-insecure-requests; default-src https: 'self'; report-uri /csp-report

The upgrade-insecure-requests directive instructs the browser to silently upgrade HTTP requests to HTTPS — useful as a temporary band-aid during migration.

Search Console: Updating Your Property

After migration, update Google Search Console to track HTTPS:

  1. Add HTTPS property: In GSC, add https://example.com as a new property (do not delete the HTTP property yet).
  2. Set preferred domain: In your HTTPS property, set https://example.com as the preferred domain.
  3. Resubmit sitemaps: Ensure your sitemap uses HTTPS URLs. Submit it in the new HTTPS property.
  4. Monitor index coverage: Over the following weeks, watch the HTTPS property's index coverage. URLs should migrate from the HTTP property to the HTTPS property as Googlebot recrawls them.
  5. Update canonical tags: Ensure all canonical tags in your HTML reference HTTPS URLs, not HTTP.

The HTTP property's index count will decrease and the HTTPS property's will increase. This process typically takes 2-8 weeks depending on your crawl budget. Rankings should be preserved — 301 redirects pass full link equity in Google's current algorithm.

Final Verification Checklist

Before considering the migration complete:

  • Certificate installs correctly and is for the right domains (test with openssl s_client -connect example.com:443)
  • All HTTP requests redirect to HTTPS with 301 (not 302)
  • HSTS header is present on HTTPS responses
  • No mixed content errors in browser console on any page
  • Canonical tags updated to HTTPS
  • Sitemap updated to HTTPS URLs
  • HTTPS property added and sitemap submitted in Search Console
  • Internal links updated to HTTPS (for performance, not SEO)
  • Certificate auto-renewal is configured and tested (certbot renew --dry-run)
  • www redirect is correct (www → non-www or vice versa, consistently, over HTTPS)
HTTPS
TLS
Let's Encrypt
HSTS
SEO
mixed content
certificates
web security

Check Your Security Score — Free

See exactly how your domain scores on DMARC, TLS, HTTP headers, and 25+ other automated security checks in under 60 seconds.