Skip to content

fix(acme,config): account key --force, DNS-01 nameserver verification, stricter server config validation - #82

Closed
sosyz wants to merge 4 commits into
mainfrom
fix/acme-config
Closed

sosyz wants to merge 4 commits into
mainfrom
fix/acme-config

Conversation

@sosyz

@sosyz sosyz commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Summary

The ACME and server config fixes from #75, ported onto current main:

  • Account key protection (pkg/acme/user.go, certdx_tools google-account): RegisterAccount no longer writes a new key over an existing private/<email>_<provider>.key. It refuses unless force is set, and a forced run that fails restores the previous key. google-account gets -f/--force.
  • DNS-01 propagation (pkg/acme/challenge.go): with disableCompletePropagationRequirement = true, lego skipped the authoritative check and did not verify the TXT record anywhere. When nameservers are configured, the record is now required on those resolvers (dns01.RecursiveNSsPropagationRequirement). Without them the server logs a warning. This is merged with main's conservativeDnsCheck (e27bcc6): the conservative check still overrides the disable flag and replaces lego's pre-check. The dnsTimeout propagation-wait override also works as before. Option building moved into dns01Options, which has tests.
  • S3 HTTP-01 provider: new optional acl. If unset, public-read is still sent. acl = "" sends no ACL header, which buckets with ACLs disabled require. Any other canned ACL is sent as-is. Present/CleanUp now use a 30s per-request timeout instead of context.Background().
  • Server config validation (pkg/config/server.go):
    • certLifeTime/renewTimeLeft must be positive. renewTimeLeft may still be longer than certLifeTime, e.g. rotate after 30d with 60d left.
    • For Google, certLifeTime + renewTimeLeft must be at most 90 days. For other providers this only logs a warning.
    • nameservers entries must be host[:port].
    • authMethod now defaults to token and must be token or mtls.
    • An empty token needs allowAnonymous = true.
    • Token auth over plain HTTP logs a warning; allowInsecureToken = true turns it off.
    • S3 needs bucket, url and credentials, and acl must be a known value.
  • Samples/docs:
    • config/server_config*.toml and docs/server.md are updated. docs/server.md has a new "Upgrading" section.
    • docs/tools.md documents --force.
    • New pkg/config/samples_test.go decodes every shipped sample in config/ (server and client) and fails on unknown keys. server_config.toml and client_config.toml must also pass Validate.

Review feedback from #75 addressed

  • maxCacheEntries: "Is it really necessary? I'd suggest don't add this." It is not in this PR: no field, sample key or docs.
  • # type = "tencent" in server_config_full.toml: "Keep using old value "tencent" for simplicity." The line stays as it is. However, the tencentcloud provider only matched "tencentcloud", so uncommenting that sample line gave unknown DnsProvider: tencent. To keep the sample usable, "tencent" is now accepted as an alias of tencentcloud in both validation and challenger selection (config.DnsProviderTypeTencent). Tests cover both. The docs name tencentcloud as the main value and mention the alias.
  • "Doc the breaking changes": they are listed below and in the new "Upgrading" section of docs/server.md. No versioned breaking-changes file was added, since the next version number is not decided yet.

Breaking changes

Configs that loaded before and are now rejected at startup:

  1. Empty token. An enabled [HttpServer] using token auth with token = "" used to serve certificates to everyone. It is now an error. Set a token, or set allowAnonymous = true if anonymous access is intended.
  2. Unknown authMethod. Values other than token/mtls are now rejected when the config loads. Before, HttpSrv failed with unsupported HTTP auth method when it started. An unset authMethod used to fail the same way; it now defaults to token (so rule 1 applies).
  3. Durations. certLifeTime or renewTimeLeft that is zero or negative is rejected. There is no ordering rule between them.
  4. Google lifetime cap. With provider = "google"/"googletest", certLifeTime + renewTimeLeft over 90 days is rejected. The CA would have refused the order anyway. Let's Encrypt only logs a warning.
  5. Nameservers. A [DnsProvider].nameservers entry that is not host or host:port is rejected: empty entries, URLs/schemes, paths, whitespace, a bad port, or an invalid hostname.
  6. S3 fields. [HttpProvider.S3] without bucket, url, accessKeyId or accessKeySecret is rejected. An unknown acl value is rejected too.
  7. Account key overwrite. certdx_tools google-account refuses to run when the account key already exists; pass --force to replace it. For Go callers, acme.RegisterAccount takes a new force bool parameter.

