Zum Inhalt springen

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

Aus LHlab Wiki

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

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_TZ setzen (und optional PW_TS_INCLUDE_UTC=1).
  • Zu viele Alerts: PW_RL_COOLDOWN_S erhöhen; Ignorierlisten (PW_NOTIFY_IGNORE_*) nutzen.
  • Healthcheck erzeugt Meldungen: PW_NOTIFY_IGNORE_LOOPBACK=1 setzen; optional PW_LOG_IGNORE_SUPPRESSED=1.

Mehrere Ports überwachen?

Portwatcher mehrfach starten (unterschiedliche PW_PORT-Werte und ggf. Benachrichtigungsthemen).