In diesem Beitrag beschreibe ich, wie ich für alle VMs zentral einen Mailserver eingerichtet habe, der zum Versand der E-Mails nach außen zuständig ist. So muss ich die Verwendung eines Smarthost nur einmal konfigurieren. Alle anderen VMs dürfen keine Mails raus schicken - nur über den Mailserver.

Bisherige Beiträge in der Reihe:

Voraussetzungen

  • Ein Account bei einem Smarthost, also bei einem Mailserver, der SMTP im Internet macht und dort auf keiner Blacklist steht. Ich nehme dafür Sendgrid, habe aber keine umfangreichen Recherchen unternommen, da den besten Anbieter zu finden. Bei Sendgrid kann man bis zu 100 Mails am Tag im kostenfreien Account versenden. Nachdem ich nicht vor habe, meine privaten Mails über meinen Server laufen zu lassen sondern nur Benachrichtigungsmails von internen Diensten herumschicken möchte, reicht das locker.
  • Eine Domain. Es geht sicherlich irgendwie auch ohne aber wenn man eine hat und dort Dinge wie SPF selbst konfigurieren kann, ist alles viel einfacher. Notfalls holt man sich bei Freenom eine mit seltsamem Suffix und macht DNS über CloudFlare.
  • Funktionierendes Puppet-Setup (siehe Link oben), im Idealfall außerdem bereits funktionierende SSL-Zertifikate von Let’s Encrypt

Beschreibung des Setups

Wie oben kurz abgerissen, plane ich, in der Firewall alle Kommunikation mit den üblichen Ports 25, 465 und 587 zu sperren und nur der einen VM, die als ausgehender Mailserver dienen soll,auf diesen Ports Traffic zu erlauben und dann auch nur zum Smarthost. Diese Sicherheitsmaßnahmen dienen nicht so sehr dem Schutz vor Spammern in meinem (vollkommen virtuellen) eigenen Netzwerk sondern vor unkontrolliertem Mailfluß nach Außen durch unzureichend konfigurierte Dienste.

Die verschiedenen VMs authentifizieren nicht mit Benutzernamen und Passwörtern mit dem zentralen Mailserver sondern mit SSL-Clientzertifikaten, weil sich das schöner und sinnvoller automatisieren lässt. Grusliger Trick an der Stelle: Ich verwende dazu einfach die von Puppet. Prinzipielles Problem an so einer Sache ist immer, die privaten Schlüssel generieren zu lassen, den CSR (Zertifikatsanfrage) zu prüfen und im Erfolgsfall zu signieren und wieder zurück an den Client zu übertragen. Genau auf diese Art und Weise funktioniert auch die Autentifizierung bei Puppet. Der MTA auf den VMs wird sowieso als root starten, also kann er auch gleich die SSL-Zertifikate und -Schlüssel lesen.

Als Software verwende ich Postfix, weil ich damit die meiste Erfahrung habe und es ein ganz gutes Modul dazu in der Puppet Forge gibt.

Der zentrale Server authentifiziert gegenüber Sendgrid mit Benutzername und Passwort, alles über TLS. Mails, die auf irgend einer VM irgendwann mal an root gingen, landen in meinem normalen Mail-Postfach.

Aufsetzen des Mailservers

Für den Mailserver setze ich einen neuen Ubuntu 16.04-Container auf und lasse mein normales Puppet-Setup drüber laufen. Ich weise dabei auch das Profil ssl zu (siehe Anleitung ganz oben), damit der Server ein gültiges Zertifikat für StartTLS aufweisen kann. Das ist nicht notwendig, vor allem nicht, weil bei mir alles virtuell läuft, aber gute Praxis und wer weiß, welche VM mal irgendwohin umzieht. Als Hostname wähle ich einfach mal mailout.domain.tld.

Der ganze Rest funktioniert jetzt über Puppet. Wie genau Ihr Euren Puppet-Code auf den Puppet Server bringt, ist wohl bei allen unterschiedlich. Ich verwende r10k und eine Strkturierung in Roles und Profiles. Andere nehmen nur git. Nachdem es zahlreiche Methoden gibt, beschreibe ich hier das Setup direkt auf dem Server, Ihr müsst für Euer Setup übersetzen.

Wir brauchen zwei Module aus der Forge, also führen wir auf dem Puppet Server aus:

sudo puppet module install camptocamp-augeas
sudo puppet module install camptocamp-postfix

Wir legen ein neues Profil an:

sudo mkdir -p /etc/puppetlabs/code/modules/profiles/{manifests,data}

In die dazugehörge hiera.yaml des Moduls nur kurz:

# Datei /etc/puppetlabs/code/modules/profiles/hiera.yaml
---
version: 5
defaults:
  datadir: data
  data_hash: yaml-data

hierarchy:
  - name: Common
    path: 'common.yaml'

Nun schreiben wir das Profil mailrelay in Puppet-Code:

# Datei /etc/puppetlabs/puppet/code/modules/profiles/manifests/mailrelay.pp
class profiles::mailrelay {
  $relayhost      = lookup('profiles::mailrelay::relayhost')
  $relayport      = lookup('profiles::mailrelay::relayport', String, 'first', '587')
  $relay_user     = lookup('profiles::mailrelay::relay_user')
  $relay_password = lookup('profiles::mailrelay::relay_password')
  $root_mail_recipient = lookup('profiles::mailrelay::root')

  class { 'postfix':
    smtp_listen         => 'all',
    master_submission   => 'submission inet n       -       -       -       -       smtpd',
    relayhost           => "[${relayhost}]:${relayport}",
    myorigin            => $facts['domain'],
    root_mail_recipient => $root_mail_recipient,
  }

  postfix::config {
    'mydomain'                   : value => $facts['domain'];
    'smtp_sasl_auth_enable'      : value => 'yes';
    'smtp_sasl_security_options' : value => 'noanonymous';
    'smtp_sasl_password_maps'    : value => 'hash:/etc/postfix/sasl_password';
    'smtp_tls_security_level'    : value => 'encrypt';
    'smtpd_tls_security_level'   : value => 'encrypt';
    'smtpd_tls_cert_file'        : value => "/etc/letsencrypt/certs/${facts['fqdn']}_fullchain.pem";
    'smtpd_tls_key_file'         : value => "/etc/letsencrypt/private/${facts['fqdn']}.key";
    'smtpd_client_restrictions'  : value => 'permit_tls_all_clientcerts,reject';
    'smtpd_relay_restrictions'   : value => '$smtpd_client_restrictions';
    'smtpd_tls_CAfile'           : value => '/etc/puppetlabs/puppet/ssl/certs/ca.pem';
    'smtpd_tls_ask_ccert'        : value => 'yes';
    'smtpd_tls_req_ccert'        : value => 'yes';
    'smtpd_enforce_tls'          : value => 'yes';
  }

  postfix::hash { '/etc/postfix/sasl_password':
    ensure  => present,
    content => "${relayhost} ${relay_user}:${relay_password}",
  }

}

Der Reihe nach erklärt:

  1. Die Konfiguration wird geladen. Ist kein Port für den Smarthost vorgegeben, wird 587 (Submission) verwendet.
  2. Die Klasse postfix wird instanziert und grob vorkonfiguriert:
    • auf allen Netzwerkinterfaces wird gelauscht.
    • der Port 587 (Submission) wird geöffnet.
    • der relayhost wird ohne Nachfrage nach MX-Records (deswegen die eckigen Klammern) konfiguriert.
    • als Absender-Domain wird die lokale Domain, also im Fall des Beispiels hier domain.tld verwendet. Das ergibt sich aus dem Hostname.
    • Mails an root werden umgeleitet.
  3. Postfix wird fein konfiguriert, hauptsächlich was TLS und Auth angeht:
    • nochmal Domain sicherstellen.
    • Ausgehende Mails zum Smarthost:
      • Mit Benutzername und Kennwort authentifizieren.
      • smtp_sasl_security_options wäre eigentlich noplaintext noanonymous, Sendgrid möchte aber Authentifizierung in Plain Text, dafür über TLS. Das ist in Ordnung, muss man aber in Postfix erst konfigurieren.
      • Die Datei mit Benutzername und Passwort für den Smarthost wird eingebunden.
      • TLS-Verschlüsselung aktivieren.
    • Eingehende Mails von VMs:
      • TLS-Verschlüsselung aktivieren.
      • Das Zertifikat von Let’s Encrypt wird für StartTLS verwendet.
      • Allen Zertifikaten, die von der als nächstes spezifizierten CA signiert sind, wird vertraut und der VM, die sich damit ausweist, wird das Versenden von E-mails erlaubt.
      • Als CA wird die von Puppet angegeben.
      • Die Nachfrage und Notwendigkeit von SSL Client-Zertifikaten wird aktiviert
      • Nichts geht ohne TLS.
  4. Benutzername und Passwort für den Smarthost werden nach /etc/postfix/sasl_password geschrieben und für Postfix nutzbar gemacht.

Daran muss auch nichts angepasst werden; das Manifest ist allgemeingültig. Der zur VM des Mailservers gehörende FQDN, die Domain, die Zugangsdaten etc werden alle von woanders geholt.

Als Standardkonfiguration muss noch etwas hinterlegt werden:

# Datei /etc/puppetlabs/code/modules/profiles/data/common.yaml
# [...]
profiles::mailrealy::relayhost: smtp.sendgrid.net
profiles::mailrelay::relay_user: apikey
profiles::mailrelay::relay_password: SELBSTWASAUSDENKEN
profiles::mailrelay::root_mail_recipient: invalid@example.com

Jetzt muss das Profil noch der VM zugewiesen werden. In meinem Beispiel also:

# Datei /etc/puppetlabs/code/environments/production/data/nodes/mailout.domain.tld.yaml

classes:
  - profiles::ssl
  - profiles::mailrelay

Als letztes noch die eigentliche Konfiguration der Zugangsdaten:

# Datei /etc/puppetlabs/code/environments/production/data/email.yaml
profiles::mailrelay::relayhost: "smtp.sendgrid.net"
profiles::mailrelay::relay_user: "apikey"
profiles::mailrelay::relay_password: "DASISTNICHTMEINAPIKEY"
profiles::mailrelay::root: "florian@domain.tld"

Sinnvoll ist es hier, das Passwort verschlüsselt abzulegen.

Alles Abspeichern und Puppet laufen lassen!

Nach erfolgreichem Lauf von Puppet sollte das Versenden von E-Mails auf der VM des Mailservers bereits gehen:

mail florian@externespostfachgibtsnicht.invalid -s testmail
Hi, hier eine Testmail!

Viele Gruesse,

-- 
ich
.

Um es explizit darzulegen: Die Mail endet mit einem Punkt in einer eigenen Zeile und dann einer Leerzeile (also zweimal Enter drücken!). So beendet sich mail wieder.

VMs konfigurieren

Nun muss noch ein Profil angelegt werden, mit dessen Hilfe VMs die Mails an den zentralen Mailserver übertragen bekommen. Dazu verwenden sie ebenfalls Postfix, in einem etwas kleineren Setup:

# Datei /etc/puppetlabs/code/modules/profiles/mail.pp
# send mail out via mail relay
# authentication is done via puppet's ssl certificates
class profiles::mail {
  $internal_relayhost = lookup('profiles::mail::internal_relayhost')

  class {'postfix':
    satellite => true,
    relayhost => $internal_relayhost,
  }

  postfix::config {
    'mydomain': value                => $facts['domain'];
    'smtp_tls_security_level': value => 'encrypt';
    'smtp_tls_cert_file': value      => "/etc/puppetlabs/puppet/ssl/certs/${facts['fqdn']}.pem";
    'smtp_tls_key_file': value       => "/etc/puppetlabs/puppet/ssl/private_keys/${facts['fqdn']}.pem";
    'smtp_enforce_tls': value        => 'yes';
  }
}

Der Reihe nach:

  1. Einzige Konfigurationseinstellung ist die Adresse des internen Relays, also des zentralen Mailservers.
  2. Die üblichen Feineinstellungen:
    • TLS verwenden.
    • SSL-Zertifikate anhand des FQDN ermitteln und nutzen

Standardeinstellungen hinterlegen:

# Datei /etc/puppetlabs/code/modules/profiles/common.yaml
# [...]
profiles::mail::internal_relayhost: invalid.example.com

Richtige Konfiguration gleich hinterher schieben:

# Datei /etc/puppetlabs/code/environments/production/data/email.yaml
profiles::mail::internal_relayhost: mailout.domain.tld

Alle VMs, die in der Lage sein sollen, über den zentralen Mailserver Mails zu versenden, brauchen jetzt das zusätzliche Profil. Möchte man beispielsweise Mails vom Puppet Server bekommen, geht das so:

# Datei /etc/puppetlabs/code/environments/production/data/nodes/puppet.domain.tld.yaml
classes:
# [...]
  - profiles::mail

Alles Abspeichern und Puppet laufen lassen

Absicherung

Die Mailserver auf den VMs sind so konfiguriert, dass sie lediglich Mails an den zentralen Mailserver weiterleiten und nicht auf Netzwerkschnittstellen außer localhost lauschen. Der zentrale Mailserver nimmt nur Anfragen über SMTP an, die sich mit einem gültigen und vom Puppet Server signierten Zertifikat ausweisen können, also längst viel gravierendere Hürden im Sicherheitskonzept genommen haben.

Die Mails werden ausschließlich über TLS an Sendgrid übertragen.

Das ist insofern schon mal recht sicher, es ist aber ausdrücklich nur eine Basiskonfiguration, die keinerlei Schutz vor etwa ausgenutzten Sicherheitslücken in Webanwendungen auf VMs bietet, die zum Spamversand missbraucht werden. Solche Maßnahmen sind je nach Anwendung natürlich selbst zu treffen!