expiration-mailer: truncate serials and dns names (#6148)

This avoids sending excessively large emails and excessively large log
lines.

Fixes #6085
This commit is contained in:
Jacob Hoffman-Andrews 2022-06-14 15:48:00 -07:00 committed by GitHub
parent e6a5532a18
commit fda4124471
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 90 additions and 36 deletions

View File

@ -106,7 +106,18 @@ func (m *mailer) sendNags(conn bmail.Conn, contacts []string, certs []*x509.Cert
} }
domains = core.UniqueLowerNames(domains) domains = core.UniqueLowerNames(domains)
sort.Strings(domains) sort.Strings(domains)
m.log.Debugf("Sending mail for %s (%s)", strings.Join(domains, ", "), strings.Join(serials, ", "))
const maxSerials = 100
truncatedSerials := serials
if len(truncatedSerials) > maxSerials {
truncatedSerials = serials[0:maxSerials]
}
const maxDomains = 100
truncatedDomains := domains
if len(truncatedDomains) > maxDomains {
truncatedDomains = domains[0:maxDomains]
}
// Construct the information about the expiring certificates for use in the // Construct the information about the expiring certificates for use in the
// subject template // subject template
@ -128,13 +139,17 @@ func (m *mailer) sendNags(conn bmail.Conn, contacts []string, certs []*x509.Cert
} }
email := struct { email := struct {
ExpirationDate string ExpirationDate string
DaysToExpiration int DaysToExpiration int
DNSNames string DNSNames string
TruncatedDNSNames string
NumDNSNamesOmitted int
}{ }{
ExpirationDate: expDate.UTC().Format(time.RFC822Z), ExpirationDate: expDate.UTC().Format(time.RFC822Z),
DaysToExpiration: int(expiresIn.Hours() / 24), DaysToExpiration: int(expiresIn.Hours() / 24),
DNSNames: strings.Join(domains, "\n"), DNSNames: strings.Join(domains, "\n"),
TruncatedDNSNames: strings.Join(truncatedDomains, "\n"),
NumDNSNamesOmitted: len(domains) - len(truncatedDomains),
} }
msgBuf := new(bytes.Buffer) msgBuf := new(bytes.Buffer)
err = m.emailTemplate.Execute(msgBuf, email) err = m.emailTemplate.Execute(msgBuf, email)
@ -144,15 +159,15 @@ func (m *mailer) sendNags(conn bmail.Conn, contacts []string, certs []*x509.Cert
} }
logItem := struct { logItem := struct {
Rcpt []string Rcpt []string
Serials []string DaysToExpiration int
DaysToExpiration int TruncatedDNSNames []string
DNSNames []string TruncatedSerials []string
}{ }{
Rcpt: emails, Rcpt: emails,
Serials: serials, DaysToExpiration: email.DaysToExpiration,
DaysToExpiration: email.DaysToExpiration, TruncatedDNSNames: truncatedDomains,
DNSNames: domains, TruncatedSerials: truncatedSerials,
} }
logStr, err := json.Marshal(logItem) logStr, err := json.Marshal(logItem)
if err != nil { if err != nil {

View File

@ -85,11 +85,50 @@ var (
"n":"rFH5kUBZrlPj73epjJjyCxzVzZuV--JjKgapoqm9pOuOt20BUTdHqVfC2oDclqM7HFhkkX9OSJMTHgZ7WaVqZv9u1X2yjdx9oVmMLuspX7EytW_ZKDZSzL-sCOFCuQAuYKkLbsdcA3eHBK_lwc4zwdeHFMKIulNvLqckkqYB9s8GpgNXBDIQ8GjR5HuJke_WUNjYHSd8jY1LU9swKWsLQe2YoQUz_ekQvBvBCoaFEtrtRaSJKNLIVDObXFr2TLIiFiM0Em90kK01-eQ7ZiruZTKomll64bRFPoNo4_uwubddg3xTqur2vdF3NyhTrYdvAgTem4uC0PFjEQ1bK_djBQ", "n":"rFH5kUBZrlPj73epjJjyCxzVzZuV--JjKgapoqm9pOuOt20BUTdHqVfC2oDclqM7HFhkkX9OSJMTHgZ7WaVqZv9u1X2yjdx9oVmMLuspX7EytW_ZKDZSzL-sCOFCuQAuYKkLbsdcA3eHBK_lwc4zwdeHFMKIulNvLqckkqYB9s8GpgNXBDIQ8GjR5HuJke_WUNjYHSd8jY1LU9swKWsLQe2YoQUz_ekQvBvBCoaFEtrtRaSJKNLIVDObXFr2TLIiFiM0Em90kK01-eQ7ZiruZTKomll64bRFPoNo4_uwubddg3xTqur2vdF3NyhTrYdvAgTem4uC0PFjEQ1bK_djBQ",
"e":"AQAB" "e":"AQAB"
}`) }`)
log = blog.UseMock()
tmpl = template.Must(template.New("expiry-email").Parse(testTmpl)) tmpl = template.Must(template.New("expiry-email").Parse(testTmpl))
subjTmpl = template.Must(template.New("expiry-email-subject").Parse("Testing: " + defaultExpirationSubject)) subjTmpl = template.Must(template.New("expiry-email-subject").Parse("Testing: " + defaultExpirationSubject))
) )
func TestSendNagsManyCerts(t *testing.T) {
mc := mocks.Mailer{}
rs := newFakeRegStore()
fc := newFakeClock(t)
staticTmpl := template.Must(template.New("expiry-email-subject-static").Parse(testEmailSubject))
tmpl := template.Must(template.New("expiry-email").Parse(
`cert for DNS names {{.TruncatedDNSNames}} is going to expire in {{.DaysToExpiration}} days ({{.ExpirationDate}})`))
m := mailer{
log: blog.NewMock(),
mailer: &mc,
emailTemplate: tmpl,
// Explicitly override the default subject to use testEmailSubject
subjectTemplate: staticTmpl,
rs: rs,
clk: fc,
stats: initStats(metrics.NoopRegisterer),
}
var certs []*x509.Certificate
for i := 0; i < 101; i++ {
certs = append(certs, &x509.Certificate{
SerialNumber: big.NewInt(0x0304),
NotAfter: fc.Now().AddDate(0, 0, 2),
DNSNames: []string{fmt.Sprintf("example-%d.com", i)},
})
}
conn, err := m.mailer.Connect()
test.AssertNotError(t, err, "connecting SMTP")
err = m.sendNags(conn, []string{emailA}, certs)
test.AssertNotError(t, err, "sending mail")
test.AssertEquals(t, len(mc.Messages), 1)
if len(strings.Split(mc.Messages[0].Body, "\n")) > 100 {
t.Errorf("Expected mailed message to truncate after 100 domains, got: %q", mc.Messages[0].Body)
}
}
func TestSendNags(t *testing.T) { func TestSendNags(t *testing.T) {
mc := mocks.Mailer{} mc := mocks.Mailer{}
rs := newFakeRegStore() rs := newFakeRegStore()
@ -97,6 +136,7 @@ func TestSendNags(t *testing.T) {
staticTmpl := template.Must(template.New("expiry-email-subject-static").Parse(testEmailSubject)) staticTmpl := template.Must(template.New("expiry-email-subject-static").Parse(testEmailSubject))
log := blog.NewMock()
m := mailer{ m := mailer{
log: log, log: log,
mailer: &mc, mailer: &mc,
@ -156,13 +196,13 @@ func TestSendNags(t *testing.T) {
if !strings.Contains(sendLogs[0], `"Rcpt":["rolandshoemaker@gmail.com"]`) { if !strings.Contains(sendLogs[0], `"Rcpt":["rolandshoemaker@gmail.com"]`) {
t.Errorf("expected first 'attempting send' log line to have one address, got %q", sendLogs[0]) t.Errorf("expected first 'attempting send' log line to have one address, got %q", sendLogs[0])
} }
if !strings.Contains(sendLogs[0], `"Serials":["000000000000000000000000000000000304"]`) { if !strings.Contains(sendLogs[0], `"TruncatedSerials":["000000000000000000000000000000000304"]`) {
t.Errorf("expected first 'attempting send' log line to have one serial, got %q", sendLogs[0]) t.Errorf("expected first 'attempting send' log line to have one serial, got %q", sendLogs[0])
} }
if !strings.Contains(sendLogs[0], `"DaysToExpiration":2`) { if !strings.Contains(sendLogs[0], `"DaysToExpiration":2`) {
t.Errorf("expected first 'attempting send' log line to have 2 days to expiration, got %q", sendLogs[0]) t.Errorf("expected first 'attempting send' log line to have 2 days to expiration, got %q", sendLogs[0])
} }
if !strings.Contains(sendLogs[0], `"DNSNames":["example.com"]`) { if !strings.Contains(sendLogs[0], `"TruncatedDNSNames":["example.com"]`) {
t.Errorf("expected first 'attempting send' log line to have 1 domain, 'example.com', got %q", sendLogs[0]) t.Errorf("expected first 'attempting send' log line to have 1 domain, 'example.com', got %q", sendLogs[0])
} }
} }
@ -193,14 +233,13 @@ func TestProcessCerts(t *testing.T) {
testCtx := setup(t, []time.Duration{time.Hour * 24 * 7}) testCtx := setup(t, []time.Duration{time.Hour * 24 * 7})
certs := addExpiringCerts(t, testCtx) certs := addExpiringCerts(t, testCtx)
log.Clear()
testCtx.m.processCerts(context.Background(), certs) testCtx.m.processCerts(context.Background(), certs)
// Test that the lastExpirationNagSent was updated for the certificate // Test that the lastExpirationNagSent was updated for the certificate
// corresponding to serial4, which is set up as "already renewed" by // corresponding to serial4, which is set up as "already renewed" by
// addExpiringCerts. // addExpiringCerts.
if len(log.GetAllMatching("DEBUG: SQL: UPDATE certificateStatus .*2006-01-02 15:04:05.999999999.*\"000000000000000000000000000000001339\"")) != 1 { if len(testCtx.log.GetAllMatching("DEBUG: SQL: UPDATE certificateStatus .*2006-01-02 15:04:05.999999999.*\"000000000000000000000000000000001339\"")) != 1 {
t.Errorf("Expected an update to certificateStatus, got these log lines:\n%s", t.Errorf("Expected an update to certificateStatus, got these log lines:\n%s",
strings.Join(log.GetAllMatching(".*"), "\n")) strings.Join(testCtx.log.GetAllMatching(".*"), "\n"))
} }
} }
@ -223,7 +262,6 @@ func TestNoContactCertIsNotRenewed(t *testing.T) {
err = insertCertificate(cert, time.Time{}) err = insertCertificate(cert, time.Time{})
test.AssertNotError(t, err, "inserting certificate") test.AssertNotError(t, err, "inserting certificate")
log.Clear()
err = testCtx.m.findExpiringCertificates(context.Background()) err = testCtx.m.findExpiringCertificates(context.Background())
test.AssertNotError(t, err, "finding expired certificates") test.AssertNotError(t, err, "finding expired certificates")
@ -275,7 +313,6 @@ func TestNoContactCertIsRenewed(t *testing.T) {
}) })
test.AssertNotError(t, err, "inserting FQDNSet for renewal") test.AssertNotError(t, err, "inserting FQDNSet for renewal")
log.Clear()
err = testCtx.m.findExpiringCertificates(context.Background()) err = testCtx.m.findExpiringCertificates(context.Background())
test.AssertNotError(t, err, "finding expired certificates") test.AssertNotError(t, err, "finding expired certificates")
@ -299,14 +336,13 @@ func TestProcessCertsParallel(t *testing.T) {
testCtx.m.parallelSends = 2 testCtx.m.parallelSends = 2
certs := addExpiringCerts(t, testCtx) certs := addExpiringCerts(t, testCtx)
log.Clear()
testCtx.m.processCerts(context.Background(), certs) testCtx.m.processCerts(context.Background(), certs)
// Test that the lastExpirationNagSent was updated for the certificate // Test that the lastExpirationNagSent was updated for the certificate
// corresponding to serial4, which is set up as "already renewed" by // corresponding to serial4, which is set up as "already renewed" by
// addExpiringCerts. // addExpiringCerts.
if len(log.GetAllMatching("DEBUG: SQL: UPDATE certificateStatus .*2006-01-02 15:04:05.999999999.*\"000000000000000000000000000000001339\"")) != 1 { if len(testCtx.log.GetAllMatching("DEBUG: SQL: UPDATE certificateStatus .*2006-01-02 15:04:05.999999999.*\"000000000000000000000000000000001339\"")) != 1 {
t.Errorf("Expected an update to certificateStatus, got these log lines:\n%s", t.Errorf("Expected an update to certificateStatus, got these log lines:\n%s",
strings.Join(log.GetAllMatching(".*"), "\n")) strings.Join(testCtx.log.GetAllMatching(".*"), "\n"))
} }
} }
@ -330,12 +366,10 @@ func TestFindExpiringCertificates(t *testing.T) {
addExpiringCerts(t, testCtx) addExpiringCerts(t, testCtx)
log.Clear()
err := testCtx.m.findExpiringCertificates(context.Background()) err := testCtx.m.findExpiringCertificates(context.Background())
test.AssertNotError(t, err, "Failed on no certificates") test.AssertNotError(t, err, "Failed on no certificates")
test.AssertEquals(t, len(log.GetAllMatching("Searching for certificates that expire between.*")), 3) test.AssertEquals(t, len(testCtx.log.GetAllMatching("Searching for certificates that expire between.*")), 3)
log.Clear()
err = testCtx.m.findExpiringCertificates(context.Background()) err = testCtx.m.findExpiringCertificates(context.Background())
test.AssertNotError(t, err, "Failed to find expiring certs") test.AssertNotError(t, err, "Failed to find expiring certs")
// Should get 001 and 003 // Should get 001 and 003
@ -364,11 +398,10 @@ func TestFindExpiringCertificates(t *testing.T) {
}, testCtx.mc.Messages[1]) }, testCtx.mc.Messages[1])
// Check that regC's only certificate being renewed does not cause a log // Check that regC's only certificate being renewed does not cause a log
test.AssertEquals(t, len(log.GetAllMatching("no certs given to send nags for")), 0) test.AssertEquals(t, len(testCtx.log.GetAllMatching("no certs given to send nags for")), 0)
// A consecutive run shouldn't find anything // A consecutive run shouldn't find anything
testCtx.mc.Clear() testCtx.mc.Clear()
log.Clear()
err = testCtx.m.findExpiringCertificates(context.Background()) err = testCtx.m.findExpiringCertificates(context.Background())
test.AssertNotError(t, err, "Failed to find expiring certs") test.AssertNotError(t, err, "Failed to find expiring certs")
test.AssertEquals(t, len(testCtx.mc.Messages), 0) test.AssertEquals(t, len(testCtx.mc.Messages), 0)
@ -528,8 +561,6 @@ func TestFindCertsAtCapacity(t *testing.T) {
addExpiringCerts(t, testCtx) addExpiringCerts(t, testCtx)
log.Clear()
// Set the limit to 1 so we are "at capacity" with one result // Set the limit to 1 so we are "at capacity" with one result
testCtx.m.limit = 1 testCtx.m.limit = 1
@ -546,7 +577,6 @@ func TestFindCertsAtCapacity(t *testing.T) {
// A consecutive run shouldn't find anything // A consecutive run shouldn't find anything
testCtx.mc.Clear() testCtx.mc.Clear()
log.Clear()
err = testCtx.m.findExpiringCertificates(context.Background()) err = testCtx.m.findExpiringCertificates(context.Background())
test.AssertNotError(t, err, "Failed to find expiring certs") test.AssertNotError(t, err, "Failed to find expiring certs")
test.AssertEquals(t, len(testCtx.mc.Messages), 0) test.AssertEquals(t, len(testCtx.mc.Messages), 0)
@ -844,6 +874,7 @@ type testCtx struct {
mc *mocks.Mailer mc *mocks.Mailer
fc clock.FakeClock fc clock.FakeClock
m *mailer m *mailer
log *blog.Mock
cleanUp func() cleanUp func()
} }
@ -856,6 +887,7 @@ func setup(t *testing.T, nagTimes []time.Duration) *testCtx {
} }
fc := newFakeClock(t) fc := newFakeClock(t)
log := blog.NewMock()
ssa, err := sa.NewSQLStorageAuthority(dbMap, dbMap, nil, nil, fc, log, metrics.NoopRegisterer, 1) ssa, err := sa.NewSQLStorageAuthority(dbMap, dbMap, nil, nil, fc, log, metrics.NoopRegisterer, 1)
if err != nil { if err != nil {
t.Fatalf("unable to create SQLStorageAuthority: %s", err) t.Fatalf("unable to create SQLStorageAuthority: %s", err)
@ -887,6 +919,7 @@ func setup(t *testing.T, nagTimes []time.Duration) *testCtx {
mc: mc, mc: mc,
fc: fc, fc: fc,
m: m, m: m,
log: log,
cleanUp: cleanUp, cleanUp: cleanUp,
} }
} }

