Linux Server einrichten 2026: Von der leeren VPS zum gehärteten Produktionsserver

Linux Server einrichten 2026: Von der leeren VPS zum gehärteten Produktionsserver

Einen Linux Server einrichten klingt nach einer Liste von Befehlen, die man abarbeitet: SSH härten, Firewall an, fail2ban installieren, fertig. Genau diese Liste steht in hunderten Anleitungen — und sie ist nicht falsch. Sie ist nur unvollständig an den Stellen, an denen es wehtut.

Wir betreiben mehrere Server: einen Hauptserver, auf dem unter anderem diese Seite läuft, einen Trading-Server in Helsinki und einen Shop-Server. Für diesen Artikel haben wir keine Anleitung nacherzählt, sondern die eigenen Maschinen ausgemessen: die Logs von 84.136 SSH-Ereignissen ausgewertet, fail2ban gegen die tatsächlichen Angriffsmuster nachgerechnet und eine Firewall-Regel live von einem fremden Server aus getestet.

Drei Ergebnisse vorweg, weil sie die gängige Reihenfolge der Ratschläge verändern:

1. Auf unserem Hauptserver liefen in 16,5 Tagen 20.815 Login-Versuche mit ungültigen Benutzernamen von 1.272 verschiedenen IP-Adressen auf. Davon hätte die Standardkonfiguration von fail2ban 70,4 % gar nicht bemerkt.

2. 10 % der Angriffe benutzten Benutzernamen, die aus unseren eigenen Domainnamen abgeleitet sindheynyx, exuvia, praxisfix. Die stehen in öffentlichen Zertifikatsprotokollen. Wer eine Domain mit HTTPS betreibt, verrät damit Kandidaten für Benutzernamen.

3. Ein Docker-Container mit dem Standard-Port-Mapping war von außen erreichbar, obwohl die Firewall den Port verbietet. Wir haben das von einem anderen Server aus nachgewiesen: HTTP 200 auf einem Port, den ufw nicht freigegeben hat.

Linux Server einrichten: Was in den ersten Minuten wirklich passiert

Es gibt einen verbreiteten Satz in Foren: „Wer soll denn ausgerechnet meinen kleinen Server angreifen?” Die Antwort ist unromantisch: niemand persönlich. Es sind Scanner, die den gesamten adressierbaren Bereich des Internets durchgehen, und sie finden eine neue IP-Adresse innerhalb von Minuten.

Wir haben das nicht geschätzt, sondern gezählt. Hier die Zahlen aus dem Systemjournal unseres Hauptservers, Zeitraum 5. bis 22. August 2026:

MesswertWert
Beobachtungszeitraum394,9 Stunden (16,5 Tage)
SSH-Journalzeilen gesamt84.136
Login-Versuche mit ungültigem Benutzernamen20.815
Angriffsversuche pro Tag1.265
Verschiedene angreifende IP-Adressen1.272
Verschiedene ausprobierte Benutzernamen3.408
Erfolgreiche Passwort-Anmeldungen von Angreifern0

Die letzte Zeile ist die wichtigste, und sie ist kein Zufall: Auf dieser Maschine ist die Passwort-Anmeldung abgeschaltet. Es gab in 16,5 Tagen null fehlgeschlagene Passwortversuche — nicht weil niemand es versucht hätte, sondern weil der Server Passwörter gar nicht erst anbietet. Die Angreifer kommen bis zur Frage „welcher Benutzer?” und werden dort abgewiesen.

Ein Server unter dauerhaftem automatisiertem Beschuss aus dem Internet, abgewehrt von einem Schutzschild

Zum Vergleich haben wir denselben Auswertungslauf auf unserem Server in Helsinki gemacht. Dort ist die Passwort-Anmeldung noch aktiv und Root darf sich per Passwort anmelden — ein Zustand, den viele Hoster ab Werk ausliefern:

Messwert (auf Tage normalisiert)Hauptserver (Key-only)Helsinki (Passwort an)
Ungültige Benutzernamen / Tag1.2652.271
Fehlgeschlagene Passwort-Versuche / Tag06.200
davon gegen root / Tag3.603
Angreifende IPs / Tag77127

6.200 Passwortversuche pro Tag, davon 3.603 direkt gegen root. Das sind rund 43 Versuche pro Minute gegen genau einen bekannten Benutzernamen. Solange dort ein rateberates Passwort steht, ist das kein theoretisches Risiko, sondern eine laufende Lotterie mit sehr vielen Losen.

Der Fund, mit dem wir nicht gerechnet hatten: Angreifer kennen deine Domain

Beim Auszählen der ausprobierten Benutzernamen standen die üblichen Verdächtigen oben: ubuntu (3.182 Versuche), admin (1.679), centos, test, user. Erwartbar.

Auf Platz vier und fünf standen dann heynyx (947 Versuche) und exuvia (783). Das sind unsere Domainnamen. Weiter unten: praxisfix (256), shellgames, getmind, nyxvault, forkcart, suedpfote — alles Projekte, die auf dieser Maschine liegen.

Zusammengerechnet: 2.082 Versuche = 10,0 % aller Angriffe benutzten einen Benutzernamen, der aus unseren eigenen Domains abgeleitet ist.

