Skip to content

Sicherheit und Hardening

ElGregor edited this page Aug 15, 2026 · 1 revision

Sicherheit und Hardening

Home

Was dieses Repo an Sicherheitsmechanik mitbringt, welche Defaults schwach sind und wo die Härtung zusätzlich zur Repo-Stand notwendig ist. Vier Bereiche: Secrets-Handhabung, Netz/Ports, RCON und Audit-Checkliste.

Vertieft in Repo: docs/05-wartung.md, docs/07-troubleshooting.md; Detailseiten ENV-Variablen, Ports-und-Netzwerk, Wartung-und-Automatik, Konventionen-und-Quellen, Cheatsheet-Tagesbetrieb.

Secrets-Übersicht (Repo-Defaults)

Die folgenden vier Werte stehen in .env.example (1:1 übernommen, Repo-Default) und sind die einzigen Geheimnisse, die das Repo aktiv verwaltet:

Variable Wert in .env.example Wirkung
ADMIN_PASSWORD CHANGE_ME In-Game-Admin-Login; fließt in servertest.iniAdminPassword= (Repo-Default)
SERVER_PASSWORD CHANGE_ME Join-Passwort für Spieler; fließt in servertest.iniPassword= (Repo-Default)
RCON_PASSWORD CHANGE_ME Passwort für RCON (mcrcon -p); genutzt von scripts/announce.sh (Repo-Default)
DISCORD_WEBHOOK_URL leer Webhook-URL für notify() aus scripts/lib/common.sh (Repo-Default)

Alle vier Default-Werte sind nicht-sicher: CHANGE_ME ist explizit ein Platzhalter, das leere Webhook schaltet Discord-Alerts still ab (notify() macht dann sofort return, ohne zu senden, 1:1 aus scripts/lib/common.sh).

