docker-mailserver / docker-mailserver

Production-ready fullstack but simple mail server (SMTP, IMAP, LDAP, Antispam, Antivirus, etc.) running inside a container.
https://docker-mailserver.github.io/docker-mailserver/latest/
MIT License
14.74k stars 1.84k forks source link

[TODO]: `SPOOF_PROTECTION=1` feature and Alias support improvements #2819

Open polarathene opened 2 years ago

polarathene commented 2 years ago

Adding this here in case anyone else has time to tackle any of it.

I am not likely to have the time until at least December, but I expect I'll have to prioritize my time on other commitments before I can come back to addressing these tasks.

Task Overview

I've provided plenty of helpful details below, but a summary of tasks to complete is:

SPOOF_PROTECTION=1 feature:

Related alias support:


SPOOF_PROTECTION=1 handling for smtpd_sender_login_maps

Jan 2021, @georglauterbach created and merged a PR to fix a user reported bug. The PR introduced a unionmap: { ... } table, which Postfix queries each listed table and merges the responses, rather than accepting the first matched response and exiting early with that outcome (as the result may fail, but have passed from another valid match rule later on).

Nothing wrong there, but it was the PR that split the non-LDAP handling for smtpd_sender_login_maps (_used for ENV SPOOF_PROTECTION=1_) into two values, only allowing the unionmap when /etc/postfix/regexp config was present. This does not seem necessary, and using the unionmap regardless is probably fine?

https://github.com/docker-mailserver/docker-mailserver/blob/ff969509f8b9c49e399327887c9a29f6fd664b80/target/scripts/helpers/aliases.sh#L29-L37

https://github.com/docker-mailserver/docker-mailserver/blob/ff969509f8b9c49e399327887c9a29f6fd664b80/target/scripts/start-mailserver.sh#L116-L123

/etc/postfix/regexp is always created during startup, and it has always been before this logic is run, even before that PR which added the conditional check on it's existence. We would always get unionmap AFAIK. It may even be useful for the LDAP configs.


As a refresher for anyone reading SPOOF_PROTECTION=1 (opt-in) prevents submitting any mail to send unless the logged in account is authorized to send from that envelope address (FROM) in smtpd_sender_login_maps lookup tables. The ENV does this by adding this restriction: smtpd_sender_restrictions = reject_authenticated_sender_login_mismatch (prefixed in front of other sender restrictions).

Without this, our default config allows a logged in user to send mail "from" whatever address they'd like to, including other accounts for the same domain, or one that DMS doesn't manage. The related bug report that introduced the fix with unionmap, has a user explain a good use-case for wanting that, non-human service accounts being restricted to their account only for sending mail in-case the service is compromised, while using a regex to allow their mail@example.com account (admin) to be able to send as any address for that domain.

When opting into SPOOF_PROTECTION=1, we permit local aliases (root, amavis, etc) (_that linked PR introduced the POSTMASTER_ADDRESS ENV and alias, along with setting mydestination in main.cf_), account aliases, and a few other lookup tables in the unionmap. One of these is a regexp file sender_login_maps.pcre that will match whatever the owners sender address is, and allow it (There is similar advice elsewhere, that suggests this approach, seems fine). A more specific match would have higher priority I think, but it may just depend on whatever is matched first from top to bottom of a table.

It should be noted that without the /etc/postfix/virtual table for account aliases included in the unionmap, you cannot send from an alias (eg: marketing@example.com), but you can authenticate over SASL as your aliased account by using that alias as the login account/username. That still logs the user in as the aliased account, not the alias, thus the actual account is still what is checked for allowed sender addresses that can be used regardless.

The original PR that introduced this feature with SPOOF_PROTECTION=1 also has discussion about enabling it by default in future breaking change. And some users sharing concerns about their workflows being broken, one from a previous maintainer who was using aliases to reply to mail keeping the same FROM address in the message, while only having a single mail account to manage.


Additional Notes

The /etc/postfix/regexp table was added in this PR, when the user previously tried to use a catch-all alias @example.com and noticed it did not work for them (received but could not send), possibly related to the later fix of using a unionmap table.

If we have any LDAP related issues with the feature, this may be helpful. That had a follow-up issue which also had a related PR merged for improving LDAP support with an optional filter config ENV - it demonstrates how to allow an additional sender address for noreply@example.com.


The tables for smtpd_sender_login_maps are pretty simple, the left-side (1st column) is to match the desired sender address (explicit value or regex pattern depending on table type), and on the right-side (2nd column) is usually the owner address (login mail account, non-aliased), but it can be multiple addresses to allow too by using a , delimiter just like the /etc/postfix/virtual alias table allows to map an alias to multiple recipients.

