Enabling wildcard and on_demand certificates

1. Caddy version (caddy version):

v2.0.0 h1:pQSaIJGFluFvu8KDGDODV8u4/QRED/OPyIR+MWYYse8=

2. How I run Caddy:

as a service

a. System environment:

Linux Amazon 2

b. Command:

/usr/bin/caddy run --environ --config /etc/caddy/config.json

c. Service/unit/compose file:

paste full file contents here

d. My complete Caddyfile or JSON config:

{
	"admin": {
		"disabled": false,
		"listen": "0.0.0.0:2020",
		"config": {
			"persist": true
		}
	},
	"logging": {
		"sink": {
			"writer": {
                "output": "file",
                "filename": "/tmp/caddy-Go.log"
            }
		},
		"logs": {
			"default": {
				"writer": {
                    "output": "file",
                    "filename": "/tmp/caddy-tmp.log"
                },
				"encoder": {
                    "format": "json"
                },
				"level": "debug"
			}
		}
	},
	"apps": {
        "tls": {
            "automation": {
                "policies": [{
                    "issuer": {
                        "module": "acme"
                    },
                    "on_demand": true
                }],
                "on_demand": {
                    "rate_limit": {
                        "interval": "5m",
                        "burst": 100
                    },
                    "ask": "http://certs.fakedomain.com/delegated.php"
                }
            }

        },
        "http": {
            "http_port": 80,
            "https_port": 443,
            "servers": {
                "tiendas": {
                    "listen": [":80",":443"],
                    "max_header_bytes": 41943040,
                    "automatic_https": {
                        "skip": ["*.fakedomain.com"]
                    },
                    "routes": [
                    {
                        "group": "grupo1",
                        "match": [{
                            "host": ["guiacanaveral.com"]
                        }],
                        "handle": [{
                            "handler": "reverse_proxy",
                            "upstreams": [{
                                "dial": "ip-172-31-4-60.us-west-2.compute.internal:80"
                            }]
                        }],
                        "terminal": true
                    },
                    {
                        "@id": "certs",
                        "group": "grupo1",
                        "match": [{
                            "host": ["certs.fakedomain.com"]
                        }],
                        "handle": [{
                            "handler": "reverse_proxy",
                            "transport": {
                                "protocol": "fastcgi",
                                "root": "/"
                            },
                            "upstreams": [{
                                "dial": "unix//var/php.sock"
                            }]
                        }],
                        "terminal": true
                    },
                    {
                        "@id": "subdominios",
                        "group": "grupo1",
                        "match": [{
                            "host": ["*.fakedomain.com"]
                        }],
                        "handle": [{
                            "handler": "reverse_proxy",
                            "upstreams": [{
                                "dial": "ip-172-31-4-60.us-west-2.compute.internal:80"
                            }]
                        }],
                        "terminal": true
                    },
                    {
                        "@id": "dominios",
                        "group": "grupo1",
                        "match": [{
                            "host": ["*"]
                        }],
                        "handle": [{
                            "handler": "reverse_proxy",
                            "upstreams": [{
                                "dial": "ip-172-31-4-60.us-west-2.compute.internal:80"
                            }]
                        }],
                        "terminal": true
                    }]
                }
            }
        }
    }
}

3. The problem I’m having:

