test / test (push) Canceled after 0s
The last stage of the 2026-10-12 audit; no cut. Doc drift: architecture.md (the sender check in § OpenDKIM, the limit: row's 48 hours, the inbound log and filter-list routes and who may open them, migrations to 0011 and every table, KitPage as production data, Reload configuration as a restore writes, the panel's roles counted), schema-migrations.md (tables and columns since v2), guide.md (a delivery's lines go at the next prune; INBOUND_JOURNAL_MILTER_SOCKET moved to architecture.md's internal list), development.md (the env guard, the deferral the e2e proves, its fixed sleeps), roadmap.md. Comments say what the code does now; TestBaselineIsTheOnlyMigration is TestChainStartsAtTheBaseline; the milter package doc names its three milters. Dead code gone: view.Facts, dmarc HostedSuggestion and ValidateHostedAddress, app UpdateMode (each with the test that alone kept it), the method checks the method-bound routes made dead; Settings.WithResult calls resultFlash. staticcheck 2026.1 in test.yml on both modules, clean: the e2e package builds without its tests now (a test and two constants moved out of non-test files), three findings fixed. The test moved, TestImageBuildPreservesLogrotateMode, had sat in a non-test file and never run; run, it failed on the entrypoint (docker run without a hostname) and now asks with --entrypoint stat. The test gaps no step named, each closed: follow() with mail.log gone past blindMin and made again; CheckRestore with a manifest that is not JSON; the origin check with a port; every /server/ pattern in web.go through server() and in the routes table (it found GET /server/health/fragment missing from the table - guarded, not tested); L9 as the owner decided it on 2026-10-12, the comment on senderSession.Headers and the test passing an h that holds the earlier messages' From:. Plan stage 6 ticked; roadmap audit row done, 81/81. Tested on the test server: go vet + go test of 27 packages, make e2e (all pass, 513 s) on this tree; staticcheck locally on both modules. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
97 lines
3.1 KiB
Go
97 lines
3.1 KiB
Go
package e2e
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"math/big"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// servedSerial is the serial of the certificate Postfix presents on the
|
|
// implicit-TLS port.
|
|
func servedSerial() (*big.Int, error) {
|
|
conn, err := tls.DialWithDialer(&net.Dialer{Timeout: 5 * time.Second}, "tcp", smtpsAddr, &tls.Config{
|
|
InsecureSkipVerify: true,
|
|
ServerName: selfpostHostname,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer conn.Close()
|
|
certs := conn.ConnectionState().PeerCertificates
|
|
if len(certs) == 0 {
|
|
return nil, fmt.Errorf("no certificate presented")
|
|
}
|
|
return certs[0].SerialNumber, nil
|
|
}
|
|
|
|
func fileSerial(path string) (*big.Int, error) {
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
block, _ := pem.Decode(raw)
|
|
if block == nil {
|
|
return nil, fmt.Errorf("%s holds no PEM block", path)
|
|
}
|
|
cert, err := x509.ParseCertificate(block.Bytes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return cert.SerialNumber, nil
|
|
}
|
|
|
|
// checkCertRenewalPickedUp replaces the certificate in the stand's mount, as
|
|
// a renewer would, and waits for port 465 to present it without a restart.
|
|
// Postfix reads a copy made at start (postfix-config.sh); until 2.5.0 nothing
|
|
// renewed that copy, and a renewed certificate reached Postfix only when the
|
|
// container was restarted (audit 2026-10-12, H2). compose.override.yml sets
|
|
// TLS_RELOAD_INTERVAL_SECONDS to 2 so this does not wait a day.
|
|
func checkCertRenewalPickedUp(s *stack) error {
|
|
certPath := filepath.Join(s.stageDir, "certs", "fullchain.pem")
|
|
before, err := servedSerial()
|
|
if err != nil {
|
|
return fmt.Errorf("read the served certificate: %w", err)
|
|
}
|
|
if err := writeSelfSignedCert(certPath, filepath.Join(s.stageDir, "certs", "privkey.pem")); err != nil {
|
|
return fmt.Errorf("renew the certificate: %w", err)
|
|
}
|
|
want, err := fileSerial(certPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if want.Cmp(before) == 0 {
|
|
return fmt.Errorf("the renewed certificate has the old serial %s", before)
|
|
}
|
|
if err := waitFor("port 465 to present the renewed certificate", 30*time.Second, 500*time.Millisecond, func() (bool, error) {
|
|
got, err := servedSerial()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if got.Cmp(want) != 0 {
|
|
return false, fmt.Errorf("serial %s served, want %s", got, want)
|
|
}
|
|
return true, nil
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
// The path that was broken is the one taken: Postfix reads the copy, and
|
|
// the copy is the renewed file, the key private to root.
|
|
out, err := s.execIn("selfpost", "sh", "-c",
|
|
"postconf -h smtpd_tls_cert_file && cmp /etc/postfix/tls/fullchain.pem /etc/postfix/tls-internal/fullchain.pem && "+
|
|
"cmp /etc/postfix/tls/privkey.pem /etc/postfix/tls-internal/privkey.pem && stat -c '%a %U' /etc/postfix/tls-internal/privkey.pem")
|
|
if err != nil {
|
|
return fmt.Errorf("the certificate Postfix reads: %v\n%s", err, out)
|
|
}
|
|
if !strings.Contains(out, "/etc/postfix/tls-internal/fullchain.pem\n") || !strings.Contains(out, "600 root") {
|
|
return fmt.Errorf("the certificate Postfix reads: %q, want Postfix reading it and the key 600 root", out)
|
|
}
|
|
return nil
|
|
}
|