Woher wissen die das? Wir haben die naheliegende Quelle geprüft: Certificate Transparency. Jedes über Let’s Encrypt ausgestellte Zertifikat landet in einem öffentlichen, durchsuchbaren Protokoll. Eine Abfrage bei der Certspotter-API ergab für unsere Hauptdomain 86 verschiedene Hostnamen — jede Subdomain, die wir je mit HTTPS versehen haben, inklusive längst vergessener Testinstanzen.

Diese Liste ist für jeden abrufbar. Die Angreifer nehmen den Domainnamen, schneiden die Endung ab und probieren ihn als Benutzernamen. Es ist billig und es kostet sie nichts.

Die praktische Konsequenz: Der Benutzername auf dem Server sollte nichts mit dem Projekt, der Firma oder der Domain zu tun haben. deployer auf einem Server, der meinefirma.de ausliefert, ist besser als meinefirma. Das ist keine echte Sicherheitsmaßnahme — Benutzernamen sind kein Geheimnis — aber es nimmt dem billigsten Angriffsmuster die Trefferquote.

Zur Gegenprobe haben wir denselben Test in Helsinki gemacht: Dort tauchten heynyx, exuvia und praxisfix 0-mal auf. Der Server hat keine Domain, die auf ihn zeigt, und bekommt deshalb ausschließlich generische Benutzernamen. Das bestätigt den Mechanismus: Die Namen kommen nicht aus dem Nichts, sie kommen aus dem DNS und den Zertifikatsprotokollen.

Schritt 1: SSH absichern — die Maßnahme mit dem größten Effekt

Wenn du von diesem Artikel nur eine Sache umsetzt, dann diese. Alle Zahlen oben zeigen dasselbe: Der Angriff kommt über SSH, und er kommt über Passwörter.

Schlüssel erzeugen und einspielen

Auf dem eigenen Rechner, nicht auf dem Server:

ssh-keygen -t ed25519 -C "arbeitsplatz-laptop"

ed25519 statt RSA: kürzere Schlüssel, moderne Kurve, seit Jahren Standard. Die Passphrase-Abfrage nicht überspringen — sie schützt den Schlüssel, falls der Laptop abhandenkommt.

Danach den öffentlichen Teil auf den Server bringen:

ssh-copy-id -i ~/.ssh/id_ed25519.pub benutzer@server-ip

Jetzt der wichtigste Schritt, den fast jede Anleitung zu beiläufig erwähnt: Öffne eine zweite Terminalsitzung und melde dich mit dem Schlüssel an — bevor du die Passwort-Anmeldung abschaltest. Wenn etwas schiefgeht, hast du noch die alte, funktionierende Verbindung. Ohne diesen Schritt sperrst du dich aus und brauchst die Notfallkonsole des Hosters.

Die Konfiguration härten

In /etc/ssh/sshd_config beziehungsweise einer eigenen Datei unter /etc/ssh/sshd_config.d/:

PermitRootLogin prohibit-password
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
MaxAuthTries 3
PermitEmptyPasswords no
X11Forwarding no

Ein Wort zu PermitRootLogin. Die verbreitete Empfehlung lautet no. Unsere Maschine läuft auf prohibit-password (gleichbedeutend mit without-password), also: Root darf sich anmelden, aber ausschließlich per Schlüssel. Beides ist vertretbar. no plus ein normaler Benutzer mit sudo ist die strengere Variante und die richtige, wenn mehrere Menschen Zugriff haben — dann ist in den Logs nachvollziehbar, wer was getan hat. Bei einer Ein-Personen-Maschine ist prohibit-password ein akzeptabler Kompromiss, solange die Passwort-Anmeldung global aus ist.

Prüfen, was wirklich gilt — nicht, was in der Datei steht. Konfigurationsdateien enthalten auskommentierte Zeilen, Include-Anweisungen und Blöcke, die sich gegenseitig überschreiben. Der Server sagt selbst, was er tut:

sshd -T | grep -E "^(port|permitrootlogin|passwordauthentication|pubkeyauthentication|maxauthtries)"

Auf unserer Maschine:

port 22
permitrootlogin without-password
passwordauthentication no
pubkeyauthentication yes
maxauthtries 6

Das ist der tatsächliche Zustand. grep in der Konfigurationsdatei hätte hier eine andere Geschichte erzählen können — dort steht MaxAuthTries an mehreren Stellen.

Vor dem Neustart die Syntax prüfen, das kostet nichts und verhindert einen Dienst, der nicht mehr startet:

sshd -t && systemctl reload ssh

Vergleich zwischen schwacher Passwortanmeldung und kryptografischer Schlüsselanmeldung

Lohnt es sich, den SSH-Port zu ändern?

Diese Frage wird in Foren erbittert diskutiert. Die eine Seite sagt „Security by Obscurity, bringt nichts”, die andere schwört darauf. Wir konnten das messen, weil einer unserer Server auf einem ungewöhnlichen Port läuft.

ServerSSH-PortBeobachtungsfensterUngültige Benutzernamen / Tag
Hauptserver2216,5 Tage1.265
Helsinki229,7 Stunden2.271
Shop-Server5220030,8 Tage6,0

Faktor 211. Der Server auf dem hohen Port sah in einem Monat weniger Angriffsversuche als der Hauptserver an einem einzigen Vormittag.

