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:
1 parent
88965096bb
commit
88f63ffb0a
43 files changed
+4584
-45
No files matched your search
@@ -0,0 +1,127 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"jarl/internal/model"
|
||||
)
|
||||
|
||||
const certCols = `id, name, source, domains, cert_file, key_file, uses, challenge, key_type, comment, cert_pem, key_pem, names,
|
||||
not_before, not_after, issuer, serial, renew_at, ari_checked_at, last_attempt, last_success, last_error, failures, last_routes,
|
||||
created_at, updated_at, created_by, updated_by`
|
||||
|
||||
func scanCert(sc interface{ Scan(...any) error }) (*model.Certificate, error) {
|
||||
c := &model.Certificate{}
|
||||
var domains, uses, names, routes, ca, ua string
|
||||
var nb, na, ra, ac, la, ls sql.NullString
|
||||
if err := sc.Scan(&c.ID, &c.Name, &c.Source, &domains, &c.CertFile, &c.KeyFile, &uses, &c.Challenge, &c.KeyType, &c.Comment,
|
||||
&c.CertPEM, &c.KeyPEM, &names, &nb, &na, &c.Issuer, &c.Serial, &ra, &ac, &la, &ls, &c.LastError, &c.Failures, &routes,
|
||||
&ca, &ua, &c.CreatedBy, &c.UpdatedBy); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, f := range []struct {
|
||||
raw string
|
||||
dst *[]string
|
||||
}{{domains, &c.Domains}, {uses, &c.Uses}, {names, &c.Names}, {routes, &c.LastRoutes}} {
|
||||
if err := json.Unmarshal([]byte(f.raw), f.dst); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if *f.dst == nil {
|
||||
*f.dst = []string{}
|
||||
}
|
||||
}
|
||||
c.NotBefore, c.NotAfter, c.RenewAt, c.ARICheckedAt = parseTSPtr(nb), parseTSPtr(na), parseTSPtr(ra), parseTSPtr(ac)
|
||||
c.LastAttempt, c.LastSuccess = parseTSPtr(la), parseTSPtr(ls)
|
||||
c.CreatedAt, c.UpdatedAt = parseTS(ca), parseTS(ua)
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Certificates returns every certificate, by name.
|
||||
func (s *Store) Certificates(ctx context.Context) ([]*model.Certificate, error) {
|
||||
rows, err := s.db.QueryContext(ctx, `SELECT `+certCols+` FROM certificates ORDER BY name`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := []*model.Certificate{}
|
||||
for rows.Next() {
|
||||
c, err := scanCert(rows)
|
||||
if err != nil {
|
||||
rows.Close()
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, c)
|
||||
}
|
||||
return out, closeRows(rows)
|
||||
}
|
||||
|
||||
// CertificateByName returns the certificate called name.
|
||||
func (s *Store) CertificateByName(ctx context.Context, name string) (*model.Certificate, error) {
|
||||
c, err := scanCert(s.db.QueryRowContext(ctx, `SELECT `+certCols+` FROM certificates WHERE name = ?`, name))
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
return c, err
|
||||
}
|
||||
|
||||
// CreateCertificate adds a certificate (ErrConflict when the name is taken).
|
||||
func (s *Store) CreateCertificate(ctx context.Context, c *model.Certificate) error {
|
||||
now := time.Now().UTC()
|
||||
c.CreatedAt, c.UpdatedAt = now, now
|
||||
res, err := s.db.ExecContext(ctx, `INSERT INTO certificates (`+certCols[len("id, "):]+`) VALUES (`+placeholders(27)+`)`, certArgs(c)...)
|
||||
if isUnique(err) {
|
||||
return ErrConflict
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.ID, err = res.LastInsertId()
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateCertificate saves every field of c.
|
||||
func (s *Store) UpdateCertificate(ctx context.Context, c *model.Certificate) error {
|
||||
c.UpdatedAt = time.Now().UTC()
|
||||
res, err := s.db.ExecContext(ctx, `UPDATE certificates SET name=?, source=?, domains=?, cert_file=?, key_file=?, uses=?, challenge=?, key_type=?,
|
||||
comment=?, cert_pem=?, key_pem=?, names=?, not_before=?, not_after=?, issuer=?, serial=?, renew_at=?, ari_checked_at=?, last_attempt=?,
|
||||
last_success=?, last_error=?, failures=?, last_routes=?, created_at=?, updated_at=?, created_by=?, updated_by=? WHERE id=?`,
|
||||
append(certArgs(c), c.ID)...)
|
||||
if isUnique(err) {
|
||||
return ErrConflict
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n, _ := res.RowsAffected(); n == 0 {
|
||||
return ErrNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteCertificate deletes a certificate.
|
||||
func (s *Store) DeleteCertificate(ctx context.Context, id int64) error {
|
||||
res, err := s.db.ExecContext(ctx, `DELETE FROM certificates WHERE id = ?`, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n, _ := res.RowsAffected(); n == 0 {
|
||||
return ErrNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func certArgs(c *model.Certificate) []any {
|
||||
list := func(l []string) string {
|
||||
if l == nil {
|
||||
l = []string{}
|
||||
}
|
||||
return jsonText(l)
|
||||
}
|
||||
return []any{c.Name, c.Source, list(c.Domains), c.CertFile, c.KeyFile, list(c.Uses), c.Challenge, c.KeyType, c.Comment,
|
||||
c.CertPEM, c.KeyPEM, list(c.Names), tsPtr(c.NotBefore), tsPtr(c.NotAfter), c.Issuer, c.Serial, tsPtr(c.RenewAt),
|
||||
tsPtr(c.ARICheckedAt), tsPtr(c.LastAttempt), tsPtr(c.LastSuccess), c.LastError, c.Failures, list(c.LastRoutes),
|
||||
ts(c.CreatedAt), ts(c.UpdatedAt), c.CreatedBy, c.UpdatedBy}
|
||||
}
|
||||
Reference in new issue
Block a user