Zum Inhalt springen

2. Server aufsetzen und härten

Zu Zen-Modus wechseln

Ihr Projekt aus dem letzten Kapitel braucht einen Ort, an dem es läuft. Bisher war dieser Ort Ihr Notebook: npm run dev starten, Browser öffnen, localhost. Das genügt zum Programmieren und blendet fast alles aus, was den Betrieb einer Webanwendung ausmacht.

Ab dieser Woche ist der Ort ein anderer. Sie mieten eine Maschine in einem Rechenzentrum, die Sie nie sehen werden, die keine Fenster und keine Maus hat, die durchläuft, während Sie schlafen, und die vom ersten Moment an unter einer öffentlichen IP-Adresse aus dem gesamten Internet erreichbar ist. Was daraus folgt, merken Sie am zweiten Tag im Log: Fremde Rechner probieren dort im Minutentakt Anmeldungen aus. Niemand hat es auf Sie abgesehen, das läuft automatisiert über alle erreichbaren Adressen. Trotzdem ist es Ihr Server, der da angeklopft wird.

Deshalb hat dieses Kapitel zwei Hälften, die zusammengehören: erst in Betrieb nehmen, dann abschließen. Beides in derselben Woche, weil ein Server, der offen steht, bis später nicht wartet.

Ein Server ist kein besonders schneller PC. Er wird für andere Bedingungen gebaut und deshalb anders bedient.

ArbeitsplatzrechnerServer
Bedienunggrafische Oberfläche, direkt davorTextkonsole, über das Netz
Laufzeitwird ein- und ausgeschaltetläuft durch, ein Neustart ist ein Ereignis
Benutzereine Personviele Dienste, jeder mit eigener Kennung
ProgrammeAnwendungen mit FensternDienste im Hintergrund, ohne Oberfläche
Fehlerfalljemand sieht die Meldungniemand sieht sie

Die letzte Zeile ist die folgenreichste. Auf Ihrem Notebook meldet sich ein Fehler von selbst, weil Sie davorsitzen. Auf einem Server passiert er um drei Uhr früh in einem Rechenzentrum in Frankfurt, und wenn er nicht in eine Datei geschrieben wird, hat er nie stattgefunden. Daraus folgt die Arbeitsweise, die dieses ganze Jahr trägt: Bei jedem Problem lautet die erste Frage nicht „Was habe ich falsch geklickt?”, sondern „Wo steht das im Log?”.

Ein VPS (Virtual Private Server) ist eine virtuelle Maschine bei einem Rechenzentrumsbetreiber: eigene IP-Adresse, eigenes Betriebssystem, volle Rechte darauf, monatlich kündbar. Die kleinste Ausbaustufe reicht für dieses Jahr aus.

  1. Konto anlegen bei Hetzner Cloud. Minderjährige brauchen dafür eine erziehungsberechtigte Person; ein Team teilt sich sinnvollerweise einen Server und die Kosten.

  2. Server erzeugen. Standort Deutschland oder Finnland, Typ CX22 mit 2 vCPU und 4 GB RAM. Damit laufen Anwendung, Datenbank, CMS und die Deployment-Oberfläche aus dem nächsten Kapitel gemeinsam.

  3. Betriebssystem wählen: Ubuntu LTS oder Debian stable. Beide sind gut dokumentiert und werden jahrelang mit Sicherheitsupdates versorgt. Alles Folgende funktioniert auf beiden.

  4. SSH-Schlüssel hinterlegen, und zwar schon im Anlegedialog. Dann startet die Maschine ohne Passwortanmeldung. Wie Sie den Schlüssel erzeugen, steht im nächsten Abschnitt; erzeugen Sie ihn also vorher.

  5. Aufschreiben: IP-Adresse, Betriebssystem und Version, Servertyp, Standort, Datum. Diese fünf Angaben sind der erste Eintrag in docs/betrieb.md.

Die Verbindung zum Server läuft über SSH (Secure Shell): ein verschlüsselter Kanal, an dessen Ende eine Shell auf dem Server steht. Alles, was Sie eintippen, wird dort ausgeführt.

