Initial commit - Hetzner SecMon
This commit is contained in:
commit
b313c3c2b6
3 files changed
+559
No files matched your search
@@ -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": []
|
||||
}
|
||||
Reference in new issue
Block a user