Skip to content

Commit

Permalink
Feat/ip whitelist (#51)
Browse files Browse the repository at this point in the history
* Implement IP whitelisting feature

Signed-off-by: Ondrej Vasko <ondrej.vaskoo@gmail.com>
  • Loading branch information
Lirt authored Mar 3, 2020
1 parent ae41641 commit 72684f8
Show file tree
Hide file tree
Showing 12 changed files with 180 additions and 85 deletions.
4 changes: 4 additions & 0 deletions anti-spam.conf
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,7 @@ ip_limit = 20
# Flush database records with last login older than 1 day
db_flush_interval = 86400
geoip_db_path = /usr/local/share/GeoIP/GeoIP.dat
# IP whitelist must be valid comma separated strings in CIDR format without whitespaces.
# It specifies IP addresses which will NOT be counted into user logins database.
# ip_whitelist = 198.51.100.0/24,203.0.113.123/32
# ip_whitelist_path = /etc/postfwd/ip_whitelist.txt
3 changes: 2 additions & 1 deletion docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM postfwd/postfwd:v2.00
FROM postfwd/postfwd:v2.02

LABEL maintainer="Postfwd GeoIp Spam Plugin Maintainer <ondrej.vaskoo@gmail.com>"

Expand Down Expand Up @@ -28,6 +28,7 @@ RUN apk --no-cache update \
DBI \
DBD::Pg \
DBD::mysql \
Net::Subnet \
Sys::Mmap \
&& apk del make \
wget \
Expand Down
42 changes: 17 additions & 25 deletions docker/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Supported tags and respective `Dockerfile` links

* [`latest` (Dockerfile)](https://github.com/Vnet-as/postfwd-anti-geoip-spam-plugin/blob/master/docker/Dockerfile)
* [`v1.40` (Dockerfile)](https://github.com/Vnet-as/postfwd-anti-geoip-spam-plugin/blob/v1.40/docker/Dockerfile)
* [`v1.30` (Dockerfile)](https://github.com/Vnet-as/postfwd-anti-geoip-spam-plugin/blob/v1.30/docker/Dockerfile)
* [`v1.21` (Dockerfile)](https://github.com/Vnet-as/postfwd-anti-geoip-spam-plugin/blob/v1.21/docker/Dockerfile)

Expand Down Expand Up @@ -71,36 +72,27 @@ ip_limit = 20
# Flush database records with last login older than 1 day
db_flush_interval = 86400
geoip_db_path = /usr/local/share/GeoIP/GeoIP.dat
# IP whitelist must be valid comma separated strings in CIDR format without whitespaces.
# It specifies IP addresses which will NOT be counted into user logins database.
# ip_whitelist = 198.51.100.0/24,203.0.113.123/32
# ip_whitelist_path = /etc/postfwd/ip_whitelist.txt
```

Second one is postfwd rules configuration. Here is sample configuration:

```bash
# Anti spam botnet rule:
# This example shows how to limit e-mail address defined by `sasl_username`
# to be able to login from max. 5 different countries, otherwise it will
# be blocked from sending messages.

&&PRIVATE_RANGES { \
client_address=!!(10.0.0.0/8) ; \
client_address=!!(172.16.0.0/12) ; \
client_address=!!(192.168.0.0/16) ; \
};
&&LOOPBACK_RANGE { \
client_address=!!(127.0.0.0/8) ; \
};

id=COUNTRY_LOGIN_COUNT ; \
sasl_username=~^(.+)$ ; \
&&PRIVATE_RANGES ; \
&&LOOPBACK_RANGE ; \
incr_client_country_login_count != 0 ; \
action=jump(BAN_BOTNET);

id=BAN_BOTNET ; \
sasl_username=~^(.+)$ ; \
&&PRIVATE_RANGES ; \
&&LOOPBACK_RANGE ; \
client_uniq_country_login_count > 5 ; \
action=rate(sasl_username/1/3600/554 Your mail account ($$sasl_username) was compromised. Please change your password immediately after next login.);
# to be able to login from max. 5 different countries or 20 different IP
# addresses, otherwise it will be blocked from sending messages.

id=BAN_BOTNET_COUNTRY ;
sasl_username=~^(.+)$ ;
client_uniq_country_login_count > 5 ;
action=rate(sasl_username/1/3600/554 Your mail account ($$sasl_username) was compromised. Please change your password immediately after next login.) ;

id=BAN_BOTNET_IP ;
sasl_username=~^(.+)$ ;
client_uniq_ip_login_count > 20 ;
action=rate(sasl_username/1/3600/554 Your mail account ($$sasl_username) was compromised. Please change your password immediately after next login.) ;
```
51 changes: 51 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,57 @@

This changelog notes changes between versions of Postfwd GeoIP Anti-Spam plugin.

## Version 1.40 [2. Mar 2020]

This stable release contains IP whitelisting feature (Reported as bug and requested by @csazku in https://github.com/Vnet-as/postfwd-anti-geoip-spam-plugin/issues/50).

This release has one new CPAN dependency - `Net::Subnet`.

You can specify line `ip_whitelist = 198.51.100.0/24,203.0.113.123/32` in main
configuration file in order to skip incrementing of login count for users
who are logging into email from specified IP addresses.
This list must be comma separated without whitespaces and IP address must always
end with CIDR mask (for plain IPs `/32` must be used).

Alternative version is to read IP whitelist from file, which can be specified in
main configuration file like `ip_whitelist_path = /etc/postfwd/ip_whitelist.txt`
and whitelisting file must have following format (comments start with `#` and
one IP CIDR must be entered per line):

```bash
###
# IP ranges must be in CIDR format with prefix specified
# for all IP addresses with `/<NUM>` notation
###
# Private ranges
10.0.0.0/8
# Whitelisted test IP addresses
198.51.100.0/24
203.0.113.123/32
```

## Version 1.30 - Postfwd3 support, Integration Testing [23. Mar 2019]

This release uses new postfwd docker tag 2.00, which uses new `postfwd3` script.

Postfwd3 changed plugin interface and therefore this release is not compatible with
`postfwd1` and `postfwd2`. If you want to use older postfwd versions, use tag `v1.21`.

Postfwd3 uses Alpine Linux for docker, so the dockerfile had to be rewritten.

To better work with GeoIP database, there is new configuration option `geoip_db_path`,
which defaults to `/usr/local/share/GeoIP/GeoIP.dat`.

There is small change to logging, number of countries and unique IPs is logged on each
request loop.

A lot of rework was done in tests directory. There is docker-compose with postgresql.
Also shell script, which automatically runs docker-compose for both supported databases
and does integration test with sample requests and verification through logs.

Plugin item now exports `request{client_uniq_ip_login_count}`
and `request{client_uniq_country_login_count}` instead of `result*`.

## Version 1.2 [11. Mar 2019]

This stable release has changes mainly in linting, readability and testability, but also
Expand Down
68 changes: 31 additions & 37 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Prebuilt ready-to-use Docker image is located on DockerHub and can be simply pul
```bash
# Postfwd3 tags
docker pull lirt/postfwd-anti-geoip-spam-plugin:latest
docker pull lirt/postfwd-anti-geoip-spam-plugin:v1.30
docker pull lirt/postfwd-anti-geoip-spam-plugin:v1.40
# Postfwd1, Postfwd2 tags
docker pull lirt/postfwd-anti-geoip-spam-plugin:v1.21
```
Expand Down Expand Up @@ -89,7 +89,7 @@ CREATE INDEX postfwd_sasl_username ON postfwd_logins (sasl_username);

- `Postfwd2` or `Postfwd3`.
- Database (`MySQL` or `PostgreSQL`).
- Perl modules - `Geo::IP`, `DBI`, `Time::Piece`, `Config::Any`, `DBD::mysql` or `DBD::Pg`.
- Perl modules - `Geo::IP`, `DBI`, `Time::Piece`, `Config::Any`, `Net::Subnet`, `DBD::mysql` or `DBD::Pg`.
- GeoIP database located in `/usr/local/share/GeoIP/GeoIP.dat`.

#### Dependencies on RedHat based distributions
Expand All @@ -102,7 +102,8 @@ yum install -y 'perl(Geo::IP)' \
'perl(Config::Any)' \
'perl(DBI)' \
'perl(DBD::mysql)' \
'perl(DBD::Pg)'
'perl(DBD::Pg)' \
'perl(Net::Subnet)'
```

#### Dependencies on Debian based distributions
Expand All @@ -116,6 +117,7 @@ apt-get install -y libgeo-ip-perl \
libdbi-perl \
libdbd-mysql-perl \
libdbd-pg-perl \
libnet-subnet-perl \
geoip-database
```

Expand All @@ -125,41 +127,25 @@ Plugin configuration file `anti-spam.conf` is INI style configuration file, in w

### Postfwd configuration

Add following rules to postfwd configuration file `postfwd.cf`. You can use your own message and value of parameter `client_uniq_country_login_count`, which sets maximum number of unique countries to allow user to log in via sasl.
Add following rules to postfwd configuration file `postfwd.cf`. You can use your own message and value of parameters:
- `client_uniq_country_login_count`: Sets maximum number of unique countries to allow user to log in via sasl.
- `client_uniq_ip_login_count`: Sets maximum number of unique IP addresses to allow user to log in via sasl.

```bash
# Anti spam botnet rule:
# This example shows how to limit e-mail address defined by `sasl_username`
# to be able to login from max. 5 different countries, otherwise it will
# be blocked from sending messages.

&&PRIVATE_RANGES { \
client_address=!!(10.0.0.0/8) ; \
client_address=!!(172.16.0.0/12) ; \
client_address=!!(192.168.0.0/16) ; \
};
&&LOOPBACK_RANGE { \
client_address=!!(127.0.0.0/8) ; \
};

id=COUNTRY_LOGIN_COUNT ; \
sasl_username=~^(.+)$ ; \
&&PRIVATE_RANGES ; \
&&LOOPBACK_RANGE ; \
incr_client_country_login_count != 0 ; \
  action=jump(BAN_BOTNET)

id=BAN_BOTNET ; \
sasl_username=~^(.+)$ ; \
&&PRIVATE_RANGES ; \
&&LOOPBACK_RANGE ; \
client_uniq_country_login_count > 5 ; \
action=rate(sasl_username/1/3600/554 Your mail account ($$sasl_username) was compromised. Please change your password immediately after next login.);

id=BAN_BOTNET_IP ; \
sasl_username=~^(.+)$ ; \
client_uniq_ip_login_count > 20 ; \
action=rate(sasl_username/1/3600/554 Your mail account ($$sasl_username): Too many messages from different hosts.);
# to be able to login from max. 5 different countries or 20 different IP
# addresses, otherwise it will be blocked from sending messages.

id=BAN_BOTNET_COUNTRY ;
sasl_username=~^(.+)$ ;
client_uniq_country_login_count > 5 ;
action=rate(sasl_username/1/3600/554 Your mail account ($$sasl_username) was compromised. Please change your password immediately after next login.) ;

id=BAN_BOTNET_IP ;
sasl_username=~^(.+)$ ;
client_uniq_ip_login_count > 20 ;
action=rate(sasl_username/1/3600/554 Your mail account ($$sasl_username) was compromised. Please change your password immediately after next login.) ;
```

### Database backend configuration
Expand All @@ -168,7 +154,7 @@ Update configuration file `/etc/postfix/anti-spam.conf` with your credentials to

In case you use different path as `/etc/postfix/anti-spam.conf` and `/etc/postfix/anti-spam-sql-st.conf` to main configuration file, export environment variables `POSTFWD_ANTISPAM_MAIN_CONFIG_PATH` and `POSTFWD_ANTISPAM_SQL_STATEMENTS_CONFIG_PATH` with your custom path.

```INI
```conf
[database]
# driver = Pg
driver = mysql
Expand All @@ -186,11 +172,17 @@ The plugin is by default configured to remove records for users with last login

Plugin looks by default for GeoIP database file in path `/usr/local/share/GeoIP/GeoIP.dat`. You can override this path in configuration `app.geoip_db_path`.

```INI
You can whitelist set of IP addresses or subnets in CIDR format by using configuration setting `app.ip_whitelist`. Whitelisting means, that if client logs into email account from IP address, which IS in whitelist, it will NOT increment login count for this pair of `sasl_username|client_address`.

```conf
[app]
# Flush database records with last login older than 1 day
db_flush_interval = 86400
geoip_db_path = /usr/local/share/GeoIP/GeoIP.dat
# IP whitelist must be valid comma separated strings in CIDR format without whitespaces.
# It specifies IP addresses which will NOT be counted into user logins database.
ip_whitelist = 198.51.100.0/24,203.0.113.123/32
# ip_whitelist_path = /etc/postfwd/ip_whitelist.txt
```

## Logging
Expand All @@ -201,7 +193,7 @@ You can disable logging completely by updating value of statement `debug` to `0`

Example configuration of file `anti-spam.conf`:

```INI
```conf
[logging]
# Remove statement `logfile`, or set it to empty `logfile = ` to log into STDOUT
logfile = /var/log/postfwd_plugin.log
Expand All @@ -212,6 +204,8 @@ autoflush = 0
debug = 1
# Make log after exceeding unique country count limit
country_limit = 5
# Make log after exceeding unique ip count limit
ip_limit = 20
```

If you use `logrotate` to rotate anti-spam logs, use option `copytruncate` which prevents [logging errors](https://github.com/Vnet-as/postfwd-anti-geoip-spam-plugin/issues/6) when log file is rotated.
Expand Down
44 changes: 44 additions & 0 deletions postfwd-anti-spam.plugin
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ use DBI;
use IO::Handle;
use Time::Piece;

# For subnet matching and IP address validation
use Net::Subnet;

# Configure default path to configuration files
# or read them from environment variables
my $cfg_anti_spam_path = '/etc/postfix/anti-spam.conf';
Expand Down Expand Up @@ -93,6 +96,42 @@ else {
mylog_info("Logging destination is file '$config{logging}{logfile}'");
}

# IP WHITELIST
# Do not whitelist any IP addresses by default
my $ip_whitelist = subnet_matcher qw(
255.255.255.255/32
);

# Make sure that either ip_whitelist or ip_whitelist_path are used in configuration file
if ( ($config{app}{ip_whitelist} || length $config{app}{ip_whitelist}) &&
($config{app}{ip_whitelist_path} || length $config{app}{ip_whitelist_path}) ) {
mylog_fatal('Both "ip_whitelist" and "ip_whitelist_path" are defined! Please choose only one method of whitelisting.');
}
# Set whitelist according to config variable ip_whitelist
if ( $config{app}{ip_whitelist} || length $config{app}{ip_whitelist} ) {
$ip_whitelist = subnet_matcher(split /,/mxs, $config{app}{ip_whitelist});
mylog_info('IP whitelist set to CIDRs: ', $config{app}{ip_whitelist});
}
# Read list of IP addresses to whitelist from file ip_whitelist_path and set whitelist according to it
if ( $config{app}{ip_whitelist_path} || length $config{app}{ip_whitelist_path} ) {
open my $ip_whitelist_fh, '<:encoding(UTF-8)', $config{app}{ip_whitelist_path} or die "ERROR: Could not open file '$config{app}{ip_whitelist_path}' $ERRNO\n";

my @ip_list;
while (my $row = <$ip_whitelist_fh>) {
chomp $row;
if ( $row =~ m/^\s*\#/msx ) {
next;
}
push @ip_list, $row;
}

$ip_whitelist = subnet_matcher(@ip_list);
close $ip_whitelist_fh or die "ERROR: Could not close file '$config{app}{ip_whitelist_path}' $ERRNO\n";
mylog_info('IP whitelist set to file: ', $config{app}{ip_whitelist_path});
mylog_info('IP CIDRs in whitelist file: ', join(', ', @ip_list));
}


# Geo IP
use Geo::IP;
my $gi = Geo::IP->open($config{app}{geoip_db_path}, GEOIP_MEMORY_CACHE | GEOIP_CHECK_CACHE);
Expand Down Expand Up @@ -176,6 +215,11 @@ my $last_cache_flush = time;
$last_cache_flush = time;
}

# Check if IP address is in whitelist and return from function if yes
if ( $ip_whitelist->($request->{client_address}) ) {
return $result;
}

# Get sasl_username from request
my $user = $request->{sasl_username};
if ( !length $user || !($user) ) {
Expand Down
4 changes: 4 additions & 0 deletions tests/dev-anti-spam-mysql.conf
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,7 @@ ip_limit = 20
# Flush database records with last login older than 1 day
db_flush_interval = 86400
geoip_db_path = /usr/local/share/GeoIP/GeoIP.dat
# IP whitelist must be valid comma separated strings in CIDR format without whitespaces.
# It specifies IP addresses which will NOT be counted into user logins database.
ip_whitelist = 198.51.100.0/24,203.0.113.123/32
# ip_whitelist_path = /etc/postfwd/ip_whitelist.txt
4 changes: 4 additions & 0 deletions tests/dev-anti-spam-postgres.conf
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,7 @@ ip_limit = 20
# Flush database records with last login older than 1 day
db_flush_interval = 86400
geoip_db_path = /usr/local/share/GeoIP/GeoIP.dat
# IP whitelist must be valid comma separated strings in CIDR format without whitespaces.
# It specifies IP addresses which will NOT be counted into user logins database.
ip_whitelist = 198.51.100.0/24,203.0.113.123/32
# ip_whitelist_path = /etc/postfwd/ip_whitelist.txt
5 changes: 5 additions & 0 deletions tests/dev-compose-mysql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ services:
build:
context: ../
dockerfile: docker/Dockerfile
environment:
- PROG=postfwd3
ports:
- "10040:10040"
volumes:
Expand All @@ -28,3 +30,6 @@ services:
- type: bind
source: ./dev-postfwd.cf
target: /etc/postfwd/postfwd.cf
- type: bind
source: ./ip_whitelist.txt
target: /etc/postfwd/ip_whitelist.txt
5 changes: 5 additions & 0 deletions tests/dev-compose-postgresql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ services:
build:
context: ../
dockerfile: docker/Dockerfile
environment:
- PROG=postfwd3
ports:
- "10040:10040"
volumes:
Expand All @@ -26,3 +28,6 @@ services:
- type: bind
source: ./dev-postfwd.cf
target: /etc/postfwd/postfwd.cf
- type: bind
source: ./ip_whitelist.txt
target: /etc/postfwd/ip_whitelist.txt
Loading

0 comments on commit 72684f8

Please sign in to comment.