Angemeldet wird nicht mit einem Passwort, sondern mit einem Schlüsselpaar. Der private Schlüssel bleibt auf Ihrem Rechner und verlässt ihn nie. Der öffentliche wird auf dem Server hinterlegt und darf herumgezeigt werden. Ein Passwort lässt sich erraten, ein Ed25519-Schlüssel nicht; darum wird die Passwortanmeldung im Härtungsteil ganz abgeschaltet.

  1. Schlüsselpaar erzeugen (auf Ihrem eigenen Rechner, nicht auf dem Server):

    Terminal-Fenster
    ssh-keygen -t ed25519 -C "<YOUR_EMAIL@ADDRESS>"

    Es entstehen zwei Dateien in ~/.ssh/: id_ed25519 (privat) und id_ed25519.pub (öffentlich). Die Passphrase, nach der gefragt wird, verschlüsselt den privaten Schlüssel auf der Platte. Vergeben Sie eine; sie schützt Sie an dem Tag, an dem das Notebook verschwindet.

  2. Öffentlichen Schlüssel auf den Server bringen, falls das nicht schon beim Anlegen passiert ist:

    Terminal-Fenster
    ssh-copy-id [email protected]
  3. Anmelden:

    Terminal-Fenster

    Beim ersten Verbindungsaufbau zeigt SSH den Fingerprint des Servers und fragt, ob er stimmt. Diese Frage ist nicht rhetorisch. Sie ist der einzige Zeitpunkt, an dem Sie prüfen können, mit welcher Maschine Sie tatsächlich sprechen. Vergleichen Sie den Fingerprint mit dem, den die Cloud-Oberfläche anzeigt.

  4. Verbindung beenden mit exit oder Strg+D.

sequenceDiagram
  participant C as Ihr Rechner
  participant S as Server
  C->>S: Verbindungsaufbau (Port 22)
  S->>C: Host-Schlüssel (Fingerprint)
  C->>C: schon bekannt? -> known_hosts
  C->>S: Anmeldung als Benutzer, Nachweis mit privatem Schlüssel
  S->>S: passt der öffentliche Schlüssel in authorized_keys?
  S->>C: Shell

Die Shell wirkt am Anfang abweisend, weil sie nichts vorschlägt und nichts anbietet. Dafür kann sie zwei Dinge, die keine grafische Oberfläche kann: Jeder Handgriff lässt sich aufschreiben und dadurch beliebig oft wiederholen, und jeder Handgriff lässt sich in die Dokumentation kopieren. Genau deshalb bedient man Server so.

Das Minimum, das sitzen sollte:

Terminal-Fenster
pwd # where am I?
ls -la # what is here, including hidden files
cd /etc/nginx # change directory
cat file.conf # print a file
less file.log # page through a file, q quits
tail -f file.log # follow along while the file grows
grep -r "error" /etc # search recursively
cp / mv / rm # copy, move, delete
mkdir -p a/b/c # create directories, nested ones too
df -h / free -h # disk and memory usage
ps aux / top # running processes

Zum Werkzeug werden diese Befehle erst durch drei Eigenschaften, die alle miteinander teilen. Jedes Programm liest von der Standardeingabe und schreibt auf die Standardausgabe; > lenkt die Ausgabe in eine Datei um, >> hängt an, < liest aus einer Datei. Eine Pipe mit | verbindet die Ausgabe des einen Programms mit der Eingabe des nächsten, sodass cat access.log | grep " 500 " | wc -l aus drei winzigen Programmen eine Antwort baut: die Anzahl der Serverfehler im Zugriffslog. Und jedes Programm hinterlässt einen Exit-Code, wobei 0 Erfolg bedeutet; echo $? zeigt ihn an. Auf dieser einen Zahl bauen später sämtliche CI-Pipelines auf.

Linux legt Dinge an vorhersehbaren Stellen ab. Sechs Pfade reichen für dieses Jahr:

PfadInhalt
/etcKonfiguration aller Dienste, durchgehend Textdateien
/var/logLogdateien
/var/wwwausgelieferte Webinhalte (Konvention)
/home/userpersönliche Verzeichnisse, kurz ~
/opt, /srvselbst installierte Anwendungen und Dienstdaten
/tmpTemporäres, wird beim Neustart geleert

