# NexusGuard SD-WAN — Full System Build Plan ## Overview Build the complete NexusGuard SD-WAN system across 3 submodules (`server-core`, `dashboard-ui`, `device-agent`). The system provides Zero-Trust network isolation via nftables, stealth WireGuard tunneling via wireguard-go, hardware-bound device identity, and a Vue 3 management dashboard. ## Current State - **Main repo**: Initialized with 3 git submodules pointing to `git.datadunia.com` - **server-core**: Empty (README only) - **dashboard-ui**: Empty (README only) - **device-agent**: Empty (README only) - **Environment**: Go 1.25.1, Node 24.7.0, Vite 8.0.13, Git 2.50.1. Docker NOT installed. - **Remote CI/CD**: Gitea Actions runner already configured ## Architecture Decisions (Binding) ### Stack Per Repo | Repo | Language | Framework | DB/Cache | Key Deps | |------|----------|-----------|----------|----------| | server-core | Go 1.25 | Gin, GORM | PostgreSQL, Redis | google/nftables, crypto/aes, golang-jwt | | dashboard-ui | TypeScript | Vue 3, Vite, Pinia, Tailwind | — | axios, vue-router | | device-agent | Go 1.25 | Static binary | None (memory only) | golang.zx2c4.com/wireguard/device, crypto/aes | ### Security Decisions - **Zero-Trust**: nftables default DROP policy. Per-user sets for isolation. - **Stealth Agent**: wireguard-go via `device.IpcSet()` — no config files on disk. Ever. - **Hardware Binding**: SHA256(HWID + Salt) for AES key derivation. HWID = product_uuid | machine-id | cpuinfo. - **Config Encryption**: AES-256-GCM between Server ↔ Agent. Key never transmitted. - **Auth**: JWT for Dashboard ↔ Server. X-Token-Auth for Agent ↔ Server (one-time registration token). ### Deployment Decisions - **Docker Compose**: PostgreSQL + Redis + Server-Core (for dev/CI) - **Bare Metal**: Systemd service + Go binary (for production) - **TLS**: Deferred. Document nginx/Caddy reverse proxy config. HTTP-only during development. - **Android**: Skipped. Web-only dashboard Phase 4. ### Database Migration - **Dev/Test**: GORM AutoMigrate - **Production**: `goose` versioned migrations - **Migration files**: `migrations/` directory in server-core ### Testing Strategy - **Go unit tests**: All business logic (crypto, IPAM, models, API handlers) - **nftables**: Manual testing only via SSH to Linux server (no Linux dev env) - **Vue tests**: Vitest for stores/utils. Manual browser testing for components. - **E2E**: Post-Phase 4 manual verification ## Scope Guardrails (Must-NOT-Have) - NO STUN/P2P implementation in Phase 1–4 - NO key rotation logic in Phase 1–4 - NO peer discovery mechanism in Phase 1–4 - NO WebSocket — polling only for "real-time" status - NO PHP migration scaffolding beyond API_SPEC.md (doc only) - NO agent disk writes of any kind (not even encrypted cache) - NO WireGuard kernel module dependency — userspace wireguard-go only - NO `nft flush table` — element-level operations only - NO GORM AutoMigrate outside dev/test mode (goose for prod) --- ## Phase Completion Protocol (CRITICAL — READ BEFORE EXECUTING) ### How Phases Work Each phase is **self-contained and sequential**. Phase N+1 MUST NOT start until Phase N is fully verified and tagged. ### Git Tagging Strategy Every phase creates a git tag in its respective submodule: ``` phase-1-server-core → apps/server-core phase-2-device-agent → apps/device-agent phase-3-devops → nexus-guard-suite (main repo) phase-4-dashboard-ui → apps/dashboard-ui ``` ### Phase Exit Gates (MUST pass before next phase) | Gate | Check | Who | |------|-------|-----| | All tasks committed | `git log --oneline` shows all tasks | Implementer | | All tests pass | `go test ./...` or `npm test` returns 0 | Implementer | | Git tag created | `git tag phase-N-name` pushed | Implementer | | Phase QA verified | Per-phase exit criteria manually checked | User confirms | | **Gate passed** | ⏸ **STOP** — user approval required to proceed | User signs off | ### Shared Code Rules (NEVER Rebuild) | Code | Built In | Used By | Rule | |------|----------|---------|------| | `shared/crypto/encryptor.go` | Phase 1 Task 1.4 | Phase 2 Task 2.1 | **Copy file from server-core. Do NOT rewrite.** Identical code. | | `.env.example` patterns | Phase 3 Task 3.5 | All repos | Finalized in Phase 3. Phase 1 creates initial stub only. | | `Dockerfile` for server-core | Phase 3 Task 3.1 | Phase 3 docker-compose | References binary built in Phase 1. **Do not rebuild Go code.** | | `API_SPEC.md` | Phase 1 Task 1.11 | Phase 4 (dashboard integration) | Document only. **Do not regenerate.** | ### Dependency Graph (Which Phase Depends On What) ``` Phase 1 (Server Core) — No dependencies. ✓ Foundation. ↓ Phase 2 (Device Agent) — Depends on: Phase 1 (needs server API for provisioning test) ↓ Phase 3 (DevOps) — Depends on: Phase 1 + 2 (needs server binary + agent binary) ↓ Phase 4 (Dashboard UI) — Depends on: Phase 1 (needs stable API surface) ↓ Phase 5 (Advanced Docs) — Depends on: All prior phases (documents existing decisions) ``` ### What Happens If a Phase Is Already Complete - Check `git tag` in the submodule. If the phase tag exists, the phase is done. - **Do NOT re-run tasks.** Skip to the next phase's entry criteria. - If code needs fixing, create a NEW task in the current phase. **Never reopen completed phases.** --- ## Phase 1: Server Core (`apps/server-core`) **Goal**: Build the Go-Gin backend with all core services — database models, nftables manager, IPAM, crypto, API endpoints, and heartbeat tracking. **Phase Dependency**: None (foundation phase) **Entry Criteria**: Submodule `apps/server-core` exists with `.git` initialized. **Exit Criteria** (all must pass before Phase 2): - [x] `go build ./...` compiles without errors - [x] `go test ./... -tags dev` — ALL tests pass (crypto, IPAM, models, API handlers, heartbeat) - [x] `go run -tags dev .` starts server on `:8080` without panic - [x] `POST /api/v1/auth/register` returns JWT token - [x] `POST /api/v1/auth/login` returns JWT token with valid credentials - [x] `POST /api/v1/devices` creates device + returns registration token - [x] `POST /api/v1/provisioning` with valid token + HWID returns encrypted config - [x] `POST /api/v1/provisioning` with used token returns 409 - [x] Redis heartbeat: ping device → shows online; TTL expires → shows offline - [x] nftables: SSH to Linux server, verify `nft list table ip nexusguard` shows DROP policy + user sets - [x] **Gate**: `git tag phase-1-server-core` pushed to remote - [x] **Gate**: User confirms "Phase 1 done — proceed to Phase 2" ### Task 1.1: Go Module Init + Project Structure ✅ - **Files**: `go.mod`, `go.sum`, `main.go`, `.env.example`, `internal/config/config.go` - **Actions**: - `go mod init github.com/nexusguard/nexus-server-core` - Create directory structure: `api/`, `internal/models/`, `internal/firewall/`, `internal/ipam/`, `internal/heartbeat/`, `internal/auth/`, `shared/crypto/`, `migrations/`, `docs/` - Create `main.go` with Gin engine initialization, config loading from env - Create `internal/config/config.go` with typed config struct (DB, Redis, JWT secret, nftables table name, IPAM pool CIDR) - Create `.env.example` with all config keys documented - **QA**: `go build ./...` succeeds. Config loads from env vars with defaults. - **Test**: `TestConfigLoad` — verify env parsing with mock env ### Task 1.2: GORM Models + AutoMigrate + Goose Migration Setup ✅ - **Files**: `internal/models/models.go`, `internal/models/migrations.go`, `migrations/001_init.sql` - **Models**: - `User`: ID (uuid), Username (unique), PasswordHash, Devices (has many) - `Device`: ID (uuid), UserID (FK), Name, HWID (unique, index), InternalIP (unique), PublicKey, PrivateKey (encrypted at rest), PresharedKey, AllowInternet (default false), LastHandshake, IsActive (default true), FirewallRules (has many) - `FirewallRule`: ID (uuid), DeviceID (FK), DestIPRange, DestPortRange, Protocol (default tcp), Action (default accept) - `WgServer`: ID (uuid), Name, PublicKey, Endpoint, ListenPort - **Actions**: - Define all GORM models with proper tags, constraints, and relations - AutoMigrate in `main.go` behind `-tags dev` build flag - Initialize `goose` with `migrations/001_init.sql` (CREATE TABLE statements matching models) - Add `goose` as dev tool dependency - **QA**: `go run -tags dev .` creates all tables. Goose migration applies cleanly. - **Test**: `TestModelRelations` — create User + Device + Rule, verify FK constraints. `TestAutoMigrate` — verify tables match structs. ### Task 1.3: nftables Manager (Init + Set CRUD + Interval Ranges) ✅ ✅ - **File**: `internal/firewall/nftables.go`, `internal/firewall/nftables_test.go` - **Key Library**: `github.com/google/nftables` - **Functions**: - `NewNetManager() *NetManager` — init nftables connection - `InitNetwork() error` — create `nexusguard` table (IPv4), `forward` chain with Policy DROP - `AddUserIsolation(userID string, ipList []net.IP) error` — create Set per user, add elements - `RemoveUserIsolation(userID string) error` — delete the user's set - `AddDeviceToSet(userID string, deviceIP net.IP) error` — add single element to existing set - `RemoveDeviceFromSet(userID string, deviceIP net.IP) error` — remove element from set - `AddRangeRule(deviceName string, startIP, endIP net.IP, startPort, endPort uint16) error` — interval set with `Interval: true`, `KeyEnd` for IP ranges, `TypeInetService` for port ranges - `RemoveRangeRule(deviceName string) error` - **Critical Guardrail**: NEVER call `nft flush table`. Only add/remove individual elements. - **QA**: Mock nftables.Conn interface. Verify InitNetwork creates table + chain with DROP policy. Verify AddUserIsolation creates set with correct type. Verify AddRangeRule creates interval set with KeyEnd. - **Test**: `TestInitNetwork` (mock verifies table/chain creation), `TestAddRemoveDevice`, `TestIntervalRange` - **Note**: Integration testing is MANUAL — run on Linux server via SSH. No automated nftables tests on Windows. ### Task 1.4: AES-256-GCM Crypto Module ✅ - **File**: `shared/crypto/encryptor.go`, `shared/crypto/encryptor_test.go` - **Functions**: - `DeriveKey(hwid string, salt []byte) []byte` — SHA256(hwid + salt), returns 32-byte key - `Encrypt(plaintext []byte, key []byte) ([]byte, error)` — AES-GCM with random nonce, returns nonce\|ciphertext - `Decrypt(ciphertext []byte, key []byte) ([]byte, error)` — split nonce, GCM Open - **Security Rules**: - Nonce must be random (crypto/rand), never zero/sequential - Decrypt with wrong key must return error (authenticated encryption) - Plaintext and key must not be logged or printed - **QA**: Encrypt/Decrypt roundtrip returns original. Wrong key returns error. Different nonces produce different ciphertexts. - **Test**: `TestEncryptDecryptRoundtrip`, `TestDecryptWrongKey`, `TestKeyDerivation`, `TestNonceUniqueness` ### Task 1.5: IPAM Manager ✅ - **File**: `internal/ipam/manager.go`, `internal/ipam/manager_test.go` - **Functions**: - `NewIPAM(poolCIDR string, db *gorm.DB) *Manager` — init with CIDR (default 10.8.0.0/16) - `AllocateIP() (net.IP, error)` — find next unused /32 from pool, mark as used in DB - `ReleaseIP(ip net.IP) error` — mark IP as available - `IsAvailable(ip net.IP) bool` — check if IP is free - **Edge Cases**: - Pool exhaustion: return error with pool stats - Concurrent allocation: DB UNIQUE constraint on Device.InternalIP handles collisions; retry up to 3 times - Release non-existent IP: no-op, no error - **QA**: Allocate returns sequential /32. Exhaust pool and verify error returned. Release and re-allocate. - **Test**: `TestAllocateSequential`, `TestPoolExhaustion`, `TestReleaseAndReallocate`, `TestConcurrentAllocation` ### Task 1.6: JWT Auth Service + API Endpoints ✅ - **Files**: `internal/auth/jwt.go`, `internal/auth/jwt_test.go`, `api/auth.go` - **Functions**: - `GenerateToken(userID uuid.UUID, username string) (string, error)` — JWT with 24h expiry - `ValidateToken(tokenString string) (*Claims, error)` — parse + validate signature + expiry - `AuthMiddleware() gin.HandlerFunc` — Gin middleware that extracts user from JWT - **API Endpoints**: - `POST /api/v1/auth/login` — username + password → JWT token - `POST /api/v1/auth/register` — create new user (admin only, X-Admin-Key header) - **Password Storage**: bcrypt hash (cost 12) - **QA**: Token generation → validation roundtrip. Expired token rejected. Wrong password returns 401. Duplicate registration returns 409. - **Test**: `TestGenerateAndValidate`, `TestExpiredToken`, `TestAuthMiddleware`, `TestLoginEndpoint` ### Task 1.7: Device Management API ✅ - **File**: `api/devices.go`, `api/devices_test.go` - **Endpoints** (all require JWT auth): - `GET /api/v1/devices` — list user's devices (name, IP, status, last handshake) - `POST /api/v1/devices` — create device (name only), returns device + registration token - `GET /api/v1/devices/:id` — get device details - `PUT /api/v1/devices/:id` — update device (name, allow_internet) - `DELETE /api/v1/devices/:id` — delete device (also removes nftables rules) - `POST /api/v1/devices/:id/regenerate-token` — invalidate old token, generate new one - **Token**: Registration token is UUIDv4, stored hashed in DB, single-use (marked used on provisioning) - **QA**: CRUD operations return correct data. Token regeneration invalidates old token. Delete removes nftables rules. - **Test**: `TestCreateDevice`, `TestDeleteDeviceRemovesRules`, `TestRegenerateToken` ### Task 1.8: Provisioning API ✅ - **File**: `api/provisioning.go`, `api/provisioning_test.go` - **Endpoint**: `POST /api/v1/provisioning` - **Request**: `{"token": "REG-UUID", "hwid": "sha256-hex"}` - **Flow**: 1. Look up token in DB — 404 if not found, 409 if already used 2. Mark token as used, bind HWID to device 3. Generate WireGuard keys if not exist (server side) 4. Derive AES key: SHA256(HWID + ServerSalt) 5. Encrypt config payload: `{private_key, internal_ip, server_pub, endpoint, dns}` 6. Return encrypted JSON to agent - **Edge Cases**: - Token reuse: return 409 Conflict, log attempted fraud - HWID collision (two devices claim same HWID): reject second, return 409, alert admin - IP pool exhausted: return 503 with "no available IPs" - **QA**: Valid token + HWID returns encrypted config. Reused token returns 409. Wrong HWID format returns 400. - **Test**: `TestProvisioningSuccess`, `TestTokenReuse`, `TestHWIDCollision` ### Task 1.9: Firewall Rules API ✅ - **File**: `api/rules.go`, `api/rules_test.go` - **Endpoints** (JWT auth + device belongs-to-user check): - `GET /api/v1/devices/:id/rules` — list rules for device - `POST /api/v1/devices/:id/rules` — create rule (dest_ip_range, dest_port_range, protocol, action) - `PUT /api/v1/rules/:ruleId` — update rule - `DELETE /api/v1/rules/:ruleId` — delete rule (also removes from nftables) - **Rule Engine**: On create/update/delete, trigger nftables sync: - If no rules + AllowInternet=false → device isolated (default DROP) - If AllowInternet=true → accept to 0.0.0.0/0 - If specific rules exist → apply as nftables verdict map - **QA**: Create rule → appears in GET. Delete rule → removed from DB + nftables. Overlapping ranges handled correctly. - **Test**: `TestCreateDeleteRule`, `TestAllowInternetToggle`, `TestRuleBelongsToDevice` ### Task 1.10: Redis Heartbeat Worker ✅ - **File**: `internal/heartbeat/redis.go`, `internal/heartbeat/redis_test.go` - **Functions**: - `StartHeartbeatCollector(rdb *redis.Client, db *gorm.DB)` — goroutine: every 30s, scan Redis keys `device:{id}:ping`, update `last_handshake` and `is_active` in DB - `RecordPing(rdb *redis.Client, deviceID uuid.UUID)` — SET `device:{id}:ping` timestamp, EX 90 (TTL) - `GetOnlineDevices(rdb *redis.Client) ([]Device, error)` — check TTL for all known devices - **Agent Push**: Device sends periodic ping to `POST /api/v1/heartbeat` (JWT or X-Device-Token auth), which calls `RecordPing` - **Heartbeat Endpoint**: - `POST /api/v1/heartbeat` — accept device ID (from auth), call RecordPing - **Graceful Degradation**: If Redis is down, server logs error and uses DB-only status (stale, but functional) - **QA**: Ping → device marked online. TTL expires → device marked offline. Redis down → server still responds. - **Test**: `TestPingAndTTL` (miniredis), `TestRedisDownGraceful` ### Task 1.11: API_SPEC.md Documentation ✅ - **File**: `docs/API_SPEC.md` - **Content**: - Full OpenAPI 3.0 specification of all endpoints - Auth scheme: JWT Bearer (Dashboard), X-Token-Auth header (Agent registration), X-Device-Token (heartbeat) - Request/response examples for every endpoint - Error codes catalog (400, 401, 404, 409, 500, 503) - Deployment notes: TLS via nginx/Caddy reverse proxy (config templates included) - **QA**: Spec is internally consistent. All endpoints documented with request/response schemas. --- ## Phase 2: Device Agent (`apps/device-agent`) **Goal**: Build the stealth WireGuard agent — HWID discovery, embedded wireguard-go tunnel via IpcSet, AES-GCM decryption of server config, provisioning client, and auto-reconnect. **Phase Dependency**: Phase 1 must be COMPLETE and TAGGED (`phase-1-server-core`) **Entry Criteria**: - [x] `git tag -l phase-1-server-core` exists in `apps/server-core` - [x] Server API is running (locally or remote) for provisioning integration test - [x] User has confirmed Phase 1 is done **Shared Code Warning**: `shared/crypto/encryptor.go` — COPY from server-core (Phase 1 Task 1.4). Do NOT rewrite. Identical code. **Exit Criteria** (all must pass before Phase 3): - [x] `go build -o sys-bridge .` compiles without errors - [x] `go test ./...` — ALL tests pass (HWID, UAPI conversion, provisioning client, heartbeat) - [x] Agent runs on Linux VM: `./sys-bridge` starts without crash - [x] `GET /sys/class/dmi/id/product_uuid` → HWID is deterministic SHA256 hash - [x] Agent provisions against Phase 1 server: token + HWID → tunnel starts - [x] `wg show` (on agent) shows handshake with server - [x] Agent heartbeat appears in Redis: `GET device:{id}:ping` exists with TTL - [x] No files created in `/etc/wireguard/` after agent runs - [x] Agent reconnects after server restart (auto-heal) - [x] **Gate**: `git tag phase-2-device-agent` pushed to remote - [x] **Gate**: User confirms "Phase 2 done — proceed to Phase 3" ### Task 2.1: Go Module Init + Agent Scaffold ✅ - **Files**: `go.mod`, `main.go`, `.env.example`, `internal/tunnel/wireguard.go`, `internal/identity/hwid.go`, `internal/client/provisioning.go` - **Actions**: - `go mod init github.com/nexusguard/nexus-device-agent` - Create directory structure: `internal/tunnel/`, `internal/identity/`, `internal/client/`, `shared/crypto/` - Create `main.go` with: config load → HWID discovery → provisioning → tunnel start → heartbeat loop - Stealth binary name: build with `-o sys-bridge` (configurable) - Copy `shared/crypto/encryptor.go` from server-core (identical code) - **QA**: `go build -o sys-bridge .` succeeds. Binary runs without config file. ### Task 2.2: HWID Discovery ✅ - **File**: `internal/identity/hwid.go`, `internal/identity/hwid_test.go` - **Functions**: - `GetHWID() (string, error)` — cascading discovery: 1. Read `/sys/class/dmi/id/product_uuid` → if exists, return SHA256(trimmed) 2. Fallback: read `/etc/machine-id` → SHA256 3. Fallback: read `/proc/cpuinfo`, extract "Serial" → SHA256 4. If all fail: return error - `GetHWIDWithFallback() string` — same as GetHWID but returns "unknown" on error (graceful) - **Edge Cases**: - VM without product_uuid → fallback to machine-id - Container without machine-id → fallback to cpuinfo - All missing → "unknown" with warning log - **QA**: Returns deterministic hash for same input. Returns error only when all sources are unavailable. - **Test**: `TestHWIDFromProductUUID` (mock fs), `TestHWIDFallback`, `TestAllSourcesMissing` ### Task 2.3: Stealth WireGuard Tunnel (IpcSet) ✅ - **File**: `internal/tunnel/wireguard.go`, `internal/tunnel/wireguard_test.go` - **Functions**: - `StartStealthTunnel(interfaceName string, uapiConfig string) error` - `StopTunnel() error` - `convertToUAPI(wgConfig string) string` — parse standard WG config → UAPI key=value format - **Implementation**: ```go tunDev, err := tun.CreateTUN(interfaceName, 1420) logger := device.NewLogger(device.LogLevelError, "(nxg-wg) ") dev := device.NewDevice(tunDev, logger) err = dev.IpcSet(uapiConfig) // INJECT TO MEMORY — NO FILES dev.Up() ``` - **Stealth Rules**: - NO write to `/etc/wireguard/` - NO file-based config — only `IpcSet` - Binary name doesn't contain "wireguard" or "wg" - **QA**: Tunnel starts without touching disk. Stop cleans up TUN device. Multiple start/stop cycles work. - **Test**: `TestUAPIConversion`, `TestStartStopCycle` (mock tun), `TestNoFileWrites` ### Task 2.4: Provisioning Client ✅ - **File**: `internal/client/provisioning.go`, `internal/client/provisioning_test.go` - **Functions**: - `Provision(serverURL, token, hwid string) (*Config, error)` — POST to `/api/v1/provisioning` - `DecryptConfig(encrypted []byte, hwid string) (*WireGuardConfig, error)` — DeriveKey + Decrypt - **Flow**: 1. Construct POST request with `{"token": token, "hwid": hwid}` 2. Parse response, extract encrypted config 3. Derive AES key from HWID + hardcoded salt (same as server) 4. Decrypt config, parse into `WireGuardConfig` struct 5. Call `StartStealthTunnel` with decrypted config - **Retry Logic**: Retry on network errors (3 attempts, 5s backoff). No retry on 400/401/409. - **QA**: Provisioning with valid response starts tunnel. Network failure retries. Invalid token stops. - **Test**: `TestProvisionSuccess` (httptest server), `TestRetryOnNetworkError`, `TestInvalidToken` ### Task 2.5: Heartbeat + Auto-Reconnect ✅ - **File**: `internal/client/heartbeat.go`, `internal/client/heartbeat_test.go` - **Functions**: - `StartHeartbeat(serverURL, deviceID string, interval time.Duration)` — goroutine: every 30s, POST to `/api/v1/heartbeat` - `MonitorHandshake(wgDev *device.Device, onFailure func())` — check last handshake time, if > 120s, trigger reconnect - `Reconnect(serverURL, token, hwid string)` — re-provision and restart tunnel - **Graceful Degradation**: - If heartbeat fails (server offline): log warning, keep running, retry - If handshake fails for 120s: attempt re-provisioning - If re-provisioning fails: exponential backoff (30s, 60s, 120s, 300s max) - **QA**: Heartbeat fires at correct interval. Handshake timeout triggers reconnection. Exponential backoff caps at 300s. - **Test**: `TestHeartbeatInterval`, `TestHandshakeTimeout`, `TestReconnectBackoff` --- ## Phase 3: DevOps & Installer **Goal**: Create the infrastructure — Docker compose for local dev, bash installer for agent deployment, systemd service templates, Gitea CI/CD pipelines, and environment configuration. **Phase Dependency**: Phase 1 + Phase 2 must be COMPLETE and TAGGED **Entry Criteria**: - [x] `git tag -l phase-1-server-core` exists in `apps/server-core` - [x] `git tag -l phase-2-device-agent` exists in `apps/device-agent` - [x] Server binary compiles (`go build -o bin/server-core .` in server-core) - [x] Agent binary compiles (`go build -o sys-bridge .` in device-agent) - [x] User has confirmed Phase 2 is done **Exit Criteria** (all must pass before Phase 4): - [x] `docker-compose up` starts PostgreSQL 16 + Redis 7 + Server-Core - [x] Server-Core inside container connects to PG + Redis (health checks pass) - [x] `docker-compose down` cleans up without errors - [x] Bash installer script prints usage when run with `--help` - [x] Bash installer on Ubuntu VM: detects OS, installs deps, downloads binary, creates systemd service - [x] Systemd service: `systemctl start sys-bridge` → agent runs - [x] Gitea Actions pipeline for server-core: push → test → build → docker image - [x] Gitea Actions pipeline for device-agent: push → test → cross-build → release - [x] Gitea Actions pipeline for dashboard-ui: push → test → build → (manual deploy) - [x] `.env.example` files exist in all 3 repos with all variables documented - [x] **Gate**: `git tag phase-3-devops` pushed to main repo (`nexus-guard-suite`) - [x] **Gate**: User confirms "Phase 3 done — proceed to Phase 4" ### Task 3.1: Docker Compose (PostgreSQL + Redis + Server-Core) ✅ - **File**: `docker-compose.yml`, `docker-compose.dev.yml`, `Dockerfile` (in server-core) - **Services**: - `postgres`: PostgreSQL 16, volume for data, health check - `redis`: Redis 7, health check - `server-core`: Go binary (multi-stage build), depends on postgres+redis, env vars - **Dockerfile** (server-core): Multi-stage — `golang:1.25-alpine` build stage → `alpine:3.20` runtime - **docker-compose.dev.yml**: Hot-reload via `air` or `nodemon`, exposed ports for local dev - **QA**: `docker-compose up` starts all containers. Server connects to postgres+redis. Health checks pass. ### Task 3.2: Bash Installer Script ✅ - **File**: `scripts/install_agent.sh` (in main repo or device-agent) - **Features**: - Root check - OS detection: Debian/Ubuntu/Raspbian → apt, CentOS/RHEL/Fedora → yum/dnf - Dependency install: nftables, curl, iproute2, wireguard-tools - Architecture detection: amd64, arm64, armv7l - Binary download from Gitea releases using X-Token-Auth - Binary installation to `/usr/local/bin/` with configurable name (default `sys-bridge`) - Systemd service creation: `/etc/systemd/system/sys-bridge.service` - Service enable + start - **Flags**: `--token`, `--server-url`, `--binary-name`, `--help` - **QA**: Runs on Ubuntu → creates systemd service. Runs on CentOS → uses yum. Missing `--token` prints usage. ### Task 3.3: Systemd Service Template ✅ - **File**: `scripts/sys-bridge.service` (as template), installer generates the actual file - **Service Config**: - Type=simple - ExecStart=/usr/local/bin/sys-bridge - Restart=always, RestartSec=5 - EnvironmentFile=/etc/sys-bridge.env - StandardOutput=journal, StandardError=journal - **Environment File** (`/etc/sys-bridge.env`): - SERVER_URL, REG_TOKEN, BINARY_NAME, LOG_LEVEL - **QA**: `systemctl start sys-bridge` starts the agent. `systemctl status sys-bridge` shows running. ### Task 3.4: Gitea Actions CI/CD - **File**: `.gitea/workflows/build.yml` (in each repo) - **server-core pipeline**: - `test`: `go test ./... -tags dev -cover` - `build`: `go build -o bin/server-core .` - `docker`: Build and push Docker image to Gitea Container Registry - **device-agent pipeline**: - `test`: `go test ./... -cover` - `cross-build`: Build for linux/amd64, linux/arm64, linux/arm - `release`: Upload binaries to Gitea Releases (tag-based trigger) - **dashboard-ui pipeline**: - `test`: `npm test` - `build`: `npm run build` - `deploy`: Copy dist/ to web server (manual approval step) - **QA**: Push triggers pipeline. Tests pass. Binary releases created. ### Task 3.5: Environment Configuration - **Files**: `.env.example` in each repo, `scripts/sys-bridge.env.example` - **server-core .env.example**: ```env DB_HOST=localhost DB_PORT=5432 DB_USER=nexusguard DB_PASSWORD= DB_NAME=nexusguard REDIS_ADDR=localhost:6379 JWT_SECRET= SERVER_SALT= NFTABLES_TABLE=nexusguard IPAM_POOL=10.8.0.0/16 LOG_LEVEL=info ``` - **device-agent .env.example**: ```env SERVER_URL=https://nxg.example.com REG_TOKEN= BINARY_NAME=sys-bridge LOG_LEVEL=info ``` - **dashboard-ui .env.example**: ```env VITE_API_BASE_URL=http://localhost:8080/api/v1 ``` - **QA**: Docs explain every variable with example values and where to obtain them. --- ## Phase 4: Dashboard UI (`apps/dashboard-ui`) **Goal**: Build the Vue 3 management dashboard — JWT login, device CRUD, firewall rule editor, real-time status monitoring. Web-only (no Android). **Phase Dependency**: Phase 1 must be COMPLETE and TAGGED (dashboard consumes Phase 1 API) **Entry Criteria**: - [ ] `git tag -l phase-1-server-core` exists in `apps/server-core` - [ ] Server API is running on `http://localhost:8080/api/v1` - [ ] At least one user and device exist in database (for testing dashboard features) - [ ] User has confirmed Phase 3 is done (or Phase 1 if skipping DevOps install) **Exit Criteria** (all must pass before Phase 5): - [ ] `npm run dev` starts Vite dev server - [ ] `npm run build` produces production bundle without errors - [ ] Login page: valid credentials → redirect to dashboard. Invalid → error message. - [ ] Device list: shows devices with name, IP, online/offline badge - [ ] Create device: dialog → POST → registration token displayed with copy button - [ ] Device detail: edit name, toggle "Allow Internet", regenerate token - [ ] Delete device: confirmation dialog → device removed from list - [ ] Firewall rule editor: add rule → appears in list. Delete → removed. - [ ] Dashboard: summary cards show correct counts. Polling updates status. - [ ] Error states: loading spinner, error message with retry, empty state - [ ] Responsive layout: sidebar collapses on mobile, works on 375px viewport - [ ] API service adds JWT header to all requests automatically - [ ] 401 response → redirect to /login automatically - [ ] **Gate**: `git tag phase-4-dashboard-ui` pushed to remote - [ ] **Gate**: User confirms "Phase 4 done — proceed to Phase 5" ### Task 4.1: Vite + Vue 3 Scaffold + API Layer - **Files**: scaffold via `npm create vite@latest`, `src/services/api.ts`, `src/stores/auth.ts` - **Actions**: - Scaffold with Vue 3 + TypeScript + Vite - Add dependencies: vue-router, pinia, axios, tailwindcss, @tailwindcss/vite - Configure Tailwind CSS - Create `src/services/api.ts` — axios instance with base URL from `import.meta.env.VITE_API_BASE_URL`, interceptors for JWT header + 401 redirect - Create `src/stores/auth.ts` — Pinia store for JWT token (localStorage persistence), login/logout actions - Create router with auth guard: redirect to /login if no token - **QA**: `npm run dev` starts. API service adds JWT header. Auth guard works. ### Task 4.2: Login Page + Auth Flow - **File**: `src/views/Login.vue`, `src/api/auth.ts` - **Components**: - Login form: username + password + submit button - Error display: invalid credentials, server error, network error - Loading state during submission - **API Service**: `src/api/auth.ts` — `login(username, password)`, `register(username, password, adminKey)` - **Flow**: Submit → POST /auth/login → store token in Pinia + localStorage → redirect to /dashboard - **QA**: Login with valid credentials → redirect to dashboard. Invalid → error message. Already logged in → redirect to dashboard automatically. ### Task 4.3: Device Management Page - **File**: `src/views/Devices.vue`, `src/views/DeviceDetail.vue`, `src/api/devices.ts`, `src/stores/devices.ts` - **Components**: - Device list table: name, IP, status (online/offline badge), last handshake, actions - Create device dialog: name input → POST → show registration token (copy button) - Device detail view: edit name, toggle "Allow Internet", regenerate token - Delete device: confirmation dialog - **API Service**: `src/api/devices.ts` — CRUD calls - **Store**: `src/stores/devices.ts` — Pinia store with device list, selected device, polling interval - **QA**: Create device → appears in list with token. Delete → removed from list. Status badge reflects online/offline. ### Task 4.4: Firewall Rule Editor - **File**: `src/views/FirewallRules.vue` (or tab in DeviceDetail), `src/api/rules.ts` - **Components**: - Rules list for selected device: table of dest_ip, dest_port, protocol, action, delete button - Add rule form: dest IP (single/CIDR/range), dest port (single/range), protocol (TCP/UDP/Both), action (Accept/Drop) - "Allow Internet" toggle switch (separate from specific rules) - **Validation**: - IP format validation (single, CIDR, range: 192.168.1.10-192.168.1.20) - Port validation (single: 80, range: 8000-9000) - No duplicate rule submission - **QA**: Add rule → appears in list. Delete rule → removed. Allow Internet toggle → updates device. Invalid IP → validation error. ### Task 4.5: Dashboard + Status Monitoring - **File**: `src/views/Dashboard.vue`, `src/stores/dashboard.ts` - **Components**: - Summary cards: total devices, online count, offline count, active rules count - Device status grid: cards with name, IP, online/offline indicator, last handshake time - Auto-refresh: polling every 10s (NOT WebSocket) - Visual indicators: green dot = online (< 90s since ping), red dot = offline, gray = unknown - **Store**: `src/stores/dashboard.ts` — polling timer, device status cache - **QA**: Dashboard shows correct counts. Online/offline indicators update with polling. Summary cards reflect data. ### Task 4.6: Navigation Shell + Responsive Layout - **File**: `src/App.vue`, `src/components/Sidebar.vue`, `src/components/Navbar.vue`, `src/router/index.ts` - **Components**: - Sidebar: logo, nav links (Dashboard, Devices), user info + logout - Top navbar: breadcrumb, mobile hamburger menu - Responsive: sidebar collapses on mobile, full sidebar on desktop - **Routes**: - `/login` — Login page (public) - `/dashboard` — Dashboard (protected) - `/devices` — Device list (protected) - `/devices/:id` — Device detail + firewall rules (protected) - **QA**: All routes work. Auth guard redirects to /login. Responsive layout works on mobile viewport. ### Task 4.7: Error Handling + UX Polish - **Files**: `src/components/ErrorBoundary.vue`, `src/components/LoadingSpinner.vue`, `src/components/EmptyState.vue` - **Components**: - Error boundary for API failures: retry button, error message - Loading spinner for async operations - Empty state: "No devices yet. Create your first device." - Toast notification component for success/error feedback (create, delete, update) - Confirmation dialog for destructive actions (delete device, regenerate token) - **QA**: API failure shows error with retry. Loading spinner shows during requests. Toast appears after CRUD actions. --- ## Phase 5: Advanced Features (Deferred) **Goal**: Document the deferred features. No implementation — only architectural notes for future phases. **Phase Dependency**: All prior phases COMPLETE and TAGGED **Entry Criteria**: - [ ] `git tag -l phase-1-server-core` exists - [ ] `git tag -l phase-2-device-agent` exists - [ ] `git tag -l phase-3-devops` exists - [ ] `git tag -l phase-4-dashboard-ui` exists - [ ] User has confirmed Phase 4 is done **Exit Criteria**: - [ ] `docs/TLS_DEPLOYMENT.md` contains nginx + Caddy reverse proxy configs with Let's Encrypt - [ ] STUN signaling architectural notes documented in `docs/STUN_ARCHITECTURE.md` - [ ] Key rotation plan documented in `docs/KEY_ROTATION.md` - [ ] Peer discovery design documented in `docs/PEER_DISCOVERY.md` - [ ] **Gate**: User confirms "All phases complete. System ready." ### Task 5.1: nginx/Caddy TLS Documentation - **File**: `docs/TLS_DEPLOYMENT.md` (in server-core) - **Content**: nginx and Caddy reverse proxy config templates for TLS termination, Let's Encrypt auto-provisioning, HTTP-to-HTTPS redirect - **Note**: Documentation only. No code changes. ### Task 5.2: STUN Signaling Notes - **Document**: Architectural notes for UDP hole punching - **Server**: STUN endpoint on server-core, `/api/v1/stun` — returns public IP:port of the agent - **Agent**: On start, send STUN request to server. Server records public endpoint. Agent receives peer's public endpoint via polling/push. - **Implementation**: Deferred to future phase. ### Task 5.3: Key Rotation Notes - **Document**: 30-day key rotation plan - **Dual-key buffer**: Server generates new keypair 5 minutes before expiry. Agent fetches new key while old key is active. Graceful handover period. - **Implementation**: Deferred to future phase. ### Task 5.4: Peer Discovery Notes - **Document**: `/api/v1/peers` endpoint spec - **Design**: Server maintains device list with internal IP per user. When new device provisions, server pushes/notifies all peers in same user group. - **Implementation**: Deferred to future phase. --- ## Roll-up Verification Wave (End-to-End) **Run this ONLY after all 5 phases are complete and tagged.** This validates the integrated system works end-to-end. Each individual check here should already have passed during per-phase exit gates — this is a final integration smoke test. ### Pre-flight: Phase Tags Check - [ ] `git tag -l` in `apps/server-core` shows `phase-1-server-core` - [ ] `git tag -l` in `apps/device-agent` shows `phase-2-device-agent` - [ ] `git tag -l` in root shows `phase-3-devops` - [ ] `git tag -l` in `apps/dashboard-ui` shows `phase-4-dashboard-ui` - [ ] All docs exist from Phase 5 ### Integration Smoke Tests | # | Test | Expected | If Fails | |---|------|----------|----------| | 1 | `docker-compose up` → PostgreSQL + Redis + Server-Core start | All 3 containers healthy | Fix Phase 3 | | 2 | `POST /api/v1/auth/register` (admin) | 201 + JWT token | Fix Phase 1 | | 3 | `POST /api/v1/auth/login` | 200 + JWT token | Fix Phase 1 | | 4 | `POST /api/v1/devices` (with JWT) | 201 + device + reg token | Fix Phase 1 | | 5 | Agent binary runs: `./sys-bridge --server-url http://localhost:8080 --token REG-TOKEN` | Tunnel established, no errors | Fix Phase 2 | | 6 | `GET /api/v1/devices` → device shows "online" | Status = online | Fix Phase 1/2 | | 7 | Dashboard login → device list shows device | Device visible in UI | Fix Phase 4 | | 8 | Dashboard: create firewall rule → SSH verify nftables | Rule appears in `nft list table` | Fix Phase 1/4 | | 9 | Toggle "Allow Internet" in dashboard → verify nftables | Verdict map changes | Fix Phase 1/4 | | 10 | Delete device in dashboard → verify nftables cleanup | Set element removed | Fix Phase 1/4 | | 11 | Kill agent process → server marks offline within 90s | Status = offline | Fix Phase 1/2 | | 12 | Restart agent → reconnects automatically | Status returns to online | Fix Phase 2 | | 13 | Bash installer on fresh Ubuntu VM | Installs deps + binary + systemd | Fix Phase 3 | | 14 | Gitea Actions: push to any repo → pipeline triggers | Green build | Fix Phase 3 | ### Pass / Fail Decision - **ALL 14 pass**: System is complete and production-ready. 🟢 - **Any fail**: Fix the failing component in its original phase. Do NOT create workarounds in other phases. **Approval required from user. Run each check, report results.**