Skip to content

Enforce we use the right issuer chain#60

Merged
mcpherrinm merged 4 commits into
mainfrom
strict-chain-selection
May 9, 2026
Merged

Enforce we use the right issuer chain#60
mcpherrinm merged 4 commits into
mainfrom
strict-chain-selection

Conversation

@mcpherrinm
Copy link
Copy Markdown
Contributor

@mcpherrinm mcpherrinm commented May 9, 2026

Lego silently falls back to the CA's default chain if no alternate matches PreferredChain.

That's not great, and we'd rather be down than serve the wrong chain (eg, if a chain were to become unavailable)

To make the integration test work, we need to generate the config with the right CN, since pebble randomizes it each run. While I'm in here, avoid hardcoding the Pebble root CA in case it ever changes.

Code by Claude

Fixes #20

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Enforces strict certificate-chain issuer pinning after ACME issuance to prevent lego’s silent fallback to a default chain, and updates local integration/dev workflow to handle Pebble’s randomized root CN by generating configuration at startup.

Changes:

  • Add post-Obtain verification that the topmost cert’s issuer matches configured IssuerCN (and IssuerO when set), failing issuance otherwise.
  • Introduce an integration/configgen sidecar and docker-compose wiring to generate config from Pebble’s management API at startup.
  • Extend config Duration with MarshalJSON and add a config round-trip test to support config generation.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
README.md Updates local testing instructions to account for config generation sidecar.
integration/test-certs-site-config.json Removes previously checked-in integration config example.
integration/configgen/main.go New helper that fetches Pebble root CN and writes a pinned test-certs-site config.
Dockerfile Builds both test-certs-site and configgen as separate targets/stages.
docker-compose.yml Adds configgen service and uses a shared volume for generated config.
config/json_duration.go Adds JSON marshaling for Duration to enable config generation.
config/config.go Adds optional IssuerO pinning field in Site config.
config/config_test.go Adds round-trip marshal/load test for config objects.
acme/issuer.go Verifies returned chain issuer matches config (CN and optional O) before storing/serving.
acme/issuer_test.go Adds unit tests for issuer-chain verification behavior.
acme/acme.go Wires IssuerO from config into issuer instances.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread Dockerfile Outdated
Comment thread README.md Outdated
Lego silently falls back to the CA's default chain if no alternate matches
PreferredChain. For this site, that is unacceptable: serving the wrong chain
defeats the entire point. After Obtain, verify the topmost certificate's
Issuer matches the configured IssuerCN (and IssuerO, when set) and fail
issuance if it does not.

Pebble randomizes its root CN on each startup, so a configgen sidecar runs
alongside Pebble in docker-compose, fetches the root from Pebble's
management API, and writes the test-certs-site config to a shared volume.
The Dockerfile gains a multi-target build that produces both binaries, and
configgen builds the config using the real config.Config types (enabled by
adding MarshalJSON to Duration).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@mcpherrinm mcpherrinm force-pushed the strict-chain-selection branch from 209dbba to ec6ed57 Compare May 9, 2026 01:34
Pebble's docker image already ships pebble.minica.pem at /test/certs/.
Mount that directory as a named volume so test-certs-site can read it via
LEGO_CA_CERTIFICATES without keeping a vendored copy of the fixture in
the repo. configgen and the integration test now use InsecureSkipVerify
when fetching the root CN from pebble's management API: the value they
want is what test-certs-site then pins to, so verifying pebble's
transport cert adds nothing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@mcpherrinm mcpherrinm changed the title Enforce strict issuer chain selection Enforce we use the right issuer chain May 9, 2026
Copy link
Copy Markdown
Contributor

@aarongable aarongable left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, code does what it says on the tin.

That said, I'm a little confused about the introduction of the check against the Org field. If the point is to be able to distinguish between all the possible issuers that we use, then the Org field isn't going to help -- even for our Staging certs, we put the (STAGING) tag in the CN, not the O. If the point is to be able to distinguish between any possible certs, then why stop at the O field; why not compare the C, or the SKID?

Comparing just the CN is (imo) good enough for our purposes. Comparing the full Subject bytes and the SKID would be the Most Correct ™️ thing to do. It's unclear to me why doing something in the middle strikes a better balance.

Comment thread acme/issuer.go
@mcpherrinm
Copy link
Copy Markdown
Contributor Author

Ah, I forgot to mention the O!

I want to align with what boulder-observer does here, where it checks both:
https://github.com/letsencrypt/boulder/tree/main/cmd/boulder-observer#schema-6

And since we have the code ourselves, we can do that.

@aarongable
Copy link
Copy Markdown
Contributor

Fair enough! I guess my question then goes to boulder-observer: why do we choose to draw the line at checking the O= field there? But since there's prior art, I'm happy to let this one stand.

@mcpherrinm
Copy link
Copy Markdown
Contributor Author

Happy to re-open that discussion here and in boulder-observer, but will go with that for now

@mcpherrinm mcpherrinm merged commit 74a2ab3 into main May 9, 2026
2 checks passed
@mcpherrinm mcpherrinm deleted the strict-chain-selection branch May 9, 2026 02:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Check chains ourselves

3 participants