Traefik_Control/ARCHITECTURE.md
2026-09-02 11:20:31 -05:00

314 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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`.