314 lines
16 KiB
Markdown
314 lines
16 KiB
Markdown
# 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`.
|