Automatic HTTPS with IP address

1. Caddy version (caddy version):

2.5.1

2. How I run Caddy:

docker-compose

a. System environment:

Ubuntu 20.04, Docker 20.10.15, Compose 1.25.0

b. Command:

docker-compose up -d

c. Service/unit/compose file:

version: "2.2"

services:
  caddy:
    container_name: caddy-cluster-proxy
    image: caddy:2.5.1-alpine
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - caddy_data:/data
      - ./:/etc/caddy/
    command: caddy run --config /etc/caddy/Caddyfile --watch

volumes:
  # Cached caddy data, e.g. certs
  caddy_data:

d. My complete Caddyfile or JSON config:

import "*.Caddyfile"

{
	debug
}

https://159.223.140.9 {
	header {
		Cache-Control "no-cache, no-store, must-revalidate"
	}
	encode gzip
	handle_path /107731/* {
		reverse_proxy http://159.223.140.9:8000 {
			flush_interval -1
		}
	}

}

3. The problem I’m having:

I want to enable HTTPS on just an IP address. When I use the above config I get HTTPS errors in my browser, and with curl:

curl https://159.223.140.9 -k -v
*   Trying 159.223.140.9:443...
* Connected to 159.223.140.9 (159.223.140.9) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* TLSv1.0 (OUT), TLS header, Certificate Status (22):
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS header, Unknown (21):
* TLSv1.3 (IN), TLS alert, internal error (592):
* error:0A000438:SSL routines::tlsv1 alert internal error
* Closing connection 0
curl: (35) error:0A000438:SSL routines::tlsv1 alert internal error

4. Error messages and/or full log output:

{"level":"info","ts":1657528686.1966174,"msg":"using provided configuration","config_file":"/etc/caddy/Caddyfile","config_adapter":""}
{"level":"warn","ts":1657528686.19679,"msg":"No files matching import glob pattern","pattern":"*.Caddyfile"}
{"level":"warn","ts":1657528686.1981165,"msg":"Caddyfile input is not formatted; run the 'caddy fmt' command to fix inconsistencies","adapter":"caddyfile","file":"/etc/caddy/Caddyfile","line":1}
{"level":"info","ts":1657528686.1991653,"logger":"admin","msg":"admin endpoint started","address":"tcp/localhost:2019","enforce_origin":false,"origins":["//localhost:2019","//[::1]:2019","//127.0.0.1:2019"]}
{"level":"info","ts":1657528686.1993258,"logger":"http","msg":"server is listening only on the HTTPS port but has no TLS connection policies; adding one to enable TLS","server_name":"srv0","https_port":443}
{"level":"info","ts":1657528686.199342,"logger":"http","msg":"enabling automatic HTTP->HTTPS redirects","server_name":"srv0"}
{"level":"info","ts":1657528686.1994512,"logger":"tls.cache.maintenance","msg":"started background certificate maintenance","cache":"0xc000217ab0"}
{"level":"info","ts":1657528686.200295,"logger":"tls","msg":"cleaning storage unit","description":"FileStorage:/data/caddy"}
{"level":"info","ts":1657528686.2009776,"logger":"tls","msg":"finished cleaning storage units"}
{"level":"info","ts":1657528686.2203028,"logger":"pki.ca.local","msg":"root certificate is already trusted by system","path":"storage:pki/authorities/local/root.crt"}
{"level":"debug","ts":1657528686.2206156,"logger":"http","msg":"starting server loop","address":"[::]:443","http3":false,"tls":true}
{"level":"debug","ts":1657528686.2206867,"logger":"http","msg":"starting server loop","address":"[::]:80","http3":false,"tls":false}
{"level":"info","ts":1657528686.2207088,"logger":"http","msg":"enabling automatic TLS certificate management","domains":["159.223.140.9"]}
{"level":"warn","ts":1657528686.2211394,"logger":"tls","msg":"stapling OCSP","error":"no OCSP stapling for [159.223.140.9]: no OCSP server specified in certificate","identifiers":["159.223.140.9"]}
{"level":"debug","ts":1657528686.2211623,"logger":"tls.cache","msg":"added certificate to cache","subjects":["159.223.140.9"],"expiration":1657569657,"managed":true,"issuer_key":"local","hash":"9d440d4d2bbab98eeaf1e2a3ed669bbec7c347598f7f6a30bbffbc73b5ebc191","cache_size":1,"cache_capacity":10000}
{"level":"info","ts":1657528686.221428,"msg":"autosaved config (load with --resume flag)","file":"/config/caddy/autosave.json"}
{"level":"info","ts":1657528686.2214453,"msg":"serving initial configuration"}
{"level":"info","ts":1657528686.2215388,"logger":"watcher","msg":"watching config file for changes","config_file":"/etc/caddy/Caddyfile"}

5. What I already tried:

I believe HTTPS is technically possible with IP addresses: https - Is it possible to have SSL certificate for IP address, not domain name? - Stack Overflow

And I think Caddy uses ZeroSSL, which supports certs for IP addresses: https://help.zerossl.com/hc/en-us/articles/360060119973-Is-It-Possible-To-Generate-a-SSL-Certificate-for-an-IP-Address-

But I’ve only seen cert errors when I use this setup.

(Why am I doing this? We spin up/down servers every day on a schedule with terraform. Getting DNS to work with this system would be a pain. Currently we have a few small permanent servers that I run Caddy on it with a regular domain name that reverse proxies to our Terraform servers, but I’m trying to remove this server-for-the-sake-of-a-domain-name).

6. Links to relevant resources:

Hi :wave:

Caddy is able to use certs for IP addresses.
It just won’t be publically trusted, but instead self-signed when using the auto-https feature currently.
The docs/automatic-https#hostname-requirements (pretty far down though) state:

[…]
In addition, hostnames qualify for publicly-trusted certificates if they:

  • are not an IP address

I am pretty sure I read an issue or some mention to track it not long ago, but I can’t seem to find it right now :frowning:
Maybe someone else has some insights on that.

A quick workaround, at least for now, if you want publicly-trusted certificates, would be to use services like https://nip.io/ that resolve 159.223.140.9.nip.io as 159.223.140.9 without any additional setup.
That way you would have a “valid” domain name :woman_shrugging:

Though, you really shouldn’t be getting SSL routines::tlsv1 alert internal error when using the plain IP.
Both https://159.223.140.9 { and 159.223.140.9 { are valid and will serve that vhost via some self-signed certificate.

Are you absolutely sure that the Caddyfile and logs you shared are from the on the server publically accessible under 159.223.140.9 and not some other server?

3 Likes

Context:

ZeroSSL supposedly supports it but nobody has tried it out yet to confirm whether they actually do. And Certmagic will probably need some updates/fixes to allow it if it does work.

But yeah, for now, you need a domain name. Or use Caddy’s internal CA.

4 Likes

As others have said, Caddy will present a certificate for an IP address, but it won’t be trusted by default. It attempts to install trust into the local root store, but that requires a password and only works for some clients on the local machine. (That’s not a limitation of Caddy.) For a publicly-trusted IP certificate, you’ll need a CA that supports issuing that (preferably via ACME, since Caddy supports that already).

1 Like

Thank you so much! nip.io provides exactly what I need. And it’s easy and free. Appreciate it!

1 Like

This topic was automatically closed after 30 days. New replies are no longer allowed.

A bit late to the party, but I think you just need to add:

network_mode: host

To your docker compose file. If you don’t do that, caddy won’t be able to see the 159.223.140.9 interface because it will be in its own network namespace with its own ip address.

Alternatively, you could not use docker and just run caddy directly on the host, and then it should be able to see the 159.223.140.9 interface

3 Likes

Just stumbled upon this problem as well and wanted to share my solution.
For me the problem seemed to be rooted somewhere in the SNI.
When I set:
default_sni <IP>
in the global block everything suddenly worked perfectly, even though caddy runs in docker-compose with simple port-forwards.

I believe caddy should see the IP, as it should be contained in the Host header, but gets confused which certificate to send because TLS negotiation happens before any header is sent, which causes problems if a client does not send a SNI.

1 Like

No, the Host header is part of the encrypted payload. Caddy can’t choose a certificate with anything inside the HTTP payload. The only thing available is TLS SNI (no client puts an IP in SNI, it’s against spec to do so) and the remote address on the TCP connection. If for some reason the remote address is different (some TCP-layer proxy or IP tables rules causing it to be changed) then it wouldn’t match the issued certificates in storage.

When you use default_sni, it overrides the “look at the remote address” behaviour and instead uses the value you give it in that config option.

3 Likes

The part about the Host header makes sense.
But overwriting the remote address might sometimes be exactly what is needed in docker-scenarios :slight_smile:

This topic was automatically closed after 11 days. New replies are no longer allowed.