Aber die Einordnung ist wichtig, und hier widersprechen wir dem naheliegenden Schluss: Das ist keine Sicherheitsmaßnahme, das ist Lärmreduktion. Ein gezielter Angreifer scannt alle 65.535 Ports und findet den SSH-Dienst in Sekunden. Was der Portwechsel wirklich bringt: Die Logs werden lesbar. Wenn 1.265 Fehlversuche pro Tag durchlaufen, sieht man den einen echten Vorfall nicht. Bei 6 pro Tag schon.

Und er hat einen Preis: Jedes Skript, jeder Cronjob, jeder Kollege braucht den Port. Wir haben ihn deshalb genau auf einem Server geändert und auf den anderen bewusst nicht.

Schritt 2: Die Firewall — und warum sie weniger schützt, als du denkst

Der Standardrat lautet: alles verbieten, nur öffnen, was gebraucht wird. Das ist richtig. Auf unserem Hauptserver:

ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp comment 'SSH'
ufw allow 80/tcp comment 'HTTP'
ufw allow 443/tcp comment 'HTTPS'
ufw enable

Vor ufw enable unbedingt sicherstellen, dass der SSH-Port freigegeben ist. Sonst trennt der Befehl die eigene Verbindung, und zwar sofort.

Eine Regel lässt sich auf eine Quell-IP einschränken, was bei Verwaltungszugängen sinnvoll ist:

ufw allow from 203.0.113.5 to any port 22 proto tcp

Der Zustand lässt sich anzeigen — aber die Anzeige ist nicht der Beweis:

ufw status verbose

Mehrere Verteidigungsringe um einen Server mit nur einem kontrollierten offenen Pfad

Der Test, der die Firewall-Illusion zerlegt

Hier wird es unangenehm, und dieser Teil ist der Grund, warum wir diesen Abschnitt überhaupt geschrieben haben.

Docker umgeht ufw. Das ist bekannt, wird aber meist als Randnotiz erwähnt. Wir wollten wissen, ob es auf unserer konkreten Maschine wirklich passiert — mit aktiver Firewall, Standardrichtlinie „deny incoming”.

Der Versuchsaufbau: ein Wegwerf-Container auf Port 39999, gebunden an alle Schnittstellen. Dieser Port ist in ufw nirgends freigegeben, es greift die Standardregel „verbieten”.

docker run -d --rm --name ufwtest -p 0.0.0.0:39999:80 nginx:alpine

Der entscheidende Punkt: Wir haben nicht vom Server selbst getestet. Ein curl auf die eigene öffentliche IP läuft über das Loopback-Interface und durchläuft die Firewall-Kette nie — der Test hätte immer „erreichbar” gesagt und nichts bewiesen. Stattdessen haben wir uns auf unseren Server in Helsinki eingeloggt, eine andere Maschine bei einem anderen Anbieter in einem anderen Land, und von dort angefragt:

--- externer Test, Docker-Port 39999 (ufw: default deny) ---
HTTP:200
--- Kontrolle: Port 631, ufw DENY ---
(Zeitüberschreitung — korrekt blockiert)
--- Kontrolle: Port 443, ufw ALLOW ---
Connection to 46.225.123.163 443 port [tcp/https] succeeded!

HTTP 200. Der Container war aus dem Internet erreichbar, obwohl die Firewall den Port verbietet. Die beiden Kontrollmessungen zeigen, dass die Firewall grundsätzlich funktioniert: Port 631 blockiert, Port 443 offen. Nur an Docker kommt sie nicht heran.

Der Grund liegt in der Reihenfolge: Docker schreibt seine Regeln direkt in die FORWARD-Kette von iptables, und zwar vor den Regeln, die ufw verwaltet. Auf unserer Maschine ist die DOCKER-USER-Kette leer und die FORWARD-Kette springt direkt hinein — ufw wird für weitergeleiteten Container-Verkehr schlicht nicht gefragt.

Der Testcontainer wurde unmittelbar danach gestoppt und der Port als geschlossen gegengeprüft; die sechs produktiven Container blieben unangetastet.

Die Lösung ist unspektakulär und steht in unseren internen Regeln, seit uns das BSI wegen einer offenen PostgreSQL-Instanz angeschrieben hat: Container-Ports immer explizit an das Loopback-Interface binden.

# falsch — landet auf 0.0.0.0 und ist aus dem Internet erreichbar
ports:
  - '5432:5432'

# richtig
ports:
  - '127.0.0.1:5432:5432'

Auf unserem Hauptserver sind das inzwischen alle Container. Nachgemessen zum Zeitpunkt dieses Artikels: 43 Dienste lauschen ausschließlich auf Loopback, 29 sind öffentlich erreichbar — und die 29 sind Webserver, Mailports und SSH, also gewollt. Kein einziger Datenbankport ist darunter.

Wer den Dienst von außen erreichbar braucht, stellt einen Reverse Proxy davor. Wie das mit nginx aussieht, haben wir in nginx als Reverse Proxy ausführlich beschrieben.

Schritt 3: fail2ban — nützlich, aber nicht so nützlich wie erhofft

fail2ban liest die Logs, erkennt wiederholte Fehlversuche und sperrt die IP-Adresse. Installation und Grundkonfiguration:

