diff --git a/README.md b/README.md index 8e401a15b..7e82aae5d 100644 --- a/README.md +++ b/README.md @@ -25,8 +25,11 @@ flowchart LR wg --> home["home APIARY stacks
@ 10.8.0.2"] ``` -**All core sensors run without compose profiles.** The only profile is the -optional on-demand `geoip-update` maintenance job. 39 deployment pieces — +**All core sensors run without compose profiles.** The only profiles are the +optional on-demand maintenance jobs `geoip-update` and `threat-intel` (both in +`honeypot-init`); the four `["legacy"]` worker stacks are defined for rollback +but do not run, and `sandbox/ghosts`'s `["test"]` client is not a sensor. +39 deployment pieces — 33 independent Arcane-managed stacks under `arcane/home/` plus 6 more at their own repository-root paths, all at home, plus the VPS (see [docs/ARCANE-GIT-SYNC.md](docs/ARCANE-GIT-SYNC.md) for how a repo commit @@ -38,12 +41,12 @@ why the home side split into this many Compose stacks): | `honeypot-keycloak` ([arcane/home/honeypot-keycloak/compose.yml](arcane/home/honeypot-keycloak/compose.yml)) | **home** | Arcane-managed Keycloak/PostgreSQL identity stack; only Keycloak is reachable from VPS Traefik over WireGuard | | `honeypot-init` ([arcane/home/honeypot-init/compose.yml](arcane/home/honeypot-init/compose.yml)) | **home** | one-shot bootstrap jobs: log paths, Elasticsearch templates, Arkime schema, persona validation | | `honeypot-cowrie`, `honeypot-dionaea`, `honeypot-conpot`, `honeypot-dnp3`, `honeypot-http`, `honeypot-multipot` (`arcane/home/honeypot-/compose.yml`, one directory each) | **home** | the sensors: Cowrie, Dionaea (+ TFTP relay), Conpot personas, DNP3, HTTP/API honeypots, multipot | -| `honeypot-dicompot`, `honeypot-dns-honeypot`, `honeypot-citrix`, `honeypot-cisco-asa`, `honeypot-sonicwall-sma`, `honeypot-rdp`, `honeypot-endlessh`, `honeypot-beelzebub`, `honeypot-hellpot`, `honeypot-elasticpot`, `honeypot-galah`, `honeypot-sentrypeer`, `honeypot-mailoney` (`arcane/home/honeypot-/compose.yml`, one directory each) | **home** | more sensors: DICOM medical-imaging decoy, DNS UDP reflection bait (response-capped, never a real amplification vector), Citrix ADC/NetScaler Gateway decoy (CVE-2019-19781), Cisco ASA WebVPN+IKE decoy (CVE-2018-0101), SonicWall SMA1000 Work Place/AMC decoy (CVE-2026-83548 Work Place SSRF), RDP decoy, SSH pre-auth tarpit, vendored multi-protocol deception runtime (SSH/LDAP/MCP/HTTP, #1418), vendored HTTP bot tarpit (#1419), vendored Elasticsearch decoy distinct from multipot's own (#1423), vendored LLM-powered HTTP honeypot behind its own broker-guarded bridge onto the shared Ollama instance (#1420), vendored SIP/VoIP fraud-detection honeypot (#1424), vendored SMTP honeypot taking over port 25 from multipot's own retired handler (#1422) — the row's `honeypot-wordpot` / WordPress/CMS decoy slot was removed when wordpot retired (#2381) | +| `honeypot-dicompot`, `honeypot-dns-honeypot`, `honeypot-citrix-honeypot`, `honeypot-cisco-asa-honeypot`, `honeypot-sonicwall-sma`, `honeypot-rdp-honeypot`, `honeypot-endlessh`, `honeypot-beelzebub`, `honeypot-hellpot`, `honeypot-elasticpot`, `honeypot-galah`, `honeypot-sentrypeer`, `honeypot-mailoney` (`arcane/home/honeypot-/compose.yml`, one directory each) | **home** | more sensors: DICOM medical-imaging decoy, DNS UDP reflection bait (response-capped, never a real amplification vector), Citrix ADC/NetScaler Gateway decoy (CVE-2019-19781), Cisco ASA WebVPN+IKE decoy (CVE-2018-0101), SonicWall SMA1000 Work Place/AMC decoy (CVE-2026-83548 Work Place SSRF), RDP decoy, SSH pre-auth tarpit, vendored multi-protocol deception runtime (SSH/LDAP/MCP/HTTP, #1418), vendored HTTP bot tarpit (#1419), vendored Elasticsearch decoy distinct from multipot's own (#1423), vendored LLM-powered HTTP honeypot behind its own broker-guarded bridge onto the shared Ollama instance (#1420), vendored SIP/VoIP fraud-detection honeypot (#1424), vendored SMTP honeypot taking over port 25 from multipot's own retired handler (#1422) — the row's `honeypot-wordpot` / WordPress/CMS decoy slot was removed when wordpot retired (#2381) | | `honeypot-canarytokens` ([arcane/home/honeypot-canarytokens/compose.yml](arcane/home/honeypot-canarytokens/compose.yml)) | **home** | self-hosted honeytoken platform (#1426) -- planted-artifact deception, not a listening protocol decoy; `canarytokens-adapter` translates its webhook alerts into this repo's shared JSON event shape. The dashboard's Settings > Canarytokens pane (#1487) creates PDF/Word/Excel/custom-image/Windows-Folder/QR tokens on demand for use *outside* this honeypot (#1662 dropped the stale design doc that described the pre-cutover plan; the shipped pane is authoritative) | | `honeypot-tanner` ([arcane/home/honeypot-tanner/compose.yml](arcane/home/honeypot-tanner/compose.yml)) | **home** | SNARE + TANNER application-emulation boundary | | `honeypot-elk` ([arcane/home/honeypot-elk/compose.yml](arcane/home/honeypot-elk/compose.yml)) | **home** | Filebeat, Elasticsearch, Kibana, EveBox, Arkime | | `honeypot-agent-intrusion-worker` ([arcane/home/honeypot-agent-intrusion-worker/compose.yml](arcane/home/honeypot-agent-intrusion-worker/compose.yml)) | **home** | the labelled corpus plus the Tier 1 contract benchmark. The worker itself was ported to Rust in #1610 and now runs as `WORKER_LOOPS=agent-intrusion` inside `honeypot-dashboard`'s `backend-worker`; the Python stack is retained under the `legacy` profile for rollback only, and the live `agent-intrusion-campaigns` index is written by the Rust loop | -| `honeypot-attacker-identity-worker`, `honeypot-correlator-worker`, `honeypot-payload-inventory-worker` (`arcane/home/honeypot-/compose.yml`, one directory each) | **home** | three more workers that had their own top-level compose file but had drifted out of the deploy/installer inventory before #1502's audit caught it (same class of gap #560 and #891 each fixed once before) -- attacker-identity correlation, cross-sensor campaign correlation, and payload inventory tracking | +| `honeypot-attacker-identity-worker`, `honeypot-correlator-worker`, `honeypot-payload-inventory-worker` (`arcane/home/honeypot-/compose.yml`, one directory each) | **home** | three more workers that had their own top-level compose file but had drifted out of the deploy/installer inventory before #1502's audit caught it (same class of gap #560 and #891 each fixed once before) -- attacker-identity correlation, cross-sensor campaign correlation, and payload inventory tracking. All three were retired by the same #1649 pass as the agent-intrusion worker: #1610 ported them to Rust, where they now run as `WORKER_LOOPS=attacker-identity` and `WORKER_LOOPS=correlator` on `honeypot-dashboard`'s `backend-worker` and `WORKER_LOOPS=payload-inventory` on `backend-worker-payload-inventory`. Like row above, the Python stacks are kept under the `legacy` profile for rollback only and are not the live writers | | `honeypot-dashboard` ([arcane/home/honeypot-dashboard/compose.yml](arcane/home/honeypot-dashboard/compose.yml)) | **home** | the live investigation dashboard: TanStack Start frontend (`dashboard-next`) in front of the Rust axum `backend-service` API tier, plus its worker containers (importer, networkless enrichment, the `WORKER_LOOPS` aggregation loops) and the services-adapter Docker control surface. Live since #1628's cutover completed 2026-08-22 -- the Go dashboard is deleted; see [docs/DASHBOARD-CUTOVER.md](docs/DASHBOARD-CUTOVER.md) and [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) | | `honeypot-dashboard-backend` ([arcane/home/honeypot-dashboard-backend/compose.yml](arcane/home/honeypot-dashboard-backend/compose.yml)) | **home** | the unprivileged read-only `backend-service` API tier (:8081), split out from `honeypot-dashboard` by #1622 so Arcane can redeploy the API tier without touching `dashboard-next`; the write-capable, host-spool-mounted instance is `backend-service-mounted` (:8082), which stayed in `honeypot-dashboard` | | `honeypot-payload-analysis` ([arcane/home/honeypot-payload-analysis/compose.yml](arcane/home/honeypot-payload-analysis/compose.yml)) | **home** | payload dedup + YARA scanning | @@ -91,7 +94,7 @@ for where those fit. | [docs/BACKUP-ESSENTIALS.md](docs/BACKUP-ESSENTIALS.md) | What is backed up so the stack can be rebuilt, where the three copies go, and the full restore procedure | | [scripts/install.sh](scripts/install.sh) | Single entry point for host provisioning — `sudo ./scripts/install.sh --profile home\|vps`, which dispatches to the installer below (or [scripts/install-vps.sh](scripts/install-vps.sh)) with that profile's answers file. Both share their retry/step/logging framework via [scripts/lib/install-common.sh](scripts/lib/install-common.sh) ([#1609](https://github.com/Xore/APIARY/issues/1609)) | | [scripts/install-homeserver.sh](scripts/install-homeserver.sh) | Unattended provisioning script (Docker, GPU/NVIDIA, WireGuard, Arcane, the stacks themselves) for a manually-installed base Ubuntu system — fill in [scripts/install-homeserver.conf.example](scripts/install-homeserver.conf.example) first, same idea as a Windows `autounattend.xml` answer file. First cut, see [#518](https://github.com/Xore/APIARY/issues/518) | -| [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) | System architecture and data flow — trust boundaries, container map, event ingestion, correlation/enrichment (p0f, HASSH/JA3/JA4, GeoIP), payload lifecycle, sandbox detonation, evidence types (6 diagrams) | +| [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) | System architecture and data flow — trust boundaries, container map, event ingestion, correlation/enrichment (p0f, HASSH/JA3/JA4, GeoIP), payload lifecycle, sandbox detonation, evidence types (4 diagrams) | | [docs/SENSORS.md](docs/SENSORS.md) | The sensor table, resource budgets, investigation UIs, SNARE+TANNER, Suricata, Arkime, and how real attacker IPs survive the tunnel | | [docs/OPERATIONS.md](docs/OPERATIONS.md) | Persona inventory, the seeded cowrie filesystem, GeoIP, and how to actually read the data (dashboard, Kibana, Arkime, backups) | | [docs/ip-reporting-plan.md](docs/ip-reporting-plan.md) | Defensive IP-blocklist reporting (AbuseIPDB/Blocklist.de), dry-run by default | @@ -104,7 +107,7 @@ for where those fit. | [docs/CONTAINER-UPDATES.md](docs/CONTAINER-UPDATES.md) | How to check pinned images for updates, assess compatibility, verify empirically, and pin by digest | | [docs/TESTING.md](docs/TESTING.md) | The three testing tiers -- CI, live feature smoke tests, and the full clean-reinstall release gate -- and how to repeat each one | | [docs/STACK-REBUILD.md](docs/STACK-REBUILD.md) | Runbook for a full deliberate reset — stop order, what's preserved vs wiped, and the ordering/permission pitfalls to avoid | -| [deploy-profiles/](deploy-profiles/) | Named deployment shapes (full / ICS-only / web-only) — which of the 20 split home stacks run for a given deployment, plus a validator catching cross-stack drift before deploy | +| [deploy-profiles/](deploy-profiles/) | Named deployment shapes (full / ICS-only / web-only) — which of the 26 split home stacks run for a given deployment, plus a validator catching cross-stack drift before deploy | | [docs/RECOVERY.md](docs/RECOVERY.md) | `factory-reset.sh` — one entry point for "back up, optionally wipe/reset, restart" on the same host | | [docs/ROADMAP.md](docs/ROADMAP.md) / [docs/WORK-LEDGER.md](docs/WORK-LEDGER.md) | What order work happens in, and how issues are claimed/reviewed | | [docs/ml-worker-plan.md](docs/ml-worker-plan.md), [docs/gpu-llm-analysis-worker.md](docs/gpu-llm-analysis-worker.md), [docs/gpu-ml-worker-acceleration.md](docs/gpu-ml-worker-acceleration.md) | The homeserver's NVIDIA GPU running local LLM log/payload analysis and CUDA-accelerated anomaly detection — no data leaves the machine | diff --git a/docs/DECEPTION-EXTENSIONS.md b/docs/DECEPTION-EXTENSIONS.md index 14255209f..57fcb72d2 100644 --- a/docs/DECEPTION-EXTENSIONS.md +++ b/docs/DECEPTION-EXTENSIONS.md @@ -109,6 +109,7 @@ traces back to at least one decision. | RDP decoy (rdphoneypot lineage) | Integrated | `arcane/home/honeypot-rdp-honeypot/` | #238 batch, per-decoy plan #412 | | Cisco ASA VPN gateway decoy | Integrated | `arcane/home/honeypot-cisco-asa-honeypot/` | #238 batch, CVE context #414 | | Citrix ADC gateway decoy | Integrated | `arcane/home/honeypot-citrix-honeypot/` | #238 batch, CVE context #414 | +| SonicWall SMA1000 Work Place/AMC decoy | Integrated | `arcane/home/honeypot-sonicwall-sma/` | #3033; CVE-2026-83548 SSRF / CVE-2026-83549 AMC command-injection chain | | DNS amplification bait | Integrated | `arcane/home/honeypot-dns-honeypot/` | #238 batch, safety-sensitive design #415 | | Mailoney (SMTP) | Integrated | `arcane/home/honeypot-mailoney/` | #1422 | | SentryPeer (VoIP/SIP) | Integrated | `arcane/home/honeypot-sentrypeer/` | #1424 | diff --git a/docs/README.md b/docs/README.md index 77b79d3c2..a41a8f9ab 100644 --- a/docs/README.md +++ b/docs/README.md @@ -97,8 +97,8 @@ Analysis and sandbox components: era references inside are historical)](dashboard-manual-ip-block-design.md) Subdirectories (`analysis/`, `research/`, `sandbox/`, `vps/`, `autoinstall/`, -`deploy-profiles/`, `archive/`) hold the same kinds of documents scoped to +`deploy-profiles/`, `design-lab/`) hold the same kinds of documents scoped to their component. Every doc must be reachable from this page through links; dated record trees (`research/`, `benchmarks/`, the VM-detection results, -`archive/`) are exempt. `scripts/check-docs-reachable.py` enforces this in CI -(#3332). +`design-lab/`, kept as a near-duplicate of `branding/design-lab/`) are +exempt. `scripts/check-docs-reachable.py` enforces this in CI (#3332). diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 50dc0c04f..c32560049 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -13,6 +13,11 @@ Last audited: 2026-08-05 > Deleted by the 2026-08-30 bulk purge and restored verbatim by #2896/#2947 > on 2026-09-04. Nothing below was updated during that window — treat > "last audited" above as still true, not as of the restore date. +> +> The *repository* did keep moving across that same window, though, so the +> audit date is not a safe lower bound for "still true": reconcile any status +> claim here against the code before relying on it. The CAPEv2 line above is +> one already re-verified (2026-09-27) and found stale. ## Current baseline @@ -35,12 +40,22 @@ Last audited: 2026-08-05 booting; the end-to-end submit-to-report path is verified for the Linux/Wine sandbox and GitHub-analysis publishing. The Windows-11 golden image epic ([#47](https://github.com/Xore/APIARY/issues/47)) is - still open — see the Windows sandbox section below. CAPEv2 (#314-322) - remains unbuilt and is post-0.1.0 backlog. -- Documentation has been consolidated: every doc that used to be scattered - next to its source now lives under `docs/`, mirroring the source tree - ([#670](https://github.com/Xore/APIARY/issues/670), closed - 2026-08-05). + still open — see the Windows sandbox section below. CAPEv2 (#314-322) is + **authored, not deployed**: [#843](https://github.com/Xore/APIARY/issues/843) + (2026-09-01) landed `sandbox/cape/` — compose stack, Packer + `win11-cape.pkr.hcl`, CAPEv2 override units and a spool worker — but unlike + GHOSTS it has no entry in `arcane/manifests/home-production.json`, so nothing + Dockge-managed deploys it. Treat it as post-0.1.0 backlog whose build work + has already started, not as unbuilt. +- Documentation has been consolidated: the subsystem docs that used to be + scattered next to their source now live under `docs/`, mirroring the source + tree ([#670](https://github.com/Xore/APIARY/issues/670), closed + 2026-08-05). The exceptions are deliberate and catalogued in + [`docs/README.md`](README.md) — per-stack and vendored `README.md` files stay + next to their code, and a few dated record trees are exempt from the + reachability gate (#3332). Root-level dated task records (`DIFF.md`, + `EVIDENCE.md`, `HANDOFF-3097.md`) are the residue of that sweep and are not + covered by it. - The dashboard rewrite is done, not pending: the TanStack Start frontend/BFF + Rust service tier ([#1608](https://github.com/Xore/APIARY/issues/1608) and its diff --git a/docs/SENSORS.md b/docs/SENSORS.md index 36faf392f..b7316ca73 100644 --- a/docs/SENSORS.md +++ b/docs/SENSORS.md @@ -52,8 +52,9 @@ The `payload-dedupe` service scans these stores hourly and atomically replaces same-filesystem duplicates with hard links. Existing event/download URLs remain valid while duplicate disk blocks are reclaimed; its last-run report is stored at `state/dedupe/payload-dedupe.json`. -Its diagnostic logger is limited to `info,warning,error` so debug chatter cannot -consume the data disk. The `log-maintenance` sidecar copy-truncates and gzips +Neither service produces per-event chatter: `payload-dedupe` prints a single +JSON result line per pass, and `log-maintenance` writes one stderr line per +rotation. The `log-maintenance` sidecar copy-truncates and gzips human-readable Dionaea, Conpot, and Cowrie logs at 256 MiB (four archives). Structured JSON event streams are deliberately never rotated by that sidecar, which preserves Filebeat offsets and dashboard ingestion. @@ -70,14 +71,22 @@ GeoIP enrichment is best-effort: empty or malformed addresses are skipped, but the original event is always retained. ILM keeps raw Suricata indices for 7 days, honeypot data streams for 30 days, and dead-letter records for 60 days so high-volume scans cannot fill the disk. +Those are the values at the *code* fallback `HONEYPOT_RETENTION_DAYS=30`. Every +window derives from that one knob, and the shipped configuration does **not** +use the fallback: all four tracked `.env.example` files set +`HONEYPOT_RETENTION_DAYS=21` (#2820), at which the three windows above become +**4d / 21d / 42d** — Suricata is `retention*7/30` (integer-truncated, floored +at 1) and dead-letter is `retention*2`. Only the ILM *policy names* +(`suricata-7d`, `honeypot-30d`, `dead-letter-60d`) stay fixed; they are +identifiers referenced by the index templates, not claims about duration. ## Runtime resource budgets Every service has a CPU, memory, and Docker `json-file` log budget. The limits are intentionally generous relative to the host (16 logical CPUs and -91 GiB RAM): Elasticsearch gets 8 GiB with a 4 GiB heap; Arkime capture 6 GiB; +91 GiB RAM): Elasticsearch gets 12 GiB with a 6 GiB heap; Arkime capture 6 GiB; Kibana, Filebeat, and the TANNER analyzer receive 2 GiB; EveBox, Dionaea, Arkime -viewer, and the live dashboard receive 1 GiB (the dashboard also has one CPU). The +viewer, and the live dashboard receive 1 GiB (the dashboard also has two CPUs). The remaining lightweight sensors receive 128-512 MiB. Docker console logs rotate at 25 MiB with three files, independently from sensor event files under `./logs`. @@ -86,7 +95,7 @@ at 25 MiB with three files, independently from sensor event files under | Dashboard | Subdomain | Container | |---|---|---| -| Live sensor view (ours) | `honeypot.` | `dashboard` :8090 | +| Live sensor view (ours) | `honeypot.` | `dashboard-next` :8080 | | Kibana (ELK + Suricata) | `kibana.` | `kibana` :5601 | | TANNER web-attack analysis | `tanner.` | `tanner_web` :8091 | | EveBox (Suricata events) | `evebox.` | `evebox` :5636 | @@ -116,10 +125,13 @@ XSS, command execution, PHP code/object injection, XXE, CRLF and template injection) and stores sessions in Redis. TANNER's emulation containers are isolated from the homeserver Docker socket and are not a malware detonation environment. Suspicious payload detonation belongs in the separate KVM/libvirt -sandbox described in [`sandbox/README.md`](sandbox/README.md). Containers: `tanner_redis`, -`tanner_phpox`, `tanner_api`, `tanner` (analyzer, `:8090`), `tanner_web` -(dashboard, `:8091`), `snare_clone` (one-shot deterministic persona installer), -`snare` (`:8080`). The page source lives under [snare/persona](../arcane/home/honeypot-tanner/snare/persona) +sandbox described in [`sandbox/README.md`](sandbox/README.md). Containers: `tanner_docker` +(the nested disposable emulator daemon), `tanner_redis`, `tanner_phpox`, `tanner_api`, +`tanner` (analyzer, `:8090`), `tanner_web` (dashboard, `:8091`), `snare` (`:8080`) -- +seven services, all on `tanner_local`. The persona clone itself is not one of them: +`snare-clone` is a one-shot job in `honeypot-init` (`hp-snare-clone`) that writes the +`snare-pages` volume `snare` reads. The page source lives under +[snare/persona](../arcane/home/honeypot-tanner/snare/persona) and is transformed into SNARE's content-addressed store during the image build; no third-party site is cloned. All `mushorg/*` images are third-party — verify tags/args upstream (needs a live build/pull). @@ -127,8 +139,10 @@ no third-party site is cloned. All `mushorg/*` images are third-party ## Suricata — analysing all the traffic `suricata` runs **on the VPS** (host networking, sniffing the public interface -`SURICATA_IFACE`, default `ens6`) so it sees real attacker source IPs before the -tunnel. It writes to `/opt/stacks/apiary/logs/suricata/` on the VPS: +`CAPTURE_INTERFACE`, written at boot by `detect-capture-interface.service`, +falling back to the legacy `SURICATA_IFACE` and then to `eth0` — *not* the old +`ens6`, which a reboot has already renamed) so it sees real attacker source IPs +before the tunnel. It writes to `/opt/stacks/apiary/logs/suricata/` on the VPS: - `eve.json` (alerts, http, dns, tls, flow) — Filebeat on the home server ships it to the `suricata-*` Elasticsearch index (stats events are dropped, see @@ -180,7 +194,9 @@ Web UI: `http://:19080` (`arkime.` via Traefik). > **http/api-honeypots** (`PROXY_PROTOCOL=1`), **dnp3** (`PROXY_PROTOCOL=1`), > **dicompot** (`PROXY_PROTOCOL=1`), **citrix-honeypot**, > **sonicwall-sma-honeypot** (`PROXY_PROTOCOL=1`), -> **cisco-asa-honeypot**'s WebVPN side and **rdp-honeypot** (`PROXY_PROTOCOL=1`) and **all conpot sensors** (`CONPOT_PROXY_PROTOCOL=1`, gevent shim baked in +> **cisco-asa-honeypot**'s WebVPN side and **rdp-honeypot** (`PROXY_PROTOCOL=1`), +> **endlessh** (`PROXY_PROTOCOL=1`, public 2022) and **all conpot sensors** +> (`CONPOT_PROXY_PROTOCOL=1`, gevent shim baked in > by `conpot/proxy_patch.py`) parse it, so those events log the true IP and > port. The http listener sniffs the header, so Traefik-routed requests (no > header) keep working too. diff --git a/docs/agent-intrusion-threat-model.md b/docs/agent-intrusion-threat-model.md index fbaad1c90..2bb52e110 100644 --- a/docs/agent-intrusion-threat-model.md +++ b/docs/agent-intrusion-threat-model.md @@ -36,9 +36,12 @@ For each of the nine areas #154 asked to cover, this maps to APIARY's **actual** current architecture — verified against the real compose files, -Go/Python source, and docs in this tree, not assumed from what a "typical" +the source, and docs in this tree, not assumed from what a "typical" honeypot stack might do. Each entry records: what exists today, whether the -published campaign's technique applies here, and the evidence. +published campaign's technique applies here, and the evidence. (The original +pass read Go and Python source; the Go tier was deleted at #1628 on +2026-08-22, so a re-read today should be against the Rust modules named in +the status banner above.) --- @@ -233,20 +236,24 @@ gap is outbound-to-internet egress policy, tracked separately in #538. **Substantially addressed for the dashboard; inconsistent elsewhere.** - The dashboard's own Docker-socket boundary is the strongest example in - this tree: the dashboard container itself never mounts `/var/run/docker.sock` - (`arcane/home/honeypot-dashboard/compose.yml`, grepped directly — absent). All + this tree: the dashboard containers themselves never mount + `/var/run/docker.sock` (`arcane/home/honeypot-dashboard/compose.yml`, grepped + directly — its one socket mount belongs to `services-adapter`, below). All Docker-lifecycle actions (start/stop/restart) go through `hp-services-adapter`, a separate container that is `cap_drop: [ALL]`, `read_only: true`, `network_mode: none`, and reachable only via an AF_UNIX socket the dashboard also holds — no TCP path exists to abuse it remotely even if the dashboard container itself were compromised. -- `hp-autoheal` is the one other service that does bind-mount the real - `/var/run/docker.sock` (`arcane/home/honeypot-utilities/compose.yml`) — it watches - containers by label daemon-wide and restarts unhealthy ones. This is a - broad, standing grant (full Docker API access, not scoped to specific - containers) held by a long-running service; workload-identity-scoped - alternatives (e.g. a narrower label-filtered API surface) were not found - in this tree. +- `hp-autoheal` was the one other service that bind-mounted the real + `/var/run/docker.sock` (`arcane/home/honeypot-utilities/compose.yml`) — it + watches containers by label daemon-wide and restarts unhealthy ones. **That + grant has since been narrowed by #592** (the strikethrough item in + [Follow-up scope](#follow-up-scope)): it now talks to `hp-docker-socket-proxy` + at `tcp://docker-socket-proxy:2375` with no socket bind mount of its own, and + the proxy holds the socket `:ro` scoped to `CONTAINERS=1`, `IMAGES=1`, + `POST=1` on a private network. What remains is that `CONTAINERS=1` is still + daemon-wide rather than label-filtered — the label scoping is + `AUTOHEAL_CONTAINER_LABEL` inside autoheal, not an API-side restriction. - `tanner_docker` (`arcane/home/honeypot-tanner/compose.yml`) is `privileged: true` with its own `tmpfs /var/lib/docker` — explicitly isolated Docker-in-Docker on the private `tanner_local` network, not a bind mount of the host socket. @@ -259,11 +266,10 @@ gap is outbound-to-internet egress policy, tracked separately in #538. repo has to short-lived workload identity today. **Verdict:** the dashboard/services-adapter split is a good existing -least-privilege pattern worth citing as the template for any future -privileged-access surface. `hp-autoheal`'s standing daemon-wide Docker -socket grant is the one credential-lifetime/least-privilege gap worth a -scoped look (whether its watch scope can be narrowed), separate from #88's -network-isolation focus. +least-privilege pattern worth citing as the template for any new +privileged-access surface. It is also now the template autoheal was moved onto: +its raw socket grant is gone, replaced by the same narrow-proxy shape, leaving +only the daemon-wide `CONTAINERS=1` scope. --- @@ -400,8 +406,10 @@ finding.** state; the same fragmentation carried into the Rust cutover's per-source worker functions): Suricata/sensor event alerts, `ghidraAlerts`, `githubAnalysisAlerts`, sandbox queue/verdict alerts, ML anomaly severity, and (as of #150) the - new `llm-analysis` index's own severity field is not yet wired into - `alerts.go` at all — it is currently browse-only via `/llm-analysis`. + new `llm-analysis` index's own severity field — which was browse-only via + `/llm-analysis` until it was wired into the sink as `llm_flagged_alerts` in + the Rust cutover, the "Done" item in [Follow-up scope](#follow-up-scope) + below. - No single "this source_ip/session/sample crossed N independent trust boundaries in a Y-minute window" correlation exists — each alert source answers its own narrow question. This is exactly the shape the campaign's @@ -413,11 +421,11 @@ finding.** identity for investigation, not by trust-boundary-crossing count for alerting. -**Verdict:** this is the clearest concrete gap this research surfaced. Wiring -`llm-analysis`'s severity into the existing alert sink is a small, immediate -follow-up; a genuine cross-source trust-boundary-crossing correlation engine -is squarely phase 3's scope, not something to build inside this research -pass. +**Verdict:** this is the clearest concrete gap this research surfaced. Its +smallest piece — wiring `llm-analysis`'s severity into the alert sink — has +since landed (`llm_flagged_alerts` in the Rust `alert-notifier`); what remains +is a genuine cross-source trust-boundary-crossing correlation engine, which is +squarely phase 3's scope, not something to build inside this research pass. --- @@ -429,11 +437,11 @@ pass. | 2 | Untrusted structured-data processing | Yes, broadly | `html/template` auto-escaping; CI YARA corpus gate | No archive/container-format parsing exists yet — must inherit this discipline when added | | 3 | Env/`/proc/*/environ` secret exposure | Yes | `secretFromEnvironment`'s `_FILE` pattern (one use) | Pattern not applied to `ARKIME_*`, `GH_PAT`, VPS SSH key | | 4 | Metadata-service / RFC 1918 reachability | No cloud metadata surface exists | Per-sensor private Docker networks | Outbound-to-internet egress policy (tracked in #538) | -| 5 | Credential lifetime / workload identity | Yes | dashboard/services-adapter split (strong pattern, since carried into the backend-service/worker split) | `hp-autoheal`'s standing daemon-wide docker.sock grant | +| 5 | Credential lifetime / workload identity | Yes | dashboard/services-adapter split (strong pattern, since carried into the backend-service/worker split); autoheal moved onto the same narrow-proxy shape by #592 | Proxy's `CONTAINERS=1` is still daemon-wide, not label-filtered | | 6 | Encoded/chunked C2 | Yes, as honeypot capture surface | Raw payload capture (tanner/Suricata); narrow fixed-destination outbound HTTP clients | No network-layer egress enforcement (folds into #538) | | 7 | Repeated recon / low-signal escalation | Yes — core motivating gap | ml-worker anomaly scoring; dashboard campaign clustering | No behavioral-phase correlation or combination-based severity escalation | | 8 | Source-control/CI write paths | Yes | `analysis/github/` publish gate (CI-tested); vendored-dep hash pinning | No image digest pinning for this repo's own built images | -| 9 | Cross-source alert correlation | Yes — core motivating gap | Multiple independent alert sources feed one sink | No trust-boundary-crossing correlation; `llm-analysis` severity not yet wired into alerts | +| 9 | Cross-source alert correlation | Yes — core motivating gap | Multiple independent alert sources feed one sink, `llm-analysis` severity included | No trust-boundary-crossing correlation engine | --- diff --git a/docs/community-threat-intel-sharing.md b/docs/community-threat-intel-sharing.md index 64a558d2b..89a89f059 100644 --- a/docs/community-threat-intel-sharing.md +++ b/docs/community-threat-intel-sharing.md @@ -54,8 +54,11 @@ commitment than this repo's existing IP-blocklist reporting: destinations (AbuseIPDB, Blocklist.de), stays dry-run by default, and needed its own multi-phase build (#68 for the dry-run foundation and safeguards, #69 for validation and metrics, #153 for reputation - filtering and observability, still open) to get the privacy posture - right. + filtering and observability) to get the privacy posture + right. All three are closed and implemented: #153 shipped GreyNoise + RIOT pre-checks (`reporter/greynoise.go`, off unless `GREYNOISE_ENABLED=1`) + and a `metrics.json` counter snapshot that the dashboard mirrors into + `reporter-metrics-v1`. - A generic `hpfeeds` publisher would share *richer* structured data (commands, credentials, payload hashes, session metadata -- whatever TANNER or another sensor chose to publish) with *whichever broker an diff --git a/docs/container-writable-layer-audit-2026-09-03.md b/docs/container-writable-layer-audit-2026-09-03.md index 34874802f..b20b9c610 100644 --- a/docs/container-writable-layer-audit-2026-09-03.md +++ b/docs/container-writable-layer-audit-2026-09-03.md @@ -1,5 +1,14 @@ # Container writable-layer and build-cache audit (#2859), 2026-09-03 +> **Dated record — 2026-09-03.** Every measurement below (245.8 GB, 13.18 GB +> reclaimable, the 45-hour leaked buildkit container, the dangling-volume +> census) was a snapshot of the homeserver on that date and is **not** a live +> status page. Per the same principle `security-fixes.md` states outright: do +> not mirror a live system's state into a markdown file. Re-run the commands +> before acting on any number. The attribution, the reasoning and the +> conclusions are the substance of this document and do not expire; the +> open items named here are tracked in their issues (#2915, #2904). + `docker system df` on the homeserver showed **245.8 GB in container writable layers**, invisible to the volume audit and the retention knob. This document is the attribution the issue asked for. @@ -78,8 +87,8 @@ failing** — GC only starts reclaiming once usage exceeds the 100 GB neither of which is close on this host. No config or timer change needed; a standing prune timer would be redundant with `builder.gc`, which already runs automatically as part of build activity per buildkit's own design -(the comment in `install-homeserver.sh:431-500` explains why a separate -timer isn't used). +(the comment in `install-homeserver.sh:381-465` — the reasoning at 392-397, +the JSON block itself at 463-465 — explains why a separate timer isn't used). One stray finding, not actioned: a leaked `buildx_buildkit_builder-` container (`docker-container` driver) has been running 45+ hours, diff --git a/docs/dashboard-manual-ip-block-design.md b/docs/dashboard-manual-ip-block-design.md index 2a2d979a3..3689aa7da 100644 --- a/docs/dashboard-manual-ip-block-design.md +++ b/docs/dashboard-manual-ip-block-design.md @@ -100,18 +100,31 @@ that already exists** (home is reachable at `10.8.0.2`, `docs/CGNAT-DEPLOYMENT.md`), the same "pull, don't get pushed to" posture `portbridge-blackhole-refresh.sh` already uses against GitHub. Concretely: -- The dashboard exposes `GET /export/portbridge-manual-blackhole.txt` - (then `dashboard/ip_block.go`'s `serveManualBlackholeExport`, now - `backend-service/src/ip_block.rs`'s `export`) — plain text, one +- The dashboard exposes the export as `GET /api/v1/ip-block-export` + (`backend-service/src/ip_block.rs`'s `export`) — plain text, one IPv4 address per line, the exact format `blackhole.go`'s existing parser already reads. No admin auth on the handler itself, the same posture every - other `/export/*.csv` GET already takes (access control is the network + other `/api/v1/export/*.csv` GET already takes (access control is the network boundary — WireGuard-only reachability — not a second app-layer secret); the data itself (a list of IPs an operator already chose to block) is no more sensitive than the maltrail feed it sits alongside. Reachable from the VPS at `10.8.0.2:19090` — the `dashboard` service's real published port (`arcane/home/honeypot-dashboard/compose.yml`, `${HP_BIND:-10.8.0.2}:19090:8080`), not an assumed default. + **The path moved at the Rust cutover and one caller was not carried across.** + This decision was written against the Go route + `GET /export/portbridge-manual-blackhole.txt` (then `dashboard/ip_block.go`'s + `serveManualBlackholeExport`); the Rust `export` keeps that handler's + *byte-compatible body* but is registered at `/api/v1/ip-block-export`, and + `backend-service/src/main.rs` has no `/export/portbridge-manual-blackhole.txt` + route at all. `vps/portbridge-manual-blackhole-refresh.sh` still defaults its + `MANUAL_BLACKHOLE_URL` to the **old** path + (`http://10.8.0.2:19090/export/portbridge-manual-blackhole.txt`), so on any + deployment that has not overridden that variable the sidecar is fetching a + 404. Either the default needs repointing at `/api/v1/ip-block-export` or the + VPS `.env` must set `MANUAL_BLACKHOLE_URL` explicitly — worth confirming + against the live host before trusting that manual blocks are reaching + portbridge. - A new sidecar, `vps/portbridge-manual-blackhole-refresh.sh`, is a near- verbatim copy of `portbridge-blackhole-refresh.sh` pointed at that URL instead of GitHub's maltrail mirror, writing to a second local file diff --git a/docs/design-lab/README.md b/docs/design-lab/README.md index 2b4b50716..160ab84bb 100644 --- a/docs/design-lab/README.md +++ b/docs/design-lab/README.md @@ -67,5 +67,17 @@ The original lab served variant builds against real Elasticsearch data on ports 19201–19205, driven by an env-guarded Go test that booted the dashboard with a stubbed OIDC session, a `STATIC_DIR` override and nil write-services so the real index stayed read-only. That harness depended on the Go dashboard -and went away with it. A `frontend-next` equivalent needs the same read-only -guarantees; scoped separately. +and went away with it. + +It has since been rebuilt for `frontend-next` as +[`branding/design-lab/lab.mjs`](../../branding/design-lab/lab.mjs) (#1828, +#1935), which serves variants on the same 19201–19205 range and the elements +playground on 19300. `frontend-next` has no nil-write-services handle — it +reaches data over HTTP through two bases — so the read-only guarantee is made +at that seam instead: `BACKEND_URL` goes through a gate that forwards +GET/HEAD and answers 405 to everything else, and `BACKEND_MOUNTED_URL` is +pointed at a stub that answers 503 to every request. That is stronger than the original, which relied +on remembering to pass nil. + +This directory is the redacted public copy and does not carry the harness +itself; run it from `branding/design-lab/`. diff --git a/docs/dionaea-bistreams-retention.md b/docs/dionaea-bistreams-retention.md index 488923132..df6fdea5a 100644 --- a/docs/dionaea-bistreams-retention.md +++ b/docs/dionaea-bistreams-retention.md @@ -1,5 +1,15 @@ # Dionaea bistreams retention — consumer inventory and decision (#2862) +> **Dated decision record — 2026-09-03.** `BISTREAMS_RETENTION_DAYS=30` is +> still the pinned value in `arcane/home/honeypot-payload-analysis/.env.example` +> and the decision has not been reopened. Read the forward-looking sections +> ("nothing is 30 days old yet", "when this window first destroys something — +> 2026-09-09") as written on 2026-09-03: that date has passed, so the pruning +> path is now live rather than pending, and the size projections below are the +> ones that were made then, not current measurements. Re-measure before acting +> on the capacity numbers. The consumer inventory and the forensic argument are +> unaffected by the passage of time and are the substance of this document. + `dionaea-lib`'s `bistreams/` tree holds Dionaea's raw per-connection capture stream: every accepted connection gets a date-named subdirectory (`YYYY-MM-DD/`) full of raw capture files, payload or not — a superset of diff --git a/docs/ip-reporting-plan.md b/docs/ip-reporting-plan.md index 1b396d400..83594a289 100644 --- a/docs/ip-reporting-plan.md +++ b/docs/ip-reporting-plan.md @@ -61,19 +61,23 @@ Primary target: **AbuseIPDB** — widely used, has a public confidence score, an ```mermaid flowchart TD Sensors["Cowrie / Dionaea / Conpot / HTTP-honeypot / DNP3"] - Reporter["reporter (Python)
new Docker Compose service"] + Reporter["reporter (Go)
hp-reporter, dry-run by default"] AbuseIPDB["AbuseIPDB"] Blocklist["Blocklist.de"] + GreyNoise["GreyNoise RIOT
(read-only pre-check)"] Sensors -->|"JSON event logs on the shared Docker volumes, tailed —
not Redis pub-sub; see 'Resolved design decisions'"| Reporter Reporter -->|"POST /api/v2/reports"| AbuseIPDB + Reporter -->|"POST /api"| Blocklist + Reporter -->|"GET /v3/riot/{ip}"| GreyNoise AbuseIPDB -->|optional| Blocklist ``` The `reporter` container: - Watches the same log/event volume already mounted by the `analysis` and `ml-worker` containers - Maintains a local SQLite DB (`/data/reported.db`) to deduplicate IPs per service per 24 h -- Exposes a `/metrics` endpoint (Prometheus) so Grafana can graph reports-per-hour +- Exposes no HTTP listener at all: Phase 4 writes a `metrics.json` snapshot into the + data volume on an interval instead (see the status banner's Phase 4 note) --- @@ -165,27 +169,32 @@ Before reporting, cross-check against: Add to `docker-compose.yml`: +> The block below is the original sketch. It is superseded — see the status +> banner. Three things in it are simply wrong against what shipped, and are +> called out because they are the kind of detail that gets copy-pasted: +> there is **no Prometheus port** (Phase 4 emits `metrics.json` into the data +> volume instead), the Blocklist.de credentials are **`BLOCKLISTDE_SENDER` + +> `BLOCKLISTDE_API_KEY`**, not `BLOCKLIST_DE_EMAIL`/`BLOCKLIST_DE_PASSWORD`, +> and the live switch is `REPORTER_LIVE`. The `whitelist.txt` mount path and +> `REPORTER_COOLDOWN_HOURS` did land as drawn. + ```yaml reporter: build: ./reporter restart: unless-stopped environment: ABUSEIPDB_API_KEY: ${ABUSEIPDB_API_KEY} - BLOCKLIST_DE_EMAIL: ${BLOCKLIST_DE_EMAIL} - BLOCKLIST_DE_PASSWORD: ${BLOCKLIST_DE_PASSWORD} - GREYNOISE_API_KEY: ${GREYNOISE_API_KEY:-} # optional + BLOCKLISTDE_SENDER: ${BLOCKLISTDE_SENDER} + BLOCKLISTDE_API_KEY: ${BLOCKLISTDE_API_KEY} + GREYNOISE_API_KEY: ${GREYNOISE_API_KEY:-} # inert unless GREYNOISE_ENABLED=1 + REPORTER_LIVE: ${REPORTER_LIVE:-} # unset = dry-run REPORTER_COOLDOWN_HOURS: ${REPORTER_COOLDOWN_HOURS:-24} - REPORTER_WHITELIST: /config/whitelist.txt volumes: - cowrie_logs:/logs/cowrie:ro - dionaea_logs:/logs/dionaea:ro - conpot_logs:/logs/conpot:ro - reporter_data:/data - ./reporter/whitelist.txt:/config/whitelist.txt:ro - ports: - - "127.0.0.1:9101:9101" # Prometheus metrics - networks: - - honeypot_internal ``` Add to `.env.example`: @@ -193,9 +202,11 @@ Add to `.env.example`: ```dotenv # IP Blocklist Reporting ABUSEIPDB_API_KEY= -BLOCKLIST_DE_EMAIL= -BLOCKLIST_DE_PASSWORD= +BLOCKLISTDE_SENDER= +BLOCKLISTDE_API_KEY= GREYNOISE_API_KEY= +GREYNOISE_ENABLED=0 +REPORTER_LIVE= # leave empty: the reporter is dry-run until set REPORTER_COOLDOWN_HOURS=24 ``` @@ -225,20 +236,39 @@ The reporter will track a daily counter and pause with exponential back-off on ` ## Files To Create +The Python layout originally sketched here was never built — the shipped +service is Go. This is what +`arcane/home/honeypot-utilities/reporter/` actually contains: + ```mermaid flowchart TD - ReporterDir["reporter/"] --> Dockerfile["Dockerfile"] - ReporterDir --> Requirements["requirements.txt"] - ReporterDir --> ReporterPy["reporter.py
main loop"] - ReporterDir --> SourcesPy["sources.py
per-sensor log parsers"] - ReporterDir --> ApisPy["apis.py
AbuseIPDB + Blocklist.de clients"] - ReporterDir --> DedupPy["dedup.py
SQLite-backed deduplication"] + ReporterDir["reporter/ (Go)"] --> MainGo["main.go
entrypoint, run loop wiring"] + MainGo --> RunloopGo["runloop.go
poll tick"] + RunloopGo --> TailGo["tail.go
per-sensor log tailing"] + TailGo --> EventGo["event.go
normalised event"] + EventGo --> CategorizeGo["categorize.go
sensor/kind to upstream category"] + CategorizeGo --> DedupGo["dedup.go
SQLite-backed deduplication"] + DedupGo --> GreynoiseGo["greynoise.go
RIOT pre-check (Phase 3)"] + GreynoiseGo --> WhitelistGo["whitelist.go
CIDR/IP allowlist"] + WhitelistGo --> BlocklistdeGo["blocklistde.go
Blocklist.de client"] + WhitelistGo --> ReportGo["report.go
AbuseIPDB client"] + GreynoiseGo --> ProcessGo["process.go
report decision + dispatch"] + ProcessGo --> ReportGo + ProcessGo --> BlocklistdeGo + ProcessGo --> MetricsGo["metrics.go
counters to metrics.json"] + ProcessGo --> AuditGo["audit.go
bounded rotating audit log"] ReporterDir --> WhitelistTxt["whitelist.txt
safe IPs/CIDRs to never report"] - ReporterDir --> MetricsPy["metrics.py
Prometheus exporter"] + ReporterDir --> Dockerfile["Dockerfile
FROM scratch, runs as 0:0"] DocsDir["docs/"] --> PlanMd["ip-reporting-plan.md
this file"] ``` +Every function here is exercised by a test, though not always in a +same-named file: `categorize.go` is covered from `event_test.go` and `main.go` ++ `report.go` from `dryrun_test.go`. There is no `requirements.txt`, +no `sources.py`/`apis.py`/`dedup.py`/`metrics.py`, and no Prometheus +exporter — see the Phase 4 note in the status banner. + --- ## Resolved design decisions diff --git a/docs/llm-worker/README.md b/docs/llm-worker/README.md index df0e476e9..72e1ce2cf 100644 --- a/docs/llm-worker/README.md +++ b/docs/llm-worker/README.md @@ -122,6 +122,20 @@ inline scripts read-only. Version 1 accepts regular text files no larger than 1 MiB, refuses symlinks and NUL-containing/binary data, and hashes content itself instead of trusting a filename. +**The deployed entry point is a third file, not this one.** +`arcane/manifests/home-production.json` points the `llm-worker` stack at +`llm-worker/docker-compose.captured-data-deploy.yml` (#1751), not at +`docker-compose.captured-data.yml`. The deploy file is a thin `include:` +wrapper listing `docker-compose.yml` then `docker-compose.captured-data.yml` +in that order, so the behaviour described above is what actually runs — but the +network/mount/volume grant lives in the included file, not the deployed one. +It exists because the captured-data authorization had been applied by hand and +was in no tracked file, so an Arcane sync silently reverted the container to +`synthetic-only` while `LLM_ALLOW_CAPTURED_DATA=true` stayed set (#1751); the +`include` list is a single two-file entry rather than two entries because +`include` does not override the way repeated `-f` does (#2225). Deleting the +file returns the deployment to synthetic-only by design. + ## Guardrails - strict pydantic schemas reject extra keys, invalid enums, malformed ATT&CK diff --git a/docs/ml-worker-evaluation.md b/docs/ml-worker-evaluation.md index 9da6a953a..430f29b5d 100644 --- a/docs/ml-worker-evaluation.md +++ b/docs/ml-worker-evaluation.md @@ -42,7 +42,7 @@ reported alongside accuracy rather than ignored. ### 2026-08-25 — Tier 1 harness landed; Tier 2 blocked -**Tier 1 (behaviour) is live.** `evaluate_detectors.py` runs six contract +**Tier 1 (behaviour) is live.** `evaluate_detectors.py` runs seven contract checks against a candidate over the per-sensor fixture corpus and emits a hashed JSON report. It is the first reusable acceptance bar `ml-worker` has had, and it makes "evaluate this candidate offline" answerable at all. diff --git a/docs/security-fixes.md b/docs/security-fixes.md index 3d2b9f492..cf9dbc234 100644 --- a/docs/security-fixes.md +++ b/docs/security-fixes.md @@ -1,5 +1,16 @@ # Code scanning +> **Dated record — 2026-07-30.** The CodeQL history below is a worked example, +> not a live status page; it is here for the lesson, per "do not mirror a live +> system's state into a markdown file" (which is also why the current alerts +> live in [#80](https://github.com/Xore/APIARY/issues/80) and the +> [Security tab](https://github.com/Xore/APIARY/security/code-scanning)). Every +> code path it names — `dashboard/sandbox.go`, `ghidra.go`, +> `dashboard/static/hp-adminlte.js` — is Go-era and no longer exists: the Go +> dashboard was deleted at #1628 (2026-08-22). The equivalents are now Rust in +> `arcane/home/honeypot-dashboard/backend-service/src/` (`artifacts.rs`, +> `report_pdf.rs`). The method below is unchanged and still current. + Open CodeQL alerts are tracked in [#80](https://github.com/Xore/APIARY/issues/80) and in the [Security tab](https://github.com/Xore/APIARY/security/code-scanning),