Subnetze und IP Adressen extrahieren aus SPF Records (z.B. Office365 oder Google Apps for Business)

Wenn man bei Office365 oder Google Apps for Business einen eigenen Mailserver (Postfix) vorschalten möchte beim versenden/empfangen muss man die Mailserver von Microsoft/Google Whitelisten in den mynetworks bei Postfix.

Das Script löst alle SPF Record includes auf und generiert CIDR Maps die sich in Postfix einbinden lassen.

Beispiel:

max@dev1:~$ python get_subnets_of_spf_record_mynetwoks.py
Working on job office365
Working on job google

Es werden 2 Files erzeugt:

max@dev1:~$ cat /etc/postfix/networks/google 
64.18.0.0/20 OK
64.233.160.0/19 OK
66.102.0.0/20 OK
66.249.80.0/20 OK
72.14.192.0/18 OK
74.125.0.0/16 OK
108.177.8.0/21 OK
173.194.0.0/16 OK
207.126.144.0/20 OK
209.85.128.0/17 OK
216.58.192.0/19 OK
216.239.32.0/19 OK
[2001:4860:4000::]/36 OK
[2404:6800:4000::]/36 OK
[2607:f8b0:4000::]/36 OK
[2800:3f0:4000::]/36 OK
[2a00:1450:4000::]/36 OK
[2c0f:fb50:4000::]/36 OK
172.217.0.0/19 OK
108.177.96.0/19 OK
max@dev1:~/test$ cat /etc/postfix/networks/office365
207.46.101.128/26 OK
207.46.100.0/24 OK
207.46.163.0/24 OK
65.55.169.0/24 OK
157.56.110.0/23 OK
157.55.234.0/24 OK
213.199.154.0/24 OK
213.199.180.0/24 OK
157.56.112.0/24 OK
207.46.51.64/26 OK
157.55.158.0/23 OK
64.4.22.64/26 OK
40.92.0.0/14 OK
40.107.0.0/17 OK
40.107.128.0/17 OK
134.170.140.0/24 OK
[2a01:111:f400::]/48 OK
23.103.128.0/19 OK
23.103.198.0/23 OK
65.55.88.0/24 OK
104.47.0.0/17 OK
23.103.200.0/21 OK
23.103.208.0/21 OK
23.103.191.0/24 OK
216.32.180.0/23 OK
94.245.120.64/26 OK
[2001:489a:2202::]/48 OK

In Posftix werden sie in der main.cf eingebunden:

# ----------------------------------------------------------------------
# My Networks
# ----------------------------------------------------------------------
mynetworks =
        cidr:/etc/postfix/networks/local
        cidr:/etc/postfix/networks/other
        cidr:/etc/postfix/networks/google
        cidr:/etc/postfix/networks/office365

Da sich zwischendurch die Records auch mal ändern können empfiehlt es sich einen Cronjob dafür einzurichten. Ich habe eine Variante mit diff die nur patcht wenn das Resultat nicht null ist.

Das Script lässt sich auch noch für andere Dienste / etc. anpassen:

lookup_spf = {
# Google Apps for Business
"google": {
          "domain": "google.com",
          "file"  : "/etc/postfix/networks/google",
          },

# Office365
"office365": {
          "domain": "spf.protection.outlook.com",
          "file"  : "/etc/postfix/networks/office365",
          },

# Example
"example": {
          "domain": "example.com",
          "file"  : "/etc/postfix/networks/example",
          },

}

Sourcecode:

#!/usr/bin/env python

#
# get_subnets_of_spf_record_mynetwoks.py
# Resolve all known ip addresses from spf record and generate cidr map for postfix
#
# Version 1.0
# Written by Maximilian Thoma (http://www.lanbugs.de)
#
# The generated files can be used in postfix config with for example mynetworks = cidr:/etc/postfix/<generated_file>
#
# This program is free software; you can redistribute it and/or modify it under the terms of the
# GNU General Public License as published by the Free Software Foundation;
# either version 2 of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with this program;
# if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, 
# MA 02110, USA
#

#
# Requirements:
# dnspython module  -> pip install dnspython
#

import dns.resolver
from dns.exception import DNSException
import re
import sys

# Look for DNS Record at:
#
# "jobname": {
#            "domain": "domainname",
#            "file": "output_file",
#            }
#

# 

lookup_spf = {
# Google Apps for Business
"google": {
          "domain": "google.com",
          "file"  : "/etc/postfix/networks/google",
          },

# Office365
"office365": {
          "domain": "spf.protection.outlook.com",
          "file"  : "/etc/postfix/networks/office365",
          },
}

############################################################################################

def getspf(record, filehandler):
    # Init Resolver
    myResolver = dns.resolver.Resolver()

    try:
        # Try to lookup TXT record
        myAnswer = myResolver.query(record,"TXT")

    except DNSException:
        sys.stderr.write("Failed to query record, SPF broken.")
        return

    results = []

    for rdata in myAnswer:
        # Get string out of records
        for txt_string in rdata.strings:
            # Append to SPF Records buffer if "spf" in string
            if "spf" in txt_string:
                results.append(txt_string)

    # If results >=1
    if len(results) >= 1:
        # Work on records
        for spf in results:
            # Split parts
            parts = spf.split(" ")
            # Check parts
            for part in parts:

                s_include = re.match(r"^include:(?P<domain>.*)$", part)
                s_ip4 = re.match(r"^ip4:(?P<ip4>.*)$", part)
                s_ip6 = re.match(r"^ip6:(?P<ip6>.*)$", part)

                # If in part "include" found, next round
                if s_include:
                    getspf(s_include.group('domain'), filehandler)
                # elif ip4 found
                elif s_ip4:
                    filehandler.write(s_ip4.group('ip4') + " OK\n")
                # elif ip6 found
                elif s_ip6:
                    filehandler.write("[" + s_ip6.group('ip6').replace("/","]/") + " OK\n")
                # else no valid record
                else:
                    pass
    # no results 
    else:
        sys.stderr.write("No results")
        pass

def main():
    # Working on jobs
    for jobname, config in lookup_spf.iteritems():

        print "Working on job %s" % jobname

        # open file
        filehandler = open(config['file'], 'w')
        # start query spf records
        getspf(config['domain'], filehandler)
        # close file
        filehandler.close()


#getspf(lookup_spf)

if __name__ == "__main__":
    main()

 

Postfix Mails aus der Queue von bestimmten Absendern löschen

Mit diesen Kommandos lassen sich aus der Postfix Queue Mails löschen die dort wegen Zustellungsproblemen liegengeblieben sind. Besonders wenn man einen Kunden drauf hat mit Malware Infektion und man die Reste aus der Queue beseitigen möchte.

Für die komplette Domain:

postqueue -p | tail -n +2 | awk 'BEGIN { RS = "" } /@example\.com/ { print $1 }' | tr -d '*!' | postsuper -d -

Für einen einzelnen Absender:

postqueue -p | tail -n +2 | awk 'BEGIN { RS = "" } /mail@example\.com/ { print $1 }' | tr -d '*!' | postsuper -d -

 

Postfix: Mail-Relay mit SMTP-Auth via Submission/TLS für ausgehende Mails

Ein Server soll als Relayhost (oder in der Windowswelt auch Smarthost gennant) dienen. Wer keine feste IP Adresse mit passenden DNS Reverse Eintrag hat wird schlechte Chancen haben das ein richtig konfigurierter Mailserver die Mails annehmen wird. Die Lösung ist die Mails an einen anderen Relayhost oder Mailserver zu schicken und dieser stellt dann die Mails zu. Manchmal bieten die Provider selbst Relayhosts an in vielen Fällen bleibt nichts anderes übrig als das ganze über ein Postfach zu versenden. Einige Provider bieten das Anliefern von Mails nur noch über Submission (TCP/587) und TLS verschlüsselt an. Ist mir persönlich auch lieber …

Beispielszenario

Provider: example.net

Mailaccount: user1@example.net Password: example%1

Mailserver: mail.example.net

Submission (TCP/587) und TLS sind notwendig.

 Lösung

„/etc/postfix/main.cf“ ergänzen mit:

relayhost = mail.example.net:submission
smtp_use_tls=yes
smtp_sasl_auth_enable = yes
smtp_sasl_password_maps = hash:/etc/postfix/smtp_auth
smtp_sasl_security_options = noanonymous, noplaintext
smtp_sasl_tls_security_options = noanonymous

/etc/postfix/smtp_auth (Map-Datei, muss initialisiert werden!!!)

mail.example.net:submission               user1@example.net:example%1

Folgende Kommandos sind noch auszuführen:

postmap /etc/postfix/smtp_auth

durchführen um die Datei zu mappen.

/etc/init.d/postfix restart

durchführen um die Änderungen zu aktivieren.

Python Version von getadsmtp.pl

Übersetzung des AD Mailadressen Sammelskripts von Perl nach Python.

#!/usr/bin/python

# getadsmtp.py
# Version 1.0
# The script is an translation from the orginal perl script getadsmtp.pl

# This script will pull all users' SMTP addresses from your Active Directory
# (including primary and secondary email addresses) and list them in the
# format "user@example.com OK" which Postfix uses with relay_recipient_maps.
# Be sure to double-check the path to python above.

# This requires python-ldap to be installed.  To install python-ldap on debian based systems,
# at a shell type "apt-get install python-ldap" or "sudo apt-get install python-ldap"

import os, sys, ldap

# Enter the path/file for the output
valid_addresses = "/etc/postfix/example_recipients"

# Enter the FQDN of your Active Directory domain controllers below
dc1="dc01.example.com"
dc2="dc02.example.com"

# Enter the LDAP container for your userbase.
# The syntax is CN=Users,dc=example,dc=com
# This can be found by installing the Windows 2000 Support Tools
# then running ADSI Edit.
# In ADSI Edit, expand the "Domain NC [domaincontroller1.example.com]" &amp;
# you will see, for example, DC=example,DC=com (this is your base).
# The Users Container will be specified in the right pane as
# CN=Users depending on your schema (this is your container).
# You can double-check this by clicking "Properties" of your user
# folder in ADSI Edit and examining the "Path" value, such as:
# LDAP://domaincontroller1.example.com/CN=Users,DC=example,DC=com
# which would be hqbase="cn=Users,dc=example,dc=com"
# Note:  You can also use just hqbase="dc=example,dc=com"
hqbase="cn=Users,dc=example,dc=com"

# Enter the username &amp; password for a valid user in your Active Directory
# with username in the form cn=username,cn=Users,dc=example,dc=com
# Make sure the user's password does not expire.  Note that this user
# does not require any special privileges.
# You can double-check this by clicking "Properties" of your user in
# ADSI Edit and examining the "Path" value, such as:
# LDAP://domaincontroller1.example.com/CN=user,CN=Users,DC=example,DC=com
# which would be $user="cn=user,cn=Users,dc=example,dc=com"
# Note: You can also use the UPN login: "user@example.com"
user="cn=user,cn=Users,dc=example,dc=com"
passwd="password"

try:
  l = ldap.initialize("ldap://%s" %(dc1))
  l.set_option(ldap.OPT_REFERRALS, 0)
  l.protocol_version = 3
  l.simple_bind_s(user, passwd)

except ldap.LDAPError, e:
  try:
    l = ldap.initialize("ldap://%s" %(dc2))
    l.set_option(ldap.OPT_REFERRALS, 0)
    l.protocol_version = 3
    l.simple_bind_s(user, passwd)

  except ldap.LDAPError, e:
    print "Error connecting to specified domain controllers\n"
    sys.exit()

# Play around with this to grab objects such as Contacts, Public Folders, etc.
# A minimal filter for just users with email would be:
# filter = "(&amp;(sAMAccountName=*)(mail=*))"
filter = "(&amp; (mailnickname=*) (| (&amp;(objectCategory=person)(objectClass=user)(!(homeMDB=*))(!(msExchHomeServerName=*)))(&amp;(objectCategory=person)(objectClass=user)(|(homeMDB=*)(msExchHomeServerName=*)))(&amp;(objectCategory=person)(objectClass=contact))(objectCategory=group)(objectCategory=publicFolder)(objectClass=msExchDynamicDistributionList) ))"

attrs = ["proxyAddresses"]
scope = ldap.SCOPE_SUBTREE

r = l.search(hqbase, scope, filter, attrs)
type,a = l.result(r)
result_set = []

for x in a:
  name,attrs = x
  if hasattr(attrs, 'has_key') and attrs.has_key('proxyAddresses'):
    proxyAddresses = attrs['proxyAddresses']
    for y in proxyAddresses:
      result_set.append("%s OK" %(y.replace("smtp:","").replace("SMTP:","")))

# Add additional restrictions, users, etc. to the output file below.
#result_set.append("user@example.com OK")
#result_set.append("user1@example.com 550 User unknown.")
#result_set.append("bad.example.com 550 User does not exist.")

#######################################################################
# Build file ...
output = file(valid_addresses,'w')

for line in result_set:
  output.write("%s\n" %(line))

output.close()

 

Postfix als Frontend für einen Microsoft Exchange Server

Für einen Kunden habe ich einen Postfix eingerichtet als Frontend Server für seine Exchange Infrastruktur. Die Mailadressen werden automatisch aus dem Active Directory ausgelesen und in einem Hash-File gespeichert. Vorteil ist das der Postfix so nur gültige Mailaliases zulässt. Der Frontend macht auch noch weitere Überprüfungen. (postgrey, policy-weight, etc.)

Folgendes Script wird für das Auslesen der Aliase aus dem AD verwendet:

#!/usr/bin/perl -T -w

# Version 1.02

# This script will pull all users' SMTP addresses from your Active Directory
# (including primary and secondary email addresses) and list them in the
# format "user@example.com OK" which Postfix uses with relay_recipient_maps.
# Be sure to double-check the path to perl above.

# This requires Net::LDAP to be installed.  To install Net::LDAP, at a shell
# type "perl -MCPAN -e shell" and then "install Net::LDAP"

use Net::LDAP;
use Net::LDAP::Control::Paged;
use Net::LDAP::Constant ( "LDAP_CONTROL_PAGED" );

# Enter the path/file for the output
$VALID = "/etc/postfix/example_recipients";

# Enter the FQDN of your Active Directory domain controllers below
$dc1="domaincontroller1.example.com";
$dc2="domaincontroller2.example.com";

# Enter the LDAP container for your userbase.
# The syntax is CN=Users,dc=example,dc=com
# This can be found by installing the Windows 2000 Support Tools
# then running ADSI Edit.
# In ADSI Edit, expand the "Domain NC [domaincontroller1.example.com]" &
# you will see, for example, DC=example,DC=com (this is your base).
# The Users Container will be specified in the right pane as
# CN=Users depending on your schema (this is your container).
# You can double-check this by clicking "Properties" of your user
# folder in ADSI Edit and examining the "Path" value, such as:
# LDAP://domaincontroller1.example.com/CN=Users,DC=example,DC=com
# which would be $hqbase="cn=Users,dc=example,dc=com"
# Note:  You can also use just $hqbase="dc=example,dc=com"
$hqbase="cn=Users,dc=example,dc=com";

# Enter the username & password for a valid user in your Active Directory
# with username in the form cn=username,cn=Users,dc=example,dc=com
# Make sure the user's password does not expire.  Note that this user
# does not require any special privileges.
# You can double-check this by clicking "Properties" of your user in
# ADSI Edit and examining the "Path" value, such as:
# LDAP://domaincontroller1.example.com/CN=user,CN=Users,DC=example,DC=com
# which would be $user="cn=user,cn=Users,dc=example,dc=com"
# Note: You can also use the UPN login: "user\@example.com"
$user="cn=user,cn=Users,dc=example,dc=com";
$passwd="password";

# Connecting to Active Directory domain controllers
$noldapserver=0;
$ldap = Net::LDAP->new($dc1) or
   $noldapserver=1;
if ($noldapserver == 1)  {
   $ldap = Net::LDAP->new($dc2) or
      die "Error connecting to specified domain controllers $@ \n";
}

$mesg = $ldap->bind ( dn => $user,
                     password =>$passwd);
if ( $mesg->code()) {
    die ("error:", $mesg->code(),"\n","error name: ",$mesg->error_name(),
        "\n", "error text: ",$mesg->error_text(),"\n");
}

# How many LDAP query results to grab for each paged round
# Set to under 1000 for Active Directory
$page = Net::LDAP::Control::Paged->new( size => 990 );

@args = ( base     => $hqbase,
# Play around with this to grab objects such as Contacts, Public Folders, etc.
# A minimal filter for just users with email would be:
# filter => "(&(sAMAccountName=*)(mail=*))"
         filter => "(& (mailnickname=*) (| (&(objectCategory=person)
                    (objectClass=user)(!(homeMDB=*))(!(msExchHomeServerName=*)))
                    (&(objectCategory=person)(objectClass=user)(|(homeMDB=*)
                    (msExchHomeServerName=*)))(&(objectCategory=person)(objectClass=contact))
                    (objectCategory=group)(objectCategory=publicFolder)(objectClass=msExchDynamicDistributionList) ))",
          control  => [ $page ],
          attrs  => "proxyAddresses",
);

my $cookie;
while(1) {
  # Perform search
  my $mesg = $ldap->search( @args );

# Filtering results for proxyAddresses attributes
  foreach my $entry ( $mesg->entries ) {
    my $name = $entry->get_value( "cn" );
    # LDAP Attributes are multi-valued, so we have to print each one.
    foreach my $mail ( $entry->get_value( "proxyAddresses" ) ) {
     # Test if the Line starts with one of the following lines:
     # proxyAddresses: [smtp|SMTP]:
     # and also discard this starting string, so that $mail is only the
     # address without any other characters...
     if ( $mail =~ s/^(smtp|SMTP)://gs ) {
       push(@valid, $mail." OK\n");
     }
    }
  }

  # Only continue on LDAP_SUCCESS
  $mesg->code and last;

  # Get cookie from paged control
  my($resp)  = $mesg->control( LDAP_CONTROL_PAGED ) or last;
  $cookie    = $resp->cookie or last;

  # Set cookie in paged control
  $page->cookie($cookie);
}

if ($cookie) {
  # We had an abnormal exit, so let the server know we do not want any more
  $page->cookie($cookie);
  $page->size(0);
  $ldap->search( @args );
  # Also would be a good idea to die unhappily and inform OP at this point
     die("LDAP query unsuccessful");
}
# Only write the file once the query is successful
open VALID, ">$VALID" or die "CANNOT OPEN $VALID $!";
print VALID @valid;
# Add additional restrictions, users, etc. to the output file below.
#print VALID "user\@example.com OK\n";
#print VALID "user1\@example.com 550 User unknown.\n";
#print VALID "bad.example.com 550 User does not exist.\n";

close VALID;

Quelle: http://www-personal.umich.edu/~malth/gaptuning/postfix/

Ich habe das Perlscript in /etc/postfix abgelegt. Rechte sollten auch 700 sein. Diese 6 Parameter müssen geändert werden.

$VALID = "/etc/postfix/exchange_recipients";

$dc1="dc01.lanbugs.intra";
$dc2="dc02.lanbugs.intra";

$hqbase="cn=Users,dc=lanbugs,dc=intra";

$user="cn=sys-read-aliases-mailfront,cn=Users,dc=lanbugs,dc=intra";
$passwd="geheim123";

Das Script wird als Cronjob jede Stunde ausgeführt. Hierzu muss nur ein File mit dem Namen run_read_aliases_from_exchange.sh mit den Rechten 700 im Verzeichnis /etc/cron.hourly angelegt werden. (Debian/Ubuntu)

run_read_aliases_from_exchange.sh:

#!/bin/sh

cd /etc/postfix ; ./getadsmtp.pl && postmap exchange_recipients

In der /etc/postfix/main.cf kann dann das neue Hash-File als relay_recipient_map verwendet werden.

main.cf:

...
relay_recipient_maps = hash:/etc/postfix/exchange_recipients
relay_domains = lanbugs.de
transport_maps = hash:/etc/postfix/transport
mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128 10.1.1.20/32
...

relay_domains erlaubt allgemein die Domain lanbugs.de. mynetworks wurde noch um die IP-Adresse des Exchange Servers erweitert damit dieser E-Mails zur Außenwelt senden kann. Die transport_maps sorgen dafür dass alle Mails an lanbugs.de an den Exchange Server weiter transportiert werden.

/etc/postfix/transport: (nach dem Anlegen postmap /etc/postfix/transport nicht vergessen!!!!)

langbugs.de      smtp:[10.1.1.20]:25

10.1.1.20 ist der interne Exchange Server.

Viel Spaß mit dem Postfix Frontend 😉

MySQL Multi Policy Server for Postfix

MySQL Multi Policy Server befindet sich noch im BETA Status bzw. Entwicklungsphase.

Download komplett unter → Downloads

Status

  • Postgrey funktioniert soweit schon. (Valid ist noch nicht fertig.)
  • Autoresponder funktioniert.

Folgende Funktionen soll MMPS dann können wenn es fertig ist

  • Postgrey per User / Domain aktivieren / deaktivieren.
  • Delay per User / Domain
  • Valid per User / Domain

Datenbank Layout

CREATE TABLE `autoresponder_mail` (
  `id` int(11) NOT NULL auto_increment,
  `sender` varchar(255) NOT NULL,
  `sender_name` varchar(255) NOT NULL,
  `subject` varchar(255) NOT NULL,
  `message` text NOT NULL,
  `von_timestamp` int(11) NOT NULL,
  `bis_timestamp` int(11) NOT NULL,
  PRIMARY KEY  (`id`),
  KEY `sender` (`sender`)
) TYPE=MyISAM  COMMENT='Autoresponder Tabelle' AUTO_INCREMENT=2 ;

--
-- Daten für Tabelle `autoresponder_mail`
-- 

INSERT INTO `autoresponder_mail` (`id`, `sender`, `sender_name`, `subject`, `message`, `von_timestamp`, `bis_timestamp`) VALUES
(1, ' test@example.com', 'TEST HELIX', 'Bin nicht im Büro', 'Hallo,\r\n\r\nich bin zurzeit nicht im Büro.\r\n\r\nAb 30. Mai 2008 bin ich wieder da.\r\n\r\nGruß\r\n\r\nMaximilian Thoma\r\n\r\näüö?ß', 0, 999999999);

CREATE TABLE `black_mail` (
  `id` int(11) NOT NULL auto_increment,
  `recipient` varchar(255) NOT NULL,
  `sender` varchar(255) NOT NULL,
  `aktiv` int(2) NOT NULL default '1',
  PRIMARY KEY  (`id`)
) TYPE=MyISAM COMMENT='Blacklist' AUTO_INCREMENT=1 ;

CREATE TABLE `grey_active` (
  `id` int(11) NOT NULL auto_increment,
  `client_address` varchar(255) NOT NULL,
  `recipient` varchar(255) NOT NULL,
  `sender` varchar(255) NOT NULL,
  `timestamp` int(11) NOT NULL,
  PRIMARY KEY  (`id`),
  KEY `recipient` (`recipient`,`sender`)
) TYPE=MyISAM  COMMENT='Aktiven Greylistenings' AUTO_INCREMENT=38 ;

CREATE TABLE `grey_mail` (
  `id` int(11) NOT NULL auto_increment,
  `name` varchar(255) NOT NULL COMMENT 'Domain oder Mailadresse',
  `delay` int(11) NOT NULL COMMENT 'Verzögerung bis es durch Greylistening freigegeben wird',
  `valid` int(11) NOT NULL COMMENT 'Wie lange gilt die Freigabe in Sekunden',
  PRIMARY KEY  (`id`),
  KEY `name` (`name`)
) TYPE=MyISAM  COMMENT='User/Domains für die Greylistening aktiviert ist' AUTO_INCREMENT=3 ;

--
-- Daten für Tabelle `grey_mail`
-- 

INSERT INTO `grey_mail` (`id`, `name`, `delay`, `valid`) VALUES
(1, ' postmaster@example.com', 60, 3600),
(2, ' test@example.com', 15, 3600);

CREATE TABLE `pps_log` (
  `id` int(11) NOT NULL auto_increment,
  `sender` varchar(255) NOT NULL,
  `recipient` varchar(255) NOT NULL,
  `client_address` varchar(255) NOT NULL,
  `timestamp` int(11) NOT NULL,
  `comment` varchar(255) NOT NULL,
  PRIMARY KEY  (`id`)
) TYPE=MyISAM COMMENT='MTHPPS LOG' AUTO_INCREMENT=1 ;

CREATE TABLE `white_mail` (
  `id` int(11) NOT NULL auto_increment,
  `recipient` varchar(255) NOT NULL,
  `sender` varchar(255) NOT NULL,
  `aktiv` int(2) NOT NULL default '1',
  PRIMARY KEY  (`id`)
) TYPE=MyISAM COMMENT='Whitelist' AUTO_INCREMENT=1 ;

PHP Script

<?php
 
// MySQL Multi Policy Server V.1.0
// Maximilian Thoma
// http://www.thoma.cc
// info@thoma.cc
//////////////////////////////////////////////////////////////////////////////////////
// OPTIONS
//////////////////////////////////////////////////////////////////////////////////////
 
$mysqlhost="localhost";
$mysqluser="mps";
$mysqlpass="PASSWORD";
$mysqldb="multipolicyserver";
 
 
//////////////////////////////////////////////////////////////////////////////////////
// Ab hier nichts mehr ändern !
//////////////////////////////////////////////////////////////////////////////////////
 
//////////////////////////////////////////////////////////////////////////////////////
// DB Class
//////////////////////////////////////////////////////////////////////////////////////
class db {
 
 var $link_id  = 0;
 var $query_id = 0;
 var $record   = array();
 
 var $errdesc    = "";
 var $errno   = 0;
 var $show_error = 0;
 
 var $server   = "";
 var $user     = "";
 var $password = "";
 var $database = "";
 
 var $appname  = "MySQL Postgrey Policy Server";
 
 function db($server,$user,$password,$database) {
 $this->server=$server;
 $this->user=$user;
 $this->password=$password;
 $this->database=$database;
 $this->connect();
 }
 
 function connect() {
 $this->link_id=mysql_connect($this->server,$this->user,$this->password);
 if (!$this->link_id) $this->print_error("Link-ID == false, connect failed");
 if ($this->database!="") $this->select_db($this->database);
 }
 
 function geterrdesc() {
 $this->error=mysql_error();
 return $this->error;
 }
 
 function geterrno() {
 $this->errno=mysql_errno();
 return $this->errno;
 }
 
function select_db($database="") {
 if ($database!="") $this->database=$database;
 if(!@mysql_select_db($this->database, $this->link_id)) $this->print_error("cannot use database ".$this->database);
 }
 
 function query($query_string) {
 global $query_count;
 //echo $query_string."<br>";
 $query_count++;
 
 $this->query_id = mysql_query($query_string,$this->link_id);
 if (!$this->query_id) $this->print_error("Invalid SQL: ".$query_string);
 return $this->query_id;
 }
 
 function fetch_array($query_id=-1) {
 if ($query_id!=-1) $this->query_id=$query_id;
 $this->record = mysql_fetch_array($this->query_id);
 return $this->record;
 }
 
 function fetch_row($query_id=-1) {
 if ($query_id!=-1) $this->query_id=$query_id;
 $this->record = mysql_fetch_row($this->query_id);
 return $this->record;
 }
 
 function fetch_object($query_id=-1) {
 if ($query_id!=-1) $this->query_id=$query_id;
 $this->record = mysql_fetch_object($this->query_id);
 return $this->record;
 }
 
 
 function free_result($query_id=-1) {
 if ($query_id!=-1) $this->query_id=$query_id;
 return @mysql_free_result($this->query_id);
 }
 
 function query_first($query_string) {
 $this->query($query_string);
 $returnarray=$this->fetch_array($this->query_id);
 $this->free_result($this->$query_id);
 return $returnarray;
 }
 
 function num_rows($query_id=-1) {
 if ($query_id!=-1) $this->query_id=$query_id;
 return mysql_num_rows($this->query_id);
 }
 
 function num_fields($query_id=-1) {
 if ($query_id!=-1) $this->query_id=$query_id;
 return mysql_num_fields($this->query_id);
 }
 
 function field_name($query_id=-1,$num) {
 if ($query_id!=-1) $this->query_id=$query_id;
 return mysql_field_name($this->query_id,$num);
 }
 
 function insert_id() {
 return mysql_insert_id($this->link_id);
 }
 
 function print_error($errormsg) {
 $this->errdesc=mysql_error();
 $this->errno=mysql_errno();
 $errormsg="Database error in $this->appname: $errormsg\n<br>";
 $errormsg.="mysql error: $this->errdesc\n<br>";
 $errormsg.="mysql error number: $this->errno\n<br>";
 $errormsg.="Date: ".date("d.m.Y @ H:i")."\n<br>";
 $errormsg.="Script: ".getenv("REQUEST_URI")."\n<br>";
 $errormsg.="Referer: ".getenv("HTTP_REFERER")."\n<br><br>";
 
 if($this->show_error) $errormsg = "$errormsg";
 else $errormsg = "\n $errormsg \n";
 die("</table>Database Error!\n".$errormsg);
 }
}
//////////////////////////////////////////////////////////////////////////////////////
 
//////////////////////////////////////////////////////////////////////////////////////
// DB INIT
//////////////////////////////////////////////////////////////////////////////////////
$db = new db($mysqlhost,$mysqluser,$mysqlpass,$mysqldb);
 
//////////////////////////////////////////////////////////////////////////////////////
// STDIN
//////////////////////////////////////////////////////////////////////////////////////
if ($fp=fopen("php://stdin","r")) {
 
 while($stop!=1){
 
 $line = fgets($fp,512);
 $teile = explode("=", $line);
 $stdin[$teile[0]]=$teile[1];
 // Entfernen von Leerzeilen und Zeilenumbrüchen
 $stdin[$teile[0]] = preg_replace("/\r|\n/s", "", $stdin[$teile[0]]);
 if($line=="\n"){$stop=1;}
 
 }
fclose($fp);
}
 
//////////////////////////////////////////////////////////////////////////////////////
 
 // LOG
 //$zeit=time();
 //$db->query("INSERT INTO pps_log (zeit, recipient, sender, client_address) VALUES ('$zeit', '$stdin[recipient]', '$stdin[sender]', '$stdin[client_address]')");
 
 // Mailadressen zerlegen
 
 $sender_expl=explode('@',$stdin[sender]);
 $sender_domain=$sender_expl[1];
 
 $recipient_expl=explode('@',$stdin[recipient]);
 
 ////////////////////////////////////////////////////////////////////////////////
/// WHITELIST
////////////////////////////////////////////////////////////////////////////////
 
 
////////////////////////////////////////////////////////////////////////////////
/// BLACKLIST
////////////////////////////////////////////////////////////////////////////////
 
 
 
////////////////////////////////////////////////////////////////////////////////
/// POSTGREYLISTENING
////////////////////////////////////////////////////////////////////////////////
 // Überprüfen ob Domain oder User überhaupt mthpostgrey verwendet ?
 
