mirror of
https://github.com/mjl-/mox.git
synced 2024-12-27 08:53:48 +03:00
when suggesting CAA records for a domain, suggest variants that bind to the account id and with validation methods used by mox
should prevent potential mitm attacks. especially when done close to the machine itself (where a http/tls challenge is intercepted to get a valid certificate), as seen on the internet last month.
This commit is contained in:
parent
ca97293cb2
commit
db3fef4981
11 changed files with 123 additions and 28 deletions
|
@ -116,10 +116,11 @@ type Dynamic struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type ACME struct {
|
type ACME struct {
|
||||||
DirectoryURL string `sconf-doc:"For letsencrypt, use https://acme-v02.api.letsencrypt.org/directory."`
|
DirectoryURL string `sconf-doc:"For letsencrypt, use https://acme-v02.api.letsencrypt.org/directory."`
|
||||||
RenewBefore time.Duration `sconf:"optional" sconf-doc:"How long before expiration to renew the certificate. Default is 30 days."`
|
RenewBefore time.Duration `sconf:"optional" sconf-doc:"How long before expiration to renew the certificate. Default is 30 days."`
|
||||||
ContactEmail string `sconf-doc:"Email address to register at ACME provider. The provider can email you when certificates are about to expire. If you configure an address for which email is delivered by this server, keep in mind that TLS misconfigurations could result in such notification emails not arriving."`
|
ContactEmail string `sconf-doc:"Email address to register at ACME provider. The provider can email you when certificates are about to expire. If you configure an address for which email is delivered by this server, keep in mind that TLS misconfigurations could result in such notification emails not arriving."`
|
||||||
Port int `sconf:"optional" sconf-doc:"TLS port for ACME validation, 443 by default. You should only override this if you cannot listen on port 443 directly. ACME will make requests to port 443, so you'll have to add an external mechanism to get the connection here, e.g. by configuring port forwarding."`
|
Port int `sconf:"optional" sconf-doc:"TLS port for ACME validation, 443 by default. You should only override this if you cannot listen on port 443 directly. ACME will make requests to port 443, so you'll have to add an external mechanism to get the connection here, e.g. by configuring port forwarding."`
|
||||||
|
IssuerDomainName string `sconf:"optional" sconf-doc:"If set, used for suggested CAA DNS records, for restricting TLS certificate issuance to a Certificate Authority. If empty and DirectyURL is for Let's Encrypt, this value is set automatically to letsencrypt.org."`
|
||||||
|
|
||||||
Manager *autotls.Manager `sconf:"-" json:"-"`
|
Manager *autotls.Manager `sconf:"-" json:"-"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -96,6 +96,11 @@ describe-static" and "mox config describe-domains":
|
||||||
# configuring port forwarding. (optional)
|
# configuring port forwarding. (optional)
|
||||||
Port: 0
|
Port: 0
|
||||||
|
|
||||||
|
# If set, used for suggested CAA DNS records, for restricting TLS certificate
|
||||||
|
# issuance to a Certificate Authority. If empty and DirectyURL is for Let's
|
||||||
|
# Encrypt, this value is set automatically to letsencrypt.org. (optional)
|
||||||
|
IssuerDomainName:
|
||||||
|
|
||||||
# File containing hash of admin password, for authentication in the web admin
|
# File containing hash of admin password, for authentication in the web admin
|
||||||
# pages (if enabled). (optional)
|
# pages (if enabled). (optional)
|
||||||
AdminPasswordFile:
|
AdminPasswordFile:
|
||||||
|
|
16
main.go
16
main.go
|
@ -792,7 +792,21 @@ configured.
|
||||||
xcheckf(err, "looking up record for dnssec-status")
|
xcheckf(err, "looking up record for dnssec-status")
|
||||||
}
|
}
|
||||||
|
|
||||||
records, err := mox.DomainRecords(domConf, d, result.Authentic)
|
var certIssuerDomainName, acmeAccountURI string
|
||||||
|
public := mox.Conf.Static.Listeners["public"]
|
||||||
|
if public.TLS != nil && public.TLS.ACME != "" {
|
||||||
|
acme, ok := mox.Conf.Static.ACME[public.TLS.ACME]
|
||||||
|
if ok && acme.Manager.Manager.Client != nil {
|
||||||
|
certIssuerDomainName = acme.IssuerDomainName
|
||||||
|
acc, err := acme.Manager.Manager.Client.GetReg(context.Background(), "")
|
||||||
|
c.log.Check(err, "get public acme account")
|
||||||
|
if err == nil {
|
||||||
|
acmeAccountURI = acc.URI
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
records, err := mox.DomainRecords(domConf, d, result.Authentic, certIssuerDomainName, acmeAccountURI)
|
||||||
xcheckf(err, "records")
|
xcheckf(err, "records")
|
||||||
fmt.Print(strings.Join(records, "\n") + "\n")
|
fmt.Print(strings.Join(records, "\n") + "\n")
|
||||||
}
|
}
|
||||||
|
|
|
@ -458,10 +458,16 @@ func WebserverConfigSet(ctx context.Context, domainRedirects map[string]string,
|
||||||
|
|
||||||
// DomainRecords returns text lines describing DNS records required for configuring
|
// DomainRecords returns text lines describing DNS records required for configuring
|
||||||
// a domain.
|
// a domain.
|
||||||
func DomainRecords(domConf config.Domain, domain dns.Domain, hasDNSSEC bool) ([]string, error) {
|
//
|
||||||
|
// If certIssuerDomainName is set, CAA records to limit TLS certificate issuance to
|
||||||
|
// that caID will be suggested. If acmeAccountURI is also set, CAA records also
|
||||||
|
// restricting issuance to that account ID will be suggested.
|
||||||
|
func DomainRecords(domConf config.Domain, domain dns.Domain, hasDNSSEC bool, certIssuerDomainName, acmeAccountURI string) ([]string, error) {
|
||||||
d := domain.ASCII
|
d := domain.ASCII
|
||||||
h := Conf.Static.HostnameDomain.ASCII
|
h := Conf.Static.HostnameDomain.ASCII
|
||||||
|
|
||||||
|
// The first line with ";" is used by ../testdata/integration/moxacmepebble.sh and
|
||||||
|
// ../testdata/integration/moxmail2.sh for selecting DNS records
|
||||||
records := []string{
|
records := []string{
|
||||||
"; Time To Live of 5 minutes, may be recognized if importing as a zone file.",
|
"; Time To Live of 5 minutes, may be recognized if importing as a zone file.",
|
||||||
"; Once your setup is working, you may want to increase the TTL.",
|
"; Once your setup is working, you may want to increase the TTL.",
|
||||||
|
@ -471,15 +477,15 @@ func DomainRecords(domConf config.Domain, domain dns.Domain, hasDNSSEC bool) ([]
|
||||||
|
|
||||||
if public, ok := Conf.Static.Listeners["public"]; ok && public.TLS != nil && (len(public.TLS.HostPrivateRSA2048Keys) > 0 || len(public.TLS.HostPrivateECDSAP256Keys) > 0) {
|
if public, ok := Conf.Static.Listeners["public"]; ok && public.TLS != nil && (len(public.TLS.HostPrivateRSA2048Keys) > 0 || len(public.TLS.HostPrivateECDSAP256Keys) > 0) {
|
||||||
records = append(records,
|
records = append(records,
|
||||||
"; DANE: These records indicate that a remote mail server trying to deliver email",
|
`; DANE: These records indicate that a remote mail server trying to deliver email`,
|
||||||
"; with SMTP (TCP port 25) must verify the TLS certificate with DANE-EE (3), based",
|
`; with SMTP (TCP port 25) must verify the TLS certificate with DANE-EE (3), based`,
|
||||||
"; on the certificate public key (\"SPKI\", 1) that is SHA2-256-hashed (1) to the",
|
`; on the certificate public key ("SPKI", 1) that is SHA2-256-hashed (1) to the`,
|
||||||
"; hexadecimal hash. DANE-EE verification means only the certificate or public",
|
`; hexadecimal hash. DANE-EE verification means only the certificate or public`,
|
||||||
"; key is verified, not whether the certificate is signed by a (centralized)",
|
`; key is verified, not whether the certificate is signed by a (centralized)`,
|
||||||
"; certificate authority (CA), is expired, or matches the host name.",
|
`; certificate authority (CA), is expired, or matches the host name.`,
|
||||||
";",
|
`;`,
|
||||||
"; NOTE: Create the records below only once: They are for the machine, and apply",
|
`; NOTE: Create the records below only once: They are for the machine, and apply`,
|
||||||
"; to all hosted domains.",
|
`; to all hosted domains.`,
|
||||||
)
|
)
|
||||||
if !hasDNSSEC {
|
if !hasDNSSEC {
|
||||||
records = append(records,
|
records = append(records,
|
||||||
|
@ -666,13 +672,48 @@ func DomainRecords(domConf config.Domain, domain dns.Domain, hasDNSSEC bool) ([]
|
||||||
fmt.Sprintf(`_submission._tcp.%s. SRV 0 1 587 .`, d),
|
fmt.Sprintf(`_submission._tcp.%s. SRV 0 1 587 .`, d),
|
||||||
fmt.Sprintf(`_pop3._tcp.%s. SRV 0 1 110 .`, d),
|
fmt.Sprintf(`_pop3._tcp.%s. SRV 0 1 110 .`, d),
|
||||||
fmt.Sprintf(`_pop3s._tcp.%s. SRV 0 1 995 .`, d),
|
fmt.Sprintf(`_pop3s._tcp.%s. SRV 0 1 995 .`, d),
|
||||||
"",
|
|
||||||
|
|
||||||
"; Optional:",
|
|
||||||
"; You could mark Let's Encrypt as the only Certificate Authority allowed to",
|
|
||||||
"; sign TLS certificates for your domain.",
|
|
||||||
fmt.Sprintf("%s. CAA 0 issue \"letsencrypt.org\"", d),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if certIssuerDomainName != "" {
|
||||||
|
// ../rfc/8659:18 for CAA records.
|
||||||
|
records = append(records,
|
||||||
|
"",
|
||||||
|
"; Optional:",
|
||||||
|
"; You could mark Let's Encrypt as the only Certificate Authority allowed to",
|
||||||
|
"; sign TLS certificates for your domain.",
|
||||||
|
fmt.Sprintf(`%s. CAA 0 issue "%s"`, d, certIssuerDomainName),
|
||||||
|
)
|
||||||
|
if acmeAccountURI != "" {
|
||||||
|
// ../rfc/8657:99 for accounturi.
|
||||||
|
// ../rfc/8657:147 for validationmethods.
|
||||||
|
records = append(records,
|
||||||
|
";",
|
||||||
|
"; Optionally limit certificates for this domain to the account ID and methods used by mox.",
|
||||||
|
fmt.Sprintf(`;; %s. CAA 0 issue "%s; accounturi=%s; validationmethods=tls-alpn-01,http-01"`, d, certIssuerDomainName, acmeAccountURI),
|
||||||
|
";",
|
||||||
|
"; Or alternatively only limit for email-specific subdomains, so you can use",
|
||||||
|
"; other accounts/methods for other subdomains.",
|
||||||
|
fmt.Sprintf(`;; autoconfig.%s. CAA 0 issue "%s; accounturi=%s; validationmethods=tls-alpn-01,http-01"`, d, certIssuerDomainName, acmeAccountURI),
|
||||||
|
fmt.Sprintf(`;; mtasts.%s. CAA 0 issue "%s; accounturi=%s; validationmethods=tls-alpn-01,http-01"`, d, certIssuerDomainName, acmeAccountURI),
|
||||||
|
)
|
||||||
|
if strings.HasSuffix(h, "."+d) {
|
||||||
|
records = append(records,
|
||||||
|
";",
|
||||||
|
"; And the mail hostname.",
|
||||||
|
fmt.Sprintf(`;; %-*s CAA 0 issue "%s; accounturi=%s; validationmethods=tls-alpn-01,http-01"`, 20-3+len(d), h+".", certIssuerDomainName, acmeAccountURI),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// The string "will be suggested" is used by
|
||||||
|
// ../testdata/integration/moxacmepebble.sh and ../testdata/integration/moxmail2.sh
|
||||||
|
// as end of DNS records.
|
||||||
|
records = append(records,
|
||||||
|
";",
|
||||||
|
"; Note: After starting up, once an ACME account has been created, CAA records",
|
||||||
|
"; that restrict issuance to the account will be suggested.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
return records, nil
|
return records, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -612,6 +612,12 @@ func PrepareStaticConfig(ctx context.Context, log mlog.Log, configFile string, c
|
||||||
addErrorf("loading ACME identity for %q: %s", name, err)
|
addErrorf("loading ACME identity for %q: %s", name, err)
|
||||||
}
|
}
|
||||||
acme.Manager = manager
|
acme.Manager = manager
|
||||||
|
|
||||||
|
// Help configurations from older quickstarts.
|
||||||
|
if acme.IssuerDomainName == "" && acme.DirectoryURL == "https://acme-v02.api.letsencrypt.org/directory" {
|
||||||
|
acme.IssuerDomainName = "letsencrypt.org"
|
||||||
|
}
|
||||||
|
|
||||||
c.ACME[name] = acme
|
c.ACME[name] = acme
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -609,8 +609,9 @@ many authentication failures).
|
||||||
if !existingWebserver {
|
if !existingWebserver {
|
||||||
sc.ACME = map[string]config.ACME{
|
sc.ACME = map[string]config.ACME{
|
||||||
"letsencrypt": {
|
"letsencrypt": {
|
||||||
DirectoryURL: "https://acme-v02.api.letsencrypt.org/directory",
|
DirectoryURL: "https://acme-v02.api.letsencrypt.org/directory",
|
||||||
ContactEmail: args[0], // todo: let user specify an alternative fallback address?
|
ContactEmail: args[0], // todo: let user specify an alternative fallback address?
|
||||||
|
IssuerDomainName: "letsencrypt.org",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -893,7 +894,7 @@ configured correctly.
|
||||||
// priming dns caches with negative/absent records, causing our "quick setup" to
|
// priming dns caches with negative/absent records, causing our "quick setup" to
|
||||||
// appear to fail or take longer than "quick".
|
// appear to fail or take longer than "quick".
|
||||||
|
|
||||||
records, err := mox.DomainRecords(confDomain, domain, domainDNSSECResult.Authentic)
|
records, err := mox.DomainRecords(confDomain, domain, domainDNSSECResult.Authentic, "letsencrypt.org", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fatalf("making required DNS records")
|
fatalf("making required DNS records")
|
||||||
}
|
}
|
||||||
|
|
|
@ -319,6 +319,10 @@ See implementation guide, https://jmap.io/server.html
|
||||||
8555 Automatic Certificate Management Environment (ACME)
|
8555 Automatic Certificate Management Environment (ACME)
|
||||||
8737 Automated Certificate Management Environment (ACME) TLS Application-Layer Protocol Negotiation (ALPN) Challenge Extension
|
8737 Automated Certificate Management Environment (ACME) TLS Application-Layer Protocol Negotiation (ALPN) Challenge Extension
|
||||||
|
|
||||||
|
# CAA
|
||||||
|
8657 Certification Authority Authorization (CAA) Record Extensions for Account URI and Automatic Certificate Management Environment (ACME) Method Binding
|
||||||
|
8659 DNS Certification Authority Authorization (CAA) Resource Record
|
||||||
|
|
||||||
# DNS
|
# DNS
|
||||||
1034 DOMAIN NAMES - CONCEPTS AND FACILITIES
|
1034 DOMAIN NAMES - CONCEPTS AND FACILITIES
|
||||||
1035 DOMAIN NAMES - IMPLEMENTATION AND SPECIFICATION
|
1035 DOMAIN NAMES - IMPLEMENTATION AND SPECIFICATION
|
||||||
|
|
2
testdata/integration/moxacmepebble.sh
vendored
2
testdata/integration/moxacmepebble.sh
vendored
|
@ -25,7 +25,7 @@ sed -i -e 's/moxtest1@mox1.example: nil/moxtest1@mox1.example: nil\n\t\t\tpostfi
|
||||||
|
|
||||||
(
|
(
|
||||||
cat /integration/example.zone;
|
cat /integration/example.zone;
|
||||||
sed -n '/^;/,/CAA /p' output.txt |
|
sed -n '/^;/,/will be suggested/p' output.txt |
|
||||||
# allow sending from postfix for mox1.example.
|
# allow sending from postfix for mox1.example.
|
||||||
sed 's/mox1.example. *TXT "v=spf1 mx ~all"/mox1.example. TXT "v=spf1 mx ip4:172.28.1.70 ~all"/'
|
sed 's/mox1.example. *TXT "v=spf1 mx ~all"/mox1.example. TXT "v=spf1 mx ip4:172.28.1.70 ~all"/'
|
||||||
) >/integration/example-integration.zone
|
) >/integration/example-integration.zone
|
||||||
|
|
3
testdata/integration/moxmail2.sh
vendored
3
testdata/integration/moxmail2.sh
vendored
|
@ -23,7 +23,8 @@ TLS:
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# A fresh file was set up by moxacmepebble.
|
# A fresh file was set up by moxacmepebble.
|
||||||
sed -n '/^;/,/CAA /p' output.txt >>/integration/example-integration.zone
|
sed -n '/^;/,/will be suggested/p' output.txt >>/integration/example-integration.zone
|
||||||
|
|
||||||
unbound-control -s 172.28.1.30 reload # reload unbound with zone file changes
|
unbound-control -s 172.28.1.30 reload # reload unbound with zone file changes
|
||||||
|
|
||||||
mox -checkconsistency serve &
|
mox -checkconsistency serve &
|
||||||
|
|
|
@ -1774,6 +1774,13 @@ func dnsblsStatus(ctx context.Context, log mlog.Log, resolver dns.Resolver) map[
|
||||||
// DomainRecords returns lines describing DNS records that should exist for the
|
// DomainRecords returns lines describing DNS records that should exist for the
|
||||||
// configured domain.
|
// configured domain.
|
||||||
func (Admin) DomainRecords(ctx context.Context, domain string) []string {
|
func (Admin) DomainRecords(ctx context.Context, domain string) []string {
|
||||||
|
log := pkglog.WithContext(ctx)
|
||||||
|
return DomainRecords(ctx, log, domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DomainRecords is the implementation of API function Admin.DomainRecords, taking
|
||||||
|
// a logger.
|
||||||
|
func DomainRecords(ctx context.Context, log mlog.Log, domain string) []string {
|
||||||
d, err := dns.ParseDomain(domain)
|
d, err := dns.ParseDomain(domain)
|
||||||
xcheckuserf(ctx, err, "parsing domain")
|
xcheckuserf(ctx, err, "parsing domain")
|
||||||
dc, ok := mox.Conf.Domain(d)
|
dc, ok := mox.Conf.Domain(d)
|
||||||
|
@ -1785,7 +1792,22 @@ func (Admin) DomainRecords(ctx context.Context, domain string) []string {
|
||||||
if !dns.IsNotFound(err) {
|
if !dns.IsNotFound(err) {
|
||||||
xcheckf(ctx, err, "looking up record to determine if dnssec is implemented")
|
xcheckf(ctx, err, "looking up record to determine if dnssec is implemented")
|
||||||
}
|
}
|
||||||
records, err := mox.DomainRecords(dc, d, result.Authentic)
|
|
||||||
|
var certIssuerDomainName, acmeAccountURI string
|
||||||
|
public := mox.Conf.Static.Listeners["public"]
|
||||||
|
if public.TLS != nil && public.TLS.ACME != "" {
|
||||||
|
acme, ok := mox.Conf.Static.ACME[public.TLS.ACME]
|
||||||
|
if ok && acme.Manager.Manager.Client != nil {
|
||||||
|
certIssuerDomainName = acme.IssuerDomainName
|
||||||
|
acc, err := acme.Manager.Manager.Client.GetReg(ctx, "")
|
||||||
|
log.Check(err, "get public acme account")
|
||||||
|
if err == nil {
|
||||||
|
acmeAccountURI = acc.URI
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
records, err := mox.DomainRecords(dc, d, result.Authentic, certIssuerDomainName, acmeAccountURI)
|
||||||
xcheckf(ctx, err, "dns records")
|
xcheckf(ctx, err, "dns records")
|
||||||
return records
|
return records
|
||||||
}
|
}
|
||||||
|
|
|
@ -914,7 +914,7 @@ const domainDNSRecords = async (d) => {
|
||||||
'DNS Records',
|
'DNS Records',
|
||||||
),
|
),
|
||||||
dom.h1('Required DNS records'),
|
dom.h1('Required DNS records'),
|
||||||
dom('pre.literal', style({maxWidth: '70em'}), records.join('\n')),
|
dom('pre.literal', records.join('\n')),
|
||||||
dom.br(),
|
dom.br(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue