Datenschutzkonformer Mailserver aus der Hand der Digital Souveränen Schule

ist das bei Nutzung (z.B.) der Mailcow sinnvoll?
SPF/DKIM/DMARC,rspamd,clamav,… ist doch alles schon da.
An welcher Stelle übersehe ich was?

LG Jesko

Nur noch mal der Hinweis, dass wir bei diesem Projekt aus gutem Grund keine Mailcow verwenden. Die Mailcow ist toll, aber sie erfüllt die Anforderungen die der LfDI an einen protokollbasierenden Zugang (IMAPS/SMTPS) hat erst mal nicht. Das Mailbox-Projekt nutzt u.a. deshalb die Konfigurationsflexibilität die aus der direkten Nutzung des Distributionspakete resultiert. Sobald ein vom LfDI akzeptierter Weg für einen weiteren Faktor bei der Authentifizierung auf Protokollebene vorliegt kann das direkt konfiguriert werden. Das mag dann mit der Mailcow auch gehen, ist aber dort dann sicher aufwändiger umzusetzen da solche Features eben Upstream nicht vorgesehen sind.

Hallo zusammen,

kann man irgendwo nachlesen, welche Anforderungen der LfDI an einen protokollbasierenden Zugang (IMAPS/SMTPS) stellt?

VG
Thomas

Hi @AnGry,
ich hatte die Mailcow stellvertretend für ein aufeinander optimiertes System bestehend aus Postfix, Dovecot, SoGo, Rspamd, ClamAV… gemeint (was ja im Grunde dem entspricht, was du beschrieben hast, nur halt vorkonfiguriert und gut verpackt, was Vor- aber auch Nachteil sein kann…
Aber das ist hier gar nicht mein Punkt, sondern ich wollte primär gefragt haben:
Was haben die Römer je für uns getan?“
oder konkret: An welcher Stelle erhöht ein Proxmox Mailgateway meine Sicherheit bzw. wo ist es von Vorteil. … und ist dieser Vorteil so groß, dass er den zusätzlichen Aufwand eines weiteren zu pflegenden Servers ausgleicht?

LG Jesko

Hallo Jesko,

ich hatte mal das Problem, dass einige E-Mail-Provider trotz korrekter SPF, DKIM, DMARC etc. E-Mails abgelehnt haben. Die Schule hatte vor Belwue als Mail-Relay für den selbstgehosteten Mailserver genutzt. Nach Einstellung der Dienste seitens Belwue hat man versucht, statt eines externen Mail-Relays die eigene FIrewall als Mail-Relay zu nutzen → ohne Erfolg…der (deutsche) E-Mail-Dienstanbieter hat E-Mails aus der Domain trotz korrekter DNS-Records abgelehnt. Erst nachdem man (wieder) einen externen Mail-Relay-Provider genommen hatte, war das Problem behoben. Zusätzlich macht dieser (deutsche) Provider auch noch SPAM-Filtering. Hat neulich geholfen, als irgendwer mal das LDAP gegen den Mail-Server gesynct hat und dabei auch Postfächer für Systemaccounts (z.b. ldapuserirgndeindienst angelegt hatte, deren Kennwörter nicht die sichersten waren…Es dauerte nicht lange, bis irgendwer die Passwörter gehackt hatte und fleissig SPAM-Mails über ldapuserirgendeindienst@email.der.schule verschickt hatte. Ohne externes Mail-Relay/SPAM-Schutz wäre die Mail-Domain-Server der Schule jetzt Internetweit tot und geächtet → blacklisted

Also Mail-Relays haben schon ihren Sinn, sofern sie etwas mehr können, als nur E-Mails ins Internet zu schicken.

VG
Thomas

P.S.: Es gibt auch E-Mail-Lösung die per Design ein Mail-Relay erwarten und ohne diese gar nicht richtig funktionieren.

:slight_smile: da steht viel richtiges, aber es geht glaub ich komplett an meinem Anliegen vorbei.

Wenn die Mails von meinem Mailserver der Schule abgelehnt werden, werden die auch über ein Proxmox MailGW in der Schulen nicht akzeptiert.

Mir geht es überhaupt nicht um den Sinn und Unsinn von einem externen smarthost.

:slight_smile: LG Jesko

Hi,

es sollte auch ausgehender Mailverkehr denselben Filtern und Rateliming wie eingehender Verkehr unterzogen werden. Zugangsdaten kommen abhanden und reguläre Mailkonten werden für Spamversand verwendet. Das will ich nur einwerfen, weil oft nur der eingehende Teil betrachtet wird.

Ich will mal zwei Aspekte beispielhaft anführen, die für eine Lösung sprechen, die über das reine lokale selber hosten von Maildiensten hinaus gehen:

a) Das DFN betreibt für Hochschulen extra einen Dienst „DFN MailSupport“

weil es heutzutage nicht mehr so einfach ist, alle n Dienste im E-Mail-Kontext korrekt einzurichten, nachzujustieren, regelmäßig zu trainieren, usw., wobei es alle paar Jahre n+1 heißt, weil sämtliche Lösungen das Problem unvollständig abdecken. DFN-MailSupport nutzen über 180 Einrichtungen und ich vermute nicht, weil die grad keinen Stelle für einen Mailadmin haben, sondern weil die von Community/Cloud-Effekten bei Filtern, Reputationssystemen u.ä. profitieren wollen. Und im Angebot wird eingehender und ausgehender Mailverkehr berücksichtigt.

b) Wer mal reinschnuppern will, welche Katastrophe allein schon die simple Frage „Ist das ein gültiger E-Mail-Absender?“ aufwirft, der kann sich ja mal die Folien zu Spoofing? - Verfahren zur Absenderprüfung bei E-Mail von der letzten DFN-Betriebstagung ansehen.

Beide Aspekte können dafür sprechen, dass man das einem Anbieter überlässt, der sich ganztägig und ständig mit solchen Themen beschäftigt. Das kann dann ein Clouddienst sein, eine VM im on-premise Virtualisierungscluster oder auch eine Appliance für den eigenen 19-Zoll-Schrank. Klar, kann man das alles mit Open-Source-Software selber hosten. Die Frage ist wie viel Zeit hat man die Spam/Reputation-Situation seiner Server/Mailkonten regelmäßig zu überwachen und wie zeitnah zu reagieren.

Von DNSBL (1998) zu SPF (2006): 8 Jahre
Von SPF (2006) zu DKIM (2007): 1 Jahr
Von DKIM (2007) zu ARC (2019): 12 Jahre
macht im Schnitt alle 7 Jahre ein neues RFC zum Thema Spamabwehr und E-Mail-Validierung ist weiterhin inhärent kaputt.

Ehrlich, ich will das nicht mehr „nebenbei“ betreiben und muss es auch zum Glück nicht mehr und das sage ich als jemand, der das 15 Jahre lang für ca. 1000 Mailkonten getan hat.

VG
Buster

2 „Gefällt mir“

mit dem wagvpn und 2fa und keycloak…
kann man doch eigentliche alle lehrerdienste (Nextcloud, Mail und auch sonstige dienste die 2fa, oidc und so nciht können, webdav, caldav… usw) dann nur noch im internen Netz / VPN verfügbar machen. Dann wäre der VPN quasi ein Lehrernetz… oder spricht da irgendwas dagegen. Leistung?
für externe links und schülerzugang haben wir sowieso ne andere Nextcloud…

Hallo zusammen,

kleiner Zwischenstand was sich beim den Mailserver-Playbooks getan hat:

  • Da Debian 13 vor der Tür steht, sind die Playbooks auf Trixie angepasst
  • OpenID-Connect Anbindung an Keycloak (wird erstmals vom neuen SOGo Release unterstützt)
  • Damit wird jetzt intern die Authentifizierung via XOAUTH2 gegen Dovecot möglich
  • Shared Folders in Dovecot und SOGo

Viele Grüße

Raphael

gibt es ein skript, mit dem man in wag die registration tokens erzeugt und an die nutzer per mail sendet für das herunterladen der konfigurationsdatei für wireguard?

Da ich hier keine Antwort bekommen habe poste ich einfach mal mein eigenes Skript…
Mit KI geht das ja alles viel leichter wie früher…
Ablauf ist folgender:
Das folgende Skript auf dem WAG VPN Server per cronskript als root laufen lassen
Lehrer beantragen einen VPN Token per mail an eine dafür deklarierte Emailadresse einen VPN Token.
Das Skript holt die neuen Mails aus der emailpostfach ab. Prüft per ldap ob die eingegangene emailadresse des absenders die adresse eines Lehrers ist.
erstellt ein Registrierungstoken und sendet dem Lehrer eine Mail zurück mit dem Token.
Löscht ungenutzte Registrierungstokens nach 14 Tagen.
Hab den WAG-VPN so eingestellt, dass der Downloadlink nur einmalig verwendet werden kann.

