TLS certificates: Jarl obtains and renews them over ACME

Jarl is now an ACME client (lego's core, no DNS providers). It answers
DNS-01 challenges in a zone it serves, or through Porkbun's DNS API while
Porkbun serves the domain; "auto" decides from the domain's public name
servers at every renewal, and a CNAME on _acme-challenge is followed.
Certificates are used for jarld's HTTPS (served without a restart; each
port speaks HTTPS and plain HTTP, plain HTTP from loopback and trusted
proxies still served) and/or written for named's DNS over TLS/HTTPS, with
rndc reconfig on renewal. Renewal follows the CA's ARI window or two
thirds of the lifetime, with back-off. Certificates you renew yourself
can be used too. Certificates page and dashboard card, jarl cert,
/api/v1/certificates, Prometheus gauges, docs/certificates.md, and an
e2e section against Pebble.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adamandClaude Opus 5.5 committed 2026-10-12 14:35:53 +01:00
1 parent 88965096bb
commit 88f63ffb0a
43 files changed
+4584 -45

No files matched your search

+34 -1
View File
@@ -1,7 +1,8 @@
// Package porkbun is a small client for the parts of Porkbun's API v3
// (https://porkbun.com/api/json/v3/documentation) that Jarl uses to manage
// a domain's delegation: its name servers, glue records (host objects at
// the registry) and DNSSEC DS records.
// the registry) and DNSSEC DS records, and the TXT records of DNS-01
// challenges while Porkbun still serves a domain (docs/certificates.md).
//
// Every call is a POST with the API key and secret in the JSON body. API
// access must be switched on for each domain in Porkbun's web interface,
@@ -79,6 +80,7 @@ type response struct {
Valid bool `json:"credentialsValid"`
Account string `json:"account"`
Scopes []string `json:"scopes"`
ID json.Number `json:"id"`
}
func (c *Client) call(ctx context.Context, path string, body map[string]any) (*response, error) {
@@ -282,6 +284,37 @@ func (c *Client) DeleteDS(ctx context.Context, domain string, keyTag uint16) err
return err
}
// MinTTL is the lowest TTL Porkbun accepts for a DNS record.
const MinTTL = 600
// CreateTXT adds a TXT record at name (a name inside domain, or the
// domain itself) in Porkbun's DNS and returns its ID.
func (c *Client) CreateTXT(ctx context.Context, domain, name, value string) (string, error) {
sub := ""
if normName(name) != normName(domain) {
var err error
if sub, err = subdomain(domain, name); err != nil {
return "", err
}
}
r, err := c.call(ctx, domainPath("/dns/create", domain), map[string]any{
"name": sub, "type": "TXT", "content": value, "ttl": strconv.Itoa(MinTTL),
})
if err != nil {
return "", err
}
if r.ID == "" {
return "", &Error{Message: "the record was created but Porkbun didn't say its ID"}
}
return r.ID.String(), nil
}
// DeleteRecord deletes the DNS record with id.
func (c *Client) DeleteRecord(ctx context.Context, domain, id string) error {
_, err := c.call(ctx, domainPath("/dns/delete", domain, id), nil)
return err
}
// subdomain returns host's label(s) below domain ("ns1" for
// ns1.example.com in example.com).
func subdomain(domain, host string) (string, error) {