.env — die wichtigsten Regeln

  • .env niemals committen. Repo-Regel, dokumentiert in der .env.example-Vorlage selbst (Zeile 3, 1:1: „Werte lokal ausfuellen, NIE committen"). Die .gitignore deckt .env, .env.local und docker/.env explizit ab (1:1).
  • Dateirechte chmod 600 .env setzen (Heuristik; Standard für Secret-Dateien). Skripte lesen die Datei als der ausführende Benutzer; lesbar nur für Eigentümer ist Standard.
  • Nach jeder Änderung an .env einmal make render laufen lassen — die Templates werden per envsubst neu geschrieben, Werte landen in servertest.ini und servertest_SandboxVars.lua (Repo-Default).
  • .env muss beim Skript-Aufruf im Repo-Root liegen — Skripte gehen vom aktuellen Verzeichnis aus (Repo-Default aus lib/common.sh).

Detailübersicht zu allen Variablen: ENV-Variablen.

CI-Secret-Scan (1:1 aus .github/workflows/ci.yml)

Im CI läuft ein einfacher grep über Shell-, YAML- und Template-Dateien (1:1 aus .github/workflows/ci.yml):

! grep -rInE "(PASSWORD|TOKEN|SECRET)=[^C][^H][^A][^N][^G][^E]" \
  --include="*.sh" --include="*.yml" --include="*.tmpl" . \
  || (echo "Moegliches Secret im Repo!" && exit 1)

Was er fängt (Beispiele):

Beispiel Erkannt?
FOO=bar Nein (kein Schlüsselwort)
PASSWORD=actualSecretValue Ja (matcht das negative Lookahead [^C][^H]… nicht)
PASSWORD=CHANGE_ME Nein (das ist genau der Default, der erlaubt sein soll)
DISCORD_WEBHOOK_URL= (leer) Nein
DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/... Ja (matcht SECRET ist zwar nicht im Schlüssel, aber PASSWORD/TOKEN/SECRET als Variablenname würde matchen — eine Webhook-URL selbst enthält keinen dieser Namen)

Was er NICHT fängt (Heuristik, aus der Mechanik abgeleitet):

  • Secret-Werte in .env — die Datei liegt per .gitignore nie im Repo und der grep läuft nur über committed files.
  • Base64-kodierte oder anderweitig verschleierte Werte.
  • Geheime Werte in Kommentaren oder Commit-Messages.
  • Hex/UUID-Werte ohne PASSWORD/TOKEN/SECRET im Variablennamen.
  • Secrets in .example-Dateien (z. B. RCON_PASSWORD=CHANGE_ME) — die sind per Design erlaubt.

Der Scan ist ein grobes Sieb, kein Audit-Ersatz (Heuristik).

UFW — Firewall-Regeln (kompakt)

Konsistent zu Ports-und-Netzwerk (dort ausführlich mit Quellen- Konflikt). Reihenfolge 1:1 aus pdf/content.json Kapitel „Linux-Grundsetup" (Community-Quelle: PDF-Handbuch; in der Repo-Doku docs/02 ohne den TCP-Zusatz).

Regel Zweck Quelle
sudo ufw default deny incoming Eingehend grundsätzlich blocken Community-Quelle (PDF-Handbuch)
sudo ufw default allow outgoing Ausgehend erlauben Community-Quelle (PDF-Handbuch)
sudo ufw allow OpenSSH SSH für Administration Community-Quelle (PDF-Handbuch)
sudo ufw allow 16261/tcp Hauptport TCP (Quellen-Konflikt, Heuristik: beides freigeben) Heuristik (PDF-Handbuch)
sudo ufw allow 16261/udp Hauptport UDP (pzwiki) Community-Quelle (pzwiki)
sudo ufw allow 16262/udp Datenverkehrs-Port (UDP) Community-Quelle (pzwiki)
sudo ufw enable Firewall scharf schalten Community-Quelle (PDF-Handbuch)
sudo ufw status verbose Kontrolle nach enable Community-Quelle (PDF-Handbuch)

RCON-Port (27015/TCP per Repo-Default) bleibt geschlossen für eingehende Verbindungen aus dem Internet — siehe nächster Abschnitt.

RCON-Sicherheit

Drei harte Repo-Fakten (1:1 aus scripts/announce.sh):

  • Host ist hardkodiert 127.0.0.1: Eine Variable RCON_HOST gibt es im Repo nicht (Repo-Befund). Das Skript verbindet sich ausschließlich loopback.
  • RCON_ENABLED als Guard: Bei false oder leer macht announce.sh still exit 0, ohne irgendetwas zu senden (1:1 aus dem Skript).
  • mcrcon -p Passwort kommt aus .env (RCON_PASSWORD, Repo-Default CHANGE_ME).

Empfehlungen (Heuristik, abgeleitet aus dem Repo-Stand):

Maßnahme Warum
RCON-Port (27015/TCP per Repo-Default) nicht öffentlich freigeben Nur Loopback ist im Repo vorgesehen
Kein Port-Forwarding auf RCON im Router Versehentlich geöffnet = volle Server-Konsole für jeden im Internet
SSH-Tunnel statt Remote-RCON Wer RCON remote braucht, tunneliert via SSH
RCON_ENABLED=false wenn keine In-Game-Ansagen nötig Reduziert die Angriffsfläche auf das lokale Loopback
RCON_PASSWORD ändern (nicht CHANGE_ME) Standard-Härtung

SSH-Härtung (allgemeine Best Practice)

Hinweis (allgemeine Best Practice, nicht PZ-/Repo-dokumentiert): Die folgenden Empfehlungen stammen aus generischer Linux-Systemadministration und werden hier nur kurz erwähnt. Das Repo selbst äußert sich nicht zu SSH-Konfiguration.

Maßnahme Zweck
SSH-Login nur mit Schlüsseln Passwort-Login brute-force-resistent
PasswordAuthentication no in sshd_config Erzwingt Schlüssel-Login
PermitRootLogin no (oder prohibit-password) SSH-Login als root ausschließen
Fail2ban o. ä. Reduziert Login-Spam

Konfiguration siehe Distribution-Doku und man sshd_config.

SteamCMD anonymous — kein Steam-Login nötig

Der Server-Bezug läuft komplett ohne Steam-Account (Community-Quelle: pzwiki, Repo-Konsens). Repo-Beleg (1:1 aus scripts/install.sh / Repo-Doku): die Installation verwendet +login anonymous. Damit:

Vorteil Anmerkung
Kein Steam-Account auf dem Server-Host nötig Reduziert Secret-Anforderungen
Funktioniert trotzdem für App 380870 und Workshop-Download Community-Quelle (Workshop-Download mit +workshop_download_item 108600 <ID>)

Wer Workshop-Mods manuell ziehen will, funktioniert das ebenfalls mit +login anonymous (Community-Quelle: SteamCMD-Doku).

Discord-Webhook — URL ist ein Secret

  • Webhook-URLs haben die Form https://discord.com/api/webhooks/<id>/<token> — das <token> ist der eigentliche Secret-Teil (Community-Quelle: Discord-Doku).
  • DISCORD_WEBHOOK_URL steht in .env, niemals ins Repo oder in Skript-Logs schreiben. Das notify() aus scripts/lib/common.sh sendet per curl an genau diese URL.
  • Wenn das Token einmal geleakt wurde: im Discord-Server den alten Webhook löschen und einen neuen anlegen, dann .env aktualisieren (Heuristik, Standard-Verfahren).

Audit-Checkliste Security (5 Prüfpunkte)

Quelle: pdf/content.json, Audit-Tabelle C — Security (1:1 übernommene Prüfpunkte). Spalte „Befehl zum Prüfen" aus dem Repo-Pfad/Standard-Linux abgeleitet.

# Prüfpunkt Befehl zum Prüfen Erwartetes Ergebnis
C1 Läuft als Nicht-Root ps -o user= -p $(pgrep -fi zomboid) Prozess gehört pzserver, niemals root (Repo-Default aus .env.example: PZ_USER=pzserver)
C2 UFW aktiv und minimal sudo ufw status verbose Aktiv; erlaubt nur 16261/tcp+udp, 16262/udp und OpenSSH (Community-Quelle: PDF-Handbuch)
C3 SSH gehärtet ssh -o PreferredAuthentications=publickey user@host Login mit Key möglich; Passwort-Login deaktiviert (allgemeine Best Practice, nicht PZ-dokumentiert)
C4 System-Updates stehen apt list --upgradable Keine relevanten offenen Sicherheitsupdates
C5 Backups vorhanden + getestet Restore-Protokoll der letzten Sicherung Backup existiert und wurde mindestens einmal testweise zurückgesichert (Heuristik)

Die Original-Reihenfolge und der genaue Wortlaut stehen in pdf/content.json unter „Audit C — Security" (1:1 übernommen).

Stolperfallen

  • Webhook-URL in Skript-Logs: Wenn notify() per curl -v oder in einem Debug-Wrapper aufgerufen wird, kann die URL im Log landen. Skripte im Repo rufen notify() ohne -v auf (1:1 aus lib/common.sh), trotzdem: Logs nicht öffentlich posten (Heuristik).
  • .env in Docker-Mounts kopiert: Wenn docker/.env oder die Repo- .env in ein Container-Mount wandert, das später committed wird, ist das Secret geleakt. Repo-Default: docker/.env liegt in .gitignore (siehe docker/.env.docker.example als Vorlage).
  • RCON-Port versehentlich geöffnet: Wenn beim UFW-Setup allow 27015 statt from <eigene-ip> läuft, ist RCON weltweit offen. Symptom: in journalctl -u zomboid tauchen fremde mcrcon-Verbindungsversuche auf (Heuristik).
  • PASSWORD=CHANGE_ME belassen: Fünf Variablen tragen den CHANGE_ME-Platzhalter — ADMIN_PASSWORD, SERVER_PASSWORD und RCON_PASSWORD für den laufenden Betrieb sowie GCP_PROJECT und GCP_BUCKET (nur für den Failover-Betrieb relevant; die drei ersteren sind die vom Repo aktiv verwalteten Geheimnisse, siehe oben). Wer den Platzhalter produktiv lässt, hat faktisch kein Passwort. CI-Scan grep't die Platzhalter explizit nicht (siehe oben).
  • make render vergessen: Änderungen in .env landen erst nach make render in den ini-Dateien. Wer den Schritt überspringt, wundert sich, warum ADMIN_PASSWORD weiter leer ist (Heuristik).
  • Backups nie getestet: Ein Backup, das nie zurückgespielt wurde, ist kein Backup. audit-checkliste C5 verlangt explizit einen Restore-Test (1:1 aus pdf/content.json).
  • Repo-Verhalten update.sh setzt KEIN .planned-stop: Während des Update-Fensters kann der minütliche Cron-Watchdog den Server mitten im Stop wieder hochziehen. Heuristik-Empfehlung: vor manuell angestoßenen Updates touch .planned-stop setzen (siehe Monitoring-und-Alerts).

Detail: .env richtig vorbereiten

Konkrete Schrittfolge nach git clone (Heuristik, abgeleitet aus Repo- Defaults und Standard-Praxis):

cp .env.example .env
chmod 600 .env
$EDITOR .env        # mindestens die vier CHANGE_ME-Platzhalter setzen
make render         # envsubst -> servertest.ini + Sandbox-Lua
make validate       # sanity-check (shellcheck wenn vorhanden)

Danach nie wieder .env committen; .gitignore deckt die Datei bereits ab (1:1 im Repo).

Detail: Logs als Secret-Quelle behandeln

Das Repo-Setup erzeugt an mehreren Stellen Logs, in denen theoretisch Secrets landen könnten:

Log Quelle Risiko (Heuristik)
${REPO_ROOT}/logs/server.log Skript-Logging aus lib/common.sh Skripte loggen Passwörter nicht aktiv; Risiko gering
sudo journalctl -u zomboid Java-Prozess-Logs Bei Fehlern können Stacktraces Konfig-Werte ausgeben
Discord-Notification notify() via curl Wenn Webhook-URL als Argument übergeben wird, kann sie im Process-Listing erscheinen
Backup-Filenames backup.sh enthält Label + Timestamp Keine Secrets; aber Backup-Pfade verraten Verzeichnisstruktur

Standard-Empfehlung (allgemeine Best Practice, nicht PZ-dokumentiert): Log-Dateien mit chmod 640 oder restriktiver, kein world-readable. Bei zentraler Log-Sammlung (Graylog, Loki etc.) Filter für bekannte Secret-Muster konfigurieren.

Detail: Update-Pfad mit mcrcon-Schwachstelle

scripts/announce.sh baut die RCON-Verbindung mit mcrcon -p auf und schluckt Fehler per || true (1:1 aus dem Skript). Drei Punkte:

  1. Wenn mcrcon aus einer alten Version stammt, kann das Passwort im Klartext über die Leitung gehen — bei 127.0.0.1 und SSH-Tunnel kein Problem, aber nie ohne Tunnel nach außen (Heuristik).
  2. mcrcon cached das Passwort nicht; jeder Aufruf liest es neu aus der .env. Nach RCON_PASSWORD-Rotation: kein Cache-Leeren nötig (Heuristik, abgeleitet aus dem Repo-Aufrufmuster).
  3. Die mcrcon-Binary sollte aus derselben Distribution wie der Server kommen (Heuristik). Versions-Drift zwischen Client- und Server-Binary ist in der Praxis unkritisch, aber dokumentiert.

Detail: Backup-Verschlüsselung

Das Repo-Backup ist ein einfaches tar.gz ohne Verschlüsselung (1:1 aus scripts/backup.sh):

tar -czf "$DEST" -C "$PZ_DATA_DIR" Saves Server

Wer das Backup auf einen Cloud-Bucket spiegelt (make failover-syncgcp/sync-backups.sh), transportiert damit Klartext-Saves in einen externen Bucket. Heuristik-Empfehlungen:

Maßnahme Zweck
Bucket-ACL auf pzserver-Service-Account beschränken Nur eigene Maschinen sehen die Backups
Object-Lock / Retention-Policy im Bucket Versehentliches Löschen verhindern
Separater Backup-Bucket für Tests Test-Restores nicht im produktiven Bucket
Backup vor gsutil rsync lokal verschlüsseln (z. B. gpg) Bei kompromittiertem Bucket bleibt Save verschlüsselt

Diese Maßnahmen sind nicht im Repo implementiert; sie sind generische Cloud-Backup-Praxis (Heuristik).

Detail: Nicht-Root-Prinzip im Repo

systemd/zomboid.service läuft mit User=__PZ_USER__ (1:1 aus dem Service-File). Das Templating füllt __PZ_USER__ aus .env (PZ_USER, Repo-Default pzserver). Folgerungen:

  • Der Java-Prozess hat keine Root-Rechte (Repo-Default).
  • Installation und Updates laufen weiterhin als root via sudo make install / sudo make update (Repo-Default, Makefile 1:1: alle systemctl-Targets sind sudo-Aufrufe).
  • Watchdog (monitor.sh) und Cron laufen typischerweise als root oder als Service-User mit sudo-fähigkeit — siehe systemd/README.md.

Wer den Server versehentlich als root startet, fällt durch Audit C1 (siehe oben) sofort auf.

Checkliste zum Mitnehmen

Kurzfassung zum Abhaken bei Erstinstallation (Heuristik, abgeleitet aus Repo-Defaults und Audit C):

# Punkt Status
1 .env aus .env.example kopiert, Rechte 600, alle CHANGE_ME-Platzhalter gesetzt [ ]
2 DISCORD_WEBHOOK_URL gesetzt oder bewusst leer gelassen [ ]
3 UFW aktiv, Regeln wie oben, RCON-Port nicht öffentlich [ ]
4 SSH-Passwort-Login deaktiviert (allgemeine Best Practice) [ ]
5 Server läuft als pzserver, nicht als root [ ]
6 Ein Test-Restore wurde durchgeführt und im Protokoll dokumentiert [ ]
7 .gitignore deckt .env und docker/.env ab (Repo-Default) [ ]
8 Logs laufen nicht world-readable (Heuristik) [ ]
9 CI-Secret-Scan ist im grünen Bereich nach git push (1:1 aus .github/workflows/ci.yml) [ ]

Diese Liste ist Selbstzweck — die formale Audit-Tabelle mit Server- Antworten steht oben (Audit C, 5 Prüfpunkte).

Hinweis zur Audit-Wiederholung

Heuristik (dieses Wikis): Das Audit nach jedem größeren Eingriff wiederholen — neuem Mod, Heap-Änderung, Storage-Umzug oder Branch-Update. Ein einmal bestandenes Audit veraltet; die Backup-Prüfung (Audit C5) gehört in jeden Auditlauf.

Konsistent zum Original aus pdf/content.json (Audit-C-Abschluss).

Siehe auch

Weiter: ENV-Variablen · Ports-und-Netzwerk · Wartung-und-Automatik · Konventionen-und-Quellen · Cheatsheet-Tagesbetrieb

Clone this wiki locally