Initial commit - Hetzner SecMon

This commit is contained in:
jhummel committed 2026-10-31 14:25:27 +00:00
commit b313c3c2b6
3 files changed
+559

No files matched your search

+400
View File
@@ -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), ...ObjecLine truncated
},
"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": []
}