Dass unter /etc ausschließlich Text liegt, hat eine praktische Folge: Serverkonfiguration ist versionierbar. Eine nginx.conf gehört genauso in ein Git-Repository wie Programmcode, und aus demselben Grund.

Auf einem Server hat nicht nur die Person eine Kennung, sondern jeder Dienst. Der Webserver läuft als www-data, die Datenbank als postgres. Dahinter steht das Principle of least privilege: Wenn ein Dienst übernommen wird, soll der Angreifer genau dessen Rechte bekommen und keine darüber hinaus.

Terminal-Fenster
whoami # who am I working as right now?
id # user and group IDs
sudo -u www-data ls /var/www # run something under a different account

Jede Datei trägt Rechte für Eigentümer, Gruppe und alle anderen, jeweils für Lesen (r, 4), Schreiben (w, 2) und Ausführen (x, 1).

Terminal-Fenster
ls -l app.js
# -rw-r--r-- 1 deploy www-data 1240 Sep 12 10:04 app.js
# ^^^ owner: rw- group: r-- others: r--
chmod 640 config.env # owner reads and writes, group reads, rest nothing
chown deploy:www-data app.js # set owner and group

Bei Verzeichnissen heißt x nicht „ausführen”, sondern „hineingehen dürfen”. Ein Verzeichnis mit r-- lässt sich auflisten, aber nicht betreten, was zu den beliebtesten Ursachen für ein rätselhaftes Permission denied gehört.

Der Benutzer root darf alles, auch das Falsche, und zwar ohne Rückfrage. Deshalb arbeitet man unter einer normalen Kennung und stellt einzelne Befehle sudo voran:

Terminal-Fenster
sudo systemctl restart nginx
sudo journalctl -u nginx -n 50

Das hat zwei Wirkungen. Sie merken, wann Sie in den gefährlichen Bereich wechseln, und jedes sudo wird protokolliert. Auf einem Server, an dem drei Personen arbeiten, ist das der Unterschied zwischen einer Erklärung und einem Rätsel.

Software wird auf einem Server nicht heruntergeladen und entpackt, sondern über den Paketmanager der Distribution installiert. Der kennt Abhängigkeiten, kennt Versionen und liefert, worauf es hier ankommt: Sicherheitsupdates.

Terminal-Fenster
sudo apt update # refresh the package lists
sudo apt install nginx # install a package
sudo apt upgrade # upgrade the installed packages
apt list --installed | grep nginx
sudo apt remove nginx

Was am Paketmanager vorbei installiert wird, müssen Sie selbst aktualisieren. Und genau das vergisst man, weil nichts daran erinnert. Veraltete Software ist einer der häufigsten Einbruchswege überhaupt, und sie entsteht fast immer auf diesem Weg.

Ein Programm, das dauerhaft im Hintergrund läuft, heißt Dienst oder Daemon. Auf aktuellen Linux-Systemen verwaltet systemd diese Dienste: Er startet sie beim Hochfahren, startet sie nach einem Absturz neu und sammelt ihre Ausgaben ein.

Terminal-Fenster
systemctl status nginx # is it running? since when? last log lines
sudo systemctl start nginx
sudo systemctl stop nginx
sudo systemctl restart nginx # stop it and start it again
sudo systemctl reload nginx # re-read the configuration, connections stay
sudo systemctl enable nginx # start it together with the system

Der Unterschied zwischen restart und reload ist im Betrieb kein Detail: restart beendet den Dienst und kappt dabei jede offene Verbindung, reload liest nur die Konfiguration neu. Wer reload verwenden kann, verwendet reload.

stateDiagram-v2
  [*] --> inactive
  inactive --> activating: systemctl start
  activating --> active: Start erfolgreich
  activating --> failed: Start fehlgeschlagen
  active --> reloading: systemctl reload
  reloading --> active
  active --> inactive: systemctl stop
  failed --> activating: Restart=on-failure

systemd sammelt die Ausgaben aller Dienste im Journal. Vier Aufrufe decken den Alltag ab:

Terminal-Fenster
journalctl -u nginx # everything about this service
journalctl -u nginx -n 100 # the last 100 lines
journalctl -u nginx -f # follow along
journalctl -p err --since today # only errors from today

Damit steht das wichtigste Diagnosewerkzeug des Jahres. Die Reihenfolge bei jedem Problem: Status ansehen, Log lesen, Konfiguration prüfen. Erst danach etwas ändern. Wer diese Reihenfolge umdreht, ändert im Dunkeln und weiß am Ende nicht, welche der fünf Änderungen geholfen hat.

Damit ist der Server in Betrieb. Bevor er abgeschlossen wird, verschaffen Sie sich ein Bild vom Ist-Zustand, denn ohne Ausgangswert lässt sich später keine Verbesserung belegen.

Terminal-Fenster
ss -tulpn # which services listen on which port?

Jede Zeile dieser Ausgabe ist eine Tür. Für jede beantworten Sie zwei Fragen: Welcher Dienst steht dahinter, und muss er wirklich aus dem Internet erreichbar sein? Eine Datenbank, die auf 0.0.0.0:5432 lauscht, ist der Klassiker unter den vermeidbaren Fehlern. Sie gehört an 127.0.0.1 gebunden oder in ein internes Container-Netz, nie ans offene Netz.

Der zweite Blick geht ins Log, am besten nach den ersten 24 Betriebsstunden:

Terminal-Fenster
sudo journalctl -u ssh --since "24 hours ago" | grep -i "failed"
sudo lastb | head -20 # failed login attempts

Was Sie dort finden, sind Anmeldeversuche mit Namen wie root, admin, test, ubuntu, im Minutenabstand, von wechselnden Adressen. Diese Zeilen sind der beste Einstieg in den Rest des Kapitels, den ein Lehrbuch nicht liefern kann: Es passiert gerade, auf Ihrer Maschine, und Sie können zusehen.

Der Zugang ist der lohnendste Angriffspunkt, also wird er zuerst geschlossen. Die Einstellungen stehen in /etc/ssh/sshd_config:

PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
AllowUsers deploy

PermitRootLogin no verbietet die direkte Anmeldung als root; wer Rechte braucht, meldet sich normal an und nimmt sudo. Die beiden Abschaltungen entfernen die Passwortanmeldung vollständig, auch über den Umweg der interaktiven Tastaturabfrage, den viele Anleitungen vergessen. AllowUsers grenzt zusätzlich auf die Kennungen ein, die sich überhaupt anmelden dürfen (hier in diesem Fall nur der User “deploy”).

Der Wechsel des SSH-Ports auf etwas anderes als 22 wird oft empfohlen. Er reduziert die Menge an Logzeilen deutlich, weil die automatisierten Scanner überwiegend Port 22 abklappern. Sicherheit gewinnen Sie dadurch nicht: Ein Portscan findet den neuen Port in Sekunden. Weniger Lärm im Log ist trotzdem ein legitimer Grund, solange Sie sich über den Unterschied im Klaren sind und ihn in der Dokumentation auch so benennen.

Eine Firewall dreht die Grundeinstellung um: Statt alles zu erlauben und Einzelnes zu verbieten, wird alles verboten und Einzelnes erlaubt. Auf Ubuntu und Debian ist ufw die einfachste Oberfläche dafür.

Terminal-Fenster
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow 80,443/tcp
sudo ufw enable
sudo ufw status verbose

Drei Türen bleiben offen: SSH für Sie, 80 und 443 für die Anwendung. Alles andere, was ss -tulpn vorher gezeigt hat, ist von außen nicht mehr erreichbar. Prüfen Sie das von einem anderen Rechner aus, nicht vom Server selbst.

Der häufigste Weg in einen Server führt nicht über einen raffinierten Angriff, sondern über eine bekannte Lücke in einem Paket, für das seit acht Wochen ein Update bereitliegt. Die Gegenmaßnahme ist unspektakulär und wirksam:

Terminal-Fenster
sudo apt install unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades

In /etc/apt/apt.conf.d/50unattended-upgrades legen Sie fest, was automatisch eingespielt wird (Sicherheitsupdates: ja) und ob die Maschine dafür neu starten darf:

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

