Docker-mailserver: Applying Limits/Quotas to Outbound Mail

Created on 28 Apr 2020  路  9Comments  路  Source: tomav/docker-mailserver

Context

I have a functional docker-mailserver environment, but I want to know if it's possible (maybe via SpamAssassin?) to apply a daily/hourly/monthly limit to the number of outbound emails from the server. If so, can the limit be applied per user?

Expected Behavior

  • Configure an outbound quota per user/month/hour/etc
  • The MTA respects the configured quotas

Actual Behavior

N/A

Possible Fix

N/A

Steps to Reproduce

N/A

Your Environment

N/A

question

All 9 comments

Using PostFWD, it should be achievable.
As a dirty hack, you could overwrite the entrypoint and install PostFWD and add it to supervisorctl services.
I would recommend forking this image and adjust it to your needs.

According to PostFWD documentation, the postfix integration uses check_policy:

127.0.0.1:10040_time_limit   = 3600
smtpd_recipient_restrictions = permit_mynetworks
                               reject_unauth_destination
                               check_policy_service inet:127.0.0.1:10040

This message from howtoforge forum is a good starting point for your rules. It does apply limitations per mailbox.

@youtous , thank you for the response! I'll see if I can follow the guidance you've given me and make it happen. I appreciate it.

So @youtous I've taken your advice and integrated PostFWD, and it seems to be promising so far. However, I'm having problems configuring Postfix. No matter what steps I try, I can't get Postfix to load my configuration properly.

I have created the file ./config/postfix-main.cf

10.5.0.6::10040_time_limit      = 3600
smtpd_recipient_restrictions    = permit_mynetworks
                                  reject_unauth_destination
                                  check_policy_service inet:10.5.0.6::10040
smtpd_sender_restrictions       = permit_mynetworks
                                  reject_unauth_destination
                                  check_policy_service inet:10.5.0.6::10040

The static IP mapping throughout this comment may be unnecessary, but I've done it to remove DNS issues as a suspect

Here's docker-compose.yml:

version: '2'
services:
  mail:
    image: tvial/docker-mailserver:latest
    hostname: ${HOSTNAME}
    domainname: ${DOMAINNAME}
    container_name: ${CONTAINER_NAME}
    ports:
    - "25:25"
    - "143:143"
    - "587:587"
    - "993:993"
    - "4190:4190"
    volumes:
    - /mnt/data/infra/mail/maildata:/var/mail
    - /mnt/data/infra/mail/mailstate:/var/mail-state
    - /mnt/data/infra/mail/maillogs:/var/log/mail
    - ./config/:/tmp/docker-mailserver/
    - /mnt/data/infra/letsencrypt/etc:/etc/letsencrypt
    env_file:
    - .env
    - env-mailserver
    cap_add:
    - NET_ADMIN
    - SYS_PTRACE
    restart: always
    networks:
      mailnet:
        ipv4_address: 10.5.0.5
  postfwd:
    image: postfwd/postfwd:stable
    environment:
      - PROG=postfwd2
      - CACHE=0
      - EXTRA=-vv --no_parent_dns_cache --summary=600
      # - EXTRA=-vv --no_parent_dns_cache --noidlestats --summary=600
    restart: always
    # port mapping may not be necessary since they're on the same network
    # ports:
    #   - 127.0.0.1:10040:10040
    networks:
      mailnet:
        ipv4_address: 10.5.0.6
    volumes:
      - ./postfwd.cf:/etc/postfwd/postfwd.cf:ro
networks:
  mailnet:
    driver: bridge
    ipam:
     config:
       - subnet: 10.5.0.0/16
         gateway: 10.5.0.1

The contents of postfwd.cf are valid, here's a snippet:

id=ip_msg_10min
  action=rate(client_address/30/600/421 4.7.1: $$client_address: sending too fast.)
id=ip_msg_3hr
  client_address!=127.0.0.1
  action=rate(client_address/120/10800/421 4.7.1: $$client_address: too many messages, try later.)
id=ip_msg_24hr
  client_address!=127.0.0.1
  action=rate(client_address/150/86400/421 4.7.1: $$client_address: too many messages, try later.)
