From e3d1dddf516513a6f03011299df689de540b6779 Mon Sep 17 00:00:00 2001 From: Hermes VM Date: Wed, 27 May 2026 20:51:17 +0000 Subject: [PATCH] docs: add VM exposure inventory --- docs/repo-map.md | 1 + docs/vm-exposure-inventory.md | 151 ++++++++++++++++++++++++ docs/vm-security-blind-spots-roadmap.md | 2 +- 3 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 docs/vm-exposure-inventory.md diff --git a/docs/repo-map.md b/docs/repo-map.md index 3fd76af..bf1ea62 100644 --- a/docs/repo-map.md +++ b/docs/repo-map.md @@ -53,6 +53,7 @@ Current key files: - `docs/hermes-setup-upgrade-roadmap.md` - `docs/hermes-operations.md` - `docs/vm-security-blind-spots-roadmap.md` +- `docs/vm-exposure-inventory.md` ### `.github/workflows/` diff --git a/docs/vm-exposure-inventory.md b/docs/vm-exposure-inventory.md new file mode 100644 index 0000000..fdfee2c --- /dev/null +++ b/docs/vm-exposure-inventory.md @@ -0,0 +1,151 @@ +# ByteLyst VM Exposure Inventory + +**Generated:** 2026-05-27 +**Host:** `srv1491630` +**Purpose:** Phase 0 inventory for `docs/vm-security-blind-spots-roadmap.md`. + +This inventory is a pre-change control document. It does not approve exposure by itself. Each `Needs decision` row requires owner approval before firewall, Compose, Caddy, or SSH changes. + +## Classification Key + +| Class | Meaning | Expected Controls | +| --- | --- | --- | +| `public-caddy` | Public app/API intended to be reached through Caddy | Caddy TLS, hostname/path routing, app auth where needed, no direct host-port exposure | +| `public-direct` | Direct host-port access intentionally public | explicit approval, provider/UFW allowance, monitoring | +| `private-admin` | Admin/dev/internal tool | Tailscale/VPN, SSH tunnel, IP allowlist, or auth gate | +| `loopback-only` | Host-local service used by Caddy or local automation | bind `127.0.0.1:port`; no external bind | +| `docker-internal` | Container-to-container only | no host port mapping | +| `retire` | Unused/deprecated | remove service or disable host exposure | +| `needs-decision` | Existing exposure with unknown/unclear intent | owner must classify before remediation | + +## Caddy Public Routes + +| Hostname/path | Upstream | Initial class | Decision needed | +| --- | --- | --- | --- | +| `api.bytelyst.com/platform/*` | `platform-service:4003` | `public-caddy` | Confirm auth posture | +| `api.bytelyst.com/extraction/*` | `extraction-service:4005` | `public-caddy` | Confirm auth posture | +| `api.bytelyst.com/mcp/*` | `mcp-server:4007` | `public-caddy` | Confirm public need | +| `api.bytelyst.com/peakpulse/*` | `peakpulse-backend:4010` | `public-caddy` | Confirm direct host port can close | +| `api.bytelyst.com/chronomind/*` | `chronomind-backend:4011` | `public-caddy` | Confirm direct host port can close | +| `api.bytelyst.com/jarvisjr/*` | `jarvisjr-backend:4012` | `public-caddy` | Confirm direct host port can close | +| `api.bytelyst.com/nomgap/*` | `nomgap-backend:4013` | `public-caddy` | Confirm direct host port can close | +| `api.bytelyst.com/mindlyst/*` | `mindlyst-backend:4014` | `public-caddy` | Confirm direct host port can close | +| `api.bytelyst.com/lysnrai/*` | `lysnrai-backend:4015` | `public-caddy` | Confirm direct host port can close | +| `api.bytelyst.com/notelett/*` | `notelett-backend:4016` | `public-caddy` | Confirm direct host port can close | +| `api.bytelyst.com/flowmonk/*` | `flowmonk-backend:4017` | `public-caddy` | Confirm direct host port can close | +| `api.bytelyst.com/actiontrail/*` | `actiontrail-backend:4020` | `public-caddy` | Confirm direct host port can close | +| `api.bytelyst.com/localmemgpt/*` | `localmemgpt-backend:4019` | `public-caddy` | Confirm direct host port can close | +| `api.bytelyst.com/invttrdg/*` | `invttrdg-backend:4018` | `public-caddy` | Confirm direct host port can close | +| `api.bytelyst.com/devops/*` | `devops-backend:4004` | `private-admin` | Should require auth/private access | +| `gitea.bytelyst.com` | `gitea-npm-registry:3000` | `public-caddy` | Confirm direct `3300` can close | +| `admin.bytelyst.com` | `admin-web:3001` | `private-admin` | Confirm route still resolves; upstream container not in current `docker ps` | +| `devops.bytelyst.com` | `devops-web:3000` | `private-admin` | Should require auth/private access | +| `tracker.bytelyst.com` | `tracker-web:3003` | `public-caddy` | Confirm direct host port can close | +| `llmlab.bytelyst.com` | `llmlab-dashboard:3075` | `private-admin` | Dashboard currently unhealthy; decide public/private/retire | +| `ollama.bytelyst.com` | `172.17.0.1:11434` | `private-admin` | Model endpoint should not be unauthenticated public | +| `trading-api.bytelyst.com` | `trading-backend:5000` | `public-caddy` | Confirm auth posture | +| `invttrdg.bytelyst.com` | `invttrdg-web:3085` | `public-caddy` | Confirm direct host port can close | +| `notes.bytelyst.com` | `notelett-web:3045` | `public-caddy` | Confirm direct host port can close | +| `clock.bytelyst.com` | `chronomind-web:3030` | `public-caddy` | Confirm direct host port can close | + +## Public Bind Inventory + +These listeners were bound on `0.0.0.0` and/or `[::]` during review. + +| Port | Service/container | Owner / Compose source | Current route | Initial class | Proposed action | +| --- | --- | --- | --- | --- | --- | +| `22` | `sshd` | host systemd | direct SSH | `public-direct` | Keep public only after SSH key hardening | +| `80`, `443` | `caddy` | `/opt/bytelyst/learning_ai_common_plat/docker-compose.ecosystem.yml` | public ingress | `public-caddy` | Keep public | +| `3000` | `notelett-web` | `/opt/bytelyst/learning_ai_notes/docker-compose.yml` | `notes.bytelyst.com` | `public-caddy` with direct bypass | Bind loopback or remove host port after Caddy smoke | +| `3002` | `lysnrai-dashboard` | `/opt/bytelyst/learning_ai_common_plat/docker-compose.ecosystem.yml` | none found in Caddy | `needs-decision` | Private/admin or retire direct exposure | +| `3003` | `tracker-web` | `/opt/bytelyst/learning_ai_common_plat/docker-compose.ecosystem.yml` | `tracker.bytelyst.com` | `public-caddy` with direct bypass | Bind loopback or remove host port after Caddy smoke | +| `3030` | `chronomind-web` | `/root/bytelyst.ai/repos/learning_ai_clock/docker-compose.yml` | `clock.bytelyst.com` | `public-caddy` with direct bypass | Bind loopback or remove host port after Caddy smoke | +| `3035` | `jarvisjr-web` | `/opt/bytelyst/learning_ai_common_plat/docker-compose.ecosystem.yml` | none found in Caddy | `needs-decision` | Unhealthy; classify as private/admin or retire | +| `3040` | `flowmonk-web` | `/opt/bytelyst/learning_ai_common_plat/docker-compose.ecosystem.yml` | none found in Caddy | `needs-decision` | Unhealthy; classify as private/admin or retire | +| `3049` | `devops-web` | `/opt/bytelyst/bytelyst-devops-tools/dashboard/docker-compose.yml` | `devops.bytelyst.com` | `private-admin` with direct bypass | Fix old repo path drift, then bind loopback/private | +| `3050` | `mindlyst-web` | `/opt/bytelyst/learning_ai_common_plat/docker-compose.ecosystem.yml` | none found in Caddy | `needs-decision` | Unhealthy; classify as private/admin or retire | +| `3055` | `nomgap-web` | `/opt/bytelyst/learning_ai_common_plat/docker-compose.ecosystem.yml` | none found in Caddy | `needs-decision` | Unhealthy; classify as private/admin or retire | +| `3060` | `actiontrail-web` | `/opt/bytelyst/learning_ai_common_plat/docker-compose.ecosystem.yml` | none found in Caddy | `needs-decision` | Unhealthy; classify as private/admin or retire | +| `3070` | `localmemgpt-web` | `/opt/bytelyst/learning_ai_common_plat/docker-compose.ecosystem.yml` | none found in Caddy | `needs-decision` | Unhealthy; classify as private/admin or retire | +| `3075` | `llmlab-dashboard` | `/opt/bytelyst/learning_ai_common_plat/docker-compose.ecosystem.yml` | `llmlab.bytelyst.com` | `private-admin` with direct bypass | Dashboard unhealthy; gate or retire | +| `3085` | `invttrdg-web` | `/opt/bytelyst/learning_ai_invt_trdg/docker-compose.yml` | `invttrdg.bytelyst.com` | `public-caddy` with direct bypass | Bind loopback or remove host port after Caddy smoke | +| `3100` | `loki` | `/opt/bytelyst/learning_ai_common_plat/docker-compose.ecosystem.yml` | none found in Caddy | `private-admin` | Remove public host bind; keep Docker/internal or Tailscale only | +| `3300` | `gitea-npm-registry` | non-Compose container labels absent | `gitea.bytelyst.com` | `public-caddy` with direct bypass | Bind loopback or private; keep Caddy route | +| `4004` | `devops-backend` | `/opt/bytelyst/learning_ai_devops_tools/dashboard/docker-compose.yml` | `api.bytelyst.com/devops/*` | `private-admin` with direct bypass | Bind loopback/private | +| `4010` | `peakpulse-backend` | `/opt/bytelyst/learning_ai_common_plat/docker-compose.ecosystem.yml` | `api.bytelyst.com/peakpulse/*` | `public-caddy` with direct bypass | Bind loopback or remove host port after Caddy smoke | +| `4011` | `chronomind-backend` | `/root/bytelyst.ai/repos/learning_ai_clock/docker-compose.yml` | `api.bytelyst.com/chronomind/*` | `public-caddy` with direct bypass | Bind loopback or remove host port after Caddy smoke | +| `4012` | `jarvisjr-backend` | `/opt/bytelyst/learning_ai_common_plat/docker-compose.ecosystem.yml` | `api.bytelyst.com/jarvisjr/*` | `public-caddy` with direct bypass | Bind loopback or remove host port after Caddy smoke | +| `4013` | `nomgap-backend` | `/opt/bytelyst/learning_ai_common_plat/docker-compose.ecosystem.yml` | `api.bytelyst.com/nomgap/*` | `public-caddy` with direct bypass | Bind loopback or remove host port after Caddy smoke | +| `4014` | `mindlyst-backend` | `/opt/bytelyst/learning_ai_common_plat/docker-compose.ecosystem.yml` | `api.bytelyst.com/mindlyst/*` | `public-caddy` with direct bypass | Bind loopback or remove host port after Caddy smoke | +| `4015` | `lysnrai-backend` | `/opt/bytelyst/learning_ai_common_plat/docker-compose.ecosystem.yml` | `api.bytelyst.com/lysnrai/*` | `public-caddy` with direct bypass | Bind loopback or remove host port after Caddy smoke | +| `4016` | `notelett-backend` | `/opt/bytelyst/learning_ai_notes/docker-compose.yml` | `api.bytelyst.com/notelett/*` | `public-caddy` with direct bypass | Bind loopback or remove host port after Caddy smoke | +| `4017` | `flowmonk-backend` | `/opt/bytelyst/learning_ai_common_plat/docker-compose.ecosystem.yml` | `api.bytelyst.com/flowmonk/*` | `public-caddy` with direct bypass | Bind loopback or remove host port after Caddy smoke | +| `4019` | `localmemgpt-backend` | `/opt/bytelyst/learning_ai_common_plat/docker-compose.ecosystem.yml` | `api.bytelyst.com/localmemgpt/*` | `public-caddy` with direct bypass | Bind loopback or remove host port after Caddy smoke | +| `4020` | `actiontrail-backend` | `/opt/bytelyst/learning_ai_common_plat/docker-compose.ecosystem.yml` | `api.bytelyst.com/actiontrail/*` | `public-caddy` with direct bypass | Bind loopback or remove host port after Caddy smoke | +| `4025` | `invttrdg-backend` | `/opt/bytelyst/learning_ai_invt_trdg/docker-compose.yml` | `api.bytelyst.com/invttrdg/*` | `public-caddy` with direct bypass | Bind loopback or remove host port after Caddy smoke | +| `1025` | `mailpit` SMTP | `/root/bytelyst.ai/repos/learning_ai_common_plat/docker-compose.yml` | none found in Caddy | `private-admin` / `docker-internal` | Remove public host bind | +| `8025` | `mailpit` UI | `/root/bytelyst.ai/repos/learning_ai_common_plat/docker-compose.yml` | none found in Caddy | `private-admin` | Bind loopback/Tailscale or remove | +| `10000` | `azurite` | `/opt/bytelyst/learning_ai_common_plat/docker-compose.ecosystem.yml` | none found in Caddy | `docker-internal` | Remove public host bind | +| `1234`, `8081` | `cosmos-emulator` | `/opt/bytelyst/learning_ai_common_plat/docker-compose.ecosystem.yml` | none found in Caddy | `docker-internal` | Remove public host bind | +| `11434` | `ollama` host process | host service | `ollama.bytelyst.com` | `private-admin` | Bind loopback/private or auth-gate; do not leave raw public | + +## Non-Public / Internal Listeners + +| Address/port | Process/service | Initial class | Notes | +| --- | --- | --- | --- | +| `127.0.0.53:53`, `127.0.0.54:53` | `systemd-resolve` | host-internal | Expected resolver listeners | +| `127.0.0.1:44561` | `ollama` | host-internal | Secondary loopback listener observed | +| `100.87.53.10:9119`, `100.87.53.10:9120` | `hermes` | private-admin | Tailscale-only bind; keep private | +| `100.87.53.10:51855`, `[fd7a:115c:a1e0::3c33:350a]:43379` | `tailscaled` | private-admin | Tailscale control/data listeners | +| Docker-internal only | `platform-service`, `mcp-server`, `extraction-service`, `prometheus`, `cadvisor`, `node-exporter`, `valkey`, `trading-backend` | docker-internal/private | No direct host bind seen, except Caddy may route to some by service name | + +## Unhealthy Containers At Inventory Time + +| Container | Port exposure | Initial action | +| --- | --- | --- | +| `learning_ai_common_plat-llmlab-dashboard-1` | `0.0.0.0:3075` and Caddy `llmlab.bytelyst.com` | Fix/gate/retire before treating public | +| `learning_ai_common_plat-actiontrail-web-1` | `0.0.0.0:3060` | Classify and fix/retire | +| `learning_ai_common_plat-jarvisjr-web-1` | `0.0.0.0:3035` | Classify and fix/retire | +| `learning_ai_common_plat-localmemgpt-web-1` | `0.0.0.0:3070` | Classify and fix/retire | +| `learning_ai_common_plat-nomgap-web-1` | `0.0.0.0:3055` | Classify and fix/retire | +| `learning_ai_common_plat-flowmonk-web-1` | `0.0.0.0:3040` | Classify and fix/retire | +| `learning_ai_common_plat-mindlyst-web-1` | `0.0.0.0:3050` | Classify and fix/retire | + +## Drift / Follow-Up Findings + +- `devops-backend` runs from `/opt/bytelyst/learning_ai_devops_tools/dashboard/docker-compose.yml`. +- `devops-web` runs from `/opt/bytelyst/bytelyst-devops-tools/dashboard/docker-compose.yml`, an older path. Align this before changing devops dashboard port bindings. +- `gitea-npm-registry` has no Compose labels in Docker inspect output. Find its systemd/compose owner before changing `3300`. +- `admin.bytelyst.com` points at `admin-web:3001`, but no `admin-web` container was present in `docker ps` during this inventory. + +## Proposed First Remediation Groups + +Do these in separate commits/windows with smoke checks after each group. + +1. **Internal emulators and mail tools:** `1025`, `8025`, `10000`, `1234`, `8081`. + - Expected class: `docker-internal` or `private-admin`. + - Preferred fix: remove host port mappings or bind to `127.0.0.1`. +2. **Observability internals:** `3100` and any future Prometheus/Grafana/exporter direct binds. + - Expected class: `private-admin`. + - Preferred fix: Docker-internal or Tailscale-only. +3. **Admin/model surfaces:** `11434`, `3075`, `3049`, `4004`. + - Expected class: `private-admin`. + - Preferred fix: auth gate/private route and no raw public port. +4. **Caddy-backed app/API direct bypass ports:** `3000`, `3003`, `3030`, `3085`, `4010`-`4025`. + - Expected class: `public-caddy`. + - Preferred fix: keep Caddy public, remove raw direct public binds. +5. **SSH:** `22`. + - Expected class: `public-direct`. + - Preferred fix: keep public only after key-only and root-login hardening. + +## Verification Commands + +```bash +date -Is +ss -ltnp +docker ps --format '{{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}' +docker ps -q | xargs -r docker inspect --format '{{.Name}}\tproject={{index .Config.Labels "com.docker.compose.project"}}\tservice={{index .Config.Labels "com.docker.compose.service"}}\tworkdir={{index .Config.Labels "com.docker.compose.project.working_dir"}}\tconfig={{index .Config.Labels "com.docker.compose.project.config_files"}}' +docker exec caddy caddy validate --config /etc/caddy/Caddyfile +sed -n '1,260p' /opt/bytelyst/Caddyfile +iptables -S DOCKER-USER +``` diff --git a/docs/vm-security-blind-spots-roadmap.md b/docs/vm-security-blind-spots-roadmap.md index d360f5a..86f22a9 100644 --- a/docs/vm-security-blind-spots-roadmap.md +++ b/docs/vm-security-blind-spots-roadmap.md @@ -377,7 +377,7 @@ Effective `sshd -T` settings showed: ### Phase 0 — Freeze and inventory before changes - [ ] Freeze new public hostnames/ports until the exposure inventory is complete. -- [ ] Generate `docs/vm-exposure-inventory.md` from Docker, Caddy, `ss`, and DNS. +- [x] Generate `docs/vm-exposure-inventory.md` from Docker, Caddy, `ss`, and DNS. - [ ] Mark each exposed service as `public`, `private`, `internal-only`, or `retire`. - [ ] Review with S before changing public access for customer/user-facing apps.