audit 5: findings of the two reviews
- The e2e's numeric-settings test ran a container that passed every gate
and never ended: the good values are checked beside a bad one after
them; each run is named, bounded to a minute and removed.
- The gates refuse a value under the parameter's minimum (a zero window
kills anvil, a zero milter timeout smtpd) and one past an int, in
seconds for a time value (src/global/conv_time.c, src/anvil/anvil.c).
- OpenDKIM waits, five seconds at most, for the relay's socket.
- The relay marks its lines `syslog: `, writes control characters out,
reads into a queue at once and drops past 200 lines a second, saying
how many (security F1, F2; a stuck relay would hold OpenDKIM in
syslog(3)).
- smtp_tls_mandatory_protocols >=TLSv1.2 too.
- extract-cert.sh keeps the old files' group and sets its trap before
mktemp.
- crashexit spares cert-reload; architecture.md "four times running" and
what else exits the container; security.md the restart after five panel
exits and the /dev/log socket.
- Measured against the technical review: smtpd has the selfpost group
(Groups 104 998), so it did reach the supervisor socket before this
stage.
Accepted, reasons in the plan: the logrotate state file in the postfix
user's directory, envdoc_test's reading of `${VAR:-}` only, crashexit's
window pruning untested.
Tested on the test server: go vet + go test of 27 packages, make e2e (all
pass, 592 s) on this tree.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,8 @@ import (
|
||||
"net"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// stamp is the BSD syslog timestamp that follows the priority, "Oct 15
|
||||
@@ -24,10 +26,17 @@ import (
|
||||
// space). The container's log has times of its own.
|
||||
var stamp = regexp.MustCompile(`^[A-Z][a-z]{2} [ 0-9][0-9] [0-9]{2}:[0-9]{2}:[0-9]{2} `)
|
||||
|
||||
// Prefix begins every relayed line. The socket takes datagrams from any user
|
||||
// in the container, so a relayed line must not pass for one of the panel's
|
||||
// or supervisord's own (security review of audit stage 5).
|
||||
const Prefix = "syslog: "
|
||||
|
||||
// Line turns one datagram into the line written out: the priority and the
|
||||
// timestamp taken off, trailing newlines too; what remains is "ident[pid]:
|
||||
// message". A severity of error or worse (RFC 3164 § 4.1.1: the priority is
|
||||
// facility*8 + severity, and 0-3 are emergency to error) is said in front.
|
||||
// facility*8 + severity, and 0-3 are emergency to error) is said in front. A
|
||||
// control character within it - a newline that would start a line of its own
|
||||
// - is written as \xNN.
|
||||
func Line(datagram []byte) string {
|
||||
b := bytes.TrimRight(datagram, "\r\n\x00")
|
||||
severity := -1
|
||||
@@ -51,16 +60,36 @@ func Line(datagram []byte) string {
|
||||
if loc := stamp.FindIndex(b); loc != nil {
|
||||
b = b[loc[1]:]
|
||||
}
|
||||
line := string(b)
|
||||
var out bytes.Buffer
|
||||
out.WriteString(Prefix)
|
||||
if severity >= 0 && severity <= 3 {
|
||||
line = "error: " + line
|
||||
out.WriteString("error: ")
|
||||
}
|
||||
return line
|
||||
for _, c := range b {
|
||||
if c < 0x20 || c == 0x7f {
|
||||
fmt.Fprintf(&out, `\x%02x`, c)
|
||||
continue
|
||||
}
|
||||
out.WriteByte(c)
|
||||
}
|
||||
return out.String()
|
||||
}
|
||||
|
||||
// The relay keeps reading whatever it can write: a datagram is read at once
|
||||
// and queued, and dropped when the queue is full or more than ratePerSecond
|
||||
// came in the last second. A sender to a unix datagram socket waits while the
|
||||
// receiver's queue is full, so a relay that stopped reading would have held
|
||||
// OpenDKIM in syslog(3) - with mail waiting on it (review of audit stage 5) -
|
||||
// and a flood would have filled the container's log.
|
||||
const (
|
||||
queueLen = 1024
|
||||
ratePerSecond = 200
|
||||
)
|
||||
|
||||
// Run listens on a datagram socket at path, which anyone in the container may
|
||||
// write to, and writes each message to w as a line until the socket fails.
|
||||
// /dev/log is a link to path (build/entrypoint.sh).
|
||||
// /dev/log is a link to path (build/entrypoint.sh). What it drops it says,
|
||||
// once a second at most.
|
||||
func Run(path string, w io.Writer) error {
|
||||
_ = os.Remove(path)
|
||||
conn, err := net.ListenUnixgram("unixgram", &net.UnixAddr{Name: path, Net: "unixgram"})
|
||||
@@ -73,15 +102,45 @@ func Run(path string, w io.Writer) error {
|
||||
if err := os.Chmod(path, 0o666); err != nil {
|
||||
return fmt.Errorf("syslog relay: %w", err)
|
||||
}
|
||||
|
||||
lines := make(chan string, queueLen)
|
||||
go func() {
|
||||
for l := range lines {
|
||||
fmt.Fprintln(w, l)
|
||||
}
|
||||
}()
|
||||
defer close(lines)
|
||||
|
||||
buf := make([]byte, 64<<10)
|
||||
var second time.Time
|
||||
inSecond, dropped := 0, 0
|
||||
for {
|
||||
n, _, err := conn.ReadFromUnix(buf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("syslog relay: read: %w", err)
|
||||
}
|
||||
now := time.Now()
|
||||
if now.Sub(second) >= time.Second {
|
||||
if dropped > 0 {
|
||||
select {
|
||||
case lines <- Prefix + "relay: dropped " + strconv.Itoa(dropped) + " lines - more than " + strconv.Itoa(ratePerSecond) + " a second, or the log was not taking them":
|
||||
default:
|
||||
}
|
||||
}
|
||||
second, inSecond, dropped = now, 0, 0
|
||||
}
|
||||
inSecond++
|
||||
if n == 0 {
|
||||
continue
|
||||
}
|
||||
fmt.Fprintln(w, Line(buf[:n]))
|
||||
if inSecond > ratePerSecond {
|
||||
dropped++
|
||||
continue
|
||||
}
|
||||
select {
|
||||
case lines <- Line(buf[:n]):
|
||||
default:
|
||||
dropped++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,11 +13,13 @@ import (
|
||||
|
||||
func TestLine(t *testing.T) {
|
||||
for in, want := range map[string]string{
|
||||
"<22>Oct 15 09:00:00 opendkim[45]: 3A2B: DKIM-Signature field added (s=sp, d=example.com)\n": "opendkim[45]: 3A2B: DKIM-Signature field added (s=sp, d=example.com)",
|
||||
"<19>Oct 5 09:00:00 opendkim[45]: key retrieval failed": "error: opendkim[45]: key retrieval failed",
|
||||
"<20>opendkim[45]: no timestamp": "opendkim[45]: no timestamp",
|
||||
"not syslog at all": "not syslog at all",
|
||||
"<x>Oct 15 09:00:00 odd": "<x>Oct 15 09:00:00 odd",
|
||||
"<22>Oct 15 09:00:00 opendkim[45]: 3A2B: DKIM-Signature field added (s=sp, d=example.com)\n": "syslog: opendkim[45]: 3A2B: DKIM-Signature field added (s=sp, d=example.com)",
|
||||
"<19>Oct 5 09:00:00 opendkim[45]: key retrieval failed": "syslog: error: opendkim[45]: key retrieval failed",
|
||||
"<20>opendkim[45]: no timestamp": "syslog: opendkim[45]: no timestamp",
|
||||
"not syslog at all": "syslog: not syslog at all",
|
||||
"<x>Oct 15 09:00:00 odd": "syslog: <x>Oct 15 09:00:00 odd",
|
||||
// A line of its own cannot be made: a newline inside is written out.
|
||||
"<14>opendkim[1]: one\npanel: forged": `syslog: opendkim[1]: one\x0apanel: forged`,
|
||||
} {
|
||||
if got := Line([]byte(in)); got != want {
|
||||
t.Errorf("Line(%q) = %q, want %q", in, got, want)
|
||||
@@ -43,26 +45,32 @@ func (s *syncBuffer) String() string {
|
||||
return s.b.String()
|
||||
}
|
||||
|
||||
// What a program sends to the socket comes out as a line.
|
||||
func TestRunRelaysADatagram(t *testing.T) {
|
||||
// startRelay runs the relay on a socket of its own and returns a connection
|
||||
// to it.
|
||||
func startRelay(t *testing.T, w *syncBuffer) net.Conn {
|
||||
t.Helper()
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("no unixgram sockets on Windows; the container is Linux")
|
||||
}
|
||||
path := filepath.Join(t.TempDir(), "log")
|
||||
out := &syncBuffer{}
|
||||
go Run(path, out)
|
||||
go Run(path, w)
|
||||
var conn net.Conn
|
||||
var err error
|
||||
for i := 0; i < 50; i++ {
|
||||
if conn, err = net.Dial("unixgram", path); err == nil {
|
||||
break
|
||||
t.Cleanup(func() { conn.Close() })
|
||||
return conn
|
||||
}
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer conn.Close()
|
||||
t.Fatal(err)
|
||||
return nil
|
||||
}
|
||||
|
||||
// What a program sends to the socket comes out as a line.
|
||||
func TestRunRelaysADatagram(t *testing.T) {
|
||||
out := &syncBuffer{}
|
||||
conn := startRelay(t, out)
|
||||
if _, err := conn.Write([]byte("<19>Oct 15 09:00:00 opendkim[7]: can't load key")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -70,7 +78,33 @@ func TestRunRelaysADatagram(t *testing.T) {
|
||||
for time.Now().Before(deadline) && !strings.Contains(out.String(), "\n") {
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
if got := out.String(); got != "error: opendkim[7]: can't load key\n" {
|
||||
if got := out.String(); got != "syslog: error: opendkim[7]: can't load key\n" {
|
||||
t.Errorf("relayed %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// A flood is cut at the rate, and what was dropped is said.
|
||||
func TestRunDropsAFlood(t *testing.T) {
|
||||
out := &syncBuffer{}
|
||||
conn := startRelay(t, out)
|
||||
for i := 0; i < ratePerSecond+100; i++ {
|
||||
if _, err := conn.Write([]byte("<14>flood")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
time.Sleep(1100 * time.Millisecond)
|
||||
if _, err := conn.Write([]byte("<14>after")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
deadline := time.Now().Add(2 * time.Second)
|
||||
for time.Now().Before(deadline) && !strings.Contains(out.String(), "syslog: after") {
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
got := out.String()
|
||||
if n := strings.Count(got, "syslog: flood\n"); n != ratePerSecond {
|
||||
t.Errorf("%d flood lines written, want %d", n, ratePerSecond)
|
||||
}
|
||||
if !strings.Contains(got, "syslog: relay: dropped 100 lines") {
|
||||
t.Errorf("the drop was not said:\n%s", got)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user