If using a regex pattern, keep in mind that whatever matches in a table first is the pass/fail for that table, regardless of the unionmap usage. As noted by this users comment that had two lines /.*/ to approve two separate domains, only the first would ever be matched. The response suggests moving the 2nd line to a separate table, but they should be able to fold the 2nd domain into a single line for that same regexp as mentioned above with , delimiter.

The ENV docs for SPOOF_PROTECTION=1 notes that extension delimiters (eg: +) are no longer usable as we don't have a table to authorize them out of the box. Technically it should be possible with regex as well? Would need some testing to ensure it's not abused.

unionmap with smtpd_sender_login_maps is covered well with the problem it solves in this blog article.

The Postfix SASL docs briefly touch on related configuration discussed here, but seems to incorrectly reference using it with smtpd_recipient_restrictions rather than smtpd_sender_restrictions.


SPOOF_PROTECTION=1 only restricts the envelope FROM sender address, spoofing still feasible?

Despite SPOOF_PROTECTION=1 applying these settings to restrict what a sender can use in FROM (mail envelope), this doesn't prevent spoofing of the from: (mail message) header apparently. This is mentioned here, in a link it references, that resource links to another that clarifies the difference between envelope and message FROM headers, and how SPF+DKIM+DMARC are leveraged to better handle this.

Thus SPOOF_PROTECTION may not be the most appropriate ENV name, it's mostly to restrict your own authenticated accounts to what sender addresses can be used in the envelope FROM when they submit mail, which is still worthwhile to do. There is this project which is a Postfix milter designed to ensure the sender envelope and message are the same, which prevents OpenDKIM signing the message sender (From: header) for a mismatch on the same domain, but different account.


Related

Supporting send-only addresses without a regex table

A recent feature-request was raised for allowing accounts to opt-out from being considered managed by Postfix. They provide their solution to use another config for scripts/helpers/postfix.sh:_vhost_collect_postfix_domains() to exclude any account using a mentioned domain. I believe we have a similar opt-out config feature for when a relayhost is enabled (due to blanket adding all accounts to the relay..).

That prompted me as likely a misconfiguration (confirmed, due to lack of our docs covering how to support the use-case). Their use-case was to send mail as noreply@<external-domain>, however SPOOF_PROTECTION=1 was restricting this without a matching account for that address - but as they wanted to send to a user at that same external domain (which was now considered managed by Postfix), it tries to deliver internally instead of outbound to the intended external mail-server. The noreply sender address was only for sending to external accounts, not receiving any inbound mail, an account is therefore not necessary to resolve this use-case properly.

I investigated and figured that they'd be better to use an opt-in config for what account should be allowed to send as an external account instead, which the smtpd_sender_login_maps can allow just fine. My link to that issue provides an example for how to setup a local testing environment that proves this approach works well. Normally it's not an issue when SPOOF_PROTECTION=0 (default), as an authenticated user can use whatever sender address they like then.

Only issue is that using regex probably isn't always appropriate - Users need to be careful with what expressions they use, often forgetting to escape regex tokens like . when providing a domain (I came across quite a few snippets where this was the case). However all the other tables aren't specifically for this feature which could cause issues, so another table should be made available within the unionmap. I haven't looked over the earlier mentioned LDAP PR that added a related config ENV to extend support, but assume it provides similar flexibility when needed.

If SPOOF_PROTECTION=1 was to be enabled in future as a default, we should first support configuring with an alternative table (eg: texthash:/etc/postfix/senders-map.cf or similar would be fine), along with supporting documentation and release notes (probably warrants a notice period before switching the default for the feature?).

External accounts being handled as internal accidentally?

Related is another scenario / issue, again regarding a noreply sending account (but this one would belong to the internal domain). It's recipients are provided from LDAP accounts, and some users are remote / external addresses, such as user@gmail.com.

I'm inclined to think this is more of an issue with their LDAP config, as DMS should not be treating @gmail as managed, unless your config is creating them as internal mail accounts for DMS (equivalent of postfix-accounts.cf), or misconfigured as aliases (since we collect any alias domain into the Postfix /etc/postfix/vhost to be treated as managed by DMS).

Their desire was to try match the recipient mail is to be sent out to in hopes of convincing Postfix to deliver as outbound mail. Again, these accounts were not to receive mail to DMS, or forward / relay any inbound mail arriving to DMS to these external mail-servers (eg: Gmail). The only intention was having the noreply@example.com send out notifications from a service to these LDAP users associated mail accounts (where some of the LDAP users are example.com accounts, but not all).

