Google’s AdSense help names two HTTPS problems among the reasons a site is not ready to show ads: a certificate that is not valid, and no redirect from HTTP to HTTPS. Both look like settings you make once, and both are easy to get half right, because HTTPS is four things that fail independently. The certificate must keep renewing, the redirect must land on one address, a header must tell browsers to remember, and no page may load anything over plain HTTP. The padlock on your home page proves the certificate is valid today. It says nothing about the other three.
Check the certificate by its dates
A padlock tells you the certificate works now. What you need to know is when it stops working and whether anything will replace it. One command prints the dates straight from the server, no browser involved:
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -dates
The -servername flag matters. A server that hosts several sites picks the certificate from that name, and without it you may be shown the wrong one. The echo | ends the connection once the handshake is done. Run the same command for www.example.com: a visitor who types www reaches that host first, and a name mismatch there is a full-page browser warning, shown before any redirect can happen. To list the names a certificate covers, replace -dates with -ext subjectAltName (OpenSSL 1.1.1 or later).
To turn the check into an alarm, -checkend makes the command fail when the certificate expires within a number of seconds. Here that number is 1209600, which is 14 days:
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
| openssl x509 -noout -checkend 1209600
Run it from cron or your monitoring and send the failure somewhere you will read. With a free authority such as Let’s Encrypt, automatic renewal is what keeps the site up, and renewal breaks for dull reasons: a moved web root, a firewall rule on port 80, a deleted timer. If you use certbot, certbot renew --dry-run rehearses a renewal without issuing anything. Run it after every change to the server.
One redirect to one final address
Plain HTTP should never serve a page. It should answer with a permanent redirect to the final HTTPS address, in a single step, because each extra hop costs the visitor a round trip. Google reads a 301 as a strong signal about the preferred address and a 302 as a weak one, so use a 301. It follows up to 10 hops, which makes a longer chain wasteful rather than fatal. This is the nginx version, with the same two small blocks as in our canonical tags guide plus the certificate validation path:
# Port 80: no content, only a redirect to the final address
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
# Certificate validation answers here instead of being redirected
location ^~ /.well-known/acme-challenge/ { root /var/www/example.com; }
location / { return 301 https://example.com$request_uri; }
}
# Port 443, www: one hop to the bare domain
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name www.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
return 301 https://example.com$request_uri;
}
On Apache, mod_ssl must be enabled (a2enmod ssl on Debian and Ubuntu). The Redirect directive from mod_alias keeps the path and the query string:
<VirtualHost *:80>
ServerName example.com
ServerAlias www.example.com
Redirect permanent / https://example.com/
</VirtualHost>
<VirtualHost *:443>
ServerName www.example.com
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem
Redirect permanent / https://example.com/
</VirtualHost>
Without access to the virtual host, the .htaccess rule in the canonical guide does the same job. Validate before you reload (nginx -t, apachectl configtest), then request each variant once. Without -L, curl -sI shows a single hop, so you can read which address each variant sends visitors to:
for u in http://example.com/ http://www.example.com/ https://www.example.com/ https://example.com/; do
echo "== $u"
curl -sI "$u" | grep -iE '^(HTTP|location)'
done
The first 3 variants should each print one 301 whose Location is https://example.com/, and the last should print a 200 with no Location at all. A Location that is not the final address is a chain. Once the chain is clean, make the rest agree with it: canonical tags, sitemap entries and internal links should all name the HTTPS address. Google also reads robots.txt per host and protocol, so the file has to answer on the HTTPS host, as covered in the robots.txt mistakes guide.
Add HSTS in steps
A redirect still lets the first request of every visit travel over plain HTTP. HSTS closes that gap. Once a browser has received the header on an HTTPS response, it connects to your site over HTTPS on its own for the stated number of seconds, even if a link says http://. The price is the same feature seen from the other side. If you later break HTTPS on a covered host, visitors who have seen the header get a hard error rather than a warning they can click through, and nothing on your server can reach into their browser to undo it. Every response that carries the header restarts the clock, so on a busy site a long max-age is close to permanent.
For that reason we recommend raising the value in 4 steps and moving to the next only when the previous one has shown nothing wrong. Sending max-age=0 over HTTPS removes the policy from browsers that come back, which makes the early steps cheap to undo and the last one not.
| Step | Header value | Lifetime | Move on when |
|---|---|---|---|
| 1 | max-age=300; includeSubDomains | 5 minutes | every page type and every subdomain has loaded over HTTPS with no warning |
| 2 | max-age=86400; includeSubDomains | 1 day | a day of normal traffic has passed with no certificate or mixed content complaint |
| 3 | max-age=15552000; includeSubDomains | 180 days | you are sure no covered host will need plain HTTP again |
| 4 | max-age=63072000; includeSubDomains | 2 years | final value |
We put includeSubDomains in from step 1 on purpose. If you intend to use it eventually, a forgotten subdomain without HTTPS then costs 5 minutes of lost access for the visitors who loaded your site, and you find it while it is cheap. Add it only after you have listed every DNS record, including the staging host and the old mail host. In nginx the header goes in the HTTPS server block for the bare domain:
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name example.com;
# ssl_certificate and ssl_certificate_key as in the www block
add_header Strict-Transport-Security "max-age=300; includeSubDomains" always;
}
In Apache it needs mod_headers (a2enmod headers), inside the HTTPS virtual host of the bare domain:
Header always set Strict-Transport-Security "max-age=300; includeSubDomains"
The always keyword makes the header appear on error pages too. Check it with curl -sI https://example.com/ | grep -i '^strict-transport', which should print exactly 1 line. A later add_header inside a location block can silently drop the header for that location, a trap we describe in the security headers guide.
Preload comes last, if at all
The preload list is a copy of the HSTS rule shipped with browsers, so even a first visit goes straight to HTTPS. The submission site, hstspreload.org, requires a valid certificate, a redirect from HTTP to HTTPS on the same host, HTTPS on all subdomains, and a header on the bare domain with a max-age of at least 31536000 (1 year), includeSubDomains and preload. It also warns that removal is hard and takes months.
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
Two consequences matter before you add that last word. First, every current and future subdomain must work over HTTPS, including the ones nobody remembers. Second, the same-host rule changes the redirect advice above. http://www.example.com must first go to https://www.example.com, and only that host sends the visitor on to the bare domain. That is 2 hops for the www variant instead of 1, which is harmless for Google, since it follows up to 10, but it is not the single step we drew earlier. Our own port 80 block works this way: it redirects to the same host first.
We do not send preload. It protects the one visit HSTS cannot, the very first, before a browser has seen your header. For a content site that already redirects and sends a 2-year header, we judge that gain too small for an exit that takes months. That is our judgment, not a rule, and a site that handles logins or payments may weigh it differently.
Find the mixed content that undoes the padlock
Mixed content is an HTTPS page that loads a script, stylesheet, image, font, frame or media file from an http:// address. Browsers block or flag these requests, so the symptoms are a stylesheet that does not load, an embed that stays empty or a padlock that turns into a warning, usually on a few old pages that hard-code the former address. The usual sources are posts imported before the switch with absolute http:// image URLs saved in the database, templates with a hard-coded protocol, url(http://...) in a stylesheet, and third-party embeds pasted years ago.
The browser’s developer console lists each flagged request, one page at a time. For a static site, scan the build output instead (here the folder dist/):
grep -rhEo '(src|href)="http://[^"]+"' dist/ | sort | uniq -c | sort -rn | head -20
Not every match is a problem. An <a href> to an outside site that only offers HTTP is a plain link, but a src or a <link rel="stylesheet"> is a real leak. On WordPress, WP-CLI can rewrite the stored addresses. Take a backup, run it once with --dry-run and read the counts before the real pass:
wp search-replace 'http://example.com' 'https://example.com' --dry-run
A Content-Security-Policy with upgrade-insecure-requests tells browsers to request http:// subresources over HTTPS instead. We would use it as a safety net and not as the fix: it breaks for any resource whose host has no HTTPS version, and it hides the source of the problem. Report-only policies and the other directives are in the security headers guide.
What we run, measured on 7 October 2026
Our server sends Strict-Transport-Security: max-age=63072000; includeSubDomains, redirects www to the bare domain with a 301, and uses a Let’s Encrypt certificate that renews automatically. On 7 October 2026 we ran the commands from this guide against our own domain:
$ curl -sI https://greenlightmysite.com/ | grep -i '^strict-transport'
Strict-Transport-Security: max-age=63072000; includeSubDomains
$ curl -sI https://www.greenlightmysite.com/ | grep -iE '^(HTTP|location)'
HTTP/1.1 301 Moved Permanently
Location: https://greenlightmysite.com/
$ echo | openssl s_client -connect greenlightmysite.com:443 -servername greenlightmysite.com 2>/dev/null | openssl x509 -noout -dates
notBefore=Sep 5 07:41:00 2026 GMT
notAfter=Dec 4 07:40:59 2026 GMT
The certificate runs from 5 September to 4 December 2026: 90 days, which is why automatic renewal is not optional for us. The one imperfect result is http://www.greenlightmysite.com/: it first goes to the same host over HTTPS and then to the bare domain, 2 hops where the single step above would do. We have not changed it, and it would be the shape to keep if we ever submitted the domain to the preload list. Our SEO checker tests for HSTS as part of its security headers category, and our AdSense checker reports the HTTPS status and the length of the homepage redirect chain.
Quick answers
Do I need a paid certificate for AdSense?
The AdSense help we rely on asks for a valid certificate and names no issuer and no price. Our own site runs on a free Let's Encrypt certificate, and the HTTPS check of our AdSense checker was green when we audited the site on 7 October 2026.
Why does curl show no Strict-Transport-Security header on my http:// address?
It should not. Browsers ignore the header when it arrives over plain HTTP, because anyone on the network path could have injected it, so the header belongs on HTTPS responses only. Test with curl -sI against the https:// address.
My site sits behind a CDN or proxy that handles HTTPS. Where do these settings go?
Set the redirect and the header at the layer that ends the HTTPS connection, since that is the one visitors talk to, and test with curl from outside your own network. If the proxy and your origin both add Strict-Transport-Security, count the lines in curl -sI: you want exactly 1.