#!/usr/bin/env python3
import imaplib
import email
import ldap
import ssl
import email.utils
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
import subprocess
import time
import json
import os

### 🔹 System-Specific Configuration
IMAP_SERVER = “my_imapserver.de
IMAP_PORT = 993
IMAP_USERNAME = “user
IMAP_PASSWORD = “pw”
IMAP_FOLDER = “INBOX”
IMAP_TRASH_FOLDER = "Trash"

### 🔹 SMTP Configuration
SMTP_SERVER = "192.168.1.2"  # Change to your SMTP server
SMTP_PORT = 25  # for ssl 465 or 587 Starttls code has to be adjusted
SMTP_USERNAME = “user”
SMTP_EMAILADDRESS ="vpn@mydomain.de"
SMTP_PASSWORD = IMAP_PASSWORD


LDAP_SERVER = "ldaps://myldapserver.de"
LDAP_BASE_DN = "ou=lehrer,…”
LDAP_USERNAME = ""
LDAP_PASSWORD = ""


URLWAG=“vpn.meineschule.de” #for the registration token link https://vpn.meineschule.de/register_device?key=…
TOKEN_FILE_PATH = "/opt/wag/tokens-temp/tmp"  # File path for storing tokens
TOKEN_EXPIRATION = 14 * 24 * 60 * 60  # 2 weeks in seconds
#TOKEN_EXPIRATION =60 #1minute for testing purposes
### 🔹 Function to Fetch Email Senders and Move Emails to Trash
def list_mailboxes():
    try:
        mail = imaplib.IMAP4_SSL(IMAP_SERVER, IMAP_PORT)
        mail.login(IMAP_USERNAME, IMAP_PASSWORD)

        result, mailboxes = mail.list()
        if result == "OK":
            print("Available Mailboxes:")
            for mailbox in mailboxes:
                print(mailbox.decode())

        mail.logout()
    except Exception as e:
        print(f"IMAP Error: {e}")

def extract_email(sender_raw):
    parsed = email.utils.parseaddr(sender_raw)
    return parsed[1]

def process_emails():
    senders = []
    try:
        mail = imaplib.IMAP4_SSL(IMAP_SERVER, IMAP_PORT)
        mail.login(IMAP_USERNAME, IMAP_PASSWORD)
        mail.select(IMAP_FOLDER)

        result, data = mail.search(None, "UNSEEN")
        email_ids = data[0].split()

        for email_id in email_ids:
            result, msg_data = mail.fetch(email_id, "(RFC822)") #use this to mark email as read
            #result, msg_data = mail.fetch(email_id, "(BODY.PEEK[])") #use this keep email as unread
            for response_part in msg_data:
                if isinstance(response_part, tuple):
                    msg = email.message_from_bytes(response_part[1])
                    sender_raw = msg["From"]
                    print(sender_raw)
                    email_address = extract_email(sender_raw).lower()  # Use robust extraction method

                    if email_address:
                        senders.append(email_address)
            # Move email to the Trash folder
            mail.copy(email_id, IMAP_TRASH_FOLDER)  # Copy email to Trash
            mail.store(email_id, '+FLAGS', '\\Deleted')  # Mark it as deleted


        mail.expunge()  # Permanently remove deleted emails

        mail.logout()
    except Exception as e:
        print(f"IMAP Error: {e}")

    return senders


### 🔹 Function to Validate Senders in LDAP & Retrieve User Info
def validate_senders(email_senders):
    validated_dict = {}

    try:
        conn = ldap.initialize(LDAP_SERVER)
        conn.set_option(ldap.OPT_REFERRALS, 0)
        conn.simple_bind_s("", "")  # Anonymous bind (adjust if needed)

        # Perform one search to retrieve all relevant entries
        ldap_filter = "(|(mail=" + ")(mail=".join(email_senders) + "))"  # OR filter for all emails
        result = conn.search_s(LDAP_BASE_DN, ldap.SCOPE_SUBTREE, ldap_filter, ["uid", "fullname", "mail"])

        for dn, entry in result:
            if "mail" in entry and "uid" in entry and "fullname" in entry:
                email_address = entry["mail"][0].decode("utf-8").lower()
                username = entry["uid"][0].decode("utf-8")
                fullname = entry["fullname"][0].decode("utf-8")  # Using `cn`, but can be changed to `displayName`

                validated_dict[email_address] = {"username": username, "fullname": fullname}

        conn.unbind()
    except ldap.LDAPError as e:
        print(f"LDAP Error: {e}")

    return validated_dict

def generate_tokens(validated_senders):
    token_data = {}
    #Loading token tmp file for appending new tokens to the tmp file
    if os.path.exists(TOKEN_FILE_PATH):
        try:
            with open(TOKEN_FILE_PATH, "r") as file:
                token_data = json.load(file)
        except json.JSONDecodeError:
            print("⚠️ Warning: Token file is corrupted, starting fresh.")
    for email in validated_senders.keys():
        username = validated_senders[email]["username"]
        time.sleep(1)
        try:
            # Run the wag registration command
            result = subprocess.run(
                ["/opt/wag/wag", "registration", "-add", "-username", username],
                capture_output=True,
                text=True
            )

            # Extract token from the second line of output
            output_lines = result.stdout.split("\n")
            if len(output_lines) > 1:
                token_info = output_lines[1].split(",")  # Assuming format "token,username"
                if len(token_info) > 0:
                    token = token_info[0].strip()
                    validated_senders[email]["token"]= token 
            
                    # Generate Unix timestamp and save the token for tmp file
                    timestamp = int(time.time())
                    token_data[timestamp] = {"token": token, "username": username}
                   # print(token_data)
        except Exception as e:
            print(f"❌ Error generating token for {email}: {e}")

        # Save tokens to file
        try:
          with open(TOKEN_FILE_PATH, "w") as file:
            json.dump(token_data, file, indent=4)
         # print(f"✅ Tokens saved to {TOKEN_FILE_PATH}")
        except Exception as e:
          print(f"❌ Error saving tokens to file: {e}")

    return validated_senders  # Return updated dictionary


def generate_email_template(email, validated_senders):
    if email not in validated_senders:
        return None  # Ensure only validated senders get a response

    fullname = validated_senders[email]["fullname"]
    username = validated_senders[email]["username"]
    token = validated_senders[email]["token"]

    html_template = f"""
    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="UTF-8">
        <title>Ihre  VPN Login-Daten</title>
        <style>
            body {{
                font-family: Arial, sans-serif;
                font-size: 12;
            }}
            .container {{
                max-width: 600px;
                margin: auto;
                padding: 20px;
                border: 1px solid #ddd;
                border-radius: 8px;
                background-color: #f9f9f9;
            }}
            h2 {{
                color: #007BFF;
            }}
        </style>
    </head>
    <body>
        <div class="container">
            <p>Hallo {fullname},</p>
            <p>Wir haben Ihre Anfrage für einen VPN-Zugang erhalten. Im folgenden werden die nötigen Schritte für die Einrichtung beschrieben.</p>
            <p>1.) Zunächst müssen Sie die VPN Software herunterladen und installieren. (Windows / MacOS / Linux /IOS / Android)<br>
            <a href="https://wireguard.com/install">Link zur VPN Software Wireguard</a><br>
            <p>2.) Als nächstes müssen sie für Wireguard eine Konfigurationsdatei herunterladen.
            <br> 
            <br>
                        <br> <a href="https://{URLWAG}/register_device?key={token}" target="_blank" >Link für die Konfigurationsdatei</a>
            <br><br> Diese Datei enthält ihr VPN Zugang. Bitte diese Datei durch Zugriff von Dritten schützen!<br>
            <br> Diese Datei lässt sich nur einmalig herunterladen. Bei weiterer Betätigung des Links erscheint "404 page not found".
            <br> Falls sie Datei nicht erfolgreich herunterladen konnten, fordern Sie einen neuen Link an.</p>

            <p>3.) Nach dem Sie die Konfigurationsdatei abgespeichert haben, öffnen Sie diese Datei, meist "wg0.conf", mit der Wireguard Software und laden Sie die Einstellungen.<br>
            Verwenden Sie diese Konfigurationsdatei nur bei einem Endgerät. <br> Falls sie Zugänge für weitere Geräte brauchen, fordern Sie einen neuen Downloadlink an. </p>

            <p>4.) Nun können sie sich mit dem VPN verbinden. Damit der IMAP Zugang klappt, muss man ggf die IMAP Software (Apple Mail) neustarten.

            <h4>Nutzungsinformation: </h4>
            <p>Der VPN  ermöglicht Ihnen einen sicheren Zugang zu den internen Diensten.</p>
           
            <p>Falls Sie weitere Fragen haben, können Sie uns jederzeit kontaktieren.</p>
            <br>Ihr Admin Team </p>
        </div>
    </body>
    </html>
    """
    
    return html_template

