16 KiB
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 requireRequireAuth+RequireCSRF+ role check. POST /api/config/previewdoes not write; it validates and diffs in memory.POST /api/config/applyrequires{"filename":"app.yml","content":"...","confirm":true}and is rejected ifconfirmmissing/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-Tokenheader for POST/PUT/DELETE - Secure, HttpOnly, SameSite=Lax;
Secureflag 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 / changemeis development-only and created only ifuserstable is empty. - Production must set
GUI_ADMIN_PASSWORDenv var (min 12 chars). IfGUI_ADMIN_PASSWORDis 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)
-- 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:
filenamecontaining..or/segments escapingconfigs/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":[...]}.
- Rejects empty content, content without
-
Atomic writes: Write to
configs/dynamic/<filename>.tmp.<8-char-rand>(0600),fsync, thenos.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
backupstable +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+ advisoryflockonconfigs/dynamic/.lock.<filename>prevents concurrent writers from interleaving. Second concurrentapplygets 409 Conflict. -
File ownership: GUI process owns
configs/dynamic/; files are created with0644. Traefik watches the directory as read-only in Docker (:roon host, but GUI container has:rwonconfigs/dynamic/). No chown is performed.
6. Local Development Setup
Traefik Configuration (Development)
# 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
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.v3parse + structural checks (http/tcp/udp/tlspresence). Traefik’sparserpackage 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 uporgo run ./cmd/traefik-gui --dev+npm run dev - Prod: Single binary with embedded
frontend/dist, reverse proxy for TLS, SQLite on persistent volume,configs/dynamicmounted rw,GUI_ADMIN_PASSWORDset via secret manager,GUI_DEV_MODE=false.