Files
Nexus-Guard-Suite/.sisyphus/plans/post-deploy-fixes.md
T

14 KiB

Post-Deploy Fixes + Config Delivery Plan

TL;DR

Quick Summary: Fix 3 immediate bugs (DNS clear, peer detection debug, peer IP change) and design secure config delivery for devices.

Deliverables:

  • DNS override can be cleared to empty (frontend fix)
  • Debug procedure documented for peer detection failures
  • InternalIP editable from DeviceDetail.vue
  • Firewall port range can be empty (apply to all ports)
  • Encrypted config delivery endpoint + companion plaintext endpoint

Estimated Effort: Medium Parallel Execution: YES - 2 waves Critical Path: T1 → [T2] → T3


Context

Original Issues

  1. DNS tidak bisa dikosongkan: Ketika user kosongkan field DNS Override di DeviceDetail.vue lalu save, advForm.value.dns jadi "", tapi "" || undefined di JavaScript menghasilkan undefined. Akibatnya field dns tidak terkirim ke API, nilai DB tidak berubah.

  2. Peer tidak mendeteksi koneksi: Device official WG client tetap Offline meski terkoneksi. Perlu debugging multi-layer: (1) cek collector jalan, (2) cek kernel handshake, (3) cek frontend isDeviceOnline().

  3. Tidak bisa ganti peer IP: UpdateDeviceRequest tidak memiliki field InternalIP. IP hanya dialokasikan sekali oleh IPAM saat provisioning.

  4. Config delivery: Ingin endpoint aman untuk device ambil config, tapi juga bisa debug plaintext untuk WireGuard client manual.

Current State

  • DNS backend: devices.go:236-238 — sudah handle req.DNS != nil, termasuk empty string
  • DNS frontend: DeviceDetail.vue:300dns: advForm.value.dns || undefined bug
  • Peer IP: UpdateDeviceRequest di devices.go:190 — TIDAK ada field InternalIP
  • Config endpoints:
    • GET /devices/:id/config → plaintext (admin only)
    • POST /provisionencrypted_config AES-256-GCM (token-based)
    • GET /devices/:id/config/qr → QR image (admin only)
    • Tidak ada endpoint publik untuk device mengambil config sendiri

Work Objectives

Objective A: Fix Bugs (Issue 1)

Must Have:

  • DNS override bisa dikosongkan dan tersimpan sebagai "" di DB
  • npm run build passes

Must NOT Have:

  • Tidak mengubah struktur API atau backend

Objective B: Debug Guide (Issue 2)

Must Have:

  • Step-by-step debugging procedure untuk peer detection
  • Commands untuk tiap layer (kernel, collector, DB, frontend)

Objective C: Peer IP Change (Issue 3)

Must Have:

  • Admin bisa mengubah internal_ip device via API
  • API memvalidasi format IP sebelum update
  • API mengecek duplikasi IP di IPAM sebelum update
  • Firewall rule re-sync jika IP berubah
  • Frontend input field untuk InternalIP di DeviceDetail.vue

Must NOT Have:

  • Jangan override alokasi IPAM untuk device yang belum provisioning
  • Jangan izinkan non-admin mengganti IP

Objective D: Config Delivery (Issue 4)

Questions to resolve (akan ditanyakan ke user):

  • Apakah device perlu endpoint autentikasi sendiri (token/API key) untuk ambil config?
  • Atau cukup admin ambilkan seperti sekarang?
  • Encrypted payload (AES-GCM) atau cukup HTTPS saja?
  • Perlu QR code juga atau config text saja?

Must Have (asumsi awal):

  • Endpoint untuk device mengambil config sendiri (setelah provisioning)
  • Opsi plaintext untuk debugging
  • Opsi encrypted payload untuk production

Verification Strategy

ZERO HUMAN INTERVENTION — ALL verification is agent-executed.

  • Frontend: npm run build + Playwright (cek DNS save + IP input)
  • Backend: go build -tags dev ./... + curl API test
  • Config: curl endpoint + validasi response format

Execution Strategy

Wave 1 (Parallel — all remaining work):
├── T1: Fix DNS clear bug (frontend) [DONE]
├── T2: Write debugging docs [INFORMATIONAL]
├── T3: Add InternalIP change to backend + frontend [DONE]
├── T4: Fix firewall port range to allow empty (models + API + frontend + nftables) [DONE]
└── F1-F3: Build verification + code review

Wave 2 (Future — separate plan):
├── Device-Agent Registration + Approval flow
└── Encrypted config delivery endpoint

