19 KiB
Plan: Firewall INPUT Fix + Device Status via WireGuard Handshake
TL;DR
Fix two critical production issues: (1) firewall INPUT chain doesn't isolate WireGuard peers, (2) device status shows offline because it depends on Redis heartbeats instead of WireGuard handshake data.
Deliverables:
- WireGuard per-peer handshake status (replace Redis heartbeat dependency)
- Firewall INPUT chain rules in code (persist across restarts)
- ICMP + established/related accept rules for WG traffic
Estimated Effort: Medium Parallel Execution: YES - 2 waves Critical Path: T1 (wgctrl) → T2 (SyncToDB) → T3 (INPUT rules) → F1-F4
Context
Original Request
- Device
gogo2connected via WireGuard (peer shows handshake inwg show) but dashboard shows offline (IsActive=false) - Firewall INPUT chain rules don't work — ping from WG peer still works even with drop rules
- User wants a proper plan, not ad-hoc fixes
Interview Summary
Key Discussions:
- Device was created manually via API, not via device-agent → no heartbeats sent
- Heartbeat system: device-agent → POST /api/v1/heartbeat → Redis TTL 90s → SyncToDB sets
is_active - Without device-agent, no heartbeats →
is_activestays false - Solution: use WireGuard handshake time from wgctrl instead of Redis heartbeats
- Firewall:
table ip nexusguardINPUT chain has rules but ping still works - Root cause: rules not in code (manually added, lost on restart), ICMP not explicitly allowed, possible
table inetvstable ippriority conflict
Research Findings:
wgmanager_linux.go:50iteratesdev.Peersand readspeer.LastHandshakeTime— but only stores MAX across all peers, losing per-peer granularityWgStatusstruct has no per-peer handshake mapSyncToDB()inheartbeat/redis.gochecks Redis key existence → setsis_active- Dashboard reads
device.IsActivefrom DB — no frontend changes needed if SyncToDB is fixed InitNetwork()creates INPUT chain with zero rules — all rules were manualAddInputRule()only addsudp dportrules — no ICMP, no established/related
Metis Review
Identified Gaps (addressed):
- INPUT chain rules must be added to
InitNetwork()code for persistence - ICMP accept rule missing — only TCP/UDP explicitly allowed
- Per-peer handshake data needed (current
GetStatus()loses per-peer granularity) - Remote node devices won't have local WG data — scoped to local-only
- Docker's
inet filterchains must not be modified
Work Objectives
Core Objective
Device status reflects actual WireGuard connectivity (not Redis heartbeats), and firewall INPUT chain properly isolates WG peers.
Concrete Deliverables
wgmanagerinterface: newGetPeerHandshakes()methodheartbeat/redis.go:SyncToDB()uses WG handshake datanftables_linux.go: INPUT chain rules inInitNetwork()- Production deployment with verification
Definition of Done
- Device with active WG peer shows
is_active=truein API - Device without WG handshake shows
is_active=false ping 10.172.21.1from WG peer → RTO (dropped)- TCP 8080 from WG peer → works (API access)
- UDP 51820 from WG peer → works (WG tunnel)
- Non-WG traffic → unaffected (Docker, SSH, etc.)
- Rules persist across server restart
Must Have
GetPeerHandshakes() map[string]time.Timein wgmanagerSyncToDB()matches device public keys to WG peer public keys- INPUT chain rules in
InitNetwork()code (not manual) - ICMP accept rule for WG traffic
- established/related accept rule for WG traffic
Must NOT Have (Guardrails)
- NEVER
nft flush table— project anti-pattern - NEVER change
policy accepton any chain - NEVER modify Docker's
inet filterchains - NEVER add IPv6 rules
- NEVER remove heartbeat API endpoint (keep as fallback)
- NEVER add new API endpoints — modify existing only
- NEVER touch
shared/crypto/encryptor.go - NEVER add per-device firewall INPUT rules (INPUT is server-level, not peer isolation)
Verification Strategy
ZERO HUMAN INTERVENTION — ALL verification is agent-executed.
Test Decision
- Infrastructure exists: YES (Go build, nft CLI, curl)
- Automated tests: Tests-after (verify existing tests still pass)
- Framework:
go build -tags dev ./...,nft list chain,curlAPI
QA Policy
Every task includes agent-executed QA scenarios.
Evidence saved to .sisyphus/evidence/task-{N}-{scenario-slug}.{ext}.
- Backend: Use Bash (curl) — API requests, assert status + response fields
- Firewall: Use Bash (ssh + nft) — rule verification, counter checks
- WireGuard: Use Bash (ssh + wg) — handshake verification
Execution Strategy
Parallel Execution Waves
Wave 1 (Start Immediately - backend core):
├── Task 1: wgmanager GetPeerHandshakes() [quick]
├── Task 2: SyncToDB uses WG handshake [quick]
└── Task 3: InitNetwork INPUT chain rules [quick]
Wave 2 (After Wave 1 - deployment + verification):
├── Task 4: Build + deploy to production [quick]
└── Task 5: Verify all acceptance criteria [quick]
Wave FINAL (After ALL tasks — 4 parallel reviews, then user okay):
├── Task F1: Plan compliance audit (oracle)
├── Task F2: Code quality review (unspecified-high)
├── Task F3: Real manual QA (unspecified-high)
└── Task F4: Scope fidelity check (deep)
-> Present results -> Get explicit user okay
Dependency Matrix
| Task | Depends On | Blocks |
|---|---|---|
| T1 | None | T2 |
| T2 | T1 | T4 |
| T3 | None | T4 |
| T4 | T2, T3 | T5, F1-F4 |
| T5 | T4 | F1-F4 |
Agent Dispatch Summary
- Wave 1: 3 tasks — T1
quick, T2quick, T3quick - Wave 2: 2 tasks — T4
quick, T5quick - FINAL: 4 tasks — F1
oracle, F2unspecified-high, F3unspecified-high, F4deep
TODOs
-
1. wgmanager: add GetPeerHandshakes() method
What to do:
- Add
GetPeerHandshakes() map[string]time.TimetoWgManagerinterface ininternal/wgmanager/manager.go - Implement in
internal/wgmanager/wgmanager_linux.go: iteratedev.Peers, return map ofpeer.PublicKey.String() → peer.LastHandshakeTime - Implement stub in
internal/wgmanager/wgmanager_stub.go(returns empty map) - Add test in
internal/wgmanager/if test file exists
Must NOT do:
- Don't modify existing
GetStatus()method - Don't change
WgStatusstruct
Recommended Agent Profile:
- Category:
quick - Skills: []
Parallelization:
- Can Run In Parallel: YES (with T3)
- Parallel Group: Wave 1 (with T3)
- Blocks: T2
- Blocked By: None
References:
apps/server-core/internal/wgmanager/manager.go:27-39— WgManager interface + UpConfig structapps/server-core/internal/wgmanager/wgmanager_linux.go:27-58— GetStatus() implementation, iterates dev.Peers at line 50apps/server-core/internal/wgmanager/wgmanager_stub.go— Stub implementation
Acceptance Criteria:
go build -tags dev ./...passesGetPeerHandshakes()returns non-empty map when peers are connected- Method is on both interface and implementations
QA Scenarios:
Scenario: GetPeerHandshakes returns peer handshake data Tool: Bash (go test or manual verification) Preconditions: Server running with connected WG peer Steps: 1. SSH to server 2. Run: docker exec nexus-guard-suite-server-core-1 ./server-core -tags dev -test.run TestGetPeerHandshakes 2>&1 || echo "verify via API" 3. Check wg show wg0 latest-handshakes for reference Expected Result: Map contains public key → timestamp for connected peer Evidence: .sisyphus/evidence/task1-peer-handshakes.jsonCommit: YES
- Message:
feat(wgmanager): add GetPeerHandshakes() for per-peer handshake data - Files:
internal/wgmanager/manager.go,internal/wgmanager/wgmanager_linux.go,internal/wgmanager/wgmanager_stub.go
- Add
-
2. heartbeat: SyncToDB uses WireGuard handshake instead of Redis
What to do:
- Modify
SyncToDB()ininternal/heartbeat/redis.goto:- Query
wgMgr.GetPeerHandshakes()(inject wgMgr into Manager or pass as parameter) - For each device, match
device.PublicKeyagainst WG peer public keys - If match found AND handshake within 120s → set
is_active=true,last_handshake=handshake_time - If no match OR handshake > 120s → set
is_active=false
- Query
- Keep Redis heartbeat recording active (don't break existing device-agents)
- Update
NewHeartbeatManagerto acceptwgmanager.WgManagerparameter - Update
main.goto passwgMgrto heartbeat manager
Must NOT do:
- Don't remove Redis heartbeat recording (keep as fallback)
- Don't change the heartbeat API endpoint
- Don't change
RecordPing()method
Recommended Agent Profile:
- Category:
quick - Skills: []
Parallelization:
- Can Run In Parallel: NO
- Parallel Group: Wave 1 (after T1)
- Blocks: T4
- Blocked By: T1
References:
apps/server-core/internal/heartbeat/redis.go:57-84— Current SyncToDB implementationapps/server-core/internal/heartbeat/redis.go:14-17— Manager struct (needs wgMgr field)apps/server-core/main.go:216-219— HeartbeatManager creation (needs wgMgr param)apps/server-core/api/devices.go:35-60— Device List handler (reads is_active from DB)apps/dashboard-ui/src/views/Devices.vue— Dashboard reads IsActive from API response
Acceptance Criteria:
go build -tags dev ./...passes- Device with active WG peer →
is_active=truein DB after SyncToDB cycle - Device without WG handshake →
is_active=false - Redis heartbeat recording still works (existing device-agents unaffected)
QA Scenarios:
Scenario: Device with active WG peer shows online Tool: Bash (curl + ssh) Preconditions: Server running, device gogo2 connected via WG Steps: 1. SSH to server, get token: TOKEN=$(curl -s http://localhost:8080/api/v1/auth/login -X POST -H "Content-Type: application/json" -d '{"username":"admin","password":"admin123"}' | python3 -c "import sys,json; print(json.load(sys.stdin)['token'])") 2. Wait 35s for SyncToDB cycle 3. curl -s http://localhost:8080/api/v1/devices -H "Authorization: Bearer $TOKEN" | python3 -c "import sys,json; devices=json.load(sys.stdin); [print(d['Name'], d.get('is_active')) for d in devices]" Expected Result: gogo2 shows True (is_active=true) Evidence: .sisyphus/evidence/task2-device-online.json Scenario: WG peer handshake data present Tool: Bash (ssh) Steps: 1. SSH to server 2. wg show wg0 latest-handshakes 3. Compare timestamp with device LastHandshake in DB Expected Result: Timestamps match (within 30s) Evidence: .sisyphus/evidence/task2-handshake-match.jsonCommit: YES (groups with T1)
- Message:
feat(heartbeat): use WireGuard handshake for device status instead of Redis - Files:
internal/heartbeat/redis.go,main.go
- Modify
-
3. nftables: add INPUT chain rules to InitNetwork()
What to do:
- In
InitNetwork()(after forward chain rules), add INPUT chain rules via nft CLI:nft add rule ip nexusguard input ct state established,related accept comment "estab" nft add rule ip nexusguard input icmp type echo-request accept comment "icmp_ping" nft add rule ip nexusguard input ip saddr 10.172.21.0/24 tcp dport 8080 accept comment "wg_api" nft add rule ip nexusguard input ip saddr 10.172.21.0/24 udp dport 51820 accept comment "wg_tunnel" nft add rule ip nexusguard input ip saddr 10.172.21.0/24 drop comment "wg_isolation" - Each rule must be idempotent (check comment before adding)
- Use
detectWGSubnet()(already exists) for WG subnet detection - Remove old manual rules on production server during deployment
Must NOT do:
- Don't change
policy accepton INPUT chain - Don't add rules to Docker's
inet filterchains - Don't add IPv6 rules
Recommended Agent Profile:
- Category:
quick - Skills: []
Parallelization:
- Can Run In Parallel: YES (with T1)
- Parallel Group: Wave 1 (with T1)
- Blocks: T4
- Blocked By: None
References:
apps/server-core/internal/firewall/nftables_linux.go:25-43— Current InitNetwork (INPUT chain created but empty)apps/server-core/internal/firewall/nftables_linux.go:46-62— detectWGSubnet() functionapps/server-core/internal/firewall/nftables_linux.go:156-174— AddInputRule/RemoveInputRule (existing pattern)
Acceptance Criteria:
go build -tags dev ./...passesnft list chain ip nexusguard inputshows all 5 rules after server start- Rules are idempotent (restart doesn't duplicate rules)
QA Scenarios:
Scenario: INPUT chain rules exist after restart Tool: Bash (ssh + nft) Preconditions: Server rebuilt with new code Steps: 1. SSH to server 2. docker compose down && docker compose up -d --build 3. sleep 5 4. nft list chain ip nexusguard input Expected Result: 5 rules visible (estab, icmp, wg_api, wg_tunnel, wg_isolation) Evidence: .sisyphus/evidence/task3-input-rules.txt Scenario: Ping from WG peer is dropped Tool: Bash (ssh) Steps: 1. SSH to server 2. nft list chain ip nexusguard input | grep wg_isolation 3. Count packets: nft -a list chain ip nexusguard input | grep wg_isolation Expected Result: Drop rule exists with counter > 0 Evidence: .sisyphus/evidence/task3-ping-dropped.txt Scenario: Docker networking unaffected Tool: Bash (ssh + curl) Steps: 1. curl -s -o /dev/null -w '%{http_code}' http://172.20.8.191:80 2. docker ps Expected Result: HTTP 200, containers running Evidence: .sisyphus/evidence/task3-docker-ok.txtCommit: YES
- Message:
fix(nftables): add INPUT chain rules for WG peer isolation in InitNetwork - Files:
internal/firewall/nftables_linux.go
- In
-
4. Build + deploy to production
What to do:
- Build server-core:
cd apps/server-core && go build -tags dev ./... - Build dashboard-ui:
cd apps/dashboard-ui && npm run build - Commit all changes
- Push to remote
- SSH to production:
git pull --recurse-submodules && docker compose down && docker compose up -d --build - Remove old manual nftables rules on production
Must NOT do:
- Don't skip build verification
- Don't push broken code
Recommended Agent Profile:
- Category:
quick - Skills: []
Parallelization:
- Can Run In Parallel: NO
- Parallel Group: Wave 2
- Blocks: T5, F1-F4
- Blocked By: T2, T3
References:
connect_remote.txt— Production server SSH detailsdocker-compose.yml— Build configuration
Acceptance Criteria:
go build -tags dev ./...passes locallynpm run buildpasses locally- Production server running with new code
curl http://172.20.8.191:80returns 200
QA Scenarios:
Scenario: Production deployment successful Tool: Bash (ssh + curl) Steps: 1. SSH to server 2. docker logs nexus-guard-suite-server-core-1 2>&1 | tail -5 3. curl -s -o /dev/null -w '%{http_code}' http://172.20.8.191:80 4. curl -s -o /dev/null -w '%{http_code}' http://172.20.8.191:8080/api/v1/health Expected Result: Server started, HTTP 200 for both endpoints Evidence: .sisyphus/evidence/task4-deployment.txtCommit: YES
- Message:
feat: firewall INPUT fix + WG handshake device status - Files: all changed files
- Build server-core:
-
5. Verify all acceptance criteria on production
What to do:
- Run all acceptance tests from Metis review
- Verify device status (gogo2 shows online)
- Verify firewall (ping dropped, API works, Docker unaffected)
- Verify rules persist (restart server, check rules)
- Save all evidence
Must NOT do:
- Don't skip any acceptance test
- Don't assume — verify each criterion
Recommended Agent Profile:
- Category:
quick - Skills: []
Parallelization:
- Can Run In Parallel: NO
- Parallel Group: Wave 2 (after T4)
- Blocks: F1-F4
- Blocked By: T4
References:
connect_remote.txt— Production server SSH details- Plan "Definition of Done" section
Acceptance Criteria:
- All 7 acceptance tests pass
- Evidence files saved
QA Scenarios:
Scenario: Full acceptance test suite Tool: Bash (ssh + curl + nft + wg) Steps: 1. Test 1: Device gogo2 is_active=true via API 2. Test 2: ping 10.172.21.1 from WG peer → RTO 3. Test 3: curl http://10.172.21.1:8080/api/v1/health → 200 4. Test 4: curl http://172.20.8.191:80 → 200 (Docker) 5. Test 5: nft list chain ip nexusguard input → 5 rules 6. Test 6: Restart server, verify rules persist 7. Test 7: Dashboard shows device Online Expected Result: All 7 tests pass Evidence: .sisyphus/evidence/task5-full-qa.txtCommit: NO
Final Verification Wave
4 review agents run in PARALLEL. ALL must APPROVE. Present consolidated results to user and get explicit "okay" before completing.
-
F1. Plan Compliance Audit —
oracleRead 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. Compare deliverables against plan. Output:Must Have [N/N] | Must NOT Have [N/N] | Tasks [N/N] | VERDICT: APPROVE/REJECT -
F2. Code Quality Review —
unspecified-highRungo build -tags dev ./...+go vet ./.... 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] | Vet [PASS/FAIL] | Files [N clean/N issues] | VERDICT -
F3. Real Manual QA —
unspecified-highStart from clean state. Execute EVERY QA scenario from EVERY task. Test cross-task integration. Test edge cases. Save to.sisyphus/evidence/final-qa/. Output:Scenarios [N/N pass] | Integration [N/N] | Edge Cases [N tested] | VERDICT -
F4. Scope Fidelity Check —
deepFor each task: read "What to do", read actual diff. Verify 1:1. Check "Must NOT do" compliance. Flag unaccounted changes. Output:Tasks [N/N compliant] | Unaccounted [CLEAN/N files] | VERDICT
Commit Strategy
- Commit #1: Backend — wgmanager + heartbeat SyncToDB
- Commit #2: Backend — nftables INPUT chain rules
- Commit #3: Root — submodule refs
Success Criteria
Verification Commands
go build -tags dev ./... # Expected: no errors
go vet ./... # Expected: no warnings
wg show wg0 latest-handshakes # Expected: Unix timestamps
curl -s http://localhost:8080/api/v1/devices | jq '.[].is_active' # Expected: true for connected devices
nft list chain ip nexusguard input # Expected: rules with icmp, established, drop
Final Checklist
- All "Must Have" present
- All "Must NOT Have" absent
- Device with active WG peer shows Online
- Firewall INPUT chain drops non-allowed WG traffic
- Rules persist across restart