def send_email(recipient_email, validated_senders):
    if recipient_email not in validated_senders:
        return False  # Ensure only validated senders get a response

    fullname = validated_senders[recipient_email]["fullname"]
    
    # Generate the personalized email template
    email_body = generate_email_template(recipient_email, validated_senders)

    try:
        # Create the email message
        msg = MIMEMultipart()
        msg["From"] = SMTP_EMAILADDRESS
        msg["To"] = recipient_email
        msg["Subject"] = "Ihre VPN Login-Daten"
        msg.attach(MIMEText(email_body, "html"))  # Attach HTML content

        # Connect to SMTP server on port 25 without authentication
        server = smtplib.SMTP(SMTP_SERVER, SMTP_PORT)
        server.ehlo()  # Initiate connection
        server.sendmail(SMTP_EMAILADDRESS, recipient_email, msg.as_string())
        server.quit()

        print(f"✅ Email sent to {recipient_email}")
        return True
    except Exception as e:
        print(f"❌ SMTP Error: {e}")
        return False

def get_unused_tokens():
    """Fetch unused tokens from WAG and return them as a list."""
    unused_tokens = []
    try:
        result = subprocess.run(
            ["/opt/wag/wag", "registration", "-list"],
            capture_output=True,
            text=True
        )
        output_lines = result.stdout.split("\n")

        # Extract tokens from the first column, skipping the first line
        for line in output_lines[1:]:  # Skip header row
            columns = line.split(",")
            if columns and columns[0].strip():
                unused_tokens.append(columns[0].strip())

    except Exception as e:
        print(f"❌ Error fetching unused tokens: {e}")

    return unused_tokens



def clean_expired_tokens():
    """Remove expired tokens from the temp file & delete unused ones in WAG."""
    # Load existing tokens
    token_data = {}
    if os.path.exists(TOKEN_FILE_PATH):
        try:
            with open(TOKEN_FILE_PATH, "r") as file:
                token_data = json.load(file)
        except json.JSONDecodeError:
            print("⚠️ Warning: Token file is corrupted, skipping cleanup.")
            return

    current_time = int(time.time())
    expired_tokens = [timestamp for timestamp in token_data if current_time - int(timestamp) > TOKEN_EXPIRATION]

    # Fetch unused tokens from WAG
    unused_tokens = get_unused_tokens()

    # Remove expired tokens from JSON file & delete unused ones in WAG
    for timestamp in expired_tokens:
        token = token_data[timestamp]["token"]
        if token in unused_tokens:
            try:
                subprocess.run(["/opt/wag/wag", "registration", "-del", "-token", token], capture_output=True, text=True)
                print(f"✅ Token {token} deleted from WAG.")
            except Exception as e:
                print(f"❌ Error deleting token {token}: {e}")

        # Remove token from temp file
        del token_data[timestamp]

    # Save updated token file
    try:
        with open(TOKEN_FILE_PATH, "w") as file:
            json.dump(token_data, file, indent=4)
       # print(f"✅ Updated token file saved.")
    except Exception as e:
        print(f"❌ Error saving updated token file: {e}")


### 🔹 Main Execution
if __name__ == "__main__":
    #list_mailboxes()
    email_senders = process_emails()
   # print(email_senders)
    valid_senders_dict = validate_senders(email_senders)
    valid_senders=valid_senders_dict.keys()      
    print("🔹 Valid Email Senders (Matched with LDAP):", valid_senders_dict)
 #   for email in validated_senders.keys():
  #    print(f"🔹 Generated Email for {email}:\n", generate_email_template(email, validated_senders))
    generate_tokens(valid_senders_dict)
    clean_expired_tokens()
    #for finalizing script make sure emails get into trash after processing and mark them as read
    for email in valid_senders:
       send_email(email, valid_senders_dict)