apt install fail2ban

Einstellungen gehören in /etc/fail2ban/jail.local, nicht in jail.conf — letztere wird bei Updates überschrieben:

[DEFAULT]
bantime  = 1h
findtime = 10m
maxretry = 5
banaction = ufw

[sshd]
enabled = true
backend = systemd

Das ist die Konfiguration, die überall empfohlen wird, und es ist auch unsere. Danach:

systemctl enable --now fail2ban
fail2ban-client status sshd

Auf unserem Server sah das so aus:

Status for the jail: sshd
|- Filter
|  |- Currently failed:  29
|  |- Total failed:      15241
|  `- Journal matches:   _SYSTEMD_UNIT=sshd.service
`- Actions
   |- Currently banned:  3
   |- Total banned:      198

198 Sperren. Klingt nach Arbeit, die getan wird. Aber: In demselben Zeitraum haben uns 1.272 verschiedene IP-Adressen angegriffen. Diese Diskrepanz wollten wir verstehen.

Die Nachrechnung

Zuerst die naheliegende Frage: Erkennt der Filter die Zeilen überhaupt? Das lässt sich prüfen, ohne zu raten:

fail2ban-regex /tmp/f2btest.log /etc/fail2ban/filter.d/sshd.conf

Ergebnis bei 2.000 echten Logzeilen: 1.153 erkannt, 847 ignoriert, 0 verpasst. Der Filter funktioniert also. Die ignorierten Zeilen sind Connection closed-Meldungen, die absichtlich nicht als Fehlversuch zählen.

Dann haben wir die eigentliche Frage nachgerechnet: Wie viele der angreifenden IP-Adressen erreichen überhaupt die Auslöseschwelle von 5 Versuchen innerhalb von 10 Minuten? Dafür haben wir alle 20.678 Ereignisse mit Zeitstempel und Quell-IP ausgewertet und für jede IP ein gleitendes 10-Minuten-Fenster geprüft.

ErgebnisWert
Angreifende IP-Adressen mit Fehlversuchen1.026
Davon lösen die Standardregel aus228 (22,2 %)
Davon lösen nicht aus798 (77,8 %)
…obwohl sie insgesamt ≥ 5 Versuche hatten488
Angriffsversuche von niemals gesperrten IPs14.550 (70,4 %)

70,4 % des Angriffsverkehrs bleibt unter dem Radar der Standardkonfiguration.

Zur Plausibilitätsprüfung: Unsere Rechnung sagt für den Zeitraum seit dem Start des Dienstes 171 auslösende IPs voraus, fail2ban selbst meldet 198 Sperren. Die Größenordnung stimmt; die Differenz erklärt sich durch Wiederholungssperren derselben IP.

Warum das so ist — und was es über Angreifer verrät

Wir haben uns die Zeitabstände der IPs angesehen, die nie auslösen, aber trotzdem fünf oder mehr Versuche machen. Über 13.399 gemessene Abstände:

  • Median: 904 Sekunden — also gut 15 Minuten zwischen zwei Versuchen
  • Unteres Viertel: 256 Sekunden
  • Oberes Viertel: 4.344 Sekunden (über eine Stunde)

Das ist kein Zufall. Ein Bot, der stumpf hämmert, wird gesperrt. Ein Bot, der alle 15 Minuten einen Versuch macht, läuft ewig weiter — und weil es tausende Bots gibt, kommt in Summe trotzdem ein Versuch pro Minute an. Die Angreifer haben sich an die Verteidigung angepasst, und zwar an genau den Standardwert, der in jeder Anleitung steht.

Man kann daran schrauben — findtime = 1d und maxretry = 3 fangen deutlich mehr:

[sshd]
enabled  = true
backend  = systemd
maxretry = 3
findtime = 1d
bantime  = 1w

Das erhöht aber auch das Risiko, sich selbst auszusperren, wenn man mal einen falschen Schlüssel benutzt. Deshalb gehört dann zwingend eine Ausnahme dazu:

ignoreip = 127.0.0.1/8 ::1 DEINE.FESTE.IP.ADRESSE

Unsere ehrliche Einschätzung: fail2ban ist sinnvoll, aber es ist die dritte Verteidigungslinie, nicht die erste. Es hält die Logs sauberer und bremst die lautesten Angreifer. Was tatsächlich verhindert, dass jemand hereinkommt, ist die abgeschaltete Passwort-Anmeldung. Die Zahl, die zählt, steht weiter oben: 0 fehlgeschlagene Passwortversuche in 16,5 Tagen, weil es nichts zu raten gab.

Wer fail2ban als Hauptschutz betrachtet, verlässt sich auf ein Werkzeug, das nachweislich 70 % des Verkehrs nicht sieht.

Schritt 4: Automatische Updates — und die Falle dahinter

Die meisten erfolgreichen Einbrüche nutzen keine neue Lücke, sondern eine alte, für die längst ein Patch existiert. Automatische Sicherheitsupdates sind deshalb Pflicht:

apt install unattended-upgrades
dpkg-reconfigure -plow unattended-upgrades

Die Konfiguration in /etc/apt/apt.conf.d/20auto-upgrades sollte so aussehen:

APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";

Auf unserem Server läuft das seit Monaten zuverlässig. Ein Blick ins Protokoll zeigt echte Aktivität:

2026-08-20 06:17:03 INFO Packages that will be upgraded: linux-image-virtual linux-libc-dev
2026-08-21 06:30:47 INFO Packages that will be upgraded: bind9-dnsutils curl libcurl4t64 nginx nginx-common
2026-08-22 06:41:17 INFO Packages that will be upgraded: libpq5 postgresql-16 vim wget xxd

Sieht gut aus. Und genau hier haben wir beim Schreiben dieses Artikels auf unserem eigenen Server einen Fehler gefunden, den wir nicht erwartet hatten.

Der Befund: Updates installiert, aber nicht wirksam

Ein Blick auf den laufenden gegenüber dem installierten Kernel:

uname -r
# 6.8.0-124-generic

dpkg -l 'linux-image-*generic' | awk '/^ii/{print $2}' | sort -V | tail -1
# linux-image-6.8.0-138-generic

Der Server läuft mit Kernel 6.8.0-124 und hat 6.8.0-138 installiert. Vierzehn ABI-Versionen Unterschied. Die Datei /var/run/reboot-required existiert seit dem 20. August.

Das System hat also alles richtig gemacht: heruntergeladen, installiert, vorgemerkt. Nur wirksam wird ein neuer Kernel erst nach einem Neustart — und der Server läuft seit dem 30. Juni 2026, also seit über sieben Wochen, ohne Neustart durch.

Zur unabhängigen Bestätigung, weil eine selbstgebaute Prüfung auch selbst falsch sein kann:

needrestart -b
# NEEDRESTART-KCUR: 6.8.0-124-generic
# NEEDRESTART-KEXP: 6.8.0-138-generic
# NEEDRESTART-KSTA: 3

KSTA: 3 bedeutet in der Sprache dieses Werkzeugs: veralteter Kernel läuft. Zwei unabhängige Quellen, gleiche Aussage. Zusätzlich: 23 laufende Prozesse haben Bibliotheken im Speicher, die auf der Platte längst ersetzt wurden — sie laufen weiter mit der alten, möglicherweise verwundbaren Version, bis der jeweilige Dienst neu startet. needrestart nennt dazu konkret dbus.service, docker.service, systemd-logind.service und weitere.

Das ist die unbequemste Erkenntnis dieses Artikels, und sie betrifft uns selbst: „Automatische Updates sind aktiv” und „das System läuft mit den aktuellen Sicherheitspatches” sind zwei verschiedene Aussagen. Die erste haben wir monatelang für die zweite gehalten. Ein Blick auf systemctl is-active unattended-upgrades hätte weiter fröhlich active gemeldet.

Automatische Updates installieren Pakete, die erst nach einem Neustart wirksam werden

Die Gegenmaßnahme ist einfach, wenn man sie einmal einrichtet. Entweder man erlaubt den automatischen Neustart in /etc/apt/apt.conf.d/50unattended-upgrades:

Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "03:00";

Oder — wenn ein unbeaufsichtigter Neustart zu riskant ist, weil Dienste nicht sauber hochkommen — man lässt sich wenigstens benachrichtigen. Ein Prüfskript, das täglich läuft und meldet, wenn /var/run/reboot-required existiert, kostet drei Zeilen:

#!/bin/bash
[ -f /var/run/reboot-required ] || exit 0
echo "Neustart ausstehend seit $(stat -c %y /var/run/reboot-required | cut -d' ' -f1)"
echo "Laufender Kernel: $(uname -r)"
exit 1

Wichtig ist der letzte Teil: Ein Alarm braucht einen Empfänger. Ein Skript, das in eine Logdatei schreibt, die niemand liest, ist ein Kommentar. Es muss in einen Kanal, den man tatsächlich sieht — Mail, Messenger, Monitoring.

Schritt 5: Der Rest der Grundausstattung

Die folgenden Punkte sind weniger dramatisch, aber sie gehören dazu.

Benutzer statt Root

adduser deployer
usermod -aG sudo deployer

Wie oben beschrieben: Der Name sollte nichts über das Projekt verraten.

Zeitzone, Zeitsynchronisation und Locale

Klingt nach Kosmetik, ist aber die Grundlage jeder Fehlersuche. Wenn Logs auf verschiedenen Maschinen unterschiedliche Zeiten zeigen, lässt sich ein Vorfall nicht rekonstruieren.

timedatectl set-timezone Europe/Berlin
timedatectl status

Unsere Empfehlung nach eigener Erfahrung: Server auf UTC lassen, wenn mehrere Maschinen in verschiedenen Ländern stehen. Wir haben Server in Deutschland und Finnland; einheitliches UTC in den Logs hat schon mehr als einmal Zeit gespart. Die lokale Zeitzone gehört in die Anzeige, nicht in die Datenhaltung.

Webserver und TLS

Für die meisten Anwendungen sind nginx oder Caddy die Wahl. Der Unterschied in der Praxis: Caddy holt und erneuert Zertifikate von selbst, nginx braucht Certbot dafür. Wir betreiben beides und haben die Unterschiede in Apache vs. nginx gegenübergestellt.

Minimalbeispiel für Caddy — mehr braucht es für eine TLS-gesicherte Seite nicht:

