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

16 KiB
Raw Permalink Blame History

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)

-- 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)

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