---
id: 2026-08-26-linux-server-haertung-fido2-crowdsec
title: "Linux Server Härtung: SSH mit FIDO2/YubiKey & CrowdSec"
slug: "linux-server-haertung-fido2-crowdsec"
date: "2026-08-26 12:00:00"
updated: "2026-08-26 12:00:00"
author:
  name: "Sebastian Palencsár"
  handle: "sebastian"
category: "serverumgebungen"
tags: ["linux", "security", "ssh", "fido2", "yubikey", "crowdsec", "haertung"]
reading_time: 40
excerpt: "Linux-Server-Härtung mit SSH FIDO2/YubiKey für passwortlose Authentifizierung und CrowdSec als kollaboratives Intrusion-Prevention-System auf Ubuntu 24.04/26.04."
toc: true
---

SSH-Brute-Force-Attacken laufen auf jedem öffentlichen Server im Dauerbetrieb. Der Großteil der Linux-Server setzt standardmäßig auf Passwörter oder statische SSH-Keys – beides hat bekannte Schwächen. Passwörter sind durch Credential-Stuffing und GPU-Cluster in Minuten geknackt, statische Keys bleiben unverändert, bis jemand sie manuell rotiert.

Dieser Artikel zeigt, wie du auf **Ubuntu 24.04 LTS** und **Ubuntu 26.04 LTS** deinen Server mit zwei konkreten Maßnahmen absicherst: **SSH mit FIDO2 und YubiKey** für hardwarebasierte, passwortlose Authentifizierung und **CrowdSec** als kollaboratives Intrusion-Prevention-System. Beides zusammen schließt die häufigsten Angriffsvektoren, ohne dass du dich um Passwort-Rotation oder Key-Management kümmern musst.

Die Konfiguration funktioniert auf einem frischen Server genauso wie auf einem bestehenden Setup – wir machen keine Annahmen über den aktuellen Zustand.

