Portwatcher – TCP-Verbindungen überwachen, Scans erkennen, sofort alarmieren

Portwatcher ist ein schlanker TCP-Watcher (Python/asyncio) für Server, NAS oder Raspberry Pi. Er lauscht auf einem frei wählbaren TCP-Port, protokolliert eingehende Verbindungen und benachrichtigt per E-Mail und ntfy Push. Mit Offline-Geolokalisierung (MaxMind GeoLite2), optionalem rDNS, Cooldown pro IP, Healthcheck-/CIDR-Ignore sowie Payload-Snippet & Protokoll-Erkennung (HTTP/TLS/SSH/…) erhältst du schnell Kontext zu Scans und Zugriffen – ohne schwergewichtiges SIEM.
Was ist Portwatcher?
Portwatcher ist ein kleiner Dienst, der auf einem TCP-Port lauscht und jeden Verbindungsversuch erfasst. Für jeden Treffer können Benachrichtigungen ausgelöst werden; Metadaten wie Land, Stadt/Region, ASN/Provider und rDNS werden angereichert. Optional wird ein kurzes Payload-Snippet mit einfacher Protokoll-Erkennung (z. B. HTTP-Methode/Host, TLS-SNI, SSH-Banner) mitgeschickt.
Haupt-Features
- Benachrichtigungen: E-Mail (SMTP) & ntfy (einzeln aktivierbar)
- Geo offline: MaxMind GeoLite2-City/ASN (Sidecar aktualisiert DBs automatisch)
- rDNS (kurzer Timeout), Cooldown pro IP, Ignore (Loopback/Private/CIDR)
- Payload-Snippet & Protokoll-Erkennung (HTTP/TLS/SSH/…)
- Timezone-aware Timestamps (Standard:
Europe/Berlin) - Fertige Docker-Images (GHCR), lauffähig auf x86_64 & ARM64 (Raspberry Pi)
Typische Einsatzzwecke
- Sichtbarkeit & Alarmierung für freigegebene Ports (Heimnetz/Server)
- Honeypot-Light: Wer klopft an? Von wo? Mit welchem Client?
- Forensik: Kontext (rDNS/ASN/Land) & erste Payload-Bytes zur schnellen Einordnung
Architektur (kurz)
- portwatcher (App-Container): Lauscht auf TCP-Port, führt Geo/rDNS/Heuristiken aus, versendet Benachrichtigungen.
- geoipupdate (Sidecar): Lädt/aktualisiert MaxMind-Datenbanken periodisch in ein gemeinsames Volume.
[geoipupdate] ──writes──> [volume: /usr/share/GeoIP] <──reads── [portwatcher]
Voraussetzungen
- Docker & Docker Compose
- MaxMind-Account (GeoLite2, kostenlos) → Account-ID & License Key
- SMTP-Zugang (für E-Mail) und/oder ein ntfy-Topic
- Zugriff auf die fertigen Images (GHCR):
- App:
ghcr.io/alaub81/portwatcher:latest - Sidecar:
ghcr.io/maxmind/geoipupdate:latest
- App:
Installation & Nutzung mit Docker Compose
1) Konfigurationsdateien anlegen
Empfohlene Struktur:
portwatcher/ ├─ docker-compose.yml ├─ .env # Port Konfiguration und TimeZone (nur allgemeine Variablen) ├─ portwatcher.env # App-Konfiguration (PW_*) └─ geoipupdate.env # MaxMind-Zugangsdaten und Konfiguration
.env
# -------- .env.example (for Portwatcher) -------- # NOTE: Do NOT put inline comments after values (e.g. KEY=123 # bad). # Use separate comment lines like this instead. # --- Container Basics --- # Portwatcher Port PW_PORT=5555 # Timezone Configuration PW_TZ=Europe/Berlin
portwatcher.env
# -------- portwatcher.env.example (for Portwatcher) --------
# NOTE: Do NOT put inline comments after values (e.g. KEY=123 # bad).
# Use separate comment lines like this instead.
# --- Basics ---
PW_HOST=0.0.0.0
PW_MAX_CONCURRENCY=200
PW_LOG_LEVEL=INFO
# Optional banner text sent to clients (raw, no escape interpretation)
PW_BANNER=
# --- Ignore rules (healthchecks / local nets) ---
PW_NOTIFY_IGNORE_LOOPBACK=1
PW_LOG_IGNORE_SUPPRESSED=1
# PW_NOTIFY_IGNORE_PRIVATES=1
# PW_NOTIFY_IGNORE_CIDRS=172.24.0.0/16,::1/128
# --- Payload capture & display ---
PW_CAPTURE_PAYLOAD=1
PW_PROBE_MAX_BYTES=2048
PW_PROBE_TIMEOUT_MS=800
PW_PAYLOAD_IN_NOTIF=1
PW_PAYLOAD_MAX_CHARS=600
PW_PAYLOAD_MODE=auto # allowed: auto|text|hex|base64
PW_PAYLOAD_STRIP_CONTROL=1
# PW_TLS_JA3=1
# --- Channels ---
PW_ENABLE_EMAIL=1
PW_ENABLE_PUSH=1
# --- SMTP (E-Mail) ---
PW_SMTP_SERVER=mx.example.org
PW_SMTP_PORT=587
PW_SMTP_STARTTLS=1
PW_SMTP_USER=user@example.org
PW_SMTP_PASS=CHANGE_ME_STRONG_PASSWORD
# Alternative: use a Docker secret instead of PW_SMTP_PASS
# PW_SMTP_PASS_FILE=/run/secrets/smtp_pass
PW_MAIL_FROM=portwatch@example.org
PW_MAIL_TO=you@example.org
# --- ntfy ---
PW_NTFY_SERVER=https://ntfy.sh
PW_NTFY_TOPIC=your-ntfy-topic
PW_NTFY_PRIORITY=5
PW_NTFY_TAGS=rotating_light,shield
# --- Geo (offline) ---
PW_GEOIP_CITY_DB=/usr/share/GeoIP/GeoLite2-City.mmdb
PW_GEOIP_ASN_DB=/usr/share/GeoIP/GeoLite2-ASN.mmdb
# --- Geo (online fallback) ---
PW_IPAPI_ENABLE=0
PW_IPAPI_BUDGET_PER_MIN=40
# --- rDNS ---
PW_RDNS_ENABLE=1
PW_RDNS_TIMEOUT=1.0
# --- Detail fields ---
PW_INCLUDE_CITY=1
PW_INCLUDE_COORDS=0
PW_LATLON_PRECISION=2
# --- Rate-limit & cache ---
PW_RL_COOLDOWN_S=1800
PW_RL_FORGET_S=86400
PW_CACHE_SIZE=20000
# --- Time ---
PW_TS_INCLUDE_UTC=0
# --- Optional: MaxMind-Download im Container (RUN_GEOIPUPDATE=1 einschalten) ---
RUN_GEOIPUPDATE=1
GEOIPUPDATE_ACCOUNT_ID=YOURID
GEOIPUPDATE_LICENSE_KEY=example_license_key_not_real
GEOIPUPDATE_EDITION_IDS=GeoLite2-City GeoLite2-ASN
geoipupdate.env
# -------- geoipupdate.env.example -------- # Example configuration for the GeoIP sidecar (MaxMind). # This file does NOT contain real credentials. # Copy it to geoipupdate.env and fill in your real values. # # Documentation: https://github.com/maxmind/geoipupdate # MaxMind credentials (replace with your real values) GEOIPUPDATE_ACCOUNT_ID=YOUR_MAXMIND_ACCOUNT_ID GEOIPUPDATE_LICENSE_KEY=YOUR_MAXMIND_LICENSE_KEY # Databases to download (space-separated) GEOIPUPDATE_EDITION_IDS=GeoLite2-City GeoLite2-ASN # Storage path INSIDE the container. # Must match the mount in docker-compose.yml: # geoipupdate -> volumes: - geoip-db:/usr/share/GeoIP GEOIPUPDATE_DB_DIR=/usr/share/GeoIP # Update frequency in hours (e.g., 24 = once per day) GEOIPUPDATE_FREQUENCY=24 # Optional: proxy settings (uncomment if needed) # HTTP_PROXY=http://user:pass@proxyhost:3128 # HTTPS_PROXY=http://user:pass@proxyhost:3128
2) docker-compose.yml (fertige Images, keine Builds)
services:
geoipupdate:
image: ghcr.io/maxmind/geoipupdate:latest
restart: unless-stopped
env_file:
- geoipupdate.env
environment:
TZ: ${PW_TZ}
volumes:
- geoip-db:/usr/share/GeoIP
healthcheck:
test: ["CMD", "sh", "-c", "test -f /usr/share/GeoIP/GeoLite2-City.mmdb -a -f /usr/share/GeoIP/GeoLite2-ASN.mmdb"]
interval: 1m
timeout: 5s
retries: 3
start_period: 30s
portwatcher:
image: ghcr.io/alaub81/portwatcher:latest
restart: unless-stopped
env_file:
- portwatcher.env
- .env
environment:
TZ: ${PW_TZ}
# vermeidet __pycache__-Writes auf read-only FS
PYTHONDONTWRITEBYTECODE: "1"
ports:
- "${PW_PORT:-5555}:${PW_PORT:-5555}/tcp"
volumes:
- geoip-db:/usr/share/GeoIP
user: "10001:10001"
read_only: true
tmpfs:
- /tmp
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
ulimits:
nofile: 16384
depends_on:
geoipupdate:
#condition: service_started # oder 'service_healthy' falls HC gesetzt
condition: service_healthy
healthcheck:
test: ["CMD", "/bin/sh", "/app/healthcheck.sh"]
interval: 30s
timeout: 5s
retries: 3
logging:
options:
max-size: "10m"
max-file: "3"
volumes:
geoip-db:
3) Start & Verifikation
docker compose pull docker compose up -d docker compose logs -f portwatcher
- Prüfen, ob Geo-DBs vorhanden sind:
docker exec -it portwatcher-geoipupdate-1 ls -lh /usr/share/GeoIP
- Test (z. B. HTTP):
curl -v http://<DEINE-IP>:5555/
Hinweis: Healthcheck-Zugriffe (127.0.0.1) werden mit PW_NOTIFY_IGNORE_LOOPBACK=1 nicht benachrichtigt und mit PW_LOG_IGNORE_SUPPRESSED=1 nicht geloggt.
Updates & Betrieb
# Auf neue Container-Versionen aktualisieren docker compose pull docker compose up -d # Anonyme Volumes aufräumen (falls jemals entstanden) docker volume ls -qf dangling=true | xargs -r docker volume rm
Troubleshooting
- Geo-Felder leer: Ist das Volume korrekt gemountet (
/usr/share/GeoIP)? Sidecar healthy? - Anonyme Volumes: Entstehen, wenn
/usr/share/GeoIP(im Image als VOLUME) nicht explizit gemountet wird → im Compose wie oben ein benanntes Volume verwenden. - Zeit/Zeitzone falsch:
PW_TZsetzen (und optionalPW_TS_INCLUDE_UTC=1). - Zu viele Alerts:
PW_RL_COOLDOWN_Serhöhen; Ignorierlisten (PW_NOTIFY_IGNORE_*) nutzen. - Healthcheck erzeugt Meldungen:
PW_NOTIFY_IGNORE_LOOPBACK=1setzen; optionalPW_LOG_IGNORE_SUPPRESSED=1.
Mehrere Ports überwachen?
Portwatcher mehrfach starten (unterschiedliche PW_PORT-Werte und ggf. Benachrichtigungsthemen).