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:
parent
e6a5532a18
commit
fda4124471
|
|
@ -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
|
||||||
|
|
@ -131,10 +142,14 @@ func (m *mailer) sendNags(conn bmail.Conn, contacts []string, certs []*x509.Cert
|
||||||
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)
|
||||||
|
|
@ -145,14 +160,14 @@ 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
|
||||||
DNSNames []string
|
TruncatedDNSNames []string
|
||||||
|
TruncatedSerials []string
|
||||||
}{
|
}{
|
||||||
Rcpt: emails,
|
Rcpt: emails,
|
||||||
Serials: serials,
|
|
||||||
DaysToExpiration: email.DaysToExpiration,
|
DaysToExpiration: email.DaysToExpiration,
|
||||||
DNSNames: domains,
|
TruncatedDNSNames: truncatedDomains,
|
||||||
|
TruncatedSerials: truncatedSerials,
|
||||||
}
|
}
|
||||||
logStr, err := json.Marshal(logItem)
|
logStr, err := json.Marshal(logItem)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue