Postfix cert based relay

From Asenjo
Jump to: navigation, search

In order to securely allow roaming smtp clients to relay through a postfix smtp server one common setup is using SASL authentication in combination with starttls and a (usually virtual) user database.

There are plenty of info about how to set that up so I will not do it here.

What not many people know is that you can setup postfix to allow relaying using a certificates (PKI).

Postfix has two ways of allowing relaying with certificates, but here I will only specify one.


in order to allow relaying we need to have some settings in place:

starttls

# TLS  SERVER settings

# offer tls to clients
smtpd_use_tls = yes

local cert and key

in this case I use the excellent startssl.com free certificates because they are trusted by most devices and they are free. The smtpd_tls_cert_file has the startssl chain cert (just cat your.cert startssl.crt > postfix.crt to get it).

The smtpd_tls_key_file should be readonly for root. I share this key with apache, so it is alse readonly for apache, but not for the rest (440 perms).

# local cert
smtpd_tls_key_file = /etc/pki/tls/private/startssl_asenjo_nl.key
smtpd_tls_cert_file = /etc/pki/tls/certs/postfix_certchain.crt

trusted CA bundly file

centos sets it here:

# CA bundle
smtpd_tls_CAfile = /etc/pki/tls/certs/ca-bundle.crt

entropy generator , logs, headers

# random source generator
tls_random_source = dev:/dev/urandom

# log level tls
# 0 default no logging
# 1 startup and cert info
# 2: 1 + info on tls negotiation
# 3: 2 + hex and ascii dumps negotiation
# 4: 3 + hex and ascii dumps trasnmission after client starttls
smtpd_tls_loglevel = 1

# add tls header info
smtpd_tls_received_header = yes

tls caching, tls ciphers

# tls session cache
smtpd_tls_session_cache_database = btree:$data_directory/smtpd_cache
smtpd_tls_session_cache_timeout = 3600s

# disable insecure ciphers
smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3
smtpd_tls_protocols = !SSLv2, !SSLv3
smtp_tls_mandatory_protocols = !SSLv2, !SSLv3
smtp_tls_protocols = !SSLv2, !SSLv3

server side certificate based relaying

Just three settings:

# ask for certificates:
smtpd_tls_ask_ccert = yes

# these certs may relay
relay_clientcerts = hash:/etc/postfix/relay_clientcerts
smtpd_tls_fingerprint_digest = sha1

the file relay_clientcerts is a normal postfix hash database with in the left hand side the fingerprint and on the right hand side any field we want. The most logical thing to put in there is the name of the owner of the certificate because locating one based just on its fingerprint is more involved ;-)

So use your favourite tool to find the fingerprint of the certificates you want to allow relaying through your server and create that file like this:

AB:9D:0F:F6...rest of fingerprint name_owner

After you are done, postmap the file like with any other postfix hash database.

smtpd restrictions

finally, one needs to allow the people using the certificates to relay. You can accomplish this like so:

# smtpd client restrictions
smtpd_client_restrictions = 
        permit_tls_clientcerts,
        reject_rbl_client zen.spamhaus.org

# smtpd recipient restriction
smtpd_recipient_restrictions =
                         permit_tls_clientcerts,
                         reject_non_fqdn_recipient,
                         reject_non_fqdn_sender,
                         permit_mynetworks,
                         reject_unauth_destination,
                         reject_rbl_client zen.spamhaus.org,
                         check_policy_service unix:postgrey/socket,

so we add permit_tls_clientcerts before other reject directives (the first one wins) and after that you can reload postfix. If everything went fine we should be able to relay from our clients.

test certificate authority

create test CA with openssl tools

you can totally skip this step if you already have a working PKI in place like Active Directory Certificate Services or Dogtag in the freeipa.org project.

Plenty of tutorials on how to create a test CA using openssl. I used this one but others may work as well. The tldr; version is:

openssl genrsa -out rootCA.key 4096
openssl req -x509 -new -nodes -key rootCA.key -days 365 -out rootCA.crt -subj '/C=NL/ST=Gelderland/L=Arnhem/CN=myhost'
openssl genrsa -out myuser.key 4096 -subj '/C=NL/ST=Gelderland/L=Arnhem/CN=myuser'
openssl req -new -key myuser.key -out myuser.csr  -subj '/C=NL/ST=Gelderland/L=Arnhem/CN=myuser'
openssl x509 -req -in myuser.csr -CA rootCA.crt -CAkey rootCA.key -CAcreateserial -out myuser.crt -days 365

So we create a CA key, a CA certificate. The we create a user key file, genereate a certificate signing request for that user and sing it. Obviously replace the NL stuff with your own stuff. The result is a directory with all those files.

add the rootCA.crt to the trusted CA's file in the postix server

Now we have a CA, and a user (or host or whatever) certificate that we can use in combination with postfix. In order to avoid those annoying invalid/unknown certificate warnings, we import the rootCA.crt in the postfix host. In RHEL/centos it is quite simple, just drop that file in /etc/pki/ca-trust/source/anchors and run update-ca-trust-enable. Done.

verify rootCA.crt imported in ca bundle file

You can verify it's in there using this simple perl script found in serverfault

#!/usr/bin/perl
# script for splitting multi-cert input into individual certs
# Artistic Licence
#
# v0.0.1         Nick Burch <nick@tirian.magd.ox.ac.uk>
# v0.0.2         Tom Yates <tyates@gatekeeper.ltd.uk>
#

$filename = shift;
unless($filename) {
  die("You must specify a cert file.\n");
}
open INP, "<$filename" or die("Unable to load \"$filename\"\n");

$thisfile = "";

while(<INP>) {
   $thisfile .= $_;
   if($_ =~ /^\-+END(\s\w+)?\sCERTIFICATE\-+$/) {
      print "Found a complete certificate:\n";
      print `echo "$thisfile" | openssl x509 -noout -text`;
      $thisfile = "";
   }
}
close INP;

in our case just run it with as first argument the path to the ca bundle file and pipe it to less so you can scroll and search for the name of your CA.

Once the test rootCA is known to the postfix server we can test if this all works. You can try using thunderbird but I thought using some programming languages could be a bit more useful (and fun).

perl client

the key file should *not* be readable but by the user running this script. You need to have the IO::Socket::SSL library which in centos is is perl-IO-Socket-SSL.noarch. Net::SMTP is part of the core libraries.

#!/usr/bin/env perl 

use strict;
use warnings;
use Net::SMTP;

my $smtp = Net::SMTP->new(
    Host    => 'host.domain.tld',
    Hello   => 'hi there',
    Timeout => 5,
    Debug   => 4,
);

$smtp->starttls(
    SSL_cert_file   => "/path/to/myuser.crt",
    SSL_key_file    => "/path/to/myuser.key",
);

$smtp->mail("user\+perl\@domain\.tld\n");
$smtp->to("user\@gmail\.com\n");
$smtp->data;
$smtp->datasend("From: Little John <user\@domain.tld>\n");
$smtp->datasend("To: Big John <suer\@gmail.com>\n");
$smtp->datasend("Subject: certificate based relay testing\n");
$smtp->datasend("MIME-Version: 1.0\n");
$smtp->datasend("Content-Type: text/plain; charset=us-ascii\n");
$smtp->datasend("X-Mailer: Net::SMTP IO::Socket::SSL\n");
$smtp->datasend( "X-mydate: " . localtime() . "\n" );
$smtp->datasend("\n");
$smtp->datasend("testing again\n");
$smtp->dataend;
$smtp->quit;

Now run it and you should see something like this:

$ perl script.pl 
Net::SMTP>>> Net::SMTP(3.07)
Net::SMTP>>>   Net::Cmd(3.07)
Net::SMTP>>>     Exporter(5.72)
Net::SMTP>>>   IO::Socket::IP(0.37)
Net::SMTP>>>     IO::Socket(1.37)
Net::SMTP>>>       IO::Handle(1.35)
Net::SMTP=GLOB(0x237d4e0)<<< 220 host.domain.tld ESMTP Postfix
Net::SMTP=GLOB(0x237d4e0)>>> EHLO hi there
Net::SMTP=GLOB(0x237d4e0)<<< 250-host.domain.tld
Net::SMTP=GLOB(0x237d4e0)<<< 250-PIPELINING
Net::SMTP=GLOB(0x237d4e0)<<< 250-SIZE 10240000
Net::SMTP=GLOB(0x237d4e0)<<< 250-VRFY
Net::SMTP=GLOB(0x237d4e0)<<< 250-ETRN
Net::SMTP=GLOB(0x237d4e0)<<< 250-STARTTLS
Net::SMTP=GLOB(0x237d4e0)<<< 250-ENHANCEDSTATUSCODES
Net::SMTP=GLOB(0x237d4e0)<<< 250-8BITMIME
Net::SMTP=GLOB(0x237d4e0)<<< 250 DSN
Net::SMTP=GLOB(0x237d4e0)>>> STARTTLS
Net::SMTP=GLOB(0x237d4e0)<<< 220 2.0.0 Ready to start TLS
Net::SMTP::_SSL=GLOB(0x237d4e0)>>> EHLO hi there
Net::SMTP::_SSL=GLOB(0x237d4e0)<<< 250-host.domain.tld
Net::SMTP::_SSL=GLOB(0x237d4e0)<<< 250-PIPELINING
Net::SMTP::_SSL=GLOB(0x237d4e0)<<< 250-SIZE 10240000
Net::SMTP::_SSL=GLOB(0x237d4e0)<<< 250-VRFY
Net::SMTP::_SSL=GLOB(0x237d4e0)<<< 250-ETRN
Net::SMTP::_SSL=GLOB(0x237d4e0)<<< 250-ENHANCEDSTATUSCODES
Net::SMTP::_SSL=GLOB(0x237d4e0)<<< 250-8BITMIME
Net::SMTP::_SSL=GLOB(0x237d4e0)<<< 250 DSN
Net::SMTP::_SSL=GLOB(0x237d4e0)>>> MAIL FROM:<user+perl@domain.tld>
Net::SMTP::_SSL=GLOB(0x237d4e0)<<< 250 2.1.0 Ok
Net::SMTP::_SSL=GLOB(0x237d4e0)>>> RCPT TO:<user@gmail.com>
Net::SMTP::_SSL=GLOB(0x237d4e0)<<< 250 2.1.5 Ok
Net::SMTP::_SSL=GLOB(0x237d4e0)>>> DATA
Net::SMTP::_SSL=GLOB(0x237d4e0)<<< 354 End data with <CR><LF>.<CR><LF>
Net::SMTP::_SSL=GLOB(0x237d4e0)>>> From: Big John <user@domain.tld>
Net::SMTP::_SSL=GLOB(0x237d4e0)>>> To: Little John <user@gmail.com>
Net::SMTP::_SSL=GLOB(0x237d4e0)>>> Subject: certificate based relay testing
Net::SMTP::_SSL=GLOB(0x237d4e0)>>> MIME-Version: 1.0
Net::SMTP::_SSL=GLOB(0x237d4e0)>>> Content-Type: text/plain; charset=us-ascii
Net::SMTP::_SSL=GLOB(0x237d4e0)>>> X-Mailer: Net::SMTP IO::Socket::SSL
Net::SMTP::_SSL=GLOB(0x237d4e0)>>> X-mydate: Wed Jul 22 21:28:02 2015
Net::SMTP::_SSL=GLOB(0x237d4e0)>>> testing again
Net::SMTP::_SSL=GLOB(0x237d4e0)>>> .
Net::SMTP::_SSL=GLOB(0x237d4e0)<<< 250 2.0.0 Ok: queued as 5493F8008A
Net::SMTP::_SSL=GLOB(0x237d4e0)>>> QUIT
Net::SMTP::_SSL=GLOB(0x237d4e0)<<< 221 2.0.0 Bye

In your postfix mail logs you should see something like this:

Jul 22 21:28:02 host postfix/smtpd[32469]: connect from host.domain.tld [xx.xx.xx.xx]
Jul 22 21:28:02 host postfix/smtpd[32469]: setting up TLS connection from host.domain.tld [xx.xx.xx.xx]
Jul 22 21:28:02 host postfix/smtpd[32469]: host.domain.nl[xx.xx.xx.xx]: Trusted: subject_CN=myuser, issuer=myhost, fingerprint=AB:9D:0F:F6:BA:52........rest of fingerprint
Jul 22 21:28:02 host postfix/smtpd[32469]: Trusted TLS connection established from host.domain.nl[xx.xx.xx.xx]: TLSv1.2 with cipher AES128-SHA256 (128/128 bits)
Jul 22 21:28:02 host postfix/smtpd[32469]: 5493F8008A: client=host.domain.nl[xx.xx.xx.xx]
Jul 22 21:28:02 host postfix/cleanup[32474]: 5493F8008A: message-id=<>
Jul 22 21:28:02 host postfix/qmgr[26512]: 5493F8008A: from=<user+perl@host.com>, size=596, nrcpt=1 (queue active)
Jul 22 21:28:02 host postfix/smtpd[32469]: disconnect from host.domain.nl[xx.xx.xx.xx]
Jul 22 21:28:02 host postfix/smtp[32476]: setting up TLS connection to gmail-smtp-in.l.google.com[173.194.65.26]:25
Jul 22 21:28:02 host postfix/smtp[32476]: Trusted TLS connection established to gmail-smtp-in.l.google.com[173.194.65.26]:25: TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)
Jul 22 21:28:03 host postfix/smtp[32476]: 5493F8008A: to=<user@gmail.com>, relay=gmail-smtp-in.l.google.com[173.194.65.26]:25, delay=1.3, delays=0.1/0.05/0.42/0.69, dsn=2.0.0, status=sent (250 2.0.0 OK 1437593283 tn8si4104968wjc.133 - gsmtp)
Jul 22 21:28:03 host postfix/qmgr[26512]: 5493F8008A: removed

And in the inbox of your gmail account you should see a message relayed from your postfix server.

The IO::Socket::SSL is pretty picky and you should use exactly the exact hostname in the subject of the postfix server certificate or you will get client certificate errors.

python client

the python standard library has everything we need

import smtplib

from email.mime.text import MIMEText

fp = open('kk.pl', 'rb')
msg = MIMEText(fp.read())

msg['Subject'] = 'the contents of %s' % 'kk.pl'
msg['From'] = 'user@domain.tld'
msg['To'] = 'user@gmail.com'

fp.close()
s = smtplib.SMTP('host.domain.tld')
s.ehlo()
s.starttls('myuser.key', 'myuser.crt')
s.ehlo()
s.sendmail('user+python@domain.tld, 'user@gmail.com', msg.as_string())
s.quit

In this case I use a text file (in fact the perl script) as the message we are sending, I just copied one of the examples of the python documenation and adapted it a bit. The python library is not so picky about the host name, it works with a different A record than the one in the certificate subject.

php client

this one is not yet fully functional. I get to relay it through postfix but I keep some errors about 'improper command pipelining after RCPT' and the connection gets dropped sometimes. But it's a start.

Something to be careful about in the php tls library is that the local_cert includes both the certificate and the key, so just concatenate them using cat to a new file and as always, do not make that world readable :-)

<?php
$server   = "host.domain.tld";
$myself   = "myuser.example.com"; 
$cabundle = '/etc/pki/ca-trust/extracted/openssl/ca-bundle.trust.crt';  
$localcert = '/path/to/user.crt';
$env_from = "user@domain.tld";
$env_to = "user@gmail.com";
$header_from = 'Little John <$env_from>';
$header_to =  'Big John <$env_to>';

// Establish the connection
$smtp = fsockopen( "tcp://$server", 25, $errno, $errstr );
fread( $smtp, 512 );

fwrite($smtp,"HELO $myself\r\n");
fread($smtp, 512);


// Switch to TLS
fwrite($smtp,"STARTTLS\r\n");
fread($smtp, 512);
stream_set_blocking($smtp, true);
stream_context_set_option($smtp, 'ssl', 'verify_peer', true);
stream_context_set_option($smtp, 'ssl', 'allow_self_signed', false);
stream_context_set_option($smtp, 'ssl', 'capture_peer_cert', true);
stream_context_set_option($smtp, 'ssl', 'cafile', $cabundle);
stream_context_set_option($smtp, 'ssl', 'local_cert', $localcert );
$secure = stream_socket_enable_crypto($smtp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
stream_set_blocking($smtp, false);

fwrite( $smtp, "mail from: <" . $env_from . ">" . "\r\n");
fwrite( $smtp, "rcpt to: <" . $env_to . ">" . "\r\n");
fwrite($smtp, 'DATA'."\r\n");
fwrite($smtp, 'Subject: hi there' . "\r\n");
fwrite($smtp, '.' . "\r\n");

?>

In all cases If you remove the starttls bits with the cert/key I get this:

Net::SMTP=GLOB(0x17de9c8)<<< 554 5.7.1 Service unavailable; Client host [xx.xx.xx.xx] blocked using zen.spamhaus.org; http://www.spamhaus.org/query/bl?ip=xx.xx.xx.xx

So postfix does not allow relaying in this case because I am in a consumer ip block.