In this case rather than matching the FROM envelope, I believe we could workaround by using a transport_maps table that matches the TO (recipient). The prior send-only issue above did have a solution proposed to opt-out of managing entire domains (by excluding them from Postfix /etc/postfix/vhost), I'm just not convinced that's something we should maintain.

I lack LDAP knowledge, but cannot think of how this would be setup in a non-LDAP config where it'd make sense to create @gmail accounts - but I have seen users create aliases to remote addresses / accounts like Gmail? (which can introduce the same problem if the remote domain is incorrectly added as the alias, not the recipient that an alias should map to)

We have the following described in our docs at the end of this example:

Create email accounts and aliases:

With SPOOF_PROTECTION=0

./setup.sh email add admin@example.com passwd123
./setup.sh email add info@example.com passwd123
./setup.sh alias add admin@example.com external-account@gmail.com
./setup.sh alias add info@example.com external-account@gmail.com
./setup.sh email list
./setup.sh alias list

Aliases make sure that any email that comes to these accounts is forwarded to your third-party email address (external-account@gmail.com), where they are retrieved (eg: via third-party web or mobile app), instead of connecting directly to docker-mailserver with POP3 / IMAP.

Directly after that SPOOF_PROTECTION=0 example is one for SPOOF_PROTECTION=1, where the advice is to create an intermediate alias, to workaround the sender map problem. That works because we include the alias table of aliases mapping to whatever recipients, and treating those as owners. A workaround that shouldn't be needed / used.

The Postfix virtual docs on alias forwarding has a similar example (_excluding the SPOOF_PROTECTION issue_).


setup alias add

The setup alias add command "usage" / help could better match our addalias utility which is more clear on the inputs?:

https://github.com/docker-mailserver/docker-mailserver/blob/ff969509f8b9c49e399327887c9a29f6fd664b80/target/bin/setup#L46-L47

The internal utility makes it more apparent the first input is an alias that is mapped to recipient(s) (internal postfix-accounts.cf, external addresses, or other aliases):

https://github.com/docker-mailserver/docker-mailserver/blob/ff969509f8b9c49e399327887c9a29f6fd664b80/target/bin/addalias#L21-L22

Apart from the bug issue, our aliases docs communicate roughly the same two scenarios described above for setup alias add :+1:

Alias domains being added to /etc/postfix/vhost

We've had this support introduced way back in Oct 2015, which now lives in a dedicated vhost helper script that I created during a refactoring via this PR (June 2022).

I discussed further details regarding it and related Postfix main.cf config in the send-only linked issue above. As the postfix-regexp.cf for alias support came after that Oct 2015 PR, and our scripts can't realistically scrape domains from a regex, it could be better to have Postfix use the correct tables to lookup directly when it needs to, which would allow for the alias regex.

Postfix provides separate config to check for accounts and aliases regarding if it should be considered the final destination for delivery of mail received/sent. We could better leverage that support directly. It would also allow for users to more easily opt-out domains I think, should they really need to.

georglauterbach commented 2 years ago

Nice! We could make people that want to start working on DMS guide to this issue as a nice "beginner-friendly" way of getting to know the project.

polarathene commented 1 year ago

Potentially useful notes related to this topic when revisiting this issue.


smtpd_sasl_auth_enable - Port 25 no longer supports SASL auth for submitting mail for Postfix to send. Potentially relevant to tracking history of SPOOF_PROTECTION=1 feature (which only applies to SASL).


Recently SPOOF_PROTECTION=1 being changed to default for the v12 release was proposed.

This issue was cited to discourage that for the time being due to variety of config changes already introduced into v12 and the concerns raised around the SPOOF_PROTECTION feature support in this issue above.


This may be useful to migrate into technical documentation for maintainers in future.

The SPOOF_PROTECTION=1 feature is implemented in Postfix with smtpd_sender_restrictions with any of the rejection policies listed below, presently the feature chooses reject_authenticated_sender_login_mismatch.

smtpd_sender_login_maps example config can be found here in Postfix docs in addition to usage with main.cf:smtpd_sender_restrictions (_docs incorrectly assign to smtpd_recipient_restrictions_). The Additional Notes section above provides more info on the smtpd_sender_login_maps config format and our usage of regex + unionmap config tables with it.

Mail received by Postfix goes through this sequence of restriction checks.


SPOOF_PROTECTION feature code has since migrated to here (with upcoming changes in this PR):

https://github.com/docker-mailserver/docker-mailserver/blob/b451742f0a6c23bd68fcc952fd3f6b6f9d291e34/target/scripts/startup/setup.d/security/spoofing.sh#L13-L28

When someone has time to, the conditionals there can be refactored, as described in the Task Overview at the start of this issue. LDAP perhaps can be updated later if easier.