NEWS
Checkout Files Upload for WooCommerce lets anyone upload PHP. See whether yours is affected
SSL certificates

HTTPS for a WordPress site without errors or mixed content

# TagsSSLSecurity
The WPhost team16 min read

HTTPS for a WordPress site looks like one setting, but in practice it is several. The certificate can be fine and the browser still warns that the connection is not secure. Or the site opens but images, styles and buttons are missing. The cause can be mixed content, meaning a page that arrives over HTTPS while part of it is still loaded over HTTP.

This guide goes through everything in order. You check the certificate, change the addresses and add one redirect. Then you find the mixed content in the browser and fix it in the database. At the end you see how Cloudflare can create a loop and whether you need HSTS.

How to check that the certificate works

In cPanel the certificates are issued by AutoSSL, if the host has turned it on. According to the cPanel documentation, it makes certificates valid for 90 days and tries to replace them on its own before they expire.

Open Security > SSL/TLS Status in cPanel. Filters there show which domains are Active, Expired, Expiring Soon, Unsecured or have an AutoSSL problem, and the Certificate Status column holds the AutoSSL error, if there is one.

Check the address with and without www. AutoSSL adds the www variant to the certificate, but only if it passes the ownership check too, otherwise www stays without a certificate. As an example of a problem, cPanel gives a domain that does not resolve to an IPv4 address on the internet. How to point a domain to hosting is in the guide to registering a .bg domain.

You can also read the certificate from the command line. The command shows who it is for, who issued it and until when it is valid.

Bash
# From your computer or cPanel > Terminal, read-only
echo | openssl s_client -connect primer.bg:443 \
  -servername primer.bg 2>/dev/null \
  | openssl x509 -noout -subject -issuer -dates

The -servername parameter tells the server which site you are asking about, because many sites share one address. The date in the notAfter line is the end of the term. Repeat the command for www.primer.bg.

How to change the WordPress addresses to HTTPS

WordPress keeps two addresses. Site Address (URL) is the address visitors type in the browser, and WordPress Address (URL) is the place of the core files. The WordPress documentation wants both to start with https:// and to have no trailing slash.

The easiest way is Tools > Site Health. Since WordPress 5.7, when the server answers over HTTPS, the Status tab has a button “Update your site to use HTTPS” (in the Bulgarian admin it reads “Обновяване на сайта за ползване на HTTPS”). It changes both addresses at once, checks that WordPress recognizes the site as HTTPS, and if it does not, puts the old addresses back.

The button is for the usual case where both addresses are the same. It does not appear if WP_HOME or WP_SITEURL are set in wp-config.php, or if WordPress cannot find working HTTPS.

The other way is Settings > General. Change both fields from http:// to https:// and save. If the fields cannot be edited, the addresses are set in wp-config.php and you change them there.

PHP
// wp-config.php, above "That's all, stop editing!"
define( 'WP_HOME', 'https://primer.bg' );
define( 'WP_SITEURL', 'https://primer.bg' );

If you changed the addresses before HTTPS worked and cannot log in, put the old http:// values in these lines and remove them when the certificate is fine.

To make login and admin HTTPS only, add define( 'FORCE_SSL_ADMIN', true ); to wp-config.php once SSL works. The constant has to be in wp-config.php, not in a plugin.

How to redirect HTTP to HTTPS in .htaccess

The new addresses do not fix the old links from search engines and other sites that still lead to http://primer.bg. For them the server has to answer with a redirect, and in cPanel that is one rule in .htaccess.

The file is in the site folder, next to wp-config.php. It is hidden, so in File Manager turn on Show Hidden Files from Settings. Add the lines above the # BEGIN WordPress block, not inside it, because WordPress rewrites that block when you change the permalinks.

Apache
# .htaccess, above # BEGIN WordPress
RewriteEngine On
RewriteCond %{HTTPS} !=on
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]

According to Apache, %{HTTPS} is on only on SSL/TLS, so the rule sends everything else to the same address with https and code 301, keeping the path and parameters.

Check with two commands. The first should return 301 and a Location line with https. The second counts the redirects and shows the final address, and you expect https://primer.bg/ 200 1. If curl stops with the message Maximum (50) redirects followed, you have a loop.

Bash
# From your computer, read-only
curl -I http://primer.bg
curl -sIL -o /dev/null \
  -w '%{url_effective} %{http_code} %{num_redirects}\n' \
  http://primer.bg

If a plugin or a rule in Cloudflare already redirects to HTTPS, do not add a second one. Two redirects can contradict each other and create a loop.

How to find mixed content in the browser

Mixed content is when a page loaded over HTTPS loads resources over HTTP. According to MDN, such a resource can be seen or changed on its way, and scripts are the most dangerous because they can change any part of the page.

Browsers react in two ways. Images, audio and video are fetched over HTTPS if they can be, and if they are not there, they do not load. Scripts, styles and iframes are blocked, so the page loses its layout or buttons.

Open the site over HTTPS and press Ctrl + Shift + J (on a Mac Cmd + Option + J) to see the console in Chrome. Reload the page and look for Mixed Content messages. Firefox and Safari also write them to the console. In the filter of the Network panel you can type mixed-content:all.

The console shows only the current page. Go through the home page, a post, a page with a form and, if you have a shop, a product, the cart and the checkout. Without tools, open view-source:https://primer.bg and search for http://. Links with <a href> are usually not mixed content, so look at src, srcset and the href of link.

Mixed content comes from old posts with full HTTP addresses, from theme and plugin settings, from the theme files and from third-party resources that are still loaded over HTTP.

Since version 5.7 WordPress replaces the old HTTP address of the site on its own when it displays content, but only in the content of posts and pages, in their excerpts, in text widgets and in the additional CSS, and only if the addresses were changed on a site with content and share a domain. Everything else stays untouched, for example http://www.primer.bg when the main address has no www.

How to replace HTTP addresses in the database

The real fix is for the addresses in the database to start with https://. Do not replace them with a REPLACE query in phpMyAdmin. WordPress keeps part of the data serialized, with the length of every string, and changing an address without changing the length corrupts it. WP-CLI and the plugin below preserve serialized data.

Then run the replacement as a trial first. The --dry-run parameter performs the whole operation and shows a report but saves nothing, and --skip-columns=guid skips the guid column, which the WordPress documentation says never to change. The address is the one that was in Site Address before the change, with or without www. More about WP-CLI is in the guide to its commands.

Bash
# cPanel > Terminal, in the site folder, trial only
wp search-replace 'http://primer.bg' 'https://primer.bg' \
  --skip-columns=guid --report-changed-only --dry-run

In the report you see which tables and columns will change and how many replacements there will be. If the numbers are reasonable, run the same command without --dry-run.

Bash
# cPanel > Terminal, in the site folder, saves the replacements
wp search-replace 'http://primer.bg' 'https://primer.bg' \
  --skip-columns=guid --report-changed-only

If a plugin keeps data as JSON, the address there has escaped slashes. Search for 'http:\/\/primer.bg' and replace with 'https:\/\/primer.bg', again with --dry-run first. Then clear the cache of the caching plugin and of Cloudflare and reload the page.

If you have no terminal

Install the Better Search Replace plugin from wordpress.org. You find it in the Tools menu. It supports serialized data in all tables and has a trial run (dry run) that shows how many fields will change before it saves. You enter http://primer.bg, replace with https://primer.bg, run the trial and only then save. If there is a choice about GUIDs, leave it off. Its page warns that a wrong string can damage the database, so the backup applies here too. Delete the plugin afterwards.

Why does Cloudflare in Flexible mode cause a redirect loop?

Cloudflare keeps two connections, from the visitor to Cloudflare and from Cloudflare to your server. The encryption mode decides how the second one behaves. With Full and Full (strict), Cloudflare talks to the server over the visitor's protocol.

ModeVisitor to CloudflareCloudflare to the server
FlexibleHTTPSHTTP
FullHTTPSHTTPS, certificate not checked
Full (strict)HTTPSHTTPS, certificate checked

With Flexible, Cloudflare sends requests to the server over HTTP. If .htaccess or a plugin redirects every HTTP request to HTTPS, the loop is ready. The visitor asks for HTTPS, Cloudflare asks the server over HTTP, the server sends it back to HTTPS again, and so on until the ERR_TOO_MANY_REDIRECTS error. WordPress describes a similar loop for FORCE_SSL_ADMIN behind a proxy that does the SSL for the site.

The solution is Full (strict). The mode needs a valid, unexpired certificate from a trusted issuer on the server, matching the name, and AutoSSL issues one. You pick it in Cloudflare under SSL/TLS, on the Overview page. If you see error 526, check the certificate on the server first, because it most likely does not meet the requirements.

Cloudflare recommends Full or Full (strict) when possible and advises against Flexible for an application with a login and personal data, and WordPress has a login. If you see Automatic SSL/TLS in the dashboard, check which mode it has chosen.

Be careful with Edge Certificates too. Always Use HTTPS redirects in Cloudflare, and Cloudflare recommends not redirecting to HTTPS on the server as well, because that can produce a loop. Pick one place. Automatic HTTPS Rewrites hides part of the mixed content on the fly, but the database stays as it was.

Do you need HSTS?

HSTS is a response header with which the server tells the browser to use only HTTPS for this site for the next max-age seconds. According to MDN, while the term runs, the browser turns HTTP into HTTPS by itself and does not let the visitor continue if the certificate is invalid. Working HTTPS does not require it, so it is optional.

The same thing that makes it useful makes it risky. If you remove HTTPS or the certificate expires while the policy runs, the site is unreachable for the length of the term.

So start with a short term. Do not turn on includeSubDomains and preload unless you know that every subdomain works over HTTPS. Cloudflare warns that subdomains without HTTPS become unreachable, and MDN says preload needs a term of at least one year. The first step is a rule in .htaccess, below the redirect, with a term of five minutes.

Apache
# .htaccess, below the redirect rule
<IfModule mod_headers.c>
Header always set Strict-Transport-Security "max-age=300" env=HTTPS
</IfModule>

The rule sends the header only over HTTPS, because browsers ignore it over HTTP, and 300 seconds are five minutes. Check with curl -sI https://primer.bg that the header comes back. If everything is fine for a few days, raise the term in steps. To stop the policy, send max-age=0. If Cloudflare is in front of the site, turn HSTS on in one place only.

Checklist

  • https://primer.bg and https://www.primer.bg open without a warning.
  • Site Address and WordPress Address use https:// and have no trailing slash.
  • http://primer.bg leads in one hop (301) to https://primer.bg, with no loop.
  • I have a database backup, and the replacement ran first with --dry-run and without the guid column.
  • The console shows no Mixed Content on any of the pages I checked, and the cache is cleared.
  • Cloudflare is in Full (strict) mode if it is in front of the site, and the redirect is in one place only.
  • HSTS is on only if I decided so, and with a short term.

If you would rather not do it yourself

Not every site needs you to touch the database. If the site is new and has no old posts, the two addresses and the redirect are enough.

If the site stopped after the address change or you cannot find where the mixed content comes from, describe what you see in the inquiry form. SSL is also part of WordPress maintenance, wherever your site is hosted.

Frequently asked questions

Why does the browser say my site is not secure even though it has an SSL certificate?

The most common cause is mixed content, meaning a page that arrives over HTTPS while part of it still loads over HTTP. Browsers block scripts, styles and iframes over HTTP, so the page loses its layout or buttons, while images, audio and video are fetched over HTTPS and do not load if they are not there. Another cause can be a certificate that does not cover the www variant.

How do I check my site's SSL certificate in cPanel?

Open Security > SSL/TLS Status in cPanel, where filters show which domains are Active, Expired, Expiring Soon, Unsecured or have an AutoSSL problem. Check the address with and without www, because AutoSSL adds the www variant only if it also passes the ownership check. According to the cPanel documentation, AutoSSL issues 90-day certificates and tries to replace them on its own before they expire.

How do I change the WordPress address from HTTP to HTTPS?

The easiest way is Tools > Site Health, which since WordPress 5.7 has an "Update your site to use HTTPS" button that changes both addresses at once and puts them back if something is wrong. The other way is Settings > General, where both addresses should start with https:// and have no trailing slash. Do not change the addresses until https:// opens without a warning.

How do I redirect HTTP to HTTPS in .htaccess?

Add one redirect rule with code 301 to .htaccess, above the # BEGIN WordPress block and not inside it, because WordPress rewrites that block when you change the permalinks. Copy the file first, since a mistake in one line can stop the site. If a plugin or a Cloudflare rule already redirects to HTTPS, do not add a second one, because the two can create a loop.

How do I find mixed content on my site?

Open the site over HTTPS and press Ctrl + Shift + J (on a Mac Cmd + Option + J) to open the Chrome console, then reload the page and look for Mixed Content messages. The console shows only the current page, so go through the home page, a post, a form and, for a shop, a product, the cart and the checkout. Without tools, put view-source: in front of the address and search for http:// in src, srcset and the href of link.

Can I replace the HTTP addresses in the database with phpMyAdmin?

Do not run a REPLACE query in phpMyAdmin. WordPress keeps part of its data serialized, with the length of every string, and changing an address without changing the length corrupts it. Use wp search-replace in WP-CLI or the Better Search Replace plugin, as a trial run first and after backing up the database.

Why does Cloudflare in Flexible mode cause a redirect loop?

In Flexible mode Cloudflare sends requests to the server over HTTP. If .htaccess or a plugin redirects every HTTP request to HTTPS, the server keeps sending the visitor back to HTTPS until the ERR_TOO_MANY_REDIRECTS error. The fix is Full (strict) mode under SSL/TLS in Cloudflare, which needs a valid certificate on the server, such as one from AutoSSL.

Do I need HSTS?

It is optional, because HTTPS works without it. HSTS tells the browser to use only HTTPS for the site for the max-age period, and if you remove HTTPS or the certificate expires during that time, the site is unreachable until the term ends. So start with a short term, such as 300 seconds, and do not turn on includeSubDomains and preload unless every subdomain works over HTTPS.

Sources

# TagsSSLSecurity