401 lines
18 KiB
JSON
401 lines
18 KiB
JSON
{
|
||
"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": []
|
||
}
|