VPS Public Surface Audit (2026-09-01 14:00 BST)

**Question from operator**: "Check everything we have on the VPS that is not open to the web. So you have to either have a password or each Mavis has a way to get in."

The architecture (quick recap)

```

Internet

↓ :80/:443

[Caddy :2019 admin] — TLS terminator, on-demand certs

↓

[nginx :8081 plain / :8444 SSL] — vhost routing

↓

Backend services on :3000 (panel), :3001 (my-portal), :3002 (?), :3003 (vibecoder-bridge), :8090 (PocketBase)

↓

[Local services]: systemd units (operator-panel, my-portal, pocketbase, vibecoder-bridge, etc.)

↓

[Filesystem]: /var/www/freshvibeapps/clients/*, /opt/operator/{backups,data,panel,secrets,vault,deploy-*}

```

Public surfaces (38 vhosts found, status check)

1. `panel.freshvibeapps.com` (THE OPERATOR PANEL) — PARTIAL AUTH

| Endpoint | Auth | Notes |

|---|---|---|

| `/` | None | Login page (public) |

| `/api/mavis/me` | Required (401) | ✅ Token required |

| `/api/mavis/threads` | Required (401) | ✅ Token required |

| `/api/mavis/bulletins` | Required (401) | ✅ Token required |

| `/api/mavis/reports` | Required (401) | ✅ Token required |

| **`/api/artifacts`** | **NONE (200)** | ⚠️ **LEAK** — returns all artifact metadata without auth |

| `/api/artifacts/:id` | NONE (200) | ⚠️ Same leak — individual artifact details exposed |

| `/api/deploys` | Required (401) | ✅ Token required |

| **`/api/plans`** | **NONE (200)** | ⚠️ **LEAK** — returns all plan metadata without auth |

| `/api/caddy/allow` | None | The Caddy allow policy endpoint (public by design) |

| `/mavis/timeline` | None (200) | SPA — public, but only renders data behind auth |

**Finding**: artifacts and plans metadata are public. Names, IDs, types, projects, descriptions, dates — all visible. The actual file CONTENT requires knowing the path on `artifacts.freshvibeapps.com` and there it's public by URL design.

2. `artifacts.freshvibeapps.com` (artifact file storage) — PUBLIC BY DESIGN

3. `appx.freshvibeapps.com` (file storage w/ TTL) — PUBLIC

4. `pb.freshvibeapps.com` (PocketBase) — PARTIALLY UP

5. `my.freshvibeapps.com` (my-portal) — PARTIALLY UP

6. `vibecoder.freshvibeapps.com` — PARTIALLY UP

7. Other 30+ vhosts (oscar-platform, gallery, smartpantry, freshcards, hopfan, etc.) — STATIC FILES

8. SSH (port 22) — PASSWORD-ONLY

9. Other ports

Summary of issues

🔴 REAL LEAKS (data exposed to anyone)

1. **`/api/artifacts` returns all artifact metadata** — IDs, names, projects, types, descriptions, dates, content_size, pinned. No auth required. ~860 artifacts visible.

2. **`/api/plans` returns all plan metadata** — titles, descriptions, step counts, status, dates. No auth required. ~1 plan visible (a863).

3. **Caddy-allow endpoint** (intentional) — lets anyone check if a hostname is allowed. Fine but enables recon.

🟡 AUTHENTICATED BUT WEAK

1. **SSH root login with password** — if password leaks, full root. Should be key-only.

2. **CUPS on 0.0.0.0:631** — shouldn't be public, minor risk.

3. **`/api/caddy/allow` returns 200 with hostname info** — gives attackers a way to enumerate allowed subdomains.

🟢 PROPERLY AUTHENTICATED

1. `/api/mavis/*` (threads, bulletins, reports, me, plans-write) — 401 without token

2. `/api/deploys` — 401 without auth

3. PocketBase admin (`_/`) — has its own auth

4. Caddy admin (`:2019`) — localhost-only

5. Backend services (3000-3003, 8090) — localhost-only

6. SSH key auth works for `operator` user (just not root)

Recommended fixes (priority order)

High priority (data leak)

1. **Make `/api/artifacts` require token** (or at least return summary without descriptions/content_size for non-owners)

2. **Make `/api/plans` require token**

Medium priority

3. **Move SSH to key-only** for root (or at least disable password on root)

4. **Block CUPS from public** (`ufw deny 631` or systemctl disable cups)

Low priority

5. **Make Caddy-allow rate-limited** to prevent recon scanning

6. **Add a `/api/auth-status` endpoint** that returns a summary of which endpoints need auth (so Mavis sessions can find this from a discoverable spot)

Mavis access patterns (each thread's way in)

| Method | Auth | Used by |

|---|---|---|

| `/api/mavis/register {name, session_id}` → token | None to register, then Bearer | Every Mavis on first turn |

| `Authorization: Bearer <token>` | Token | All Mavis API calls |

| SSH password (root) | Password | Direct VPS work (most Mavis don't have this) |

| SSH key (operator) | Key | Only Mavis with key file |

| POST bulletin/report | Bearer | All Mavis |

| WebSocket `/ws` | None | SPA only |

**Mavis doesn't need a password** — the token model works. The leaks above are about LISTING data without auth, not about Mavis being able to get in. Mavis can always get in via token.

The question is: **should the LISTING endpoints be public?** Today yes (anyone can see what's in HQ). That might be a feature ("public timeline of Mavis work") or a bug ("Mavis work is supposed to be private to the operator"). The operator needs to decide.