diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a4c07d..22f6383 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version check. Adding a user, setting a password, changing an e-mail or a role on the user form, and deleting a user now ask for your own account password, as a backup does, under the same limit on wrong guesses. +- **The entrypoint follows no link at the DMARC maps** (audit I2): the + three `dmarc_*` maps root creates at the start were missing from the list + of paths where a symbolic link is removed first. ## [2.14.3] - 2026-10-17 diff --git a/build/entrypoint.sh b/build/entrypoint.sh index 4ea7455..e92a802 100644 --- a/build/entrypoint.sh +++ b/build/entrypoint.sh @@ -91,6 +91,7 @@ drop_links /data/dkim /data/dkim.new /data/opendkim /data/sasl /data/postfix /da /data/sasl/sasldb2 /data/postfix/queue \ /data/postfix/sender_login_maps /data/postfix/relay_domains \ /data/postfix/transport /data/postfix/relay_recipients /data/postfix/tls_policy \ + /data/postfix/dmarc_recipients /data/postfix/dmarc_transport /data/postfix/dmarc_relay_domains \ /data/log/mail.log # Restored backups or previously-created state may contain panel-owned files # under /data; make sure they stay writable without disturbing anything that a diff --git a/build/postfix-config.sh b/build/postfix-config.sh index def988c..2305239 100644 --- a/build/postfix-config.sh +++ b/build/postfix-config.sh @@ -534,8 +534,12 @@ if [ "${INBOUND_RELAY_ENABLE}" = "true" ] || [ "${DMARC_REPORTS_ENABLE}" = "true TRANSPORT_SETTING="" RECIPIENT_RESTRICTIONS="reject" if [ "${INBOUND_RELAY_ENABLE}" = "true" ]; then + # [ -e ] is false for a dangling link and the redirect would follow + # it: the entrypoint's drop_links removes links at these paths, and + # this refuses to create through one all the same (audit 2026-10-17, + # I2). for f in "$RELAY_DOMAINS_MAP" "$TRANSPORT_MAP" "$RELAY_RECIPIENTS_MAP" "$TLS_POLICY_MAP"; do - [ -e "$f" ] || : > "$f" + [ -e "$f" ] || [ -L "$f" ] || : > "$f" done RELAY_DOMAINS_SETTING="texthash:${RELAY_DOMAINS_MAP}" TRANSPORT_SETTING="texthash:${TRANSPORT_MAP}" @@ -543,7 +547,7 @@ if [ "${INBOUND_RELAY_ENABLE}" = "true" ] || [ "${DMARC_REPORTS_ENABLE}" = "true fi if [ "${DMARC_REPORTS_ENABLE}" = "true" ]; then for f in "$DMARC_RECIPIENTS_MAP" "$DMARC_TRANSPORT_MAP" "$DMARC_RELAY_DOMAINS_MAP"; do - [ -e "$f" ] || : > "$f" + [ -e "$f" ] || [ -L "$f" ] || : > "$f" done ln -sf /usr/local/bin/panel /usr/local/bin/dmarc-ingest # flags=, user= and argv= are arguments of pipe(8), not -o parameters: diff --git a/docs/security-audit-2026-10-17.md b/docs/security-audit-2026-10-17.md index e484f83..6f1a224 100644 --- a/docs/security-audit-2026-10-17.md +++ b/docs/security-audit-2026-10-17.md @@ -693,9 +693,10 @@ the argument. tests pass with the field. โ€” **Opus**, 2 h. *Done 2026-10-17:* also on a change of e-mail (where reset links go); a save of the name and the reach alone is not asked; TestUserFormAsksForTheAccountPassword. -- [ ] **I2.** The three DMARC maps in `drop_links`; `[ -L ]` beside `[ -e ]` +- [x] **I2.** The three DMARC maps in `drop_links`; `[ -L ]` beside `[ -e ]` in postfix-config.sh:538,545; an e2e case plants a link and expects the - log line. โ€” **Opus**, 0.5 h. + log line. โ€” **Opus**, 0.5 h. *Done 2026-10-17:* TestStartsOnA28Data + plants a dangling `dmarc_transport` with DMARC reports on. - [ ] **M2.** Greedy reply group anchored on Postfix's own tail; the live line above as a test; security.md ยง Mail path reworded ("the last `; from=<`, which is Postfix's because a HELO name can hold none of diff --git a/test/e2e/upgrade_test.go b/test/e2e/upgrade_test.go index fb0c82d..5e9bf54 100644 --- a/test/e2e/upgrade_test.go +++ b/test/e2e/upgrade_test.go @@ -95,6 +95,14 @@ func TestStartsOnA28Data(t *testing.T) { t.Fatalf("generate throwaway TLS cert: %v", err) } + // A dangling link where root creates an empty DMARC map: the panel owns + // /data/postfix and could plant one. The entrypoint removes it and + // creates nothing at its target (audit 2026-10-17, I2). + const planted = "/etc/selfpost-planted-by-the-panel" + if err := os.Symlink(planted, filepath.Join(dataDir, "postfix", "dmarc_transport")); err != nil { + t.Fatal(err) + } + name := "selfpost-e2e-upgrade" _ = exec.Command("docker", "rm", "-f", name).Run() defer func() { @@ -103,7 +111,7 @@ func TestStartsOnA28Data(t *testing.T) { _ = exec.Command("docker", "rm", "-f", name).Run() }() if out, err := exec.Command("docker", "run", "-d", "--name", name, - "-e", "SELFPOST_HOSTNAME="+host, "-e", "MILTER_WAIT_TIMEOUT=10", + "-e", "SELFPOST_HOSTNAME="+host, "-e", "MILTER_WAIT_TIMEOUT=10", "-e", "DMARC_REPORTS_ENABLE=true", "-v", dataDir+":/data", "-v", certDir+":/etc/postfix/tls:ro", "selfpost:e2e").CombinedOutput(); err != nil { t.Fatalf("docker run: %v\n%s", err, out) @@ -147,6 +155,12 @@ func TestStartsOnA28Data(t *testing.T) { // its postrotate asks no reload of a Postfix not running yet (the test // server's rollout of 2026-10-16 logged "fatal: the Postfix mail system // is not running"; build/logrotate-mail.conf). + if !strings.Contains(logs(), "entrypoint: removing the symbolic link /data/postfix/dmarc_transport") { + t.Errorf("the entrypoint did not remove the planted link:\n%s", logs()) + } + if got := exec1(`test -e ` + planted + ` && echo created || echo absent; test -f /data/postfix/dmarc_transport && ! test -L /data/postfix/dmarc_transport && echo file`); got != "absent\nfile" { + t.Errorf("planted link: %q, want the target absent and the map a regular file", got) + } if got := exec1(`test -f /data/log/mail.log.1 && echo rotated`); got != "rotated" { t.Errorf("the rotation due at the start did not run: %q", got) }