Compare commits

...

2 Commits

Author SHA1 Message Date
mo 0d6ee22247 Document VM 102 security stack and update IPs to 192.168.1.105.
Add ARCHITECTURE.md and HOMELAB_IPS.md, refresh inventory and app configs
for Postgres, Neo4j, Homelab Command, pgAdmin, Homarr, and Homepage links.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-25 23:15:42 +02:00
mo 02b1d155d4 Add home-security-agent with PostgreSQL persistence for dashboard.
The autonomous agent writes all observations to agent.* tables consumed by Homelab Command on port 8765.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-17 21:57:16 +02:00
30 changed files with 1265 additions and 28 deletions
+3
View File
@@ -1,7 +1,10 @@
# Private homelab — echte credentials (repo is privé op Gitea) # Private homelab — echte credentials (repo is privé op Gitea)
NAS_IP=192.168.1.211 NAS_IP=192.168.1.211
# Proxmox VM 102 — Postgres, Neo4j, Homelab Command, syslog, NATS, security agent
VM102_IP=192.168.1.105
POSTGRES_USER=mo POSTGRES_USER=mo
PG_HOST=192.168.1.105
POSTGRES_PASSWORD=WaQTUw2t POSTGRES_PASSWORD=WaQTUw2t
POSTGRES_DB=homelab POSTGRES_DB=homelab
PG_HOST_PORT=5433 PG_HOST_PORT=5433
+150
View File
@@ -0,0 +1,150 @@
# Homelab architectuur
Overzicht van hoe de diensten bij elkaar hangen na de verplaatsing van de **security stack** naar Proxmox VM 102. Vaste IP-referentie: [HOMELAB_IPS.md](HOMELAB_IPS.md).
## Fysieke hosts
```mermaid
flowchart TB
subgraph lan [LAN 192.168.1.0/24]
NAS["Synology NAS\n192.168.1.211"]
VM102["Proxmox VM 102 Postgress\n192.168.1.105"]
VM105["Proxmox VM 105 docker\n192.168.1.227"]
PVE216["Proxmox pve\n192.168.1.216"]
PVE56["Proxmox dell\n192.168.1.56"]
UDM["UniFi / gateway\n192.168.1.24"]
HA["Home Assistant\n192.168.1.235"]
end
User["Browser / Git client"] --> NAS
User --> VM102
```
## Security stack (productie op VM 102)
Het hart van monitoring, syslog, graph en agent-draait op **één VM**. De NAS toont nog dashboards in Homarr/Homepage en beheert Git + metrics.
```mermaid
flowchart LR
subgraph sources [Data-bronnen LAN]
UniFi[UniFi controller .24]
SyslogDev[Routers / switches / APs]
Zeek[Zeek / Suricata]
end
subgraph vm102 [VM 102 — 192.168.1.105]
UI[homelab-command :8765]
SyslogUDP[Syslog UDP :5514]
PG[(postgres-homelab :5433)]
Neo[(Neo4j :49153 / UI :49154)]
NATS[NATS :4222]
Mesh[mesh-normalizer]
Agent[el-kadi-security-agent]
end
subgraph nas [NAS — 192.168.1.211]
Gitea[Gitea :3000]
PgAdmin[pgAdmin :5434]
Graf[Grafana :3002]
Prom[Prometheus :9090]
PGexp[postgres-exporter :9187]
AdGuard[AdGuard :3001]
PGBkp[(postgres-homelab backup :5433)]
end
User2[Gebruiker] --> UI
User2 --> PgAdmin
PgAdmin --> PG
Graf --> PG
Prom --> PGexp
PGexp --> PG
SyslogDev -->|UDP 5514| SyslogUDP
UniFi -->|API| UI
Zeek -->|NATS publish| NATS
NATS --> Mesh
SyslogUDP --> PG
UI --> PG
UI --> Neo
Mesh --> PG
Agent --> PG
AdGuard -->|DNS stats API| UI
PG -.->|rollback kopie| PGBkp
```
## Datastromen
| Stroom | Van | Naar | Poort / protocol |
|--------|-----|------|------------------|
| Dashboard UI | Browser | VM 102 | HTTP 8765 |
| SQL (homelab DB) | homelab-command, agent, mesh | Postgres VM 102 | 5433 |
| Syslog ingest | Netwerkapparaten | homelab-command | UDP 5514 → `mesh.syslog_entries` |
| Zeek/Suricata events | Sensors | NATS → mesh-normalizer | 4222 → Postgres |
| Netwerk-topologie | homelab-command | Neo4j VM 102 | Bolt 49153 |
| DB beheer | pgAdmin (NAS) | Postgres VM 102 | 5433 |
| Metrics | Prometheus (NAS) | postgres-exporter → VM 102 | 9187 scrape |
| Git configs | Ontwikkelaar | Gitea (NAS) | 3000 / SSH 2222 |
## NAS — overige Docker (niet op VM 102)
```mermaid
flowchart TB
NAS["192.168.1.211"]
NAS --> Gitea
NAS --> AdGuard
NAS --> Portainer
NAS --> Homarr
NAS --> Homepage
NAS --> PromGraf[Prometheus + Grafana]
NAS --> PgAdmin
NAS --> PGbak[Postgres backup]
```
| Service | Poort | Opmerking |
|---------|-------|-----------|
| Gitea | 3000 | Config-repo's, Git SSH 2222 |
| AdGuard | 3001 | DNS (dashboard haalt stats op) |
| Portainer | 9000 | Containerbeheer NAS |
| Homarr / Homepage | 4755 / 3010 | Links naar `.105` voor security |
| Grafana | 3002 | Postgres-datasource → `.105:5433` |
| Prometheus | 9090 | Scrapes o.a. Neo4j `.105:2004` |
| postgres (backup) | 5433 | Oude kopie; stoppen na validatie |
## Proxmox VM 105 docker (apart)
| VM | IP | Rol |
|----|-----|-----|
| 102 Postgress | 192.168.1.105 | Security + Postgres + Neo4j |
| 105 docker | 192.168.1.227 | `office_desk_agent` :8000 (Proxmox/office tooling) |
## Repo-structuur (Gitea)
```mermaid
flowchart LR
Gitea["Gitea :3000\n192.168.1.211"]
Gitea --> CFG[homelab-configs\nDocker compose per app]
Gitea --> CMD[homelab-command\nDashboard broncode]
CFG --> DeployNAS[Deploy NAS apps]
CFG --> DeployVM[Documentatie VM 102]
CMD --> BuildVM[Build op VM 102\n~/homelab-command]
```
| Repository | Inhoud |
|------------|--------|
| `homelab-configs` | Compose, env-voorbeelden, Homarr/Homepage, monitoring |
| `homelab-command` | FastAPI dashboard, mesh-ingest, Grafana-dockerfile |
## Snelle URL-lijst
| Wat | URL |
|-----|-----|
| Security dashboard | http://192.168.1.105:8765/dashboard |
| Neo4j Browser | http://192.168.1.105:49154 |
| pgAdmin | http://192.168.1.211:5434 |
| Gitea | http://192.168.1.211:3000 |
| Grafana | http://192.168.1.211:3002 |
| Portainer | http://192.168.1.211:9000 |
## Rollback
- Postgres: zie [homelab-command/docs/POSTGRES_ROLLBACK.md](http://192.168.1.211:3000/mo/homelab-command/src/branch/main/docs/POSTGRES_ROLLBACK.md) (in homelab-command repo) — `PG_HOST` terug naar `.211`, NAS-container herstarten.
+30
View File
@@ -0,0 +1,30 @@
# Homelab IP-adressen (referentie)
## Waar draait wat?
| IP | Host | Services |
|----|------|----------|
| **192.168.1.105** | Proxmox VM 102 `Postgress` | Postgres :5433, Neo4j :4915349155, Dashboard :8765, Syslog UDP :5514, NATS :4222, Security agent |
| **192.168.1.211** | Synology NAS | Gitea :3000, pgAdmin :5434, Grafana :3002, Prometheus :9090, AdGuard :3001, Portainer :9000, Postgres **backup** :5433 |
| **192.168.1.227** | Proxmox VM 105 `docker` | Office desk agent :8000 |
| **192.168.1.216** | Proxmox pve | API :8006 |
| **192.168.1.56** | Proxmox dell | API :8006 |
## Standaard URLs (productie)
- Dashboard: http://192.168.1.105:8765/dashboard
- Neo4j Browser: http://192.168.1.105:49154
- pgAdmin: http://192.168.1.211:5434 (server → `192.168.1.105:5433`)
- Gitea: http://192.168.1.211:3000
## Env-variabelen (`.env` in repo)
```env
NAS_IP=192.168.1.211
VM102_IP=192.168.1.105
PG_HOST=192.168.1.105
```
## Syslog
Remote syslog → **192.168.1.105:5514** (niet `.211`).
+18 -6
View File
@@ -1,17 +1,29 @@
# Homelab inventaris — alles thuis # Homelab inventaris — alles thuis
Private repo. Laatst bijgewerkt vanaf NAS `192.168.1.211`. Private repo. Laatst bijgewerkt: security stack op VM 102 (`192.168.1.105`), overige apps op NAS (`192.168.1.211`).
## Proxmox VM 102 Postgress — `192.168.1.105` (productie security)
| App | Map / pad op VM | IP:poort | Status |
|-----|-----------------|----------|--------|
| PostgreSQL | `~/homelab-postgres/` | :5433 | running |
| Neo4j | `~/neo4j/` | :4915349155 | running |
| Homelab Command | `~/homelab-command/` | :8765 | running |
| Syslog UDP | homelab-command | :5514 | → `.105` |
| NATS + mesh-normalizer | `~/homelab-command/` | :4222 | running |
| Security Agent | `~/home-security-agent/` | host | running |
**Dashboard:** http://192.168.1.105:8765/dashboard · **Neo4j UI:** http://192.168.1.105:49154
## Synology NAS — Docker (actief) ## Synology NAS — Docker (actief)
| App | Map | IP:poort | Status | | App | Map | IP:poort | Status |
|-----|-----|----------|--------| |-----|-----|----------|--------|
| PostgreSQL | [apps/postgres](apps/postgres/) | :5433 | running | | PostgreSQL (backup) | [apps/postgres](apps/postgres/) | 192.168.1.211:5433 | running · fallback |
| pgAdmin | [apps/pgadmin](apps/pgadmin/) | :5434 | running | | pgAdmin | [apps/pgadmin](apps/pgadmin/) | :5434 | running → DB op `.105` |
| Gitea | [apps/gitea](apps/gitea/) | :3000 | running | | Gitea | [apps/gitea](apps/gitea/) | :3000 | running |
| AdGuard Home | [apps/adguard](apps/adguard/) | :53, :3001 | running | | AdGuard Home | [apps/adguard](apps/adguard/) | :53, :3001 | running |
| DuckDNS | [apps/duckdns](apps/duckdns/) | — | running | | DuckDNS | [apps/duckdns](apps/duckdns/) | — | running |
| Neo4j | [apps/neo4j](apps/neo4j/) | :4915349155 | running |
| Homarr | [apps/homarr](apps/homarr/) | :4755 | running | | Homarr | [apps/homarr](apps/homarr/) | :4755 | running |
| Homepage | [apps/homepage](apps/homepage/) | http://192.168.1.192:3000 (pve CT 120) | running | | Homepage | [apps/homepage](apps/homepage/) | http://192.168.1.192:3000 (pve CT 120) | running |
| Portainer | [apps/portainer](apps/portainer/) | :9000 | running | | Portainer | [apps/portainer](apps/portainer/) | :9000 | running |
@@ -19,8 +31,6 @@ Private repo. Laatst bijgewerkt vanaf NAS `192.168.1.211`.
| Excalidraw | [apps/excalidraw](apps/excalidraw/) | :3765 | running | | Excalidraw | [apps/excalidraw](apps/excalidraw/) | :3765 | running |
| Prometheus | [apps/monitoring](apps/monitoring/) | :9090 | running | | Prometheus | [apps/monitoring](apps/monitoring/) | :9090 | running |
| Grafana | [apps/monitoring](apps/monitoring/) | :3002 | running | | Grafana | [apps/monitoring](apps/monitoring/) | :3002 | running |
| Homelab Command | [homelab-command repo](http://192.168.1.211:3000/mo/homelab-command) | :8765 | running |
| NATS + mesh | [apps/monitoring](apps/monitoring/) | :4222 | running |
## Synology NAS — Docker (gestopt / image aanwezig) ## Synology NAS — Docker (gestopt / image aanwezig)
@@ -81,6 +91,8 @@ python3 scripts/pull-lxc-from-proxmox.py # op NAS, via Proxmox SSH
| IP | Rol | | IP | Rol |
|----|-----| |----|-----|
| 192.168.1.211 | Synology NAS | | 192.168.1.211 | Synology NAS |
| 192.168.1.105 | Proxmox VM 102 Postgress (Postgres, Neo4j, Homelab Command) |
| 192.168.1.227 | Proxmox VM 105 docker (office agent) |
| 192.168.1.216 | Proxmox pve | | 192.168.1.216 | Proxmox pve |
| 192.168.1.56 | Proxmox dell | | 192.168.1.56 | Proxmox dell |
| 192.168.1.24 | UniFi controller | | 192.168.1.24 | UniFi controller |
+1 -1
View File
@@ -1,6 +1,6 @@
# Homelab Infrastructure Configuration # Homelab Infrastructure Configuration
Private Gitea-repo met **alle configs per applicatie** voor Synology NAS (`192.168.1.211`) en Proxmox hosts. Private Gitea-repo met **alle configs per applicatie** voor Synology NAS (`192.168.1.211`) en Proxmox VM 102 (`192.168.1.105`). Zie [HOMELAB_IPS.md](HOMELAB_IPS.md).
**Snel herstellen:** [RESTORE.md](RESTORE.md) **Snel herstellen:** [RESTORE.md](RESTORE.md)
**Volledige inventaris:** [INVENTORY.md](INVENTORY.md) **Volledige inventaris:** [INVENTORY.md](INVENTORY.md)
+6 -6
View File
@@ -4007,7 +4007,7 @@
{ {
"id": "1446d0cd-5449-4e41-b68b-15b4052f6325", "id": "1446d0cd-5449-4e41-b68b-15b4052f6325",
"name": "Neo4j Browser", "name": "Neo4j Browser",
"url": "http://192.168.1.211:49154", "url": "http://192.168.1.105:49154",
"appearance": { "appearance": {
"iconUrl": "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/neo4j.png", "iconUrl": "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/neo4j.png",
"appNameStatus": "normal", "appNameStatus": "normal",
@@ -4028,7 +4028,7 @@
}, },
"behaviour": { "behaviour": {
"isOpeningNewTab": true, "isOpeningNewTab": true,
"externalUrl": "http://192.168.1.211:49154" "externalUrl": "http://192.168.1.105:49154"
}, },
"area": { "area": {
"type": "category", "type": "category",
@@ -4066,7 +4066,7 @@
{ {
"id": "039d3bf6-bf8a-4944-a8b1-7cc886daebe7", "id": "039d3bf6-bf8a-4944-a8b1-7cc886daebe7",
"name": "HA Voice Ctrl", "name": "HA Voice Ctrl",
"url": "http://192.168.1.211:8765", "url": "http://192.168.1.105:8765",
"appearance": { "appearance": {
"iconUrl": "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/home-assistant.png", "iconUrl": "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/home-assistant.png",
"appNameStatus": "normal", "appNameStatus": "normal",
@@ -4087,7 +4087,7 @@
}, },
"behaviour": { "behaviour": {
"isOpeningNewTab": true, "isOpeningNewTab": true,
"externalUrl": "http://192.168.1.211:8765" "externalUrl": "http://192.168.1.105:8765"
}, },
"area": { "area": {
"type": "category", "type": "category",
@@ -5426,7 +5426,7 @@
{ {
"id": "9be593d8-a4b4-460a-8998-6cafefb4271e", "id": "9be593d8-a4b4-460a-8998-6cafefb4271e",
"name": "Home Control", "name": "Home Control",
"url": "http://192.168.1.211:8765/dashboard#live", "url": "http://192.168.1.105:8765/dashboard#live",
"appearance": { "appearance": {
"iconUrl": "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/svg/crafty-controller.svg", "iconUrl": "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/svg/crafty-controller.svg",
"appNameStatus": "normal", "appNameStatus": "normal",
@@ -5447,7 +5447,7 @@
}, },
"behaviour": { "behaviour": {
"isOpeningNewTab": true, "isOpeningNewTab": true,
"externalUrl": "http://192.168.1.211:8765/dashboard#live" "externalUrl": "http://192.168.1.105:8765/dashboard#live"
}, },
"area": { "area": {
"type": "category", "type": "category",
+14
View File
@@ -0,0 +1,14 @@
# Telegram (verplicht voor meldingen)
TELEGRAM_BOT_TOKEN=
TELEGRAM_CHAT_ID=
# PostgreSQL — observaties voor dashboard http://192.168.1.211:8765
PG_HOST=192.168.1.211
PG_PORT=5433
PG_USER=mo
PG_PASSWORD=WaQTUw2t
PG_DATABASE=homelab
# Optioneel: LLM-agent (zonder key = regel-gebaseerde modus)
OPENAI_API_KEY=
AGENT_MODEL=gpt-4o-mini
+14
View File
@@ -0,0 +1,14 @@
# Telegram (verplicht voor meldingen)
TELEGRAM_BOT_TOKEN=
TELEGRAM_CHAT_ID=
# PostgreSQL — observaties voor dashboard http://192.168.1.105:8765
PG_HOST=192.168.1.105
PG_PORT=5433
PG_USER=mo
PG_PASSWORD=
PG_DATABASE=homelab
# Optioneel: LLM-agent (zonder key = regel-gebaseerde modus)
OPENAI_API_KEY=
AGENT_MODEL=gpt-4o-mini
+14
View File
@@ -0,0 +1,14 @@
FROM python:3.11-slim-bookworm
RUN apt-get update && apt-get install -y --no-install-recommends iputils-ping docker.io \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY agent ./agent
COPY config ./config
ENV PYTHONUNBUFFERED=1
CMD ["python", "-m", "agent.main"]
+79
View File
@@ -0,0 +1,79 @@
# EL-KADI Home Security Agent
**Autonome** security agent voor thuis — zonder Wazuh, Uptime Kuma, n8n of Security Mesh.
De agent:
1. **Observeert** zelf (HTTP/TCP, Docker, Proxmox, LAN-gateway)
2. **Redeneert** (OpenAI met tools, of regels zonder API-key)
3. **Onthoudt** incidenten (SQLite, dedupe)
4. **Meld** via **Telegram**
## Starten
```bash
cd /volume1/docker/homelab-configs/apps/home-security-agent
cp .env.example .env
# Vul TELEGRAM_BOT_TOKEN en TELEGRAM_CHAT_ID in
# Optioneel: OPENAI_API_KEY voor agentische modus
docker-compose up -d --build
```
Eén run testen:
```bash
docker-compose run --rm security-agent python -m agent.main once
```
## Configuratie
| Bestand | Doel |
|---------|------|
| `config/targets.yaml` | Wat gemonitord wordt |
| `config/policies.yaml` | Interval, quiet hours, severity |
| `.env` | Telegram + OpenAI |
## Agentische modus (LLM)
Met `OPENAI_API_KEY` krijgt het model tools (`probe_tcp`, `probe_http`, `probe_proxmox`) en mag zelf verifiëren voordat het alert=true zet.
Zonder key: **regel-engine** (down services → Telegram).
## Uitbreiden
Voeg in `targets.yaml` services toe. Voor diepere agent-gedrag later:
- SSH-log tail (auth failures)
- Proxmox API (VM status) als aparte tool
- LAN device discovery + `known_hosts` whitelist
- Lokale Ollama (`AGENT_MODEL` + OpenAI-compatible URL)
## Dashboard
Alle observaties gaan naar **PostgreSQL** (`agent.observation_runs`, `agent.findings`, `agent.incidents`).
Bekijk ze in **Homelab Command**: http://192.168.1.105:8765/dashboard#security (tab Security → Home Security Agent).
Eénmalig schema:
```bash
docker exec -i postgres-homelab psql -U mo -d homelab < migrations/004_home_agent_observations.sql
# Postgres draait op VM 102: ssh mo@192.168.1.105 → docker exec postgres-homelab psql ...
```
Of vanuit homelab-command: `scripts/apply_mesh_migrations.sh` (past alle `migrations/*.sql` toe).
## Architectuur
```
┌─────────────┐ ┌──────────────┐ ┌────────────┐ ┌──────────┐
│ Observer │────▶│ Brain │────▶│ PostgreSQL │────▶│ Dashboard│
│ (eigen │ │ LLM + tools │ │ agent.* │ │ :8765 │
│ probes) │ │ of regels │ └────────────┘ └──────────┘
└─────────────┘ └──────┬───────┘ │
│ │
┌──────▼───────┐ ┌──────▼───────┐
│ SQLite state │ │ Telegram │
│ dedupe │ │ meldingen │
└──────────────┘ └──────────────┘
```
@@ -0,0 +1 @@
# EL-KADI Home Security Agent
+243
View File
@@ -0,0 +1,243 @@
"""Agentisch brein — LLM met tools, of regel-fallback zonder API."""
import json
import os
from dataclasses import dataclass
from datetime import datetime
from typing import List, Optional
from agent.observer import (
ObservationReport,
load_yaml,
probe_http,
probe_proxmox,
probe_tcp,
)
from agent.state import recent_incidents
@dataclass
class AgentDecision:
alert: bool
severity: str
title: str
body: str
fingerprint: str
actions: List[str]
TOOLS_SCHEMA = [
{
"type": "function",
"function": {
"name": "probe_tcp",
"description": "Test of een TCP-poort open is op een host",
"parameters": {
"type": "object",
"properties": {
"host": {"type": "string"},
"port": {"type": "integer"},
},
"required": ["host", "port"],
},
},
},
{
"type": "function",
"function": {
"name": "probe_http",
"description": "HTTP GET check op een URL",
"parameters": {
"type": "object",
"properties": {
"url": {"type": "string"},
"insecure_tls": {"type": "boolean"},
},
"required": ["url"],
},
},
},
{
"type": "function",
"function": {
"name": "probe_proxmox",
"description": "Check Proxmox web UI bereikbaarheid",
"parameters": {
"type": "object",
"properties": {"host": {"type": "string"}},
"required": ["host"],
},
},
},
]
def _run_tool(name: str, args: dict) -> str:
if name == "probe_tcp":
ok, detail = probe_tcp(args["host"], int(args["port"]))
return json.dumps({"ok": ok, "detail": detail})
if name == "probe_http":
ok, detail = probe_http(args["url"], insecure=bool(args.get("insecure_tls")))
return json.dumps({"ok": ok, "detail": detail})
if name == "probe_proxmox":
ok, detail = probe_proxmox(args["host"])
return json.dumps({"ok": ok, "detail": detail})
return json.dumps({"error": "unknown tool"})
def _rule_based_decide(report: ObservationReport, policies: dict) -> AgentDecision:
rules = policies.get("rules") or {}
failed = [f for f in report.findings if not f.ok]
if not failed:
return AgentDecision(
alert=False,
severity="info",
title="Alles OK",
body=f"{len(report.findings)} checks geslaagd.",
fingerprint="all_ok",
actions=[],
)
worst = "low"
lines = []
for f in failed:
lines.append(f"{f.kind}/{f.name}: {f.detail}")
if f.kind == "proxmox" and not f.ok:
worst = rules.get("proxmox_unreachable", "critical")
elif f.kind == "nas" and not f.ok:
worst = max_sev(worst, rules.get("nas_unreachable", "critical"))
elif f.kind == "service" and not f.ok:
worst = max_sev(worst, rules.get("any_service_down", "high"))
elif f.kind == "docker" and not f.ok:
worst = max_sev(worst, "high")
fp = "fail:" + "|".join(sorted(f"{f.kind}:{f.name}" for f in failed))[:200]
return AgentDecision(
alert=True,
severity=worst,
title=f"{len(failed)} probleem(en) gedetecteerd",
body="\n".join(lines),
fingerprint=fp,
actions=["Herhaal check over 5 min", "Controleer host handmatig"],
)
def max_sev(a: str, b: str) -> str:
order = ["info", "low", "medium", "high", "critical"]
return a if order.index(a) >= order.index(b) else b
def _in_quiet_hours(policies: dict) -> bool:
qh = policies.get("quiet_hours") or {}
if not qh:
return False
try:
from zoneinfo import ZoneInfo
tz = ZoneInfo(qh.get("timezone", "UTC"))
except Exception:
tz = None
now = datetime.now(tz) if tz else datetime.now()
start = qh.get("start", "23:00")
end = qh.get("end", "07:00")
h, m = map(int, now.strftime("%H %M").split())
cur = h * 60 + m
sh, sm = map(int, start.split(":"))
eh, em = map(int, end.split(":"))
s, e = sh * 60 + sm, eh * 60 + em
if s <= e:
return s <= cur < e
return cur >= s or cur < e
return False
def decide(report: ObservationReport) -> AgentDecision:
policies = load_yaml("policies.yaml")
history = recent_incidents(8)
api_key = os.environ.get("OPENAI_API_KEY", "").strip()
model = os.environ.get("AGENT_MODEL", "gpt-4o-mini")
if not api_key:
return _rule_based_decide(report, policies)
try:
from openai import OpenAI
client = OpenAI(api_key=api_key)
system = """Je bent de autonome security agent voor het EL-KADI homelab thuis.
Je krijgt ruwe observaties (eigen probes, geen Wazuh/Uptime Kuma).
Beslis of de gebruiker een Telegram-melding moet krijgen.
Wees conservatief: alleen alert bij echt problemen of verdachte wijzigingen.
Antwoord uiteindelijk ALTIJD met JSON:
{"alert":bool,"severity":"info|low|medium|high|critical","title":"...","body":"...","fingerprint":"korte_sleutel","actions":["..."]}
Je mag tools aanroepen om te verifiëren voordat je alert=true zet."""
user_msg = json.dumps(
{
"observations": report.to_dict(),
"recent_incidents": history,
"policies": {
"quiet_hours": policies.get("quiet_hours"),
"dedupe_minutes": policies.get("dedupe_minutes"),
},
},
indent=2,
default=str,
)
messages = [
{"role": "system", "content": system},
{"role": "user", "content": user_msg},
]
for _ in range(5):
resp = client.chat.completions.create(
model=model,
messages=messages,
tools=TOOLS_SCHEMA,
tool_choice="auto",
)
msg = resp.choices[0].message
if msg.tool_calls:
messages.append(msg)
for tc in msg.tool_calls:
result = _run_tool(tc.function.name, json.loads(tc.function.arguments))
messages.append(
{
"role": "tool",
"tool_call_id": tc.id,
"content": result,
}
)
continue
text = (msg.content or "").strip()
if "```" in text:
text = text.split("```")[1].replace("json", "").strip()
data = json.loads(text)
return AgentDecision(
alert=bool(data.get("alert")),
severity=str(data.get("severity", "medium")),
title=str(data.get("title", "Security")),
body=str(data.get("body", "")),
fingerprint=str(data.get("fingerprint", "llm")),
actions=list(data.get("actions") or []),
)
except Exception as e:
dec = _rule_based_decide(report, policies)
dec.body += f"\n\n(LLM fallback: {e})"
return dec
return _rule_based_decide(report, policies)
def should_notify(decision: AgentDecision, policies: dict) -> bool:
if not decision.alert:
return False
allowed = policies.get("severity_telegram") or ["critical", "high"]
if decision.severity not in allowed:
return False
if _in_quiet_hours(policies):
return decision.severity == (policies.get("quiet_hours") or {}).get(
"allow_severity", "critical"
)
return True
+91
View File
@@ -0,0 +1,91 @@
#!/usr/bin/env python3
"""EL-KADI Home Security Agent — autonome loop."""
import os
import sys
import time
from pathlib import Path
# Package root on path
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT))
from dotenv import load_dotenv
load_dotenv(ROOT / ".env")
from agent.brain import decide, should_notify
from agent.observer import load_yaml, run_observation
from agent.pg_store import (
persist_incident as pg_record_incident,
persist_observation,
was_notified_recently_pg,
)
from agent.state import log_run, record_incident, was_notified_recently
from agent.telegram_notify import format_alert, send_message
def _record_incident(
run_id,
fingerprint: str,
severity: str,
title: str,
body: str,
notified: bool,
) -> None:
record_incident(fingerprint, severity, title, body, notified)
pg_record_incident(run_id, fingerprint, severity, title, body, notified)
def run_once() -> int:
policies = load_yaml("policies.yaml")
report = run_observation()
decision = decide(report)
log_run(decision.title, report.to_dict())
run_id = persist_observation(report, decision)
print(f"[{report.timestamp}] {decision.severity}: {decision.title} (alert={decision.alert})")
if run_id:
print(f" → PostgreSQL run #{run_id}")
if not should_notify(decision, policies):
if decision.alert:
_record_incident(run_id, decision.fingerprint, decision.severity, decision.title, decision.body, False)
return 0
dedupe = int(policies.get("dedupe_minutes", 30))
if was_notified_recently(decision.fingerprint, dedupe) or was_notified_recently_pg(
decision.fingerprint, dedupe
):
print(f" dedupe skip ({decision.fingerprint})")
return 0
text = format_alert(decision.severity, decision.title, decision.body, decision.actions)
if send_message(text):
_record_incident(run_id, decision.fingerprint, decision.severity, decision.title, decision.body, True)
print(" → Telegram verzonden")
return 0
print(" → Telegram mislukt (check TELEGRAM_BOT_TOKEN / TELEGRAM_CHAT_ID)")
_record_incident(run_id, decision.fingerprint, decision.severity, decision.title, decision.body, False)
return 1
def main():
if len(sys.argv) > 1 and sys.argv[1] == "once":
sys.exit(run_once())
policies = load_yaml("policies.yaml")
interval = int(policies.get("interval_seconds", 300))
print(f"EL-KADI Security Agent — loop elke {interval}s (Ctrl+C stop)")
while True:
try:
run_once()
except KeyboardInterrupt:
raise
except Exception as e:
print(f"run error: {e}")
time.sleep(interval)
if __name__ == "__main__":
main()
+159
View File
@@ -0,0 +1,159 @@
"""Directe observatie — geen Wazuh/Uptime Kuma/n8n."""
import socket
import subprocess
from dataclasses import dataclass, field
from datetime import datetime
from typing import Any
import httpx
import yaml
CONFIG_DIR = __import__("pathlib").Path(__file__).resolve().parent.parent / "config"
@dataclass
class Finding:
kind: str
name: str
ok: bool
detail: str
meta: dict = field(default_factory=dict)
@dataclass
class ObservationReport:
timestamp: str
findings: list[Finding]
def to_dict(self) -> dict:
return {
"timestamp": self.timestamp,
"findings": [
{"kind": f.kind, "name": f.name, "ok": f.ok, "detail": f.detail, "meta": f.meta}
for f in self.findings
],
"summary": {
"total": len(self.findings),
"failed": sum(1 for f in self.findings if not f.ok),
},
}
def load_yaml(name: str) -> dict:
with open(CONFIG_DIR / name, encoding="utf-8") as f:
return yaml.safe_load(f) or {}
def probe_tcp(host: str, port: int, timeout: float = 4) -> tuple[bool, str]:
try:
with socket.create_connection((host, port), timeout=timeout):
return True, "open"
except Exception as e:
return False, str(e)
def probe_http(url: str, insecure: bool = False, timeout: float = 8) -> tuple[bool, str]:
try:
r = httpx.get(url, timeout=timeout, verify=not insecure, follow_redirects=True)
if r.status_code < 500:
return True, f"HTTP {r.status_code}"
return False, f"HTTP {r.status_code}"
except Exception as e:
return False, str(e)
def probe_proxmox(host: str, port: int = 8006) -> tuple[bool, str]:
ok, detail = probe_tcp(host, port)
if not ok:
return False, detail
try:
r = httpx.get(f"https://{host}:{port}/", timeout=6, verify=False)
return r.status_code in (200, 301, 302, 401, 403), f"HTTPS {r.status_code}"
except Exception as e:
return False, str(e)
def docker_container_states() -> list[Finding]:
"""Leest lokale Docker socket (NAS)."""
findings = []
try:
out = subprocess.run(
["docker", "ps", "-a", "--format", "{{.Names}}|{{.Status}}"],
capture_output=True,
text=True,
timeout=30,
)
if out.returncode != 0:
return [Finding("docker", "docker", False, out.stderr.strip() or "docker ps failed")]
for line in out.stdout.strip().splitlines():
if not line or "|" not in line:
continue
name, status = line.split("|", 1)
up = status.lower().startswith("up")
findings.append(
Finding(
"docker",
name,
up,
status,
{"exited": "exited" in status.lower()},
)
)
except FileNotFoundError:
findings.append(Finding("docker", "docker", False, "docker CLI niet beschikbaar"))
return findings
def lan_ping_watch(subnet: str, sample_hosts: list[str] | None = None) -> list[Finding]:
"""Lightweight: ping gateway + optioneel lijst — geen mass scan standaard."""
findings = []
gateway = ".".join(subnet.split(".")[:3]) + ".1"
ok, detail = _ping_once(gateway)
findings.append(Finding("lan", "gateway", ok, detail, {"ip": gateway}))
return findings
def _ping_once(ip: str) -> tuple[bool, str]:
try:
out = subprocess.run(
["ping", "-c", "1", "-W", "2", ip],
capture_output=True,
text=True,
timeout=5,
)
return out.returncode == 0, "reachable" if out.returncode == 0 else "no reply"
except Exception as e:
return False, str(e)
def run_observation() -> ObservationReport:
targets = load_yaml("targets.yaml")
findings: list[Finding] = []
nas = targets.get("nas") or {}
host = nas.get("host", "192.168.1.211")
for chk in nas.get("checks") or []:
name = chk.get("name", "check")
if chk.get("type") == "tcp":
ok, detail = probe_tcp(host, int(chk["port"]))
elif chk.get("type") == "http":
ok, detail = probe_http(chk["url"])
else:
ok, detail = False, "unknown check type"
findings.append(Finding("nas", name, ok, detail))
for px in targets.get("proxmox_hosts") or []:
ok, detail = probe_proxmox(px["host"], int(px.get("port", 8006)))
findings.append(Finding("proxmox", px.get("name", px["host"]), ok, detail))
for svc in targets.get("services") or []:
ok, detail = probe_http(svc["url"], insecure=bool(svc.get("insecure_tls")))
findings.append(Finding("service", svc.get("name", svc["url"]), ok, detail))
findings.extend(docker_container_states())
lan = targets.get("lan_watch") or {}
if lan.get("enabled"):
findings.extend(lan_ping_watch(lan.get("subnet", "192.168.1.0/24")))
return ObservationReport(timestamp=datetime.utcnow().isoformat() + "Z", findings=findings)
+152
View File
@@ -0,0 +1,152 @@
"""PostgreSQL — alle observaties voor dashboard :8765."""
from __future__ import annotations
import json
import logging
import os
from datetime import datetime, timezone
from typing import Any, Optional
import psycopg2
import psycopg2.extras
from agent.brain import AgentDecision
from agent.observer import ObservationReport
logger = logging.getLogger(__name__)
def _pg_enabled() -> bool:
return os.getenv("PG_DISABLED", "").lower() not in ("1", "true", "yes")
def _connect():
url = os.getenv("DATABASE_URL", "").strip()
if url:
return psycopg2.connect(url)
return psycopg2.connect(
host=os.getenv("PG_HOST", "192.168.1.105"),
port=int(os.getenv("PG_PORT", "5433")),
user=os.getenv("PG_USER", "mo"),
password=os.getenv("PG_PASSWORD", ""),
dbname=os.getenv("PG_DATABASE", "homelab"),
)
def _parse_ts(ts: str) -> datetime:
if ts.endswith("Z"):
ts = ts[:-1] + "+00:00"
return datetime.fromisoformat(ts)
def persist_observation(report: ObservationReport, decision: AgentDecision) -> Optional[int]:
"""Schrijf run + findings naar agent.*; retourneert run_id."""
if not _pg_enabled():
return None
summary = report.to_dict().get("summary") or {}
decision_json = {
"alert": decision.alert,
"severity": decision.severity,
"title": decision.title,
"body": decision.body,
"fingerprint": decision.fingerprint,
"actions": decision.actions,
}
try:
conn = _connect()
conn.autocommit = False
with conn.cursor() as cur:
cur.execute("SET search_path TO agent, public;")
cur.execute(
"""
INSERT INTO observation_runs
(observed_at, total_checks, failed_checks, summary, decision)
VALUES (%s, %s, %s, %s::jsonb, %s::jsonb)
RETURNING id
""",
(
_parse_ts(report.timestamp),
int(summary.get("total", len(report.findings))),
int(summary.get("failed", sum(1 for f in report.findings if not f.ok))),
json.dumps(report.to_dict(), default=str),
json.dumps(decision_json, default=str),
),
)
run_id = cur.fetchone()[0]
rows = [
(
run_id,
f.kind,
f.name,
f.ok,
f.detail,
json.dumps(f.meta or {}, default=str),
)
for f in report.findings
]
if rows:
psycopg2.extras.execute_batch(
cur,
"""
INSERT INTO findings (run_id, kind, name, ok, detail, meta)
VALUES (%s, %s, %s, %s, %s, %s::jsonb)
""",
rows,
)
conn.commit()
conn.close()
logger.info("PostgreSQL run %s: %d findings", run_id, len(rows))
return int(run_id)
except Exception as e:
logger.warning("PostgreSQL observatie mislukt: %s", e)
return None
def persist_incident(
run_id: Optional[int],
fingerprint: str,
severity: str,
title: str,
body: str,
notified: bool,
) -> None:
if not _pg_enabled():
return
try:
conn = _connect()
conn.autocommit = True
with conn.cursor() as cur:
cur.execute("SET search_path TO agent, public;")
cur.execute(
"""
INSERT INTO incidents (run_id, fingerprint, severity, title, body, notified)
VALUES (%s, %s, %s, %s, %s, %s)
""",
(run_id, fingerprint, severity, title, body, notified),
)
conn.close()
except Exception as e:
logger.warning("PostgreSQL incident mislukt: %s", e)
def was_notified_recently_pg(fingerprint: str, minutes: int) -> bool:
if not _pg_enabled():
return False
try:
conn = _connect()
with conn.cursor() as cur:
cur.execute("SET search_path TO agent, public;")
cur.execute(
"""
SELECT 1 FROM incidents
WHERE fingerprint = %s AND notified = true
AND created_at > NOW() - (%s || ' minutes')::interval
LIMIT 1
""",
(fingerprint, str(int(minutes))),
)
row = cur.fetchone()
conn.close()
return row is not None
except Exception:
return False
+75
View File
@@ -0,0 +1,75 @@
"""SQLite — geheugen van de agent (dedupe, incidenten)."""
import json
import sqlite3
from datetime import datetime, timedelta
from pathlib import Path
DB_PATH = Path(__file__).resolve().parent.parent / "data" / "agent.db"
def connect():
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
conn.executescript(
"""
CREATE TABLE IF NOT EXISTS incidents (
id INTEGER PRIMARY KEY AUTOINCREMENT,
fingerprint TEXT NOT NULL,
severity TEXT NOT NULL,
title TEXT NOT NULL,
body TEXT NOT NULL,
created_at TEXT NOT NULL,
notified INTEGER DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_fp ON incidents(fingerprint);
CREATE TABLE IF NOT EXISTS runs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
started_at TEXT NOT NULL,
summary TEXT,
raw_observations TEXT
);
"""
)
return conn
def was_notified_recently(fingerprint: str, minutes: int) -> bool:
since = (datetime.utcnow() - timedelta(minutes=minutes)).isoformat()
conn = connect()
row = conn.execute(
"SELECT 1 FROM incidents WHERE fingerprint=? AND notified=1 AND created_at>?",
(fingerprint, since),
).fetchone()
conn.close()
return row is not None
def record_incident(fingerprint: str, severity: str, title: str, body: str, notified: bool):
conn = connect()
conn.execute(
"INSERT INTO incidents (fingerprint, severity, title, body, created_at, notified) VALUES (?,?,?,?,?,?)",
(fingerprint, severity, title, body, datetime.utcnow().isoformat(), 1 if notified else 0),
)
conn.commit()
conn.close()
def recent_incidents(limit: int = 10) -> list[dict]:
conn = connect()
rows = conn.execute(
"SELECT severity, title, body, created_at FROM incidents ORDER BY id DESC LIMIT ?",
(limit,),
).fetchall()
conn.close()
return [dict(r) for r in rows]
def log_run(summary: str, observations: dict):
conn = connect()
conn.execute(
"INSERT INTO runs (started_at, summary, raw_observations) VALUES (?,?,?)",
(datetime.utcnow().isoformat(), summary, json.dumps(observations, default=str)),
)
conn.commit()
conn.close()
@@ -0,0 +1,42 @@
"""Telegram — enige externe meldkanaal."""
import os
import httpx
def send_message(text: str, parse_mode: str = "HTML") -> bool:
token = os.environ.get("TELEGRAM_BOT_TOKEN", "").strip()
chat_id = os.environ.get("TELEGRAM_CHAT_ID", "").strip()
if not token or not chat_id:
return False
url = f"https://api.telegram.org/bot{token}/sendMessage"
# Telegram max ~4096
if len(text) > 4000:
text = text[:3990] + ""
r = httpx.post(
url,
data={"chat_id": chat_id, "text": text, "parse_mode": parse_mode},
timeout=30,
)
return r.status_code == 200
def format_alert(severity: str, title: str, body: str, actions=None) -> str:
icon = {"critical": "🔴", "high": "🟠", "medium": "🟡", "low": "🔵", "info": ""}.get(
severity, ""
)
lines = [
f"{icon} <b>EL-KADI SECURITY</b> · {severity.upper()}",
f"<b>{_esc(title)}</b>",
"",
_esc(body),
]
if actions:
lines.append("")
lines.append("<b>Agent acties:</b>")
for a in actions:
lines.append(f"{_esc(a)}")
return "\n".join(lines)
def _esc(s: str) -> str:
return s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
@@ -0,0 +1,20 @@
# Agent-gedrag
interval_seconds: 300
quiet_hours:
start: "23:00"
end: "07:00"
timezone: Europe/Brussels
allow_severity: critical
dedupe_minutes: 30
severity_telegram:
- critical
- high
# Zonder LLM: regels
rules:
any_service_down: high
proxmox_unreachable: critical
nas_unreachable: critical
unknown_lan_device: medium
@@ -0,0 +1,44 @@
# Doelen die de agent zelf monitort (geen Wazuh/Uptime Kuma/n8n)
nas:
host: 192.168.1.211
checks:
- name: NAS SSH
type: tcp
port: 22
- name: Gitea
type: http
url: http://192.168.1.211:3000
- name: AdGuard
type: http
url: http://192.168.1.211:3001
proxmox_hosts:
- name: pve
host: 192.168.1.216
port: 8006
tls: true
- name: dell-proxmox
host: 192.168.1.56
port: 8006
tls: true
services:
- name: Homepage
url: http://192.168.1.192:3000
- name: Home Assistant
url: http://192.168.1.235:8123
- name: UniFi
url: https://192.168.1.24
insecure_tls: true
- name: Frigate
url: https://192.168.1.185:30058
insecure_tls: true
- name: Homelab Command
url: http://192.168.1.105:8765
# Optioneel: bekende apparaten op LAN (ARP/ping — geen externe SIEM)
lan_watch:
enabled: true
subnet: 192.168.1.0/24
# Bekende MACs → negeer of label (vul aan na eerste scan)
known_hosts: []
@@ -0,0 +1,20 @@
# Autonome home security agent — geen Wazuh/Uptime Kuma/n8n
services:
security-agent:
build: .
container_name: el-kadi-security-agent
restart: unless-stopped
network_mode: host
env_file:
- .env
environment:
PG_HOST: ${PG_HOST:-192.168.1.105}
PG_PORT: ${PG_PORT:-5433}
PG_USER: ${PG_USER:-mo}
PG_PASSWORD: ${PG_PASSWORD:-}
PG_DATABASE: ${PG_DATABASE:-homelab}
volumes:
- ./config:/app/config:ro
- ./data:/app/data
- /var/run/docker.sock:/var/run/docker.sock:ro
working_dir: /app
@@ -0,0 +1,51 @@
-- Home Security Agent — observaties voor dashboard :8765 (schema agent.*)
CREATE SCHEMA IF NOT EXISTS agent;
CREATE TABLE IF NOT EXISTS agent.observation_runs (
id bigserial PRIMARY KEY,
observed_at timestamptz NOT NULL,
total_checks integer NOT NULL DEFAULT 0,
failed_checks integer NOT NULL DEFAULT 0,
summary jsonb NOT NULL DEFAULT '{}',
decision jsonb,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS ix_agent_runs_observed
ON agent.observation_runs (observed_at DESC);
CREATE TABLE IF NOT EXISTS agent.findings (
id bigserial PRIMARY KEY,
run_id bigint NOT NULL REFERENCES agent.observation_runs(id) ON DELETE CASCADE,
kind text NOT NULL,
name text NOT NULL,
ok boolean NOT NULL,
detail text,
meta jsonb NOT NULL DEFAULT '{}',
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS ix_agent_findings_run ON agent.findings (run_id);
CREATE INDEX IF NOT EXISTS ix_agent_findings_failed ON agent.findings (run_id, ok) WHERE NOT ok;
CREATE TABLE IF NOT EXISTS agent.incidents (
id bigserial PRIMARY KEY,
run_id bigint REFERENCES agent.observation_runs(id) ON DELETE SET NULL,
fingerprint text NOT NULL,
severity text NOT NULL,
title text NOT NULL,
body text,
notified boolean NOT NULL DEFAULT false,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS ix_agent_incidents_created
ON agent.incidents (created_at DESC);
CREATE INDEX IF NOT EXISTS ix_agent_incidents_fp
ON agent.incidents (fingerprint, created_at DESC);
GRANT USAGE ON SCHEMA agent TO PUBLIC;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA agent TO PUBLIC;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA agent TO PUBLIC;
@@ -0,0 +1,5 @@
httpx==0.28.1
pyyaml==6.0.2
openai==1.58.1
python-dotenv==1.0.1
psycopg2-binary==2.9.10
+6 -6
View File
@@ -207,9 +207,9 @@
- HA Voice Ctrl: - HA Voice Ctrl:
icon: home-assistant.png icon: home-assistant.png
href: http://192.168.1.211:8765 href: http://192.168.1.105:8765
description: HA Voice Ctrl description: HA Voice Ctrl
siteMonitor: http://192.168.1.211:8765 siteMonitor: http://192.168.1.105:8765
statusStyle: dot statusStyle: dot
- Productivity: - Productivity:
@@ -302,9 +302,9 @@
- Neo4j Browser: - Neo4j Browser:
icon: neo4j.png icon: neo4j.png
href: http://192.168.1.211:49154 href: http://192.168.1.105:49154
description: Neo4j Browser description: Neo4j Browser
siteMonitor: http://192.168.1.211:49154 siteMonitor: http://192.168.1.105:49154
statusStyle: dot statusStyle: dot
- OnlyOffice: - OnlyOffice:
@@ -478,9 +478,9 @@
- Home Control: - Home Control:
icon: mdi-server-network-#14b8a6 icon: mdi-server-network-#14b8a6
href: http://192.168.1.211:8765/dashboard#live href: http://192.168.1.105:8765/dashboard#live
description: Home Control description: Home Control
siteMonitor: http://192.168.1.211:8765 siteMonitor: http://192.168.1.105:8765
statusStyle: dot statusStyle: dot
- Web Design: - Web Design:
+1 -1
View File
@@ -14,7 +14,7 @@ services:
POSTGRES_DATABASE: ${JOPLIN_DB:-joplin} POSTGRES_DATABASE: ${JOPLIN_DB:-joplin}
POSTGRES_USER: ${POSTGRES_USER:-mo} POSTGRES_USER: ${POSTGRES_USER:-mo}
POSTGRES_PORT: 5432 POSTGRES_PORT: 5432
POSTGRES_HOST: postgres-homelab POSTGRES_HOST: ${POSTGRES_HOST:-192.168.1.105}
depends_on: depends_on:
- joplin-db - joplin-db
+1 -1
View File
@@ -41,7 +41,7 @@ services:
ports: ports:
- "${POSTGRES_EXPORTER_PORT:-9187}:9187" - "${POSTGRES_EXPORTER_PORT:-9187}:9187"
environment: environment:
DATA_SOURCE_NAME: "postgresql://${PG_USER:-mo}:${PG_PASSWORD}@postgres-homelab:5432/${PG_DATABASE:-homelab}?sslmode=disable" DATA_SOURCE_NAME: "postgresql://${PG_USER:-mo}:${PG_PASSWORD}@${PG_HOST:-192.168.1.105}:${PG_PORT:-5433}/${PG_DATABASE:-homelab}?sslmode=disable"
networks: networks:
- homelab-monitor - homelab-monitor
+2 -2
View File
@@ -12,14 +12,14 @@ scrape_configs:
static_configs: static_configs:
- targets: ["postgres-exporter-homelab:9187"] - targets: ["postgres-exporter-homelab:9187"]
labels: labels:
instance: postgres-homelab instance: postgres-vm102
# Neo4j 4.4+ enterprise metrics.prometheus.enabled → endpoint op poort 2004 # Neo4j 4.4+ enterprise metrics.prometheus.enabled → endpoint op poort 2004
- job_name: neo4j - job_name: neo4j
scrape_interval: 30s scrape_interval: 30s
metrics_path: /metrics metrics_path: /metrics
static_configs: static_configs:
- targets: ["neo4j:2004"] - targets: ["192.168.1.105:2004"]
labels: labels:
instance: neo4j instance: neo4j
+2 -1
View File
@@ -2,7 +2,8 @@
| | | | | |
|---|---| |---|---|
| **Productie** | **192.168.1.105** — Bolt :49153, Browser :49154 (`~/neo4j/` op VM 102) |
| **NAS compose** | Oude map; stack verplaatst naar VM 102 |
| **Poort** | 49153 | | **Poort** | 49153 |
| **Start** | `docker compose up -d` |
Zie [apps/README.md](../README.md) en [RESTORE.md](../../RESTORE.md). Zie [apps/README.md](../README.md) en [RESTORE.md](../../RESTORE.md).
+1
View File
@@ -22,6 +22,7 @@ services:
# Masquerade root URL voor nginx reverse proxy # Masquerade root URL voor nginx reverse proxy
- PGADMIN_CONFIG_SERVER_MODE=True - PGADMIN_CONFIG_SERVER_MODE=True
- PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED=False - PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED=False
- PGADMIN_SERVER_JSON_FILE=/pgadmin4/servers.json
volumes: volumes:
- pgadmin-data:/var/lib/pgadmin # persistentie: server lijst, instellingen - pgadmin-data:/var/lib/pgadmin # persistentie: server lijst, instellingen
+18 -3
View File
@@ -1,8 +1,23 @@
{ {
"Servers": { "Servers": {
"1": { "1": {
"Name": "Homelab PostgreSQL", "Name": "Homelab PostgreSQL (VM102)",
"Group": "Servers", "Group": "Homelab",
"Host": "192.168.1.105",
"Port": 5433,
"MaintenanceDB": "homelab",
"Username": "mo",
"Password": "WaQTUw2t",
"SSLMode": "prefer",
"PassFile": "",
"SSLCert": "",
"SSLKey": "",
"SSLRootCert": "",
"Comment": "Proxmox VM 102 Postgress — productie homelab DB"
},
"2": {
"Name": "Homelab PostgreSQL NAS backup",
"Group": "Homelab",
"Host": "192.168.1.211", "Host": "192.168.1.211",
"Port": 5433, "Port": 5433,
"MaintenanceDB": "homelab", "MaintenanceDB": "homelab",
@@ -13,7 +28,7 @@
"SSLCert": "", "SSLCert": "",
"SSLKey": "", "SSLKey": "",
"SSLRootCert": "", "SSLRootCert": "",
"Comment": "Synology NAS — Homelab dashboard database" "Comment": "Oude NAS-kopie — rollback / vergelijken"
} }
} }
} }
+2 -1
View File
@@ -2,7 +2,8 @@
| | | | | |
|---|---| |---|---|
| **Productie** | **192.168.1.105:5433** (VM 102, `~/homelab-postgres/`) |
| **NAS backup** | 192.168.1.211:5433 (`docker compose up -d` in deze map) |
| **Poort** | 5433 | | **Poort** | 5433 |
| **Start** | `docker compose up -d` |
Zie [apps/README.md](../README.md) en [RESTORE.md](../../RESTORE.md). Zie [apps/README.md](../README.md) en [RESTORE.md](../../RESTORE.md).