Skip to content

ns8-mail: bypass sender/domain map has no effect when email is sent via a third-party mailing service #8060

Open

Description

@stephdl

Steps to reproduce

  1. Add a sender domain to the bypass map via the ns8-mail UI or directly in bypass_sender_domain.map (e.g. tikehaucapital.com)
  2. Receive an email where the MIME From header is info@tikehaucapital.com but the SMTP envelope sender (MAIL FROM) is a different address from a mailing/relay service (e.g. fo@constance.fr)
  3. Check rspamd scoring in the history or logs

Expected behavior

The email should be accepted without any spam scoring, since the sender domain tikehaucapital.com is whitelisted in the bypass map. The BYPASS_SENDER_DOMAIN prefilter rule should fire and set action: no action.

Actual behavior

Rspamd still scores the message and applies rules such as DMARC_POLICY_REJECT, R_DKIM_REJECT, FORGED_SENDER, etc. The bypass has no effect. The following rspamd symbols confirm the issue:

FORGED_SENDER       (0.3) [info@tikehaucapital.com, fo@constance.fr]
FROM_NEQ_ENVFROM    (0)   [info@tikehaucapital.com, fo@constance.fr]
DMARC_POLICY_REJECT (2)   [tikehaucapital.com : No valid SPF, reject]
R_DKIM_REJECT       (1)   [tikehaucapital.com:s=200608]

Root cause

The BYPASS_SENDER and BYPASS_SENDER_DOMAIN rules in multimap.conf.j2 use type = "from" without the extract_from attribute:

BYPASS_SENDER_DOMAIN {
    type = "from";
    filter = "email:domain";
    map = ["${DBDIR}/bypass_sender_domain.map"];
    ...
}

According to rspamd documentation, type = "from" matches the SMTP envelope sender (MAIL FROM) by default — not the MIME From header. When an email is sent through a third-party mailing service (newsletters, marketing tools, etc.), the envelope sender belongs to the service's domain, while the From header shows the original sender's domain. The bypass map matches neither.

Proposed fix

Add extract_from = "both" to BYPASS_SENDER and BYPASS_SENDER_DOMAIN in multimap.conf.j2. This instructs rspamd to check both the SMTP envelope sender and the MIME From header against the map:

BYPASS_SENDER_DOMAIN {
    prefilter = true;
    action = "accept";
    type = "from";
    extract_from = "both";
    filter = "email:domain";
    map = ["${DBDIR}/bypass_sender_domain.map"];
}

extract_from has been available since rspamd 2.1 (October 2019).

Why block rules should NOT get the same change

The asymmetry between bypass and block rules is intentional:

  • Bypass + extract_from = "both": acceptable tradeoff. The MIME From header is user-controlled and can be forged, but for whitelisting legitimate newsletters this is the only practical option.
  • Block rules without extract_from: correct by design. Block rules should only match the SMTP envelope sender, which is the only data authenticated by the MTA. Adding extract_from = "both" to block rules would offer no real benefit — a spammer can trivially change the MIME From header to evade a domain block while keeping their actual envelope sender.

The conclusion is: check both sources when trusting, check only the authenticated envelope when blocking.

Security note

Since bypass maps now match the MIME From header, they should be kept small and contain only genuinely trusted domains. A forged From header with a whitelisted domain would bypass all rspamd checks.

On the security tradeoff

The extract_from = "both" change introduces a known tradeoff worth understanding:

A forged From: info@whitelisted-domain.com header would bypass rspamd checks. However, this risk is limited in practice for several reasons:

  • An attacker would need to know in advance which domains are in the bypass map, which is not public information.
  • The rspamd prefilter only skips spam scoring. Other defenses (ClamAV, Postfix restrictions, etc.) remain fully active.
  • This is the standard approach used by virtually all rspamd and SpamAssassin deployments that handle newsletters. SpamAssassin's whitelist_from directive works the same way and carries the same documented tradeoff.
  • The alternative — keeping bypass maps ineffective for newsletters sent via relay services — leads administrators to disable filters entirely, which is a far greater security risk.

The key operational recommendation is to keep bypass maps small and limit entries to domains whose emails are genuinely expected and recognized by users. A bypass map is not a general-purpose whitelist; it should be used sparingly.

Components

  • ns8-mail (current main branch)
  • rspamd multimap module

See also

