Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Upgrade: ACMEz v2, CertMagic, and ZeroSSL issuer #6229

Merged
merged 9 commits into from
Apr 14, 2024
Merged

Upgrade: ACMEz v2, CertMagic, and ZeroSSL issuer #6229

merged 9 commits into from
Apr 14, 2024

Conversation

mholt
Copy link
Member

@mholt mholt commented Apr 8, 2024

This (currently-WIP) branch is the final phase in upgrading the certificate automation stack:

  • ACMEz v2, which introduces a slightly simpler and improved high-level API, while also exposing the ability to customize ACME orders more from those higher-level APIs. For example, users can now set NotBefore and NotAfter to customize cert lifetime (if CA-supported), and also to use draft-03 ARI by setting the previous cert the new order is replacing.

  • CertMagic, wherein we use the new v2 of ACMEz, but also have introduced a new ZeroSSLIssuer type which is capable of obtaining certs using ZeroSSL's proprietary API. This has some advantages over its ACME endpoint if you need even stronger SLAs / higher availability or are a paying customer. I think it also allows you to get IP certificates.

  • Caddy's implementation of ZeroSSLIssuer is changing. Moving the ZeroSSL issuer into CertMagic has some implications, because before this, the ZeroSSLIssuer is only a thin wrapper over the ACMEIssuer that can generate free EAB automatically and then the underlying ACMEIssuer is used after that.

Now, ZeroSSL is doing away with the default email address that Caddy was using to generate EAB when users didn't provide their own email address. In other words, now in order to use ZeroSSL you will need to do it one of these ways:

  • Provide your own email address in your config. (Caddy has always had the email Caddyfile option for this and we've always recommended using it. That will do.)
  • Provide your own ZeroSSL EAB credentials.
  • Use the ZeroSSL API with an API key.

This transition might be a little tricky because we already allow you to use your ZeroSSL API key, but it's only for generating the EAB (in lieu of an email address), and nothing else. After this change is done, providing your ZeroSSL API key will actually use the ZeroSSL API to obtain certificates, which is paywalled (for good reason).

After this change is done, here's what I suppose Caddy's behavior will be:

  • Let's Encrypt will remain the default CA.
  • If (and only if) an email address is provided in the Caddyfile, ZeroSSL will implicitly become an additional default CA. This will be done by implicitly configuring an ACMEIssuer with the ZeroSSL endpoint, and that endpoint will trigger the generation of EAB (using the configured email address) if there's no EAB yet.
  • The zerossl issuer module is now a thin wrapper over CertMagic's ZeroSSLIssuer type (kind of like how the acme issuer module is a thin wrapper over CertMagic's ACMEIssuer type). It uses the ZeroSSL API exclusively and thus requires an API key.

Breaking changes (out of our control) that will be needed for, I suppose, a very small subset of users:

  • If you're using the ZeroSSL API key for EAB generation (again, that should only be needed the first time anyway) and don't want to use the ZeroSSL API for certificates, change your config to simply have your ZeroSSL account email instead (remove the use of the zerossl issuer module).
  • If you're not providing an email address in your config and are relying on ZeroSSL specifically, add your email address to your config. (You do not need a ZeroSSL account, but it's recommended.)

In summary:

  • Default CA with no extra config is now Let's Encrypt only.
  • Caddyfile with an email address enables ZeroSSL ACME as an additional CA:
{
    email [email protected]
}

example.com

# because an email address is provided, LE and ZeroSSL are both default CAs, for redundancy
  • The zerossl issuer is now API-only:
{
    cert_issuer zerossl <api_key>
}

or, for a particular site using the tls directive:

example.com

tls {
    issuer zerossl <api_key>
}
  • Everything else is basically the same / as expected. (For example, you can still configure ZeroSSL's ACME endpoint manually using EAB, etc.)

Again, I think breaking changes should only affect a certain subset of users. But I'm told the reliability of ZeroSSL's services will improve with these changes.

@mholt mholt added this to the v2.8.0 milestone Apr 8, 2024
@mholt mholt self-assigned this Apr 8, 2024
@devsnek
Copy link

devsnek commented Apr 9, 2024

I tried running this and I'm seeing this error for IP subjects:

tls.obtain	will retry	{"error": "[94.26.24.128] Obtain: subject does not qualify for a public certificate: 94.26.24.128", "attempt": 1, "retrying_in": 60, "elapsed": 0.000330948, "max_duration": 2592000}

does this check need to be bypassed for the zerossl issuer?

@mholt
Copy link
Member Author

mholt commented Apr 9, 2024

I might need to update the logic regarding IP certs. I think it's still true for ACME but I need to double check on CA policies these days. I'm headed to bed right now but will check on this in the morning.

Ps. Thanks for trying it!

@mholt
Copy link
Member Author

mholt commented Apr 9, 2024

@devsnek Ok, so that error only happens if:

  • The ACMEIssuer is being used,
  • and the CA is Let's Encrypt, ZeroSSL, or Google Trust Services (common public CAs) -- but I just found out that GTS actually does issue IP certificates, so I will update the logic here.

Are you getting that while using the ZeroSSL API? ZeroSSL's ACME endpoint, AFAIK, does not support IP certs still. (I don't have ZeroSSL API implemented into Caddy yet)

