Check CAA records before ACME certificate issuance
Before requesting a certificate from the ACME provider, verify that CAA DNS records for the domain permit issuance by the configured CA. This avoids wasting ACME attempts on domains whose CAA policy would reject the request. Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
|
// Copyright (c) 2025-2026 Probo Inc <hello@getprobo.com>.
|
||||||
//
|
//
|
||||||
// Permission to use, copy, modify, and/or distribute this software for any
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
// purpose with or without fee is hereby granted, provided that the above
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
@@ -168,6 +168,7 @@ func (b *Builder) Build() (*probod.FullConfig, error) {
|
|||||||
ProvisionInterval: b.getEnvIntOrDefault("CUSTOM_DOMAINS_PROVISION_INTERVAL", 30),
|
ProvisionInterval: b.getEnvIntOrDefault("CUSTOM_DOMAINS_PROVISION_INTERVAL", 30),
|
||||||
CnameTarget: b.getEnvOrDefault("CUSTOM_DOMAINS_CNAME_TARGET", "custom.getprobo.com"),
|
CnameTarget: b.getEnvOrDefault("CUSTOM_DOMAINS_CNAME_TARGET", "custom.getprobo.com"),
|
||||||
ResolverAddr: b.getEnvOrDefault("CUSTOM_DOMAINS_RESOLVER_ADDR", "8.8.8.8:53"),
|
ResolverAddr: b.getEnvOrDefault("CUSTOM_DOMAINS_RESOLVER_ADDR", "8.8.8.8:53"),
|
||||||
|
CAAIssuerDomain: b.getEnvOrDefault("CUSTOM_DOMAINS_CAA_ISSUER_DOMAIN", "letsencrypt.org"),
|
||||||
ACME: probod.ACMEConfig{
|
ACME: probod.ACMEConfig{
|
||||||
Directory: b.getEnvOrDefault("ACME_DIRECTORY", "https://acme-v02.api.letsencrypt.org/directory"),
|
Directory: b.getEnvOrDefault("ACME_DIRECTORY", "https://acme-v02.api.letsencrypt.org/directory"),
|
||||||
Email: b.getEnvOrDefault("ACME_EMAIL", "admin@getprobo.com"),
|
Email: b.getEnvOrDefault("ACME_EMAIL", "admin@getprobo.com"),
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
|
// Copyright (c) 2025-2026 Probo Inc <hello@getprobo.com>.
|
||||||
//
|
//
|
||||||
// Permission to use, copy, modify, and/or distribute this software for any
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
// purpose with or without fee is hereby granted, provided that the above
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
@@ -36,6 +36,7 @@ type (
|
|||||||
acmeService *ACMEService
|
acmeService *ACMEService
|
||||||
encryptionKey cipher.EncryptionKey
|
encryptionKey cipher.EncryptionKey
|
||||||
cnameTarget string
|
cnameTarget string
|
||||||
|
caaIssuerDomain string
|
||||||
interval time.Duration
|
interval time.Duration
|
||||||
resolverAddr string
|
resolverAddr string
|
||||||
logger *log.Logger
|
logger *log.Logger
|
||||||
@@ -51,6 +52,7 @@ func NewProvisioner(
|
|||||||
acmeService *ACMEService,
|
acmeService *ACMEService,
|
||||||
encryptionKey cipher.EncryptionKey,
|
encryptionKey cipher.EncryptionKey,
|
||||||
cnameTarget string,
|
cnameTarget string,
|
||||||
|
caaIssuerDomain string,
|
||||||
interval time.Duration,
|
interval time.Duration,
|
||||||
resolverAddr string,
|
resolverAddr string,
|
||||||
logger *log.Logger,
|
logger *log.Logger,
|
||||||
@@ -60,6 +62,7 @@ func NewProvisioner(
|
|||||||
acmeService: acmeService,
|
acmeService: acmeService,
|
||||||
encryptionKey: encryptionKey,
|
encryptionKey: encryptionKey,
|
||||||
cnameTarget: cnameTarget,
|
cnameTarget: cnameTarget,
|
||||||
|
caaIssuerDomain: caaIssuerDomain,
|
||||||
interval: interval,
|
interval: interval,
|
||||||
resolverAddr: resolverAddr,
|
resolverAddr: resolverAddr,
|
||||||
logger: logger.Named("certmanager.provisioner"),
|
logger: logger.Named("certmanager.provisioner"),
|
||||||
@@ -135,6 +138,51 @@ func (p *Provisioner) checkDNSConfiguration(domain string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Provisioner) checkCAARecords(domain string) error {
|
||||||
|
fqdn := domain
|
||||||
|
if !strings.HasSuffix(fqdn, ".") {
|
||||||
|
fqdn = fqdn + "."
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := &dns.Msg{MsgHeader: dns.MsgHeader{ID: dns.ID(), RecursionDesired: true}}
|
||||||
|
msg.Question = []dns.RR{&dns.CAA{Hdr: dns.Header{Name: fqdn, Class: dns.ClassINET}}}
|
||||||
|
|
||||||
|
client := dns.NewClient()
|
||||||
|
|
||||||
|
resp, _, err := client.Exchange(
|
||||||
|
context.Background(),
|
||||||
|
msg,
|
||||||
|
"udp",
|
||||||
|
p.resolverAddr,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot exchange dns message for caa records: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var caaRecords []*dns.CAA
|
||||||
|
for _, rr := range resp.Answer {
|
||||||
|
if caa, ok := rr.(*dns.CAA); ok {
|
||||||
|
caaRecords = append(caaRecords, caa)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(caaRecords) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, caa := range caaRecords {
|
||||||
|
if caa.Tag == "issue" && strings.EqualFold(caa.Value, p.caaIssuerDomain) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf(
|
||||||
|
"caa records for domain %q do not permit issuance by %q",
|
||||||
|
domain,
|
||||||
|
p.caaIssuerDomain,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Provisioner) checkPendingDomains(ctx context.Context) error {
|
func (p *Provisioner) checkPendingDomains(ctx context.Context) error {
|
||||||
err := p.pg.WithTx(
|
err := p.pg.WithTx(
|
||||||
ctx,
|
ctx,
|
||||||
@@ -293,6 +341,17 @@ func (p *Provisioner) provisionDomainCertificate(
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := p.checkCAARecords(domain.Domain); err != nil {
|
||||||
|
p.logger.WarnCtx(
|
||||||
|
ctx,
|
||||||
|
"caa record check failed",
|
||||||
|
log.String("domain", domain.Domain),
|
||||||
|
log.Error(err),
|
||||||
|
)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
p.logger.InfoCtx(ctx, "DNS configuration verified, initiating HTTP challenge for domain", log.String("domain", domain.Domain))
|
p.logger.InfoCtx(ctx, "DNS configuration verified, initiating HTTP challenge for domain", log.String("domain", domain.Domain))
|
||||||
|
|
||||||
challenge, err := p.acmeService.GetHTTPChallenge(ctx, domain.Domain)
|
challenge, err := p.acmeService.GetHTTPChallenge(ctx, domain.Domain)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
|
// Copyright (c) 2025-2026 Probo Inc <hello@getprobo.com>.
|
||||||
//
|
//
|
||||||
// Permission to use, copy, modify, and/or distribute this software for any
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
// purpose with or without fee is hereby granted, provided that the above
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
@@ -19,6 +19,7 @@ type CustomDomainsConfig struct {
|
|||||||
ProvisionInterval int `json:"provision-interval"`
|
ProvisionInterval int `json:"provision-interval"`
|
||||||
ResolverAddr string `json:"resolver-addr"`
|
ResolverAddr string `json:"resolver-addr"`
|
||||||
CnameTarget string `json:"cname-target"`
|
CnameTarget string `json:"cname-target"`
|
||||||
|
CAAIssuerDomain string `json:"caa-issuer-domain"`
|
||||||
ACME ACMEConfig `json:"acme"`
|
ACME ACMEConfig `json:"acme"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
|
// Copyright (c) 2025-2026 Probo Inc <hello@getprobo.com>.
|
||||||
//
|
//
|
||||||
// Permission to use, copy, modify, and/or distribute this software for any
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
// purpose with or without fee is hereby granted, provided that the above
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
@@ -830,7 +830,7 @@ func (impl *Implm) runTrustCenterServer(
|
|||||||
if certProvisioningInterval == 0 {
|
if certProvisioningInterval == 0 {
|
||||||
certProvisioningInterval = 30 * time.Second
|
certProvisioningInterval = 30 * time.Second
|
||||||
}
|
}
|
||||||
certProvisioner := certmanager.NewProvisioner(pgClient, acmeService, encryptionKey, impl.cfg.CustomDomains.CnameTarget, certProvisioningInterval, impl.cfg.CustomDomains.ResolverAddr, l)
|
certProvisioner := certmanager.NewProvisioner(pgClient, acmeService, encryptionKey, impl.cfg.CustomDomains.CnameTarget, impl.cfg.CustomDomains.CAAIssuerDomain, certProvisioningInterval, impl.cfg.CustomDomains.ResolverAddr, l)
|
||||||
|
|
||||||
g, ctx := errgroup.WithContext(ctx)
|
g, ctx := errgroup.WithContext(ctx)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user