deine-domain.de {
    reverse_proxy 127.0.0.1:3000
}

Für nginx als vorgelagerten Proxy haben wir eine ausführliche Anleitung geschrieben; der Kern ist ein server-Block mit proxy_pass auf den lokalen Dienst.

Logs im Blick behalten

Es reicht nicht, Logs zu haben — jemand muss hineinsehen. Ein paar Befehle, die wir regelmäßig benutzen:

# Wer hat sich erfolgreich angemeldet?
journalctl -u ssh | grep "Accepted"

# Welche Methode wurde benutzt?
journalctl -u ssh | grep -oE "Accepted [a-z-]+ for [^ ]+" | sort | uniq -c

Auf unserem Server sieht das so aus:

     81 Accepted publickey for root
      1 Accepted password for tunnelfabi

Genau eine Passwortanmeldung — ein bewusst eingerichteter Tunnelzugang. Alles andere läuft über Schlüssel. Solche Übersichten sind der schnellste Weg, um zu merken, dass sich etwas verändert hat.

Backups

Ein Server ohne Backup ist kein Server, sondern ein Risiko mit Uptime. Der wichtigste Punkt ist nicht die Software, sondern die Prüfung: Ein Backup, das nie zurückgespielt wurde, ist eine Vermutung. Wir haben das bei unserem Nextcloud-Test durchgemessen — Wartungsmodus an, Datenbank-Dump, Wartungsmodus aus, insgesamt 1,1 Sekunden Ausfallzeit. Der Aufwand ist fast immer kleiner als befürchtet; was fehlt, ist die Gewohnheit.

Was passiert, wenn man das alles nicht macht

Wir schreiben hier nicht aus der Theorie. Auf einem unserer Server lief fünf Tage lang ein fremder Krypto-Miner, hereingekommen über eine bekannte Lücke in einer Anwendungsabhängigkeit — nicht über SSH. Fünf Tage, in denen niemand es bemerkt hat.

Zwei Lehren daraus, die diesen Artikel prägen:

Erstens: Die Härtung von SSH schützt die Haustür. Der Einbruch kam durch das Anwendungsfenster. Ein sauber konfigurierter SSH-Dienst ist notwendig, aber er sagt nichts über die Anwendungen, die auf dem Server laufen.

Zweitens: Es hat fünf Tage gedauert, weil niemand hingesehen hat. Alle Werkzeuge waren installiert. Was fehlte, war der Blick — beziehungsweise ein Alarm, der an einer Stelle ankommt, die jemand liest.

Wir haben den Vorfall ausführlich dokumentiert — und in IT-Sicherheitslücken aufgeschrieben, wie Abhängigkeiten überhaupt zum größten Einfallstor wurden.

Abgeschlossene Checkliste zur Absicherung eines Linux-Servers

Die Checkliste zum Abarbeiten

Zusammengefasst, in der Reihenfolge, in der wir es machen würden — sortiert nach Wirkung, nicht nach Aufwand:

#SchrittWirkung
1SSH-Schlüssel einrichten, zweite Sitzung offen lassenGrundlage
2PasswordAuthentication noHöchste — schaltet den häufigsten Angriff ab
3Benutzer ohne Projektbezug anlegen, Root per Schlüssel oder ganz ausHoch
4sshd -T prüfen — nicht die KonfigurationsdateiVerifikation
5Firewall: alles zu, nur 22/80/443 aufHoch
6Docker-Ports an 127.0.0.1 bindenHoch — Firewall greift hier nicht
7unattended-upgrades aktivierenHoch
8Neustart-Benachrichtigung einrichtenHoch — sonst wirken Kernel-Updates nie
9fail2ban installieren, Werte anpassenMittel
10Zeitzone/NTP, einheitlich über alle ServerMittel
11Webserver + TLSBetrieb
12Backups und ein RückspieltestExistenziell
13Portwechsel bei SSHNiedrig — Lärmreduktion

Die Reihenfolge ist bewusst nicht die aus den üblichen Anleitungen. Punkt 6 und 8 stehen in den meisten Listen gar nicht — und beide waren auf unseren eigenen Servern die Stellen, an denen etwas nicht stimmte.

Welche Distribution für den Server?

Kurz, weil wir das in Debian vs. Ubuntu und Fedora vs. Ubuntu ausführlich behandelt haben: Für Server nehmen wir Debian stable als Standard und Ubuntu LTS dort, wo eine Software neuere Pakete braucht. Debian 13 wird bis August 2028 unterstützt, mit verlängertem Support bis Mitte 2030; Ubuntu 26.04 LTS bis April 2031, mit kostenpflichtiger Verlängerung bis 2036.

Der Unterschied, der in der Praxis zählt, ist nicht die Länge des Supports, sondern seine Breite: Ubuntus kostenlose Fünf-Jahres-Zusage deckt nur die Komponenten main und restricted ab — rund 10 % des Katalogs. Viele typische Serverpakete liegen in universe. Wir haben das für unseren Vergleich von Debian und Ubuntu selbst ausgezählt — dort stehen auch die Zahlen zu Paketalter und Support-Umfang.