View File

@ -0,0 +1,6 @@
Hello,
Your SSL certificate for names {{.TruncatedDNSNames}}{{if(gt .NumDNSNamesOmitted 0)}} (and {{.NumDNSNamesOmitted}} more){{end}} is going to expire in {{.DaysToExpiration}}
days ({{.ExpirationDate}}), make sure you run the renewer before then!
Regards

View File

@ -11,7 +11,7 @@
}, },
"certLimit": 100000, "certLimit": 100000,
"nagTimes": ["480h", "240h"], "nagTimes": ["480h", "240h"],
"emailTemplate": "test/example-expiration-template", "emailTemplate": "test/config-next/expiration-mailer.gotmpl",
"debugAddr": ":8008", "debugAddr": ":8008",
"parallelSends": 10, "parallelSends": 10,
"tls": { "tls": {

View File

@ -12,7 +12,7 @@
"certLimit": 100000, "certLimit": 100000,
"nagTimes": ["480h", "240h"], "nagTimes": ["480h", "240h"],
"nagCheckInterval": "24h", "nagCheckInterval": "24h",
"emailTemplate": "test/example-expiration-template", "emailTemplate": "test/config/expiration-mailer.gotmpl",
"debugAddr": ":8008", "debugAddr": ":8008",
"tls": { "tls": {
"caCertFile": "test/grpc-creds/minica.pem", "caCertFile": "test/grpc-creds/minica.pem",