id=ip_msg_72hr
  client_address!=127.0.0.1
  action=rate(client_address/300/259200/421 4.7.1: $$client_address: too many messages, try later.)

The mail container says it can't load the config lines for PostFWD+Postfix when I use the postfix-main.cf posted at the top of this comment:

mail       | postconf: fatal: missing '=' after attribute name: "reject_unauth_destination"                                                                      
mail       | postconf: fatal: missing '=' after attribute name: "check_policy_service inet:10.5.0.6::10040"                                                      
mail       | postconf: fatal: missing '=' after attribute name: "reject_unauth_destination"
mail       | postconf: fatal: missing '=' after attribute name: "check_policy_service inet:10.5.0.6::10040"

If I try and condense these into something that Postfix doesn't complain about via postconf, nothing happens at all:

$ docker exec -it mail bash
root@mail:/# postconf smtpd_recipient_restrictions="permit_mynetworks","reject_unauth_destination","check_policy_service inet:10.5.0.6::10040"
root@mail:/# postconf -P | grep -i smtpd_recipient
127.0.0.1:10025/inet/smtpd_recipient_restrictions = permit_mynetworks,reject
root@mail:/# postconf smtpd_recipient_restrictions="reject_unauth_destination"
root@mail:/# postconf -P | grep -i smtpd_recipient
127.0.0.1:10025/inet/smtpd_recipient_restrictions = permit_mynetworks,reject

You can see that it doesn't seem to set the values for the current Postfix config. Am I missing something here? Do you have any thoughts? I appreciate it.

This image uses one line per configuration KV parameter.
https://github.com/tomav/docker-mailserver/blob/master/target/postfix/main.cf#L47

I suspect https://github.com/tomav/docker-mailserver/blob/master/target/start-mailserver.sh#L1213 trying to load each line of ./config/postfix-main.cf as an individual parameter.

Could you try something like:

./config/postfix-main.cf

10.5.0.6::10040_time_limit = 3600
smtpd_recipient_restrictions = permit_mynetworks, reject_unauth_destination, check_policy_service inet:10.5.0.6::10040
smtpd_sender_restrictions = permit_mynetworks, reject_unauth_destination, check_policy_service inet:10.5.0.6::10040

Thanks again @youtous. It seems that your suggestion has helped me get closer to my goal, but PostFWD doesn't seem to be doing anything.

I've got a special test rule to restrict more than 1 email message from coming from the same IP over the last 10 minutes below:

id=ip_msg_10min
  action=rate(client_address/1/600/421 4.7.1: $$client_address: sending too fast.)

I send a test email to another account on the server, with a mail client located at an external IP. I don't see any logging in PostFWD at all and I was able to send many mails through.

The good news is that I've verified that the rule has been applied by running docker exec -it mail bash and inspecting /etc/postfix/main.cf:

main.cf:smtpd_recipient_restrictions = permit_sasl_authenticated, permit_mynetworks, reject_unauth_destination, check_policy_service unix:private/policyd-spf, check_policy_service inet:10.5.0.6::10040, reject_un
auth_pipelining, reject_invalid_helo_hostname, reject_non_fqdn_helo_hostname, reject_unknown_recipient_domain, reject_rbl_client zen.spamhaus.org, reject_rbl_client bl.spamcop.net
main.cf:smtpd_client_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination, reject_unauth_pipelining,check_policy_service inet:10.5.0.6::10040
main.cf:smtpd_sender_restrictions = permit_sasl_authenticated, permit_mynetworks, reject_unknown_sender_domain, check_policy_service inet:10.5.0.6::10040

For reference, I updated the values in postfix-main.cf to:

10.5.0.6::10040_time_limit=3600
smtpd_recipient_restrictions = permit_sasl_authenticated, permit_mynetworks, reject_unauth_destination, check_policy_service unix:private/policyd-spf, check_policy_service inet:10.5.0.6::10040, reject_unauth_pipelining, reject_invalid_helo_hostname, reject_non_fqdn_helo_hostname, reject_unknown_recipient_domain, reject_rbl_client zen.spamhaus.org, reject_rbl_client bl.spamcop.net
smtpd_client_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination, reject_unauth_pipelining,check_policy_service inet:10.5.0.6::10040
smtpd_sender_restrictions = permit_sasl_authenticated, permit_mynetworks, reject_unknown_sender_domain, check_policy_service inet:10.5.0.6::10040