Was für die Absicherung wichtiger ist als die Distribution: dass sie noch Sicherheitsupdates bekommt. Ein Server auf einer abgelaufenen Version bekommt keine Patches mehr, egal wie gut SSH konfiguriert ist.

Fazit: Zwei Zahlen, die alles sagen

Nach 16,5 Tagen Messung und einer Reihe unangenehmer Funde auf den eigenen Maschinen bleiben zwei Zahlen hängen:

1.265 Angriffsversuche pro Tag, 0 erfolgreiche. Der Unterschied zwischen diesen beiden Zahlen ist im Wesentlichen eine Konfigurationszeile: PasswordAuthentication no. Nicht fail2ban, nicht der Portwechsel, nicht eine ausgefeilte Firewall. Eine Zeile.

70,4 % des Angriffsverkehrs sieht fail2ban in der Standardkonfiguration nicht. Werkzeuge, die man installiert und dann für erledigt hält, erzeugen ein Sicherheitsgefühl, das nicht durch eine Messung gedeckt ist. Wir haben nachgerechnet, weil wir es wissen wollten — und das Ergebnis hat unsere Reihenfolge der Empfehlungen verändert.

Und der Fund, mit dem wir am wenigsten gerechnet hatten: Unser eigener Server lief sieben Wochen lang mit einem Kernel, der vierzehn Versionen alt war, während die Updates brav jede Nacht installiert wurden. „Automatisch” heißt nicht „wirksam”.

Wenn du einen Linux Server einrichten willst: Fang bei Punkt 2 der Liste an. Der Rest ist wichtig, aber der Rest ist Ergänzung.

Was wir nicht gemessen haben

Aus Gründen der Ehrlichkeit, weil eine geschätzte Zahl in einer Tabelle genauso aussieht wie eine gemessene:

  • Wir haben keinen echten Einbruchsversuch simuliert. Alle Angriffszahlen stammen aus echten, unaufgeforderten Zugriffen auf unsere Server — aber wir haben keinen Penetrationstest durchgeführt.
  • Die fail2ban-Auswertung ist eine Nachrechnung, kein Live-Experiment mit abgeschalteter Sperre. Wir haben die gemessenen Zeitstempel gegen die Regel gerechnet und das Ergebnis mit der von fail2ban selbst gemeldeten Sperrzahl plausibilisiert — die Größenordnung passt, exakt deckungsgleich ist es nicht.
  • Das Helsinki-Fenster ist mit 9,7 Stunden kurz. Die Tageswerte sind hochgerechnet und damit anfälliger für Schwankungen als die Zahlen des Hauptservers über 16,5 Tage.
  • Der Portwechsel-Vergleich ist kein sauberes Experiment. Die drei Server unterscheiden sich nicht nur im Port, sondern auch in Alter, Hoster und Bekanntheit ihrer IP-Adressen. Der Faktor 211 zeigt eine deutliche Tendenz, aber die Ursache ist nicht isoliert.
  • Keine Aussage zu SELinux/AppArmor, Intrusion Detection oder Zwei-Faktor-Authentifizierung für SSH. Alles sinnvoll, alles nicht Teil dieser Messung.

Häufige Fragen

Wie richte ich einen Linux Server sicher ein?

In dieser Reihenfolge: SSH-Schlüssel einrichten und mit einer zweiten offenen Sitzung testen, dann die Passwort-Anmeldung abschalten (PasswordAuthentication no), einen Benutzer ohne Projektbezug anlegen, die Firewall auf „alles verbieten” stellen und nur SSH, HTTP und HTTPS öffnen, Docker-Ports an 127.0.0.1 binden, automatische Sicherheitsupdates aktivieren und eine Benachrichtigung für ausstehende Neustarts einrichten. Die mit Abstand wirksamste Einzelmaßnahme ist die abgeschaltete Passwort-Anmeldung: Auf unserem Server standen 1.265 Angriffsversuche pro Tag null erfolgreichen gegenüber.

Wie viele Angriffe bekommt ein normaler Server?

Wir haben das auf unseren eigenen Maschinen gezählt: 1.265 Login-Versuche mit ungültigen Benutzernamen pro Tag von durchschnittlich 77 verschiedenen IP-Adressen, über einen Zeitraum von 16,5 Tagen. Auf einem Server mit aktivierter Passwort-Anmeldung kamen zusätzlich 6.200 Passwortversuche pro Tag hinzu, davon 3.603 direkt gegen den Benutzer root. Das sind keine gezielten Angriffe, sondern automatisierte Scanner, die den gesamten Adressbereich des Internets abklappern.

Reicht fail2ban als Schutz für SSH?

Nein. Wir haben 20.678 Angriffsereignisse gegen die Standardregel (5 Versuche in 10 Minuten) nachgerechnet: Nur 22,2 % der angreifenden IP-Adressen lösen sie überhaupt aus, 70,4 % des gesamten Angriffsverkehrs bleibt unbemerkt. Der Grund ist, dass viele Bots bewusst langsam vorgehen — der mittlere Abstand zwischen zwei Versuchen lag bei den unauffälligen IPs bei 15 Minuten. fail2ban ist sinnvoll, um Logs sauber zu halten und die lautesten Angreifer zu bremsen, aber es ist die dritte Verteidigungslinie, nicht die erste.

Sollte ich den SSH-Port ändern?