Ein automatischer Neustart um halb vier ist für ein Schulprojekt die richtige Wahl. Kernel-Updates wirken erst nach einem Neustart, und ein Server, der aus Vorsicht nie neu startet, sammelt Lücken an, die längst geschlossen wären. In größeren Umgebungen ist dieselbe Frage schwieriger, dort hängen Wartungsfenster und Ausfallzeiten daran; darum geht es in der 5. Klasse.

Die Härtungsmaßnahmen, die am wenigsten aussehen, wirken am längsten. Legen Sie eine Arbeitskennung an und arbeiten Sie dauerhaft unter ihr:

Terminal-Fenster
sudo adduser deploy
sudo usermod -aG sudo deploy

Anwendungen laufen unter eigenen Kennungen und nicht als root, jede mit Zugriff nur auf die eigenen Verzeichnisse. Pakete, die Sie nicht brauchen, gehören deinstalliert, weil jedes zusätzliche Paket eine zusätzliche Stelle ist, die Sicherheitsupdates braucht. Und ein Dienst, der nur lokal gebraucht wird, lauscht auf 127.0.0.1 statt auf allen Adressen. Das ist derselbe Gedanke wie oben bei den Dateirechten, nur eine Ebene höher.

fail2ban liest Logdateien mit und sperrt Adressen, die sich wiederholt erfolglos anmelden.

Terminal-Fenster
sudo apt install fail2ban
sudo systemctl enable --now fail2ban
sudo fail2ban-client status sshd

Die letzte Zeile zeigt, wie viele Adressen aktuell gesperrt sind. Sehen Sie sich diese Zahl nach 24 Stunden an, sie ist eindrucksvoller als jede Statistik aus einem Lehrbuch.

Zur Einordnung gehört die Grenze dazu: fail2ban hält automatisierte Massenversuche auf und reduziert das Rauschen im Log. Gegen jemanden, der es gezielt auf Ihr System abgesehen hat, hilft es kaum, weil er nicht zehntausend Passwörter aus derselben Adresse probiert. Der eigentliche Schutz bleibt die abgeschaltete Passwortanmeldung, fail2ban ist die Bequemlichkeit obendrauf.

Eine Härtungsmaßnahme, die niemand nachweisen kann, ist im Zweifel nicht vorhanden. Deshalb führen Sie ab dieser Woche zwei Dateien im Repository.

In docs/betrieb.md steht jeder Eingriff am Server mit Datum, Befehl, Grund und Ergebnis. In drei Monaten ist das der Unterschied zwischen „ich richte es neu ein” und „ich lese nach”.

In docs/haertung.md steht jede Maßnahme mit ihrem Nachweis, und zwar in Form der Befehlsausgabe vorher und nachher:

## SSH: Passwortanmeldung abgeschaltet
Datum: 2026-09-29
Vorher:
$ sudo sshd -T | grep passwordauthentication
passwordauthentication yes
Maßnahme:
PasswordAuthentication no, KbdInteractiveAuthentication no
in /etc/ssh/sshd_config; zusätzlich 50-cloud-init.conf angepasst
Nachher:
$ sudo sshd -T | grep passwordauthentication
passwordauthentication no
Gegenprobe: ssh -o PubkeyAuthentication=no deploy@server
-> Permission denied (publickey).

Am Ende des Kapitels stellen Sie den Ausgangs- und den Endzustand der offenen Ports gegenüber. Diese beiden ss -tulpn-Ausgaben nebeneinander sind der kürzeste Beleg dafür, dass sich etwas geändert hat.

