IT security, FreeBSD, Linux, mail server hardening, post-quantum crypto, DNS, retro computing & hands-on hardware hacks. Privater Tech-Blog seit 2003.

Schlagwort: Postfix (Seite 3 von 5)

TLSA DANE Record für E-Mail in Postfix prüfen: Schritt-für-Schritt-Anleitung

Habe ich ganz aktuell gesehen dass sich über die Webseite ssl-tools.net nun ebenfalls die DANE RECORDS testen lassen. Also einmal ob sie vorhanden sind und natürlich auch die Gültigkeit des RECORDS. Zu dem Thema bin ich ja bereits ein paar mal gefragt worden, da „einfache“ Test Tools noch rar waren. Was sich ja inzwischen endlich zu ändern scheint.

KLICK: https://de.ssl-tools.net/mailservers/kernel-error.de

Siehe auch: TLSA und DANE manuell prüfen

Fragen? Einfach melden.

TLSA- und DANE-Records manuell prüfen: Schritt für Schritt mit OpenSSL

Es gibt inzwischen viele Webtools die TLSA-Records prüfen. Aber wer es einmal von Hand gemacht hat, versteht was dabei passiert. Der Ablauf ist immer gleich: Zertifikat vom Server holen, Hash berechnen, mit dem DNS-Record vergleichen.

Zertifikat holen

Verbindung zum Mailserver aufbauen und das Zertifikat per STARTTLS abholen:

openssl s_client -starttls smtp -connect smtp.kernel-error.de:25 \
  -servername smtp.kernel-error.de 2>/dev/null | \
  openssl x509 -outform PEM > /tmp/server.crt

Das Zertifikat liegt jetzt in /tmp/server.crt.

Hash berechnen

Welchen Hash man berechnen muss, hängt vom TLSA-Record ab. Die drei Felder im Record bestimmen das:

Usage0 = CA, 1 = End-Entity (Kette muss gültig sein), 2 = Trust Anchor, 3 = End-Entity (keine Kettenprüfung)
Selector0 = ganzes Zertifikat, 1 = nur Public Key (SPKI)
Matching Type0 = exakter Vergleich, 1 = SHA-256, 2 = SHA-512

Am häufigsten sieht man 3 1 1 (End-Entity, nur Public Key, SHA-256) oder 3 0 1 (End-Entity, ganzes Zertifikat, SHA-256). Zuerst den TLSA-Record aus dem DNS holen um zu sehen was erwartet wird:

dig _25._tcp.smtp.kernel-error.de TLSA +short

Dann den passenden Hash berechnen. Bei Selector 0 (ganzes Zertifikat) und Matching Type 1 (SHA-256):

# Selector 0 (Full Certificate), SHA-256
openssl x509 -in /tmp/server.crt -outform DER | openssl sha256
# Ausgabe: SHA2-256(stdin)= 94c8e1bd...

Bei Selector 1 (nur SPKI, Public Key) und SHA-256:

# Selector 1 (SPKI), SHA-256
openssl x509 -in /tmp/server.crt -noout -pubkey | \
  openssl pkey -pubin -outform DER | openssl sha256

Den berechneten Hash mit dem Wert aus dem TLSA-Record vergleichen. Stimmen sie überein, ist der Record korrekt.

Schnelltest mit posttls-finger

Wer nicht alles von Hand machen will: posttls-finger (Teil von Postfix) prüft den kompletten DANE-Ablauf in einem Schritt:

posttls-finger -t30 -T180 -c -L verbose,summary kernel-error.de

In der Ausgabe steht am Ende entweder Verified TLS connection established (DANE-Prüfung bestanden) oder eine Fehlermeldung mit dem konkreten Problem. Das Tool löst die MX-Records auf, holt den TLSA-Record, baut die TLS-Verbindung auf und vergleicht alles automatisch.

Wer DANE für den eigenen Mailserver einrichten will, findet die Anleitung unter Postfix mit DANE und DNSSEC absichern. Die Grundlagen zu DANE und TLSA-Records erklärt der Beitrag DNSSEC und DANE: TLS-Zertifikate mit TLSA-Records absichern. Fragen? Einfach melden.

Mailgraph um SPF-, DMARC- und DKIM-Graphen erweitern

Mailgraph von David Schweikert ist ein simples Perl-Script, das Postfix-Logdateien parst und daraus RRDtool-Graphen erzeugt. Empfangene, gesendete, abgelehnte Mails auf einen Blick. Für kleinere Mailserver oder Testsysteme reicht das völlig.

Was mailgraph nicht kann: SPF-, DMARC– und DKIM-Ergebnisse darstellen. Wie viele Mails bestehen den SPF-Check? Wie oft schlägt DMARC fehl? Gibt es einen Trend? Ich habe zwar einen existierenden SPF-Patch für mailgraph gefunden, wollte aber alle drei Protokolle in einem Aufwasch.

Die folgenden Patches erweitern mailgraph 1.14 um drei zusätzliche Graphen: SPF (pass/none/fail), DMARC (pass/none/fail) und DKIM (pass/none/fail). Ausgelegt für Postfix mit postfix-policyd-spf, opendkim und opendmarc.

Installation

Auf einem Debian-System müssen zwei Dateien ersetzt werden: /usr/sbin/mailgraph (der Daemon) und /usr/lib/cgi-bin/mailgraph.cgi (die Weboberfläche). Vorher sichern.

Die Patches als Download: mailgraph-dmarc-spf-dkim.patch.tar.gz

Patch 1: /usr/sbin/mailgraph

Der Daemon-Patch fügt neun RRD-Datasources hinzu (spfnone, spffail, spfpass, dmarcnone, dmarcfail, dmarcpass, dkimnone, dkimfail, dkimpass) und parst die Logzeilen von policy-spf, opendmarc und opendkim:

*** mailgraph	2012-06-17 00:00:00.000000000 +0200
--- mailgraph	2014-04-24 08:59:58.964977886 +0200
***************
*** 4,9 ****
--- 4,10 ----
  # copyright (c) 2000-2007 ETH Zurich
  # copyright (c) 2000-2007 David Schweikert <david@schweikert.ch>
  # released under the GNU General Public License
+ # with dkim-, dmarc, spf-patch Sebastian van de Meer <kernel-error@kernel-error.de>

  ######## Parse::Syslog 1.09 (automatically embedded) ########
  package Parse::Syslog;
***************
*** 382,388 ****
  my $rrd_greylist = "mailgraph_greylist.rrd";
  my $year;
  my $this_minute;
! my %sum = ( sent => 0, received => 0, bounced => 0, rejected => 0, virus => 0, spam => 0, greylisted => 0, delayed => 0);
  my $rrd_inited=0;

  my %opt = ();
--- 383,389 ----
  my $rrd_greylist = "mailgraph_greylist.rrd";
  my $year;
  my $this_minute;
! my %sum = ( sent => 0, received => 0, bounced => 0, rejected => 0, spfnone => 0, spffail => 0, spfpass => 0, dmarcnone => 0, dmarcfail => 0, dmarcpass => 0, dkimnone => 0, dkimfail => 0, dkimpass => 0, virus => 0, spam => 0, greylisted => 0, delayed => 0);
  my $rrd_inited=0;

  my %opt = ();
***************
*** 396,401 ****
--- 397,411 ----
  sub event_rejected($);
  sub event_virus($);
  sub event_spam($);
+ sub event_spfnone($);
+ sub event_spffail($);
+ sub event_spfpass($);
+ sub event_dmarcnone($);
+ sub event_dmarcfail($);
+ sub event_dmarcpass($);
+ sub event_dkimnone($);
+ sub event_dkimfail($);
+ sub event_dkimpass($);
  sub event_greylisted($);
  sub event_delayed($);
  sub init_rrd($);
***************
*** 533,538 ****
--- 543,557 ----
  				'DS:recv:ABSOLUTE:'.($rrdstep*2).':0:U',
  				'DS:bounced:ABSOLUTE:'.($rrdstep*2).':0:U',
  				'DS:rejected:ABSOLUTE:'.($rrdstep*2).':0:U',
+ 				'DS:spfnone:ABSOLUTE:'.($rrdstep*2).':0:U',
+ 				'DS:spffail:ABSOLUTE:'.($rrdstep*2).':0:U',
+ 				'DS:spfpass:ABSOLUTE:'.($rrdstep*2).':0:U',
+ 				'DS:dmarcnone:ABSOLUTE:'.($rrdstep*2).':0:U',
+ 				'DS:dmarcfail:ABSOLUTE:'.($rrdstep*2).':0:U',
+ 				'DS:dmarcpass:ABSOLUTE:'.($rrdstep*2).':0:U',
+ 				'DS:dkimnone:ABSOLUTE:'.($rrdstep*2).':0:U',
+ 				'DS:dkimfail:ABSOLUTE:'.($rrdstep*2).':0:U',
+ 				'DS:dkimpass:ABSOLUTE:'.($rrdstep*2).':0:U',
  				"RRA:AVERAGE:0.5:$day_steps:$realrows",   # day
  				"RRA:AVERAGE:0.5:$week_steps:$realrows",  # week
  				"RRA:AVERAGE:0.5:$month_steps:$realrows", # month
***************
*** 614,619 ****
--- 633,649 ----
  				event($time, 'bounced');
  			}
  		}
+ 	  elsif ($prog eq 'policy-spf') {
+ 	    if ($text =~ /Received-SPF: none/) {
+ 	      event($time, 'spfnone');
+ 	    }
+ 	    elsif($text =~ /Received-SPF: pass/) {
+ 				event($time, 'spfpass');
+ 			}
+ 	    elsif($text =~ /Received-SPF:/) {
+ 				event($time, 'spffail');
+ 			}
+ 	  }
  		elsif($prog eq 'local') {
  			if($text =~ /\bstatus=bounced\b/) {
  				event($time, 'bounced');
***************
*** 862,867 ****
--- 892,919 ----
  			event($time, 'virus');
  		}
  	}
+ 	elsif ($prog eq 'opendmarc') {
+ 		if ($text =~ /pass/) {
+ 			event($time, 'dmarcpass');
+ 		}
+ 		elsif($text =~ /none/) {
+ 			event($time, 'dmarcnone');
+ 		}
+ 		elsif($text =~ /fail/) {
+ 			event($time, 'dmarcfail');
+ 		}
+ 	}
+ 	elsif ($prog eq 'opendkim') {
+ 		if ($text =~ /DKIM verification successful/) {
+ 			event($time, 'dkimpass');
+ 		}
+ 		elsif($text =~ /no signature data/) {
+ 			event($time, 'dkimnone');
+ 		}
+ 		elsif($text =~ /bad signature data/) {
+ 			event($time, 'dkimfail');
+ 		}
+ 	}
  	elsif($prog eq 'avmilter') {
  		# AntiVir Milter
  		if($text =~ /^Alert!/) {
***************
*** 918,931 ****
  	return 1 if $m == $this_minute;
  	return 0 if $m < $this_minute;

! 	print "update $this_minute:$sum{sent}:$sum{received}:$sum{bounced}:$sum{rejected}:$sum{virus}:$sum{spam}:$sum{greylisted}:$sum{delayed}\n" if $opt{verbose};
! 	RRDs::update $rrd, "$this_minute:$sum{sent}:$sum{received}:$sum{bounced}:$sum{rejected}" unless $opt{'no-mail-rrd'};
  	RRDs::update $rrd_virus, "$this_minute:$sum{virus}:$sum{spam}" unless $opt{'no-virus-rrd'};
  	RRDs::update $rrd_greylist, "$this_minute:$sum{greylisted}:$sum{delayed}" unless $opt{'no-greylist-rrd'};
  	if($m > $this_minute+$rrdstep) {
  		for(my $sm=$this_minute+$rrdstep;$sm<$m;$sm+=$rrdstep) {
! 			print "update $sm:0:0:0:0:0:0:0:0 (SKIP)\n" if $opt{verbose};
! 			RRDs::update $rrd, "$sm:0:0:0:0" unless $opt{'no-mail-rrd'};
  			RRDs::update $rrd_virus, "$sm:0:0" unless $opt{'no-virus-rrd'};
  			RRDs::update $rrd_greylist, "$sm:0:0" unless $opt{'no-greylist-rrd'};
  		}
--- 970,983 ----
  	return 1 if $m == $this_minute;
  	return 0 if $m < $this_minute;

! 	print "update $this_minute:$sum{sent}:$sum{received}:$sum{bounced}:$sum{rejected}:$sum{spfnone}:$sum{spffail}:$sum{spfpass}:$sum{dmarcnone}:$sum{dmarcfail}:$sum{dmarcpass}:$sum{dkimnone}:$sum{dkimfail}:$sum{dkimpass}:$sum{virus}:$sum{spam}:$sum{greylisted}:$sum{delayed}\n" if $opt{verbose};
! 	RRDs::update $rrd, "$this_minute:$sum{sent}:$sum{received}:$sum{bounced}:$sum{rejected}:$sum{spfnone}:$sum{spffail}:$sum{spfpass}:$sum{dmarcnone}:$sum{dmarcfail}:$sum{dmarcpass}:$sum{dkimnone}:$sum{dkimfail}:$sum{dkimpass}" unless $opt{'no-mail-rrd'};
  	RRDs::update $rrd_virus, "$this_minute:$sum{virus}:$sum{spam}" unless $opt{'no-virus-rrd'};
  	RRDs::update $rrd_greylist, "$this_minute:$sum{greylisted}:$sum{delayed}" unless $opt{'no-greylist-rrd'};
  	if($m > $this_minute+$rrdstep) {
  		for(my $sm=$this_minute+$rrdstep;$sm<$m;$sm+=$rrdstep) {
! 			print "update $sm:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0 (SKIP)\n" if $opt{verbose};
! 			RRDs::update $rrd, "$sm:0:0:0:0:0:0:0:0:0:0:0:0:0" unless $opt{'no-mail-rrd'};
  			RRDs::update $rrd_virus, "$sm:0:0" unless $opt{'no-virus-rrd'};
  			RRDs::update $rrd_greylist, "$sm:0:0" unless $opt{'no-greylist-rrd'};
  		}
***************
*** 935,940 ****
--- 987,1001 ----
  	$sum{received}=0;
  	$sum{bounced}=0;
  	$sum{rejected}=0;
+ 	$sum{spfnone}=0;
+ 	$sum{spffail}=0;
+ 	$sum{spfpass}=0;
+ 	$sum{dmarcnone}=0;
+ 	$sum{dmarcfail}=0;
+ 	$sum{dmarcpass}=0;
+ 	$sum{dkimnone}=0;
+ 	$sum{dkimfail}=0;
+ 	$sum{dkimpass}=0;
  	$sum{virus}=0;
  	$sum{spam}=0;
  	$sum{greylisted}=0;

Patch 2: /usr/lib/cgi-bin/mailgraph.cgi

Der CGI-Patch erzeugt die drei neuen Graphen (SPF, DMARC, DKIM) und bindet sie in die HTML-Ausgabe ein. Jeder Graph zeigt pass als Fläche, none als Stack und fail als Linie:

*** mailgraph.cgi	2012-06-17 00:00:00.000000000 +0200
--- mailgraph.cgi	2014-04-24 09:03:11.917988368 +0200
***************
*** 4,9 ****
--- 4,10 ----
  # copyright (c) 2000-2007 ETH Zurich
  # copyright (c) 2000-2007 David Schweikert <david@schweikert.ch>
  # released under the GNU General Public License
+ # with dkim-, dmarc, spf-patch Sebastian van de Meer <kernel-error@kernel-error.de>

  use RRDs;
  use POSIX qw(uname);
***************
*** 15,21 ****
--- 16,25 ----
  my $xpoints = 540;
  my $points_per_sample = 3;
  my $ypoints = 160;
+ my $ypoints_spf = 96;
  my $ypoints_err = 96;
+ my $ypoints_dmarc = 96;
+ my $ypoints_dkim = 96;
  my $ypoints_grey = 96;
  my $rrd = '/var/lib/mailgraph/mailgraph.rrd';
  my $rrd_virus = '/var/lib/mailgraph/mailgraph_virus.rrd';
***************
*** 32,37 ****
--- 36,50 ----
  	sent       => '000099',
  	received   => '009900',
  	rejected   => 'AA0000',
+ 	spfnone    => '000AAA',
+ 	spffail    => '12FF0A',
+ 	spfpass    => 'D15400',
+ 	dmarcnone  => 'FFFF00',
+ 	dmarcfail  => 'FF00EA',
+ 	dmarcpass  => '00FFD5',
+ 	dkimnone   => '3013EC',
+ 	dkimfail   => '006B3A',
+ 	dkimpass   => '491503',
  	bounced    => '000000',
  	virus      => 'DDBB00',
  	spam       => '999999',
***************
*** 154,159 ****
--- 167,292 ----
  	);
  }

+ sub graph_spf($)
+ {
+ 	my ($range, $file) = @_;
+ 	my $step = $range*$points_per_sample/$xpoints;
+ 	rrd_graph($range, $file, $ypoints_spf,
+ 		"DEF:spfpass=$rrd:spfpass:AVERAGE",
+ 		"DEF:mspfpass=$rrd:spfpass:MAX",
+ 		"CDEF:rspfpass=spfpass,60,*",
+ 		"CDEF:dspfpass=spfpass,UN,0,spfpass,IF,$step,*",
+ 		"CDEF:sspfpass=PREV,UN,dspfpass,PREV,IF,dspfpass,+",
+ 		"CDEF:rmspfpass=mspfpass,60,*",
+ 		"AREA:rspfpass#$color{spfpass}:SPF pass",
+ 		'GPRINT:sspfpass:MAX:total\: %8.0lf msgs',
+ 		'GPRINT:rspfpass:AVERAGE:avg\: %5.2lf msgs/min',
+ 		'GPRINT:rmspfpass:MAX:max\: %4.0lf msgs/min\l',
+ 		"DEF:spfnone=$rrd:spfnone:AVERAGE",
+ 		"DEF:mspfnone=$rrd:spfnone:MAX",
+ 		"CDEF:rspfnone=spfnone,60,*",
+ 		"CDEF:dspfnone=spfnone,UN,0,spfnone,IF,$step,*",
+ 		"CDEF:sspfnone=PREV,UN,dspfnone,PREV,IF,dspfnone,+",
+ 		"CDEF:rmspfnone=mspfnone,60,*",
+ 		"STACK:rspfnone#$color{spfnone}:SPF none",
+ 		'GPRINT:sspfnone:MAX:total\: %8.0lf msgs',
+ 		'GPRINT:rspfnone:AVERAGE:avg\: %5.2lf msgs/min',
+ 		'GPRINT:rmsrspfnone:MAX:max\: %4.0lf msgs/min\l',
+ 		"DEF:spffail=$rrd:spffail:AVERAGE",
+ 		"DEF:mspffail=$rrd:spffail:MAX",
+ 		"CDEF:rspffail=spffail,60,*",
+ 		"CDEF:dspffail=spffail,UN,0,spffail,IF,$step,*",
+ 		"CDEF:sspffail=PREV,UN,dspffail,PREV,IF,dspffail,+",
+ 		"CDEF:rmspffail=mspffail,60,*",
+ 		"LINE2:rspffail#$color{spffail}:SPF fail",
+ 		'GPRINT:sspffail:MAX:total\: %8.0lf msgs',
+ 		'GPRINT:rspffail:AVERAGE:avg\: %5.2lf msgs/min',
+ 		'GPRINT:rmspffail:MAX:max\: %4.0lf msgs/min\l',
+ 	);
+ }
+
+ sub graph_dmarc($)
+ {
+ 	my ($range, $file) = @_;
+ 	my $step = $range*$points_per_sample/$xpoints;
+ 	rrd_graph($range, $file, $ypoints_dmarc,
+ 		"DEF:dmarcpass=$rrd:dmarcpass:AVERAGE",
+ 		"DEF:mdmarcpass=$rrd:dmarcpass:MAX",
+ 		"CDEF:rdmarcpass=dmarcpass,60,*",
+ 		"CDEF:ddmarcpass=dmarcpass,UN,0,dmarcpass,IF,$step,*",
+ 		"CDEF:sdmarcpass=PREV,UN,ddmarcpass,PREV,IF,ddmarcpass,+",
+ 		"CDEF:rmdmarcpass=mdmarcpass,60,*",
+ 		"AREA:rdmarcpass#$color{dmarcpass}:DMARC pass",
+ 		'GPRINT:sdmarcpass:MAX:total\: %8.0lf msgs',
+ 		'GPRINT:rdmarcpass:AVERAGE:avg\: %5.2lf msgs/min',
+ 		'GPRINT:rmdmarcpass:MAX:max\: %4.0lf msgs/min\l',
+ 		"DEF:dmarcnone=$rrd:dmarcnone:AVERAGE",
+ 		"DEF:mdmarcnone=$rrd:dmarcnone:MAX",
+ 		"CDEF:rdmarcnone=dmarcnone,60,*",
+ 		"CDEF:ddmarcnone=dmarcnone,UN,0,dmarcnone,IF,$step,*",
+ 		"CDEF:sdmarcnone=PREV,UN,ddmarcnone,PREV,IF,ddmarcnone,+",
+ 		"CDEF:rmdmarcnone=mdmarcnone,60,*",
+ 		"STACK:rdmarcnone#$color{dmarcnone}:DMARC none",
+ 		'GPRINT:sdmarcnone:MAX:total\: %8.0lf msgs',
+ 		'GPRINT:rdmarcnone:AVERAGE:avg\: %5.2lf msgs/min',
+ 		'GPRINT:rmdmarcnone:MAX:max\: %4.0lf msgs/min\l',
+ 		"DEF:dmarcfail=$rrd:dmarcfail:AVERAGE",
+ 		"DEF:mdmarcfail=$rrd:dmarcfail:MAX",
+ 		"CDEF:rdmarcfail=dmarcfail,60,*",
+ 		"CDEF:ddmarcfail=dmarcfail,UN,0,dmarcfail,IF,$step,*",
+ 		"CDEF:sdmarcfail=PREV,UN,ddmarcfail,PREV,IF,ddmarcfail,+",
+ 		"CDEF:rmdmarcfail=mdmarcfail,60,*",
+ 		"LINE2:rdmarcfail#$color{dmarcfail}:DMARC fail",
+ 		'GPRINT:sdmarcfail:MAX:total\: %8.0lf msgs',
+ 		'GPRINT:rdmarcfail:AVERAGE:avg\: %5.2lf msgs/min',
+ 		'GPRINT:rmdmarcfail:MAX:max\: %4.0lf msgs/min\l',
+ 	);
+ }
+
+ sub graph_dkim($)
+ {
+ 	my ($range, $file) = @_;
+ 	my $step = $range*$points_per_sample/$xpoints;
+ 	rrd_graph($range, $file, $ypoints_dkim,
+ 		"DEF:dkimpass=$rrd:dkimpass:AVERAGE",
+ 		"DEF:mdkimpass=$rrd:dkimpass:MAX",
+ 		"CDEF:rdkimpass=dkimpass,60,*",
+ 		"CDEF:ddkimpass=dkimpass,UN,0,dkimpass,IF,$step,*",
+ 		"CDEF:sdkimpass=PREV,UN,ddkimpass,PREV,IF,ddkimpass,+",
+ 		"CDEF:rmdkimpass=mdkimpass,60,*",
+ 		"AREA:rdkimpass#$color{dkimpass}:DKIM pass",
+ 		'GPRINT:sdkimpass:MAX:total\: %8.0lf msgs',
+ 		'GPRINT:rdkimpass:AVERAGE:avg\: %5.2lf msgs/min',
+ 		'GPRINT:rmdkimpass:MAX:max\: %4.0lf msgs/min\l',
+ 		"DEF:dkimnone=$rrd:dkimnone:AVERAGE",
+ 		"DEF:mdkimnone=$rrd:dkimnone:MAX",
+ 		"CDEF:rdkimnone=dkimnone,60,*",
+ 		"CDEF:ddkimnone=dkimnone,UN,0,dkimnone,IF,$step,*",
+ 		"CDEF:sdkimnone=PREV,UN,ddkimnone,PREV,IF,ddkimnone,+",
+ 		"CDEF:rmdkimnone=mdkimnone,60,*",
+ 		"STACK:rdkimnone#$color{dkimnone}:DKIM none",
+ 		'GPRINT:sdkimnone:MAX:total\: %8.0lf msgs',
+ 		'GPRINT:rdkimnone:AVERAGE:avg\: %5.2lf msgs/min',
+ 		'GPRINT:rmdkimnone:MAX:max\: %4.0lf msgs/min\l',
+ 		"DEF:dkimfail=$rrd:dkimfail:AVERAGE",
+ 		"DEF:mdkimfail=$rrd:dkimfail:MAX",
+ 		"CDEF:rdkimfail=dkimfail,60,*",
+ 		"CDEF:ddkimfail=dkimfail,UN,0,dkimfail,IF,$step,*",
+ 		"CDEF:sdkimfail=PREV,UN,ddkimfail,PREV,IF,ddkimfail,+",
+ 		"CDEF:rmdkimfail=mdkimfail,60,*",
+ 		"LINE2:rdkimfail#$color{dkimfail}:DKIM fail",
+ 		'GPRINT:sdkimfail:MAX:total\: %8.0lf msgs',
+ 		'GPRINT:rdkimfail:AVERAGE:avg\: %5.2lf msgs/min',
+ 		'GPRINT:rmdkimfail:MAX:max\: %4.0lf msgs/min\l',
+ 	);
+ }
+
  [...]

+ 		print "<img src=\"$scriptname?${n}-s\" alt=\"mailgraph\"/></p>\n";
+ 		print "<img src=\"$scriptname?${n}-d\" alt=\"mailgraph\"/></p>\n";
+ 		print "<img src=\"$scriptname?${n}-k\" alt=\"mailgraph\"/></p>\n";

  [...]

+ 		elsif($img =~ /^(\d+)-s$/) {
+ 			my $file = "$tmp_dir/$uri/mailgraph_$1_spf.png";
+ 			graph_spf($graphs[$1]{seconds}, $file);
+ 			send_image($file);
+ 		}
+ 		elsif($img =~ /^(\d+)-d$/) {
+ 			my $file = "$tmp_dir/$uri/mailgraph_$1_dmarc.png";
+ 			graph_dmarc($graphs[$1]{seconds}, $file);
+ 			send_image($file);
+ 		}
+ 		elsif($img =~ /^(\d+)-k$/) {
+ 			my $file = "$tmp_dir/$uri/mailgraph_$1_dkim.png";
+ 			graph_dkim($graphs[$1]{seconds}, $file);
+ 			send_image($file);
+ 		}

Was die Patches tun

Der Daemon-Patch erweitert die RRD-Datenbank um neun Datasources und erkennt im Syslog die Ausgaben von drei Diensten:

DienstLog-PatternErgebnis
policy-spfReceived-SPF: pass/none/*spfpass, spfnone, spffail
opendmarcpass/none/faildmarcpass, dmarcnone, dmarcfail
opendkimDKIM verification successful / no signature data / bad signature datadkimpass, dkimnone, dkimfail

Der CGI-Patch erzeugt drei neue Graphen unterhalb der bestehenden. Jeder zeigt pass als Fläche, none als Stack darüber und fail als rote Linie. Die Zeiträume (Tag, Woche, Monat, Jahr) werden wie bei den Standard-Graphen automatisch erzeugt.

Hinweise

Die Patches sind für mailgraph 1.14 geschrieben. Weil mailgraph die RRD-Datasources beim ersten Start festlegt, muss nach dem Patchen die bestehende mailgraph.rrd gelöscht werden. Die alten Daten gehen dabei verloren, die neuen Graphen fangen bei Null an.

Es gibt auch einen separaten Patch für DANE-Graphen in mailgraph.

Wer heute mit dem Monitoring anfängt: rspamd bringt eine eigene Weboberfläche mit und prüft SPF, DKIM und DMARC in einem Durchgang. Zusammen mit Prometheus und Grafana bekommt man deutlich flexiblere Dashboards als mit RRDtool. Mailgraph bleibt trotzdem nützlich wenn man schnell was Schlankes braucht, das ohne externe Abhängigkeiten läuft.

Fragen oder Verbesserungsvorschläge? Einfach melden.

DMARC-Prüfung in Postfix: OpenDMARC und rspamd im Vergleich

Eine DMARC-Policy zu veröffentlichen ist die eine Seite. Die andere ist, eingehende Mails gegen die DMARC-Policy des Absenders zu prüfen. Postfix kann das nicht selbst. Dafür gibt es zwei Wege: OpenDMARC als eigenständigen Milter oder rspamd, der DMARC als eines von vielen Modulen mitbringt.

Variante 1: OpenDMARC

OpenDMARC ist ein dedizierter DMARC-Milter. Er liest die Ergebnisse von SPF und DKIM aus den Mailheadern und prüft ob die DMARC-Policy des Absenders erfüllt ist.

# Debian/Ubuntu
apt install opendmarc

# FreeBSD
pkg install opendmarc

Die Konfiguration in /etc/opendmarc.conf:

AuthservID mail.example.de
Socket inet:8893@localhost
UserID opendmarc
SoftwareHeader true
Syslog true
RejectFailures false
HistoryFile /var/run/opendmarc/opendmarc.dat

RejectFailures false ist ein guter Startwert. Damit werden Mails die den DMARC-Check nicht bestehen nicht sofort abgelehnt, sondern nur markiert. So kann man erst beobachten bevor man scharf schaltet. AuthservID muss zum Hostnamen des Mailservers passen.

Wichtig: OpenDMARC verlässt sich darauf, dass SPF und DKIM bereits geprüft wurden und die Ergebnisse in den Authentication-Results-Headern stehen. Die Reihenfolge der Milter in Postfix ist daher entscheidend: SPF und DKIM müssen vor OpenDMARC laufen.

Postfix-Integration

In der main.cf den Milter registrieren. OpenDMARC muss nach dem DKIM-Milter stehen:

# main.cf
smtpd_milters = inet:localhost:8891, inet:localhost:8893
non_smtpd_milters = inet:localhost:8891, inet:localhost:8893

Port 8891 ist hier OpenDKIM, Port 8893 OpenDMARC. Nach einem postfix reload prüft OpenDMARC jede eingehende Mail und schreibt einen Authentication-Results-Header mit dem DMARC-Ergebnis.

Variante 2: rspamd

Wer rspamd als Spamfilter einsetzt, braucht keinen separaten DMARC-Milter. rspamd prüft SPF, DKIM und DMARC in einem Durchgang und verrechnet das Ergebnis mit dem Gesamtscore. Das DMARC-Modul ist standardmäßig aktiv und unterstützt auch DMARC-Reporting (rua/ruf).

Der Vorteil von rspamd: Statt einer harten Ablehnung bei DMARC-Fail fließt das Ergebnis in die Gesamtbewertung ein. Eine Mail die DMARC nicht besteht aber sonst sauber aussieht, wird nicht sofort abgelehnt. Umgekehrt kann eine Mail die DMARC besteht trotzdem als Spam markiert werden wenn andere Indikatoren dagegen sprechen.

Was passt besser?

OpenDMARCrspamd
AufwandEigener Dienst, eigene ConfigBereits als Milter integriert
VerhaltenHarte Entscheidung (reject/accept)Score-basiert, flexibel
ReportingEigenes Reporting-Tool nötigrua-Reports eingebaut
AbhängigkeitenBraucht SPF/DKIM in den HeadernPrüft SPF/DKIM selbst

Für neue Setups ist rspamd meist die bessere Wahl, weil es SPF, DKIM, DMARC, ARC und Spam-Filterung in einem Paket liefert. OpenDMARC ist sinnvoll wenn man einen minimalen Stack ohne Content-Filter will oder bereits einen anderen Spamfilter einsetzt.

Übrigens: DMARC und Mailinglisten vertragen sich nicht ohne Weiteres. ARC löst das Problem. Fragen? Einfach melden.

Bash Schnipsel Message Size Limit

Ich muss gerade bei einigen Systemen den Message Size Limit von Postfix kontrollieren. Jetzt liegen die verschiedenen Kundendomains in verschiedenen Containern. Ich muss also immer nachschauen welcher, da ich faul bin hat mir folgender Einzeiler die Arbeit etwas erleichtert….

kernel@errorpc ~ $ ssh xyz-user@`dig kernel-error.de in MX +short|sed -e 's/^.\{,3\}//'` "postconf | grep message_size_limit"
message_size_limit = 104857600

Er sammelt sich per dig den gesetzten MX Eintrag der jeweiligen Domain aus dem DNS (es gibt nur einen pro Domain), schneidet die Prio und das Leerzeichen ab und übergibt den übrigen Hostname dann an ssh, welches sich direkt als xyz-user dort anmeldet, Postfix nach dem eingestellten Wert fragt und direkt wieder „auflegt“.

Passt natürlich nicht für jeden Fall, war aber hier wieder schön.

Fragen? Einfach melden.

Sichere SSL/TLS Konfiguration für ejabberd

Unsichere Protokolle und Chiper aus Postfix, Dovecot und Apache2 sind schonmal weg!

Jetzt ist mein xmpp Jabber Server ejabberd dran… Auch hier müssen die Protokolle SSLv2 sowie SSLv3 weichen. Nur alles größer TLSv1 ist erlaubt…. Bei den Chipern fliegt alles raus das „böse“ ist… Zum Beispiel MD5, RC4 usw. usw… Natürlich werden auch die ECDHE Cipher suite aktiviert um Forward secrecy zu unterstützen.

Test lässt sich alles am besten über die folgenden Links. Einmal aus der Sicht eines Client und einemal aus der Serversicht!
Client: https://xmpp.net/result.php?id=19649
Server: https://xmpp.net/result.php?id=19650

Next…

Siehe auch: Openfire: Unsichere TLS-Cipher deaktivieren

Fragen? Einfach melden.

Postfix SSL / TLS Cipher Suite Auswertung

Nachdem ich nun am Postfix hinsichtlich der SSL / TLS Sicherheit einige Einstellungen vorgenommen habe und sogar die nutzbare Cipher Suite eingeschränkt habe, da interessiert mich natürlich welche von den Clients genutzt werden. Daher fische ich für meinen Testzeitraum einfach mal per Regex (Regular expression) und egrep ein paar Infos aus dem Logfile und lege es hier zur Begutachtung hin.

So lässt sich nun gut verfolgen, welche Cipher, wie oft genutzt wird.

 

 

Siehe auch: TLS-Infos im E-Mail-Header

Fragen? Einfach melden.

Perfect Forward Secrecy für Postfix und Dovecot konfigurieren

Perfect Forward Secrecy (PFS) sorgt dafür, dass jede TLS-Verbindung einen eigenen temporären Schlüssel verwendet. Selbst wenn jemand den privaten Schlüssel des Servers in die Hände bekommt, kann er damit früher aufgezeichnete Verbindungen nicht entschlüsseln. Für E-Mail ist das besonders relevant, weil Mailverkehr gern auf Vorrat mitgeschnitten wird.

PFS bekommt man durch ECDHE- oder DHE-Schlüsselaustausch. Moderne OpenSSL-Versionen und aktuelle Postfix/Dovecot-Releases machen das Richtige von Haus aus. Trotzdem lohnt sich ein Blick auf die Konfiguration, um sicherzustellen, dass keine veralteten Protokolle oder Cipher-Suiten aktiv sind.

Postfix

Die relevanten Einstellungen in der main.cf:

# Eingehend (smtpd)
smtpd_tls_security_level = may
smtpd_tls_protocols = >=TLSv1.2
smtpd_tls_mandatory_protocols = >=TLSv1.2
smtpd_tls_mandatory_ciphers = medium

# Ausgehend (smtp)
smtp_tls_security_level = dane
smtp_tls_protocols = >=TLSv1.2
smtp_tls_mandatory_protocols = >=TLSv1.2
smtp_tls_mandatory_ciphers = medium

# Cipher-Präferenz beim Server
tls_preempt_cipherlist = yes

>=TLSv1.2 schaltet TLS 1.0 und 1.1 ab. tls_preempt_cipherlist = yes lässt den Server die Cipher-Reihenfolge bestimmen, nicht den Client. Postfix sortiert AEAD-Ciphers (GCM, CHACHA20) automatisch nach vorn, PFS-Ciphers haben Vorrang. Die medium-Stufe schließt alles unterhalb von 128-Bit-Verschlüsselung aus.

Für den ausgehenden Versand ist smtp_tls_security_level = dane die beste Wahl. Damit prüft Postfix TLSA-Records per DANE. Gibt es keinen TLSA-Record, fällt Postfix auf opportunistisches TLS zurück. Wer zusätzlich MTA-STS auswertet, deckt auch Server ohne DNSSEC ab.

Dovecot

In /usr/local/etc/dovecot/conf.d/10-ssl.conf (FreeBSD) bzw. /etc/dovecot/conf.d/10-ssl.conf (Linux):

ssl = required
ssl_min_protocol = TLSv1.2
ssl_prefer_server_ciphers = yes
ssl_options = no_ticket

ssl = required erzwingt TLS für alle Verbindungen. ssl_min_protocol = TLSv1.2 schließt alte Protokolle aus. ssl_prefer_server_ciphers = yes gibt dem Server die Kontrolle über die Cipher-Wahl. no_ticket deaktiviert TLS Session Tickets, die PFS untergraben können wenn der Ticket-Key kompromittiert wird.

Eine explizite Cipher-Liste ist bei aktuellem OpenSSL nicht nötig. Die Defaults bevorzugen ECDHE mit AEAD. Wer es trotzdem einschränken will:

ssl_cipher_list = ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:!aNULL:!eNULL
ssl_cipher_suites = TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256

ssl_cipher_list gilt für TLS 1.2, ssl_cipher_suites für TLS 1.3. Bei TLS 1.3 ist PFS immer aktiv, da gibt es keine Cipher-Suiten ohne Schlüsselaustausch mehr.

Testen

# Postfix (STARTTLS auf Port 25)
openssl s_client -starttls smtp -connect smtp.kernel-error.de:25 2>/dev/null | grep -E "Protocol|Cipher|Server Temp Key"

# Postfix (Implicit TLS auf Port 465)
openssl s_client -connect smtp.kernel-error.de:465 2>/dev/null | grep -E "Protocol|Cipher|Server Temp Key"

# Dovecot (STARTTLS auf Port 143)
openssl s_client -starttls imap -connect imap.kernel-error.de:143 2>/dev/null | grep -E "Protocol|Cipher|Server Temp Key"

# Dovecot (Implicit TLS auf Port 993)
openssl s_client -connect imap.kernel-error.de:993 2>/dev/null | grep -E "Protocol|Cipher|Server Temp Key"

Entscheidend ist die Zeile Server Temp Key. Steht dort X25519, ECDH P-256 oder DH 2048, ist PFS aktiv. Mit OpenSSL 3.5+ und entsprechender Konfiguration sieht man dort auch X25519MLKEM768 für Post-Quantum-Schlüsselaustausch.

Siehe auch: Post-Quantum TLS für E-Mail

Fragen? Einfach melden.

Postfix mit DANE und DNSSEC absichern

Postfix-Mailserver mit DANE und DNSSEC, TLSA-Record-Prüfung, DNS als Vertrauensanker und verifizierter SMTP-TLS-Verbindung zwischen Mailservern.

Meine Domains sind schon lange per DNSSEC gesichert. Für den Webserver hatte ich auch schnell TLSA-Records veröffentlicht, damit die TLS-Verbindung per DANE abgesichert ist. Seit Postfix 2.11 beherrscht auch der MTA die Prüfung von TLSA-Records, damit lässt sich ausgehende E-Mail-Verschlüsselung kryptographisch verifizieren, ohne sich auf das CA-System verlassen zu müssen.

Postfix konfigurieren

Zwei Zeilen in der main.cf reichen, damit Postfix beim Versand TLSA-Records prüft:

postconf -e "smtp_dns_support_level = dnssec"
postconf -e "smtp_tls_security_level = dane"

smtp_dns_support_level = dnssec weist den SMTP-Client an, DNSSEC-validierte DNS-Antworten zu verlangen. smtp_tls_security_level = dane aktiviert die TLSA-Prüfung, Postfix sucht automatisch nach TLSA-Records für den Zielserver. Gibt es keinen TLSA-Record oder kein DNSSEC, fällt Postfix auf opportunistisches TLS zurück. Die Kommunikation mit Servern ohne DANE leidet also nicht. Details stehen in der Postfix DANE-Dokumentation.

TLSA-Record erstellen

Der TLSA-Record enthält einen Hash des TLS-Zertifikats (oder des Public Keys), den der Empfänger-Mailserver im DNS veröffentlicht. So erzeugt man den SHA-256-Hash des Public Keys aus dem Zertifikat:

openssl x509 -in postfix.pem -noout -pubkey \
  | openssl pkey -pubin -outform DER \
  | openssl dgst -sha256

Den Hash trägt man als TLSA-Record in die DNS-Zone ein, für Port 25 (SMTP) und optional Port 465 (Implicit TLS):

_25._tcp.smtp.kernel-error.de.  3600 IN TLSA 3 1 1 8cb0fc6c527506a053f4f14c8464bebbd6dede2738d11468dd953d7d6a3021f1
_465._tcp.smtp.kernel-error.de. 3600 IN TLSA 3 1 1 8cb0fc6c527506a053f4f14c8464bebbd6dede2738d11468dd953d7d6a3021f1

Die Felder 3 1 1 bedeuten: DANE-EE (End Entity, kein CA-Vertrauen nötig), SPKI (nur Public Key, nicht das ganze Zertifikat), SHA-256. Der TTL von 3600 Sekunden ist bewusst kurz, bei einem Zertifikatswechsel soll der alte Record schnell ablaufen. Mehr zum Aufbau von TLSA-Records steht in RFC 7672.

Testen

Mit posttls-finger (Teil von Postfix) lässt sich prüfen, ob die DANE-Verifikation funktioniert:

posttls-finger -t30 -T180 -c -L verbose,summary kernel-error.de

Die entscheidende Zeile in der Ausgabe:

Verified TLS connection established to smtp.kernel-error.de[...]:25:
  TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)

Verified statt Untrusted, der TLSA-Record stimmt mit dem Zertifikat überein, die Verbindung ist DANE-verifiziert. Im Postfix-Log sieht man denselben Unterschied:

smtp[3779]: Verified TLS connection established to mx02.example.de[...]:25
smtp[3779]: Untrusted TLS connection established to smtp2.example.de[...]:25

Verified = DANE-geprüft und in Ordnung. Untrusted = TLS aktiv, aber kein gültiger TLSA-Record vorhanden, die Verschlüsselung funktioniert trotzdem, nur ohne kryptographische Verifikation des Zertifikats.

Warum DANE besser ist als das CA-System

DANE verankert das Vertrauen im DNS statt bei Certificate Authorities. Der Domaininhaber veröffentlicht den Hash seines Zertifikats selbst, DNSSEC schützt den Record vor Manipulation. Keine CA kann ein falsches Zertifikat für die Domain ausstellen und damit die Verbindung aufbrechen. Das Ganze kostet nichts außer einem DNS-Record und funktioniert für jeden, der DNSSEC betreibt.

Zum Vergleich: Das damalige „E-Mail made in Germany“ (EmiG) setzte auf eine TÜV-Zertifizierung, die sich nur große Provider leisten konnten. Sicherheit nur für Unternehmen mit Budget, das ist das Gegenteil von dem, was ich unterstütze.


Update August 2014: Ich habe Mailgraph um DANE-Graphen erweitert, um den Anteil verifizierter Verbindungen zu visualisieren.

Siehe auch: TLSA/DANE-Records prüfen, TLSA- und DANE-Records manuell prüfen: Schritt für Schritt mit OpenSSL, Postfix und DANE: „Server certificate not verified“ debuggen, Postfix: Eingehende E-Mails ohne TLS ablehnen

Fragen? Einfach melden.

Outlook Autodiscover für IMAP/SMTP: Wie die automatische Kontoeinrichtung funktioniert

Benutzer wollen ihr E-Mail-Postfach einrichten. Sie geben E-Mail-Adresse und Passwort ein — den Rest soll der Client erledigen. Bei Exchange mit Active Directory funktioniert das seit Jahren automatisch. Aber was, wenn der Mailserver Postfix und Dovecot heißt und kein Exchange in Sicht ist?

Microsoft Outlook unterstützt auch für IMAP und SMTP die automatische Konfiguration per Autodiscover. Man muss nur wissen, wo Outlook nachschaut — und dort die richtigen Antworten bereithalten.

Outlook Autodiscover für IMAP und SMTP – automatische Mailkonto-Konfiguration über DNS, HTTPS und XML

Wo Outlook nach der Konfiguration sucht

Outlook arbeitet eine feste Reihenfolge ab. Sobald ein Schritt eine gültige Konfiguration liefert, ist die Einrichtung fertig. Schlägt ein Schritt fehl, geht es zum nächsten:

1. Active Directory (SCP) — Ist der Rechner Domänenmitglied, fragt Outlook per LDAP nach einem Service Connection Point. Dort steht normalerweise die URL des Exchange-Servers. Für reine IMAP-Setups irrelevant.

2. HTTPS auf der E-Mail-Domain — Outlook versucht https://example.org/autodiscover/autodiscover.xml. Funktioniert nur, wenn der Webserver der Domain den Pfad bedient.

3. HTTPS auf autodiscover.<domain> — Outlook versucht https://autodiscover.example.org/autodiscover/autodiscover.xml. Das ist der Weg, den wir nutzen. Ein Webserver unter diesem Hostnamen mit gültigem TLS-Zertifikat — mehr braucht es nicht.

4. HTTP-Redirect — Outlook versucht http://autodiscover.example.org/autodiscover/autodiscover.xml und folgt einem 301/302-Redirect. Weniger sicher, aber ein Fallback.

5. DNS SRV-Record — Outlook fragt _autodiscover._tcp.example.org und folgt dem SRV-Eintrag. Bei einem SRV-Verweis auf eine andere Domain zeigt Outlook eine Sicherheitsabfrage: „Konfiguration von dieser Webseite zulassen?“ Einmalig bestätigen, danach läuft es.

6. Lokale XML-Datei — Outlook sucht auf dem Rechner nach einer vorinstallierten Konfigurationsdatei. Für Firmenumgebungen mit Software-Verteilung relevant.

7. Manueller Assistent — Wenn nichts funktioniert hat, öffnet Outlook den Konfigurationsassistenten. Genau das wollen wir vermeiden.

Was Outlook per POST schickt und erwartet

Outlook macht einen HTTP-POST auf /autodiscover/autodiscover.xml und schickt im Body ein XML mit der E-Mail-Adresse des Benutzers:

<Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006">
  <Request>
    <EMailAddress>benutzer@example.org</EMailAddress>
    <AcceptableResponseSchema>
      http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a
    </AcceptableResponseSchema>
  </Request>
</Autodiscover>

Die Antwort enthält IMAP- und SMTP-Einstellungen. Der Clou: Weil Outlook die E-Mail-Adresse im POST mitschickt, kann ein PHP-Script sie auslesen und als <LoginName> in die Antwort einsetzen. Ohne diesen Trick würde Outlook nur den Teil vor dem @ als Benutzernamen verwenden — bei Mailservern, die die volle E-Mail-Adresse als Login erwarten, ein Problem.

Multi-Domain per DNS SRV-Record

Ein Autodiscover-Webserver reicht für beliebig viele Maildomains. Jede zusätzliche Domain braucht nur einen SRV-Record im DNS:

_autodiscover._tcp.example.org.  IN SRV 0 0 443 autodiscover.kernel-error.de.

Outlook folgt dem SRV-Record, zeigt einmalig die Sicherheitsabfrage, und hat danach die komplette Konfiguration. Das PHP-Script auf dem Zielserver beantwortet Anfragen für alle Domains — die E-Mail-Adresse kommt ja im POST mit.

Umsetzung und aktuelle Konfiguration

Die konkrete Einrichtung — nginx-Konfiguration, PHP-Script und DNS — beschreibe ich im Nachfolgebeitrag: Outlook Autodiscover für IMAP und SMTP konfigurieren. Dort steht auch das Update von 2026 mit den Korrekturen am PHP-Script (GET-Absicherung, XSS-Schutz) und der Zusammenlegung mit Thunderbird Autoconfig.

Wer seine Konfiguration testen will: Microsoft bietet unter testconnectivity.microsoft.com den „Microsoft Remote Connectivity Analyzer“ an. Dort den Autodiscover-Test auswählen und die eigene E-Mail-Adresse eingeben.

Fragen? Gerne per Kontaktformular.

« Ältere Beiträge Neuere Beiträge »

© 2026 -=Kernel-Error=-RSS

Theme von Anders NorénHoch ↑