From b313c3c2b6eee6067b8a67f879c068e756d2de34 Mon Sep 17 00:00:00 2001 From: Joachim Hummel Date: Sat, 31 Oct 2026 14:25:27 +0000 Subject: [PATCH] Initial commit - Hetzner SecMon --- Hetzner-Secmon-Produktion.json | 400 +++++++++++++++++++++++++++++++++ README.md | 159 +++++++++++++ pictures/Hetzner-SecMon.webp | Bin 0 -> 25370 bytes 3 files changed, 559 insertions(+) create mode 100644 Hetzner-Secmon-Produktion.json create mode 100644 README.md create mode 100644 pictures/Hetzner-SecMon.webp diff --git a/Hetzner-Secmon-Produktion.json b/Hetzner-Secmon-Produktion.json new file mode 100644 index 0000000..692c029 --- /dev/null +++ b/Hetzner-Secmon-Produktion.json @@ -0,0 +1,400 @@ +{ + "name": "Hetzner-Secmon-Produktion", + "nodes": [ + { + "parameters": { + "url": "https://api.hetzner.cloud/v1/servers/actions", + "authentication": "genericCredentialType", + "genericAuthType": "httpBearerAuth", + "sendQuery": true, + "queryParameters": { + "parameters": [ + { + "name": "per_page", + "value": "50" + }, + { + "name": "sort", + "value": "id:desc" + } + ] + }, + "options": { + "timeout": 10000 + } + }, + "id": "83538a61-d7e8-45a5-84c0-a9d573443207", + "name": "Hetzner Aktionen", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 224, + 80 + ], + "retryOnFail": true, + "maxTries": 2, + "waitBetweenTries": 2000, + "credentials": { + "httpBearerAuth": { + "id": "0QTgAYwEIMRwFwY3", + "name": "API-Token-Hetzner-Produktion" + } + } + }, + { + "parameters": { + "url": "https://api.hetzner.cloud/v1/servers", + "authentication": "genericCredentialType", + "genericAuthType": "httpBearerAuth", + "sendQuery": true, + "queryParameters": { + "parameters": [ + { + "name": "per_page", + "value": "50" + } + ] + }, + "options": { + "timeout": 10000 + } + }, + "id": "5112d4e0-8133-4aa2-adc6-3a1b42557edd", + "name": "Hetzner Server", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 448, + 80 + ], + "retryOnFail": true, + "maxTries": 2, + "waitBetweenTries": 2000, + "credentials": { + "httpBearerAuth": { + "id": "0QTgAYwEIMRwFwY3", + "name": "API-Token-Hetzner-Produktion" + } + } + }, + { + "parameters": { + "url": "https://api.hetzner.cloud/v1/firewalls", + "authentication": "genericCredentialType", + "genericAuthType": "httpBearerAuth", + "sendQuery": true, + "queryParameters": { + "parameters": [ + { + "name": "per_page", + "value": "50" + } + ] + }, + "options": { + "timeout": 10000 + } + }, + "id": "235dc45c-5438-427a-aeb9-7055db8f7166", + "name": "Hetzner Firewalls", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 672, + 80 + ], + "retryOnFail": true, + "maxTries": 2, + "waitBetweenTries": 2000, + "credentials": { + "httpBearerAuth": { + "id": "0QTgAYwEIMRwFwY3", + "name": "API-Token-Hetzner-Produktion" + } + } + }, + { + "parameters": { + "url": "https://api.hetzner.cloud/v1/ssh_keys", + "authentication": "genericCredentialType", + "genericAuthType": "httpBearerAuth", + "sendQuery": true, + "queryParameters": { + "parameters": [ + { + "name": "per_page", + "value": "50" + } + ] + }, + "options": { + "timeout": 10000 + } + }, + "id": "004d5ed1-c140-456d-920b-de3d001fe01b", + "name": "Hetzner SSH Keys", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 880, + 80 + ], + "retryOnFail": true, + "maxTries": 2, + "waitBetweenTries": 2000, + "credentials": { + "httpBearerAuth": { + "id": "0QTgAYwEIMRwFwY3", + "name": "API-Token-Hetzner-Produktion" + } + } + }, + { + "parameters": { + "jsCode": "// Hetzner Security Watchdog – Erkennung + Outbox (at-least-once).\n// 1) ALLE Eingaben validieren, BEVOR der State angefasst wird.\n// 2) Änderungen -> state.outbox; Snapshot wird im selben Lauf aktualisiert.\n// 3) Ausgabe = komplette Outbox. Erst der Bestätigungs-Node entfernt zugestellte Einträge.\nconst state = $getWorkflowStaticData('global');\n\nconst listOf = (body, field, strict) => {\n if (!body || !Array.isArray(body[field])) throw new Error(`Hetzner-Antwort ohne '${field}'-Array`);\n const p = body.meta?.pagination;\n if (strict && p && (p.next_page != null || (p.total_entries != null && p.total_entries > body[field].length)))\n throw new Error(`${field}: Liste paginiert/unvollständig – Abbruch ohne State-Änderung`);\n return body[field];\n};\nconst actions = listOf($('Hetzner Aktionen').first().json, 'actions', false);\nconst servers = listOf($('Hetzner Server').first().json, 'servers', false);\nconst firewalls = listOf($('Hetzner Firewalls').first().json, 'firewalls', true);\nconst sshKeys = listOf($('Hetzner SSH Keys').first().json, 'ssh_keys', true);\nfor (const k of sshKeys) if (k.id == null) throw new Error('SSH-Key-Eintrag ohne ID');\n\n// ---------- Ab hier wird State verändert ----------\nstate.outbox = Array.isArray(state.outbox) ? state.outbox : [];\nconst now = new Date().toISOString();\nlet seq = 0;\nconst enqueue = (alertId, title, message, priority) => {\n if (state.outbox.some(a => a.alertId === alertId)) return;\n state.outbox.push({ alertId, title, message, priority, created: now, attempts: 0 });\n};\nconst uid = tag => `${tag}-${Date.now()}-${seq++}`;\nconst J = x => JSON.stringify(x);\n\n// ===== 1. Server-Aktionen =====\n// Hetzner-Command-Namen (nicht Endpoint-Namen!): poweroff = stop_server, shutdown = shutdown_server\nconst LABELS = {\n reboot_server: 'Neustart', reset_server: 'Hard-Reset', start_server: 'Server eingeschaltet',\n stop_server: 'Ausgeschaltet (Power-Off)', shutdown_server: 'Herunterfahren (ACPI)',\n enable_rescue: 'Rescue-Modus aktiviert', disable_rescue: 'Rescue-Modus deaktiviert',\n request_console: 'Remote-Konsole angefordert', rebuild_server: 'Server neu installiert',\n reset_password: 'Root-Passwort zurückgesetzt', change_protection: 'Schutz geändert',\n create_image: 'Snapshot/Image erstellt', attach_iso: 'ISO eingehängt', detach_iso: 'ISO ausgehängt'\n};\nconst CRIT = /rescue|console|password|rebuild|create_image|attach_iso/;\nconst names = Object.fromEntries(servers.map(s => [String(s.id), s.name]));\nconst ids = actions.map(a => String(a.id));\nif (!state.initialized) {\n state.seen = ids.slice(0, 1000);\n state.initialized = true;\n} else {\n const seen = new Set(state.seen || []);\n const fresh = actions.filter(a => !seen.has(String(a.id)));\n if (actions.length >= 50 && fresh.length === actions.length)\n enqueue(`actions-overflow-${ids[0]}`, 'Hetzner Alarm: Aktionen evtl. verpasst',\n `Alle ${actions.length} abgerufenen Aktionen sind neu. Ältere Aktionen liegen evtl. außerhalb des Abfragefensters – Hetzner-Konsole prüfen.`, 8);\n for (const a of [...fresh].reverse()) {\n if (!Object.hasOwn(LABELS, a.command)) continue;\n const srv = (a.resources || []).filter(r => r.type === 'server')\n .map(r => `${names[String(r.id)] || 'Server'} (#${r.id})`).join(', ') || 'Unbekannter Server';\n const label = LABELS[a.command];\n enqueue(`action-${a.id}`, `Hetzner Alarm: ${label}`,\n `Aktion: ${label}\\nServer: ${srv}\\nStatus: ${a.status || '?'}\\nZeit UTC: ${a.started || '?'}\\nAction ID: ${a.id}\\nAPI-Befehl: ${a.command}\\nHinweis: API-Aktion erkannt, kein Beweis für unbefugten Zugriff.`,\n CRIT.test(a.command) ? 10 : 8);\n }\n state.seen = [...new Set([...ids, ...(state.seen || [])])].slice(0, 1000);\n}\n\n// ===== 2. Firewalls =====\nconst normRule = r => ({direction:r.direction, protocol:r.protocol, port:r.port ?? null,\n source_ips:[...(r.source_ips||[])].sort(), destination_ips:[...(r.destination_ips||[])].sort(),\n description:r.description || ''});\nconst fwSnap = {};\nfor (const f of firewalls) {\n const rules = (f.rules||[]).map(normRule).sort((x,y)=>J(x).localeCompare(J(y)));\n const assigned = (f.applied_to||[]).map(v=>({type:v.type, server_id:v.server?.id ?? null,\n label_selector:v.label_selector?.selector ?? null})).sort((x,y)=>J(x).localeCompare(J(y)));\n fwSnap[String(f.id)] = {id:f.id, name:f.name, rules, assigned};\n}\nconst fmtRule = r => `${r.direction.toUpperCase()} ${r.protocol.toUpperCase()} Port ${r.port||'alle'} | Quell-IP: ${r.source_ips.join(', ')||'-'} | Ziel-IP: ${r.destination_ips.join(', ')||'-'}${r.description?' | '+r.description:''}`;\nif (!state.hetznerFirewallInitialized) {\n state.hetznerFirewallInitialized = true;\n} else {\n const old = state.hetznerFirewallSnapshot || {};\n const fw = (t, m) => enqueue(uid('fw'), 'Hetzner Firewall: ' + t, m, 10);\n for (const id of new Set([...Object.keys(old), ...Object.keys(fwSnap)])) {\n const b = old[id], a = fwSnap[id];\n if (!b) { fw('Neue Firewall', `${a.name} (#${id}) wurde angelegt.\\nRegeln:\\n${a.rules.map(fmtRule).join('\\n') || '(keine)'}`); continue; }\n if (!a) { fw('Firewall gelöscht', `${b.name} (#${id}) wurde gelöscht.`); continue; }\n const oR = new Set(b.rules.map(J)), nR = new Set(a.rules.map(J));\n const add = a.rules.filter(x => !oR.has(J(x))), rem = b.rules.filter(x => !nR.has(J(x)));\n if (add.length || rem.length)\n fw('Regeln geändert', `Firewall: ${a.name} (#${id})\\n${add.map(r=>'+ '+fmtRule(r)).join('\\n')}\\n${rem.map(r=>'- '+fmtRule(r)).join('\\n')}`.trim());\n if (J(b.assigned) !== J(a.assigned))\n fw('Zuweisungen geändert', `Firewall: ${a.name} (#${id})\\nVorher: ${J(b.assigned)}\\nNachher: ${J(a.assigned)}`);\n if (b.name !== a.name) fw('Umbenannt', `#${id}: ${b.name} -> ${a.name}`);\n }\n}\nstate.hetznerFirewallSnapshot = fwSnap;\n\n// ===== 3. SSH-Keys =====\nconst keySnap = {};\nfor (const k of sshKeys) keySnap[String(k.id)] = {\n name: k.name || '(ohne Namen)', fingerprint: k.fingerprint || '(kein Fingerprint)',\n public_key: k.public_key || '', labels: k.labels || {}, created: k.created || ''\n};\nconst canonLabels = o => J(Object.entries(o || {}).sort(([x],[y]) => x.localeCompare(y)));\nif (!state.hetznerSSHKeysInitialized) {\n state.hetznerSSHKeysInitialized = true;\n} else {\n const prev = state.hetznerSSHKeysSnapshot || {};\n const sk = (t, m) => enqueue(uid('ssh'), 'Hetzner SSH-Key: ' + t, m, 10);\n for (const id of new Set([...Object.keys(prev), ...Object.keys(keySnap)])) {\n const b = prev[id], a = keySnap[id];\n if (!b) sk('NEU angelegt', `Name: ${a.name}\\nID: ${id}\\nFingerprint: ${a.fingerprint}\\nErstellt UTC: ${a.created || '?'}\\nAchtung: Unbekannten Key sofort prüfen.`);\n else if (!a) sk('GELÖSCHT', `Name: ${b.name}\\nID: ${id}\\nFingerprint: ${b.fingerprint}\\nSchlüssel aus Hetzner-Projekt entfernt.`);\n else {\n const d = [];\n if (b.name !== a.name) d.push(`Name: ${b.name} -> ${a.name}`);\n if (b.fingerprint !== a.fingerprint) d.push(`Fingerprint: ${b.fingerprint} -> ${a.fingerprint}`);\n if (b.public_key !== a.public_key) d.push('Public Key geändert');\n if (canonLabels(b.labels) !== canonLabels(a.labels)) d.push('Labels geändert');\n if (d.length) sk('GEÄNDERT', `Key ID: ${id}\\n${d.join('\\n')}`);\n }\n }\n}\nstate.hetznerSSHKeysSnapshot = keySnap;\n\n// ===== Outbox ausgeben =====\nconst MAX_ATTEMPTS = 1440; // ~24 h bei 1-Minuten-Intervall\nstate.outbox = state.outbox.filter(a => a.attempts < MAX_ATTEMPTS).slice(-200);\nfor (const a of state.outbox) a.attempts++;\nreturn state.outbox.map(a => ({ json: {\n alertId: a.alertId,\n title: a.attempts > 1 ? `${a.title} [verzögert, Versuch ${a.attempts}]` : a.title,\n message: a.attempts > 1 ? `${a.message}\\nErkannt UTC: ${a.created}` : a.message,\n priority: a.priority\n}}));\n" + }, + "id": "adf0c62f-cf48-4ed6-8b9e-1fb35c6ff9ad", + "name": "Änderungen erkennen", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1104, + 80 + ], + "notes": "Validiert alle API-Antworten vor State-Änderung. Neue Alarme landen in staticData.outbox und werden bis zur bestätigten Zustellung jede Minute erneut ausgegeben." + }, + { + "parameters": { + "method": "POST", + "url": "https://notify.example.de/message", + "authentication": "genericCredentialType", + "genericAuthType": "httpBearerAuth", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ JSON.stringify({ title: $json.title, message: $json.message, priority: $json.priority }) }}", + "options": { + "timeout": 10000 + } + }, + "id": "46f6c063-5eed-41fd-9488-e56b35d2b615", + "name": "Gotify Warnung", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 1328, + 80 + ], + "retryOnFail": true, + "maxTries": 3, + "waitBetweenTries": 3000, + "credentials": { + "httpBearerAuth": { + "id": "fBjDrQFrS2vUmHw2", + "name": "Gotify-Token" + } + }, + "onError": "continueErrorOutput" + }, + { + "parameters": { + "jsCode": "// Entfernt nur erfolgreich an Gotify zugestellte Einträge aus der Outbox.\nconst state = $getWorkflowStaticData('global');\nconst delivered = new Set(\n $input.all().map((_, i) => $('Änderungen erkennen').itemMatching(i)?.json?.alertId).filter(Boolean)\n);\nstate.outbox = (state.outbox || []).filter(a => !delivered.has(a.alertId));\nreturn [{ json: { delivered: delivered.size, pending: state.outbox.length } }];\n" + }, + "id": "057d1dbb-6a45-4fc0-a2c1-f12bcd3fa2a7", + "name": "Zustellung bestätigen", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1552, + 0 + ] + }, + { + "parameters": {}, + "id": "4909d4ce-ef55-453b-92c5-8040b553c5f3", + "name": "Error Trigger", + "type": "n8n-nodes-base.errorTrigger", + "typeVersion": 1, + "position": [ + 432, + 272 + ] + }, + { + "parameters": { + "jsCode": "// Error Trigger -> Gotify. Drosselung: gleicher Workflow+Node+Fehler max. 1x pro 15 min.\nconst state = $getWorkflowStaticData('global');\nconst d = $input.first().json;\nconst wfName = d.workflow?.name || '?';\nconst node = d.execution?.lastNodeExecuted || d.trigger?.error?.node?.name || '?';\nconst msg = String(d.execution?.error?.message || d.trigger?.error?.message || 'unbekannter Fehler').slice(0, 400);\nconst key = `${d.workflow?.id}|${node}|${msg}`;\nstate.last = state.last || {};\nconst now = Date.now();\nfor (const [k, t] of Object.entries(state.last)) if (now - t > 24 * 3600e3) delete state.last[k];\nif (state.last[key] && now - state.last[key] < 15 * 60e3) return [];\nstate.last[key] = now;\nreturn [{ json: {\n title: `n8n Workflow-Fehler: ${wfName}`,\n message: `Workflow: ${wfName}\\nNode: ${node}\\nFehler: ${msg}\\nModus: ${d.execution?.mode || '?'}\\nExecution: ${d.execution?.url || d.execution?.id || '-'}\\nZeit UTC: ${new Date().toISOString()}`,\n priority: /hetzner|watchdog|security/i.test(wfName) ? 9 : 6\n}}];\n" + }, + "id": "668dfb8c-a198-4c2f-95bf-7c01efa01469", + "name": "Fehler formatieren", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 656, + 272 + ] + }, + { + "parameters": { + "method": "POST", + "url": "https://notify.example.de/message", + "authentication": "genericCredentialType", + "genericAuthType": "httpBearerAuth", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ JSON.stringify({ title: $json.title, message: $json.message, priority: $json.priority }) }}", + "options": { + "timeout": 10000 + } + }, + "id": "75bb1fa0-4ebe-499e-be64-02555e986e1f", + "name": "Gotify Fehler", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 880, + 272 + ], + "retryOnFail": true, + "maxTries": 3, + "waitBetweenTries": 5000, + "credentials": { + "httpBearerAuth": { + "id": "fBjDrQFrS2vUmHw2", + "name": "Gotify-Token" + } + } + }, + { + "parameters": { + "rule": { + "interval": [ + { + "field": "minutes" + } + ] + } + }, + "id": "83ac8c29-7917-4ae5-aad7-b6896dbd095e", + "name": "Alle 5 Minuten", + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1.2, + "position": [ + 0, + 80 + ] + } + ], + "pinData": {}, + "connections": { + "Hetzner Aktionen": { + "main": [ + [ + { + "node": "Hetzner Server", + "type": "main", + "index": 0 + } + ] + ] + }, + "Hetzner Server": { + "main": [ + [ + { + "node": "Hetzner Firewalls", + "type": "main", + "index": 0 + } + ] + ] + }, + "Hetzner Firewalls": { + "main": [ + [ + { + "node": "Hetzner SSH Keys", + "type": "main", + "index": 0 + } + ] + ] + }, + "Hetzner SSH Keys": { + "main": [ + [ + { + "node": "Änderungen erkennen", + "type": "main", + "index": 0 + } + ] + ] + }, + "Änderungen erkennen": { + "main": [ + [ + { + "node": "Gotify Warnung", + "type": "main", + "index": 0 + } + ] + ] + }, + "Gotify Warnung": { + "main": [ + [ + { + "node": "Zustellung bestätigen", + "type": "main", + "index": 0 + } + ] + ] + }, + "Error Trigger": { + "main": [ + [ + { + "node": "Fehler formatieren", + "type": "main", + "index": 0 + } + ] + ] + }, + "Fehler formatieren": { + "main": [ + [ + { + "node": "Gotify Fehler", + "type": "main", + "index": 0 + } + ] + ] + }, + "Alle 5 Minuten": { + "main": [ + [ + { + "node": "Hetzner Aktionen", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": true, + "settings": { + "executionOrder": "v1", + "binaryMode": "separate" + }, + "versionId": "46972dd5-b363-43a6-b05c-6e0a1d487406", + "meta": { + "instanceId": "34" + }, + "nodeGroups": [], + "id": "LaAEQrLaPcrMhQOK", + "tags": [] +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..204700d --- /dev/null +++ b/README.md @@ -0,0 +1,159 @@ +# Hetzner Secmon – Projektbezogenes Security-Monitoring mit n8n + +> Überwachung sicherheitsrelevanter Änderungen in Hetzner-Cloud-Projekten, mit Zustandsvergleich und Gotify-Alarmierung. + +![Hetzner SecMon – Security-Monitoring mit n8n](pictures/Hetzner-SecMon.webp) + +## Warum ich dieses Projekt gebaut habe + +Ausgangspunkt war ein konkreter Sicherheitsvorfall in meiner Hetzner-Cloud-Umgebung: Ein Angreifer hatte Zugriff auf einen API-Token erlangt. In der anschließenden Untersuchung musste ich Aktivitäten nachvollziehen und betroffene Server absichern. Dabei wurde deutlich, dass die reine nachträgliche Kontrolle über die Hetzner-Konsole für meine Anforderungen nicht ausreicht. + +Ich wollte insbesondere wissen, **wann in einem Projekt ein Server neu gestartet, abgeschaltet, neu installiert oder in den Rescue-Modus versetzt wird**, ob **Firewall-Regeln beziehungsweise Zuweisungen verändert** werden und ob jemand **SSH-Schlüssel hinzufügt, entfernt oder modifiziert**. Diese Ereignisse können legitime Administrationsarbeiten sein – aber auch Indikatoren für einen unberechtigten Zugriff. + +Deshalb habe ich einen eigenständigen Monitor mit **n8n**, der über die Hetzner Cloud API regelmäßig den aktuellen Zustand abfragt, Veränderungen erkennt und mir über **Gotify** meldet. Die Überwachung wird je Hetzner-Projekt beziehungsweise Umgebung mit entsprechendem API-Zugang betrieben. Wichtig war mir dabei nicht nur die Erkennung, sondern auch, dass Benachrichtigungen bei vorübergehenden Zustellproblemen nicht sofort verloren gehen. + +**Grundgedanke:** Sicherheitsrelevante Änderungen sollen mir zeitnah auffallen, statt erst bei einer späteren manuellen Kontrolle. + +## Funktionsübersicht + +| Bereich | Erkennung / Verhalten | +|---|---| +| Serveraktionen | Neustart, Reset, Ein-/Ausschalten, Herunterfahren, Rescue-Modus, Remote-Konsole, Rebuild, Root-Passwort-Reset, Schutzänderung, Image-Erstellung und ISO-Mount/-Unmount | +| Firewalls | Neue/gelöschte Firewalls, geänderte Regeln, Server-/Label-Zuweisungen und Umbenennungen | +| SSH-Schlüssel | Neue/gelöschte Keys, Änderungen an Namen, Fingerprint, Public Key oder Labels | +| Benachrichtigungen | Gotify-Nachrichten mit Ereignisdetails und Priorität | +| Zustellung | Outbox mit erneutem Versand bis zur Bestätigung, innerhalb definierter Grenzen | +| Fehler | Separater Pfad zur Gotify-Fehlermeldung, sofern korrekt als Error Workflow konfiguriert | + +## Architektur + +```text +Schedule Trigger + | + v +Hetzner API: Serveraktionen + | + v +Hetzner API: Serverliste + | + v +Hetzner API: Firewalls + | + v +Hetzner API: SSH-Keys + | + v +Änderungen erkennen + | (n8n Workflow Static Data: + | Snapshots + gesehene Aktionen + Outbox) + v +Gotify Warnung + | + v +Zustellung bestätigen + +Error Trigger -> Fehler formatieren -> Gotify Fehler +``` + +Der Workflow ist im exportierten Zustand **aktiv**. Die API-Aufrufe laufen nacheinander und verwenden HTTP Bearer Authentication über n8n-Credentials. HTTP-Timeout: jeweils 10 Sekunden; für die API-Abfragen sind zwei Versuche konfiguriert. Die Gotify-Requests haben maximal drei Versuche. + +## So funktioniert die Erkennung + +### 1. Serveraktionen + +Der Workflow fragt `GET /v1/servers/actions` mit `per_page=50` und absteigender Action-ID ab. IDs bereits bekannter Aktionen werden in `state.seen` gehalten (maximal 1.000). Neue Aktionen mit einem im Code bekannten Kommando erzeugen einen Alarm. Die Serverliste dient dabei zur Zuordnung von IDs zu Servernamen. + +Besonders hoch priorisiert sind Aktionen im Zusammenhang mit Rescue, Konsole, Passwort, Rebuild, Image-Erstellung und ISO-Einhängen. + +**Hinweis:** Beim ersten Lauf werden vorhandene Action-IDs nur als Ausgangszustand gespeichert. Es werden dafür keine rückwirkenden Benachrichtigungen erstellt. Wenn alle 50 eingelesenen Aktionen neu sind, gibt es einen zusätzlichen Warnhinweis auf mögliche Lücken. + +### 2. Firewall-Überwachung + +Die aktuelle Firewall-Konfiguration wird normalisiert und als Snapshot gespeichert. Hierbei werden Regeln und Zuweisungen unabhängig von ihrer Listenreihenfolge verglichen. Daraus lassen sich unter anderem folgende Änderungen erkennen: + +- Neue oder gelöschte Firewall +- Hinzugefügte oder entfernte Firewall-Regel +- Geänderte Regelparameter wie Richtung, Protokoll, Port, Quell-/Ziel-IP oder Beschreibung +- Geänderte Zuordnung zu Servern oder Label-Selektoren +- Umbenennung einer Firewall + +Änderungen werden in einer Alarmmeldung mit den jeweils erkannten Differenzen ausgegeben. Der erste Lauf erstellt nur die Vergleichsbasis. + +### 3. SSH-Key-Überwachung + +Der Workflow speichert je SSH-Key-ID Name, Fingerprint, öffentlichen Schlüssel, Labels und Erstellungszeitpunkt. Er meldet neu angelegte, gelöschte oder veränderte Keys. Auch hier erzeugt der Initiallauf zunächst einen Snapshot ohne Änderungsalarm. + +### 4. Outbox und Zustellbestätigung + +Neue Alarme werden zunächst in `state.outbox` gespeichert. Bei jedem Lauf werden die noch ausstehenden Alarme erneut an Gotify übergeben. Erst der Node **„Zustellung bestätigen“** entfernt Einträge, die über den erfolgreichen Gotify-Ausgang gelaufen sind. + +Dieses Muster nähert eine *At-least-once-Zustellung* an: Bei zeitweiligen Übertragungsfehlern können Meldungen wiederholt werden. Eine garantierte einmalige Zustellung gibt es nicht; Duplikate sind möglich. Außerdem gelten ausdrücklich Grenzen: maximal 200 Outbox-Einträge und maximal 1.440 Ausgabeversuche pro Eintrag. Danach können Alarme verworfen werden. + +### 5. Workflow-Fehler + +Der separate Pfad `Error Trigger -> Fehler formatieren -> Gotify Fehler` erstellt Fehlermeldungen mit Node, Fehlermeldung und Ausführungsreferenz. Identische Fehler werden anhand von Workflow, Node und Meldung für 15 Minuten gedrosselt. + +**Wichtig:** Ein Error Trigger allein stellt noch nicht sicher, dass Fehler dieses Workflows an diesen Pfad gelangen. Die n8n-Error-Workflow-Konfiguration und die tatsächliche Funktion müssen separat überprüft werden. + +## Installation und Konfiguration + +1. Die Workflow-JSON in n8n importieren. +2. Ein Hetzner Cloud API-Credential vom Typ **HTTP Bearer Auth** für das zu überwachende Projekt hinterlegen und den vier Hetzner-HTTP-Nodes zuweisen. Der Token benötigt Lesezugriff auf die betroffenen Ressourcen; die konkret passenden Berechtigungen sind in Hetzner zu prüfen. +3. Ein Gotify-Credential für die beiden Benachrichtigungs-Nodes konfigurieren und die Gotify-Server-URL auf die eigene Umgebung anpassen. +4. Den Schedule prüfen und das gewünschte Intervall festlegen. +5. Den Workflow einmal ausführen, damit die Ausgangssnapshots erstellt werden. +6. Danach kontrollierte Teständerungen in Hetzner auslösen: beispielsweise eine Firewall-Regel ändern, einen Test-SSH-Key anlegen und eine Serveraktion durchführen. Prüfen, ob die Meldungen tatsächlich ankommen. +7. Fehlerpfad und Verhalten bei vorübergehend unerreichbarem Gotify gezielt testen. + +**Mehrere Projekte:** Die Hetzner Cloud API liefert projektbezogene Daten. Für eine getrennte Überwachung verschiedener Umgebungen können separate Workflow-Instanzen mit jeweils passendem Projekt-Credential betrieben werden. Credentials und Zustände dürfen dabei nicht versehentlich zwischen Projekten vermischt werden. + +## Datenhaltung + +Die Implementierung verwendet **n8n Workflow Static Data** (`$getWorkflowStaticData('global')`). Darin werden unter anderem gespeichert: + +- Bereits bekannte Server-Action-IDs +- Firewall-Snapshot +- SSH-Key-Snapshot +- Outbox mit Alarm-ID, Titel, Nachricht, Priorität, Erstellungszeitpunkt und Anzahl der Versuche +- Zeitpunkte für die Drosselung wiederholter Workflow-Fehler + +Eine externe Datenbank oder n8n Data Table wird im bereitgestellten Export **nicht** verwendet. Die gespeicherten Werte dienen zum Vergleich zwischen Workflow-Ausführungen. Ein verlässlicher Betrieb setzt voraus, dass Static Data erhalten bleibt und Workflow-Ausführungen nicht unkontrolliert parallel laufen. + +## Aktuelle Grenzen und wichtige Prüfungen + +Diese Punkte sind keine theoretische Funktionsbeschreibung, sondern ergeben sich aus dem vorliegenden Workflow-Export: + +1. **Intervall:** Der Trigger heißt „Alle 5 Minuten“, seine Regel lautet aber lediglich `field: minutes` ohne expliziten Wert `5`. In n8n ist damit typischerweise ein einminütiger Rhythmus gemeint. Das tatsächliche Intervall muss im Editor geprüft werden. Auch die Code-Kommentare gehen von einer Minute aus. +2. **API-Paginierung:** Serveraktionen werden mit maximal 50 Ergebnissen pro Abruf eingelesen; weitere Seiten werden nicht nachgeladen. Bei hoher Aktivität können Ereignisse außerhalb dieses Fensters fehlen. Für Firewalls und SSH-Keys wird bei erkennbar unvollständiger Paginierung abgebrochen; Serverlisten werden nicht vollständigkeitsgeprüft. +3. **Zeitverzögerung:** Das Verfahren ist Polling, keine Echtzeit-Eventübertragung. Änderungen zwischen zwei Abfragen können unentdeckt bleiben, insbesondere wenn sie vor der nächsten Abfrage wieder rückgängig gemacht wurden. +4. **Outbox-Begrenzung:** Die maximale Länge von 200 Alarmen und die Grenze von 1.440 Versuchen können bei längerem Ausfall zu Verlust führen. „At least once“ ist deshalb keine uneingeschränkte Garantie. +5. **Keine revisionssichere Historie:** Die Static Data stellen keine manipulationssicheren Audit-Logs dar. Es gibt im Export auch keine dauerhaft gespeicherte Ereignishistorie außerhalb der aktuellen Zustände. +6. **Kein Nachweis der Ursache:** Eine API-Aktion belegt nicht automatisch einen Angriff. Die Auswertung identifiziert weder sicher die handelnde Person noch eindeutig, ob eine Aktion legitim war. +7. **Fehlerüberwachung:** Ein Ausfall von n8n selbst oder seiner Infrastruktur kann auch den Alarmweg verhindern. Der separate Error-Trigger-Pfad ist entsprechend zu prüfen. +8. **Zustelllogik:** Zu testen ist, ob die Zuordnung der erfolgreichen Gotify-Items zu ihren ursprünglichen `alertId`-Werten bei Teilfehlern und mehreren gleichzeitigen Alarmen zuverlässig funktioniert. + +## Sicherheitsmaßnahmen + +- Hetzner-Token **niemals im README oder in Git hinterlegen**; nur n8n-Credentials verwenden. +- Token möglichst mit minimal notwendigen Rechten und getrennt pro Projekt führen. +- n8n, Gotify und ihre Zugangsdaten gegen unberechtigten Zugriff absichern. +- Änderungen durch kontrollierte Tests regelmäßig verifizieren; besonders Firewall- und SSH-Key-Änderungen. +- Für höhere Verlässlichkeit einen dauerhaften Ereignisspeicher, Monitoring der letzten erfolgreichen Ausführung und einen unabhängigen Alarmweg ergänzen. +- Bei Verdacht auf Token-Kompromittierung die betroffenen Zugangsdaten widerrufen/rotieren und die Hetzner-Aktivitäten unabhängig untersuchen. + +## Geplanter Ausbau + +- Vollständige API-Paginierung für alle relevanten Ressourcen +- Dauerhafte Historie und differenzierter Zustandsvergleich in einer Datenbank beziehungsweise n8n Data Tables +- Überwachung des letzten erfolgreichen Pollings (Heartbeat/Dead-Man-Switch) +- Tests für Zustellfehler, gleichzeitige Änderungen und Wiederanlauf +- Optional zweiter Alarmkanal unabhängig von Gotify +- Vereinheitlichte Auswertung mehrerer Projekte in einer zentralen Übersicht + +## Einordnung + +**Hetzner Secmon ist eine zusätzliche Detektions- und Alarmierungsschicht, kein Ersatz für Hetzners eigene Protokolle, restriktive Berechtigungen, Backups oder Incident Response.** Das Projekt entstand aus einem realen Sicherheitsbedarf und verfolgt einen pragmatischen Ansatz: vorhandene APIs und selbst betriebene Infrastruktur nutzen, Änderungen sichtbar machen und den manuellen Kontrollaufwand reduzieren. + +## Technischer Stand + +Dokumentiert anhand des bereitgestellten n8n-Exports `Hetzner-Secmon-Produktion.json` vom 31. Oktober 2026. Die README beschreibt die implementierten Funktionen sowie die aus dem Code erkennbaren Grenzen; sie bestätigt keine erfolgreich durchgeführten End-to-End-Tests. diff --git a/pictures/Hetzner-SecMon.webp b/pictures/Hetzner-SecMon.webp new file mode 100644 index 0000000000000000000000000000000000000000..5b644990d7beb574a31ef91883faf58811c20330 GIT binary patch literal 25370 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