Skip to content

feat(ra): pluggable server-cert issuance via certificate-order lifecycle#45

Draft
csnitker-godaddy wants to merge 1 commit into
mainfrom
feat/acme-server-cert-issuer
Draft

feat(ra): pluggable server-cert issuance via certificate-order lifecycle#45
csnitker-godaddy wants to merge 1 commit into
mainfrom
feat/acme-server-cert-issuer

Conversation

@csnitker-godaddy

Copy link
Copy Markdown
Collaborator

Summary

Wires an RFC 8555 ACME provider (Let's Encrypt–shaped) alongside the local
self-signed CA behind a single port.ServerCertificateIssuer, so a deployment
can swap in a public or cloud CA (AWS Private CA, GCP CAS, etc.) without
touching the service layer. Issuance now runs through an explicit
certificate-order lifecycle rather than assuming synchronous, local signing.

Order lifecycle

  • CreateOrder (at registration / renewal submission) returns the
    provider's domain-control challenges, relayed verbatim to the domain owner.
    ANS never writes DNS or serves challenge files on the owner's behalf — the
    pending response carries the records/paths for the owner to publish.
  • FinalizeOrder (at verify-acme, gated on a verified challenge) returns
    the certificate, or ErrOrderPending for asynchronous providers such as ACME
    CAs. Re-POSTing verify-acme re-drives the pending order (202 /
    PENDING_CERTS), so the async path is plumbed end-to-end instead of being
    skipped.

Identity certificates remain privately issued behind
port.IdentityCertificateAuthority (self-CA default; swappable for AWS Private
CA / GCP CAS) and are signed only after the server order completes. Both CA
ports gain an idempotent RevokeCertificate.

Correctness / hardening

Closes holes so the flows behave correctly regardless of the underlying adapter:

  • Always gate FinalizeOrder on a verified DNS-01 or HTTP-01 artifact;
    gate/order rejections return 422, pending-conflict 409.
  • SSRF-harden the HTTP-01 verifier: guarded dialer rejecting non-public dial
    targets (loopback, link-local incl. the cloud metadata endpoint, RFC 1918,
    ULA, RFC 6598 CGNAT 100.64.0.0/10, 6to4, NAT64), refuse redirects, bounded
    body read — fails closed.
  • Distinguish "no server cert on file" from a transient store failure, so a
    recoverable fault can never activate an agent and sign a wrong, immutable
    transparency-log leaf.
  • Persist order/challenge state and certificate serial/ref in SQLite
    (migrations 006/007); guarded set-based expiry of lapsed pending
    validations; in-process agent auto-expiry sweeper.

Wire contract

spec/api-spec-v2.yaml and the docsui copy stay byte-identical. PENDING_CERTS
is restored to RegistrationPending.status; CANCEL added to the next-step
action enum.

Testing

  • make check passes (fmt, vet, golangci-lint, coverage gate).
  • internal/domain at 100% statement coverage; overall ≥ 90%; make test-race
    clean.
  • New unit suites cover the order lifecycle, the ACME adapter (against an
    in-process RFC 8555 fake), the SSRF guard, and the SQLite persistence
    round-trips.
  • The ACME provider is wired into scripts/demo/start.sh behind --with-acme
    for live testing against a real domain + Let's Encrypt staging.

Notes

Marked draft for review of the port surface before merge.

Wire an RFC 8555 ACME provider (Let's Encrypt-shaped) alongside the
local self-signed CA behind a single port.ServerCertificateIssuer, so a
deployment can swap in a public/cloud CA without touching the service
layer. Issuance runs through a certificate-order lifecycle:

- CreateOrder (at registration/renewal submission) returns the
  provider's domain-control challenges, relayed verbatim to the domain
  owner. ANS never writes DNS or serves challenge files on their behalf.
- FinalizeOrder (at verify-acme, gated on a verified challenge) returns
  the cert, or ErrOrderPending for asynchronous providers such as ACME
  CAs; re-POSTing verify-acme re-drives the order (202 / PENDING_CERTS).

Identity certificates remain privately issued behind
port.IdentityCertificateAuthority (self-CA default, swappable for AWS
Private CA / GCP CAS), signed only after the server order completes.
Both CA ports gain an idempotent RevokeCertificate.

Close several holes so the flows behave correctly regardless of the
underlying adapter:

- Always gate FinalizeOrder on a verified DNS-01 or HTTP-01 artifact;
  gate/order rejections return 422, pending-conflict 409.
- SSRF-harden the HTTP-01 verifier: guarded dialer rejecting non-public
  dial targets (loopback, link-local incl. the cloud metadata endpoint,
  RFC 1918, ULA, RFC 6598 CGNAT 100.64/10, 6to4, NAT64), refuse
  redirects, bounded body read.
- Distinguish "no server cert on file" from a transient store failure,
  so a recoverable fault can never activate an agent and sign a wrong,
  immutable transparency-log leaf.
- Persist order/challenge state and certificate serial/ref in SQLite
  (migrations 006/007); guarded set-based expiry of lapsed pending
  validations; in-process agent auto-expiry sweeper.

spec/api-spec-v2.yaml and the docsui copy stay byte-identical.
internal/domain remains at 100% statement coverage; overall >=90%;
race-clean.

Signed-off-by: Connor Snitker <csnitker@godaddy.com>
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.

1 participant