@devsnek
Copy link

devsnek commented Apr 10, 2024

ah my bad I should have read the diff more carefully, I was using issuer zerossl {token} assuming it was hooked up to the new api in certmagic.

@mholt
Copy link
Member Author

mholt commented Apr 10, 2024

Ah no worries, I am still working on that part. Soon!! I will update this issue when that's ready.

@mholt
Copy link
Member Author

mholt commented Apr 11, 2024

@devsnek Ok it should be ready to test now.

To use ZeroSSL's ACME endpoint as an implicit default, you have to provide an email address in your config.

To use ZeroSSL's API, you need to provide an API key and use the zerossl issuer, which now only supports ZeroSSL's API.

@mholt mholt marked this pull request as ready for review April 11, 2024 18:38
@mholt mholt requested review from mohammed90 and francislavoie and removed request for mohammed90 April 11, 2024 18:42
@francislavoie francislavoie added feature ⚙️ New feature or request dependencies ⛓️ Pull requests that update a dependency file labels Apr 11, 2024
@devsnek
Copy link

devsnek commented Apr 11, 2024

it does try to issue now, but it hits an issue during validation

2024/04/11 23:04:09.158	INFO	tls.obtain	obtaining certificate	{"identifier": "94.26.24.128"}
2024/04/11 23:04:09.159	INFO	tls.issuance.zerossl	creating certificate	{"identifiers": ["94.26.24.128"]}
2024/04/11 23:04:12.952	INFO	tls.issuance.zerossl	created certificate	{"identifiers": ["94.26.24.128"], "cert_id": "8c748ed2a414fa8536c9e65456fd7e5b"}
2024/04/11 23:04:12.952	INFO	tls.issuance.zerossl	validating identifiers	{"identifiers": ["94.26.24.128"], "cert_id": "8c748ed2a414fa8536c9e65456fd7e5b", "verification_method": "HTTP_CSR_HASH"}
2024/04/11 23:04:14.149	INFO	tls.issuance.zerossl	canceled certificate	{"identifiers": ["94.26.24.128"], "cert_id": "8c748ed2a414fa8536c9e65456fd7e5b", "verification_method": "HTTP_CSR_HASH"}
2024/04/11 23:04:14.150	ERROR	tls.obtain	could not get certificate from issuer	{"identifier": "94.26.24.128", "issuer": "zerossl", "error": "verifying identifiers: POST https://api.zerossl.com/certificates/8c748ed2a414fa8536c9e65456fd7e5b/challenges?access_key=redacted: HTTP 200: API error 0: domain_control_validation_failed (details=map[94.26.24.128:map[http://94.26.24.128/.well-known/pki-validation/D1C00E794ACC211811DFED34503DFAAE.txt:{{0 0   } {0 true http_transport_failed Error: HTTP transport exception in file validation. Check if your server and the validation file are reachable and maybe retry afterwards?}}]]) (raw={\"success\":false,\"error\":{\"code\":0,\"type\":\"domain_control_validation_failed\",\"details\":{\"94.26.24.128\":{\"http:\\/\\/94.26.24.128\\/.well-known\\/pki-validation\\/D1C00E794ACC211811DFED34503DFAAE.txt\":{\"file_found\":0,\"error\":true,\"error_slug\":\"http_transport_failed\",\"error_info\":\"Error: HTTP transport exception in file validation. Check if your server and the validation file are reachable and maybe retry afterwards?\"}}}}} decode_error=json: unknown field \"success\")"}

@mholt
Copy link
Member Author

mholt commented Apr 11, 2024

@devsnek Thanks -- are you sure that URL is publicly reachable? http://94.26.24.128/.well-known/pki-validation/D1C00E794ACC211811DFED34503DFAAE.txt

I get a redirect to HTTPS which is probably not going to work 🤔

Hmm, I got it working for me in testing with CertMagic, I wonder why Caddy is redirecting. It doesn't redirect ACME challenges (but this is not an ACME challenge). Maybe that has something to do with it. But then again, I tested the ZeroSSL API on my computer and worked... hmm.

@devsnek
Copy link

devsnek commented Apr 11, 2024

@mholt the only thing running on the server at that ip is caddy, and during that test it had a config like this:

foo.snek.dev,
bar.snek.dev
{
	reverse_proxy http://100.70.171.69
}

94.26.24.128 {
	tls {
		issuer zerossl [redacted]
	}
	respond "hello"
}

its not currently running with that config though, as i normally use the gandi dns plugin and i couldn't make xcaddy compile this branch with it.

@mholt
Copy link
Member Author

mholt commented Apr 12, 2024

I think I know the problem... the HTTP server has to be made aware of ACME challenges, it likely also has to be aware of ZeroSSL API's HTTP validation requests... which are slightly different.

@mholt
Copy link
Member Author

mholt commented Apr 12, 2024

@devsnek Ok, I was able to verify that the HTTP validation from the ZeroSSL API works for me now. Can you give it a shot again? Thank you!! 😊

@devsnek
Copy link

devsnek commented Apr 13, 2024

@mholt its working!

@devsnek
Copy link

devsnek commented Apr 13, 2024

seems ipv6 does not currently work though

2024/04/13 09:48:33.065	ERROR	tls.obtain	could not get certificate from issuer	{"identifier": "2a14:14c0:0:10::", "issuer": "zerossl", "error": "creating certificate: POST https://api.zerossl.com/certificates?access_key=redacted: HTTP 200: API error 2808: invalid_certificate_domain (details=map[]) (raw={\"success\":false,\"error\":{\"code\":2808,\"type\":\"invalid_certificate_domain\"}} decode_error=json: unknown field \"success\")"}
2024/04/13 09:48:33.065	ERROR	tls.obtain	will retry	{"error": "[2a14:14c0:0:10::] Obtain: creating certificate: POST https://api.zerossl.com/certificates?access_key=redacted: HTTP 200: API error 2808: invalid_certificate_domain (details=map[]) (raw={\"success\":false,\"error\":{\"code\":2808,\"type\":\"invalid_certificate_domain\"}} decode_error=json: unknown field \"success\")", "attempt": 1, "retrying_in": 60, "elapsed": 1.387451133, "max_duration": 2592000}

i played around on the zerossl site for a bit and it seems like it only accepts the address in the expanded form: 2a14:14c0:0000:0010:0000:0000:0000:0000

@mholt
Copy link
Member Author

mholt commented Apr 13, 2024

Thanks for trying it out!

That's good to know about the IPv6 addresses. That's probably intentional, to avoid ambiguity or mishaps when validating the certificates (just a single form for the IP).

I will probably merge this shortly then.

@devsnek
Copy link

devsnek commented Apr 13, 2024

I will probably merge this shortly then.

would it be possible for caddy to support the expanded form? currently no matter how you write the ip in the config, it uses the "canonical" (most compact) form rather than the fully expanded form. could it always pass the expanded form to zerossl?

@mholt
Copy link
Member Author

mholt commented Apr 13, 2024

Hm, actually, I don't know. The x509.CertificateRequest struct (in the Go standard library) takes IP addresses as []net.IP and then the standard lib also does the marshaling of the CSR that we send to the CA. So on a second look, I'm not sure we have control over that, and maybe it's actually supposed to be in the condensed form? I dunno.

Related, sort of: golang/go#30264

@devsnek
Copy link

devsnek commented Apr 13, 2024

Yeah tbc I think the correct thing would be for zerossl to accept the canonical format, but idk how realistic that is... might just be too much effort to support ipv6 here for the moment :(

@mholt mholt enabled auto-merge (squash) April 13, 2024 23:30
@mholt
Copy link
Member Author

mholt commented Apr 13, 2024

@francislavoie @mohammed90 I know this is a huge change, I don't expect a thorough review, but if you want to skim it and look for anything you care about, feel free to approve/request changes and we can get this merged :)

@mholt mholt merged commit 81413ca into master Apr 14, 2024
25 checks passed
@mholt mholt deleted the zerossl branch April 14, 2024 01:31
@mholt mholt mentioned this pull request Apr 14, 2024
willnorris added a commit to willnorris/caddy that referenced this pull request May 19, 2024
Certificate automation has permission modules that are designed to
prevent inappropriate issuance of unbounded or wildcard certificates.
When an explicit cert manager is used, no additional permission should
be necessary. For example, this should be a valid caddyfile:

    https:// {
      tls {
        get_certificate tailscale
      }
      respond OK
    }

