Infrastructure
·5 min read

Why wget https://1.2.3.4 fails with an SSL error (and what SNI has to do with it)

A common debugging mistake: trying to test an HTTPS server by hitting its IP address directly with wget or curl. It always fails certificate validation, and the reason is one of the more interesting corners of how SSL/TLS actually works on the modern web.

Note

**TL;DR — HTTPS certificates are issued for hostnames, not IP addresses.** When you connect to `https://1.2.3.4` the server has no idea which site you're trying to reach and either presents a default certificate (which won't match the IP) or fails the handshake entirely. Use the `--resolve` flag in curl or `--header Host:` in wget to talk to a specific server while still presenting the right hostname for SNI and certificate validation.

I get this question a lot from junior engineers and from developers who are trying to test a server that hasn't had its DNS pointed at yet. They want to verify the new server is serving the site correctly before flipping the DNS, and they reach for the obvious approach:

bash
wget https://1.2.3.4/

And it fails:

text
ERROR: cannot verify 1.2.3.4's certificate, issued by '/C=US/O=Let\'s Encrypt/CN=R3':
  Unable to locally verify the issuer's authority.
ERROR: certificate common name 'example.com' doesn't match requested host name '1.2.3.4'.
To connect to 1.2.3.4 insecurely, use `--no-check-certificate'.

And the temptation is to just slap on `--no-check-certificate` and move on. Don't do that. The error is telling you something useful. The certificate is for `example.com`, you asked for `1.2.3.4`, and the names don't match because — well, this is the interesting part.

Why are HTTPS certificates tied to hostnames?

Modern HTTPS is built around the idea that you trust a server because a Certificate Authority has confirmed that *the holder of this certificate also controls this hostname*. The hostname is what gets verified during issuance — when you request a Let's Encrypt cert for `example.com`, Let's Encrypt asks you to prove you control the DNS or HTTP serving for `example.com` specifically. They don't care what IP address that domain happens to point at, because the IP could change tomorrow.

When you connect to `https://1.2.3.4`, your client (wget, curl, browser, whatever) presents `1.2.3.4` as the hostname it's trying to reach. The server hands back its certificate. The client looks at the certificate's `Subject Alternative Name` field (and the older `Common Name` field) and asks "does any of that match `1.2.3.4`?" Usually it doesn't, because nobody issues certificates for raw IP addresses outside of very specific internal scenarios. Validation fails, error.

What is SNI doing here?

There's a second wrinkle. A single IP address can host hundreds or thousands of HTTPS sites — that's how shared hosting works. When the client connects, the server has to know *which* certificate to send back, because there's no way to send all of them. The mechanism that solves this is called Server Name Indication (SNI). Right at the start of the TLS handshake, the client tells the server "I'm trying to reach `example.com`," and the server picks the matching certificate for that hostname.

When you connect to `https://1.2.3.4`, your client sends `1.2.3.4` as the SNI value. The server looks for a vhost matching that hostname, doesn't find one (because you almost never configure your server with a vhost for its raw IP), and falls back to its default vhost. The default vhost might be the right site or it might be a completely unrelated one — and even if it's the right one, its certificate is issued for `example.com`, not for `1.2.3.4`, so validation fails anyway.

The right way to test a server before DNS exists

What you actually want is "connect to this specific IP address, but pretend the hostname is example.com so SNI and certificate validation both work correctly." curl has exactly the flag for this:

bash
curl --resolve example.com:443:1.2.3.4 https://example.com/

What this does: it tells curl "when I ask you to reach `example.com:443`, don't actually look it up in DNS — go straight to `1.2.3.4` instead." Everything else proceeds normally. SNI sends `example.com`, the server picks the right vhost, the certificate validates against `example.com`, you get a real response from the new server without ever touching DNS.

wget has a slightly clunkier equivalent. There's no `--resolve`, but you can override the Host header and use the IP directly:

bash
wget --header="Host: example.com" https://1.2.3.4/

This sends `Host: example.com` as an HTTP header, which is enough for most servers to route to the right vhost — but it doesn't fix the SNI problem, so certificate validation still fails. You can either accept that and use `--no-check-certificate` knowing exactly why you're doing it, or you can switch to curl, which is what I'd recommend.

When --no-check-certificate is actually fine

The flag isn't evil. It's just usually used as a workaround for the wrong problem. There are legitimate cases for it: you're testing a self-signed cert on a staging box, you're scripting against an internal service that doesn't have a public CA, you're on the inside of a private network where the trust model is different. In those cases, disabling cert validation is fine, but you should know that's what you're doing and not just be defaulting to it because the error scared you.

When you're testing a public production site that's *supposed* to have a valid certificate, `--no-check-certificate` is hiding information you actually need. Use `--resolve` instead.

Tip

Add `/etc/hosts` overrides if you're going to test the same server-pretending-to-be-a-domain repeatedly. Edit your local `/etc/hosts` and add `1.2.3.4 example.com`. Now every tool on your machine — curl, wget, your browser, ping — will resolve `example.com` to the new IP. Just remember to remove the line when you're done. I have an embarrassing number of stories about leaving hosts entries in place for weeks and then wondering why a site "isn't deploying."

The mental model that helps

The thing to internalise is that on the modern web, an IP address isn't a server. It's a router that points at one of many possible servers based on what hostname you ask for. HTTPS makes this explicit by tying the trust model to hostnames instead of IPs. When you bypass that by connecting directly to an IP, you're not testing what your real users will see — they'll always reach the server through DNS, with a hostname, with SNI, with certificate validation. Test the same way they'll connect, even before the DNS exists. That's what `--resolve` is for.

Topics
SSL/TLSSNIHTTPSwgetcurlcertificate validationLinux server administrationdebugging
Zunaid Amin

Zunaid Amin

Manages Linux infrastructure at Rocket.net. WordPress Core Contributor since 6.3 and Hosting Team Representative for WordPress.org. Based in Dhaka, Bangladesh.

zunaid321@gmail.com