Geänderte Dateien aus /etc kopieren Sie zusätzlich ins Repository und committen sie. Wer nur auf dem Server ändert, hat den Zustand nirgends beschrieben und beginnt beim nächsten Server wieder bei null. Das klingt nach Bürokratie und ist die erste Stufe dessen, was später Infrastructure as Code heißt: eine Umgebung, die sich aus ihrer Beschreibung wiederherstellen lässt.

  1. Als root arbeiten, weil dann alles funktioniert. Es funktioniert dann auch jeder Fehler, und zwar sofort und vollständig.
  2. Rechteprobleme mit chmod 777 beenden. Das Symptom geht weg, das Problem wächst.
  3. restart statt reload im laufenden Betrieb, womit ohne Not alle bestehenden Verbindungen abreißen.
  4. Konfiguration ändern, ohne sie zu testen, und danach vor einem Dienst stehen, der nicht mehr startet.
  5. Die SSH-Sitzung schließen, bevor die neue Konfiguration geprüft ist. Der schnellste Weg, sich selbst auszusperren.
  6. Nur sshd_config ändern und die Dateien in sshd_config.d/ übersehen. Die Härtung steht dann auf dem Papier und nirgends sonst.
  7. Container-Ports veröffentlichen und annehmen, ufw fange das ab. Tut es nicht.
  8. Software am Paketmanager vorbei installieren und die Sicherheitsupdates dafür stillschweigend selbst übernehmen.
  9. Den privaten SSH-Schlüssel weitergeben, in ein Repository, auf den Server, in einen Chat. Er bleibt auf Ihrem Rechner, ausnahmslos.
  10. Ohne Aufzeichnung arbeiten. Nach zwei Wochen weiß niemand mehr, warum eine Einstellung so ist, wie sie ist, Sie eingeschlossen.
  • Erklären: benennen, worin sich ein Server von einem Arbeitsplatzrechner unterscheidet, und begründen, warum daraus eine textbasierte Bedienung und eine Logkultur folgen.
  • Anwenden: einen virtuellen Server anlegen, Betriebssystem und Größe begründet wählen und den Zugang von Anfang an über Schlüssel einrichten.
  • Anwenden: ein SSH-Schlüsselpaar erzeugen, den öffentlichen Schlüssel hinterlegen, den Fingerprint prüfen und sich anmelden.
  • Anwenden: sich im Dateisystem bewegen, Dateien ansehen und durchsuchen und Befehle über Pipes zu einer Auswertung verketten.
  • Anwenden: Eigentümer und Rechte setzen und ein Permission denied systematisch eingrenzen.
  • Erklären: das Prinzip der geringsten Rechte erklären und an den Diensten des eigenen Servers zeigen.
  • Anwenden: Pakete verwalten und Dienste mit systemctl starten, neu laden und im Status prüfen.
  • Anwenden: ein Dienstproblem in der Reihenfolge Status, Log, Konfiguration eingrenzen und mit journalctl belegen.
  • Anwenden: die Angriffsfläche des eigenen Servers mit ss -tulpn bestimmen und jeden offenen Port begründen.
  • Anwenden: SSH härten, eine Firewall mit Standardverbot einrichten und automatische Sicherheitsupdates aktivieren.
  • Beurteilen: die Wirkung einer Maßnahme einschätzen, etwa Portwechsel gegen abgeschaltete Passwortanmeldung, und die Grenzen von fail2ban benennen.
  • Anwenden: Eingriffe und Härtungsmaßnahmen mit Nachweis dokumentieren und Konfigurationsdateien versionieren.

Am Ende dieses Kapitels läuft der Server, auf dem Ihr Projekt das restliche Jahr wohnt. Abzugeben sind:

  • Der Server ist erreichbar, die Anmeldung erfolgt ausschließlich über Schlüssel unter einer Arbeitskennung.
  • docs/betrieb.md mit den Eckdaten der Maschine und allen bisherigen Eingriffen.
  • docs/haertung.md mit den Maßnahmen zu SSH, Firewall und automatischen Updates, jeweils mit Nachweis vorher und nachher.
  • Die Gegenüberstellung der offenen Ports zu Beginn und am Ende des Kapitels.
  • Ein Snapshot des gehärteten Zustands, damit der Ausgangspunkt für das nächste Kapitel gesichert ist.
  • Aufgabe 04 - Erster Kontakt: Server und Shell
  • Aufgabe 05 - Schlüssel statt Passwort
  • Aufgabe 06 - Benutzer, Gruppen und Rechte
  • Aufgabe 07 - Dienst kaputt: Fehlersuche mit journalctl
  • Aufgabe 08 - Angriffsfläche bestimmen und schließen