I need to use a fixed wildcard cert for subdomains *.fakedomain.com for this specific route and use tls on_demand for all the domains pointing to my server (configured as a route that matches ["*"] everything.

Or I need to transparent proxy all the traffico that matches my subdomains to forward traffic “as is” to my webservers and let my webservers without ssl provisioning for my subdomains.

"ask": "http://certs.fakedomain.com/delegated.php" this endpoint sholud check if the domain has a valid cert? Or it sholud only check that the domain/subdomain is pointing to my caddy server?

4. Error messages and/or full log output:

I understood from the docs that if a configure a site with a wildcard subdomain I will get a wildcard certificate, but Im getting a certificate for each subdomain like :

2020/05/20 22:10:58 [INFO] [www.serinterior.fakedomain.com] acme: Obtaining bundled SAN certificate given a CSR
2020/05/20 22:11:00 [INFO] [www.serinterior.fakedomain.com] acme: Obtaining bundled SAN certificate given a CSR
2020/05/20 22:12:02 [INFO] [www.serinterior.fakedomain.com] acme: Obtaining bundled SAN certificate given a CSR
2020/05/20 22:12:03 [INFO] [www.serinterior.fakedomain.com] AuthURL: https://acme-staging-v02.api.letsencrypt.org/acme/authz-v3/58120303
2020/05/20 22:12:03 [INFO] [www.serinterior.fakedomain.com] acme: authorization already valid; skipping challenge
2020/05/20 22:12:03 [INFO] [www.serinterior.fakedomain.com] acme: Validations succeeded; requesting certificates
2020/05/20 22:12:03 [INFO] [www.serinterior.fakedomain.com] Server responded with a certificate.
2020/05/20 22:12:03 [INFO] [www.serinterior.fakedomain.com] acme: Obtaining bundled SAN certificate given a CSR
2020/05/20 22:12:06 [INFO] [www.serinterior.fakedomain.com] acme: Obtaining bundled SAN certificate given a CSR
2020/05/20 22:12:30 [INFO] [coralcosta.fakedomain.com] acme: Obtaining bundled SAN certificate given a CSR
2020/05/20 22:12:33 [INFO] [coralcosta.fakedomain.com] acme: Obtaining bundled SAN certificate given a CSR
2020/05/20 22:13:35 [INFO] [coralcosta.fakedomain.com] acme: Obtaining bundled SAN certificate given a CSR
2020/05/20 22:13:35 [INFO] [coralcosta.fakedomain.com] AuthURL: https://acme-staging-v02.api.letsencrypt.org/acme/authz-v3/58120860
2020/05/20 22:13:35 [INFO] [coralcosta.fakedomain.com] acme: authorization already valid; skipping challenge
2020/05/20 22:13:35 [INFO] [coralcosta.fakedomain.com] acme: Validations succeeded; requesting certificates
2020/05/20 22:13:35 [INFO] [coralcosta.fakedomain.com] Server responded with a certificate.
2020/05/20 22:13:35 [INFO] [coralcosta.fakedomain.com] acme: Obtaining bundled SAN certificate given a CSR
2020/05/20 22:13:37 [INFO] [coralcosta.fakedomain.com] acme: Obtaining bundled SAN certificate given a CSR
2020/05/20 22:14:36 [INFO] [balancenatural.fakedomain.com] acme: Obtaining bundled SAN certificate given a CSR
2020/05/20 22:14:38 [INFO] [balancenatural.fakedomain.com] acme: Obtaining bundled SAN certificate given a CSR
2020/05/20 22:15:33 [INFO] [www.serinterior.fakedomain.com] acme: Obtaining bundled SAN certificate given a CSR
2020/05/20 22:15:35 [INFO] [www.serinterior.fakedomain.com] acme: Obtaining bundled SAN certificate given a CSR
2020/05/20 22:15:35 [INFO] [pruebaluisk.fakedomain.com] acme: Obtaining bundled SAN certificate given a CSR
2020/05/20 22:15:37 [INFO] [pruebaluisk.fakedomain.com] acme: Obtaining bundled SAN certificate given a CSR
2020/05/20 22:15:41 [INFO] [balancenatural.fakedomain.com] acme: Obtaining bundled SAN certificate given a CSR
2020/05/20 22:15:41 [INFO] [balancenatural.fakedomain.com] AuthURL: https://acme-staging-v02.api.letsencrypt.org/acme/authz-v3/58120667
2020/05/20 22:15:41 [INFO] [balancenatural.fakedomain.com] acme: authorization already valid; skipping challenge
2020/05/20 22:15:41 [INFO] [balancenatural.fakedomain.com] acme: Validations succeeded; requesting certificates
2020/05/20 22:15:41 [INFO] [balancenatural.fakedomain.com] Server responded with a certificate.
2020/05/20 22:15:41 [INFO] [balancenatural.fakedomain.com] acme: Obtaining bundled SAN certificate given a CSR
2020/05/20 22:15:42 [INFO] [tustrabajoscd.fakedomain.com] acme: Obtaining bundled SAN certificate given a CSR
2020/05/20 22:15:43 [INFO] [balancenatural.fakedomain.com] acme: Obtaining bundled SAN certificate given a CSR
2020/05/20 22:15:44 [INFO] [tustrabajoscd.fakedomain.com] acme: Obtaining bundled SAN certificate given a CSR

5. What I already tried:

tried to skip ssl issuing with :

"automatic_https": {
                        "skip": ["*.fakedomain.com"]
                    },

but doesnt seems to work for me.

6. Links to relevant resources:

1 Like

:hot_face:

1 Like

I’m not sure I’ll be able to fully answer your question, so I’ll let @matt chime in when he has a chance, but I can hopefully explain a couple things regardless.

The ask option is typically used as “hey backend, does x.example.com belong to an existing customer?”

It’s essentially a way to avoid abuse, because if you didn’t configure ask, someone could make HTTPS requests to your server for domains you don’t care about and make your server issue certificates for them, and they could have your server issue an unlimited amount of certificates, maybe eventually exhausting your server’s disk space (after millions of certificates).

Obviously that’s no good, so you can limit which ones are allowed with an ask callback to prevent unwanted domains from being issued a certificate.

Your suggestion of “should check if the domain has a valid cert” wouldn’t work, because it’s a chicken and egg problem; you wouldn’t have a valid cert yet if Caddy is asking! Also your other suggestion “should only check that the subdomain is pointing to my Caddy server” wouldn’t make sense either, because Caddy would only be asking if it got an HTTPS request for that domain; the domain already has the right DNS if it reached Caddy!

I don’t know enough about the app you’re setting up, so I’m not sure what to suggest your approach should be, but essentially you would probably store a list of all the domains you want to allow to have certificates in your database, and if a request comes in with a query of ?domain=x.example.com, you could check if your DB contains x.example.com in the list. If so, return status 200. If not, return any 400 (or any 4xx/5xx code, whatever makes sense to you).

That said, I think on_demand isn’t what you’re actually looking to use, since you seem to want a wildcard certificate. I think (@matt correct me if I’m wrong) you just need to remove on_demand from your config entirely and it might fetch a wildcard certificate instead.

But to get wildcard certs, you MUST use the DNS challenge which it doesn’t seem you’re doing right now. See this post for an explanation of how to set that up:

The DNS records requirement is something set by Let’s Encrypt. It must be used because using the HTTP or ALPN challenges (i.e. make a request over HTTP, or make a request over HTTPS with some metadata) isn’t enough to know whether the program requesting a wildcard cert belongs to someone who has access to the entire domain and not just individual subdomains. Let’s Encrypt needs to verify that you control the domain and not that you just have a server pointed to by one of the DNS entries.

Here’s where wildcard cert support is described in the docs:

2 Likes

Francis, thanks for taking the time to answer.
Im working for a SaaS startup.
Clients from this startup should access the service by:

  • A simple URL like: https://tienda.fakedomain.com/username
  • A subdomain like: https://username.fakedomain.com
  • Their own domain pointing to our server

This is what Im trying to do with my Caddy config.

So I need to have just 1 wildcard certificate for all the subdomains and 1 certificate for each domain pointing to my server.

I’m using the ask directive the right way then ! Thanks

@Sebastian_Perez Thanks for filling out all the info, that’s very helpful. :slight_smile: I know it’s tiring, but it makes the whole process a lot easier, and it balances our efforts with yours.

  • Wildcard certificates should be used for lots of subdomains.
  • On-Demand should be used for lots of registered domains that you don’t control.
  • You can do both, but if you add your customer’s domains to your config with “host” matchers in the top-level route, then Caddy will manage them in the background rather than on-demand and you don’t need on-demand.
  • You shouldn’t add your customer’s domains to your config file if you do not know that their DNS is set up properly yet.

Are you adding a route to your config for every customer’s site? If so, you don’t need on-demand at all, but you do need to make sure their DNS is configured properly before asking Caddy to handle it. With on-demand that’s not really an issue because usually once clients start resolving the domain name to your server, it means their DNS is finally set up and the ACME CA will also resolve their domain to your server.

1 Like

Well I did it ! :metal:
I have added 2 automation policies:

  1. for the wildcard certificate (to catch all subdomains)
  2. for the domains of my clients (to catch all domains)

I configured the domain wildcard properly and builded the server with the cloudflare plugin:

$ xcaddy build --with github.com/caddy-dns/cloudflare

This is the config file that worked !

{
	"admin": {
		"disabled": false,
		"listen": "0.0.0.0:2020",
		"config": {
			"persist": true
		}
	},
	"logging": {
		"sink": {
			"writer": {
			"output": "file",
			"filename": "/tmp/caddy-Go.log"
		}
		},
		"logs": {
			"default": {
				"writer": {
					"output": "file",
					"filename": "/tmp/caddy-tmp.log"
				},
				"encoder": {
					"format": "json"
				},
				"level": "debug"
			}
		}
	},
	"apps": {
		"tls": {
			"automation": {
				"policies": [{
					"subjects": ["*.fakedomain.com"],
					"issuer": {
						"module": "acme",
						"email": "soporte@fakedomain.com",
						"challenges": {
							"dns": {
								"provider": {
									"name": "cloudflare",
									"api_token": "MY_CLOUDFLARE_TOKEN"
								}
							}
						}
					},
					"on_demand": false
				},
				{
					"issuer": {
						"module": "acme",
						"email": "soporte@fakedomain.com"
					},
					"on_demand": true
				}],
				"on_demand": {
					"rate_limit": {
						"interval": "5m",
						"burst": 100
					},
					"ask": "http://certs.fakedomain.com/delegated.php"
				}
			}
		},
		"http": {
			"http_port": 80,
			"https_port": 443,
			"servers": {
				"tiendas": {
					"listen": [":80",":443"],
					"max_header_bytes": 41943040,
					"routes": [
					{
						"group": "grupo1",
						"match": [{
							"host": ["certs.fakedomain.com"]
						}],
						"handle": [{
							"handler": "reverse_proxy",
							"transport": {
								"protocol": "fastcgi",
								"root": "/var/www/html/"
							},
							"upstreams": [{
								"dial": "unix//var/php.sock"
							}]
						}],
						"terminal": true
					},
					{
						"group": "grupo1",
						"match": [{
							"host": ["*.fakedomain.com"]
						}],
						"handle": [{
							"handler": "reverse_proxy",
							"upstreams": [{
								"dial": "ip-172-31-4-60.us-west-2.compute.internal:80"
							}]
						}],
						"terminal": true
					},
					{
						"@id": "dominios",
						"group": "grupo1",
						"handle": [{
							"handler": "reverse_proxy",
							"upstreams": [{
								"dial": "ip-172-31-4-60.us-west-2.compute.internal:80"
							}]
						}],
						"terminal": true
					}]
				}
			}
		}
	}
}
3 Likes

Great! So, problem solved?

Yeap solved !
Thanks !!
I will contribute improving the docs :muscle:

2 Likes

Excellent, you figured that out quick! Thanks for posting your final solution and for being involved.

2 Likes