TODOs

  • 1. Fix DNS clear bug

    What to do:

    • File: apps/dashboard-ui/src/views/DeviceDetail.vue baris 300
    • Ubah dns: advForm.value.dns || undefined menjadi dns: advForm.value.dns === '' ? '' : advForm.value.dns || undefined
    • Run npm run build untuk verifikasi

    Must NOT do:

    • Jangan ubah backend — sudah support empty string

    Parallelization: Wave 1, parallel with T2, T3

    Acceptance Criteria:

    • npm run build passes
    • Set DNS → kosongkan → save → reload → DNS tetap kosong

    QA Scenarios:

    Scenario: Clear DNS override saves correctly
      Tool: Bash (curl)
      Preconditions: Device with existing DNS
      Steps:
        1. curl -X PUT /api/v1/devices/$ID -H "Content-Type: application/json" -d '{"dns": ""}' -H "Authorization: Bearer $TOKEN"
        2. curl -X GET /api/v1/devices/$ID -H "Authorization: Bearer $TOKEN"
      Expected Result: Response JSON includes "dns": "" or null
      Evidence: .sisyphus/evidence/postdeploy-t1-dns-clear.txt
    
    Scenario: Build passes
      Tool: Bash
      Steps: cd apps/dashboard-ui && npm run build
      Expected Result: vue-tsc + vite build succeeds
      Evidence: .sisyphus/evidence/postdeploy-t1-build.txt
    

    Commit: YES — fix(ui): allow clearing DNS override to empty

  • 2. Document peer detection debugging procedure

    What to do: Create debugging doc covering 5 layers:

    1. Kernel: sudo wg show — peer handshake dari kernel
    2. Collector: docker logs grep handshake — apakah collector jalan
    3. Node type: query DB wg_servers.name — local node atau remote
    4. DB data: psql cek is_active, last_handshake, client_info
    5. Status API: curl /devices/:id/status — realtime status dari API

    Parallelization: Wave 1, parallel

    Acceptance Criteria:

    • Setiap langkah punya command exact yang bisa di-copy-paste
    • Setiap langkah punya expected output

    Commit: NO (informational)

  • 3. Add InternalIP to UpdateDeviceRequest + handler + UI

    What to do:

    Backendapps/server-core/api/devices.go:

    • Tambah InternalIP *string json:"internal_ip" ke UpdateDeviceRequest struct
    • Di Update handler, setelah blok DNS (line ~238), tambah:
      if req.InternalIP != nil {
          ip := net.ParseIP(*req.InternalIP)
          if ip == nil {
              c.JSON(http.StatusBadRequest, gin.H{"error": "invalid internal_ip format"})
              return
          }
          // Check duplicate IP across all devices + servers
          var count int64
          h.db.Model(&models.Device{}).Where("internal_ip = ? AND id != ?", *req.InternalIP, id).Count(&count)
          if count > 0 {
              c.JSON(http.StatusConflict, gin.H{"error": "internal_ip already in use"})
              return
          }
          updates["internal_ip"] = *req.InternalIP
          // Re-sync firewall if endpoint_allowed_ips also changed or existed
          if req.EndpointAllowedIPs != nil && *req.EndpointAllowedIPs != "" {
              destCIDR := *req.EndpointAllowedIPs
              // Remove old rule, add new rule
              h.fw.AddForwardRule(device.Name, ip, destCIDR)
          }
      }
      

    Frontendapps/dashboard-ui/src/views/DeviceDetail.vue:

    • Di advanced settings section, tambah input field:
      <div>
        <label class="block text-xs text-gray-500 mb-1">Internal IP (WireGuard)</label>
        <input v-model="advForm.internalIP" placeholder="10.172.21.x" class="w-full bg-black/50 border border-white/10 rounded p-2 text-white text-sm focus:border-cyan-500 focus:outline-none" />
      </div>
      
    • Di advForm ref, tambah internalIP: ''
    • Di load(), tambah advForm.value.internalIP = device.value.InternalIP || ''
    • Di saveAdvanced(), tambah internal_ip: advForm.value.internalIP || undefined

    Must NOT do:

    • Jangan izinkan non-admin mengganti IP
    • Jangan override IP device yang belum provisioning (!device.InternalIP == nil)
    • Jangan lupa re-sync firewall rule

    Parallelization: Wave 1

    Acceptance Criteria:

    • go build -tags dev ./... passes
    • npm run build passes
    • curl PUT dengan internal_ip → 200, IP berubah
    • curl PUT dengan IP duplikat → 409 Conflict
    • curl PUT dengan IP invalid → 400 Bad Request
    • IP baru muncul di Devices table + DeviceDetail

    QA Scenarios:

    Scenario: Change IP successfully
      Tool: Bash (curl)
      Preconditions: Existing device with internal_ip "10.172.21.5"
      Steps:
        1. curl -X PUT /devices/$ID -d '{"internal_ip":"10.172.21.100"}' -H "Authorization: Bearer $ADMIN_TOKEN"
        2. curl -X GET /devices/$ID -H "Authorization: Bearer $ADMIN_TOKEN"
      Expected Result: internal_ip = "10.172.21.100"
      Evidence: .sisyphus/evidence/postdeploy-t3-ip-change.txt
    
    Scenario: Duplicate IP rejected
      Tool: Bash (curl)
      Preconditions: Another device already has 10.172.21.5
      Steps:
        1. curl -X PUT /devices/$ID -d '{"internal_ip":"10.172.21.5"}' -H "Authorization: Bearer $ADMIN_TOKEN"
      Expected Result: HTTP 409, error "internal_ip already in use"
      Evidence: .sisyphus/evidence/postdeploy-t3-ip-duplicate.txt
    
    Scenario: Invalid IP rejected
      Tool: Bash (curl)
      Steps:
        1. curl -X PUT /devices/$ID -d '{"internal_ip":"not-an-ip"}' -H "Authorization: Bearer $ADMIN_TOKEN"
      Expected Result: HTTP 400, error "invalid internal_ip format"
      Evidence: .sisyphus/evidence/postdeploy-t3-ip-invalid.txt
    

    Commit: YES

    • Message: feat(api): add internal_ip update to device endpoint


