V2: Comprehensive Guide to using Self-Signed Certs?

1. My Caddy version (caddy -version):

v2.0.0-beta12

2. How I run Caddy:

Installed as service according to tutorial with config file served as v2 json from /etc/caddy. Generated custom certificate using

sudo -u caddy openssl req -newkey rsa:2048 -nodes -keyout /var/lib/caddy/.local/share/caddy/key.pem -x509 -days 365 -out /var/lib/caddy/.local/share/caddy/certificate.pem

I tried providing public ip and “” as FQDN.

a. System environment:

Ubuntu 19.10 eoan (Budgie)

b. Command:

N/A (Service)

c. Service/unit/compose file:

[Unit]
Description=Caddy Web Server
Documentation=https://caddyserver.com/docs/
After=network.target

[Service]
User=caddy
Group=caddy
ExecStart=/usr/bin/caddy run --config /etc/caddy/caddy.json --resume --environ
ExecReload=/usr/bin/caddy reload --config /etc/caddy/caddy.json
TimeoutStopSec=5s
LimitNOFILE=1048576
LimitNPROC=512
PrivateTmp=true
ProtectSystem=full
AmbientCapabilities=CAP_NET_BIND_SERVICE

[Install]
WantedBy=multi-user.target

d. My complete Caddyfile:

{
	"apps": {
			"http": {
					"servers": {
							"mediabox": {
									"listen": [":443"],
									"routes": [
											{
													"handle": [{
															"handler": "file_server",
															"root": "/var/www"
													}]
											}
									],
									"tls_connection_policies": [
											{
													"match": {
															"sni": [""]
													},
													"certificate_selection": {
															"policy": "custom",
															"tag": "selfsigned"
													}
											}
									]
							}
					}
			},
			"tls": {
					"certificates": {
							"load_files": [
									{
											"certificate": "/var/lib/caddy/.local/share/caddy/certificate.pem",
											"key": "/var/lib/caddy/.local/share/caddy/key.pem",
											"format": "pem",
											"tags": ["selfsigned"]
									}
							]
					}
			}
	}
}

3. The problem I’m having:

I’d like to run a caddy server so that the connection to it is encrypted. It temporary for testing and I’d like to use a self-signed cert instead of ACME. It should be accesible via it’s local ip as well as the public ip with forwarded port 443. I don’t know how to configure caddy such that it will use the loaded cert for the SNI ‘’, according to error

http: TLS handshake error from xxx:57774: no certificate available for ''

4. Error messages and/or full log output:

http: TLS handshake error from xxxx:57774: no certificate available for ''

5. What I already tried:

Various permutations of the config file. The pasted one is the most “complete”.

Welcome @Xaser, thanks for using Caddy 2 while it’s still in beta!

That config looks good to me, let me see if I can get a chance to debug this more this weekend.

FWIW, we have a better solution in the works. @sarge has started a PR for this, I believe, where Caddy will be able to manage local/internal certs just as easily and automatically as it does public ones already. But it’ll be a while before that is ready.

1 Like

Just an update: I plan to revisit this topic in about 1-2 weeks.

Great, thanks! Looking forward to it!

I believe this is happening because CertMagic will only match certificates for a ClientHello without SNI if the server’s IP address matches one of the subjects on the cert (and you can’t typically get publicly-trusted certs for IP addresses). There’s a long history behind why this is (and I mostly disagree with how it ended up tbh) but the thing that it boils down to is that CertMagic needs a “default” hostname to use if the ClientHello’s ServerName is empty. Caddy does not set this value currently.

Although tags are used for selecting a certificate, selection policies are only applied after matching certificates by the ClientHello’s ServerName. This is because a cert with a mismatched subject name is invalid and should be rejected by clients. So, using tags won’t help you here.

We need to figure out a way to expose configuration for DefaultServerName in Caddy – it’s not hard, but I want to make sure we get it right.

And then, you will need to decide which name you want to use if the ServerName is empty; i.e. which certificate (by hostname) you want to serve up in the case of an empty server name. A “default” certificate as it were. I.e. if a ClientHello comes in with an empty ServerName, CertMagic would be configured to load certificates for example.com.

