// Runs in the exact scratch filesystem during the image build. No external mail. package main import ( "bufio" "crypto/rand" "crypto/rsa" "crypto/tls" "crypto/x509" "crypto/x509/pkix" "encoding/base64" "encoding/pem" "fmt" "math/big" "net" "net/smtp" "os" "os/exec" "path/filepath" "strings" "syscall" "time" ) func must(err error) { if err != nil { panic(err) } } func command(args ...string) []byte { c := exec.Command(args[0], args[1:]...) out, err := c.CombinedOutput() if err != nil { panic(fmt.Sprintf("%v: %v\n%s", args, err, out)) } return out } func certificate() tls.Certificate { key, err := rsa.GenerateKey(rand.Reader, 2048) must(err) template := &x509.Certificate{SerialNumber: big.NewInt(1), Subject: pkix.Name{CommonName: "localhost"}, NotBefore: time.Now().Add(-time.Hour), NotAfter: time.Now().Add(time.Hour), KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}} der, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key) must(err) cert, err := tls.X509KeyPair(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}), pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})) must(err) return cert } func serve(conn net.Conn, cert tls.Certificate, mechanism string, delivered chan<- string) { defer conn.Close() must(conn.SetDeadline(time.Now().Add(25 * time.Second))) reader := bufio.NewReader(conn) secured, authenticated := false, false send := func(s string) { _, err := fmt.Fprint(conn, s+"\r\n"); must(err) } send("220 localhost test relay") for { line, err := reader.ReadString('\n') if err != nil { return } line = strings.TrimSpace(line) fields := strings.Fields(line) if len(fields) == 0 { continue } switch strings.ToUpper(fields[0]) { case "EHLO": if secured { send("250-localhost\r\n250 AUTH " + mechanism) } else { send("250-localhost\r\n250 STARTTLS") } case "STARTTLS": send("220 Ready for TLS") secure := tls.Server(conn, &tls.Config{Certificates: []tls.Certificate{cert}, MinVersion: tls.VersionTLS12}) must(secure.Handshake()) conn = secure reader = bufio.NewReader(conn) secured = true case "AUTH": if !secured || len(fields) < 2 { send("535 TLS required") continue } decode := func(encoded string) string { decoded, err := base64.StdEncoding.DecodeString(strings.TrimSpace(encoded)) must(err) return string(decoded) } if fields[1] == "LOGIN" { encoded := "" if len(fields) > 2 { encoded = fields[2] } else { send("334 VXNlcm5hbWU6") encoded, err = reader.ReadString('\n') must(err) } if decode(encoded) != "test-user" { panic("incorrect SASL username") } send("334 UGFzc3dvcmQ6") encoded, err = reader.ReadString('\n') must(err) if decode(encoded) != "test-password" { panic("incorrect SASL password") } } else if fields[1] == "PLAIN" { encoded := "" if len(fields) > 2 { encoded = fields[2] } else { send("334 ") encoded, err = reader.ReadString('\n') must(err) } if decode(encoded) != "\x00test-user\x00test-password" { panic("incorrect SASL credentials") } } else { panic("unexpected SASL mechanism: " + fields[1]) } authenticated = true send("235 Authentication successful") case "MAIL", "RCPT": if !authenticated { send("530 Authentication required") } else { send("250 OK") } case "DATA": if !authenticated { send("530 Authentication required") continue } send("354 End with dot") var body strings.Builder for { part, err := reader.ReadString('\n') must(err) if part == ".\r\n" { break } body.WriteString(part) } send("250 Queued") delivered <- body.String() case "QUIT": send("221 Bye") return case "RSET", "NOOP": send("250 OK") default: panic("unexpected SMTP command: " + line) } } } func start() *exec.Cmd { cmd := exec.Command("/usr/local/bin/postfix-entrypoint") cmd.Env = append(os.Environ(), "SMTP_SERVER=127.0.0.1", "SMTP_PORT=2525", "SMTP_USERNAME=test-user", "SMTP_PASSWORD=test-password", "SERVER_HOSTNAME=relay.example.test", "DOMAIN=example.test", "LOCAL_NETWORK=127.0.0.0/8", "SMTP_HEADER_TAG=ci-test") cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr must(cmd.Start()) deadline := time.Now().Add(20 * time.Second) for time.Now().Before(deadline) { if exec.Command("/usr/local/bin/postfix-entrypoint", "--healthcheck").Run() == nil { return cmd } time.Sleep(200 * time.Millisecond) } panic("Postfix did not become healthy") } func stop(cmd *exec.Cmd) { must(cmd.Process.Signal(syscall.SIGTERM)) done := make(chan error, 1) go func() { done <- cmd.Wait() }() select { case <-done: case <-time.After(10 * time.Second): cmd.Process.Kill() panic("Postfix did not stop on SIGTERM") } } func main() { // Guarantee a bounded build even if a daemon misbehaves. go func() { time.Sleep(80 * time.Second); panic("smoke test timeout") }() for _, path := range []string{"/bin/sh", "/bin/bash", "/bin/busybox", "/sbin/apk", "/usr/bin/python3", "/usr/bin/go"} { if _, err := os.Stat(path); !os.IsNotExist(err) { panic("unexpected runtime tool: " + path) } } missing := exec.Command("/usr/local/bin/postfix-entrypoint") missing.Env = []string{"PATH=/usr/sbin:/usr/bin:/bin"} if missing.Run() == nil { panic("missing credentials accepted") } listener, err := net.Listen("tcp4", "127.0.0.1:2525") must(err) defer listener.Close() delivered := make(chan string, 4) cert := certificate() go func() { count := 0 for { conn, err := listener.Accept() if err != nil { return } mechanism := "PLAIN" if count%2 == 1 { mechanism = "LOGIN" } count++ go serve(conn, cert, mechanism, delivered) } }() // An empty queue volume, including stale PID contents, must initialize safely. must(os.MkdirAll("/var/spool/postfix/pid", 0755)) must(os.WriteFile("/var/spool/postfix/pid/master.pid", []byte("999999\n"), 0644)) // Reproduce the deprecated setting from the previous image. command("/usr/sbin/postconf", "-e", "smtp_use_tls = yes") cmd := start() if strings.Contains(string(command("/usr/sbin/postconf", "-n")), "smtp_use_tls") { panic("deprecated smtp_use_tls setting survived migration") } for _, path := range []string{"/etc/postfix/sasl_passwd", "/etc/postfix/sasl_passwd.lmdb"} { info, err := os.Stat(path) must(err) if info.Mode().Perm() != 0600 { panic("unsafe credential mode") } } command("/usr/local/bin/postfix-entrypoint", "--check") // Ensure dynamically loaded PCRE and LMDB maps both work. must(os.WriteFile("/tmp/test.pcre", []byte("/^test$/ OK\n"), 0644)) if strings.TrimSpace(string(command("/usr/sbin/postmap", "-q", "test", "pcre:/tmp/test.pcre"))) != "OK" { panic("PCRE plugin failed") } for round := 0; round < 2; round++ { message := "From: sender@example.test\r\nTo: recipient@example.net\r\nSubject: distroless smoke\r\nMIME-Version: 1.0\r\n\r\nLocal smoke message\r\n" must(smtp.SendMail("127.0.0.1:25", nil, "sender@example.test", []string{"recipient@example.net"}, []byte(message))) select { case body := <-delivered: if !strings.Contains(body, "Local smoke message") || !strings.Contains(body, "RelayTag: ci-test") { panic("message/header missing") } case <-time.After(25 * time.Second): panic("SASL/TLS relay delivery timed out") } // Wait for qmgr to remove the successfully delivered message. for i := 0; i < 30; i++ { if strings.Contains(string(command("/usr/sbin/postqueue", "-p")), "Mail queue is empty") { break } time.Sleep(100 * time.Millisecond) } if !strings.Contains(string(command("/usr/sbin/postqueue", "-p")), "Mail queue is empty") { panic("queue did not drain") } stop(cmd) if round == 0 { cmd = start() } } if exec.Command("/usr/local/bin/postfix-entrypoint", "--healthcheck").Run() == nil { panic("healthcheck succeeded after shutdown") } // No source or test binary is copied into the final runtime. must(os.MkdirAll(filepath.Dir("/tmp/smoke-passed"), 0755)) must(os.WriteFile("/tmp/smoke-passed", []byte("startup, healthcheck, empty queue, restart, SMTP, STARTTLS, SASL, LMDB, PCRE, header tag, SIGTERM: passed\n"), 0644)) fmt.Println("Distroless Postfix smoke tests passed") }