# caddy-opnsense-blocker `caddy-opnsense-blocker` is a local-first daemon that ingests Caddy access logs in their default JSON format, evaluates suspicious requests, keeps persistent local state in SQLite, provides a lightweight web UI for review, and blocks or unblocks IP addresses through an OPNsense alias. ## Features - Real-time ingestion of multiple Caddy JSON log files. - One heuristic profile per log source. - Persistent local state in SQLite. - Local-only web UI for reviewing events, IPs, and the full request history of a selected address. - On-demand IP investigation with persistent caching for bot verification, reverse DNS, RDAP, and Spamhaus lookups. - Manual block, unblock, and clear-override actions with OPNsense-aware UI state. - OPNsense alias backend with automatic alias creation. - Concurrent polling across multiple log files. ## Current scope This first version is intentionally strong on ingestion, persistence, UI, and OPNsense integration. The decision engine is deliberately simple and deterministic for now: - suspicious path prefixes - unexpected `POST` requests - `.php` path detection - explicit known-agent allow/deny rules - excluded CIDR ranges - manual overrides This keeps the application usable immediately while leaving room for a more advanced policy engine later. ## Architecture - `internal/caddylog`: parses default Caddy JSON access logs - `internal/engine`: evaluates requests against a profile - `internal/investigation`: performs on-demand bot verification and IP enrichment - `internal/store`: persists events, IP state, manual decisions, backend actions, and source offsets - `internal/opnsense`: manages the target OPNsense alias through its API - `internal/service`: runs concurrent log followers and applies automatic decisions - `internal/web`: serves the local review UI and JSON API ## License This project is licensed under the MIT License. See `LICENSE`. ## Quick start 1. Generate or provision OPNsense API credentials. 2. Copy `config.example.yaml` to `config.yaml` and adapt it. 3. Start the daemon: ```bash CGO_ENABLED=0 go run ./cmd/caddy-opnsense-blocker -config ./config.yaml ``` 4. Open the local UI on the configured address, for example `http://127.0.0.1:9080`. ## Example configuration See `config.example.yaml`. Important points: - Each source points to one Caddy log file. - Each source references exactly one profile. - `initial_position: end` means “start following new lines only” on first boot. - The `investigation` section controls how long IP enrichment is cached and whether on-demand Spamhaus lookups are enabled. - The web UI should stay bound to a local address such as `127.0.0.1:9080`. ## Web UI and API The web UI is intentionally small and server-rendered. It refreshes through lightweight JSON polling and exposes these endpoints: - `GET /healthz` - `GET /api/overview` - `GET /api/events` - `GET /api/ips` - `GET /api/ips/{ip}` - `POST /api/ips/{ip}/investigate` - `POST /api/ips/{ip}/block` - `POST /api/ips/{ip}/unblock` - `POST /api/ips/{ip}/clear-override` The legacy `POST /api/ips/{ip}/reset` endpoint is still accepted as a backwards-compatible alias for `clear-override`. ## Development Run the test suite: ```bash CGO_ENABLED=0 go test ./... ``` Build the daemon: ```bash CGO_ENABLED=0 go build ./cmd/caddy-opnsense-blocker ``` `CGO_ENABLED=0` is useful on systems without a C toolchain. The application itself only relies on pure-Go dependencies. ## Nix packaging The repository ships with first-class Nix files: - `package.nix`: reusable package definition - `default.nix`: convenience entry point for `nix-build` - `module.nix`: reusable NixOS module Build the package directly from the repository root: ```bash nix-build ``` Use the NixOS module from another configuration: ```nix { imports = [ /path/to/caddy-opnsense-blocker/module.nix ]; services.caddy-opnsense-blocker = { enable = true; credentials.opnsenseApiKeyFile = "/run/secrets/opnsense-api-key"; credentials.opnsenseApiSecretFile = "/run/secrets/opnsense-api-secret"; settings = { opnsense = { enabled = true; base_url = "https://router.example.test"; ensure_alias = true; alias.name = "blocked-ips"; }; profiles.public-web = { auto_block = true; block_unexpected_posts = true; block_php_paths = true; suspicious_path_prefixes = [ "/wp-admin" "/wp-login.php" "/.env" ]; }; sources = [ { name = "public-web"; path = "/var/log/caddy/public-web.json"; profile = "public-web"; } ]; }; }; } ``` ## Roadmap - richer decision engine - optional GeoIP and ASN providers beyond RDAP - richer review filters in the UI - alternative blocking backends besides OPNsense - direct streaming ingestion targets in addition to file polling