How to setup a custom Caddy PKI and ACME Server with an existing CA

(first draft)

(this works with a Caddy release after 2.4.0-beta.1)

If you have an existing CA infrastructure with your own root certificate and want Caddy to use it within the pki app and the acme_server handler because this certificate is already distributed and trusted
in your organization, this is how to set it up.

First, you have to create a certificate to be used as the root certificate in Caddy.
How to do this depends on your CA, but one requirement is that the CA which signs your certificate has at least a value of 2 in the pathLenConstraint field of the v3 Extension Basic Constraints.
(see RFC 5280 - Internet X.509 Public Key Infrastructure Certificate and Certificate Revocation List (CRL) Profile and Certificates Basic Constraint's Path Length - Stack Overflow)

In my organization we have a CA structure based on Windows Server, with an offline Root CA and an intermediate CA, both having unrestricted pathLenConstraint values.

The Caddy root certificate needs to have at least a value of 1 in the pathLenConstraint, otherwise you cannot create & sign the Caddy intermediate certificate.

Next I created a Caddy intermediate certificate. With only a root certificate I got an error at the start of Caddy.

As the relevant settings are only configurable via JSON, you have to create this JSON either by hand or by adapting from a Caddyfile.

The pki settings are under the apps key “pki”, then “certificate_authorities”.
See JSON Config Structure - Caddy Documentation for the detailed documentation.
I defined a name, the root CA with key and certificate file, the intermediate CA with key and certificate file.

"pki": {
			"certificate_authorities": {
				"caname": {
					"name": "Name of the CA",
					"root": {
						"certificate": "d:\\caddy\\root.crt",
						"private_key": "d:\\caddy\\root.key",
						"format": "pem_file"
					}					,
					"intermediate": {
						"certificate": "d:\\caddy\\intermediate.crt",
						"private_key": "d:\\caddy\\intermediate.key",
						"format": "pem_file"
					}
				}
			}
        }

To use this new internal pki issuer for the acme_server endpoint, I configured a tls automation policy
(see JSON Config Structure - Caddy Documentation) with the subject of the acme_server host, the issuers with the new internal ca.
I had to set the option “sign_with_root” to true to get the certificate for the acme_server endpoint recognized as valid. I also needed to set the key_type of the automation policy to “rsa4096” (as all other certificates had RSA keys and I got errors mixing RSA and ECDSA).

		"tls": {
			"automation": {
				"policies": [                {
					"subjects": [
					    "localhost"
					],
					"issuers": [ {
						"module": "internal",
						"ca": "caname",
						"sign_with_root": true
					}					],
					"key_type": "rsa4096"
				} ]
            }
        }

For the acme_server handler I configured the new caddy pki as the “ca”, but had again to set the “sign_with_root” option to true (this was added after the release of Caddy 2.4.0-beta.1). As this example only uses a single Caddy instance as acme_server and acme_client, I let the “host” option be the default value “localhost”.
See JSON Config Structure - Caddy Documentation

						"match": [ {
							"host": [
							"localhost"
							]
						}						],
						"handle": [ {
							"handler": "subroute",
							"routes": [ {
								"handle": [ {
									"handler": "acme_server",
									"ca": "caname",
                                    "host": "localhost",
									"sign_with_root": true
								}	]
							}	]
						}	],

As an example acme_client I used a hostname for the computer running Caddy. For this I configured a tls automation policy with ca URL of the acme_server with the new pki, and put the relevant ca certificates into the “trusted_roots_pem_files” setting. Again had to set the key_type setting to “rsa406”:

{
					"subjects": [
					    "host.domain.local"
					],
					"issuers": [ {
						"ca": "https://localhost/acme/caname/directory",
						"email": "www@example.com",
						"module": "acme",
						"trusted_roots_pem_files": [
						    "d:\\caddy\\root.crt",
						    "d:\\caddy\\windows-ca.cer",
						    "d:\\caddy\\windows-root-ca.cer"
						]
					}					],
					"key_type": "rsa4096"
				}

After running with this configuration I got “secure connections” with both localhost and the acme enabled local domain, both in Firefox (worked without “sign_with_root” for acme enabled domain) and Edge (needed “sign_with_root” for both).

The complete configuration:

{
	"apps": {
		"http": {
			"servers": {
				"srv0": {
					"listen": [
					":443"
					],
					"routes": [ {
						"match": [ {
							"host": [
							"host.domain.local"
							]
						}						],
						"handle": [ {
							"handler": "subroute",
							"routes": [ {
								"handle": [ {
									"body": "acme client static response",
									"handler": "static_response"
								}								]
							}							]
						}						],
						"terminal": true
					}					, {
						"match": [ {
							"host": [
							"localhost"
							]
						}						],
						"handle": [ {
							"handler": "subroute",
							"routes": [ {
								"handle": [ {
									"handler": "acme_server",
									"ca": "caname",
									"host": "localhost",
									"sign_with_root": true
								}								]
							}							]
						}						],
						"terminal": true
					}					]
				}
			}
		}		,
		"tls": {
			"automation": {
				"policies": [ {
					"subjects": [
					"localhost"
					],
					"issuers": [ {
						"module": "internal",
						"ca": "caname",
						"sign_with_root": true
					}					],
					"key_type": "rsa4096"
				}				, {
					"subjects": [
					"host.domain.local"
					],
					"issuers": [ {
						"ca": "https://localhost/acme/caname/directory",
						"email": "www@example.com",
						"module": "acme",
						"trusted_roots_pem_files": [
						"d:\\caddy\\root.crt",
						"d:\\caddy\\windows-ca.cer",
						"d:\\caddy\\windows-root-ca.cer"
						]
					}					],
					"key_type": "rsa4096"
				}				]
			}
		}		,
		"pki": {
			"certificate_authorities": {
				"caname": {
					"name": "Name of the CA",
					"root": {
						"certificate": "d:\\caddy\\root.crt",
						"private_key": "d:\\caddy\\root.key",
						"format": "pem_file"
					}					,
					"intermediate": {
						"certificate": "d:\\caddy\\intermediate.crt",
						"private_key": "d:\\caddy\\intermediate.key",
						"format": "pem_file"
					}
				}
			}
		}
	}
}

1 Like

It’s now possible as of v2.5.0 to configure the pki app via Caddyfile global options:

1 Like

Quick update on this tutorial. I have mostly the same requirements and the steps described did not work for me when enabling “sign_with_root”

My solution was simply to put one CA signed by my deployed CA as an intermediate in caddy PKI. So when caddy PKI generates certificates the chain would be “My Deployed CA” → “Caddy Intermediate CA” → “Server CA”

Interesting, why’s that? What was the problem?

A post was split to a new topic: Caddy’s root certificate overridden when intermediate renewed