Sync docs, audit artifacts, project notes, and VerdictTank proposal docs

- audit/phase-one + phase-two: security audit briefs, findings, credential-rotation plan, Docker-USER hardening scripts, rollback refs
- disaster-recovery/restore-test-log.md + backup-dr-audit-2026-08-10.md
- clients/ (modelortho SEO audit, ai-biz-dev competitive landscape), notes/ (tiktok strategy)
- projects/: front-desk-voice-agent, seo-visibility-checker product plan, hotnow-savannah HTML, resend-transactional-email, backup-dashboard-enhancements, code-review-graph, seo-ci-architecture
- proposals/verdicttank/: architecture v4.0, methodology, judge-pool review, consolidation reasoning, cross-check review
- docs/super-search/firecrawl-provider-strategy.md
- updates: CHANGELOG, model-chain, projects-master-readme, intelsight.io
- .gitignore: exclude nested standalone repos (seo-tool, venturebuilt)
This commit is contained in:
root
2026-08-26 02:27:28 -04:00
parent 23e9751d38
commit f5175f1ce0
55 changed files with 14669 additions and 3 deletions
+197
View File
@@ -0,0 +1,197 @@
# NetEng-B - Network Segmentation and Blast-Radius Assessment (Phase One, Read-Only)
**Auditor:** NetEng-B
**Date:** 2026-08-13
**Scope:** Tier classification, inter-host segmentation, blast-radius chains, and product/tenant isolation across the ITPP estate (Core, app1, app2, app3, app1-bu, wphost02).
**Method:** Read-only discovery only (`ip`, `ufw status verbose`, `ss`, `tailscale status --json`, `docker network ls`, `docker inspect`, `cat` of config files, `curl`/TCP reachability probes between hosts). Builds directly on NetEng-A's raw port/DNS inventory (`neteng-a.md`) - no port/DNS re-enumeration performed here. No configuration was changed on any host.
**Severity legend:** Critical = a single compromise gives an attacker control of most or all of the estate, or of a system holding client/security data with no compensating boundary. High = a compromise crosses a trust boundary that should exist (internal-to-client, product-to-product) with material impact. Medium = defense-in-depth gap that increases blast radius but requires a second failure to be catastrophic. Low = hygiene issue with limited blast-radius effect.
---
## 1. Executive summary
- **There is no network segmentation between Core, app1, app2, app3, app1-bu, and wphost02.** All six hosts sit on public Netcup/Hetzner IP space with no VLAN, no subnetting, and no firewall boundary between each other. The only inter-host overlay is a **single flat Tailscale mesh** with **no ACL tags applied to any node** - every server and every personal device (phone, laptop, home router) is in one undifferentiated group with implicit allow-all reachability.
- Because UFW is bypassed by Docker on 4 of 6 hosts (confirmed by NetEng-A and re-verified here), the "segmentation" that exists on paper (UFW allow-lists) does not match what is actually reachable. Verified independently: Core -> app1 and app1 -> app2 both reach each other over their **public IPs**, not just Tailscale, with no filtering in between.
- **Compromise of any one of the six hosts gives an attacker a foothold that can reach every other host** over the public internet (all are mutually pingable/routable on public IPs) and, once inside, over the flat Tailscale mesh as well. There is no host that is network-isolated from the rest.
- **Products and internal ITPP operations are not isolated.** app1 runs the internal LLM gateway (LiteLLM/admin-ai), the internal CRM (Twenty), the security stack (Wazuh), and the reverse proxy for two products (Komodo, TransitPin) all as sibling Docker containers on one host with one Caddy instance. app3 runs a **single shared MySQL/Percona instance** behind ~24 CloudPanel sites that mix internal ops (mainwp, support, panel, ippadmin), client sites (katiewatts, modelortho, vigilanttac, boxpilotlogistics, timapta), and products (transitpin, myverdicttank, buzz, hexclave) with no per-tenant database server or credential vault separating them.
- **Tier misclassification is widespread**: management/admin consoles (Wazuh dashboard+indexer, UniFi controller, UNMS/UISP, CloudPanel admin, backup-restore UI, MinIO console, RAGFlow, Infinity DB, Komodo) are all internet-reachable, several unintentionally via the Docker/UFW bypass NetEng-A documented. These are control planes; none should be directly public.
---
## 2. Tier classification
Legend: **INT** = Internal-only (should never be public), **CLI** = Client-facing (legitimately public), **PROD** = Product/dev (micro-SaaS), **MGMT** = Management/admin control plane.
| Service | Host | Correct tier | Actually public? | Tier violation? |
|---|---|---|---|---|
| SSH (22) all hosts | all | MGMT | Yes (intended, key-only) | No - acceptable exposure |
| Caddy/nginx 80/443 (ingress) | Core, app1, app2, app3, wphost02 | CLI | Yes (intended) | No |
| Grafana :3002 | Core | MGMT | **Yes (explicit UFW allow)** | **Yes - dashboards public** |
| Prometheus/node_exporter/telegraf | Core | INT | No (UFW blocks; Docker bypass N/A, native binaries) | No, but bound 0.0.0.0 (Medium hygiene per NetEng-A) |
| browserless / camofox | Core | INT (SSRF-capable tool) | **Yes - Docker bypass** | **Yes** |
| WireGuard :51821 | Core, app1-bu | MGMT/VPN | Yes (intended) | No |
| Wazuh manager 1514/1515 | app1 | MGMT | Yes (intended, but should be source-restricted) | Partial - over-broad |
| Wazuh indexer :9200, dashboard :5601, API :55000, syslog 514/udp | app1 | MGMT | **Yes - Docker bypass** | **Yes - critical control plane public** |
| Twenty CRM :3003 | app1 | INT (holds client PII) | **Yes - Docker bypass** | **Yes** |
| Komodo core :9120 | app1 | MGMT (deploy/automation control plane) | **Yes - Docker bypass**, also proxied at komodo.iamgmb.com | **Yes** |
| LiteLLM/admin-ai (via Caddy) | app1 | MGMT | Yes (intended, proxied) | No (proxy correct; bypass risk is at Docker layer for adjacent services) |
| TransitPin static site | app1 | PROD | Yes (intended) | No |
| giftaroast.com site + API | app1 | PROD/CLI | Yes (intended) | No |
| UniFi controller 8080/8443/8843/8880 | app2 | MGMT | **Yes - Docker bypass** | **Yes - wireless/tower control plane public** |
| UNMS/UISP 81/8089/8444 | app2 | MGMT | **Yes - Docker bypass** | **Yes - entire WISP management plane public** |
| Technitium DNS :53 | app2 | INT (should be source-restricted to home router only) | **Yes, open resolver - Docker bypass defeats UFW source restriction** | **Yes** |
| Gitea SSH :3022 | app2 | MGMT (source code) | **Yes - Docker bypass** | **Yes** |
| BookStack :6875 | app2 | INT (internal wiki) | **Yes - Docker bypass** | **Yes** |
| support-api :6880 | app2 | INT/CLI boundary (unclear which) | **Yes - Docker bypass** | Flag for clarification |
| MinIO console :9001 | app2 | MGMT | **Yes - Docker bypass** | **Yes** |
| RAGFlow :9380-9392 | app2 | PROD/INT (unclear) | **Yes - Docker bypass** | Flag for clarification |
| Infinity DB :23817/23820 | app2 | INT (database) | **Yes - Docker bypass** | **Yes** |
| Traccar device listeners 5000-5150 | app2 | PROD (device protocol, must be public) | Yes (intended) | No - legitimate, but see APP2-4 in NetEng-A for hardening |
| Hudu, Dawarich | app2 | INT (internal IT docs / location tracking) | Proxied via Caddy (correctly) at hudu.itpropartner.com | No tier issue found at proxy layer |
| CloudPanel admin panel :8443 | app3 | MGMT | **Yes (explicit UFW allow)** | **Yes - hosting control plane for ~24 sites public** |
| backup-restore web UI :8090 | app3 | MGMT (destructive capability) | **Yes (explicit UFW allow)** | **Yes** |
| buzz-prod-relay :3000 | app3 | PROD | **Yes - Docker bypass** | Bypass mechanism wrong even though public exposure may be intended |
| MySQL X protocol :33060 | app3 | INT (database) | Bound `*` but currently UFW-blocked | No public exposure confirmed, hygiene flag only |
| CloudPanel client/product sites (24 total) | app3 | Mixed CLI/PROD/INT (see section 5) | Yes (intended, various) | See section 5 for isolation gap, not tier gap |
| wphost02 nginx-rc 80/443 | wphost02 | CLI (legacy) | Yes (intended, legacy) | No tier issue, but should be decommissioned per NetEng-A |
### Tier-classification counts (from table above)
- **INTERNAL-ONLY services incorrectly public: 12** (Grafana, browserless, camofox, Wazuh indexer/dashboard/API/syslog treated as one group counted once = Wazuh stack, Twenty CRM, Technitium open resolver, BookStack, MinIO console, Infinity DB, plus MySQL X hygiene flag)
- **MANAGEMENT/ADMIN consoles incorrectly public: 6** (Wazuh dashboard, UniFi controller, UNMS/UISP, CloudPanel admin, backup-restore UI, Komodo core)
- **CLIENT-FACING correctly public: 6** (Core/app1/app2/app3/wphost02 web ingress, Traccar device range)
- **PRODUCT/DEV correctly public (by design): 4** (TransitPin, giftaroast.com, buzz relay -- though via wrong mechanism, Traccar)
- **Ambiguous tier, needs Germaine clarification: 2** (support-api, RAGFlow)
---
## 3. Segmentation map
```
PUBLIC INTERNET
|
+--------------------------------+--------------------------------+
| | | | |
Core app1 (Netcup) app2 (Netcup) app3 (Netcup) app1-bu / wphost02
152.53.192.33 152.53.36.131 152.53.39.202 152.53.241.111 (Hetzner, separate /32s)
| | | | |
|<===============+===============+===============+================|
FLAT PUBLIC IP MESH -- every host reaches every other host's
public IP directly. No VLAN. No inter-host firewall. Verified:
Core->app1:*, app1->app2:* all reachable on PUBLIC IPs, unfiltered
for whatever Docker/UFW leaves open on the receiving end.
| | | | |
+----------------+---------------+---------------+----------------+
|
TAILSCALE MESH (100.x.x.x/32 each)
tailscale status --json: NO "Tags" field on ANY
node (Core, app1, app2, app3, app1-bu-1). Default
tailnet ACL = allow-all between all nodes.
app3 has no tailscaled at all listed in this scan --
wait, confirmed: app3 100.72.15.12 IS on tailnet.
wphost02 has NO tailscale client installed at all --
it is reachable ONLY over the flat public internet.
|
+---------------+----------------+------------------+
| | | |
Core app1 app2 app3
(+ personal devices: iphone, m4-mac-mini, ipp-g-lap, home
router "liberty-udm-pro" -- ALL in the SAME flat tailnet group
as the six production servers, no tag-based isolation)
app1-bu-1 (Hetzner standby) is ALSO in this same flat tailnet.
wphost02 -- OUTSIDE the tailnet entirely. Reachable from Core/app1/
app2/app3 only via public internet + SSH key. One-directional trust:
Core has an autossh reverse tunnel INTO wphost02 (mysql-tunnel,
127.0.0.1:33060 -> wphost02:3306) plus a root SSH cron backup job.
wphost02 has no client that can reach back into Core/app1/app2/app3.
INSIDE EACH HOST: dozens of isolated Docker bridge networks per
compose stack (e.g. app1 has 15 separate bridge networks: litellm,
twenty, komodo, n8n, docuseal, etc.) -- this is REAL intra-host
container isolation. But it is undermined by the Docker-publish-vs-UFW
bypass NetEng-A documented: many of those "isolated" containers punch
a hole straight to 0.0.0.0 on the host's public interface, which
erases the isolation the bridge network was providing.
```
**Bottom line: there is effectively ONE trust zone across the entire estate.** The only segmentation primitives in play (UFW allow-lists, per-stack Docker bridge networks, Tailscale) are all either bypassed (UFW/Docker), unused for isolation (Tailscale has no ACL tags), or absent entirely between hosts (no VLAN/subnet separation of any kind exists between Core/app1/app2/app3/app1-bu; wphost02 is flat-public with an SSH+tunnel trust relationship back to Core).
---
## 4. Per-host blast-radius table
| Host | If compromised, attacker gets... | Falls in blast radius | Evidence |
|---|---|---|---|
| **Core** | Root on the Hermes agent host: WireGuard keys to home network + WISP towers (10.77.0.0/24, 10.1.0.0/16, 10.2.0.0/16, 172.16.1.0/24, 172.18.18.0/24 routed via wg0), plaintext L2TP/IPsec creds to home gateway, the `itpp-infra` SSH private key (used to reach ALL 6 hosts), the autossh tunnel credential path into wphost02's MySQL, Grafana/Prometheus telemetry, Tailscale identity (can pivot to every tailnet peer incl. personal devices) | app1, app2, app3, app1-bu, wphost02 (via SSH key + tunnels), home network + WISP towers (via WireGuard), personal devices on tailnet (phone, laptop, home router) | `ip route` shows wg0 routes; `/root/.ssh/itpp-infra` used identically against all 6 hosts in this audit; crontab shows nightly root SSH job to wphost02; NetEng-A VPN-2 documents plaintext creds |
| **app1** | Root on the box hosting Wazuh (SIEM for the whole estate), Twenty CRM (client PII), LiteLLM/admin-ai (the AI control plane and its API keys), Komodo (deployment/automation control plane), n8n (workflow automation with stored credentials) | Every host Wazuh agents report from (SIEM blast radius = estate-wide visibility loss/tamper), every client whose PII sits in Twenty, every downstream system Komodo can deploy to, every credential n8n workflows hold | Docker inspect showed `PG_DATABASE_URL`, `APP_SECRET`, `REDIS_URL` for Twenty; `KOMODO_JWT_SECRET`/`KOMODO_WEBHOOK_SECRET` for Komodo; `DATABASE_URL` for LiteLLM postgres; Caddyfile proxies vault/n8n/ai/admin-ai/noc/wz/komodo/transitpin all from this one host |
| **app2** | Root on the box running UniFi controller (wireless/tower control), UNMS/UISP (entire WISP customer/device management plane), Gitea (source code + SSH deploy keys), Hudu (IT documentation, likely holds more credentials/secrets), Traccar (fleet GPS data), BookStack, RAGFlow, Infinity DB, MinIO | The WISP tower network and its customers, all git repos + CI secrets on Gitea, all documented IT credentials in Hudu, fleet-tracking customer data, any data indexed in RAGFlow/Infinity | `docker inspect` showed `UNMS_PG_HOST`, `SECURE_LINK_SECRET`, Hudu `SECRET_KEY_BASE`/`S3_SECRET_ACCESS_KEY`, Gitea SSH port 3022 public per NetEng-A |
| **app3** | Root on the box running CloudPanel (control plane for ~24 sites) and ONE shared MySQL/Percona instance backing internal ops sites, client sites, and product sites simultaneously | All ~24 CloudPanel-hosted sites: internal (mainwp, support, panel, ippadmin), clients (katiewatts, modelortho, vigilanttac, boxpilotlogistics, timapta), products (transitpin, myverdicttank, buzz, hexclave) - a single DB-engine compromise threatens every one of them at once | `ss -tlnp` showed one `mysqld` process on 127.0.0.1:3306/*:33060; `docker ps` on app3 showed buzz-prod-postgres, hexclave-postgres as separate containers, but CloudPanel's own PHP/static sites run against the single host-level Percona instance shown above |
| **app1-bu** | Root on the warm-standby box. Minimal live footprint (per NetEng-A). Sync direction is **pull-only**: `hermes-standby-sync.sh` pulls from Wasabi S3, does not push to or read live secrets directly off Core over the network | Limited - compromise here does not directly expose Core, because sync is one-way pull from S3, not a live network tunnel to Core. Attacker would get whatever is in the last S3 snapshot (which may include full Hermes state/secrets) | `hermes-standby-sync.sh`: `aws s3 sync s3://hermes-vps-backups/live/ ...`; `authorized_keys` on app1-bu matches Core's `itpp-infra.pub`, meaning Core (not app1-bu) is the initiator of any direct SSH, consistent with pull-based design |
| **wphost02** | Root on legacy WordPress/RunCloud host. Directly exposes MySQL 3306 to Core via the standing autossh tunnel (`-L 127.0.0.1:33060:localhost:3306`) | Core's tunneled MySQL access (`apextrackexperience` DB per `service-health-check.sh`); any WordPress sites still live here | `ps aux` on Core shows `autossh ... -L 127.0.0.1:33060:localhost:3306 -N root@5.161.62.38`; `service-health-check.sh` references `MySQL SSH tunnel (wphost02)` and `MySQL database (apextrackexperience)` |
---
## 5. Blast-radius dependency chains (worst-case, critical services)
1. **Core mysql-tunnel -> wphost02**: Core maintains a permanent autossh reverse tunnel (`127.0.0.1:33060 -> wphost02:3306`) plus a nightly cron root-SSH backup job to wphost02. If Core is compromised, the attacker inherits this tunnel and reaches wphost02's database directly, no additional credential theft required. If wphost02 is compromised first, the exposed MySQL on `localhost:3306` there is only reachable over the SSH tunnel Core already holds the key for - so wphost02 compromise does not directly threaten Core, but Core compromise fully threatens wphost02's DB.
2. **app1 super-search / MCP tooling -> Core**: app1 hosts multiple MCP servers (`mcp-browser`, `mcp-email`, `mcp-git`, `mcp-filesystem`, `super-search`) that are part of the same automation fabric as Core's `super-search` (:8899, allow-listed to `172.17.0.0/16` on Core). A compromised app1 MCP container that can reach Core's Docker subnet peer range could attempt to reach Core's super-search MCP if any cross-host trust exists in the MCP orchestration layer. Full confirmation of live cross-host MCP calls was not possible read-only this session - **flagged for Phase Two verification**, but the underlying network path (Core and app1 both reachable from each other over public IP and Tailscale with no ACL) means the network layer would not stop such a call if the application layer permits it.
3. **Core Wazuh agents -> app1 Wazuh manager**: every host in the estate almost certainly runs a Wazuh agent reporting to app1's manager (1514/1515, intentionally public per NetEng-A APP1-2). If app1's Wazuh stack is compromised (already publicly reachable via the Docker/UFW bypass - APP1-1, Critical), the attacker controls the SIEM for the entire estate: they can see all security telemetry and potentially inject false negatives, blinding detection across Core/app2/app3/app1-bu/wphost02 simultaneously. This is the single highest-leverage compromise in the estate.
4. **app3 shared Percona -> all 24 CloudPanel sites**: any SQL injection, credential leak, or root compromise on app3 threatens every site's database in one blast, because there is one MySQL engine process backing internal, client, and product sites alike (see section 6).
5. **app1-bu standby -> S3 snapshot, not live Core**: because sync is pull-only from Wasabi S3 rather than a live tunnel to Core, app1-bu compromise does NOT directly hand an attacker live access to Core. This is a **positive isolation finding** - call it out as something already done right, worth preserving in Phase Two hardening (don't accidentally add a live push tunnel later).
---
## 6. Product/tenant isolation findings
- **app3 (CloudPanel) hosts ~24 sites behind one shared MySQL/Percona instance** with no per-tenant database server, container, or credential vault: `apx, boxpilotlogistics, buzz, clp, debtreecoveryexperts, docs, drecovery, forms, gmb, hexclave-api, hexclave-dash, iAmGMB, intelsight, ippadmin, katiewatts, mainwp, mockups, modelortho, modelorthowww, myverdicttank, myvoip, panel, proposals, support, timapta, transitpin, transitpin-dash, vigilanttac, voipsimplicity`. This list mixes **internal ITPP ops** (mainwp, support, panel, ippadmin, docs, proposals, mockups), **client sites** (katiewatts, modelortho/modelorthowww, vigilanttac, boxpilotlogistics, timapta), and **products** (transitpin/transitpin-dash, myverdicttank, buzz, hexclave-api/dash) on the same host, same web server, same database engine. Some products (buzz, hexclave) do run their own dedicated Postgres containers alongside the shared Percona instance, which is better isolation for those two, but the CloudPanel-native PHP/static sites (the majority) share the one host-level MySQL.
- **app1 mixes internal control-plane services with two client-facing products in the same Docker/Caddy stack**: Wazuh (SIEM), Twenty CRM (internal, holds PII for debt-recovery-experts.com), LiteLLM/admin-ai (AI gateway), and Komodo (deploy automation) run as containers alongside TransitPin and giftaroast.com (products) and crm.debtrecoveryexperts.com (client-branded CRM instance). A single Caddy instance and a single Docker host boundary is the only thing separating "internal admin tooling" from "product the client interacts with directly."
- **No evidence of per-product credential vaults.** Each product/service has its own DB password baked into its own container's env (Twenty's `APP_SECRET`, Komodo's `KOMODO_JWT_SECRET`, buzz's `BUZZ_S3_SECRET_KEY`, hexclave's `STACK_SERVER_SECRET`, etc.) - that part is correctly per-service. But nothing enforces that these secrets stay scoped: any root compromise of the host reads all of them from `docker inspect`, as this audit itself demonstrated read-only.
- **HotNow was not found** deployed anywhere in this pass (no matching directory/container/proxy entry across app1/app2/app3). **Flag for Phase Two / Git-A cross-reference**: confirm whether HotNow is live, decommissioned, or hosted somewhere outside the six audited servers.
- **VerdictTank / RFP Tank**: found evidence of `myverdicttank` CloudPanel site on app3 and a `/root/.verdicttank-key.json` LiteLLM API key file on app1 (model access key scoped to specific models, not a raw cloud credential) - so VerdictTank spans app1 (LLM access) and app3 (web/DB), with no isolation boundary tying those two halves together other than the key itself.
---
## 7. Shared credentials / shared reverse-proxy / shared database findings
- **Shared reverse proxy (app1 Caddy)**: one Caddyfile terminates TLS for internal tools (vault, n8n, ai, admin-ai, noc, wz/Wazuh) and product/client domains (giftaroast.com, crm.debtrecoveryexperts.com, komodo.iamgmb.com, transitpin.iamgmb.com) side by side. A Caddy-level misconfiguration or compromise affects both tiers simultaneously.
- **Shared reverse proxy (app3 CloudPanel/nginx)**: same pattern - one web-server control plane (CloudPanel, itself a public admin panel) fronts internal, client, and product sites.
- **Shared database engine (app3 Percona/MySQL)**: single instance backs the majority of the 24 CloudPanel sites across all three tiers, as detailed in section 6.
- **Shared Tailscale mesh with no ACL segmentation**: all 6 production hosts AND personal devices (iPhone, MacBook, Mac Mini, home router) are members of one flat tailnet group with no tags observed on any peer. A compromised personal device is one hop from every production server over Tailscale; a compromised production server is one hop from personal devices.
- **Shared SSH key (`itpp-infra`) across all 6 hosts**: this is an operational convenience but means loss of that one private key (which lives on Core) compromises SSH access to the entire estate at once. This is the single most consequential shared credential in the estate.
---
## 8. Findings (severity-rated)
| ID | Severity | Finding | Evidence | Why it matters |
|---|---|---|---|---|
| NETB-1 | **Critical** | **No network segmentation exists anywhere in the estate.** Core, app1, app2, app3, app1-bu are flat on public Netcup/Hetzner IP space with no VLAN or subnet isolation, and the Tailscale overlay that connects most of them has no ACL tags on any node (default allow-all). wphost02 sits fully outside the mesh, reachable only over the open internet. | `tailscale status --json` on Core/app1/app2/app3/app1-bu shows zero `Tags` entries for any peer; `ip route`/`ping` tests confirmed Core, app1, app2 mutually reach each other's PUBLIC IPs directly with no filtering observed between them | If any one host is popped, the attacker is not contained to that host's blast radius alone. They inherit a direct network path (public IP or Tailscale) to every other host and to personal devices, turning a single compromise into an estate-wide incident with no lateral-movement friction. |
| NETB-2 | **Critical** | **Wazuh SIEM stack (indexer, dashboard, manager API) is the single highest-leverage target in the estate** because (a) it is publicly exposed via the Docker/UFW bypass NetEng-A found (APP1-1) and (b) compromising it gives an attacker visibility into and potential control over security telemetry for every other host, which likely reports to it as a Wazuh agent. | app1 Docker/iptables inspection (NetEng-A APP1-1) + this audit's confirmation that app1 accepts inbound connections on its public IP from other estate hosts with no gate | Losing the SIEM is worse than losing any single production host: it blinds detection across the whole estate at the exact moment an attacker needs detection to fail, and it sits on a host with no segmentation from the rest of the fleet. |
| NETB-3 | **Critical** | **app3 shares one MySQL/Percona instance across ~24 sites spanning internal ops, client sites, and micro-SaaS products** with no per-tenant database server or vault boundary. | `ss -tlnp` shows a single `mysqld` on 127.0.0.1:3306/*:33060; CloudPanel site list (`apx, boxpilotlogistics, buzz, ... myverdicttank, panel, support, transitpin, vigilanttac, voipsimplicity`, ~24 total) all on one host | A SQLi or credential leak against ANY one of the 24 sites is a plausible path to every other site's data on the same instance, including internal ITPP operational data (mainwp, support, panel) sitting next to client and product databases. |
| NETB-4 | **High** | **Management/admin control planes are public with no compensating network boundary**: UniFi controller (app2), UNMS/UISP (app2), CloudPanel admin panel (app3), backup-restore UI (app3), Komodo (app1). Several via unintended Docker/UFW bypass, some via explicit-but-broad UFW rules. | NetEng-A APP1-1/2/3, APP2-1/2, APP3-1/2; cross-referenced here against tier classification (section 2) | Each of these is a "master key" for a whole subsystem (wireless network, ISP customer management, ~24 hosted sites, backup/restore capability, deploy automation). None should be reachable without a network-level gate (VPN, allow-list, or reverse-proxy auth) in addition to application login. |
| NETB-5 | **High** | **Internal tooling and client-facing/product systems share the same Docker host and reverse proxy with no boundary** on app1 (Wazuh + Twenty CRM + LiteLLM + Komodo alongside TransitPin, giftaroast.com, crm.debtrecoveryexperts.com) and app3 (CloudPanel mixing mainwp/support/panel with client and product sites). | app1 Caddyfile listing both internal and product/client domains; app3 site directory listing (section 6) | A compromise anywhere on either host crosses tiers for free - there is no reason a breach that starts in a low-value product site should reach the internal CRM or deploy-automation control plane, but on these hosts it can, because nothing stops it at the network layer. |
| NETB-6 | **High** | **Estate-wide single SSH key (`itpp-infra`) is the sole authentication factor for root on all 6 hosts**, and it lives on Core alongside a live autossh tunnel into wphost02's database and WireGuard routes into the home network and WISP towers. | This audit used the same key against all 6 hosts successfully; `ip route`/`ps aux` on Core show the WireGuard routes and the standing tunnel | Core is a de facto master key for the entire estate plus the home network and WISP towers. Its compromise is strictly worse than any other single host's compromise and there is no network segmentation limiting the blast radius once that key is in an attacker's hands. |
| NETB-7 | **Medium** | **Tailscale mesh includes personal devices (phone, laptop, Mac Mini, home router) in the same untagged group as all six production servers.** | `tailscale status` output on every host lists `iphone-15-pro-max`, `m4-mac-mini`, `ipp-g-lap`, `liberty-udm-pro` alongside `core/app1/app2/app3/app1-bu` with no tag differentiation | A compromised personal device (phishing, stolen laptop, malicious app) is one network hop from production infrastructure with no policy boundary in between. Tailscale ACL tags exist specifically to prevent this and are not being used. |
| NETB-8 | **Medium** | **Product credentials for HotNow/VerdictTank/RFP Tank/TransitPin are scoped per-service but not vaulted**, and the products themselves are split across hosts (app1 for LLM access key, app3 for web/DB) with no documented boundary tying the split together. | `/root/.verdicttank-key.json` on app1 (model-scoped LiteLLM key); `myverdicttank`, `transitpin`, `transitpin-dash` CloudPanel sites on app3 | If the split is intentional, it should be documented as an architecture decision with an explicit trust boundary; as observed, it looks like an artifact of convenience rather than a designed isolation boundary, which risks silent credential sprawl as more products are added. |
| NETB-9 | **Low** | **app1-bu standby sync design is pull-only from S3 (not a live tunnel to Core)** - noted as a positive control worth preserving explicitly in Phase Two policy, not a finding to remediate. | `hermes-standby-sync.sh`: `aws s3 sync s3://hermes-vps-backups/live/ ...`; `authorized_keys` on app1-bu matches Core's public key (Core-initiated direction only) | Call this out so future changes to the standby (e.g. adding a live replication tunnel) are evaluated against the blast-radius benefit of the current pull-only design before being adopted. |
### Severity summary
- Critical: 3 (NETB-1, NETB-2, NETB-3)
- High: 3 (NETB-4, NETB-5, NETB-6)
- Medium: 2 (NETB-7, NETB-8)
- Low: 1 (NETB-9, informational/positive-control note)
---
## 9. Access limitations / items requiring Phase Two follow-up
- Could not confirm from read-only evidence whether app1's MCP tooling makes live cross-host calls into Core's super-search MCP (:8899) - the network path exists and is unfiltered, but application-layer confirmation needs a Phase Two trace (log review, not a live test).
- Could not run `clpctl db:show:master-credentials` per-site on app3 (would require a site name argument and returns credentials - out of scope for a read-only, no-credential-exposure audit) to confirm whether each CloudPanel site has a distinct MySQL user/schema on the shared instance versus a fully shared root-level credential. Section 6's finding stands regardless (one instance = one blast radius for the engine itself), but Phase Two should verify per-site credential scoping.
- Tailscale ACL policy file itself (the tailnet admin console's actual ACL JSON) was not inspected - only the absence of `Tags` on every peer via `tailscale status --json`, which is strong but indirect evidence of default-allow-all. Phase Two with tailnet-admin access should pull the actual ACL policy to confirm.
- HotNow was not located on any of the six audited servers. Needs confirmation from Git-A/Docs-W on whether it is live elsewhere, decommissioned, or renamed.
---
**Note on remediation:** all items above are Phase Two candidates. No firewall, container, DNS, database, or Tailscale configuration was changed on any host during this audit.