Would that work for you?

Hi matt, thanks for digging deeper on this! The explanation makes sense to me, but whether the proposed solution is the right way to go for the caddy project, I could not possibly judge. Anything that would allow me to use some self-signed certs (either with the changes in @sarge PR or other), would make me happy!

best,
Max

Thanks Max.

To clarify, you can use self-signed certs already. The question you’re asking is orthogonal to (has nothing to do with) self-signed certificates.

The real issue here is that the client is not setting the ServerName field of the ClientHello. Caddy doesn’t know which certificate to use because the client is not requesting one.

Ideally, you should fix the client so it requests a particular certificate. What is your client?

FWIW, I generated a cert and key real quick with this command (don’t use this for anything outside of dev/testing):

openssl req -newkey rsa:2048 -nodes -keyout key.pem -x509 -days 365 -out cert.pem -config <(
cat <<-EOF
[ req ]
default_bits        = 2048
default_keyfile     = server-key.pem
distinguished_name  = subject
req_extensions      = extensions
x509_extensions     = extensions
string_mask         = utf8only
prompt              = no

[ subject ]
countryName = US


[ extensions ]

subjectKeyIdentifier        = hash
authorityKeyIdentifier  = keyid,issuer

basicConstraints        = CA:FALSE
keyUsage            = nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage    = serverAuth
subjectAltName          = @alternate_names
nsComment           = "OpenSSL Generated Certificate"

[ alternate_names ]

DNS.1       = 127.0.0.1
DNS.2       = ::1
)

Then this Caddyfile worked for me (using the latest on the v2 branch, so newer than beta 13):

:2443 {
	tls /Users/matt/Desktop/cert.pem /Users/matt/Desktop/key.pem
	respond "Hello, there!"
}

I used curl:

$ curl -v --insecure https://127.0.0.1:2443
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 2443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*   CAfile: /etc/ssl/cert.pem
  CApath: none
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-AES256-GCM-SHA384
* ALPN, server accepted to use h2
* Server certificate:
*  subject: C=US
*  start date: Feb  6 22:28:00 2020 GMT
*  expire date: Feb  5 22:28:00 2021 GMT
*  issuer: C=US
*  SSL certificate verify result: unable to get local issuer certificate (20), continuing anyway.
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Using Stream ID: 1 (easy handle 0x7fbaf5005400)
> GET / HTTP/2
> Host: 127.0.0.1:2443
> User-Agent: curl/7.64.1
> Accept: */*
> 
* Connection state changed (MAX_CONCURRENT_STREAMS == 250)!
< HTTP/2 200 
< server: Caddy
< content-length: 13
< date: Thu, 06 Feb 2020 22:31:16 GMT
< 
* Connection #0 to host 127.0.0.1 left intact
Hello, there!* Closing connection 0

All works as expected.

You can take my Caddyfile and use caddy adapt to see what the resulting JSON is, and compare it with what you’ve got. (Might be worth upgrading to the latest on the v2 branch, although I don’t think I’ve made any significant changes related to this question recently.)

So, I can’t reproduce the problem you’re having.

Hi matt,

now I’m a bit confused. But it’s been a long day at the office and maybe I’ll get it tomorrow. Let me see if I can get this straight.

So if I want to equip my server which is only reachable by IP address with a self-signed cert, what will happen is that the client, firefox, will not set the SNI field as I’m accessing the server by IP and not by hostname. CertMagic will match a cert if the server’s ip is in the cert’s subjects, but only if a default hostname is set by caddy in case the SNI field is empty. Right now caddy doesn’t set that, but then how does your configuration work?

1 Like

That’s correct! :slight_smile:

Almost! Sorry I made this confusing. CertMagic will match a cert if the server’s IP is in the cert, even if a default hostname is not specified.

If you follow the steps that I took, does it work for you? After trying curl, try with Firefox too.

If my steps work, then I would suggest taking the working JSON (or Caddyfile, whatever you prefer) and further tweaking it from there.