I based these values off of the template values in https://github.com/tomav/docker-mailserver/blob/master/target/postfix/main.cf#L47 and basically added a check_policy_service action in the spots that I thought looked best.

So, it seems the config is definitely working, Do you think it's possible I've misconfigured the order of these check_policy_service configuration parameters maybe? Any suggestions? You've been very helpful, I really can't thank you enough for your help.

I may have gotten it working @youtous !! It turns out that I had two colons in the inet addresses and that was a syntax error. Example:

10.5.0.6::10040_time_limit=3600

should be

10.5.0.6:10040_time_limit=3600

It appears my rule has been processed and run, and it _did_ actually throttle a message successfully. I'll report back if I run into any other problems...

Ok, so this took some tedious work but I finally figured out how to limit outbound emails from my users - example error message from Thunderbird:

An error occurred while sending mail. The mail server responded:
450 4.7.1 <[email protected]>: Sender address rejected: sorry, max 2 requests per hour.
Please check the message recipient "[email protected]" and try again.

In order to ensure that the PostFWD rules _even get run_, you have to check that the order of operations in your postfix-main.cf are valid for what you want. For example, if permit_mynetworks is in front of your check_policy_service action, it might not run since the result of permit_mynetworks returns OK. This is what was happening to me - calls to PostFWD were not running when sending mail with one of my accounts, but it _was_ running when I was receiving mail from an external domain (such as gmail).

I had to shuffle the check_policy_service call high up in the list for each of the options. See my postfix-main.cf below:

10.5.0.6:10040_time_limit=3600
smtpd_recipient_restrictions=reject_unauth_pipelining,check_policy_service inet:10.5.0.6:10040,permit_sasl_authenticated,permit_mynetworks,reject_unauth_destination,check_policy_service unix:private/policyd-spf,reject_invalid_helo_hostname,reject_non_fqdn_helo_hostname,reject_unknown_recipient_domain,reject_rbl_client zen.spamhaus.org,reject_rbl_client bl.spamcop.net
smtpd_client_restrictions=reject_unauth_destination,reject_unauth_pipelining,check_policy_service inet:10.5.0.6:10040,permit_mynetworks,permit_sasl_authenticated,
smtpd_sender_restrictions=reject_unknown_sender_domain,check_policy_service inet:10.5.0.6:10040,permit_sasl_authenticated,permit_mynetworks

And my postfwd.cf now _only has one rule_ to keep things simple:

id=limit_rate ; protocol_state==RCPT
                action=rate(client_address/2/3600/450 4.7.1 sorry, max 2 requests per hour)

From the reading I've done and my limited understanding of Postfix/PostFWD, reject_unauth_pipelining has to go before the check_policy_service action. Aside from that, the check_policy_service call to PostFWD is the next immediate action for each of the smtp_*_restrictions config options above.

Some of these options may not be needed, or could potentially be in dangerously wrong order. Please, anyone reading this, correct my rules if you see anything scary. I'll be refining the rules as time goes on, but this is what I've got for now. Thanks again @youtous for all the help.

It's worth noting that this was the thread that helped figure out the postfix rule ordering issue: https://serverfault.com/a/810085

For example, if permit_mynetworks is in front of your check_policy_service action, it might not run since the result of permit_mynetworks returns OK. This is what was happening to me - calls to PostFWD were not running when sending mail with one of my accounts, but it was running when I was receiving mail from an external domain (such as gmail).

Good to know! Thank you.

Interesting use case and your explanations are well detailed!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

strarsis picture strarsis  路  5Comments

nekrondev picture nekrondev  路  3Comments

Hamsterman picture Hamsterman  路  3Comments

domdorn picture domdorn  路  4Comments

cottonthread picture cottonthread  路  4Comments