This is accomplished when provisioning an AutomationPolicy by tracking
whether there were explicit managers configured directly on the policy
(in the ManagersRaw field). Only when a number of potentially unsafe
conditions are present AND no explicit cert managers are configured is
an error returned.

The problem arises from the fact that ctx.LoadModule deletes the raw
bytes after loading in order to save memory. The first time an
AutomationPolicy is provisioned, the ManagersRaw field is populated, and
everything is fine.

An AutomationPolicy with no subjects is treated as a special "catch-all"
policy. App.createAutomationPolicies ensures that this catch-all policy
has an ACME issuer, and then calls its Provision method again because it
may have changed. This second time Provision is called, ManagesRaw is no
longer populated, and the permission check fails because it appears as
though the policy has no explicit managers.

Address this by storing a new boolean on AutomationPolicy recording
whether it had explicit cert managers configured on it.

Also fix an inverted boolean check on this value when setting
failClosed.

Updates caddyserver#6060
Updates caddyserver#6229
Updates caddyserver#6327

Signed-off-by: Will Norris <[email protected]>
mholt pushed a commit that referenced this pull request May 20, 2024
Certificate automation has permission modules that are designed to
prevent inappropriate issuance of unbounded or wildcard certificates.
When an explicit cert manager is used, no additional permission should
be necessary. For example, this should be a valid caddyfile:

    https:// {
      tls {
        get_certificate tailscale
      }
      respond OK
    }

This is accomplished when provisioning an AutomationPolicy by tracking
whether there were explicit managers configured directly on the policy
(in the ManagersRaw field). Only when a number of potentially unsafe
conditions are present AND no explicit cert managers are configured is
an error returned.

The problem arises from the fact that ctx.LoadModule deletes the raw
bytes after loading in order to save memory. The first time an
AutomationPolicy is provisioned, the ManagersRaw field is populated, and
everything is fine.

An AutomationPolicy with no subjects is treated as a special "catch-all"
policy. App.createAutomationPolicies ensures that this catch-all policy
has an ACME issuer, and then calls its Provision method again because it
may have changed. This second time Provision is called, ManagesRaw is no
longer populated, and the permission check fails because it appears as
though the policy has no explicit managers.

Address this by storing a new boolean on AutomationPolicy recording
whether it had explicit cert managers configured on it.

Also fix an inverted boolean check on this value when setting
failClosed.

Updates #6060
Updates #6229
Updates #6327

Signed-off-by: Will Norris <[email protected]>
@andreground
Copy link

in the release notes you say:

If you use JSON to configure certificate automation policies, you will need to ensure you use the acme issuer with your email filled out, and the ca field set to ZeroSSL's ACME server URL.

Thus, I've updated my JSON config to include:

	"tls": {
		"automation": {
			"policies": [{
				"issuers": [{
				"module":"acme",
				"ca": "https://acme.zerossl.com/v2/DV90",
				"email":"[email protected]"
			}]
		}]}
	}

Now, all my certs get generated from Zerossl and that was not the intended behaviour.
I would like to have both ACME providers active, as it should be.

Am I missing something?

Thanks

@mholt
Copy link
Member Author

mholt commented Jun 3, 2024

@andreground Caddy does what you configure it to do, so if you tell it to use ZeroSSL as the issuer it will use ZeroSSL. If you want to use multiple issuers then be sure to add them to your configuration. 👍

@andreground
Copy link

@mholt thanks, I think I was updating the config according to the release notes and this ticket comments.

In the first post of this ticket, you wrote:

here's what I suppose Caddy's behavior will be:

Let's Encrypt will remain the default CA.
If (and only if) an email address is provided in the Caddyfile, ZeroSSL will implicitly become an additional default CA. This will be done by implicitly configuring an ACMEIssuer with the ZeroSSL endpoint, and that endpoint will trigger the generation of EAB (using the configured email address) if there's no EAB yet.

Probably I've misunderstood something or probably the behaviour using Caddyfile is way different from that of the JSON config 👍

This is how I've updated the config to make Caddy behave like you stated, so using Let's Encrypt as first and Zerossl as fallback. Am I doing right?

"tls": {
	"automation": {
		"policies": [
			{
				"issuers": [
					{
						"module": "acme"
					},
					{
						"ca": "https://acme.zerossl.com/v2/DV90",
						"email": "[email protected]",
						"module": "acme"
					}
				]
			}
		]
	}
}

Thanks!

@mholt
Copy link
Member Author

mholt commented Jun 4, 2024

behaviour using Caddyfile is way different from that of the JSON config 👍

Yes, so in the release notes we boil it down:

If you use JSON to configure certificate automation policies, you will need to ensure you use the acme issuer with your email filled out, and the ca field set to ZeroSSL's ACME server URL.

Hopefully that's clear. EDIT: I see why maybe it's not clear when talking about redundancy (multiple ACME CAs). I've edited the wording of the release notes.

So yeah, your updated config looks like what you want. Try ACME with Let's Encrypt first, then ACME with ZeroSSL after. And the ZeroSSL one has an email address. 💯

@simaotwx
Copy link
Contributor

simaotwx commented Sep 4, 2024

This is very confusing to me and I've not quite understood yet how this works. So we were using ZeroSSL before with our free account and our use case is following:

  • We want to use our account to obtain certificates so that we receive email notifications and can look at the certificates in the dashboard (we don't have many domains, but we want to keep track of them, so being able to see 100 certificates is fine)
  • We want to use ZeroSSL only
  • We do not pay for a subscription as we don't need it (ACME is unlimited)

Our configuration has this:

{
	cert_issuer zerossl {$ZEROSSL_API_KEY}
}

Do I understand correctly that I need to provide the email like this:

{
	cert_issuer zerossl {$ZEROSSL_API_KEY}
	email [email protected]
        
}

to make sure that the ACME is used instead of the API?

We are getting errors like such:

HTTP 200: API error 2817: certificate_limit_reached (details=map[]) (raw={\"success\":false,\"error\":{\"code\":2817,\"type\":\"certificate_limit_reached\"}}

Maybe it would be a good idea to add some code to check for "certificate_limit_reached" and use the ACME endpoint in that case?

@mholt
Copy link
Member Author

mholt commented Sep 4, 2024

@simaotwx Specifying the zerossl issuer will use the ZeroSSL API, which has pretty restrictive free limits. Remove the cert_issuer line from your config (and keep the email) to use both Let's Encrypt and ZeroSSL. To use only ZeroSSL's ACME endpoint, specify acme_ca, with ZeroSSL's ACME endpoint according to their documentation: https://zerossl.com/documentation/acme/. If you don't have a ZeroSSL ACME account in your storage already and you don't specify your email address, you'll need to specify your EAB credentials as well:

{
	acme_ca https://acme.zerossl.com/v2/DV90

	# if you don't specify an email:
	acme_eab {
		key_id ...
		mac_key ...
	}

	# but specifying an email instead is recommended
	email [email protected] 
}

@simaotwx
Copy link
Contributor

simaotwx commented Sep 4, 2024

@simaotwx Specifying the zerossl issuer will use the ZeroSSL API, which has pretty restrictive free limits. Remove the cert_issuer line from your config (and keep the email) to use both Let's Encrypt and ZeroSSL. To use only ZeroSSL's ACME endpoint, specify acme_ca, with ZeroSSL's ACME endpoint according to their documentation: https://zerossl.com/documentation/acme/. If you don't have a ZeroSSL ACME account in your storage already and you don't specify your email address, you'll need to specify your EAB credentials as well:

Thank you! Your reply couldn't have been more clear.

@simaotwx
Copy link
Contributor

simaotwx commented Sep 10, 2024

For anyone interested, this is how it looks like now:

	acme_ca https://acme.zerossl.com/v2/DV90
	acme_eab {
		key_id {$ZEROSSL_KID}
		mac_key {$ZEROSSL_HMAC_KEY}
	}

The cert_issuer definitely needs to be removed, otherwise it won't work.

Both variables are passed from outside (Terraform -> Packer -> .env -> docker compose -> docker -> Caddy)

If anyone needs a way to generate the EAB credentials in Terraform to then pass it all the way through to Caddy, you can use our provider for this: https://registry.terraform.io/providers/toowoxx/zerossl/latest/docs/resources/eab_credentials

I made this provider two years ago and realized I didn't actually need it but now it proved to be useful.

The alternative to this is to upgrade the subscription and use the API through Caddy for which EAB credentials are not needed (just the API token).

@jorgegonzalez
Copy link

We were previously using this configuration on 2.7.4; am I reading this thread right that we can no longer use the zerossl module with a valid API key while using JSON configurations?

"tls": {
  "automation": {
    "policies": [
      {
        "disable_ocsp_stapling": true,
        "issuers": [
          {
            "api_key": "ZEROSSL_API_KEY",
            "module": "zerossl"
          }
        ]
      }
    ]
  },
  "disable_ocsp_stapling": true
}

@francislavoie
Copy link
Member

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
dependencies ⛓️ Pull requests that update a dependency file feature ⚙️ New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

"get_certificate tailscale" forces "on_demand_tls" global option
7 participants