<blockquote class="infobox infobox--info">
💡 **Voraussetzungen:** Grundkenntnisse in `SSH` und `systemd`. Ein Ubuntu 24.04 oder 26.04 Server mit Root- oder sudo-Zugang. Für FIDO2 benötigst du einen YubiKey (oder ein anderes FIDO2-Gerät). Für [CrowdSec](https://www.crowdsec.net/){.badge-link-text} reicht ein TCP-Port für SSH. Der Artikel ist für Linux-Administratoren, DevOps Engineers und Homelab-Betreiber, die bestehende Server absichern oder neue nach aktuellem Standard aufsetzen möchten.
</blockquote>

<blockquote class="infobox infobox--warn">
⚠️ **Hinweis:** Dieser Artikel behandelt Serverhärtung auf Betriebssystem-Ebene. Anwendungsspezifische Härtung (Webserver, Datenbanken) bleibt außen vor, da jede Anwendung eigene Anforderungen hat.
</blockquote>

## SSH mit FIDO2 & YubiKey

### Warum FIDO2 für SSH?

Passwörter sind das schwächste Glied in der SSH-Kette. Jeder Server mit [öffentlicher IP](/de/netzwerk/oeffentliche-vs-private-ip-adresse-die-hauptunterschiede){.badge-link-text} wird permanent von Brute-Force-Scannern bearbeitet – `fail2ban`-Logs zeigen das täglich. Statische SSH-Keys (Ed25519, RSA) sind besser, aber sie rotieren nicht. Wer einen kompromittierten Private Key hat, bleibt unbemerkt aktuell.

<span class="nb-accent">FIDO2 löst beide Probleme:</span>

Der Private Key wird auf einem physischen Gerät gespeichert (YubiKey) und verlässt dieses nie. Ohne den YubiKey am lokalen Rechner ist kein Login möglich – egal wie gut Passwort oder Key sind. Zusätzlich erzwingt FIDO2 eine Benutzer-Verifikation (PIN oder Biometrie) bei jedem Login.

**Was sich ändert:**

| Eigenschaft | Passwort | Statischer SSH-Key | FIDO2 + YubiKey |
|-------------|----------|-------------------|-----------------|
| Physische Komponente | Nein | Nein | Ja |
| Brute-Force-resistent | Bedingt | Ja | Ja |
| Phishing-resistent | Nein | Bedingt | Ja |
| Rotation | Manuell | Manuell | Automatisch (Key auf Gerät) |
| Multi-Factor | Optional | Nein | Standard |

Die Kombination aus physischem Token und PIN macht SSH-Logins nahezu unangreifbar. Ein Angreifer bräuchte sowohl den YubiKey als auch den PIN – und beides an derselben physischen Station.

<blockquote class="infobox infobox--info">
💡 **FIDO2 vs. U2F:** FIDO2 ist der Nachfolger von U2F und unterstützt WebAuthn (Browser) und CTAP2 (native Apps). Für SSH ist CTAP2 relevant – der YubiKey wird direkt vom SSH-Client angesprochen, ohne Browser-Umweg.
</blockquote>

### FIDO2 & WebAuthn: Wie es funktioniert

FIDO2 arbeitet Challenge-Response-basiert. Der Server sendet eine zufällige Challenge, der YubiKey signiert sie mit einem auf dem Gerät gespeicherten privaten Schlüssel, und der Server verifiziert die Signatur. Der private Schlüssel verlässt nie den YubiKey.

```markdown
┌─────────────────────────────────────────────────────────────┐
│                  FIDO2 Challenge-Response                   │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   ┌──────────────┐       ┌──────────┐      ┌──────────────┐ │
│   │  SSH-Client  │       │  YubiKey │      │  SSH-Server  │ │
│   └──────┬───────┘       └────┬─────┘      └──────┬───────┘ │
│          │                    │                   │         │
│          │ Challenge (random) │                   │         │
│          │ ──────────────────>│                   │         │
│          │                    │  PIN / Touch      │         │
│          │                    │ ────────── (User) │         │
│          │                    │                   │         │
│          │ Signature(Chall.)  │                   │         │
│          │ <───────────────── │                   │         │
│          │                    │                   │         │
│          │ Challenge + Signat.│                   │         │
│          │ ──────────────────────────────────────>│         │
│          │                    │                   │         │
│          │                    │ Verify(PubKey,Sig)│         │
│          │                    │ ───────────────── │         │
│          │                    │                   │         │
│          │ Access Granted     │                   │         │
│          │ <──────────────────────────────────────│         │
│                                                             │
└─────────────────────────────────────────────────────────────┘
```

**Was passiert im Detail:**

1. Der SSH-Client erkennt, dass ein FIDO2-Key konfiguriert ist
2. Er fordert den YubiKey zur Signierung der Challenge auf
3. Der Nutzer gibt seinen PIN ein oder berührt den Sensor
4. Der YubiKey signiert die Challenge mit dem ed25519-sk Schlüsselpaar
5. Der Server verifiziert die Signatur gegen den gespeicherten öffentlichen Schlüssel
6. Der Login wird gewährt

Der entscheidende Unterschied zu statischen Keys: Die Challenge ist bei jedem Login-Versuch anders. Selbst wenn jemand die Signatur abfängt, kann er sie nicht wiederverwenden (Replay-Schutz).

### YubiKey Hardware-Setup

Für SSH mit FIDO2 eignen sich alle YubiKeys der 5er-Serie mit USB-C oder USB-A. Wichtig ist, dass der Key **FIDO2** und nicht nur U2F unterstützt – das erkennst du an der Produktbezeichnung "YubiKey 5" (nicht "YubiKey Security Key", der nur U2F kann).

**YubiKey initialisieren:**

```bash
# YubiKey Manager installieren (Debian/Ubuntu)
sudo apt install yubikey-manager -y

# FIDO2-PIN setzen (Standard-PIN ist leer)
ykman fido access change-pin
```

Der PIN schützt den YubiKey vor unbefugter Nutzung. Wähl einen PIN mit mindestens 6 Zeichen. Ohne PIN kann jeder mit physischem Zugriff zum YubiKey die FIDO2-Keys auslesen und nutzen.

**Verifikation:**

```bash
# Prüfe ob der YubiKey erkannt wird
ykman fido info
```

Erwartete Ausgabe:

```bash
Applications: FIDO2
    slots: 2
     pin: true
     pin retries: 3
```

<blockquote class="infobox infobox--warn">
⚠️ **Backup-YubiKey & Break-Glass-Notfallanker:** Erstelle immer mindestens einen zweiten YubiKey als physisches Backup an einem sicheren Ort. Wenn du deinen einzigen YubiKey verlierst, bist du ausgesperrt – es gibt keinen Reset-Mechanismus für FIDO2-Keys. Für Root- und Cloud-Server empfiehlt sich zudem die Absicherung der Out-of-Band-Webkonsole des Hosters (z. B. Hetzner Robot, Proxmox NoVNC oder IPMI) mit separatem 2FA als Notfallanker.
</blockquote>

### SSH-Schlüssel mit FIDO2 erzeugen

OpenSSH 8.2+ unterstützt FIDO2-Schlüssel nativ. Der Befehl `ssh-keygen` erzeugt mit der Option `-t ed25519-sk` einen Schlüssel, der auf dem YubiKey gespeichert wird.

**Schlüssel erzeugen:**

```bash
# FIDO2-SSH-Schlüssel erzeugen
ssh-keygen -t ed25519-sk -C "admin@server"
```

Während der Erzeugung wirst du aufgefordert, den YubiKey zu berühren (Touch) und deinen FIDO2-PIN einzugeben. Der private Schlüssel wird auf dem YubiKey gespeichert, nicht auf der Festplatte.

**Was passiert bei der Erzeugung:**

1. `ssh-keygen` erzeugt ein ed25519-sk Schlüsselpaar
2. Der private Schlüssel wird im Secure Element des YubiKeys abgelegt
3. Der öffentliche Schlüssel wird in `~/.ssh/id_ed25519-sk.pub` gespeichert
4. Für jeden Login-Versuch wird der YubiKey benötigt

**Optionen:**

```bash
# Ohne Touch (nur PIN)
ssh-keygen -t ed25519-sk -O no-touch-required

# Mit benutzerdefiniertem Dateinamen
ssh-keygen -t ed25519-sk -f ~/.ssh/id_yubikey -C "admin@server"
```

<blockquote class="infobox infobox--info">
💡 **Resident vs. Non-Resident Keys:** Mit `-O resident` wird der private Schlüssel vollständig auf dem YubiKey gespeichert und kann von dort exportiert werden. Ohne diese Option wird nur ein Verweis gespeichert. Für SSH empfiehlt sich Non-Resident (Standard), da der Schlüssel dann nicht exportierbar ist.
</blockquote>

**Öffentlichen Schlüssel auf den Server kopieren:**

```bash
# Öffentlichen Schlüssel auf den Server übertragen
ssh-copy-id -i ~/.ssh/id_ed25519-sk.pub user@server
```

Falls `ssh-copy-id` mit FIDO2 Probleme macht (manchmal der Fall bei älteren OpenSSH-Versionen), kopiere den Schlüssel manuell:

```bash
# Manuelle Installation
cat ~/.ssh/id_ed25519-sk.pub | ssh user@server "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys"
```

**Überprüfung auf dem Server:**

```bash
# Prüfe den installierten Schlüssel
cat ~/.ssh/authorized_keys | grep "sk@"
```

Die Zeile sollte mit `ssh-ed25519-cert-v01@openssh.com` beginnen oder `sk@` enthalten – das bestätigt, dass es sich um einen FIDO2-Schlüssel handelt.

### SSH-Server für FIDO2 konfigurieren

Der SSH-Server muss wissen, dass er FIDO2-Schlüssel akzeptiert. In der Standardkonfiguration ist das bereits der Fall, aber wir wollen explizit sein und zusätzliche Sicherheitseinstellungen setzen.

**Server-Konfiguration:**

```bash
sudo nano /etc/ssh/sshd_config
```

**Relevante Einstellungen:**

```ini
# FIDO2-Schlüssel erlauben (Standard: yes)
PubkeyAuthentication yes

# Nur SSH-Keys erlauben (Passwörter deaktivieren)
PasswordAuthentication no

# Root-Login verbieten
PermitRootLogin no

# Maximal 3 Authentifizierungsversuche
MaxAuthTries 3

# Grace Time auf 30 Sekunden begrenzen
LoginGraceTime 30

# Information Disclosure minimieren
DebianBanner no
Banner none

# Keepalive für Long-Running Sessions
ClientAliveInterval 300
ClientAliveCountMax 2
```

**Optional: Nur FIDO2-Keys erlauben (keine statischen Keys mehr):**

Wenn du ausschließlich FIDO2-Key verwenden möchtest, kannst du die `PubkeyAuthOptions` einschränken:

```bash
# Nur verifizierte FIDO2-Keys akzeptieren
PubkeyAuthOptions verify-required
```

<blockquote class="infobox infobox--warn">
⚠️ **Vorsicht mit `verify-required`:** Diese Option akzeptiert nur FIDO2-Keys mit Benutzer-Verifikation (PIN oder Biometrie). Statische Ed25519- und RSA-Keys werden abgelehnt. Teste die Änderung zuerst in einer zusätzlichen Session, bevor du die bestehende schließt.
</blockquote>

**SSH-Server neu starten:**

```bash
sudo systemctl restart sshd
```

<blockquote class="infobox infobox--warn">
⚠️ **Nie die aktive Session schließen, bevor du getestet hast!** Öffne ein zweites Terminal und teste den Login mit dem neuen FIDO2-Key. Erst wenn der Login funktioniert, schließt du die alte Session.
</blockquote>

**Testen:**

```bash
# Login mit FIDO2-Key
ssh -i ~/.ssh/id_ed25519-sk user@server

# Prüfe, welche Methode verwendet wurde
ssh -v user@server 2>&1 | grep "Offering"
```

Die Verbose-Ausgabe sollte `Offering public key: .../id_ed25519-sk` zeigen und `Accepted publickey` als Ergebnis.

### Zertifikatsbasierte Authentifizierung

Für größere Umgebungen lohnt sich eine SSH Certificate Authority (CA). Statt einzelne Keys auf jedem Server zu verteilen, signiert eine CA öffentliche Schlüssel und der Server vertraut nur der CA.

**CA auf dem signierenden Host erstellen:**

```bash
# CA-Schlüsselpaar erzeugen
sudo mkdir -p /etc/ssh/ca
sudo ssh-keygen -t ed25519 -f /etc/ssh/ca/server-ca -C "SSH CA"
```

**Öffentlichen CA-Schlüssel auf allen Servern verteilen:**

```bash
# Auf jedem Server: CA Public Key als TrustedKey eintragen
echo "cert-authority $(cat /etc/ssh/ca/server-ca.pub)" >> /etc/ssh/authorized_keys.d/trusted-ca
```

**Benutzer-Key mit CA signieren:**

```bash
# FIDO2-Key signieren (erzeugt ein Zertifikat)
sudo ssh-keygen -s /etc/ssh/ca/server-ca \
    -I "admin-fido2" \
    -n admin \
    -V +52w \
    ~/.ssh/id_ed25519-sk.pub
```

**Optionen:**

| Option | Bedeutung |
|--------|----------|
| `-s` | CA-Private Key zum Signieren |
| `-I` | Zertifikat-ID (für Logs) |
| `-n` | Erlaubte Usernames (kommagetrennt) |
| `-V` | Gültigkeitsdauer (`+52w` = 52 Wochen) |

Das erzeugte Zertifikat (`id_ed25519-sk-cert.pub`) wird vom SSH-Client automatisch erkannt und beim Login mitgeschickt.

<blockquote class="infobox infobox--info">
💡 **Vorteil der CA:** Du musst auf jedem Server nur den öffentlichen CA-Schlüssel hinterlegen. Individuelle Benutzer-Keys werden zentral verwaltet und können über die CA widerrufen werden, ohne dass du auf jedem Server die `authorized_keys` editierst.
</blockquote>

### SSH-Config & ssh-agent

Für den täglichen Gebrauch lohnt sich eine saubere SSH-Konfiguration auf dem Client.

**`~/.ssh/config`:**

```ini
# Server-Definition mit FIDO2-Key
Host server
    HostName 192.168.1.100
    User admin
    IdentityFile ~/.ssh/id_ed25519-sk
    IdentitiesOnly yes

# Jump-Host für interne Server
Host internal-server
    HostName 10.0.0.50
    User admin
    ProxyJump server
    IdentityFile ~/.ssh/id_ed25519-sk
    IdentitiesOnly yes
```

**`IdentitiesOnly yes`** zwingt den SSH-Client, nur den angegebenen Key zu verwenden. Das verhindert, dass versehentlich ein anderer Key aus dem Agent angeboten wird.

**ssh-agent mit FIDO2:**

Der `ssh-agent` kann FIDO2-Keys unterstützen, aber da der private Schlüssel auf dem YubiKey bleibt, wird nur der Verweis im Agent gespeichert. Für FIDO2 macht der Agent wenig Sinn – der YubiKey muss sowieso physisch angeschlossen sein.

```bash
# ssh-agent starten (optional für FIDO2)
eval $(ssh-agent -s)

# Key hinzufügen (Touch wird bei jedem Login angefordert)
ssh-add ~/.ssh/id_ed25519-sk
```

<blockquote class="infobox infobox--tip">
💡 **Empfehlung:** Für FIDO2-Keys den ssh-agent weglassen. Der Agent bringt keinen Vorteil, da der YubiKey bei jedem Login geberührt werden muss. Stattdessen die `IdentityFile` in `~/.ssh/config` nutzen.
</blockquote>

### Troubleshooting: Häufige SSH-FIDO2-Probleme

**Problem: "Key rejected by server"**

```bash
# Prüfe ob FIDO2 im SSH-Server unterstützt wird
ssh -V  # OpenSSH_8.9 oder höher

# Prüfe die Server-Konfiguration
sudo sshd -T | grep pubkeyauth
```

<blockquote class="infobox infobox--warn">
⚠️ **Ursache: OpenSSH-Version zu alt (unter 8.2). FIDO2-Support kam mit OpenSSH 8.2. Ubuntu 24.04 hat OpenSSH 9.6, Ubuntu 26.04 hat OpenSSH 9.7 – beide unterstützen FIDO2.
</blockquote>

**Problem: "Sign_and_send_pubkey: signing failed: agent refused operation"**

```bash
# ssh-agent läuft und versucht den Key zu signieren
# Lösung: Key explizit angeben oder Agent deaktivieren
ssh -i ~/.ssh/id_ed25519-sk user@server
```

Ursache: Der ssh-agent versucht, den Key mit einer alten Methode zu signieren. `IdentitiesOnly yes` in der SSH-Config löst das Problem.

**Problem: "Touch your authenticator" erscheint nicht**

```bash
# Prüfe ob der YubiKey erkannt wird
lsusb | grep Yubico

# Prüfe ob das FIDO2-Modul geladen ist
ls /dev/hidraw*
```

Ursache: YubiKey nicht erkannt oder Treiber fehlen. Unter Ubuntu sind alle nötigen Treiber standardmäßig vorhanden.

**Problem: "PIN auth required" aber PIN wird nicht abgefragt**

```bash
# FIDO2-PIN manuell setzen oder zurücksetzen
ykman fido access change-pin
```

<blockquote class="infobox infobox--warn">
⚠️ **Ursache:** Der YubiKey wurde ohne PIN initialisiert oder der PIN ist veraltet.
</blockquote>

**Verifikation auf dem Server:**

```bash
# Logs prüfen
journalctl -u sshd -f

# Erfolgreiche FIDO2-Logins erkennen
journalctl -u sshd | grep "Accepted publickey"
```

Die Logs zeigen, welcher Key verwendet wurde und ob es ein FIDO2-Key (`sk@`) war.

## CrowdSec: Kollaborativer Intrusion-Prevention

### Fail2Ban vs. CrowdSec

Fail2Ban liest lokalen Log-Dateien und bannt IPs über iptables/nftables-Regeln. Jeder Server agiert isoliert – ein Angreifer, der von 1.000 IPs scannt, muss auf jedem Server einzeln gebannt werden.

CrowdSec geht anders vor: Der Agent analysiert Logs und Entscheidungen (Decisions) werden zentral in einer Community-Datenbank geteilt. Wenn ein Angreifer auf einem Community-Server gebannt wird, profitieren alle anderen davon. Zusätzlich trennt CrowdSec die Analyse (Agent) von der Durchsetzung (Bouncer), was die Architektur flexibler macht.

**Was sich ändert:**

| Eigenschaft | Fail2Ban | CrowdSec |
|-------------|----------|----------|
| Log-Analyse | Lokal | Lokal + Community |
| Durchsetzung | iptables direkt | Bouncer (extern) |
| Performance | Guter bei wenigen Jails | Skalierbar für viele Server |
| Community | Keine | Geteilte Decisions |
| Dashboard | Kein | Web-Dashboard + Metriken |
| Architektur | Monolith | Agent + Bouncer getrennt |

<blockquote class="infobox infobox--info">
💡 **Kernidee:** CrowdSec sammelt Signaturen von Angriffsmustern (Scenarios) und vergleicht sie mit deinen Logs. Treffer führen zu Decisions (Bans), die von Bouncers durchgesetzt werden. Die Community-Intelligence ergänzt lokale Erkennung.
</blockquote>

### Installation & Ersteinrichtung

**CrowdSec Agent installieren:**

```bash
# Repository hinzufügen (Debian/Ubuntu)
curl -s https://packagecloud.io/install/repositories/crowdsec/crowdsec/script.deb.sh | sudo bash

# CrowdSec installieren
sudo apt install crowdsec -y
```

**Firewall-Bouncer installieren:**

```bash
# Bouncer für nftables/iptables
sudo apt install crowdsec-firewall-bouncer-nftables -y
```

<blockquote class="infobox infobox--warn">
⚠️ **Reihenfolge beachten:** Zuerst den Agent installieren, dann den Bouncer. Der Bouncer verbindet sich mit dem Agenten über die lokale API. Starte den Bouncer nicht, bevor der Agent läuft.
</blockquote>

**Status prüfen:**

```bash
sudo systemctl status crowdsec
sudo systemctl status crowdsec-firewall-bouncer
```

Erwartete Ausgabe für den Agenten:

```bash
● crowdsec.service - CrowdSec SIEM
     Loaded: loaded (/lib/systemd/system/crowdsec.service; enabled)
     Active: active (running) since Mon 2026-08-26 12:00:00 UTC
```

**Erste Decisions prüfen:**

```bash
sudo cscli decisions list
```

Im Anfangszustand ist die Liste leer. Sobald der Agent Logs analysiert und Angriffsmuster erkennt, erscheinen hier Decisions.

### Szenarien & Entscheidungen

CrowdSec arbeitet mit Scenarios – das sind Signaturen, die bestimmte Angriffsmuster beschreiben. Der Agent vergleicht Log-Einträge mit diesen Scenarios und erstellt Decisions bei Treffern.

**Standardmäßig aktivierte Scenarios:**

```bash
sudo cscli scenarios list
```

| Name | Service | Type |
|------|---------|------|
| `ssh-bf` | ssh | leak |
| `ssh-slow-bf` | ssh | leak |
| `http-bad-user-agent` | http | leak |
| `http-dos-bf-path` | http | leak |

**Für SSH relevante Scenarios:**

| Scenario | Erkennung | Aktion |
|----------|-----------|--------|
| `ssh-bf` | 5 fehlgeschlagene Logins in 10 Min | Ban für 1h |
| `ssh-slow-bf` | Langsamer Brute-Force über Stunden | Ban für 24h |
| `ssh-crawl` | Verhaltensanalyse von Login-Versuchen | Ban für 4h |

**Eigene Scenarios erstellen:**

Falls die Standard-Scenarios nicht passen, kannst du eigene definieren:

```bash
sudo nano /etc/crowdsec/scenarios/custom-ssh-aggressive.yaml
```

```yaml
type: leak
name: ssh-aggressive-scan
description: "Aggressive SSH-Scans mit vielen fehlgeschlagenen Logins"
labels:
  service: ssh
  remediation: true
filter:
  LABELS.service == "ssh" && MSG.ContainerName == "sshd"
groupby: LABELS.source_ip
capacity: 5
leakspeed: 1m
blackhole: 1m
certainty: 75
for: 10m
```

**Verifikation:**

```bash
# Scenarios neu laden
sudo cscli scenarios list | grep custom

# Testszenario simulieren
sudo cscli scenarios install --local /etc/crowdsec/scenarios/custom-ssh-aggressive.yaml
```

### Bouncers (Firewall-Integration)

Bouncers setzen die Decisions des Agents durch. Es gibt verschiedene Bouncer-Typen für unterschiedliche Anwendungsfälle.

**nftables-Bouncer (Standard):**

```bash
# Prüfe ob der Bouncer aktiv ist
sudo cscli bouncers list
```

```bash
Name       │ IP Address │ Valid Until │ Type
───────────┼────────────┼─────────────┼──────
firewall   │ 127.0.0.1  │ 2027-01-01  │ nftables
```

**Bouncer-Typen:**

| Bouncer | Einsatz | Vorteil |
|---------|---------|---------|
| `nftables` | Server-Firewall | Schnell, kernel-nah |
| `nginx` | Webserver-Inline | Kein extra Port |
| `caddy` | Caddy-Integration | Einfache Konfiguration |
| `crowdsec-openapi` | Externe APIs | Flexibel |

**Firewall-Regeln prüfen:**

```bash
# CrowdSec-Chain in nftables prüfen
sudo nft list chain inet crowdsec-crowdsec-block
```

```bash
table inet crowdsec-crowdsec {
    chain crowdsec-block {
        type filter hook input priority filter; policy accept;
        ip saddr @crowdsec-blacklist-jump drop
        ip6 saddr @crowdsec-blacklist6-jump drop
    }
}
```

### Whitelisting

Nicht jede IP soll gebannt werden. Lokale Server, Monitoring-Systeme oder vertrauenswürdige Netzwerke brauchen Whitelisting.

**Whitelist-Datei:**

```bash
sudo nano /etc/crowdsec/parsers/s02-enrich/whitelist.yaml
```

```yaml
filter: "Meta.IP != ''"
whitelist:
  reason: "Lokale Adresse"
  expression: "Ip('127.0.0.1') || Ip('::1')"
  expression: "Ip('192.168.0.0/16')"
  expression: "Ip('10.0.0.0/8')"
```

**Alternativ: Whitelist über cscli:**

```bash
# Einzelne IP whitelishten
sudo cscli decisions add --ip 192.168.1.100 --duration 0 --type whitelist --reason "Monitoring"

# IP-Range whitelishten
sudo cscli decisions add --range 192.168.1.0/24 --duration 0 --type whitelist --reason "Lokales Netz"
```

<blockquote class="infobox infobox--warn">
⚠️ **Whitelist vor Bans prüfen:** Prüfe immer, ob eine IP möglicherweise aus einem lokalen Monitoring-System oder einem vertrauenswürdigen Netzwerk kommt, bevor du Banns analysierst. Sonst bannt CrowdSec versehentlich eigene Systeme.
</blockquote>

### Dashboards & Alerting

CrowdSec bietet ein Web-Dashboard und Metriken für Monitoring-Systeme.

**Web-Dashboard:**

```bash
# Dashboard-Zugangsdaten anzeigen
sudo cscli console status
```

Das Dashboard ist auf `http://localhost:6060` erreichbar (Standard-Port). Für externe Zugriffe die Firewall anpassen.

**Metriken für Prometheus:**

```bash
# Metriken-Endpunkt prüfen
curl -s http://localhost:6060/metrics | head -20
```

```bash
# Wichtige Metriken
crowdsec_decisions_total{source_ip="...",scenario="..."}
crowdsec_alerts_total{scenario="..."}
crowdsec_bouncers_total{type="..."}
```

**Alerting über Webhook:**

```bash
sudo nano /etc/crowdsec/notifications/email.yaml
```

```yaml
type: email
name: email_default
log_level: info
 # Email-Konfiguration
smtp_host: "smtp.example.com"
smtp_port: 587
smtp_username: "alert@example.com"
smtp_password: "DEIN_PASSWORT"
smtp_from: "crowdsec@example.com"
smtp_to:
  - "admin@example.com"
smtp_subject: "CrowdSec Alert: {scenario}"
format: |
  CrowdSec hat einen Angriff erkannt.
  Scenario: {scenario}
  Source IP: {source_ip}
  Entscheidung: {decision}
  Zeit: {created_at}
```

**Verifikation:**

```bash
# Notifications prüfen
sudo cscli notifications list

# Test-Benachrichtigung senden
sudo cscli alert inspect --id 1
```

### Performance & Skalierung

Für größere Umgebungen (mehr als 10 Server) lohnt sich eine zentrale CrowdSec-Instanz.

**Architektur für mehrere Server:**

```markdown
┌─────────────────────────────────────────────────────────────┐
│              CrowdSec Multi-Server Architektur              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   ┌─────────┐        ┌─────────┐        ┌─────────┐         │
│   │ Server1 │        │ Server2 │        │ Server3 │         │
│   │ Agent   │        │ Agent   │        │ Agent   │         │
│   └────┬────┘        └────┬────┘        └────┬────┘         │
│        │                  │                  │              │
│        ▼                  ▼                  ▼              │
│   ┌─────────────────────────────────────────────────────┐   │
│   │                Lokale API (zentral)                 │   │
│   │                http://central:8080                  │   │
│   └──────────────────────────┬──────────────────────────┘   │
│                              │                              │
│                              ▼                              │
│   ┌─────────────────────────────────────────────────────┐   │
│   │                 Community Database                  │   │
│   │                 (geteilte Decisions)                │   │
│   └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘
```

**Lokalen Agent auf zentrale API zeigen:**

```bash
sudo nano /etc/crowdsec/config.yaml
```

```yaml
api:
  client:
    insecure_skip_verify: false
  server:
    listen_uri: http://127.0.0.1:8080
```

<blockquote class="infobox infobox--info">
💡 **Performance:** Ein einzelner CrowdSec-Agent kann problemlos 10.000+ Log-Einträge pro Sekunde verarbeiten. Für die meisten Setups reicht ein zentraler Agent auf einem dedizierten Server.
</blockquote>

### Troubleshooting

**Problem: "crowdsec service not running"**

```bash
# Logs prüfen
sudo journalctl -u crowdsec -f

# Häufige Ursache: Port 8080 belegt
sudo ss -tulnp | grep 8080
```

**Problem: "Bouncer nicht verbunden"**

```bash
# Bouncer-API-Token prüfen
sudo cscli bouncers list

# Token neu generieren
sudo cscli bouncers add my-bouncer --auto
```

**Problem: Eigene IPs werden gebannt**

```bash
# Whitelist prüfen
sudo cscli decisions list --type whitelist

# Lokale IP zur Whitelist hinzufügen
sudo cscli decisions add --ip 192.168.1.100 --duration 0 --type whitelist --reason "Eigener Server"
```

**Verifikation:**

```bash
# Aktive Decisions prüfen
sudo cscli decisions list

# Logs des Agenten prüfen
sudo journalctl -u crowdsec | grep "decision"
```

## System-Härtung

### Kernel-Parameter (sysctl)

Der Linux-Kernel bietet über `sysctl` Dutzende von Parametern, die Angriffsfläche minimieren. Die Standardeinstellungen sind auf Kompatibilität optimiert, nicht auf Sicherheit.

**Datei erstellen:**

```bash
sudo nano /etc/sysctl.d/99-hardening.conf
```

**Netzwerk-Sicherheit:**

```ini
# IP-Spoofing verhindern
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1

# ICMP-Redirects deaktivieren
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv6.conf.all.accept_redirects = 0
net.ipv6.conf.default.accept_redirects = 0

# Source-Routing deaktivieren
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.default.accept_source_route = 0
net.ipv6.conf.all.accept_source_route = 0
net.ipv6.conf.default.accept_source_route = 0

# SYN-Flood-Schutz aktivieren
net.ipv4.tcp_syncookies = 1

# Log Martian-Pakete
net.ipv4.conf.all.log_martians = 1
net.ipv4.conf.default.log_martians = 1
```

**Speicher-Schutz:**

```ini
# Address Space Layout Randomization
kernel.randomize_va_space = 2

# Dmesg einschränken (nur für Root)
kernel.dmesg_restrict = 1

# Kernel-Pointer verbergen
kernel.kptr_restrict = 2

# BPF einschränken
kernel.unprivileged_bpf_disabled = 1

# User namespaces einschränken (für Container)
kernel.unprivileged_userns_clone = 0
```

**Kernel-Laden verhindern:**

```ini
# Module nicht mehr laden können (nach Boot)
kernel.modules_disabled = 1
```

<blockquote class="infobox infobox--warn">
⚠️ **`kernel.modules_disabled = 1`** verhindert das Laden neuer Kernel-Module nach dem Boot. Das kann etwa NVIDIA-Treiber oder VirtualBox-Module brechen. Nur aktivieren, wenn du sicher bist, dass alle nötigen Module beim Boot geladen werden.
</blockquote>

**Parameter laden:**

```bash
sudo sysctl -p /etc/sysctl.d/99-hardening.conf
```

**Verifikation:**

```bash
# Prüfe ob Parameter gesetzt sind
sysctl net.ipv4.conf.all.rp_filter
sysctl kernel.randomize_va_space
```

**Erwartete Ausgabe:**

```bash
net.ipv4.conf.all.rp_filter = 1
kernel.randomize_va_space = 2
```

### Filesystem-Hardening

Standardmäßig haben viele Dateien und Verzeichnisse zu weite Berechtigungen. Besonders `/tmp`, `/var/tmp` und `/dev/shm` sind Angriffsflächen.

**Mount-Optionen für `/tmp`:**

```bash
# /tmp ist oft bereits ein tmpfs, prüfe mit:
mount | grep tmp
```

Falls `/tmp` auf der Hauptfestplatte liegt, empfiehlt sich eine dedizierte Partition mit sicheren Mount-Optionen:

```bash
# In /etc/fstab eintragen (bei separater /tmp-Partition)
# UUID=... /tmp ext4 defaults,noexec,nosuid,nodev 0 2
```

**Verzeichnisrechte strikt setzen:**

```bash
# Kritische Verzeichnisse absichern
sudo chmod 700 /root
sudo chmod 600 /etc/shadow
sudo chmod 600 /etc/gshadow
sudo chmod 644 /etc/passwd
sudo chmod 644 /etc/group

# SSH-Verzeichnis
sudo chmod 700 /root/.ssh
sudo chmod 600 /root/.ssh/authorized_keys
```

**SUID/SGID-Bits reduzieren:**

```bash
# SUID-Bits auf Standard-Tools prüfen
find / -perm -4000 -type f 2>/dev/null
```

**Unnötige SUID-Bits entfernen (Beispiele):**

```bash
# Nur wenn die Programme nicht als SUID benötigt werden
sudo chmod u-s /usr/bin/newgrp
sudo chmod u-s /usr/bin/chsh
sudo chmod u-s /usr/bin/chfn
```

<blockquote class="infobox infobox--tip">
💡 **Tipp:** `find / -perm -4000 -type f` zeigt alle Programme mit SUID-Bit. Nicht blind entfernen – manche Programme brauchen das Bit für ihre Funktion (z. B. `sudo`, `passwd`).
</blockquote>

### Service-Minimierung

Jeder laufende Dienst ist eine potenzielle Angriffsfläche. Auf einem frischen Ubuntu-Server laufen Dutzende Services, die du möglicherweise nicht brauchst.

**Aktive Services prüfen:**

```bash
# Alle laufenden Services anzeigen
systemctl list-units --type=service --state=running

# Häufig unnötige Services auf einem Server
systemctl list-units --type=service --state=running | grep -E "cups|bluetooth|avahi|modem"
```

**Unnötige Services deaktivieren:**

```bash
# Drucker-Service (auf einem Server nicht benötigt)
sudo systemctl stop cups
sudo systemctl disable cups
sudo systemctl mask cups

# Bluetooth (auf einem Server nicht benötigt)
sudo systemctl stop bluetooth
sudo systemctl disable bluetooth
sudo systemctl mask bluetooth

# mDNS/Avahi (Service Discovery – auf einem Server unnötig)
sudo systemctl stop avahi-daemon
sudo systemctl disable avahi-daemon
sudo systemctl mask avahi-daemon
```

**`mask` vs. `disable`:**

| Aktion | Effekt |
|--------|--------|
| `disable` | Service startet nicht automatisch, kann aber manuell gestartet werden |
| `mask` | Service kann überhaupt nicht gestartet werden (Symlink auf `/dev/null`) |

<blockquote class="infobox infobox--warn">
⚠️ **Nur masken, was du wirklich nicht brauchst.** Falls du später doch einen Service brauchst, musst du erst `systemctl unmask` ausführen.
</blockquote>

**Systemd-Security-Score prüfen:**

```bash
# Security Score für einen Service anzeigen
systemd-analyze security sshd.service
```

```bash
→ Overall exposure level for sshd.service: 9.6 OK
```

Je niedriger der Wert, desto besser. `sshd` hat normalerweise einen guten Score, wenn `PasswordAuthentication no` und `PermitRootLogin no` gesetzt sind.

### Audit-Logging

`auditd` protokolliert sicherheitsrelevante Ereignisse und speichert sie in einer manipulationsresistenten Log-Datei. Das ist die Grundlage für Forensik und Compliance.

**Installation:**

```bash
sudo apt install auditd -y
sudo systemctl enable auditd
```

**Regeln für SSH und sudo:**

```bash
# SSH-Konfiguration überwachen
sudo auditctl -w /etc/ssh/sshd_config -p wa -k sshd_config

# Authorized-Keys überwachen
sudo auditctl -w /root/.ssh/authorized_keys -p wa -k ssh_keys

# sudo-Konfiguration überwachen
sudo auditctl -w /etc/sudoers -p wa -k sudoers
sudo auditctl -w /etc/sudoers.d/ -p wa -k sudoers

# Crontab überwachen
sudo auditctl -w /etc/crontab -p wa -k cron
sudo auditctl -w /var/spool/cron/ -p wa -k cron
```

**Regeln dauerhaft speichern:**

```bash
sudo nano /etc/audit/rules.d/hardening.rules
```

```ini
# SSH
-w /etc/ssh/sshd_config -p wa -k sshd_config
-w /root/.ssh/authorized_keys -p wa -k ssh_keys

# Sudo
-w /etc/sudoers -p wa -k sudoers
-w /etc/sudoers.d/ -p wa -k sudoers

# Cron
-w /etc/crontab -p wa -k cron
-w /var/spool/cron/ -p wa -k cron

# Benutzeränderungen
-w /etc/passwd -p wa -k user_change
-w /etc/shadow -p wa -k user_change
-w /etc/group -p wa -k group_change
```

**Verifikation:**

```bash
# Regeln anzeigen
sudo auditctl -l

# Audits für einen Key suchen
sudo ausearch -k sshd_config --interpret

# Audit-Report erstellen
sudo aureport --auth
```

### File-Integrity-Monitoring (AIDE)

AIDE (Advanced Intrusion Detection Environment) vergleicht Dateien mit einem bekannten Zustand und meldet Änderungen.

**Installation:**

```bash
sudo apt install aide -y
```

**Initial-Scan:**

```bash
# Datenbank initialisieren
sudo aideinit
```

<blockquote class="infobox infobox--warn">
⚠️ **Initial-Scan kann dauern.** Auf einem großen Server kann der erste Scan 10-30 Minuten dauern. Führe ihn nicht während Produktivbetrieb aus.
</blockquote>

**Datenbank aktivieren:**

```bash
# Neue Datenbank verschieben
sudo cp /var/lib/aide/aide.db.new /var/lib/aide/aide.db
```

**Cron-basierte Überprüfung:**

```bash
sudo nano /etc/cron.d/aide-check
```

```ini
# Täglicher Check um 03:00
0 3 * * * root /usr/bin/aide --check | mail -s "AIDE Report" admin@example.com
```

**Verifikation:**

```bash
# Manuellen Check durchführen
sudo aide --check

# Erwartete Ausgabe bei sauberem System:
# AIDE found NO differences between database and filesystem.
```

### Automatische Sicherheits-Patches (Unattended-Upgrades)

Eine rein manuelle Patch-Routine lässt Zero-Day-Lücken und bekannte CVEs tagelang ungeschützt. Automatische Sicherheits-Patches schließen kritische Schwachstellen zeitnah, während `needrestart` veraltete Prozesse nach Bibliotheks-Updates neu startet.

**Installation & Aktivierung:**

```bash
# Automatische Updates und Neustart-Prüfer installieren
sudo apt install unattended-upgrades needrestart -y

# Konfiguration aktivieren
sudo dpkg-reconfigure -plow unattended-upgrades
```

**Konfiguration anpassen (`/etc/apt/apt.conf.d/50unattended-upgrades`):**

```ini
// Nur Sicherheitsupdates automatisch installieren
Unattended-Upgrade::Allowed-Origins {
    "${distro_id}:${distro_codename}-security";
};

// Automatisch verwaiste Abhängigkeiten entfernen
Unattended-Upgrade::Remove-Unused-Dependencies "true";

// Automatischer Neustart bei Kernel-Updates um 04:00 Uhr
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "04:00";
```

**Verifikation:**

```bash
# Testlauf ohne Änderungen durchführen (Dry-Run)
sudo unattended-upgrade --dry-run --debug
```

### Mandatory Access Control (AppArmor)

AppArmor schränkt Anwendungen (wie Nginx, Unbound oder PHP-FPM) auf die absolut notwendigen Dateisystem- und Netzwerk-Zugriffe ein. Selbst wenn ein Dienst eine Remote-Code-Execution-Schwachstelle hat, verhindert das AppArmor-Profil das Ausbrechen ins System.

**Status und aktive Profile prüfen:**

```bash
sudo aa-status
```

**Erwartete Ausgabe:**

```bash
apparmor module is loaded.
XX profiles are in enforce mode.
0 processes are unconfined but have a profile defined.
```

**Profile in den Enforce-Modus schalten:**

```bash
# Profile aktivieren und strikt erzwingen
sudo apt install apparmor-utils -y
sudo aa-enforce /etc/apparmor.d/*
```

<blockquote class="infobox infobox--tip">
💡 **Tipp:** Neue oder eigene Profile kannst du mit `aa-genprof <programm>` im interaktiven Lernmodus aufzeichnen und anschließend per `aa-enforce` scharf schalten.
</blockquote>

## Netzwerk-Härtung

### nftables: Die moderne Firewall

`nftables` hat `iptables` als Standard-Firewall-Manager abgelöst. Unter Ubuntu 24.04 und 26.04 ist `nftables` der Kernel-Standard. Wer noch `iptables` nutzt, arbeitet mit einer Kompatibilitätsschicht – das funktioniert, aber `nftables` ist effizienter und bietet klarere Syntax.

**Grundstruktur:**

```bash
# Aktuelle Tabellen anzeigen
sudo nft list ruleset
```

```bash
table inet filter {
    chain input {
        type filter hook input priority filter; policy drop;
        # Regeln hier
    }
    chain forward {
        type filter hook forward priority filter; policy drop;
    }
    chain output {
        type filter hook output priority filter; policy accept;
    }
}
```

**Firewall-Grundregeln erstellen:**

```bash
sudo nano /etc/nftables.conf
```

```bash
#!/usr/sbin/nft -f

flush ruleset

table inet filter {
    chain input {
        type filter hook input priority filter; policy drop;

        # Erlaube etablierte Verbindungen
        ct state established,related accept

        # Erlaube Loopback
        iif lo accept

        # Erlaube ICMP (Ping)
        ip protocol icmp accept
        ip6 nexthdr icmpv6 accept

        # Erlaube SSH (Port 22)
        tcp dport 22 accept

        # Erlaube HTTP/HTTPS (falls Webserver)
        tcp dport { 80, 443 } accept

        # Logging für abgelehnten Traffic
        log prefix "nft-drop: " counter drop
    }

    chain forward {
        type filter hook forward priority filter; policy drop;
    }

    chain output {
        type filter hook output priority filter; policy accept;
    }
}
```

**Service starten:**

```bash
sudo systemctl enable nftables
sudo systemctl start nftables
```

<blockquote class="infobox infobox--warn">
⚠️ **Reihenfolge beachten:** Bei Remote-Servern immer zuerst SSH (Port 22) erlauben, bevor du die Standard-Policy auf `drop` setzt. Sonst sperrst du dich selbst aus.
</blockquote>

**Rate-Limiting für SSH:**

```bash
# SSH-Rate-Limit: Maximal 5 Verbindungen pro Minute
sudo nft add rule inet filter input tcp dport 22 meter ssh-rate { limit rate 5/minute burst 5 packets } accept
```

**Verifikation:**

```bash
# Aktive Regeln prüfen
sudo nft list ruleset

# Verbindungen prüfen
sudo ss -tulnp | grep -E "22|80|443"

# Drops in Logs prüfen
sudo journalctl -k | grep "nft-drop"
```

### CrowdSec + nftables Integration

CrowdSec arbeitet mit `nftables` über den Firewall-Bouncer zusammen. Der Agent analysiert Logs und der Bouncer setzt Banns über `nftables`-Regeln durch.

**Bouncer-Regeln prüfen:**

```bash
# CrowdSec-spezifische Chains anzeigen
sudo nft list chain inet crowdsec-crowdsec-block
```

```bash
table inet crowdsec-crowdsec {
    chain crowdsec-block {
        type filter hook input priority filter; policy accept;
        ip saddr @crowdsec-blacklist-jump drop
        ip6 saddr @crowdsec-blacklist6-jump drop
    }
}
```

**Traffic-Flow:**

```markdown
┌─────────────────────────────────────────────────────────────┐
│                    Firewall Traffic-Flow                    │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   [Eingehender Traffic]                                     │
│          │                                                  │
│          ▼                                                  │
│   ┌──────────────────┐                                      │
│   │  CrowdSec Check  │  ◀── Whitelist? ──▶ ACCEPT           │
│   │  (blacklist)     │  ◀── Ban? ────────▶ DROP + LOG       │
│   └────────┬─────────┘                                      │
│            │                                                │
│            ▼                                                │
│   ┌──────────────────┐                                      │
│   │  Firewall Rules  │  ◀── SSH/HTTP ────▶ ACCEPT           │
│   │  (nftables)      │  ◀── Rest ────────▶ DROP             │
│   └────────┬─────────┘                                      │
│            │                                                │
│            ▼                                                │
│   ┌──────────────────┐                                      │
│   │  CrowdSec Agent  │  ◀── Logs analysieren                │
│   │  (Log-Analyse)   │  ◀── Decisions erstellen             │
│   └──────────────────┘                                      │
│                                                             │
└─────────────────────────────────────────────────────────────┘
```

### TLS-Konfiguration

Für Webserver ist TLS-1.3 der aktuelle Standard. TLS 1.2 ist noch akzeptabel, aber ältere Versionen (1.0, 1.1) sollten deaktiviert werden.

**Let's Encrypt mit Certbot:**

```bash
# Certbot installieren
sudo apt install certbot -y

# Zertifikat anfordern (Nginx)
sudo certbot --nginx -d example.com

# Zertifikat anfordern (Apache)
sudo certbot --apache -d example.com
```

**TLS-Konfiguration für Nginx:**

```bash
sudo nano /etc/nginx/snippets/ssl-params.conf
```

```nginx
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;

# OCSP Stapling
ssl_stapling on;
ssl_stapling_verify on;

# Security Headers
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options DENY always;
```

**Zertifikat-Auto-Renewal:**

```bash
# Cron-Job für automatische Erneuerung
sudo crontab -e
```

```ini
# Täglich um 02:30, Zertifikat bei Bedarf erneuern
30 2 * * * certbot renew --quiet --post-hook "systemctl reload nginx"
```

**Verifikation:**

```bash
# TLS-Konfiguration testen
openssl s_client -connect example.com:443 -tls1_3

# Zertifikats-Details prüfen
echo | openssl s_client -connect example.com:443 2>/dev/null | openssl x509 -noout -dates
```

### DNS-over-HTTPS

Standardmäßig sendet dein Server DNS-Anfragen unverschlüsselt über UDP Port 53. Jeder auf dem Weg kann sehen, welche Domains du auflöst. DNS-over-HTTPS (DoH) verschlüsselt diese Anfragen.

**Unbound als lokaler Resolver:**

```bash
sudo apt install unbound -y
```

**Konfiguration:**

```bash
sudo nano /etc/unbound/unbound.conf.d/hardening.conf
```

```ini
server:
    interface: 127.0.0.1
    access-control: 127.0.0.0/8 allow

    # DNSSEC aktivieren
    auto-trust-anchor-file: /var/lib/unbound/root.key

    # Datenschutz
    hide-identity: yes
    hide-version: yes

    # Performance
    msg-cache-slabs: 8
    rrset-cache-slabs: 8
    infra-cache-slabs: 8
    key-cache-slabs: 8
```

**Mit Cloudflare DoH aufwärtsauflösen:**

```ini
forward-zone:
    name: "."
    forward-addr: 1.1.1.1@853#cloudflare-dns.com
    forward-addr: 9.9.9.9@853#dns.quad9.net
    forward-tls-upstream: yes
```

**Service starten:**

```bash
sudo systemctl enable unbound
sudo systemctl start unbound
```

**Verifikation:**

```bash
# DNS-Auflösung testen
dig example.com @127.0.0.1

# DNSSEC-Test
dig dnssec-failed.org @127.0.0.1
# Erwartung: SERVFAIL (DNSSEC-Breaker korrekt erkannt)

# DNS-over-HTTPS testen
curl -v https://cloudflare-dns.com/dns-query?name=example.com 2>&1 | grep "HTTP/2"
```

<blockquote class="infobox infobox--info">
💡 **Empfehlung:** Für Server mit vielen Clients (z. B. Homelab) ist Unbound als lokaler Resolver die beste Wahl. Clients konfigurierst du mit `127.0.0.1` als DNS-Server, Unbound resolved aufwärts über DoH/DoT.
</blockquote>

## Automatisierung

### Ansible-Playbook für Server-Härtung

Die bisherigen Abschnitte haben gezeigt, welche Konfigurationen nötig sind. Einzelne Befehle auf jedem Server auszuführen ist arbeitsintensiv und fehleranfällig. Ansible automatisiert das – du beschreibst den gewünschten Zustand, und Ansible setzt ihn auf allen Servern um.

**Projektstruktur:**

```bash
mkdir -p ~/ansible-hardening/{roles,inventory,group_vars}
```

**Inventory:**

```bash
nano ~/ansible-hardening/inventory/hosts.ini
```

```ini
[production]
server1 ansible_host=192.168.1.100
server2 ansible_host=192.168.1.101

[homelab]
server3 ansible_host=192.168.10.50

[all:vars]
ansible_user=admin
ansible_python_interpreter=/usr/bin/python3
```

**Playbook:**

```bash
nano ~/ansible-hardening/hardening.yml
```

```yaml
---
- name: Linux Server Härtung
  hosts: all
  become: yes
  vars:
    ssh_port: 22
    allowed_users:
      - admin
    enable_crowdsec: true
    enable_aide: true

  tasks:
    # System-Updates
    - name: System aktualisieren
      apt:
        update_cache: yes
        upgrade: dist

    # SSH-Härtung
    - name: SSH-Konfiguration härten
      lineinfile:
        path: /etc/ssh/sshd_config
        regexp: "{{ item.regexp }}"
        line: "{{ item.line }}"
      loop:
        - { regexp: '^#?PermitRootLogin', line: 'PermitRootLogin no' }
        - { regexp: '^#?PasswordAuthentication', line: 'PasswordAuthentication no' }
        - { regexp: '^#?MaxAuthTries', line: 'MaxAuthTries 3' }
        - { regexp: '^#?LoginGraceTime', line: 'LoginGraceTime 30' }
      notify: sshd neu starten

    # Sysctl-Härtung
    - name: Sysctl-Parameter setzen
      sysctl:
        name: "{{ item.name }}"
        value: "{{ item.value }}"
        state: present
        reload: yes
      loop:
        - { name: 'net.ipv4.conf.all.rp_filter', value: '1' }
        - { name: 'net.ipv4.conf.all.accept_redirects', value: '0' }
        - { name: 'kernel.randomize_va_space', value: '2' }
        - { name: 'kernel.dmesg_restrict', value: '1' }

    # Unnötige Services
    - name: Unnötige Services deaktivieren
      systemd:
        name: "{{ item }}"
        state: stopped
        enabled: no
        masked: yes
      loop:
        - cups
        - bluetooth
        - avahi-daemon

    # Audit-Logging
    - name: Auditd installieren
      apt:
        name: auditd
        state: present

    - name: Audit-Regeln deployen
      copy:
        src: files/audit-rules.rules
        dest: /etc/audit/rules.d/hardening.rules
      notify: auditd neu starten

  handlers:
    - name: sshd neu starten
      systemd:
        name: sshd
        state: restarted

    - name: auditd neu starten
      systemd:
        name: auditd
        state: restarted
```

**Playbook ausführen:**

```bash
cd ~/ansible-hardening
ansible-playbook -i inventory/hosts.ini hardening.yml
```

<blockquote class="infobox infobox--info">
💡 **Idempotenz:** Ansible ist idempotent – du kannst das Playbook mehrmals ausführen, ohne dass sich am System unnötig etwas ändert. Nur abweichende Zustände werden korrigiert.
</blockquote>

**Verifikation:**

```bash
# Prüfe ob alle Server erreichbar sind
ansible all -i inventory/hosts.ini -m ping

# SSH-Konfiguration prüfen
ansible all -i inventory/hosts.ini -m shell -a "sshd -T | grep permitrootlogin"
```

Erwartete Ausgabe:

```bash
server1 | CHANGED | rc=0 >>
permitrootlogin no

server2 | CHANGED | rc=0 >>
permitrootlogin no
```

### CIS-Scoring mit OpenSCAP

CIS (Center for Internet Security) Benchmarks definieren Sicherheitsstandards für verschiedene Systeme. OpenSCAP prüft dein System gegen diese Benchmarks und gibt einen Score aus.

**Installation:**

```bash
sudo apt install libopenscap8 scap-security-guide -y
```

**Ubuntu 24.04/26.04 Benchmark ausführen:**

```bash
# Verfügbare Profiles anzeigen
oscap info /usr/share/xml/scap/ssg/content/ssg-ubuntu2404-ds.xml | grep "Profile"

# CIS-Level-1-Scan
sudo oscap xccdf eval \
    --profile cis_level1_server \
    --results cis-results.xml \
    --report cis-report.html \
    /usr/share/xml/scap/ssg/content/ssg-ubuntu2404-ds.xml
```

**Report im Browser öffnen:**

```bash
# Report auf den lokalen Rechner kopieren und öffnen
scp admin@server:/tmp/cis-report.html ~/Desktop/
```

**Score interpretieren:**

| Score | Bedeutung |
|-------|-----------|
| 100% | Alle CIS-Empfehlungen erfüllt |
| 80-99% | Guter Stand, kleinere Anpassungen nötig |
| 60-79% | Grundkonfiguration, einige Härtungen fehlen |
| < 60% | Kritische Lücken, dringender Handlungsbedarf |

**Automatisierte Remediation (Vorsicht):**

```bash
# Nur wenn du die Auswirkungen verstehst
sudo oscap xccdf generate fix \
    --profile cis_level1_server \
    --fix-type ansible \
    --output cis-remediation.yml \
    /usr/share/xml/scap/ssg/content/ssg-ubuntu2404-ds.xml
```

<blockquote class="infobox infobox--warn">
⚠️ **Remediation nur gezielt:** Automatische Remediation kann funktionierende Konfigurationen überschreiben. Prüfe jede Änderung vor dem Ausführen. Im Zweifel: Manuell und mit Verstand.
</blockquote>

**Verifikation:**

```bash
# Score prüfen
oscap xccdf generate report cis-results.xml > /dev/null
grep -E "score|pass|fail" cis-results.xml | tail -10
```

## Härtungs-Checkliste

| Schritt | Maßnahme | Wirkung |
|---------|----------|---------|
| 1 | SSH: `PasswordAuthentication no`, `PermitRootLogin no` | Brute-Force blockiert |
| 2 | FIDO2/YubiKey einrichten | Passwortlose, hardwarebasierte Auth |
| 3 | CrowdSec Agent + Bouncer installieren | Kollaborativer Intrusion-Schutz |
| 4 | Sysctl-Härtung anwenden | Kernel-Parameter absichern |
| 5 | Unnötige Services maskieren | Angriffsfläche verkleinern |
| 6 | auditd aktivieren | Sicherheitsereignisse protokollieren |
| 7 | AIDE initialisieren | File-Integrity-Überwachung |
| 8 | nftables Firewall aufsetzen | Netzwerk-Traffic filtern |
| 9 | TLS 1.3 + Security Headers | Webserver absichern |
| 10 | Unbound + DoH konfigurieren | DNS-Anfragen verschlüsseln |

Jeder Schritt baut auf dem vorherigen auf. Die Reihenfolge ist bewusst so gewählt: Zuerst die kritischsten Schwachstellen (SSH, CrowdSec), dann System-, dann Netzwerk-Schicht.

## Fazit

Serverhärtung ist kein einmaliges Projekt, sondern ein fortlaufender Prozess. Die in diesem Artikel beschriebenen Maßnahmen – SSH mit FIDO2, CrowdSec, Kernel-Härtung, nftables und TLS – bilden das Fundament, auf dem jedes produktive Linux-System aufbauen sollte.

Wichtig ist der Startpunkt: Nicht alles auf einmal, sondern schrittweise. Die Härtungs-Checkliste oben gibt die Reihenfolge vor. Mit jedem Schritt wird die Angriffsfläche kleiner, und die Kombination aus mehreren Maßnahmen schafft ein robustes Sicherheitsprofil, das einzelnen Schwachstellen nicht das gleiche Schadenspotenzial lässt.

Für Teams lohnt sich die Automatisierung mit Ansible – was einmal konfiguriert ist, kann auf beliebig viele Server übertragen werden. CIS-Benchmarks mit OpenSCAP geben den Score vor und zeigen, wo noch Luft nach oben ist.

**Ein gehärteter Server ist kein Server, der gegen alles abgesichert ist.** Es ist ein Server, bei dem du weißt, was abgesichert ist und was nicht – und warum.

## Weiterführende Ressourcen

### Offizielle Dokumentationen

[OpenSSH FIDO2](https://www.openssh.com/manual.html){.badge-link-text}
[CrowdSec Dokumentation](https://doc.crowdsec.net/){.badge-link-text}
[nftables Wiki](https://wiki.nftables.org/){.badge-link-text}
[CIS Benchmarks](https://www.cisecurity.org/cis-benchmarks){.badge-link-text}
[OpenSCAP Dokumentation](https://www.open-scap.org/tools/){.badge-link-text}

### Tools & Pakete

[YubiKey Manager](https://developers.yubico.com/yubikey-manager/){.badge-link-text}
[CrowdSec Hub](https://hub.crowdsec.net/){.badge-link-text}
[Certbot](https://certbot.eff.org/){.badge-link-text}
[Unbound](https://nlnet.nl/projects/unbound/){.badge-link-text}

### Verwandte Artikel auf admindocs.de

| Thema | Artikel |
|-------|---------|
| VPN-Grundlagen | [WireGuard VPN-Server einrichten](/de/serverumgebungen/ubuntu-24-04-server-einen-eigenen-wireguard-vpn-server-einrichten){.badge-link-text} |
| Container | [Ubuntu 26.04 Docker](/de/serverumgebungen/ubuntu-26-04-docker-installieren-und-verwenden){.badge-link-text} |
| Automatisierung | [Ansible Grundlagen](/de/devops/ansible-grundlagen-automatisierung-fuer-linux-administratoren){.badge-link-text} |
| Netzwerk | [Netzwerkprobleme analysieren](/de/netzwerk/netzwerkprobleme-unter-linux-systematisch-analysieren){.badge-link-text} |
| Grundlagen | [LPIC-1 Serie](/de/category/lpic-1-serie){.badge-link-text} |
| Server | [Ubuntu-Upgrade: 22.04 auf 24.04 LTS](/de/serverumgebungen/ubuntu-upgrade-von-version-22-04-lts-auf-24-04-lts){.badge-link-text} |

