# NexusGuard Config Architecture ## TL;DR > Unified config system for non-Docker deployment: `/etc/nexusguard/nexusguard.conf` (shell-sourceable), install/uninstall scripts, systemd service, and nginx runtime config injection for dashboard. > > **Deliverables**: > - Server-core config file loader > - Dashboard runtime config via nginx > - Install/uninstall shell scripts > - Systemd service file > - Nginx config template > > **Estimated Effort**: Medium > **Parallel Execution**: YES - 3 waves > **Critical Path**: Config loader → Dashboard changes → Install scripts → Testing --- ## Context ### Original Request User wants unified config architecture for non-Docker deployment. Currently config is split: - Server-core: env vars from docker-compose `.env` - Dashboard: `VITE_API_BASE_URL` baked at build time - Device-agent: CLI args (no change needed) ### Interview Summary **Key Discussions**: - Config format: Shell-sourceable (export KEY=VALUE) - Config location: `/etc/nexusguard/nexusguard.conf` - Structure: Server-core + dashboard-ui share ONE config file - Device-agent: No config file (uses CLI args) - Dashboard: Runtime config via nginx template (window.__CONFIG__) - Docker: Keep env vars unchanged - Non-Docker: Read from nexusguard.conf - Install: Shell script (nexusguard-install.sh) - Uninstall: Shell script (nexusguard-uninstall.sh) - Systemd: Service file for server-core **Research Findings**: - Server-core config loading: `internal/config/config.go` reads env vars via `os.Getenv()` - Dashboard config: `src/services/api.ts` uses `import.meta.env.VITE_API_BASE_URL` - Existing patterns: Device-agent's `install_agent.sh` and `sys-bridge.service` - Real server: 172.20.8.191 for testing ### Metis Review **Identified Gaps** (addressed): - Config file path should be overridable via `NEXUSGUARD_CONF` env var - nginx can't source bash files → use `envsubst` with template - Secrets in conf file need `chmod 600` permissions - Config loading order: conf file → env vars → defaults - Install script must be idempotent - Dashboard `window.__CONFIG__` timing: script tag MUST appear before Vue bundle - Edge cases: empty values, spaces, special characters, BOM, Windows line endings --- ## Work Objectives ### Core Objective Implement unified config system for non-Docker NexusGuard deployment with `/etc/nexusguard/nexusguard.conf`, install/uninstall scripts, and nginx runtime config injection. ### Concrete Deliverables - `apps/server-core/internal/config/config_loader.go` - Config file loader - `apps/server-core/internal/config/config_test.go` - Unit tests - `apps/server-core/main.go` - Updated to call config loader - `apps/dashboard-ui/src/services/api.ts` - Runtime config support - `apps/dashboard-ui/nginx.conf.template` - Nginx config template - `nexusguard-install.sh` - Install script - `nexusguard-uninstall.sh` - Uninstall script - `apps/server-core/nexusguard-server.service` - Systemd service file ### Definition of Done - [ ] Server-core reads config from `/etc/nexusguard/nexusguard.conf` - [ ] Dashboard reads runtime config from nginx-injected `window.__CONFIG__` - [ ] Install script copies binaries, creates config, sets up systemd, configures nginx - [ ] Uninstall script stops service, removes files, reloads nginx - [ ] All unit tests pass - [ ] Tested on real server (172.20.8.191) ### Must Have - Config file loading with fallback to env vars - Dashboard runtime config injection via nginx - Idempotent install/uninstall scripts - Secure config file permissions (chmod 600) - Systemd service with restart on failure ### Must NOT Have (Guardrails) - **NEVER** modify Docker behavior (docker-compose.yml, Dockerfiles stay unchanged) - **NEVER** touch device-agent (uses CLI args, not config file) - **NEVER** add PostgreSQL/Redis installation to install script - **NEVER** add TLS/HTTPS setup to install script - **NEVER** add auto-reload on config changes - **NEVER** add config file validation (schema) - **NEVER** add log rotation or monitoring - **NEVER** add multi-server deployment support - **NEVER** add `nexusguard-ctl` management CLI --- ## Verification Strategy > **ZERO HUMAN INTERVENTION** - ALL verification is agent-executed. No exceptions. ### Test Decision - **Infrastructure exists**: YES (Go tests for server-core, npm for dashboard) - **Automated tests**: YES (Tests-after) - **Framework**: Go testing (server-core), no framework for dashboard (manual verification) ### QA Policy Every task MUST include agent-executed QA scenarios. Evidence saved to `.sisyphus/evidence/task-{N}-{scenario-slug}.{ext}`. - **Go code**: Use `go test` - Run tests, assert pass - **Shell scripts**: Use `bash` - Run script, verify exit code and file creation - **Config loading**: Use Go test with temp files - **Nginx config**: Use `nginx -t` to validate syntax - **Real server**: SSH to 172.20.8.191 and test --- ## Execution Strategy ### Parallel Execution Waves ``` Wave 1 (Start Immediately - foundation): ├── Task 1: Config file loader for server-core [deep] ├── Task 2: Dashboard runtime config support [quick] └── Task 3: Nginx config template [quick] Wave 2 (After Wave 1 - scripts): ├── Task 4: Install script (depends: 1, 2, 3) [deep] ├── Task 5: Uninstall script (depends: 4) [quick] └── Task 6: Systemd service file (depends: 1) [quick] Wave FINAL (After ALL tasks): ├── Task F1: Plan compliance audit (oracle) ├── Task F2: Code quality review (unspecified-high) ├── Task F3: Real manual QA on server 172.20.8.191 (unspecified-high) └── Task F4: Scope fidelity check (deep) ``` ### Dependency Matrix | Task | Depends On | Blocks | |------|-----------|--------| | 1 | None | 4, 6 | | 2 | None | 4 | | 3 | None | 4 | | 4 | 1, 2, 3 | F1-F4 | | 5 | 4 | F1-F4 | | 6 | 1 | F1-F4 | ### Agent Dispatch Summary - **Wave 1**: 3 tasks - T1 → `deep`, T2 → `quick`, T3 → `quick` - **Wave 2**: 3 tasks - T4 → `deep`, T5 → `quick`, T6 → `quick` - **FINAL**: 4 tasks - F1 → `oracle`, F2 → `unspecified-high`, F3 → `unspecified-high`, F4 → `deep` --- ## TODOs - [x] 1. Config file loader for server-core **What to do**: - Create `apps/server-core/internal/config/config_loader.go` - Implement `LoadConfFile(path string)` function - Parse shell-sourceable format (export KEY=VALUE) - Handle edge cases: comments (#), empty lines, spaces, Windows line endings (\r\n), BOM - Skip malformed lines (no = sign) - Trim whitespace around keys and values - Call `os.Setenv()` for each valid key - Return error if file doesn't exist (but don't fatal) - Support path override via `NEXUSGUARD_CONF` env var - Default path: `/etc/nexusguard/nexusguard.conf` **Must NOT do**: - Don't modify existing `config.Load()` function - Don't add external dependencies (use stdlib only) - Don't make config loading fatal (log warning if file missing) **Recommended Agent Profile**: - **Category**: `deep` - **Skills**: [] - **Reason**: Go code, requires understanding of existing config pattern **Parallelization**: - **Can Run In Parallel**: YES - **Parallel Group**: Wave 1 (with Tasks 2, 3) - **Blocks**: Tasks 4, 6 - **Blocked By**: None **References**: - `apps/server-core/internal/config/config.go:25-55` - Existing config loading pattern - `apps/server-core/main.go:176` - Where config.Load() is called - `apps/device-agent/scripts/install_agent.sh` - Shell script pattern to follow **Acceptance Criteria**: - [ ] File created: `apps/server-core/internal/config/config_loader.go` - [ ] Function `LoadConfFile(path string) error` exists - [ ] Parses `export KEY=VALUE` format - [ ] Skips comments (#) and empty lines - [ ] Handles Windows line endings (\r\n) - [ ] Trims whitespace around keys and values - [ ] Calls `os.Setenv()` for each valid key - [ ] Returns error for missing file (non-fatal) - [ ] Supports `NEXUSGUARD_CONF` env var override **QA Scenarios**: ``` Scenario: Parse valid config file Tool: Bash (go test) Preconditions: None Steps: 1. Create temp file with: export JWT_SECRET=test123\nexport SERVER_SALT=salt456\nexport DB_HOST=customhost 2. Call LoadConfFile(tempPath) 3. Assert os.Getenv("JWT_SECRET") == "test123" 4. Assert os.Getenv("DB_HOST") == "customhost" Expected Result: All values set correctly Evidence: .sisyphus/evidence/task-1-parse-valid.txt Scenario: Handle missing config file Tool: Bash (go test) Preconditions: None Steps: 1. Call LoadConfFile("/nonexistent/path") 2. Assert error is returned 3. Assert no panic or fatal Expected Result: Error returned, process continues Evidence: .sisyphus/evidence/task-1-missing-file.txt Scenario: Skip malformed lines Tool: Bash (go test) Preconditions: None Steps: 1. Create temp file with: export VALID=yes\nINVALID_LINE\nexport ALSO_VALID=ok 2. Call LoadConfFile(tempPath) 3. Assert os.Getenv("VALID") == "yes" 4. Assert os.Getenv("ALSO_VALID") == "ok" Expected Result: Malformed line skipped Evidence: .sisyphus/evidence/task-1-malformed-lines.txt Scenario: Handle Windows line endings Tool: Bash (go test) Preconditions: None Steps: 1. Create temp file with: export KEY1=val1\r\nexport KEY2=val2\r\n 2. Call LoadConfFile(tempPath) 3. Assert os.Getenv("KEY1") == "val1" 4. Assert os.Getenv("KEY2") == "val2" Expected Result: \r\n handled correctly Evidence: .sisyphus/evidence/task-1-windows-endings.txt ``` **Commit**: YES - Message: `feat(server-core): add config file loader for /etc/nexusguard/nexusguard.conf` - Files: `apps/server-core/internal/config/config_loader.go` - Pre-commit: `go test ./internal/config/... -v` --- - [x] 2. Dashboard runtime config support **What to do**: - Modify `apps/dashboard-ui/src/services/api.ts` - Add TypeScript type declaration for `window.__CONFIG__` - Read `window.__CONFIG__?.apiBaseUrl` with fallback to `import.meta.env.VITE_API_BASE_URL` - Keep Docker compatibility (VITE_API_BASE_URL still works) - Add comment explaining runtime config injection **Must NOT do**: - Don't remove VITE_API_BASE_URL support (Docker compatibility) - Don't change build process - Don't add external dependencies **Recommended Agent Profile**: - **Category**: `quick` - **Skills**: [] - **Reason**: Simple TypeScript change, single file **Parallelization**: - **Can Run In Parallel**: YES - **Parallel Group**: Wave 1 (with Tasks 1, 3) - **Blocks**: Task 4 - **Blocked By**: None **References**: - `apps/dashboard-ui/src/services/api.ts:1-10` - Current API client setup - `apps/dashboard-ui/nginx.conf` - Current nginx config **Acceptance Criteria**: - [ ] File modified: `apps/dashboard-ui/src/services/api.ts` - [ ] `window.__CONFIG__` type declared - [ ] Runtime config read with fallback to VITE_API_BASE_URL - [ ] TypeScript compiles without errors **QA Scenarios**: ``` Scenario: Runtime config override Tool: Bash (manual verification) Preconditions: None Steps: 1. Read api.ts file 2. Verify window.__CONFIG__?.apiBaseUrl is checked first 3. Verify fallback to import.meta.env.VITE_API_BASE_URL Expected Result: Runtime config takes precedence Evidence: .sisyphus/evidence/task-2-runtime-config.txt Scenario: Docker compatibility Tool: Bash (manual verification) Preconditions: None Steps: 1. Read api.ts file 2. Verify VITE_API_BASE_URL fallback exists Expected Result: Docker builds still work Evidence: .sisyphus/evidence/task-2-docker-compat.txt ``` **Commit**: YES (groups with Task 1) - Message: `feat(dashboard): add runtime config support via window.__CONFIG__` - Files: `apps/dashboard-ui/src/services/api.ts` --- - [x] 3. Nginx config template **What to do**: - Create `apps/dashboard-ui/nginx.conf.template` - Use `envsubst` placeholders for runtime config injection - Proxy `/api/` to `http://127.0.0.1:${API_PORT}` - Serve static SPA from `/usr/share/nexusguard/dashboard` - Inject `window.__CONFIG__` script tag before Vue bundle - Handle SPA fallback (try_files $uri $uri/ /index.html) - Listen on port 80 (or configurable) **Must NOT do**: - Don't add TLS/HTTPS configuration - Don't hardcode API_PORT (use envsubst) - Don't modify existing Docker nginx.conf **Recommended Agent Profile**: - **Category**: `quick` - **Skills**: [] - **Reason**: Simple nginx config template **Parallelization**: - **Can Run In Parallel**: YES - **Parallel Group**: Wave 1 (with Tasks 1, 2) - **Blocks**: Task 4 - **Blocked By**: None **References**: - `apps/dashboard-ui/nginx.conf` - Current Docker nginx config - `apps/server-core/main.go:356` - API port default (8080) **Acceptance Criteria**: - [ ] File created: `apps/dashboard-ui/nginx.conf.template` - [ ] `envsubst` placeholders for `${API_PORT}`, `${API_BASE_URL}` - [ ] `window.__CONFIG__` injection via `sub_filter` or template - [ ] SPA fallback configured - [ ] `nginx -t` validates syntax **QA Scenarios**: ``` Scenario: Nginx config syntax Tool: Bash Preconditions: nginx installed Steps: 1. Run: nginx -t -c /path/to/nginx.conf.template 2. Assert exit code 0 Expected Result: Config syntax valid Evidence: .sisyphus/evidence/task-3-nginx-syntax.txt Scenario: Runtime config injection Tool: Bash (manual verification) Preconditions: None Steps: 1. Read nginx.conf.template 2. Verify window.__CONFIG__ injection mechanism exists 3. Verify it appears before Vue bundle script Expected Result: Config injected correctly Evidence: .sisyphus/evidence/task-3-config-injection.txt ``` **Commit**: YES (groups with Tasks 1, 2) - Message: `feat(dashboard): add nginx config template for runtime config injection` - Files: `apps/dashboard-ui/nginx.conf.template` --- - [x] 4. Install script **What to do**: - Create `nexusguard-install.sh` at project root - Parse arguments (--help, --server-port, --web-port) - Check dependencies (nginx, systemctl) - Copy server-core binary to `/usr/local/bin/` - Copy dashboard dist to `/usr/share/nexusguard/dashboard/` - Create `/etc/nexusguard/nexusguard.conf` with template values - Set permissions: `chmod 600 /etc/nexusguard/nexusguard.conf` - Create systemd service file at `/etc/systemd/system/nexusguard-server.service` - Create nginx config at `/etc/nginx/conf.d/nexusguard.conf` - Enable and start service - Print access URL - Handle idempotency (check existing service, skip or overwrite gracefully) **Must NOT do**: - Don't install PostgreSQL or Redis - Don't install nginx (assume pre-installed) - Don't configure TLS/HTTPS - Don't build from source (expect pre-built binaries) - Don't modify Docker behavior **Recommended Agent Profile**: - **Category**: `deep` - **Skills**: [] - **Reason**: Complex shell script with multiple system interactions **Parallelization**: - **Can Run In Parallel**: NO - **Parallel Group**: Wave 2 (sequential after Wave 1) - **Blocks**: Tasks 5, F1-F4 - **Blocked By**: Tasks 1, 2, 3 **References**: - `apps/device-agent/scripts/install_agent.sh` - Pattern to follow - `apps/server-core/nexusguard-server.service` - Systemd template - `apps/dashboard-ui/nginx.conf.template` - Nginx config to copy **Acceptance Criteria**: - [ ] File created: `nexusguard-install.sh` - [ ] `--help` flag shows usage - [ ] Creates `/etc/nexusguard/nexusguard.conf` with chmod 600 - [ ] Creates systemd service file - [ ] Creates nginx config - [ ] Enables and starts service - [ ] Idempotent (safe to run twice) **QA Scenarios**: ``` Scenario: Install --help Tool: Bash Preconditions: None Steps: 1. Run: bash nexusguard-install.sh --help 2. Assert exit code 0 3. Assert usage text displayed Expected Result: Help text shown Evidence: .sisyphus/evidence/task-4-install-help.txt Scenario: Install creates config file Tool: Bash Preconditions: None Steps: 1. Run: bash nexusguard-install.sh 2. Assert /etc/nexusguard/nexusguard.conf exists 3. Assert file permissions are 600 4. Assert file contains export statements Expected Result: Config file created securely Evidence: .sisyphus/evidence/task-4-config-created.txt Scenario: Install creates systemd service Tool: Bash Preconditions: None Steps: 1. Run: bash nexusguard-install.sh 2. Assert /etc/systemd/system/nexusguard-server.service exists 3. Assert service is enabled Expected Result: Systemd service configured Evidence: .sisyphus/evidence/task-4-systemd-created.txt Scenario: Install idempotency Tool: Bash Preconditions: None Steps: 1. Run: bash nexusguard-install.sh 2. Run: bash nexusguard-install.sh (again) 3. Assert no errors 4. Assert service still running Expected Result: Safe to run multiple times Evidence: .sisyphus/evidence/task-4-idempotency.txt ``` **Commit**: YES - Message: `feat: add nexusguard-install.sh for non-Docker deployment` - Files: `nexusguard-install.sh` - Pre-commit: `bash nexusguard-install.sh --help` --- - [x] 5. Uninstall script **What to do**: - Create `nexusguard-uninstall.sh` at project root - Stop and disable nexusguard-server service - Remove `/usr/local/bin/nexusguard-server-core` - Remove `/usr/share/nexusguard/dashboard/` - Remove `/etc/nexusguard/nexusguard.conf` - Remove `/etc/systemd/system/nexusguard-server.service` - Remove `/etc/nginx/conf.d/nexusguard.conf` - Reload nginx - Reload systemd daemon - Print confirmation message **Must NOT do**: - Don't remove PostgreSQL or Redis - Don't remove database data - Don't remove WireGuard state - Don't remove device-agent (separate concern) **Recommended Agent Profile**: - **Category**: `quick` - **Skills**: [] - **Reason**: Simple cleanup script **Parallelization**: - **Can Run In Parallel**: NO - **Parallel Group**: Wave 2 (after Task 4) - **Blocks**: F1-F4 - **Blocked By**: Task 4 **References**: - `nexusguard-install.sh` - Install script to reverse **Acceptance Criteria**: - [ ] File created: `nexusguard-uninstall.sh` - [ ] Stops and disables service - [ ] Removes all installed files - [ ] Reloads nginx and systemd - [ ] Preserves database and WireGuard state **QA Scenarios**: ``` Scenario: Uninstall removes files Tool: Bash Preconditions: Install script run first Steps: 1. Run: bash nexusguard-uninstall.sh 2. Assert /etc/nexusguard/nexusguard.conf does not exist 3. Assert /etc/systemd/system/nexusguard-server.service does not exist 4. Assert service is stopped Expected Result: All files removed Evidence: .sisyphus/evidence/task-5-uninstall-removes.txt Scenario: Uninstall preserves data Tool: Bash Preconditions: Install script run first Steps: 1. Run: bash nexusguard-uninstall.sh 2. Assert PostgreSQL data still exists 3. Assert Redis data still exists Expected Result: User data preserved Evidence: .sisyphus/evidence/task-5-uninstall-preserves.txt ``` **Commit**: YES (groups with Task 4) - Message: `feat: add nexusguard-uninstall.sh` - Files: `nexusguard-uninstall.sh` --- - [x] 6. Systemd service file **What to do**: - Create `apps/server-core/nexusguard-server.service` - Use `EnvironmentFile=/etc/nexusguard/nexusguard.conf` - Set `Restart=always` and `RestartSec=5` - Run as root (needed for nftables/WireGuard) - Set working directory to `/usr/local/bin` - Add proper logging (journal) - Add `After=network.target postgresql.service redis.service` **Must NOT do**: - Don't hardcode paths (use EnvironmentFile) - Don't add Docker-specific settings - Don't add resource limits (cgroup) **Recommended Agent Profile**: - **Category**: `quick` - **Skills**: [] - **Reason**: Simple systemd unit file **Parallelization**: - **Can Run In Parallel**: YES - **Parallel Group**: Wave 2 (with Tasks 4, 5) - **Blocks**: F1-F4 - **Blocked By**: Task 1 **References**: - `apps/device-agent/scripts/sys-bridge.service` - Systemd template **Acceptance Criteria**: - [ ] File created: `apps/server-core/nexusguard-server.service` - [ ] `EnvironmentFile=/etc/nexusguard/nexusguard.conf` - [ ] `Restart=always` and `RestartSec=5` - [ ] Runs as root - [ ] `After=network.target postgresql.service redis.service` **QA Scenarios**: ``` Scenario: Systemd service syntax Tool: Bash Preconditions: systemd installed Steps: 1. Run: systemd-analyze verify nexusguard-server.service 2. Assert exit code 0 Expected Result: Service file valid Evidence: .sisyphus/evidence/task-6-systemd-syntax.txt Scenario: Environment file configured Tool: Bash Preconditions: None Steps: 1. Read nexusguard-server.service 2. Assert EnvironmentFile=/etc/nexusguard/nexusguard.conf exists Expected Result: Config file path correct Evidence: .sisyphus/evidence/task-6-env-file.txt ``` **Commit**: YES (groups with Tasks 4, 5) - Message: `feat: add systemd service file for non-Docker deployment` - Files: `apps/server-core/nexusguard-server.service` --- ## Final Verification Wave - [x] F1. **Plan Compliance Audit** — `oracle` Read the plan end-to-end. For each "Must Have": verify implementation exists. For each "Must NOT Have": search codebase for forbidden patterns. Check evidence files exist. Output: `Must Have [N/N] | Must NOT Have [N/N] | Tasks [N/N] | VERDICT: APPROVE/REJECT` - [x] F2. **Code Quality Review** — `unspecified-high` Run `go vet`, `go test`, `npm run build`. Review all changed files for: empty catches, console.log in prod, commented-out code, unused imports. Check AI slop: excessive comments, over-abstraction. Output: `Build [PASS/FAIL] | Tests [N pass/N fail] | Files [N clean/N issues] | VERDICT` - [x] F3. **Real Manual QA on 172.20.8.191** — `unspecified-high` SSH to server. Run install script. Verify config file created. Verify systemd service running. Verify nginx serving dashboard. Verify runtime config injection. Test uninstall. Output: `Install [PASS/FAIL] | Service [RUNNING/STOPPED] | Dashboard [ACCESSIBLE/INACCESSIBLE] | VERDICT` - [x] F4. **Scope Fidelity Check** — `deep` For each task: read "What to do", read actual diff. Verify 1:1 — everything in spec was built, nothing beyond spec was built. Check "Must NOT do" compliance. Detect cross-task contamination. Output: `Tasks [N/N compliant] | Contamination [CLEAN/N issues] | Unaccounted [CLEAN/N files] | VERDICT` --- ## Commit Strategy - **Task 1**: `feat(server-core): add config file loader for /etc/nexusguard/nexusguard.conf` - **Task 2-3**: `feat(dashboard): add runtime config support and nginx template` - **Task 4-6**: `feat: add install/uninstall scripts and systemd service` --- ## Success Criteria ### Verification Commands ```bash # Server-core config loading cd apps/server-core && go test ./internal/config/... -v # Expected: PASS # Dashboard build cd apps/dashboard-ui && npm run build # Expected: succeeds # Install script bash nexusguard-install.sh --help # Expected: shows usage # Uninstall script bash nexusguard-uninstall.sh # Expected: removes files, stops service # Real server test (172.20.8.191) ssh root@172.20.8.191 "bash nexusguard-install.sh" # Expected: success ssh root@172.20.8.191 "systemctl status nexusguard-server" # Expected: active (running) ssh root@172.20.8.191 "curl -s http://localhost/" # Expected: HTML with window.__CONFIG__ ``` ### Final Checklist - [ ] All "Must Have" present - [ ] All "Must NOT Have" absent - [ ] All tests pass - [ ] Tested on real server (172.20.8.191)