Consuming Caddy v2 local PKI in another Go Program

First of all, thank you for Caddy - it’s really easy to use and I love the auto-TLS on the local host!

I looked into how I could embed Caddy’s auto-TLS capabilities in other projects, for example GitHub - ory/hydra: OpenID Certified™ OpenID Connect and OAuth Provider written in Go - cloud native, security-first, open source API security for your infrastructure. SDKs for any language. Works with Hardware Security Modules. Compatible with MITREid.

However, from issuance of the CA, to registration, to issuance of TLS leafs, it looks quite complex with lots of internals in Caddy. I wanted to ask if there is a way to either embed caddy (similar to http.Server) in another Go program or if it is possible to use the TLS PKI in another Go program.

It looks like some of the necessary functions (such as installing the TLS certificate) are not exported which leads me to believe that this is not really possible at the moment.

I think you’re probably looking for certmagic:

Thank you! I looked into certmagic but was not able to figure out how to run certmagic for a localhost CA, similar to how caddy’s “internal” TLS issuer works. I looked through caddy’s code without getting a good grasp of what I need to do to get that working

Caddy’s internal issuer uses Caddy’s PKI app, which wraps Smallstep’s PKI/CA libraries. All CertMagic knows how to do is call Issue() to get a certificate from an issuer. I’m afraid a port of this logic does not exist outside of Caddy yet – it is implemented as a Caddy module. It may be possible to splice it out, similar to how we’ve done with the ACMEIssuer, but it’d require some refactoring effort.

I see, thank you for the reply! For now I have resorted to creating a self-signed certificate which I registered with smallstep/truststore in the OS - this got the certificate loading properly. What I like about the way Caddy is solving this is that we get a root CA which is then used for several certificates, while my approach only ever allows one - meaning that you have to go through the password prompts every time (on macOS up to 3 prompts).

What I find intriguing is all the work that went into choosing the correct TLS parameters in caddy (e.g. serial numbers, extensions, … and so on. I think a lot of that knowledge is only accessible to few - having this wrapped in a general purpose library would help the ecosystem a lot, but I digress :slight_smile:

I also tried embedding caddy into e.g. but was not able to properly set it up. It gets especially tricky once you want to run tests, because (as far as I understand) Caddy runs as a singleton.

Going back to TLS - If anyone is looking for some guidance, here is what I came up with - even if it is not truly related to Caddy:

certificate := &x509.Certificate{ /* ... */ }

der, err := x509.CreateCertificate(rand.Reader, certificate, certificate, PublicKey(key), key)
if err != nil {
	return cert, errors.Errorf("failed to create certificate: %s", err)

cert, err = x509.ParseCertificate(der)
if err != nil {
	return cert, errors.Errorf("failed to encode private key: %s", err)

block, err := tlsx.PEMBlockForKey(key)
if err != nil {
	return nil, nil, err

pemCert := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: c.Raw})
pemKey := pem.EncodeToMemory(block)
cert, err := tls.X509KeyPair(pemCert, pemKey)
if err != nil {
	return err

if err := truststore.Install(c,
	truststore.WithJava()); err != nil {
	return err
1 Like

The defaults spin up an admin API at localhost:2019 which is usually the conflict – you can turn this off or change the port with config if you need to during tests, with config.

Thank you for the tip! I will try that out!

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