Activity

  1. self-assigned this
    on Jun 23, 2026
  2. added theissue type on Jun 23, 2026
  3. added this to the NethServer 8.9 milestone on Jun 23, 2026
  4. DavidePrincipi commented on Jun 24, 2026

    @DavidePrincipi
    Member

    The email should be accepted without any spam scoring, since the sender domain tikehaucapital.com is whitelisted in the bypass map. The BYPASS_SENDER_DOMAIN prefilter rule should fire and set action: no action.

    Prefilter rule is applied unless RSPAMD_bypass_score and RSPAMD_block_score environment variables are defined. Please consider that they may change the behavior of rules.

    https://github.com/NethServer/ns8-mail/blob/bc6eb29cfcf3ea2976152e51d883f67b23bb7b52/rspamd/README.md?plain=1#L35-L40

  5. stephdl commented on Sep 4, 2026

    @stephdl
    Author

    Checked — both variables are undefined on that server, so the rules do run as prefilters (prefilter = true; action = "accept"; in the rendered multimap.conf).

    Same message scanned twice, only the envelope sender differs:

    envelope result
    bounce@relay.example.net reject, no BYPASS_* symbol
    info@tikehaucapital.com no action, matched map

    So the map only ever sees MAIL FROM.

    Correction to my issue body: I wrote that the envelope is the only MTA-authenticated data. It is not — MAIL FROM is as forgeable as the header, and the bypass prefilter runs before SPF and DMARC. So extract_from = "both" opens nothing new, and it matches the sender the admin actually sees.

    Fix on BYPASS_SENDER and BYPASS_SENDER_DOMAIN only, BLOCK_* untouched:

        type = "from";
        extract_from = "both";
    

    Outside the {% if env.bypass_score %}, so it covers both modes. Plus a README line and a release note — existing map entries will start matching more mail after the update.

    PR?

  6. moved this from ToDo to In Progress in NethServeron Sep 23, 2026
  7. stephdl commented on Oct 20, 2026

    @stephdl
    Author

    QA

    Target: mail:1.9.2.dev-1

    Replace mail1 and user@mydomain.tld with your instance and an existing mailbox.
    Each test builds a fake message and scans it with rspamc, nothing is delivered.
    From: is the header the user sees, --from is the SMTP envelope sender.

    Prepare

    • Update the module
      api-cli run update-module --data '{"module_url":"ghcr.io/nethserver/mail:1.9.2.dev-1","instances":["mail1"],"force":true}'
      
    • Add a bypass rule for the sender domain example.com
      api-cli run module/mail1/add-bypass-rule --data '{"type":"domain","direction":"from","value":"example.com"}'
      
    • Add a bypass rule for the sender address news@example.net
      api-cli run module/mail1/add-bypass-rule --data '{"type":"email","direction":"from","value":"news@example.net"}'
      

    Bypass sender domain

    • Newsletter via relay: header info@example.com, envelope bounce@relay.example.org

      printf "From: info@example.com\nTo: user@mydomain.tld\nSubject: test\n\nhello\n" | \
        runagent -m mail1 podman exec -i rspamd sh -c 'rspamc -h 127.0.0.1:11333 -P $RSPAMD_adminpw --from bounce@relay.example.org --rcpt user@mydomain.tld' | \
        grep -E "^Action|BYPASS_|BLOCK_"
      

      Expected: Action: no action and BYPASS_SENDER_DOMAIN

    • Direct sender: header and envelope info@example.com

      printf "From: info@example.com\nTo: user@mydomain.tld\nSubject: test\n\nhello\n" | \
        runagent -m mail1 podman exec -i rspamd sh -c 'rspamc -h 127.0.0.1:11333 -P $RSPAMD_adminpw --from info@example.com --rcpt user@mydomain.tld' | \
        grep -E "^Action|BYPASS_|BLOCK_"
      

      Expected: Action: no action and BYPASS_SENDER_DOMAIN

    Bypass sender address

    • Newsletter via relay: header news@example.net, envelope bounce@relay.example.org

      printf "From: news@example.net\nTo: user@mydomain.tld\nSubject: test\n\nhello\n" | \
        runagent -m mail1 podman exec -i rspamd sh -c 'rspamc -h 127.0.0.1:11333 -P $RSPAMD_adminpw --from bounce@relay.example.org --rcpt user@mydomain.tld' | \
        grep -E "^Action|BYPASS_|BLOCK_"
      

      Expected: Action: no action and BYPASS_SENDER

    • Other address of the same domain: header other@example.net, envelope bounce@relay.example.org

      printf "From: other@example.net\nTo: user@mydomain.tld\nSubject: test\n\nhello\n" | \
        runagent -m mail1 podman exec -i rspamd sh -c 'rspamc -h 127.0.0.1:11333 -P $RSPAMD_adminpw --from bounce@relay.example.org --rcpt user@mydomain.tld' | \
        grep -E "^Action|BYPASS_|BLOCK_"
      

      Expected: no BYPASS_ symbol

    Block rules are unchanged

    • Block the domain other.org

      runagent -m mail1 python3 -c "import mail; mail.rspamd_api_set_vmap('block_sender_domain.map', mail.rspamd_api_get_vmap('block_sender_domain.map') + ['other.org'])"
      
    • Forged header only: header info@other.org, envelope bounce@relay.example.org

      printf "From: info@other.org\nTo: user@mydomain.tld\nSubject: test\n\nhello\n" | \
        runagent -m mail1 podman exec -i rspamd sh -c 'rspamc -h 127.0.0.1:11333 -P $RSPAMD_adminpw --from bounce@relay.example.org --rcpt user@mydomain.tld' | \
        grep -E "^Action|BYPASS_|BLOCK_"
      

      Expected: no BLOCK_ symbol

    • Blocked envelope: header info@other.org, envelope x@other.org

      printf "From: info@other.org\nTo: user@mydomain.tld\nSubject: test\n\nhello\n" | \
        runagent -m mail1 podman exec -i rspamd sh -c 'rspamc -h 127.0.0.1:11333 -P $RSPAMD_adminpw --from x@other.org --rcpt user@mydomain.tld' | \
        grep -E "^Action|BYPASS_|BLOCK_"
      

      Expected: Action: reject and BLOCK_SENDER_DOMAIN

  8. moved this from In Progress to Testing in NethServeron Oct 20, 2026
  9. added
    testingPackages are available from testing repositories
    on Oct 20, 2026
  10. removed their assignment
    on Oct 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    testingPackages are available from testing repositories

    Type

    Projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions