backup: pre-hardening baseline

This commit is contained in:
backup 2026-09-02 11:20:31 -05:00
commit 9e4c612dcb
57 changed files with 10393 additions and 0 deletions

314
ARCHITECTURE.md Normal file
View file

@ -0,0 +1,314 @@
# Traefik GUI - Architecture Document
## 1. Backend Directory Structure
```
backend/
├── cmd/
│ └── traefik-gui/ # Main entry point
│ └── main.go
├── internal/
│ ├── api/ # REST API handlers
│ │ ├── handlers/
│ │ │ ├── auth.go # Login, logout, session validation
│ │ │ ├── health.go # Health check endpoint
│ │ │ ├── config.go # File-provider config management (Phase 2)
│ │ │ └── traefik.go # Traefik status proxy (read-only)
│ │ ├── middleware/
│ │ │ ├── auth.go # Session + CSRF validation
│ │ │ ├── cors.go # CORS handling (inline in auth.go)
│ │ │ └── logging.go # Request logging
│ │ ├── routes.go # Route registration
│ │ └── server.go # HTTP server setup
│ ├── auth/ # Authentication logic
│ │ ├── session.go # Session management (SQLite-backed)
│ │ ├── password.go # Password hashing (bcrypt)
│ │ └── csrf.go # CSRF protection helpers
│ ├── config/
│ │ ├── file/ # File-provider service (Phase 2)
│ │ │ ├── service.go # Atomic writes, validation, backup, rollback
│ │ │ ├── validate.go # YAML/TOML validation via parser
│ │ │ ├── diff.go # Unified diff generation
│ │ │ └── lock.go # File-level advisory lock for concurrency
│ │ ├── adapters/ # Provider adapters (stubs Phase 2)
│ │ │ ├── docker.go # Docker labels adapter (stub)
│ │ │ └── kubernetes.go # Kubernetes CRD adapter (stub)
│ │ ├── env.go # Environment variable config
│ │ └── types.go # Config structs
│ ├── database/ # Database layer
│ │ ├── sqlite.go # SQLite connection & migrations
│ │ └── repositories/
│ │ ├── user.go # User repository
│ │ └── session.go # Session repository
│ ├── models/
│ │ ├── user.go
│ │ ├── session.go
│ │ └── traefik.go # Traefik resource models
│ └── traefik/ # Traefik read-only integration
│ ├── client.go # Traefik API client interface (read-only)
│ ├── mock.go # Mock implementation for local MVP
│ └── fileprovider.go # Deprecated: use config/file/service.go
├── pkg/
│ └── version/
│ └── version.go
├── go.mod
├── go.sum
└── Makefile
```
## 2. API Routes (Phase 2 — Exact)
| Method | Path | Description | Auth | CSRF | Role |
|--------|------|-------------|------|------|------|
| GET | `/api/health` | Health check | No | No | — |
| GET | `/api/ready` | Readiness (DB + config dir writable) | No | No | — |
| POST | `/api/auth/login` | Login `{username,password}` | No | No | — |
| POST | `/api/auth/logout` | Logout + session delete | Yes | Yes | any |
| GET | `/api/auth/me` | Current user | Yes | No | any |
| GET | `/api/traefik/overview` | Read-only overview (mock or proxy) | Yes | No | any |
| GET | `/api/traefik/routers` | Read-only routers | Yes | No | any |
| GET | `/api/traefik/services` | Read-only services | Yes | No | any |
| GET | `/api/traefik/middlewares` | Read-only middlewares | Yes | No | any |
| GET | `/api/traefik/certificates` | Read-only certificates | Yes | No | any |
| GET | `/api/traefik/entrypoints` | Read-only entrypoints | Yes | No | any |
| GET | `/api/config/files` | List files in `configs/dynamic/` | Yes | No | any |
| GET | `/api/config/files/:name` | Read raw YAML file | Yes | No | any |
| POST | `/api/config/preview` | Validate YAML + return unified diff (no write) | Yes | Yes | admin,operator |
| POST | `/api/config/apply` | Confirm + atomic write + backup (requires `confirm:true`) | Yes | Yes | admin,operator |
| POST | `/api/config/rollback` | Restore last backup / previous git commit | Yes | Yes | admin |
| GET | `/api/config/history` | List backups / commits | Yes | No | admin,operator |
| GET | `/api/config/validate` | Validate raw YAML body (no write) | Yes | Yes | any |
| — | `/api/config/routers` etc. (legacy mock) | Deprecated, kept for dashboard reads | Yes | No | any |
**Notes:**
- All `/api/config/*` write operations require `RequireAuth` + `RequireCSRF` + role check.
- `POST /api/config/preview` does **not** write; it validates and diffs in memory.
- `POST /api/config/apply` requires `{"filename":"app.yml","content":"...","confirm":true}` and is rejected if `confirm` missing/false.
- Traefik API integration is **read-only**; the GUI never pushes config via Traefik API.
## 3. Authentication Flow
```
┌─────────┐ POST /api/auth/login ┌─────────┐
│ Browser │ ───────────────────────────▶ │ Backend │
└─────────┘ {username, password} └────┬────┘
│
┌───────────────┴───────────────┐
▼ ▼
Validate credentials Create session
│ │
▼ ▼
┌─────────────┐ ┌─────────────────┐
│ SQLite │ │ Set-Cookie: │
│ (users) │ │ session=<id>; │
└─────────────┘ │ HttpOnly; │
│ Secure (prod); │
│ SameSite=Lax │
└────────┬────────┘
│
Set-Cookie header │
(HttpOnly, Secure) │
▼
┌─────────┐ Subsequent requests ┌─────────┐
│ Browser │ ───────────────────────────▶ │ Backend │
└─────────┘ Cookie: session=<id> └────┬────┘
Header: X-CSRF-Token │
┌───────────────┴───────────────┐
▼ ▼
Validate session Load user + role
(SQLite lookup) (attach to ctx)
│ │
▼ ▼
CSRF check (state-changing) Role check (admin/operator/viewer)
```
**Session Details:**
- Stored in SQLite: `id`, `user_id`, `csrf_token`, `created_at`, `expires_at`
- 24-hour expiry, deleted on logout or expiry
- CSRF token: random 32 chars, rotated on successful write, validated via `X-CSRF-Token` header for POST/PUT/DELETE
- Secure, HttpOnly, SameSite=Lax; `Secure` flag enabled in production (`GUI_DEV_MODE=false`), disabled in dev
- Viewer can read; operator can preview/apply; admin can preview/apply/rollback/history
**Admin Password (dev-only note):**
- Default `admin / changeme` is **development-only** and created only if `users` table is empty.
- Production **must** set `GUI_ADMIN_PASSWORD` env var (min 12 chars). If `GUI_ADMIN_PASSWORD` is set, the GUI hashes it with bcrypt and creates/updates the admin user on startup. If neither default nor env var is acceptable, operator must provision the admin via direct DB insert before first run.
- On startup the GUI logs a warning if the default password is in use and `GUI_DEV_MODE=false`.
## 4. Database Schema (SQLite)
```sql
-- Users table
CREATE TABLE users (
id TEXT PRIMARY KEY,
username TEXT UNIQUE NOT NULL,
email TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL, -- bcrypt
role TEXT NOT NULL DEFAULT 'viewer',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
last_login DATETIME
);
-- Sessions table
CREATE TABLE sessions (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
csrf_token TEXT NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires_at DATETIME NOT NULL,
CHECK (expires_at > created_at)
);
CREATE INDEX idx_sessions_user_id ON sessions(user_id);
CREATE INDEX idx_sessions_expires_at ON sessions(expires_at);
-- Backups table (Phase 2) — tracks pre-apply snapshots for rollback
CREATE TABLE backups (
id TEXT PRIMARY KEY, -- UUID
filename TEXT NOT NULL,
content TEXT NOT NULL, -- full previous file content (or empty if new file)
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
created_by TEXT NOT NULL REFERENCES users(id),
reason TEXT NOT NULL -- "apply", "rollback"
);
CREATE INDEX idx_backups_filename ON backups(filename);
-- Settings table (key-value for GUI settings)
CREATE TABLE settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
```
## 5. Configuration Lifecycle (Phase 2 — File Provider)
**Scope:** The GUI **only** manages files under `configs/dynamic/` (Traefik file provider directory). It **never** modifies Traefik static configuration (`traefik.yml`, entryPoints, providers, certificatesResolvers).
```
[User edits in GUI]
│
▼
POST /api/config/preview {filename, content}
│ 1) Sanitize filename (no traversal, must end .yml/.yaml/.toml)
│ 2) Reject empty / dangerous content (see Validation)
│ 3) YAML parse via parser (gopkg.in/yaml.v3) + structural checks
│ 4) Generate unified diff vs current file on disk
▼
{valid, errors[], diff, warnings}
│ User confirms
▼
POST /api/config/apply {filename, content, confirm:true}
│ 1) Re-validate (same as preview)
│ 2) Acquire per-file advisory lock (flock)
│ 3) Create backup: copy current file → backups table + filesystem backups/<filename>.bak.<timestamp>
│ 4) Atomic write: write content → <filename>.tmp.<rand> → fsync → rename <filename>
│ 5) Optional: git commit if repo detected (`configs/` is git worktree)
│ 6) Release lock
▼
Traefik file provider (watch:true) detects rename → hot-reload dynamic config (no restart)
│
▼
GET /api/traefik/overview reflects new routers/services status (enabled/warning/disabled)
```
**Validation and Rollback Behavior:**
- **Validation:** Uses YAML parser (`gopkg.in/yaml.v3`) plus structural checks:
- Rejects empty content, content without `http:`/`tcp:`/`udp:`/`tls:` top-level keys, or YAML syntax errors (returned with line/col).
- Rejects dangerous patterns: `filename` containing `..` or `/` segments escaping `configs/dynamic/`, absolute paths, or non-whitelisted extensions.
- Rejects files that would overwrite static config paths.
- Errors are returned without writing; HTTP 400 with `{"valid":false,"errors":[...]}`.
- **Atomic writes:** Write to `configs/dynamic/<filename>.tmp.<8-char-rand>` (0600), `fsync`, then `os.Rename` (atomic on POSIX). Temp files are cleaned on error.
- **Backup:** Before every successful apply, the previous content (or empty if new file) is stored in `backups` table + `configs/backups/<filename>.<unix>.bak`. Retention: DB keeps last 50 per file; filesystem keeps last 20 (pruned on apply).
- **Rollback:** `POST /api/config/rollback {filename, backupId?}` restores the most recent (or specified) backup via the same atomic write path, creating a new backup of the current content before restoring. `GET /api/config/history?filename=` lists backups. Rollback is atomic and validated.
- **Concurrency:** Per-file `sync.Mutex` + advisory `flock` on `configs/dynamic/.lock.<filename>` prevents concurrent writers from interleaving. Second concurrent `apply` gets 409 Conflict.
- **File ownership:** GUI process owns `configs/dynamic/`; files are created with `0644`. Traefik watches the directory as read-only in Docker (`:ro` on host, but GUI container has `:rw` on `configs/dynamic/`). No chown is performed.
## 6. Local Development Setup
### Traefik Configuration (Development)
```yaml
# docker/traefik.dev.yml
api:
dashboard: true
insecure: true # Only for local dev!
entryPoints:
web: { address: ":80" }
websecure: { address: ":443" }
providers:
file:
directory: "/etc/traefik/dynamic"
watch: true
log: { level: DEBUG }
```
### Docker Compose Services
```yaml
services:
traefik-gui:
build: .
ports: ["8080:8080"]
environment:
- GUI_DB_PATH=/data/traefik-gui.db
- GUI_SESSION_SECRET=dev-secret-change-in-production
- GUI_ADMIN_PASSWORD=changeme # dev-only; must be changed in prod
- GUI_CORS_ORIGIN=http://localhost:5173
- TRAEFIK_API_URL=http://traefik:8080/api
volumes:
- ./data:/data
- ./configs:/etc/traefik-gui/configs
traefik:
image: traefik:v3.7
ports: ["80:80","443:443","8081:8080"]
volumes:
- ./docker/traefik.dev.yml:/etc/traefik/traefik.yml:ro
- ./configs/dynamic:/etc/traefik/dynamic:ro
```
### Environment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `GUI_DB_PATH` | `./data/traefik-gui.db` | SQLite path |
| `GUI_SESSION_SECRET` | Required | Session signing secret (32+ chars) |
| `GUI_ADMIN_PASSWORD` | *(none — dev creates admin/changeme)* | Production admin password (≥12 chars); if set, creates/updates admin on startup |
| `GUI_DEV_MODE` | `false` | Dev mode disables Secure cookie, enables vite proxy |
| `GUI_CORS_ORIGIN` | `http://localhost:5173` | Frontend origin |
| `GUI_ADDR` | `:8080` | Listen address |
| `GUI_CONFIG_DIR` | `./configs/dynamic` | File provider directory (must be under `configs/`) |
| `TRAEFIK_API_URL` | `http://localhost:8080/api` | Traefik API base (read-only) |
## 7. Frontend Structure
```
frontend/src/
├── api/client.ts # Axios + CSRF
├── components/layout/ # Layout
├── hooks/useAuth.tsx # Auth context
└── pages/ # Dashboard, Routers, Services, Middlewares, Certificates, Settings
```
*Phase 2 adds `pages/ConfigEditor.tsx` for preview/diff/apply/rollback.*
## 8. Configuration Validation Strategy
- Server-side: `gopkg.in/yaml.v3` parse + structural checks (`http`/`tcp`/`udp`/`tls` presence). Traefik’s `parser` package is referenced for future TOML parity but not vendored in MVP.
- Client-side: TypeScript preview only; authoritative validation is server-side.
## 9. Security Model
- **Authentication:** Session-based, HttpOnly, SameSite=Lax; Secure in prod
- **Authorization:** RBAC (`admin` > `operator` > `viewer`); viewers cannot write, operators cannot rollback
- **CSRF:** Double-submit header `X-CSRF-Token`, rotated on write success, required for all POST/PUT/DELETE
- **CORS:** Restricted to `GUI_CORS_ORIGIN`
- **Secrets:** Env vars only
## 10. Deployment Model
- **Dev:** `docker compose -f docker/docker-compose.yml up` or `go run ./cmd/traefik-gui --dev` + `npm run dev`
- **Prod:** Single binary with embedded `frontend/dist`, reverse proxy for TLS, SQLite on persistent volume, `configs/dynamic` mounted rw, `GUI_ADMIN_PASSWORD` set via secret manager, `GUI_DEV_MODE=false`.