$grey_req1=$db->query("SELECT * FROM grey_mail WHERE name='$stdin[recipient]'");
$grey_fet=$db->fetch_array($grey_req1);
$grey_sec=$grey_fet[delay];
 
 $user_on=$db->num_rows($grey_req1);
 
 if($user_on==0){
 $action="DUNNO";
 $stdout = fopen('php://stdout', 'w');
 fwrite($stdout,"action=$action\n\n");
 fclose($stdout);
 exit;
 } else {
 $grey_test1_q=$db->query("SELECT * FROM grey_active WHERE sender='$stdin[sender]' AND recipient='$stdin[recipient]' AND client_address='$stdin[client_address]'");
 $grey_test1_r=$db->num_rows($grey_test1_q);
 
 if($grey_test1_r==0){
 $action="defer_if_permit 1 MySQL Multi Policy Server is active. Refer to http://www.thoma.cc/  - Greylisted for: ".$wartezeit1." Seconds.";
 $stdout = fopen('php://stdout', 'w');
 fwrite($stdout,"action=$action\n\n");
 fclose($stdout);
 $a_time=time();
 $db->query("INSERT INTO grey_active (sender, recipient, client_address,timestamp) VALUES ('$stdin[sender]','$stdin[recipient]','$stdin[clie$
 exit;
 } else {
 $grey_test2_q=$db->query("SELECT * FROM grey_active WHERE sender='$stdin[sender]' AND recipient='$stdin[recipient]' AND client_address='$stdin[client_address]'");
 $grey_test3_r=$db->fetch_array($db->query("SELECT * FROM grey_mail WHERE name='$stdin[recipient]'"));
 $delay_time=$grey_test3_r[delay];
 //$valid_time=$grey_test3_r[valid];
 $grey_test2_r=$db->fetch_array($grey_test2_q);
 $first_time=$grey_test2_r[timestamp];
 $b_time=time();
 
 //$time_valid_max=$b_time+$valid_time;
 $time_delay=$b_time+$delay_time;
 // Überprüfen ob Delay Time schon vorbei ist
 if($delay_time<(time()-$first_time)){
 $action="DUNNO";
 $action2="PREPEND X-MTHPPS: Greylistening Filter active.";
 $stdout = fopen('php://stdout', 'w');
 fwrite($stdout,"action=$action2\n\n");
 fwrite($stdout,"action=$action\n\n");
 fclose($stdout);
 } else {
 $wartezeit1=$delay_time-(time()-$first_time);
 $action="defer_if_permit 2 MySQL Multi Policy Server is active. Refer to http://www.thoma.cc/  - Greylisted for: ".$wartezeit1." Seconds.";
 $stdout = fopen('php://stdout', 'w');
 fwrite($stdout,"action=$action\n\n");
 fclose($stdout);
 exit;
 }
 
 }
 
 
 }
 
 
////////////////////////////////////////////////////////////////////////////////
/// AUTORESPONDER
////////////////////////////////////////////////////////////////////////////////
 
 $auto_test1_q=$db->query("SELECT * FROM autoresponder_mail where sender='$stdin[recipient]'");
 $auto_test1_r=$db->num_rows($auto_test1_q);
 
 if($auto_test1_r!=0){
 
 $auto_test1_r2=$db->fetch_array($auto_test1_q);
 
 $empfaenger = $stdin[sender];
 $betreff = $auto_test1_r2[subject];
 $text = $auto_test1_r2[message];
 mail($empfaenger, $betreff, $text, "From: $auto_test1_r2[sender_name] <$auto_test1_r2[sender]>");
 
 }
 
 
 
//////////////////////////////////////////////////////////////////////////////////////
 
//$action="DUNNO";
//$stdout = fopen('php://stdout', 'w');
//fwrite($stdout,"action=$action\n\n");
//fclose($stdout);
 
//////////////////////////////////////////////////////////////////////////////////////
 
 
 
 
?>

Anpassungen main.cf

smtpd_recipient_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_non_fqdn_recipient,   reject_unauth_destination, reject_unauth_pipelining, reject_invalid_hostname, check_policy_service inet:127.0.0.1:9990

Anpassungen master.cf

127.0.0.1:9990 inet n n n – 0 spawn user=phppostgrey argv=/opt/dev_postgrey/bin/php -f /postgrey/postgrey_v4.php

Weniger Spam & Phishing durch Sanesecurity & MSRBL-SPAM für ClamAV

Trotz Amavis & Spamassassin und sehr restriktiver Regeln im Postfix kommen immernoch viele Phishing und Bilder/PDF Spams durch. Ich habe nach einer bequemen Lösung gesucht und bin auf Sanesecurity und MSRBL-SPAM gestoßen. Beide bieten Signaturenfiles für ClamAV an die Spams & Phishings erkennen können. Auf der Webseite von Sanesecurity werden mehrere Scripts angeboten die die Signaturen runterladen und ClamAV reloaden. Diese haben mir aber nicht gefallen deshalb habe ich ein kleineres und einfacheres geschieben das täglich als Cronjob ausgeführt wird.

Lösung

Benötigt wird rsync, gunzip und clamav.

Die Signaturen von ClamAV liegen bei Debian,Ubuntu,Gentoo,etc. in

/var/lib/clamav

Folgendes Script liegt in /etc/cron.daily und heisst get_sane:

cd /tmp
wget http://www.sanesecurity.co.uk/clamav/scamsigs/scam.ndb.gz
wget http://www.sanesecurity.co.uk/clamav/phishsigs/phish.ndb.gz

rsync rsync://rsync.mirror.msrbl.com/msrbl/MSRBL-Images.hdb /tmp/MSRBL-Images.hdb
rsync rsync://rsync.mirror.msrbl.com/msrbl/MSRBL-SPAM.ndb /tmp/MSRBL-SPAM.ndb

gunzip -f scam.ndb.gz
gunzip -f phish.ndb.gz
cp -f scam.ndb /var/lib/clamav
cp -f phish.ndb /var/lib/clamav
cp -f MSRBL-Images.hdb /var/lib/clamav
cp -f MSRBL-SPAM.ndb /var/lib/clamav

chown clamav:clamav /var/lib/clamav/scam.ndb
chown clamav:clamav /var/lib/clamav/phish.ndb
chown clamav:clamav /var/lib/clamav/MSRBL-Images.hdb
chown clamav:clamav /var/lib/clamav/MSRBL-SPAM.ndb

/etc/init.d/clamav-daemon reload-database

Script noch ausführbar für root machen.

chmod 700 /etc/cron.daily/get_sane

Test

Da der Cronjob nur Täglich ausgeführt wird muss man es einmal manuell machen damit man es gleich testen kann.

Folgende Datei ausführen:

/etc/cron.daily/get_sane

Danach sollte ein ls -la /var/lib/clamav folgendes ausgeben:

sys@monster /tmp # ls -la /var/lib/clamav
insgesamt 14276
drwxr-xr-x  3 clamav clamav     4096 2008-03-26 11:35 .
drwxr-xr-x 33 root   root       4096 2008-03-02 11:23 ..
drwxr-xr-x  2 clamav clamav     4096 2008-03-26 11:35 daily.inc
-rw-r--r--  1 clamav clamav 11347852 2008-03-02 11:47 main.cvd
-rw-------  1 clamav clamav      572 2008-03-26 11:28 mirrors.dat
-rw-r--r--  1 clamav clamav      24670 2008-03-26 06:27 MSRBL-Images.hdb
-rw-r--r--  1 clamav clamav     235227 2008-03-26 06:27 MSRBL-SPAM.ndb
-rw-r--r--  1 clamav clamav    1450465 2008-03-26 06:27 phish.ndb
-rw-r--r--  1 clamav clamav    1500415 2008-03-26 06:27 scam.ndb
sys@monster /tmp #

Sanesecurity bietet Testfiles an um die Signaturen zu Testen.

Testdateien herunterladen und testen:

sys@monster ~ # cd /tmp
sys@monster /tmp # wget http://www.sanesecurity.com/clamav/phish_sigtest.txt
...
sys@monster /tmp # wget http://www.sanesecurity.com/clamav/scam_sigtest.txt
...
sys@monster /tmp # clamscan phish_sigtest.txt
phish_sigtest.txt: Html.Phishing.Sanesecurity.TestSig FOUND

----------- SCAN SUMMARY -----------
Known viruses: 253652
Engine version: 0.92.1
Scanned directories: 0
Scanned files: 1
Infected files: 1
Data scanned: 0.00 MB
Time: 4.394 sec (0 m 4 s)

sys@monster /tmp # clamscan scam_sigtest.txt
scam_sigtest.txt: Html.Scam.Sanesecurity.TestSig FOUND

----------- SCAN SUMMARY -----------
Known viruses: 253652
Engine version: 0.92.1
Scanned directories: 0
Scanned files: 1
Infected files: 1
Data scanned: 0.00 MB
Time: 4.462 sec (0 m 4 s)

sys@monster /tmp # 

Fertig! Viel Spaß ….



Standard SMTP Dialog

Genauere Definition und Erklärung von SMTP bei Wikipedia

RFCs

Default Ports

  • 25 TCP
  • 587 TCP (Submission Port)

Status-Codes

Das SMTP-Protokoll hält zum Status der Kommunikation zwischen Mailserver und Mailclient folgende Error-Codes bereit:

  • 1XX: Mailserver hat die Anforderung akzeptiert, ist aber selbst noch nicht tätig geworden. Eine Bestätigungsmeldung ist erforderlich.
  • 2XX: Mailserver hat die Anforderung erfolgreich ohne Fehler ausgeführt.
  • 3XX: Mailserver hat die Anforderung verstanden, benötigt aber zur Verarbeitung weitere Informationen.
  • 4XX: Mailserver hat einen temporären Fehler festgestellt. Wenn die Anforderung ohne jegliche Änderung wiederholt wird, kann die Verarbeitung möglicherweise abgeschlossen werden.
  • 5XX: Mailserver hat einen fatalen Fehler festgestellt. Ihre Anforderung kann nicht verarbeitet werden.

Quelle: Wikipedia

Standard Kommandos

Kommando Beschreibung
HELO host Eigenen Servernamen senden und Dialog beginnen
EHLO host Extended HELO; Zeigt Serverfunktionen/möglichkeiten an
MAIL FROM:xxx Absender E-Mail Adresse
RCPT TO:xxx Empfänger E-Mail Adresse
DATA Beginn der Übermittlung von Daten
Das Ende der Übermittlung wird mit <CR><LF>.<CR><LF> signalisiert.
QUIT Dialog beenden

Der SMTP Dialog

Um einen SMTP Server zu Testen kann man das Tool telnet verwenden:

# telnet mailserver.example.com 25

Wichtig ist die Portangabe 25. Protokoll ist TCP. Neuere Server verwenden unteranderem auch den Submission Port 587 TCP für Clienteinlieferungen.

>> sysadmin@vmserver01:~$ telnet mailserver.example.com 25
<< Trying 10.23.45.10...
<< Connected to mailserver.example.com.
<< Escape character is '^]'.
<< 220 mailserver.example.com ESMTP Postfix (Debian/GNU)
>> helo pc0815.example.com
<< 250 mailserver.example.com
>> mail from:absender@example.org
<< 250 2.1.0 Ok
>> rcpt to:empfaenger@example.org
<< 250 2.1.5 Ok
>> data
<< 354 End data with <CR><LF>.<CR><LF>
>> Dies ist eine Testmail.
>> .
<< 250 2.0.0 Ok: queued as 708D72C80BE
>> quit
<< 221 2.0.0 Bye
<< Connection closed by foreign host.
<< sysadmin@vmserver01:~$

SMTP Serverfunktionen abfragen mit EHLO (Extended HELO)

Der Dialog funktioniert genau so wie der andere es wird nur statt helo ehlo verwendet.

>> sysadmin@vmserver01:~$ telnet mailserver.example.com 25
<< Trying 10.23.45.10...
<< Connected to mailserver.example.com.
<< Escape character is '^]'.
<< 220 mailserver.example.com ESMTP Postfix (Debian/GNU)
>> ehlo pc0815.example.org
<< 250-mailserver.example.com
<< 250-PIPELINING
<< 250-SIZE 10240000
<< 250-VRFY
<< 250-ETRN
<< 250-STARTTLS
<< 250-ENHANCEDSTATUSCODES
<< 250-8BITMIME
<< 250 DSN
>> quit
<< 221 2.0.0 Bye
<< Connection closed by foreign host.
<< sysadmin@vmserver01:~$

ClamAV mit Sanesecurity erweitern

Benötigte Tools installieren

sudo apt-get install rsync wget gunzip

Verzeichnis anlegen

mkdir -p /root/sanesecurity

Script erstellen

nano /root/sanesecurity/get_sane

Script:

cd /root/sanesecurity
wget http://www.sanesecurity.co.uk/clamav/scamsigs/scam.ndb.gz
wget http://www.sanesecurity.co.uk/clamav/phishsigs/phish.ndb.gz

rsync rsync://rsync.mirror.msrbl.com/msrbl/MSRBL-Images.hdb /root/sanesecurity/MSRBL-Images.hdb
rsync rsync://rsync.mirror.msrbl.com/msrbl/MSRBL-SPAM.ndb /root/sanesecurity/MSRBL-SPAM.ndb

gunzip -f scam.ndb.gz
gunzip -f phish.ndb.gz
cp -f scam.ndb /var/lib/clamav
cp -f phish.ndb /var/lib/clamav
cp -f MSRBL-Images.hdb /var/lib/clamav
cp -f MSRBL-SPAM.ndb /var/lib/clamav

/etc/init.d/clamav-daemon reload-database

Das ganze noch lauffähig machen:

chmod u+x /root/sanesecurity/get_sane

In Crontab einbinden

Updates täglich:

ln -s /root/sanesecurity/get_sane /etc/cron.daily/01get_sane

oder stündlich:

ln -s /root/sanesecurity/get_sane /etc/cron.hourly/01get_sane

Testlauf

/root/sanesecurity/get_sane

Testmail verschicken ob die Sanepatterns auch funktionieren

Mit einem Standardmailclient eine Testmail senden und folgende Testsignaturen verwenden

Einfach mit Copy & Paste einfügen und versenden. Resultat sollte folgendermaßen aussehen:

Nov 26 06:59:25 server amavis[20891]: (20891-03) Blocked INFECTED (Html.Phishing.Sanesecurity.TestSig), [10.72.30.1] [10.

Spamtrap / Blackhole Adresse unter Postfix einrichten

Eine E-Mail Adresse einrichten die Richtung “/dev/null“ geht.

main.cf

check_recipient_access hash:/etc/postfix/spam_trap sollte hinter reject_unauth_destination kommen. Die Konfig für smtpd_recipient_restrictions ist nur exemplarisch.

smtpd_recipient_restrictions =
        reject_non_fqdn_recipient
        reject_non_fqdn_sender
        reject_unknown_sender_domain
        reject_unknown_recipient_domain
        permit_mynetworks
        permit_sasl_authenticated
        reject_unauth_destination
        check_recipient_access hash:/etc/postfix/spam_trap
        reject_multi_recipient_bounce

/etc/postfix/spam_trap

Inhalt der Datei:

devnull@thoma.cc                DISCARD

Diese Datei muss noch ins Postmap Format konvertiert werden.

Hierzu folgenden Befehl ausführen:

postmap /etc/postfix/spam_trap

Postfix reloaden

Zuletzt muss noch Postfix neu gestartet werden. Ein

postfix reload

reicht.

Fertig!

Alle Mails die z.B. an devnull@thoma.cc geschickt werden verschwinden im Nirvana.

Standard POP3 Dialog

Genauere Definition und Erklärung von POP3 bei Wikipedia

RFCs

Default Ports

  • 110 TCP
  • 995 TCP für SSL Verbindungen

Standard Kommandos

Kommando Beschreibung
USER xxx Auswahl der Benutzerkontos auf dem Server
PASS xxx Sendet das Passwort an den Server (Klartext)
STAT Liefert den Status der Mailbox, u. a. die Anzahl der neuen E-Mails.
LIST (n) Liefert die Anzahl und die Größe der (n-ten) E-Mail(s).
RETR n Holt die n-te E-Mail vom E-Mail-Server.
DELE n Löscht die n-te E-Mail am E-Mail-Server.
NOOP Keine Funktion, der Server antwortet mit +OK.
(Wird bei FTP-Servern öfters dazu verwendet die Verbindung offen zu halten.)
RSET Setzt alle DELE-Kommandos zurück.
QUIT Beendet die aktuelle POP3-Sitzung und führt alle DELE-Kommandos durch.

Der POP3 Dialog

>> sysadmin@vmserver01:~$ telnet pop3.example.net 110
<< Trying 10.23.45.10...
<< Connected to pop3.example.net.
<< Escape character is '^]'.
<< +OK POP3 Ready pop3.example.net 
>> user testuser@example.org
<<  +OK USER testuser@example.org set, mate
>> pass 123geheimKENNWORT123
<< +OK You are so in
>> list
<< +OK POP3 clients that break here, they violate STD53.
<< 1 236
>> retr 1
<< +OK 236 octets follow.
<< Date: Mon, 18 Oct 2004 04:11:45 +0200
<< From: Someone <someone@example.com>
<< To: testuser@example.org
<< Subject: Test-E-Mail
<< Content-Type: text/plain; charset=us-ascii; format=flowed
<< Content-Transfer-Encoding: 7bit
<< 
<< Dies ist eine Test-E-Mail
<< 
<< .
>> quit
<< +OK Bye-bye.
<< Connection closed by foreign host.
<< sysadmin@vmserver01:~$

Standard IMAP Dialog

Genauere Definition und Erklärung von IMAP bei Wikipedia

RFCs

Default Ports

  • 143 TCP
  • 993 TCP für SSL Verbindungen

Standard Kommandos

xx – Fortlaufende 2stellige Nummer 01, 02, 03, 04, …

Kommando Beschreibung
xx LOGIN username passwort User anmelden
xx LIST ““ * IMAP-Ordner auflisten (zwei Anführungszeichen nach List)
xx SELECT yyyy IMAP-Ordner auswählen
xx STATUS yyy (zzz) Status abfragen von einem IMAP-Ordner
Folgende Optionen für zzz sind möglich:
MESSAGES, UNSEEN, RECENT, UIDNEXT und UIDVALIDITY
xx FETCH n yyy Nachricht/Kopfzeile abrufen
n – Nachrichtenummer oder * für alle Nachrichten
yyy – Folgende Werte sind möglich:
ALL # Alle IMAP Header
FULL # Alle Headers und Body Infos
BODY # Body
ENVELOPE # Envelope
xx UID fetch n:n (UID RFC822.SIZE FLAGS BODY.PEEK[]) Nachricht komplett Empfangen
xx LOGOUT Ausloggen

Der IMAP Dialog

>> sysadmin@vmserver01:~$ telnet imap.example.org 143
<< Trying 10.23.45.10...
<< Connected to imap.example.org.
<< Escape character is '^]'.
<< * OK IMAP4 Ready imap.example.org
>> 01 LOGIN testuser@example.net 123geheimKENNWORT123
<< 01 OK You are so in
>> 02 LIST "" *
<< * LIST (\HasNoChildren) "." "INBOX.Junk"
<< * LIST (\HasNoChildren) "." "INBOX.Sent"
<< * LIST (\HasNoChildren) "." "INBOX.Drafts"
<< * LIST (\HasNoChildren) "." "INBOX.Trash"
<< * LIST (\Unmarked \HasChildren) "." "INBOX"
<< 02 OK LIST completed
>> 03 SELECT INBOX
<< * FLAGS (\Draft \Answered \Flagged \Deleted \Seen \Recent)
<< * OK [PERMANENTFLAGS (\* \Draft \Answered \Flagged \Deleted \Seen)] Limited
<< * 1 EXISTS
<< * 0 RECENT
<< * OK [UIDVALIDITY 1196314665] Ok
<< * OK [MYRIGHTS "acdilrsw"] ACL
<< 03 OK [READ-WRITE] Ok
>> 04 STATUS INBOX (MESSAGES)
<< * STATUS "INBOX" (MESSAGES 1)
<< 04 OK STATUS Completed.
>> 05 FETCH 1 ALL
<< * 1 FETCH (FLAGS (\Seen) INTERNALDATE "28-Nov-2007 11:55:53 +0100" RFC822.SIZE 912 ENVELOPE ("Wed, 28 Nov 2007 11:24:08 +0100 (CET)" NIL ((NIL NIL "testuser" "example.net")) ((NIL NIL "testuser" "example.net")) ((NIL NIL "testuser" "example.net")) ((NIL NIL "undisclosed-recipients" NIL)(NIL NIL NIL NIL)) NIL NIL NIL "<20071128102415.7245146C215@mailserver.example.org>"))
<< 05 OK FETCH completed.
>> 06 CLOSE
<< 06 OK mailbox closed.
>> 07 LOGOUT
<< * BYE Courier-IMAP server shutting down
<< 07 OK LOGOUT completed
<< Connection closed by foreign host.
<< sysadmin@vmserver01:~$

Postgrey installieren (Debian Etch)

Installation

sudo apt-get install postgrey

Service checken

mxadm@server:/# netstat -tulpen | grep 60000
tcp        0      0 127.0.0.1:60000         0.0.0.0:*               LISTEN     0          23135      7884/postgrey

Wenn der Service noch nicht läuft mit

/etc/init.d/postgrey start

starten.

In Postfix einbinden

main.cf:

smtpd_recipient_restrictions =
  permit_mynetworks
  permit_sasl_authenticated
  reject_unauth_destination
  ...
  check_policy_service inet:127.0.0.1:60000
  ...

Postfix reload

postfix reload

Postfix Quota Policy Daemon

Postfix Quota Policy Daemon ist ein Experiment, das ich im Halbschalf entwickelt habe :-). Ziel ist es auf einem Vmail System ohne Linuxquota auszukommen da dies eh nicht funktionieren würde. (Alle Mails haben bei einem Vmail System den selben User/Gruppe.) Ich habe den Daemon in PHP geschrieben in Perl wäre er sicher besser aber das beherrsche ich nicht sooo gut. Testen konnte ich es leider noch nicht, habe noch keine Virtualmachine installiert um das zu testen, sollte aber funktionieren. Status ist BETA.

Ihr findet mein Experiment auch unter → Downloads

Quelltext

<?php

/**
 * Postfix Quota Policy Daemon
 * based on PHP
 *
 * @version 1.0 beta
 * @copyright 2007 Maximilian Thoma nospam@thoma.cc
 *
 * Anleitung:
 * master.cf:
 * 127.0.0.1:9990  inet  n       n       n       -       0       spawn  user=root argv=/opt/bin/php -f /etc/postfix/deamon/pqpd.php
 *
 * main.cf:
 * smtpd_recipient_restrictions = ... , check_policy_service inet:127.0.0.1:9990, ...
 * smtpd_end_of_data_restrictions = ..., check_policy_service inet:127.0.0.1:9990, ...
 *
 */

////////////////////////////////////////////////////////////////////////////////
/// CONFIG
////////////////////////////////////////////////////////////////////////////////
$db_host="localhost"; // DB Host
$db_user="mailadmin"; // DB User
$db_pass="123test123"; // DB Passwort
$db_db="mailserver"; // Database
$db_tab_box="mailbox"; // Tabelle mit den Mailboxen
$db_box_user_col="username"; // Spalte mit dem Usernamen
$db_tab_alias="aliases"; // Tabelle mit den Aliases
$db_alias_alias_col="address"; // Spalte mit dem Alias
$db_alias_goto_col="goto"; // Spalte mit dem Ziel des Alias
$db_quota_col="quota"; // Spalte mit Quota
$db_maildir_col="maildir"; // Spalte mit dem Maildir

$q_ulimited="0"; // Quota Unlimited
$path_base="/srv/mail"; // Maildir
$path_box=$path_base."/".$user_maildir; // Maildir Struktur

////////////////////////////////////////////////////////////////////////////////
/// AB HIER NICHTS MEHR ÄNDERN !!!!
////////////////////////////////////////////////////////////////////////////////

//MySQL connect
$verbindung=mysql_connect($db_host,$db_user,$db_pass);
mysql_select_db($db_db);

// STDIN
if ($fp=fopen("php://stdin","r")) {

while($stop!=1){
        $line = fgets($fp,512);
        $teile = explode("=", $line);
        $stdin[$teile[0]]=$teile[1];
        // Entfernen von Leerzeilen und Zeilenumbrüchen
        $stdin[$teile[0]] = preg_replace("/\r|\n/s", "", $stdin[$teile[0]]);
        if($line=="\n"){$stop=1;}
     }
fclose($fp);
}

////////////////////////////////////////////////////////////////////////////////

// User Zerlegen
$recipient_expl=explode('@',$stdin[recipient]);
$user=$recipient_expl[0];
$domain=$recipient_expl[1];
// User Mail setzen
$email=$stdin[recipient];

// Check ob das eine Mailbox ist oder ein alias
$sql="SELECT * FROM ".$db_tab_box." WHERE ".$db_box_user_col."='".$email."'";
$ergebnis=mysql_num_rows(mysql_query($sql));

// Wenn Ergebnis gleich null dann soll er mal kucken ob das evtl. ein alias ist
// wenn nicht folgt noch der catchall check
if($ergebnis==0){
$sql2="SELECT * FROM ".$db_tab_alias." LEFT JOIN ".$db_tab_box." ON ".$db_tab_alias.".".$db_alias_goto_col."=".$db_tab_box.".".$db_box_user_col." WHERE ".$db_alias_alias_col."='".$email."'";
$ergebnis2=mysql_num_rows(mysql_query($sql2));

	// Check ob Catchall
	if($ergebnis2==0){
	$sql3="SELECT * FROM ".$db_tab_alias." LEFT JOIN ".$db_tab_box." ON ".$db_tab_alias.".".$db_alias_goto_col."=".$db_tab_box.".".$db_box_user_col." WHERE ".$db_alias_alias_col."='@".$domain."'";
	$ergebnis3=mysql_num_rows(mysql_query($sql3));

		if($ergebnis3==0){
		// User existiert nicht lokal, wir lassen die Mail passieren
		$entscheidung="pass";

		} else {
		// Emailadresse gehört zu einem Catchall
			$query_catchall=mysql_query($sql3);
			while($row_catchall=mysql_fetch_object($query_catchall)){
				$user_quota=$row_catchall->$db_quota_col;
				$user_maildir=$row_catchall->$db_maildir_col;
			}
		}

	} else {
	// Ja es ist ein Alias mit Postfach
	$query_alias=mysql_query($sql2);
			while($row_alias=mysql_fetch_object($query_alias)){
				$user_quota=$row_alias->$db_quota_col;
				$user_maildir=$row_alias->$db_maildir_col;
			}
	}
} else {
// Ja es ist ein Postfach
$query_box=mysql_query($sql);
			while($row_box=mysql_fetch_object($query_box)){
				$user_quota=$row_box->$db_quota_col;
				$user_maildir=$row_box->$db_maildir_col;
			}
}

////////////////////////////////////////////////////////////////////////////////

if($entscheidung!="pass"){

// Get Disc Quota
$disk_quota_string=exec("du -s $path_box");
$disk_quota_expl=explode(" ",$disk_quota_string);
$disk_quota_sum=$disk_quota_expl[0];

// Der ultimative Quota check !!!
	if($user_quota>=$disk_quota_sum OR $q_ulimited==$user_quota){
	// Der User hat entweder Quota unlimited oder noch genügend Platz also immer rein damit
	$action="DUNNO";
	} else {
		// Der User hat keinen Platz mehr und darf nichts mehr speichern
		$action="defer_if_permit User Quotalimit exceeded. Please try again later.";
		}
	} else {
	// Der User scheint kein Postfach oder Alias oder Catchall zu haben und ist wahrscheinlich extern deshalb pass
	$action="DUNNO";
	}

////////////////////////////////////////////////////////////////////////////////
//STD OUT
////////////////////////////////////////////////////////////////////////////////
if(isset($action)){
$stdout = fopen('php://stdout', 'w');
fwrite($stdout,"action=$action\n\n");
fclose($stdout);
}

mysql_close($verbindung);

?>

Policyd-weight installieren (Debian Etch)

Installation

sudo apt-get install policyd-weight

Service checken

mxadm@server:/# netstat -tulpen | grep policyd-weight
tcp        0      0 127.0.0.1:12525         0.0.0.0:*               LISTEN     0          23134      7883/policyd-weight

Wenn der Service noch nicht laufen sollte dann mit

/etc/init.d/policyd-weight start

starten.

Einbinden in Postfix für bestimmte User / Domains

main.cf:

smtpd_restriction_classes = check_policyd_weight
 check_policyd_weight =
   check_policy_service inet:127.0.0.1:12525

smtpd_recipient_restrictions =
...
check_recipient_access hash:/etc/postfix/policyd_weight_users
...

/etc/postfix/policyd_weight_users:

für Domain:

example-domain.de check_policyd_weight

für User:

user@example-domain.de check_policyd_weight

postmap /etc/postfix/policyd_weight_users nicht vergessen !!!

Einbinden in Postfix für alle Domains

main.cf:

smtpd_recipient_restrictions =
...
check_policy_service inet:127.0.0.1:12525      # required
...

POP3/IMAP Proxy Perdition mit MySQL backend

Perdition ist ein POP3/IMAP Proxy (SSL fähig) der zu mehreren Backend Servern (POP3 und IMAP) sich Verbinden kann. Die Entscheidung zu welchem Server er weiterverbinden soll erfolgt durch eine Tabelle. Perdition unterstützt mehrere Tabellenformate. Ich verwende MySQL.

Installation

apt-get install perdition perdition-mysql

Konfiguration

/etc/default/perdition:

######################################################################
# /etc/sysconfig/perdition (RPM based systems)
# /etc/default/perdition   (Debian)
#
# Run time configuration parameters for perdition
######################################################################

# Run perdition
# Set to "yes" to run perdition
# Set to any other value to not run perdition
RUN_PERDITION=yes

# Command line parameters to pass to perdition when run in any mode.
# This is in addition to any mode specific flags.
# That is, it is in addtion to any command line options supplied
# by POP3_FLAGS, POP3S_FLAGS, IMAP4_FLAGS or IMAP4S_FLAGS
FLAGS=

# Run an instance of perdition in POP3 mode
# Set to "yes" to run this instance of perdition
# Set to any other valye to not run this instance of perdition
POP3=yes

#Command line parameters to pass to perdition when run in POP3 mode
POP3_FLAGS="--ssl_mode tls_listen"

# Run an instance of perdition in POP3S mode
# Set to "yes" to run this instance of perdition
# Set to any other valye to not run this instance of perdition
POP3S=yes

#Command line parameters to pass to perdition when run in POP3S mode
POP3S_FLAGS="--ssl_mode ssl_listen -p 110"

# Run an instance of perdition in IMAP4 mode
# Set to "yes" to run this instance of perdition
# Set to any other valye to not run this instance of perdition
IMAP4=yes

#Command line parameters to pass to perdition when run in IMAP4 mode
IMAP4_FLAGS="--ssl_mode tls_listen"

# Run an instance of perdition in IMAP4S mode
# Set to "yes" to run this instance of perdition
# Set to any other valye to not run this instance of perdition
IMAP4S=yes

#Command line parameters to pass to perdition when run in IMAP4S mode
IMAP4S_FLAGS="--ssl_mode ssl_listen -p 143"

/etc/perdition/perdition.conf:

connection_logging
connection_limit 1000
map_library /usr/lib/libperditiondb_mysql.so.0
map_library_opt "localhost:3306:db:table:user:password"
username_from_database
# Interner IMAP an den weitergeleitet werden soll wenn keine Weiterleitung in der DB ist
outgoing_server  127.0.0.1
# Hier das SSL Cert wenn gewünscht.
ssl_cert_file /etc/perdition/ssl_perdition.cert
ssl_key_file /etc/perdition/ssl_perdition.key
# Perdition an ein Interface binden da er sonst 0.0.0.0 versucht und auf 127.0.0.1 lauscht ja schon unser lokaler
bind_address 10.4.4.1

Datenbank

CREATE TABLE `pop3_imap_proxy` (
`user` varchar(128) NOT NULL,
`servername` varchar(255) NOT NULL,
`port` varchar(8) default NULL,
PRIMARY KEY  (`user`),
KEY `idxtblPerdition_user` (`user`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;

Noch ein restart …

/etc/init.d/perdition restart

Perdition log

tail -f /var/log/syslog | grep perdition

Spamassassin Test mit GTUBE

GTUBE bedeutet

  • Generic
  • Test for
  • Unsolicited
  • Bulk
  • Email

GTUBE ist ein String der bei Spamassassin mit ca. 1000 Punkten bewertet wird. Hiermit kann man Testen ob Spamassassin überhaupt Spam erkennt und die gewünschten Aktionen ausführt.

Um Spamassassin zu Testen müssen Sie einfach den String per Mail (von einem externen Konto) an den Zielserver/adresse schicken.

GTUBE String

XJS*C4JDBQADN1.NSBN3*2IDNEN*GTUBE-STANDARD-ANTI-UBE-TEST-EMAIL*C.34X

Auszug aus /var/log/mail.log:

Jul 25 14:20:23 mailserver amavis[31947]: (31947-07) Blocked SPAM, [xx.xx.xx.xx] [xx.xx.xx.xx] <test@example.com> -> <ziel@example.com>, Message-ID: <2016475223@example.com>, Hits: 1000.568, 386 ms

Weitere Informationen auf Wikipedia

Amavisd-new/Clamav Test mit EICAR

Die EICAR-Testdatei (auch „Eicar test file“ genannt) ist ein am European Institute for Computer Antivirus Research entwickeltes Testmuster, mit dessen Hilfe die Funktionen von Antivirenprogrammen getestet werden können. Dabei handelt es sich um eine reine Textdatei mit 68 ASCII-Zeichen und einer daraus resultierenden Dateigröße von 68 Byte (bzw. 70 Byte), welche somit in jeden beliebigen Texteditor eingegeben werden kann. Die Datei ist gutartig und richtet keinerlei Schaden an, sollte aber dennoch von allen Virenscannern als Virus erkannt und angezeigt werden. Damit lässt sich beispielsweise auch testen, ob ein Virenscanner ein Archiv korrekt lesen kann.

Es gibt verschiedene Anbieter die diesen Testvirus und andere per Mail zuschicken.

Quelle: Wikipedia EICAR-Testdatei

Verhindern von Bruteforce Attaken auf Postfix SASL, Courier-IMAP und SSH

Hinweis

Dieses Howto wurde für Debian Etch geschrieben.

Installation

apt-get install fail2ban

Konfiguration anpassen

/etc/fail2ban/jail.local

[DEFAULT]

# "ignoreip" can be an IP address, a CIDR mask or a DNS host
ignoreip = 127.0.0.1 
bantime  = 600
maxretry = 3

# "backend" specifies the backend used to get files modification. Available
# options are "gamin", "polling" and "auto".
# yoh: For some reason Debian shipped python-gamin didn't work as expected
#      This issue left ToDo, so polling is default backend for now
backend = polling

#
# Destination email address used solely for the interpolations in
# jail.{conf,local} configuration files.
destemail = root@localhost

# Default action to take: ban only
action = iptables[name=%(__name__)s, port=%(port)s]


[ssh]

enabled = true
port    = ssh
filter  = sshd
logpath  = /var/log/auth.log
maxretry = 5

[postfix]

enabled  = false
port     = smtp
filter   = postfix
logpath  = /var/log/mail.log
maxretry = 5


[courierpop3]

enabled  = true
port     = pop3
filter   = courierlogin
failregex = courierpop3login: LOGIN FAILED.*ip=\[.*:<HOST>\]
logpath  = /var/log/mail.log
maxretry = 5


[courierimap]

enabled  = true
port     = imap2
filter   = courierlogin
failregex = imapd: LOGIN FAILED.*ip=\[.*:<HOST>\]
logpath  = /var/log/mail.log
maxretry = 5

[sasl]

enabled  = true
port     = smtp
filter   = sasl
failregex = warning: [-._\w]+\[<HOST>\]: SASL (?:LOGIN|PLAIN|(?:CRAM|DIGEST)-MD5) authentication failed
logpath  = /var/log/mail.log
maxretry = 5

Restart von fail2ban

/etc/init.d/fail2ban restart

Ergebnis kontrollieren

iptables -L -n

Ergebnis:

Chain INPUT (policy ACCEPT)
target     prot opt source               destination
fail2ban-sasl  tcp  --  0.0.0.0/0            0.0.0.0/0           tcp dpt:25
fail2ban-courierimap  tcp  --  0.0.0.0/0            0.0.0.0/0           tcp dpt:143
fail2ban-ssh  tcp  --  0.0.0.0/0            0.0.0.0/0           tcp dpt:22
fail2ban-courierpop3  tcp  --  0.0.0.0/0            0.0.0.0/0           tcp dpt:110

Chain FORWARD (policy ACCEPT)
target     prot opt source               destination

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination

Chain fail2ban-courierimap (1 references)
target     prot opt source               destination
RETURN     0    --  0.0.0.0/0            0.0.0.0/0

Chain fail2ban-courierpop3 (1 references)
target     prot opt source               destination
RETURN     0    --  0.0.0.0/0            0.0.0.0/0

Chain fail2ban-sasl (1 references)
target     prot opt source               destination
RETURN     0    --  0.0.0.0/0            0.0.0.0/0

Chain fail2ban-ssh (1 references)
target     prot opt source               destination
RETURN     0    --  0.0.0.0/0            0.0.0.0/0

Test

Loggen Sie sich einfach mit einem vorhandenen Benutzer und falschen Passwort 5 mal an. Dann sollte Ihre IP-Adresse für 5 Minuten blockiert sein.

Es sollte nur der Dienst gesperrt werden der betroffen ist.

Iptables:

Chain fail2ban-courierimap (1 references)
target     prot opt source               destination
RETURN     0    --  0.0.0.0/0            0.0.0.0/0

Chain fail2ban-courierpop3 (1 references)
target     prot opt source               destination
DROP       0    --  62.158.111.222       0.0.0.0/0
RETURN     0    --  0.0.0.0/0            0.0.0.0/0

Chain fail2ban-sasl (1 references)
target     prot opt source               destination
RETURN     0    --  0.0.0.0/0            0.0.0.0/0

Chain fail2ban-ssh (1 references)
target     prot opt source               destination
RETURN     0    --  0.0.0.0/0            0.0.0.0/0

Logeintrag:

Rechner wurde gesperrt:

2007-07-11 21:41:49,063 fail2ban.actions: WARNING [courierpop3] Ban 62.158.111.222

Rechner wurde entsperrt:

2007-07-11 21:51:49,324 fail2ban.actions: WARNING [courierpop3] Unban 62.158.111.222