Es reduziert den Lärm massiv, ist aber keine echte Sicherheitsmaßnahme. Unser Server auf Port 52200 verzeichnete 6 Angriffsversuche pro Tag, unsere Server auf Port 22 zwischen 1.265 und 2.271 — ein Faktor von rund 211. Ein gezielter Angreifer findet den Dienst trotzdem in Sekunden, weil ein Portscan alle 65.535 Ports abfragt. Der echte Nutzen liegt in der Lesbarkeit der Logs: Zwischen tausend Fehlversuchen pro Tag fällt der eine echte Vorfall nicht auf, zwischen sechs schon. Der Preis sind Skripte und Zugänge, die den abweichenden Port kennen müssen.

Warum werden Benutzernamen wie mein Domainname angegriffen?

Weil sie öffentlich sind. Jedes TLS-Zertifikat wird in einem öffentlichen Certificate-Transparency-Protokoll eingetragen und ist für jeden abrufbar. Für unsere Hauptdomain fanden wir dort 86 verschiedene Hostnamen, inklusive vergessener Testinstanzen. Angreifer schneiden von diesen Namen die Endung ab und probieren sie als Benutzernamen. In unseren Logs entfielen 10 % aller Angriffe auf Benutzernamen, die direkt aus unseren Domains abgeleitet waren. Auf einem Server ohne zugehörige Domain tauchten dieselben Namen null Mal auf. Deshalb: Benutzernamen wählen, die nichts mit dem Projekt zu tun haben.

Schützt die Firewall meine Docker-Container?

Nein, nicht bei der Standardkonfiguration. Wir haben das live nachgewiesen: Ein Container mit dem Mapping -p 0.0.0.0:39999:80 war von einem externen Server aus mit HTTP 200 erreichbar, obwohl ufw auf „alles verbieten” stand und der Port nirgends freigegeben war. Zwei Kontrollmessungen im selben Test zeigten, dass die Firewall ansonsten funktioniert. Der Grund: Docker schreibt seine Regeln vor die von ufw verwalteten in die FORWARD-Kette. Die Lösung ist, Container-Ports explizit an das Loopback-Interface zu binden: 127.0.0.1:5432:5432 statt 5432:5432.

Reichen automatische Updates aus, um einen Server aktuell zu halten?

Nicht allein. Auf unserem eigenen Server liefen die automatischen Updates monatelang zuverlässig — trotzdem lief er mit Kernel 6.8.0-124, während 6.8.0-138 installiert war. Grund: Ein neuer Kernel wird erst nach einem Neustart wirksam, und die Maschine lief seit über sieben Wochen durch. Zusätzlich hatten 23 Prozesse bereits ersetzte Bibliotheken im Speicher. Prüfen lässt sich das mit needrestart -b oder über die Datei /var/run/reboot-required. Entweder automatische Neustarts aktivieren oder eine Benachrichtigung einrichten, die an einer Stelle ankommt, die tatsächlich gelesen wird.

Sollte ich Root-Login komplett deaktivieren?

PermitRootLogin no in Kombination mit einem normalen Benutzer und sudo ist die strengere und bei mehreren Zugriffsberechtigten die richtige Wahl, weil in den Logs nachvollziehbar bleibt, wer was getan hat. prohibit-password erlaubt Root-Anmeldung ausschließlich per Schlüssel und ist bei einer Ein-Personen-Maschine ein vertretbarer Kompromiss — vorausgesetzt, PasswordAuthentication ist global abgeschaltet. Entscheidend ist nicht die Wahl zwischen beiden, sondern dass gegen root keine Passwörter geraten werden können: Auf unserem Server mit aktiver Passwort-Anmeldung liefen 3.603 Rateversuche pro Tag allein gegen diesen einen Benutzer.

Welche Linux-Distribution eignet sich am besten für einen Server?

Wir nutzen Debian stable als Standard und Ubuntu LTS dort, wo neuere Pakete gebraucht werden. Debian 13 hat Support bis August 2028 (verlängert bis Mitte 2030), Ubuntu 26.04 LTS bis April 2031 (kostenpflichtig verlängerbar bis 2036). Der praktisch wichtigere Unterschied ist nicht die Dauer, sondern der Umfang: Ubuntus kostenlose Support-Zusage deckt nur main und restricted ab, während viele typische Serverpakete in universe liegen. Für die Sicherheit zählt vor allem, dass die eingesetzte Version überhaupt noch Updates erhält.

Wie merke ich, dass mein Server kompromittiert wurde?

Meistens gar nicht — das ist das eigentliche Problem. Auf einem unserer Server lief fünf Tage lang ein fremder Krypto-Miner, hereingekommen über eine bekannte Lücke in einer Anwendungsabhängigkeit, nicht über SSH. Alle Sicherheitswerkzeuge waren installiert; was fehlte, war ein Alarm, den jemand liest. Praktische Ansatzpunkte: erfolgreiche Anmeldungen regelmäßig prüfen (journalctl -u ssh | grep Accepted), auffällige CPU-Last beobachten, unerwartete offene Ports mit ss -tlnp kontrollieren und ausgehende Verbindungen im Blick behalten. Ein Alarm, der nur in eine Logdatei schreibt, hilft nicht.