{"id":50432891,"url":"https://github.com/styliteag/dashboard","last_synced_at":"2026-07-02T01:02:04.860Z","repository":{"id":354413102,"uuid":"1207002390","full_name":"styliteag/dashboard","owner":"styliteag","description":"STYLiTE Orbit Dashboard for managing multiple firewalls like OPNsense, pfSense","archived":false,"fork":false,"pushed_at":"2026-06-26T01:03:18.000Z","size":1022,"stargazers_count":0,"open_issues_count":5,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-26T02:22:23.175Z","etag":null,"topics":["dashboard","opnsense","orbit","pfsense"],"latest_commit_sha":null,"homepage":"https://blog.stylite.de","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/styliteag.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-04-10T13:24:46.000Z","updated_at":"2026-06-26T01:03:20.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/styliteag/dashboard","commit_stats":null,"previous_names":["styliteag/dashboard"],"tags_count":13,"template":false,"template_full_name":null,"purl":"pkg:github/styliteag/dashboard","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/styliteag%2Fdashboard","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/styliteag%2Fdashboard/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/styliteag%2Fdashboard/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/styliteag%2Fdashboard/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/styliteag","download_url":"https://codeload.github.com/styliteag/dashboard/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/styliteag%2Fdashboard/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35028642,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-01T02:00:05.325Z","response_time":130,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["dashboard","opnsense","orbit","pfsense"],"created_at":"2026-05-31T15:01:59.351Z","updated_at":"2026-07-02T01:02:04.854Z","avatar_url":"https://github.com/styliteag.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# STYLiTE Orbit Dashboard\n\nCentral dashboard for monitoring and managing a fleet of **OPNsense, pfSense, and\nSecurepoint UTM** firewalls from one place — OPNsense/pfSense including sites behind\nNAT (reached through an outbound push agent), Securepoint polled directly over its API.\n\n## TL;DR\n\n- **What:** one dashboard for a fleet of OPNsense, pfSense, and **Securepoint UTM**\n  firewalls — live status, IPsec/VPN, gateways, firmware compliance, service checks,\n  audit log, notifications.\n- **How boxes connect:** `direct` (dashboard → box API) or `push` (box → outbound\n  `wss://`, a stdlib-only FreeBSD agent). Push works behind NAT with no inbound access\n  and no stored API key; the agent also tunnels the box's REST API (**relay**) and its\n  web GUI (**GUI proxy**). **Securepoint** is direct-poll/agentless (its `spcgi.cgi`\n  JSON API, with optional SSH enrichment for richer IPsec data).\n- **Agent lifecycle:** dashboard-triggered, signature-verified (Ed25519) **self-update**\n  with two-layer rollback, one-time **enrollment**, **uninstall**. Config (e.g. IPsec\n  ping monitors) is pushed on every (re)connect — the agent persists nothing, the DB is\n  the source of truth.\n- **Stack:** FastAPI + async SQLAlchemy on **MariaDB**, React 18 + Vite frontend, pure-\n  stdlib agent. Ships as a single combined Docker image; TLS is operator-side.\n- **Run it:** `cp .env.example .env`, set the secrets, `just up` (or `docker compose up`).\n  Dev: `just dev-up`. See [Quickstart](#quickstart-production).\n\n## What it does\n\n- **Instances** — register firewalls, see live status (CPU, memory, disk, uptime,\n  interfaces and throughput) and recent history.\n- **VPN / IPsec overview** — tunnel state across the fleet, with human-readable\n  connection names pulled from each box's `config.xml`; restart a tunnel from the UI.\n- **Gateways** — per-gateway up/down and latency.\n- **Firmware compliance** — which boxes are up to date, which have updates pending;\n  check (and stage) updates in bulk.\n- **Service checks** — each box rolled up to OK / WARN / CRIT per service, exported\n  for **Checkmk/OMD** (one piggyback host per firewall, no agent on the box).\n- **Bulk actions + CSV export** — run `firmware_check` / `ipsec_restart` across many\n  instances in parallel and export the results.\n- **Audit log** — who did what, when.\n- **Notifications** — webhook, Telegram, and ntfy on state changes (all optional).\n- **Read-only API keys** — service-account auth for Checkmk and other integrations.\n\n## How firewalls connect\n\nTransport and device type are decoupled. Two paths are in use today:\n\n| Transport | Who initiates | Use |\n|---|---|---|\n| `direct` | Dashboard → firewall API | Firewall directly reachable from the dashboard (OPNsense/pfSense REST API, or **Securepoint** `spcgi.cgi`) |\n| `push` | Firewall → outbound `wss://…/api/ws/agent` | **Primary for OPNsense/pfSense behind NAT** |\n\nIn **push** mode a small stdlib-only Python agent runs on the firewall (FreeBSD),\nopens an outbound WebSocket to the dashboard, and pushes metrics on an interval. It\nalso exposes an optional **relay** — the dashboard tunnels HTTP requests to the box's\nown REST API through the agent connection, so the dashboard needs no inbound access\nand no stored API key. The agent supports dashboard-triggered **self-update**,\none-time **enrollment** (trade a code for a token), and **uninstall**. See\n[`docs/agent-architecture.md`](docs/agent-architecture.md) for the full design.\n\n**Securepoint UTM** boxes are **direct-poll only** — no on-box agent. The dashboard\nmaps the appliance's `spcgi.cgi` JSON API (session-auth) onto the same `DeviceClient`\ncontract as the others, so VPN/IPsec and service status surface in the same views.\nOptionally, enabling **SSH enrichment** lets the dashboard pull IPsec via\n`swanctl --raw` for richer detail (SPIs, cookies, byte counters) the `spcgi.cgi` API\ndoesn't expose. Agent-only features (relay, GUI proxy, on-box ping monitors,\nself-update) don't apply to Securepoint.\n\n## Stack\n\n- **Backend:** Python 3.12, FastAPI, SQLAlchemy 2 (async), Alembic, httpx, APScheduler,\n  structlog. Managed with [`uv`](https://docs.astral.sh/uv/), `src/`-layout package.\n- **Frontend:** React 18 + TypeScript, Vite, Tailwind, TanStack Query, React Router,\n  Recharts.\n- **Database:** **MariaDB 11** (async via `aiomysql`). Metrics retention and a 5-minute\n  rollup are handled by in-process scheduler jobs — no TimescaleDB.\n- **Agent:** pure-stdlib Python (no pip dependencies), runs on OPNsense/pfSense (FreeBSD).\n- **Container:** single combined image — nginx serves the built frontend on `:80` and\n  proxies `/api/` to uvicorn at `127.0.0.1:8000` inside the same container.\n- **Deployment:** Docker Compose. TLS is operator-side (host reverse proxy, cloud LB).\n\n## Quickstart (production)\n\nPrerequisites: Docker, Docker Compose, and (optionally) [`just`](https://github.com/casey/just).\n\n```bash\n# 1. Configure secrets\ncp .env.example .env\njust gen-key            # paste output into DASH_MASTER_KEY in .env\n# also set DB_PASSWORD, DB_ROOT_PASSWORD, and DASH_ADMIN_PASSWORD\n\n# 2. Start the stack (MariaDB + combined app image)\njust up                 # or: docker compose up -d --build\n\n# 3. Open\n# http://localhost  (DASH_PORT in .env to remap)\n```\n\nTo pull a published image instead of building locally, edit `compose.yml` — swap the\n`build:` block under `app` for `image: ghcr.io/styliteag/dashboard:latest`.\n\n## Connecting a firewall via the push agent\n\nOn the OPNsense/pfSense box (FreeBSD):\n\n```sh\n# Copy the agent/ files to the box, then:\nsh install.sh\nvi /usr/local/etc/orbit-agent.conf   # set dashboard_url + agent_token (or enroll_code)\nsysrc orbit_agent_enable=YES\nservice orbit_agent start\ntail -f /var/log/orbit_agent.log\n```\n\nThe agent auto-discovers the box's own GUI/API port from `config.xml` (it does not\nassume 443/4444). Config reference: [`agent/orbit-agent.conf.example`](agent/orbit-agent.conf.example).\n\n## Firewall GUI proxy (optional)\n\nReach a NAT'd firewall's **web GUI** through its agent — no inbound access or VPN.\nThe dashboard tunnels raw TCP to the firewall over the agent's WebSocket; a reverse\nproxy in front gives each firewall a **per-instance origin** (so the GUI's absolute\nURLs resolve) and a valid TLS cert. The browser speaks TLS end-to-end with the\nfirewall — nothing is rewritten, so AJAX/forms/live views work. Access is gated by a\none-time handoff from your dashboard session → an origin-scoped cookie checked on\nevery request (`forwardAuth`), bound to that one firewall.\n\n**Off by default.** It needs the reverse proxy set up, so enable it only then:\n\n```sh\nDASH_GUI_PROXY_ENABLED=true\nDASH_GUI_BASE_TEMPLATE=https://gui-{slug}.example.com # prod; {slug} = instance slug\nDASH_GUI_IDLE_MINUTES=15                               # close idle forwarders\n```\n\nWith it on, instance pages show an **Open GUI** button (→ new tab). Leave it `false`\nand the button is hidden — no wildcard/DNS needed.\n\n- **Dev** (ports, no wildcard): `just dev` runs Caddy ([`docker/Caddyfile.dev`](docker/Caddyfile.dev))\n  mapping `https://localhost:900\u003cid\u003e` → instance `\u003cid\u003e`'s forwarder. Already enabled\n  in `compose-dev.yml`. Accept Caddy's internal-CA cert once.\n- **Prod, behind Traefik** (wildcard subdomain): Orbit ships its own `gui-proxy`\n  Caddy (in `compose.yml`, `--profile gui`). Your **external Traefik** terminates TLS\n  for `*.gui.example.com` (DNS-01 wildcard cert) and forwards the wildcard to that\n  Caddy over HTTP — see [`docker/traefik-gui.example.yml`](docker/traefik-gui.example.yml).\n  Caddy host-matches `gui-\u003cslug\u003e`, runs the `forwardAuth` gate, and proxies to that\n  firewall's forwarder (`app:14400+id`), so Traefik needs **no per-instance config**.\n  Set `ORBIT_GUI_DOMAIN=gui.example.com`, `DASH_GUI_PROXY_ENABLED=true`,\n  `DASH_GUI_BASE_TEMPLATE=https://gui-{slug}.gui.example.com`,\n  `DASH_GUI_CADDY_ADMIN_URL=http://gui-proxy:2019/load`, attach `gui-proxy` to\n  Traefik's network, then `docker compose --profile gui up -d`.\n\n  \u003e **`DASH_GUI_CADDY_ADMIN_URL` is required** — it's how the backend pushes the\n  \u003e vhost map to Caddy. The bundled `compose.yml` defaults it for you\n  \u003e (`${DASH_GUI_CADDY_ADMIN_URL:-http://gui-proxy:2019/load}`), but a **hand-written\n  \u003e compose / Swarm stack has no such default** — you must set it explicitly. If it's\n  \u003e unset while the proxy is enabled, the hot-load **silently no-ops**: Caddy stays on\n  \u003e the empty bootstrap and every `gui-\u003cslug\u003e` host returns a blank `200`. The backend\n  \u003e logs `gui_caddy.admin_url_unset` at startup when this happens.\n\n  Each instance gets a **persistent, URL-safe `slug`** (auto-derived from its name —\n  \"Firewall Büro Süd\" → `firewall-buero-sued`, editable, unique). Because the host is\n  now a slug (not arithmetic from the id), the host→port binding lives in the DB: the\n  mounted Caddyfile is just a **bootstrap** (admin API + empty wildcard), and the\n  backend regenerates the per-slug vhost map and **hot-loads it through Caddy's admin\n  API** (`gui-proxy:2019`, internal network only — never publish it) on every instance\n  create/slug-change/delete and at startup. No per-instance file editing, no `gui-N`\n  cap. Regenerate the bootstrap only if its global block changes:\n  `uv --project backend run python scripts/gen-gui-caddyfile.py \u003e docker/Caddyfile.gui-prod`.\n\n  Wire the Traefik router either via the **file provider**\n  ([`docker/traefik-gui.example.yml`](docker/traefik-gui.example.yml)) or, if your\n  Traefik uses the **Docker/Swarm provider**, via **labels** — see the commented\n  `deploy.labels` block on the `gui-proxy` service in `compose.yml`. Either way the\n  router is a single wildcard rule → `gui-proxy:80` — `HostRegexp(`{subdomain:gui-[a-z0-9-]+}.\u003cdomain\u003e`)`\n  in Traefik **v2** (named group, no anchors), or the raw Go regexp\n  `HostRegexp(^gui-[a-z0-9-]+\\.\u003cdomain\u003e$)` in **v3**.\n  Traefik needs no per-instance config. Two gotchas: `deploy.labels` is read only by\n  Traefik's **Swarm** provider (plain compose → use top-level `labels:`), and `gui-proxy`\n  must share a network with Traefik (set `traefik.docker.network` if it's on several).\n\n\u003e Security: each origin fronts a firewall **admin** GUI — the `forwardAuth` gate is\n\u003e what keeps it closed. Don't remove it, and keep the forwarder ports off the public\n\u003e internet (reachable only by your reverse proxy). See `docs/agent-architecture.md` §18.\n\n## Layout\n\n```\nDockerfile              combined prod image (multi-stage: frontend + backend)\ncompose.yml             production stack (MariaDB + app)\ncompose-dev.yml         dev stack (db + backend + frontend, src bind-mounted)\ndocker/                 nginx.conf + start.sh used by the prod image\nbackend/                FastAPI app (src/app/), tests, Dockerfile.dev\nfrontend/               Vite + React + TS app, Dockerfile.dev\nagent/                  stdlib push agent for OPNsense/pfSense + install.sh + rc.d\ncheckmk/                Checkmk special-agent plugin (pulls /api/export/checkmk)\nscripts/                sign_agent.py — Ed25519 signing for agent self-update\ndocs/                   agent-architecture.md (living design doc)\n.github/workflows/      release.yml — multi-arch publish on tag push\nVERSION                 source of truth, baked into image at build\nrelease.sh              version bump + tag + push helper\n```\n\n## Development\n\nTwo workflows — pick one:\n\n### A) Local (fast feedback, recommended)\n\nBackend and frontend run on the host. Database can run in Docker (just `db` from the dev compose) or locally.\n\n```bash\njust backend-install        # uv sync --all-extras (creates backend/.venv)\njust backend-run            # uvicorn --reload on http://localhost:8000\njust backend-test           # pytest\n\njust frontend-install       # npm install\njust frontend-dev           # vite on http://localhost:5173 (proxies /api → backend)\n```\n\n### B) Docker dev compose (everything in containers)\n\nBoth backend and frontend run as separate containers with their `src/` bind-mounted, so saving a file triggers `uvicorn --reload` (backend) or Vite HMR (frontend).\n\n```bash\ncp .env.example .env        # set DASH_MASTER_KEY at minimum\njust dev-up                 # docker compose -f compose-dev.yml up -d --build\njust dev-logs\n\n# Browse: http://localhost:5173 (frontend)\n# Direct: http://localhost:8000/api/health (backend)\n```\n\n### Tests \u0026 gates\n\n```bash\njust backend-test           # pytest (backend)\njust agent-test             # pytest over agent/tests (runs in the backend venv)\njust checkmk-test           # pytest over checkmk/tests\njust frontend-build         # tsc -b \u0026\u0026 vite build — the only frontend gate\n```\n\n## Releasing\n\n```bash\njust release patch          # or: minor / major\n```\n\n`release.sh` bumps `VERSION`, inserts a dated section in `CHANGELOG.md`, commits, tags `${VERSION}`, and pushes. The `.github/workflows/release.yml` workflow then builds a multi-arch image (`linux/amd64,linux/arm64`) and publishes it to:\n\n- `docker.io/styliteag/dashboard:${VERSION}` and `:latest`\n- `ghcr.io/styliteag/dashboard:${VERSION}` and `:latest`\n\nRequired CI secrets: `DOCKERHUB_USERNAME`, `DOCKERHUB_TOKEN` (GHCR uses the default `GITHUB_TOKEN`).\n\n## Security notes\n\n- Firewall API credentials are stored **encrypted at rest** with Fernet. The master\n  key (`DASH_MASTER_KEY`) lives only in `.env`.\n- Prefer the **push agent / relay** for boxes behind NAT — the dashboard then needs no\n  inbound reachability and stores no per-box API key (the relay forwards to the box's\n  loopback API, and on OPNsense the agent auto-provisions a dedicated `orbit` user).\n- For **direct** transport, expose each firewall's API only over HTTPS with a source-IP\n  allowlist for the dashboard host, pin the per-instance CA bundle, and use a dedicated\n  service user with minimal ACLs (Diagnostics read, IPsec start/stop, Firmware update) —\n  never root.\n- **Checkmk** integration authenticates with a **read-only API key** (`POST /api/apikeys`,\n  stored hashed, rejected on any non-GET request) — the admin password stays out of WATO.\n- Agent **self-update is signature-verified** (Ed25519). The agent bakes a public key\n  (`_UPDATE_PUBKEY` in `agent/orbit_agent.py`); every pushed update must carry a valid\n  signature over the code, so a compromised dashboard can't push forged agent code — the\n  dashboard only relays the signature, it never holds the key. Setup:\n  1. Generate a keypair once, offline: `just sign-agent --gen` → keep `PRIV_B64` offline,\n     bake `PUB_HEX` into `_UPDATE_PUBKEY`.\n  2. Put the private key where the release machine can read it: `DASH_AGENT_SIGNING_KEY` in\n     the environment or the gitignored repo-root `.env`.\n\n  `release.sh` (`just release`) then **signs the agent automatically** before tagging —\n  it refreshes `agent/orbit_agent.py.sig` from the current agent bytes, verifies it against\n  the baked `_UPDATE_PUBKEY`, and includes it in the release commit (the signature is\n  committed because it isn't secret; only the private key is). If `_UPDATE_PUBKEY` is set\n  but no signing key is available, the release **aborts** — so only the offline key holder\n  can cut a release, and a build that would brick every agent's self-update can't ship.\n  To sign by hand outside a release: `DASH_AGENT_SIGNING_KEY=\u003cPRIV_B64\u003e just sign-agent`.\n\n  **Dev escape hatch.** While iterating on the agent you can push an unsigned/stale build\n  without re-signing by telling the *agent* (not the dashboard) to skip the check — it's\n  off by default and logs a loud warning when active. Two channels, since the agent runs on\n  the box, not in compose:\n  - **Locally-run agent:** `AGENT_INSECURE_SKIP_SIG=1 python agent/orbit_agent.py`.\n  - **Installed agent (rc.d):** add `\"insecure_skip_sig\": true` to its\n    `/usr/local/etc/orbit-agent.conf` and restart it (`service orbit_agent restart`) — the\n    env var doesn't reach an rc.d-launched process, so use the config flag there.\n\n  \u003e Never set either in production — it disables the forgery protection. It doesn't flow\n  \u003e from `compose-dev`; you set it on the agent itself.\n\n## Further docs\n\n- [`docs/agent-architecture.md`](docs/agent-architecture.md) — agent \u0026 connectivity design (transports, self-update, pfSense port, relay, Checkmk).\n- [`CHECKMK.md`](CHECKMK.md) — full Checkmk integration guide (what's exposed, API key, datasource program, piggyback hosts, troubleshooting).\n- [`checkmk/README.md`](checkmk/README.md) — Checkmk special-agent install and auth.\n- [`CLAUDE.md`](CLAUDE.md) — repository conventions and done-criteria.\n\n## License\n\nSTYLiTE Orbit Dashboard is **source-available** under the **Business Source\nLicense 1.1 (BSL 1.1)** — see [`LICENSE`](LICENSE) and [`LICENSING.md`](LICENSING.md).\n\n- ✅ Read, build, modify, and run it for your **own** organization.\n- ❌ Offering it to third parties as a hosted / managed service, or reselling it,\n  needs a **commercial license** — contact `office@stylite.de`.\n- Each released version becomes **GPL-3.0-or-later** four years after its release.\n\nBSL is *not* an OSI-approved \"Open Source\" license; the correct term is\n*source-available*.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstyliteag%2Fdashboard","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fstyliteag%2Fdashboard","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstyliteag%2Fdashboard/lists"}