2

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 with a sortable “Recent IPs” view for the last 24 hours and a full request history for each 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:
CGO_ENABLED=0 go run ./cmd/caddy-opnsense-blocker -config ./config.yaml
  1. 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/recent-ips?hours=24
  • 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:

CGO_ENABLED=0 go test ./...

Build the daemon:

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:

nix-build

Use the NixOS module from another configuration:

{
  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
Description
Real-time Caddy log ingestion with manual review and OPNsense blocking
Readme 1.5 MiB
Languages
Go 97.6%
Nix 1.9%
JavaScript 0.5%