No behaviour change for S3 users who leave acl out: public-read is still sent.

Test results

  • gofmt -l pkg exec test: clean
  • go vet ./pkg/...: clean
  • go test -race ./pkg/...: all pass
  • exec/tools, exec/server: go build ./... && go vet ./... && go test ./... pass
  • exec/client, exec/caddytls: go build ./... pass
  • test/e2e: go vet -tags e2e ./... and go test -tags e2e ./... pass (346s)

Split from #75.

🤖 Generated with Claude Code

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-29T08:24:39.856670Z c0dbdc1 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c0dbdc1f2d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread pkg/config/server.go Outdated
Comment on lines +111 to +113
if c.ACME.RenewTimeLeftDuration > c.ACME.CertLifeTimeDuration {
return fmt.Errorf("RenewTimeLeft (%q) must not be longer than CertLifeTime (%q)",
c.ACME.RenewTimeLeft, c.ACME.CertLifeTime)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Allow renewal windows longer than the active lifetime

This rejects previously valid configurations such as certLifeTime = "720h" and renewTimeLeft = "1440h", causing the server to fail at startup after an upgrade. The certificate is not due before issuance in this case: renew records ValidBefore as now + 720h and requests a Google NotAfter of now + 2160h, while the renewal loop checks every 360h. Thus the certificate rotates after 30 days with 60 days remaining, exactly as configured and within the 90-day provider limit. Remove this ordering restriction and retain the total provider-lifetime check.

Useful? React with 👍 / 👎.

sosyz and others added 4 commits September 29, 2026 17:51
RegisterAccount used to write a fresh key over an existing
private/<email>_<provider>.key, losing the only proof of ownership of the
old account. It now refuses unless force is set, and when forced it puts
the previous key back if the registration fails half way through.

certdx_tools google-account gains -f/--force to opt in.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Reject configs that could never work, or silently did something else,
when they are loaded instead of deep inside an ACME order or request:

- certLifeTime / renewTimeLeft must be positive and renewTimeLeft must
  not be longer than certLifeTime; for the Google providers, which put
  the requested lifetime on the wire, their sum must fit in 90 days
  (other providers only warn).
- DnsProvider nameservers must be host[:port].
- HttpServer authMethod defaults to "token" and must be token or mtls.
  An enabled token server with an empty token used to serve everyone;
  that now needs an explicit allowAnonymous = true. Token auth over plain
  HTTP logs a warning unless allowInsecureToken = true.
- HttpProvider S3 requires bucket, url and credentials.

The S3 challenge provider gains an optional acl: unset keeps sending
"public-read" as before, acl = "" sends no ACL header (needed for buckets
with ACLs disabled). Each S3 request now times out after 30s instead of
hanging the challenge.

"tencent", the spelling used in the sample config, is accepted as an
alias of the "tencentcloud" DNS provider type. A test checks the shipped
sample configs keep decoding without unknown keys.

The stricter rules are listed under "Upgrading" in docs/server.md.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
lego only asks the recursive nameservers for zone / CNAME discovery and
verifies the TXT value on the authoritative servers. With
disableCompletePropagationRequirement set, that check was skipped and the
record was not verified anywhere before the CA was asked to validate.

When the authoritative check is disabled (and conservativeDnsCheck, which
overrides it, is off) and nameservers are configured, require the record
on those resolvers instead; without nameservers, log a warning. The
option building moves into dns01Options, keeping the dnsTimeout
propagation-wait override and the conservative checker as they were.

Also route the "tencent" alias to the Tencent Cloud provider.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The cert is requested for certLifeTime + renewTimeLeft and rotated once
certLifeTime has passed, so a renew window longer than certLifeTime
(e.g. rotate after 30d with 60d left) is a valid setup. Drop the
ordering check; the positive-duration and provider total-lifetime checks
stay.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Comment thread docs/server.md
- `[HttpServer] authMethod = "token" with an empty token ...` — set `token`,
or `allowAnonymous = true` to serve without authentication.

## Upgrading

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The next release will be v0.8.0, so please write them into a new breaking-changes-v0.8.0.md

@sosyz
sosyz marked this pull request as draft October 5, 2026 03:59
@LDLDL LDLDL closed this Oct 10, 2026
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.

2 participants