Restore source files from Google Drive backup
This commit is contained in:
1 parent
dff2b5a9c4
commit
328525b37f
27 files changed
+975
-106
No files matched your search
@@ -0,0 +1,174 @@
|
||||
# Public read-only mirror (`PUBLIC_READONLY`)
|
||||
|
||||
CraftDeck is an **admin** panel: it mounts the Docker socket and the project
|
||||
directory, runs console commands, edits files, and manages mods. It must only
|
||||
ever be reachable on a trusted LAN/VPN (see [networking.md](networking.md)).
|
||||
|
||||
`PUBLIC_READONLY=1` turns the **same image** into a locked-down, anonymous,
|
||||
read-only mirror that is safe to expose to the internet — a live "status page"
|
||||
for a portfolio. It cannot touch the server: no login, no Docker socket, no
|
||||
RCON, no project files, no secrets. It serves a pre-exported **feed** and
|
||||
nothing else.
|
||||
|
||||
## How it works
|
||||
|
||||
```
|
||||
LAN box (trusted) Public host (untrusted)
|
||||
┌───────────────────────────┐ ┌────────────────────────────┐
|
||||
│ dashboard (admin) │ data/dashboard │ feed-export (sidecar) │
|
||||
│ Docker socket + RCON │ craftdeck.db │ reads DB read-only │
|
||||
│ writes craftdeck.db ─────┼──────ro────────▶│ writes feed.db │
|
||||
└───────────────────────────┘ │ │ data/feed │
|
||||
│ ▼ │
|
||||
│ dashboard-public │
|
||||
│ PUBLIC_READONLY=1 │
|
||||
│ serves feed.db only │
|
||||
└────────────────────────────┘
|
||||
```
|
||||
|
||||
Two independent guards keep it safe:
|
||||
|
||||
1. **Middleware allowlist** (`dashboard/src/proxy.ts`) — default-deny. When the
|
||||
flag is set, **every** path returns `404` unless it is on a tiny GET-only
|
||||
allowlist. Non-GET methods are refused before anything else, so every
|
||||
`POST/PUT/PATCH/DELETE` route (console, power, files, mods, config, auth…)
|
||||
is unreachable. `404` (not `401/403`) is used so the public deployment
|
||||
never even hints an admin app exists.
|
||||
|
||||
2. **Data isolation** — in public mode the sampler, log watcher, auth watcher,
|
||||
scheduler, RCON client and Docker client are **never imported** (they are
|
||||
loaded via dynamic `import()` only in the admin code paths — see
|
||||
`dashboard/src/server/boot.ts` and the allowed route handlers). All reads
|
||||
come from the feed file at `FEED_PATH` via `dashboard/src/server/feed.ts`.
|
||||
|
||||
### The allowlist (enforced in `src/proxy.ts`)
|
||||
|
||||
| Surface | Path | Source in public mode |
|
||||
|---|---|---|
|
||||
| Overview page | `GET /` | feed |
|
||||
| Players page | `GET /players` | feed |
|
||||
| Performance page | `GET /performance` | feed |
|
||||
| Health probe | `GET /api/health` | static |
|
||||
| Server status | `GET /api/server` | feed status blob |
|
||||
| Metrics history | `GET /api/metrics` | feed `metrics` table |
|
||||
| Activity | `GET /api/activity` | feed `activity` (join/leave only) |
|
||||
| Players | `GET /api/players` | feed status (names + counts only) |
|
||||
| Lag / tick health | `GET /api/lag` | feed `metrics` (no per-player ping) |
|
||||
| Live stream (SSE) | `GET /api/stream/events` | feed (metrics + join/leave frames only) |
|
||||
|
||||
**Everything else → 404**, explicitly including: `/console`, `/access`,
|
||||
`/security`, `/mods`, `/backups`, `/files`, `/config`, `/schedules`,
|
||||
`/settings`, `/login`; all of `/api/auth/*`, `/api/admin`, `/api/approved`,
|
||||
`/api/notifications`, `/api/console`, `/api/server/power`, `/api/world`,
|
||||
`/api/mods*`, `/api/files*`, `/api/backups*`, `/api/config*`, `/api/schedules`,
|
||||
`/api/security`, `/api/settings*`, `/api/lag/healthreport`, and
|
||||
**`/api/stream/logs`** (the raw console stream). Public mode has **no login** —
|
||||
it is anonymous read-only, so there is no session to protect.
|
||||
|
||||
## The feed
|
||||
|
||||
`dashboard/scripts/export-feed.mjs` reads the live panel DB **read-only** and
|
||||
writes a separate SQLite file containing **only**:
|
||||
|
||||
- `metrics` — the sampler time-series (last 7 days)
|
||||
- `activity` — **join/leave events only**, as `(id, ts, type, player)`. The
|
||||
free-text `detail` column is dropped, so IP addresses that appear on
|
||||
`connect`/`blocked` rows can never reach the feed.
|
||||
- `feed_meta` — one small status JSON (running, online names, counts, version)
|
||||
|
||||
It **never** copies `panel_users`, `approved_players`, `settings`,
|
||||
`notifications`, `schedules`, or `ping_samples`. The file is written to a temp
|
||||
path and atomically renamed, so readers never see a half-written feed; the
|
||||
reader hot-reloads when the file's mtime changes. A status blob older than 5
|
||||
minutes is treated as "server offline" so a dead exporter can't show a stale
|
||||
"online".
|
||||
|
||||
## Running it
|
||||
|
||||
### Option A — compose sidecars (on the LAN box)
|
||||
|
||||
The `public` profile adds two services (excluded from the normal `up`):
|
||||
|
||||
```bash
|
||||
# normal admin stack (unchanged)
|
||||
docker compose up -d
|
||||
|
||||
# add the public mirror + its feed exporter
|
||||
docker compose --profile public up -d
|
||||
|
||||
# the mirror is now on http://<host>:8090 (override with DASHBOARD_PUBLIC_PORT)
|
||||
```
|
||||
|
||||
- `feed-export` — reads `data/dashboard/craftdeck.db` (ro) → writes
|
||||
`data/feed/feed.db` every `FEED_INTERVAL_SECONDS` (default 30).
|
||||
- `dashboard-public` — `PUBLIC_READONLY=1`, mounts **only** `data/feed` (ro).
|
||||
No `docker.sock`, no `./:/project`, no `RCON_PASSWORD`, no
|
||||
`PANEL_SESSION_SECRET`.
|
||||
|
||||
To actually expose it, run `dashboard-public` on a separate untrusted host and
|
||||
ship `data/feed/feed.db` to it (rsync/scp/object storage). Put TLS + the
|
||||
`Secure` cookie concerns aside — there is no cookie — but do front it with a
|
||||
reverse proxy for HTTPS.
|
||||
|
||||
### Option B — cron on the LAN box
|
||||
|
||||
```cron
|
||||
# refresh the public feed every minute
|
||||
* * * * * cd /path/to/minecraft/dashboard && \
|
||||
SOURCE_DB=../data/dashboard/craftdeck.db FEED_PATH=../data/feed/feed.db \
|
||||
MC_VERSION=26.2 SERVER_TYPE=FABRIC node scripts/export-feed.mjs >/dev/null 2>&1
|
||||
```
|
||||
|
||||
Then run the public dashboard container/image anywhere with
|
||||
`PUBLIC_READONLY=1` and `FEED_PATH` pointing at the copied `feed.db`.
|
||||
|
||||
### Environment variables
|
||||
|
||||
| Var | Where | Meaning |
|
||||
|---|---|---|
|
||||
| `PUBLIC_READONLY` | dashboard-public | `1` enables read-only mirror mode |
|
||||
| `FEED_PATH` | dashboard-public + exporter | path to `feed.db` |
|
||||
| `SOURCE_DB` | exporter | path to the live panel DB (read-only) |
|
||||
| `MC_VERSION`, `SERVER_TYPE` | exporter (optional) | shown on the status blob |
|
||||
| `FEED_INTERVAL_SECONDS` | feed-export sidecar | export cadence (default 30) |
|
||||
| `DASHBOARD_PUBLIC_PORT` | compose | host port for the mirror (default 8090) |
|
||||
|
||||
## Security checklist (verified)
|
||||
|
||||
| # | Check | Result |
|
||||
|---|---|---|
|
||||
| 1 | No `POST/PUT/PATCH/DELETE` reaches any handler in public mode | **PASS** — `proxy.ts` refuses every non-GET before routing (`method !== "GET" && !== "HEAD" → 404`) |
|
||||
| 2 | Admin pages/APIs 404 (console, files, mods, backups, config, schedules, settings, access, security, admin, approved, notifications, world, power) | **PASS** — default-deny allowlist; curl matrix below |
|
||||
| 3 | Auth routes 404; no login in public mode | **PASS** — `GET /api/auth/me`, `POST /api/auth/login`, `GET /login` all 404 |
|
||||
| 4 | Raw console SSE (`/api/stream/logs`) unreachable | **PASS** — not on the allowlist → 404 |
|
||||
| 5 | `/api/stream/events` emits **only** `metrics` + join/leave; never notifications or console lines | **PASS** — notification listener not registered in public mode; `mc-event` filtered to `join`/`leave`; `detail`/IP stripped |
|
||||
| 6 | RCON / Docker / sampler modules never imported in public mode | **PASS** — grep shows no static `server/{docker,rcon,mc,sampler}` import in `boot.ts`, `api.ts`, or any allowed route; error classes live in `server/errors.ts` |
|
||||
| 7 | Feed excludes `panel_users`, `approved_players`, `settings`, `notifications`, `schedules`, `ping_samples`, and the activity `detail`/IP column | **PASS** — feed has only `metrics`, `activity(id,ts,type,player)`, `feed_meta`; leak scan for hashes/IPs/notification text found nothing |
|
||||
| 8 | `dashboard-public` has no docker.sock / project dir / RCON or session secret | **PASS** — `docker compose --profile public config` shows only `data/feed:ro` |
|
||||
| 9 | Admin mode unchanged when the flag is unset | **PASS** — auth still enforced (`/api/server`→401, `/console`→307→login); full panel services boot |
|
||||
| 10 | Committed `dev-data` cannot ship in an image | **PASS** — `dashboard/.dockerignore` excludes `dev-data` |
|
||||
|
||||
### curl verification (booted with `PUBLIC_READONLY=1` + a generated feed)
|
||||
|
||||
```
|
||||
ALLOWED (200): / /players /performance
|
||||
/api/health /api/metrics /api/server /api/players /api/activity /api/lag
|
||||
BLOCKED (404): /console /settings /login
|
||||
GET /api/auth/me · POST /api/auth/login · GET /api/notifications
|
||||
GET /api/admin · POST /api/console · POST /api/server/power
|
||||
GET /api/stream/logs · GET /api/world · POST /api/metrics
|
||||
```
|
||||
|
||||
A leak scan across every allowed API response found no password hashes, IP
|
||||
addresses, or notification content. With a fresh feed the mirror served live
|
||||
data (`/api/server` running=true, 2 players; `/api/metrics` 360 buckets;
|
||||
`/api/lag` verdict `all-good`).
|
||||
|
||||
## Note on the committed dev database
|
||||
|
||||
`dashboard/dev-data/app/craftdeck.db` is git-ignored (root `.gitignore` →
|
||||
`dashboard/dev-data/`) and its `panel_users` table **does contain a real
|
||||
scrypt password hash** for the `admin` user (`must_change=1`). It is a local
|
||||
dev seed, not a production credential, but it is a hash nonetheless — it must
|
||||
never end up in a built image. `dashboard/.dockerignore` now excludes
|
||||
`dev-data`, and the public mirror never mounts it. Do not commit this file.
|
||||
Reference in new issue
Block a user