(Tambahan) T4: Fix port range firewall agar bisa kosong

What to do:

  • models.go (apps/server-core/internal/models/models.go:100): Ubah DestPortRange string \gorm:"not null;size:32"`` menjadi:
    DestPortRange string `gorm:"size:32;default:''"`
    
  • rules.go (apps/server-core/api/rules.go:44): Ubah DestPortRange string \json:"dest_port_range" binding:"required"`` menjadi:
    DestPortRange string `json:"dest_port_range"`
    
  • rules.go (handler Create di rules.go): Tambah default empty string jika kosong:
    if req.DestPortRange == "" {
        req.DestPortRange = ""  // "all ports"
    }
    
  • rules.go (nftables sync rules.go:126-134): Jika DestPortRange kosong, jangan generate dport match di nftables:
    if rule.DestPortRange != "" {
        // parse port range dan apply
    } else {
        // rule tanpa port filter — berlaku untuk semua port
    }
    
  • FirewallEditor.vue (apps/dashboard-ui/src/components/FirewallEditor.vue:12): Hapus required, ganti placeholder:
    <input v-model="form.port" placeholder="80 or 8000-9000 (kosongkan untuk semua port)">
    

Must NOT do:

  • Jangan hapus migrate/rollback — GORM AutoMigrate handle kolom not null → nullable
  • Jangan ubah nftables InitNetwork atau default policy

Commit: YES — fix(firewall): allow empty port range for all-port rules


Wave 2: Device-Agent Registration & Config Delivery (Plan Terpisah)

Klarifikasi User (Sudah Dijawab)

Pertanyaan Jawaban
Auth method API key per-device. User register → device-agent daftar dengan API key → User approve
Encryption Keduanya: endpoint encrypted (production) + endpoint plaintext (debug)
Scope Yang aman, tapi bisa di-debug dengan official WireGuard client

Short Term (NOW) — Pakai existing flow untuk official WG client:

Official WireGuard client sudah bisa dapat config via:

  1. Admin export: GET /devices/:id/config (admin only) → return plaintext WireGuard config
  2. QR Code: GET /devices/:id/config/qr (admin only) → scan di mobile WG app
  3. Download .conf: Via UI DeviceDetail → button Show Config

Ini sudah jalan. Tidak perlu perubahan. Admin tinggal export config untuk device official WG client.

Medium Term — Device-Agent Auto-Registration

Flow yang diminta user:

1. Admin create device di dashboard → system generate API key
2. Device-agent daftar dengan API key → server validasi
3. Admin approve device (atau auto-approve)
4. Device-agent dapat akses ke endpoint config
5. GET /device/config → return encrypted_config (AES-256-GCM)
6. GET /admin/device/:id/config/debug → return plaintext (admin only, untuk debug)

Config endpoint plaintext untuk debugging SUDAH ADA (GET /devices/:id/config admin-only)

Yang perlu dibangun (device-agent plan terpisah):

  • Backend: API key generation endpoint (generate key per device)
  • Backend: Device self-registration endpoint (POST /devices/register dengan API key)
  • Backend: Approval flow (auto atau manual)
  • Backend: New endpoint GET /device/config (auth with API key) → return encrypted_config
  • Device-agent: Registration flow (daftar dengan API key, simpan credential)
  • Device-agent: Config fetch & decryption flow
+ 🔜 Akan dijadikan plan terpisah: `device-agent-registration.md`

Untuk sekarang, fokus ke T3 (IP Change) dulu karena itu yang blocking.


Final Verification Wave

  • F1. Build Verification — go build + npm build PASS
  • F2. DNS clear test — code diterapkan, build PASS
  • F3. IP change test — code di-commit oleh subagent, build PASS
  • F4. Config endpoint test — Wave 2 (device-agent registration plan)

Commit Strategy

  • T1: fix(ui): allow clearing DNS override to empty
  • T3: feat(api): add internal_ip update to device endpoint
  • T4: fix(firewall): allow empty port range for all-port rules
  • Wave 2: Device-Agent Registration (plan terpisah nanti)

Success Criteria

Verification Commands

go build -tags dev ./...  # Expected: PASS
cd apps/dashboard-ui && npm run build  # Expected: PASS

Final Checklist

  • DNS override bisa dikosongkan
  • Debug procedure documented (informational)
  • IP bisa diganti via API + UI
  • Config delivery endpoint Wave 2
  • Semua build pass