Project 7: Software Defined Perimeter (SDP) Controller (The "Black Cloud")

Project 7: Software Defined Perimeter (SDP) Controller (The “Black Cloud”)

Build a system that makes your services invisible to the public internet. No open ports, no attack surface.

Quick Reference

Attribute Value
Difficulty Level 5: Master
Time Estimate 1 Month
Primary Language Go
Alternative Languages C, Python
Prerequisites Network programming, cryptography, Linux administration, iptables
Key Topics Single Packet Authorization (SPA), WireGuard, raw sockets, firewall orchestration
Core Reading “Zero Trust Networks” by Gilman & Barth, Chapter 10

1. Learning Objectives

By completing this project, you will:

  1. Implement Single Packet Authorization (SPA): Design a stateless authentication mechanism using cryptographically signed UDP packets
  2. Master raw socket programming: Capture and parse network packets at the lowest level using libpcap/gopacket
  3. Orchestrate dynamic firewalls: Programmatically insert and remove iptables rules in real-time
  4. Integrate WireGuard: Automate VPN peer addition and removal through the kernel’s netlink interface
  5. Implement anti-replay protection: Use timestamps, nonces, and sliding windows to prevent packet replay attacks
  6. Build a “dark” network service: Create infrastructure that is invisible to port scanners while remaining accessible to authorized clients

2. Deep Theoretical Foundation

2.1 The “Dark Cloud” Concept

Traditional network security follows a “perimeter defense” model: services are exposed to the internet, and security relies on authentication and access control. This creates a fundamental problem: if an attacker can see your service, they can attack it.

The Software-Defined Perimeter (SDP) flips this model. Services are hidden behind closed firewall ports. To gain access, a client must prove their identity BEFORE any TCP connection is established.

+===========================================================================+
|                    TRADITIONAL ARCHITECTURE (Visible)                       |
+===========================================================================+
|                                                                            |
|   Internet                              Your Network                       |
|   --------                              ------------                       |
|                                                                            |
|   [Attacker]                            [Your Server]                      |
|       |                                      |                             |
|       |  nmap scan                           |                             |
|       |------------------------------------->| Port 443: OPEN              |
|       |                                      | Port 22: OPEN               |
|       |<-------------------------------------|                             |
|       |  "I found your ports!"               |                             |
|       |                                      |                             |
|       |  CVE-2024-XXXX exploit               |                             |
|       |------------------------------------->| COMPROMISED                 |
|       |                                      |                             |
|                                                                            |
|   PROBLEM: Attackers can find and exploit your services                    |
|                                                                            |
+===========================================================================+

+===========================================================================+
|                    SDP ARCHITECTURE (Dark Cloud)                            |
+===========================================================================+
|                                                                            |
|   Internet                              Your Network                       |
|   --------                              ------------                       |
|                                                                            |
|   [Attacker]                            [Your Server]                      |
|       |                                      |                             |
|       |  nmap scan                           |                             |
|       |------------------------------------->| ALL PORTS: FILTERED         |
|       |                                      | (no response)               |
|       |  ... silence ...                     |                             |
|       |                                      |                             |
|   "Is there even a server here?"             |                             |
|                                                                            |
|   [Authorized User]                     [SDP Controller]                   |
|       |                                      |                             |
|       |  1. SPA Packet (signed UDP)          |                             |
|       |------------------------------------->| Verify signature            |
|       |                                      | Verify timestamp            |
|       |                                      | Check authorization         |
|       |                                      |                             |
|       |  2. Controller opens firewall        |                             |
|       |                                      | iptables -A INPUT           |
|       |                                      |   -s <client-ip>            |
|       |                                      |   -p tcp --dport 443        |
|       |                                      |   -j ACCEPT                 |
|       |                                      |                             |
|       |  3. Now client can connect           |                             |
|       |------------------------------------->| Port 443: OPEN (for you)    |
|       |<-------------------------------------|                             |
|       |  Normal HTTPS session                |                             |
|       |                                      |                             |
|       |  4. After timeout, hole closes       |                             |
|       |                                      | Rule automatically removed  |
|       |                                      | Port 443: FILTERED again    |
|                                                                            |
|   SOLUTION: Zero attack surface until proven authorized                    |
|                                                                            |
+===========================================================================+

2.2 Attack Surface Reduction

Attack Surface Reduction is a fundamental Zero Trust principle. The SDP model implements this at the network layer:

+===========================================================================+
|                    ATTACK SURFACE COMPARISON                                |
+===========================================================================+
|                                                                            |
|   TRADITIONAL                               SDP (Black Cloud)              |
|   -----------                               -----------------              |
|                                                                            |
|   Visible Attack Surface:                   Visible Attack Surface:        |
|   +-------------------------+               +-------------------------+    |
|   | SSH (22)      [EXPOSED] |               |                         |    |
|   | HTTPS (443)   [EXPOSED] |               |  (nothing visible)      |    |
|   | API (8080)    [EXPOSED] |               |                         |    |
|   | Database (5432)[EXPOSED]|               +-------------------------+    |
|   +-------------------------+                                              |
|                                                                            |
|   Attacker Options:                         Attacker Options:              |
|   - Port scanning: Works                    - Port scanning: Nothing found |
|   - Banner grabbing: Works                  - Banner grabbing: No banners  |
|   - CVE exploitation: Works                 - CVE exploitation: No targets |
|   - Brute force: Works                      - Brute force: No endpoints    |
|   - DoS attacks: Works                      - DoS attacks: What service?   |
|                                                                            |
|   Authentication:                           Authentication:                |
|   After connection established              BEFORE connection possible     |
|                                                                            |
+===========================================================================+

2.3 Single Packet Authorization (SPA) vs Port Knocking

SPA is the evolution of “port knocking” but addresses critical security weaknesses:

+===========================================================================+
|                    PORT KNOCKING vs SPA COMPARISON                          |
+===========================================================================+
|                                                                            |
|   PORT KNOCKING (Original Concept)                                         |
|   --------------------------------                                         |
|                                                                            |
|   Client sends sequence:  Port 1234 -> Port 5678 -> Port 9012              |
|                                                                            |
|   PROBLEMS:                                                                |
|   1. Sequence can be sniffed and replayed                                  |
|   2. Stateful server must track partial sequences                          |
|   3. No cryptographic authentication                                       |
|   4. No payload for additional context                                     |
|   5. DoS via partial sequence flooding                                     |
|                                                                            |
|   Attacker captures:                                                       |
|   +------------------+                                                     |
|   | SYN to port 1234 |  ---> Replay these packets                          |
|   | SYN to port 5678 |  ---> Attacker gains access!                        |
|   | SYN to port 9012 |                                                     |
|   +------------------+                                                     |
|                                                                            |
+---------------------------------------------------------------------------+
|                                                                            |
|   SINGLE PACKET AUTHORIZATION (Modern Approach)                            |
|   ---------------------------------------------                            |
|                                                                            |
|   Client sends ONE UDP packet containing:                                  |
|   +------------------------------------------------------------------+     |
|   | Random Data | Username | Client IP | Timestamp | HMAC Signature  |     |
|   +------------------------------------------------------------------+     |
|                                                                            |
|   ADVANTAGES:                                                              |
|   1. Cryptographically signed (cannot forge)                               |
|   2. Timestamp prevents replay (window of validity)                        |
|   3. Stateless verification (no DoS via partial state)                     |
|   4. Carries authorization context                                         |
|   5. Single packet - minimal network footprint                             |
|                                                                            |
|   Attacker captures and replays:                                           |
|   +------------------------------------------------------------------+     |
|   | Same packet from earlier |  ---> Timestamp expired!               |     |
|   |                          |  ---> Nonce already used!              |     |
|   |                          |  ---> Source IP mismatch!              |     |
|   +------------------------------------------------------------------+     |
|   REJECTED                                                                 |
|                                                                            |
+===========================================================================+

2.4 SPA Packet Format (CSA SDP v2.0 Standard)

The Cloud Security Alliance (CSA) defines the SDP specification. Here is the SPA packet structure:

+===========================================================================+
|                    SPA PACKET FORMAT (Based on CSA SDP v2.0)                |
+===========================================================================+
|                                                                            |
|   Byte Offset    Size      Field Description                               |
|   -----------    ----      -----------------                               |
|                                                                            |
|   0-15           16        Random Data (entropy, obscures pattern)         |
|   16-31          16        SHA-256 Digest (first 16 bytes for ID)          |
|   32-63          32        Username / Client ID                            |
|   64-67          4         Timestamp (Unix epoch, seconds)                 |
|   68-71          4         SPA Protocol Version                            |
|   72-73          2         Message Type (0x01 = Access Request)            |
|   74-75          2         Requested Service Port                          |
|   76-79          4         Client IP Address (claimed source)              |
|   80-83          4         Nonce (random, never reused)                    |
|   84-115         32        HMAC-SHA256 Signature                           |
|   -----------    ----      -----------------                               |
|   Total          116       bytes (minimum SPA packet)                      |
|                                                                            |
+---------------------------------------------------------------------------+
|                                                                            |
|   PACKET LAYOUT DIAGRAM:                                                   |
|                                                                            |
|   0                   1                   2                   3            |
|   0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1          |
|   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+         |
|   |                                                               |         |
|   |                    Random Data (16 bytes)                     |         |
|   |                                                               |         |
|   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+         |
|   |                                                               |         |
|   |                    SHA-256 Digest (16 bytes)                  |         |
|   |                                                               |         |
|   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+         |
|   |                                                               |         |
|   |                    Username (32 bytes, null-padded)           |         |
|   |                                                               |         |
|   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+         |
|   |                          Timestamp                            |         |
|   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+         |
|   |                       Protocol Version                        |         |
|   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+         |
|   |       Message Type        |       Requested Port              |         |
|   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+         |
|   |                      Client IP Address                        |         |
|   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+         |
|   |                           Nonce                               |         |
|   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+         |
|   |                                                               |         |
|   |                   HMAC-SHA256 (32 bytes)                      |         |
|   |                                                               |         |
|   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+         |
|                                                                            |
+===========================================================================+

2.5 Anti-Replay Protection

Replay attacks are the primary threat to SPA. Multiple layers of protection are required:

+===========================================================================+
|                    ANTI-REPLAY PROTECTION MECHANISMS                        |
+===========================================================================+
|                                                                            |
|   LAYER 1: TIMESTAMP VALIDATION                                            |
|   ------------------------------                                           |
|                                                                            |
|   +-----------------+                                                      |
|   | Packet arrives  |                                                      |
|   +-----------------+                                                      |
|           |                                                                |
|           v                                                                |
|   +---------------------------------------------+                          |
|   | timestamp_diff = abs(now - packet.timestamp)|                          |
|   |                                             |                          |
|   | if timestamp_diff > MAX_CLOCK_SKEW (30s):  |                          |
|   |     REJECT "Timestamp outside window"       |                          |
|   +---------------------------------------------+                          |
|                                                                            |
|   NOTE: Requires synchronized clocks (NTP)                                 |
|                                                                            |
+---------------------------------------------------------------------------+
|                                                                            |
|   LAYER 2: NONCE TRACKING (Sliding Window)                                 |
|   ----------------------------------------                                 |
|                                                                            |
|   Maintain a bloom filter or hash set of recently seen nonces:             |
|                                                                            |
|   +------------------------------------------------------------------+     |
|   | Nonce Cache (last 5 minutes of valid nonces)                     |     |
|   +------------------------------------------------------------------+     |
|   | 0x8a3f21b4 | 0x1c9e72d8 | 0x5f3a8e19 | 0x7d2c4b6a | 0x9e1f3c8b | |     |
|   +------------------------------------------------------------------+     |
|                                                                            |
|   if packet.nonce in nonce_cache:                                          |
|       REJECT "Nonce already used (replay detected)"                        |
|   else:                                                                    |
|       nonce_cache.add(packet.nonce)                                        |
|       expire_old_nonces()  # Remove nonces older than window               |
|                                                                            |
+---------------------------------------------------------------------------+
|                                                                            |
|   LAYER 3: SOURCE IP BINDING                                               |
|   ---------------------------                                              |
|                                                                            |
|   if packet.claimed_ip != packet.actual_source_ip:                         |
|       REJECT "Source IP mismatch"                                          |
|                                                                            |
|   NOTE: Prevents captured packets from being used from different IP        |
|         (unless attacker can spoof IP, which requires network access)      |
|                                                                            |
+---------------------------------------------------------------------------+
|                                                                            |
|   LAYER 4: CRYPTOGRAPHIC BINDING                                           |
|   ------------------------------                                           |
|                                                                            |
|   hmac_input = random_data || digest || username || timestamp ||           |
|                version || msg_type || port || client_ip || nonce           |
|                                                                            |
|   expected_hmac = HMAC-SHA256(shared_secret, hmac_input)                   |
|                                                                            |
|   if packet.hmac != expected_hmac:                                         |
|       REJECT "Invalid signature (tampered or wrong key)"                   |
|                                                                            |
|   NOTE: Without the secret key, attacker cannot forge valid packets        |
|                                                                            |
+===========================================================================+

2.6 Raw Socket Programming and Packet Capture

To capture SPA packets without opening a port, we use raw sockets with libpcap:

+===========================================================================+
|                    RAW SOCKET / LIBPCAP ARCHITECTURE                        |
+===========================================================================+
|                                                                            |
|   APPLICATION LAYER                                                        |
|   -----------------                                                        |
|                                                                            |
|   +-------------------+                                                    |
|   |  SDP Controller   |                                                    |
|   +-------------------+                                                    |
|           |                                                                |
|           | gopacket / libpcap                                             |
|           |                                                                |
|   --------+--------                                                        |
|           |                                                                |
|           v                                                                |
|   +-------------------+                                                    |
|   |   Packet Filter   |  BPF: "udp and dst port 62201"                     |
|   +-------------------+                                                    |
|           |                                                                |
|   --------+--------                                                        |
|           |                                                                |
|   +-------------------+                                                    |
|   |   Raw Socket      |  PF_PACKET / SOCK_RAW                              |
|   +-------------------+                                                    |
|           |                                                                |
|   --------+--------                                                        |
|           |                                                                |
|   +-------------------+                                                    |
|   |   Network Driver  |                                                    |
|   +-------------------+                                                    |
|           |                                                                |
|           v                                                                |
|   +-------------------+                                                    |
|   |   NIC (eth0)      |  Receives all packets                              |
|   +-------------------+                                                    |
|                                                                            |
+---------------------------------------------------------------------------+
|                                                                            |
|   WHY LIBPCAP INSTEAD OF NORMAL UDP SOCKET?                                |
|                                                                            |
|   Normal UDP Socket:                                                       |
|   - Requires binding to a port                                             |
|   - Port appears OPEN in nmap scans                                        |
|   - Kernel responds to traffic (even if we ignore it)                      |
|                                                                            |
|   Libpcap / Raw Socket:                                                    |
|   - No port binding required                                               |
|   - Port appears FILTERED in nmap scans                                    |
|   - Completely passive - kernel's netfilter drops traffic                  |
|   - We "sniff" the packet before it's dropped                              |
|                                                                            |
+---------------------------------------------------------------------------+
|                                                                            |
|   PACKET CAPTURE FLOW:                                                     |
|                                                                            |
|   1. Network card receives packet                                          |
|   2. Kernel copies packet to our raw socket buffer (via BPF filter)        |
|   3. Original packet continues through netfilter                           |
|   4. Netfilter's DROP rule blocks the packet (port appears filtered)       |
|   5. Our application processes the copy we received                        |
|                                                                            |
|   +-----------+     +-----------+     +-----------+                        |
|   |  Packet   | --> | Copy to   | --> | Continue  |                        |
|   |  Arrives  |     | our buffer|     | to netflt |                        |
|   +-----------+     +-----------+     +-----------+                        |
|                           |                 |                              |
|                           v                 v                              |
|                     +-----------+     +-----------+                        |
|                     | SDP Ctrl  |     |   DROP    |                        |
|                     | processes |     | (no resp) |                        |
|                     +-----------+     +-----------+                        |
|                                                                            |
+===========================================================================+

2.7 WireGuard Architecture and Peer Management

WireGuard provides the encrypted tunnel after SPA authorization:

+===========================================================================+
|                    WIREGUARD INTEGRATION ARCHITECTURE                       |
+===========================================================================+
|                                                                            |
|   WIREGUARD CONCEPTS:                                                      |
|                                                                            |
|   +------------------+          +------------------+                       |
|   |  CLIENT (Peer)   |          |  SERVER (Peer)   |                       |
|   +------------------+          +------------------+                       |
|   | Private Key: A   |          | Private Key: B   |                       |
|   | Public Key: A'   |<-------->| Public Key: B'   |                       |
|   | Endpoint: ?:?    |          | Endpoint: S:51820|                       |
|   | Allowed IPs:     |          | Allowed IPs:     |                       |
|   |   10.200.0.0/24  |          |   10.200.0.2/32  |                       |
|   +------------------+          +------------------+                       |
|                                                                            |
+---------------------------------------------------------------------------+
|                                                                            |
|   SDP + WIREGUARD FLOW:                                                    |
|                                                                            |
|   1. Client generates WireGuard keypair (on enrollment)                    |
|      - Stores public key on SDP Controller                                 |
|      - Keeps private key locally                                           |
|                                                                            |
|   2. Client sends SPA packet (includes their public key hash)              |
|                                                                            |
|   3. SDP Controller verifies SPA, then:                                    |
|      +--------------------------------------------------------------+      |
|      | wg set wg0 peer <client_pubkey>                              |      |
|      |   allowed-ips 10.200.0.X/32                                  |      |
|      |   endpoint <client_ip>:random_port                           |      |
|      +--------------------------------------------------------------+      |
|                                                                            |
|   4. Client can now establish WireGuard handshake                          |
|                                                                            |
|   5. After timeout, SDP Controller removes peer:                           |
|      +--------------------------------------------------------------+      |
|      | wg set wg0 peer <client_pubkey> remove                       |      |
|      +--------------------------------------------------------------+      |
|                                                                            |
+---------------------------------------------------------------------------+
|                                                                            |
|   WIREGUARD NETLINK API (Programmatic Control):                            |
|                                                                            |
|   Traditional approach:                                                    |
|   - Shell out to `wg` command                                              |
|   - Parse text output                                                      |
|   - Error-prone, slow                                                      |
|                                                                            |
|   Modern approach (used in this project):                                  |
|   - Use netlink socket directly                                            |
|   - golang.zx2c4.com/wireguard/wgctrl library                              |
|   - Atomic operations, no parsing                                          |
|                                                                            |
|   import "golang.zx2c4.com/wireguard/wgctrl"                               |
|                                                                            |
|   client, _ := wgctrl.New()                                                |
|   client.ConfigureDevice("wg0", wgtypes.Config{                            |
|       Peers: []wgtypes.PeerConfig{{                                        |
|           PublicKey:  peerPubKey,                                          |
|           AllowedIPs: []net.IPNet{clientNet},                              |
|       }},                                                                  |
|   })                                                                       |
|                                                                            |
+===========================================================================+

2.8 Dynamic Firewall Rule Management

The SDP Controller orchestrates iptables rules in real-time:

+===========================================================================+
|                    IPTABLES RULE ORCHESTRATION                              |
+===========================================================================+
|                                                                            |
|   BASELINE FIREWALL STATE (No Authorized Clients):                         |
|                                                                            |
|   iptables -L INPUT -n --line-numbers                                      |
|   +----+----------------------------------------------------------+        |
|   | #  | Rule                                                     |        |
|   +----+----------------------------------------------------------+        |
|   | 1  | ACCEPT all -- 0.0.0.0/0 0.0.0.0/0 state ESTABLISHED     |        |
|   | 2  | ACCEPT icmp -- 0.0.0.0/0 0.0.0.0/0 (optional ping)       |        |
|   | 3  | DROP all -- 0.0.0.0/0 0.0.0.0/0 (default deny)          |        |
|   +----+----------------------------------------------------------+        |
|                                                                            |
|   Result: ALL incoming connections blocked (ports appear filtered)         |
|                                                                            |
+---------------------------------------------------------------------------+
|                                                                            |
|   AFTER SPA AUTHORIZATION (Client 203.0.113.50 approved for port 443):     |
|                                                                            |
|   iptables -L INPUT -n --line-numbers                                      |
|   +----+----------------------------------------------------------+        |
|   | #  | Rule                                                     |        |
|   +----+----------------------------------------------------------+        |
|   | 1  | ACCEPT all -- 0.0.0.0/0 0.0.0.0/0 state ESTABLISHED     |        |
|   | 2  | ACCEPT icmp -- 0.0.0.0/0 0.0.0.0/0                       |        |
|   | 3  | ACCEPT tcp -- 203.0.113.50 0.0.0.0/0 dport 443 [NEW]    |        |
|   | 4  | DROP all -- 0.0.0.0/0 0.0.0.0/0                          |        |
|   +----+----------------------------------------------------------+        |
|                                                                            |
|   Result: Only client 203.0.113.50 can connect to port 443                 |
|                                                                            |
+---------------------------------------------------------------------------+
|                                                                            |
|   RULE MANAGEMENT CONSIDERATIONS:                                          |
|                                                                            |
|   1. INSERTION POSITION                                                    |
|      - Rules must be inserted BEFORE the DROP rule                         |
|      - Use: iptables -I INPUT <position> ...                               |
|      - Track rule count to calculate correct position                      |
|                                                                            |
|   2. RULE EXPIRATION                                                       |
|      - Each rule has a TTL (e.g., 5 minutes)                               |
|      - Background goroutine tracks and removes expired rules               |
|      - Use iptables comments to tag rules: -m comment --comment "sdp-XXX"  |
|                                                                            |
|   3. RULE IDENTIFICATION                                                   |
|      - Include unique ID in comment for safe removal                       |
|      - Example: --comment "sdp-$(uuid)-expires-$(timestamp)"               |
|                                                                            |
|   4. ATOMIC OPERATIONS                                                     |
|      - Use iptables-restore for batch updates (more efficient)             |
|      - Or use nftables (modern replacement, better API)                    |
|                                                                            |
|   5. RULE CLEANUP ON CRASH                                                 |
|      - On startup, remove all rules with "sdp-" prefix                     |
|      - Prevents stale rules from previous runs                             |
|                                                                            |
+---------------------------------------------------------------------------+
|                                                                            |
|   IPTABLES COMMANDS USED:                                                  |
|                                                                            |
|   # Insert rule at position 3 (before DROP)                                |
|   iptables -I INPUT 3 -s 203.0.113.50 -p tcp --dport 443 \                 |
|       -m comment --comment "sdp-abc123-exp-1703894400" -j ACCEPT           |
|                                                                            |
|   # Remove rule by specification                                           |
|   iptables -D INPUT -s 203.0.113.50 -p tcp --dport 443 \                   |
|       -m comment --comment "sdp-abc123-exp-1703894400" -j ACCEPT           |
|                                                                            |
|   # List rules with comments                                               |
|   iptables -L INPUT -n -v --line-numbers                                   |
|                                                                            |
|   # Remove all SDP rules (cleanup)                                         |
|   iptables-save | grep -v "sdp-" | iptables-restore                        |
|                                                                            |
+===========================================================================+

3. Complete Project Specification

3.1 What You Will Build

A complete Software-Defined Perimeter system consisting of:

  1. SDP Controller: A Go daemon that listens for SPA packets, verifies them, and orchestrates firewall/WireGuard changes
  2. SPA Client: A command-line tool that generates and sends SPA packets
  3. Enrollment System: A method to pre-authorize clients with shared secrets

3.2 Functional Requirements

Component Requirement
SPA Packet Capture Capture UDP packets on port 62201 using raw sockets (not bound socket)
SPA Verification Verify HMAC-SHA256 signature, timestamp (30s window), and nonce
Anti-Replay Maintain sliding window of seen nonces, reject duplicates
Firewall Integration Insert/remove iptables rules for specific source IP and port
WireGuard Integration Add/remove peers dynamically using wgctrl
Rule Expiration Automatically remove rules after configurable timeout (default: 5 min)
Logging Comprehensive logging of all authorization decisions
Client Tool CLI to generate SPA packets with proper format and signing

3.3 Non-Functional Requirements

Requirement Specification
Performance Process 1000+ SPA packets per second
Latency Firewall rule inserted within 10ms of valid SPA
Reliability Controller must handle crash/restart gracefully
Security No information leakage (port scanning reveals nothing)
Portability Linux only (kernel requirements for raw sockets, iptables, WireGuard)

3.4 Acceptance Criteria

  1. Dark Port Test: nmap -sS -p 443 <server> shows port as “filtered” (not “open” or “closed”)
  2. SPA Access Test: After sending valid SPA, nmap -sS -p 443 <server> shows port as “open” from authorized IP only
  3. Replay Prevention: Replaying captured SPA packet fails authorization
  4. Timeout Test: Access revoked automatically after configured timeout

4. Real World Outcome

When you complete this project, here’s exactly what you’ll see:

+===========================================================================+
|                    REAL WORLD DEMONSTRATION                                 |
+===========================================================================+

SCENARIO: Server at 198.51.100.10 running web service on port 443

=== BEFORE SPA AUTHORIZATION ===

$ nmap -sS -Pn -p 443,22,80,8080 198.51.100.10

Starting Nmap 7.94 ( https://nmap.org )
Nmap scan report for 198.51.100.10
Host is up.

PORT     STATE    SERVICE
22/tcp   filtered ssh
80/tcp   filtered http
443/tcp  filtered https
8080/tcp filtered http-proxy

Nmap done: 1 IP address (1 host up) scanned in 3.21 seconds

INTERPRETATION: All ports appear filtered (no response at all).
               The server is "dark" - attacker cannot determine:
               - Whether ports are in use
               - What services are running
               - What software versions exist


=== SENDING SPA PACKET ===

$ ./sdp-client --server 198.51.100.10 --port 443 --key /etc/sdp/client.key

[2024-12-27 10:30:45] Generating SPA packet...
[2024-12-27 10:30:45]   Username:   alice
[2024-12-27 10:30:45]   Timestamp:  1703676645
[2024-12-27 10:30:45]   Nonce:      0x8a3f21b4
[2024-12-27 10:30:45]   Target:     443/tcp
[2024-12-27 10:30:45]   HMAC:       b7c9e2f1... (truncated)
[2024-12-27 10:30:45] Sending SPA to 198.51.100.10:62201...
[2024-12-27 10:30:45] SPA packet sent successfully (116 bytes)


=== SERVER SIDE (Controller Logs) ===

$ journalctl -u sdp-controller -f

Dec 27 10:30:45 server sdp-controller[1234]: Captured SPA packet from 203.0.113.50
Dec 27 10:30:45 server sdp-controller[1234]: Verifying SPA for user: alice
Dec 27 10:30:45 server sdp-controller[1234]:   Timestamp: valid (drift: 0s)
Dec 27 10:30:45 server sdp-controller[1234]:   Nonce: not in replay cache
Dec 27 10:30:45 server sdp-controller[1234]:   HMAC: verified
Dec 27 10:30:45 server sdp-controller[1234]:   Source IP: matches claimed IP
Dec 27 10:30:45 server sdp-controller[1234]: AUTHORIZED: alice from 203.0.113.50 for port 443
Dec 27 10:30:45 server sdp-controller[1234]: Inserting iptables rule: -I INPUT 3 -s 203.0.113.50 -p tcp --dport 443 -j ACCEPT
Dec 27 10:30:45 server sdp-controller[1234]: Rule expires at: 2024-12-27 10:35:45 (5m0s)


=== AFTER SPA AUTHORIZATION (From Authorized Client) ===

$ nmap -sS -Pn -p 443 198.51.100.10

Starting Nmap 7.94 ( https://nmap.org )
Nmap scan report for 198.51.100.10
Host is up (0.023s latency).

PORT    STATE SERVICE
443/tcp open  https      <-- PORT IS NOW OPEN (for this IP only!)

Nmap done: 1 IP address (1 host up) scanned in 0.15 seconds


=== SAME SCAN FROM UNAUTHORIZED IP ===

$ ssh other-server
other-server$ nmap -sS -Pn -p 443 198.51.100.10

PORT    STATE    SERVICE
443/tcp filtered https   <-- Still filtered for everyone else!


=== REPLAY ATTACK ATTEMPT ===

Attacker captures and replays the SPA packet:

$ tcpreplay -i eth0 captured_spa.pcap

Controller logs:
Dec 27 10:31:15 server sdp-controller[1234]: Captured SPA packet from 192.0.2.99
Dec 27 10:31:15 server sdp-controller[1234]: REJECTED: Source IP mismatch (claimed: 203.0.113.50, actual: 192.0.2.99)

Attacker tries again 5 minutes later (after capturing another packet):

Dec 27 10:35:45 server sdp-controller[1234]: Captured SPA packet from 192.0.2.99
Dec 27 10:35:45 server sdp-controller[1234]: REJECTED: Timestamp expired (received: 1703676645, now: 1703676945, diff: 300s > 30s)


=== AUTOMATIC RULE EXPIRATION ===

Dec 27 10:35:45 server sdp-controller[1234]: Rule expired: alice from 203.0.113.50 for port 443
Dec 27 10:35:45 server sdp-controller[1234]: Removing iptables rule: -D INPUT -s 203.0.113.50 -p tcp --dport 443 -j ACCEPT
Dec 27 10:35:45 server sdp-controller[1234]: Active rules: 0

=== PORT IS FILTERED AGAIN ===

$ nmap -sS -Pn -p 443 198.51.100.10

PORT    STATE    SERVICE
443/tcp filtered https   <-- Back to dark!

+===========================================================================+

The Core Question You’re Answering

“How can I make my infrastructure invisible to unauthorized users while providing seamless, secure access to authorized ones?”

This project addresses the fundamental limitation of traditional network security: visible services can be attacked. Every open port is an invitation for reconnaissance, exploitation attempts, and brute-force attacks. The Software-Defined Perimeter (SDP) solves this by implementing a “dark cloud” architecture where your services simply do not exist on the network until an authorized user proves their identity.

Think of it like a speakeasy during Prohibition: there’s no sign, no visible door, just a blank wall. Only those who know the secret knock (cryptographic authentication) can even see the entrance. For everyone else, including attackers, your infrastructure appears as an empty void. Port scans return nothing. Fingerprinting fails. Zero-day exploits have no target.

The key insight is authenticate first, connect second. Traditional security does it backwards: you connect to SSH, THEN enter credentials. With SDP, you prove identity via a cryptographically signed Single Packet Authorization (SPA) BEFORE the firewall even allows your packets through. This isn’t just security-in-depth; it’s making the attack surface mathematically zero for unauthorized parties.


Concepts You Must Understand First

Before diving into implementation, ensure you have a solid grasp of these foundational concepts:

1. Software-Defined Perimeter (SDP) Architecture - CSA Specification

The Cloud Security Alliance (CSA) defines the SDP specification that this project implements. The architecture has three core components:

  • Controller: The brain that validates SPA packets and orchestrates access decisions
  • Gateway: The enforcement point that manages firewall rules and WireGuard peers
  • Client: The initiating host that generates and sends SPA packets

Key principle: “Need to know” access. The controller only reveals gateway information AFTER authentication. This prevents reconnaissance entirely.

Study: CSA SDP v2.0 Specification (free download from CSA website)

2. Single Packet Authorization (SPA)

SPA is the cryptographic handshake that replaces traditional authentication. A single UDP packet contains:

  • Client identity (username/certificate hash)
  • Timestamp (prevents replay outside time window)
  • Nonce (prevents replay within time window)
  • Requested resource (port/service)
  • HMAC signature (proves possession of shared secret)

Why UDP? It’s connectionless - the server can drop packets silently without any response, maintaining true invisibility. TCP would require a SYN-ACK, revealing the server’s presence.

Study: “Zero Trust Networks” Ch. 10, fwknop documentation

3. WireGuard or IPSec Tunnels

After SPA authorization, traffic flows through an encrypted tunnel. WireGuard is preferred for this project because:

  • Simple, auditable codebase (~4000 lines vs 100,000+ for IPSec)
  • Modern cryptography (Curve25519, ChaCha20, Poly1305)
  • Stateless design that meshes well with SDP’s dynamic peer management
  • Kernel-level performance with userspace simplicity
  • Built-in roaming support for mobile clients

The SDP controller dynamically adds/removes WireGuard peers based on SPA authorization state.

Study: WireGuard whitepaper, wgctrl Go library documentation

4. Controller/Gateway/Client Architecture

Understanding the data flow between components is critical:

Client                    Controller                 Gateway
   |                          |                         |
   |---[SPA Packet]---------->|                         |
   |                          |--[Verify HMAC]          |
   |                          |--[Check Nonce]          |
   |                          |--[Verify Timestamp]     |
   |                          |                         |
   |                          |---[Add Firewall Rule]-->|
   |                          |---[Add WG Peer]-------->|
   |                          |                         |
   |---[WireGuard Handshake]-------------------------- >|
   |<--[Encrypted Tunnel]------------------------------>|
   |                          |                         |
   |                          |---[Timeout: Remove]---->|

In this project, Controller and Gateway are combined into a single daemon for simplicity. Production deployments often separate them.

5. Network Invisibility - Port Knocking Evolution

Port knocking was the precursor to SPA, but had critical flaws:

Port Knocking SPA
Sequence of SYN packets Single UDP packet
No cryptographic auth HMAC-SHA256 signature
Can be sniffed and replayed Timestamp + nonce prevents replay
Stateful (server tracks partial sequences) Stateless (single packet contains all info)
Subject to sequence flooding DoS No state to exhaust

SPA is the “port knocking done right” - cryptographically secure, replay-resistant, and DoS-resilient.

Study: “Port Knocking: Theory and Practice” paper, fwknop vs knockd comparison

6. Raw Socket Programming and BPF Filters

To capture SPA packets without opening a port, you need to understand:

  • Raw sockets: Bypass the kernel’s TCP/UDP stack, receive raw IP packets
  • BPF (Berkeley Packet Filter): Kernel-level filtering to select only packets you care about
  • libpcap/gopacket: User-space libraries that abstract raw socket complexity

The key insight: you can capture a copy of packets destined for a “closed” port. The kernel drops them at the firewall, but your raw socket sees them first. This is how you remain invisible while still receiving SPA packets.

Study: gopacket documentation, tcpdump BPF filter syntax


Questions to Guide Your Design

Before writing code, work through these design questions:

SPA Implementation Questions

  1. Packet Format: What fields go into your SPA packet? How do you handle variable-length usernames? What byte order do you use (hint: network byte order)?

  2. Key Distribution: How do clients obtain their shared secrets? Do you pre-provision them, use a registration endpoint, or integrate with an identity provider?

  3. Clock Synchronization: What happens if client and server clocks are off by 5 minutes? How much skew do you allow before rejecting packets? What if an attacker intentionally delays packets?

  4. Nonce Storage: How do you track used nonces? A simple map will grow unbounded. What data structure allows O(1) lookup with bounded memory? (Hint: bloom filter or time-windowed map)

  5. UDP Unreliability: What if the SPA packet is lost? Should the client retry? How do you prevent a retry from looking like a replay attack?

Tunnel Establishment Questions

  1. Firewall Rule Insertion: Where in the iptables chain do you insert rules? What happens if you insert AFTER the default DROP rule?

  2. Rule Identification: How do you find and remove a specific rule later? iptables doesn’t provide rule IDs - what’s your strategy?

  3. WireGuard Peer Lifecycle: When do you add a peer? When do you remove them? What if they disconnect and reconnect before the timeout?

  4. Graceful Degradation: What if WireGuard isn’t available? Can you fall back to direct iptables-only access for legacy clients?

  5. Crash Recovery: If the controller crashes, stale firewall rules persist. How do you clean them up on restart?


Thinking Exercise

Before writing any code, trace through the complete SDP connection flow on paper. This exercise will reveal edge cases and design decisions you’ll face during implementation.

Exercise: Trace the SDP Connection Flow

Scenario: Alice (IP: 203.0.113.50) wants to access HTTPS on server 198.51.100.10.

Your Task: Draw or write out each step, answering the questions along the way.

STEP 1: Alice's SDP Client generates SPA packet
- What fields are included?
- How is the HMAC computed?
- What's in the nonce and why?

STEP 2: Packet travels over UDP to server port 62201
- Why UDP instead of TCP?
- What does an attacker see if they capture this packet?
- Why port 62201? (Hint: check fwknop default)

STEP 3: Server's raw socket captures the packet
- Why raw socket instead of bind()?
- What BPF filter would you use?
- Is the packet also processed by the kernel's UDP stack?

STEP 4: SDP Controller verifies the SPA
- In what ORDER should you check timestamp vs HMAC?
  (Hint: think about timing attacks)
- What happens if verification fails? (Hint: silent drop)
- How do you log failures without revealing information?

STEP 5: Controller inserts iptables rule
- What's the exact iptables command?
- Where in the rule chain?
- How do you tag it for later removal?

STEP 6: Controller adds WireGuard peer (optional)
- What API/library do you use?
- What AllowedIPs do you assign?
- How does the client know the server's WireGuard public key?

STEP 7: Alice's client establishes WireGuard tunnel
- Does Alice need to know the server opened a hole?
- How does WireGuard handshake work?
- What happens if Alice tries to connect before the rule is added?

STEP 8: Alice accesses HTTPS through the tunnel
- Is traffic now encrypted twice (WireGuard + TLS)?
- What IP addresses appear in server logs?

STEP 9: Timeout expires, access revoked
- How does the controller track expiration?
- What cleanup happens?
- Can Alice immediately re-authorize?

Complete this exercise before proceeding. You should be able to answer every question confidently.


Hints in Layers

Use these hints only when stuck. Each layer reveals more detail - try to solve problems yourself first.

Layer 1: Getting Started

Hint 1.1 - Project Structure: Start with three main packages: spa (packet format and signing), capture (raw socket handling), and firewall (iptables management). This separation allows you to test each component independently.

Hint 1.2 - Development Environment: You’ll need Linux with root access (raw sockets require CAP_NET_RAW). Use a VM or container. Set up iptables with a default DROP policy from the start - this proves your SPA is working.

Hint 1.3 - Test Incrementally: Before building the full system, verify each piece:

  1. Can you send/receive a raw UDP packet?
  2. Can you add/remove iptables rules programmatically?
  3. Can you compute matching HMACs on client and server?

Layer 2: Packet Capture Challenges

Hint 2.1 - gopacket Setup:

handle, err := pcap.OpenLive("eth0", 1600, false, pcap.BlockForever)
filter := "udp and dst port 62201"
handle.SetBPFFilter(filter)

The 1600 is the snapshot length (enough for your SPA packet). false disables promiscuous mode.

Hint 2.2 - Extracting UDP Payload: You need to parse through layers: Ethernet -> IP -> UDP -> Payload. The gopacket library provides packet.Layer(layers.LayerTypeUDP) to navigate directly to UDP.

Hint 2.3 - Why You See Nothing: If packets aren’t captured, check: (1) correct interface name, (2) BPF filter syntax, (3) your iptables isn’t dropping packets BEFORE pcap sees them (pcap hooks early in the chain).

Layer 3: Verification Edge Cases

Hint 3.1 - HMAC Timing Attack Prevention: Use hmac.Equal() instead of bytes.Equal() for HMAC comparison. The former is constant-time and prevents timing attacks.

Hint 3.2 - Timestamp Ordering: Check timestamp BEFORE HMAC. Why? Timestamp check is cheap. If you compute HMAC first, an attacker can measure response time to detect valid usernames.

Hint 3.3 - Nonce Cache with Auto-Expiry: Use sync.Map with a cleanup goroutine:

// Add with timestamp
nonceCache.Store(nonce, time.Now())

// Cleanup goroutine (runs every minute)
nonceCache.Range(func(k, v interface{}) bool {
    if time.Since(v.(time.Time)) > 5*time.Minute {
        nonceCache.Delete(k)
    }
    return true
})

Layer 4: Firewall Integration

Hint 4.1 - Finding INSERT Position: Before inserting, count existing rules. Your ACCEPT must come before the final DROP:

// Get current rule count
cmd := exec.Command("iptables", "-L", "INPUT", "--line-numbers")
// Parse output, find DROP rule position
// Insert at position = DROP_position

Hint 4.2 - Rule Tagging with Comments: iptables supports comments for identification:

iptables -A INPUT -s 1.2.3.4 -p tcp --dport 443 \
  -m comment --comment "sdp-abc123" -j ACCEPT

Later, find and remove by grepping for your comment prefix.

Hint 4.3 - Cleanup on Startup: Scan all rules for your prefix and remove them:

iptables-save | grep "sdp-" | while read rule; do
  # Convert -A to -D and execute
done

Layer 5: Putting It All Together

Hint 5.1 - Concurrency Model: Run three goroutines: (1) packet capture loop, (2) authorization processor, (3) expiration tracker. Use channels to communicate between them.

Hint 5.2 - Graceful Shutdown: Use context.Context with cancellation. On SIGTERM: stop accepting new SPA, wait for pending authorizations to complete, clean up all rules, then exit.

Hint 5.3 - Testing Without Full Setup: Create a “dry-run” mode that logs what iptables commands WOULD execute. This lets you test logic without root access.


5. Solution Architecture

5.1 High-Level Design

+===========================================================================+
|                    SDP CONTROLLER ARCHITECTURE                              |
+===========================================================================+
|                                                                            |
|   +-----------------------------------------------------------------+      |
|   |                      SDP Controller                              |      |
|   +-----------------------------------------------------------------+      |
|   |                                                                   |      |
|   |   +-------------------+    +-------------------+                 |      |
|   |   |   Packet Capture  |    |   Config Manager  |                 |      |
|   |   |   (gopacket/pcap) |--->|   (user secrets,  |                 |      |
|   |   +-------------------+    |    policies)      |                 |      |
|   |           |                +-------------------+                 |      |
|   |           v                         |                            |      |
|   |   +-------------------+             |                            |      |
|   |   |   SPA Parser      |             v                            |      |
|   |   |   (decode packet) |    +-------------------+                 |      |
|   |   +-------------------+    |   Authorization   |                 |      |
|   |           |                |   Engine          |                 |      |
|   |           v                +-------------------+                 |      |
|   |   +-------------------+             |                            |      |
|   |   |   SPA Verifier    |<------------+                            |      |
|   |   |   - HMAC check    |                                          |      |
|   |   |   - Timestamp     |                                          |      |
|   |   |   - Nonce replay  |                                          |      |
|   |   +-------------------+                                          |      |
|   |           |                                                      |      |
|   |           v (if valid)                                           |      |
|   |   +-------------------+    +-------------------+                 |      |
|   |   |  Firewall Manager |    |  WireGuard Manager|                 |      |
|   |   |  (iptables/nftab) |    |  (wgctrl/netlink) |                 |      |
|   |   +-------------------+    +-------------------+                 |      |
|   |           |                         |                            |      |
|   |           v                         v                            |      |
|   |   +-------------------+    +-------------------+                 |      |
|   |   |   Rule Tracker    |    |   Peer Tracker    |                 |      |
|   |   |   (expiration)    |    |   (expiration)    |                 |      |
|   |   +-------------------+    +-------------------+                 |      |
|   |                                                                   |      |
|   +-----------------------------------------------------------------+      |
|                                                                            |
|           |                             |                                  |
|           v                             v                                  |
|   +-----------------+           +-----------------+                        |
|   |    iptables     |           |    WireGuard    |                        |
|   |    (kernel)     |           |    Interface    |                        |
|   +-----------------+           +-----------------+                        |
|                                                                            |
+===========================================================================+

5.2 Key Data Structures

// SPA Packet structure matching CSA SDP v2.0
type SPAPacket struct {
    RandomData    [16]byte   // Entropy to obscure patterns
    Digest        [16]byte   // SHA-256 truncated (for logging/debugging)
    Username      [32]byte   // Client identifier (null-padded)
    Timestamp     uint32     // Unix epoch seconds
    Version       uint32     // Protocol version
    MessageType   uint16     // 0x01 = Access Request
    RequestedPort uint16     // Port to access
    ClientIP      [4]byte    // Claimed source IP (IPv4)
    Nonce         uint32     // Anti-replay nonce
    HMAC          [32]byte   // HMAC-SHA256 signature
}

// Client configuration (loaded from file or database)
type ClientConfig struct {
    Username    string
    SharedKey   []byte      // Pre-shared secret for HMAC
    AllowedPorts []uint16   // Ports this client can request
    PublicKey   *wgtypes.Key // WireGuard public key (optional)
}

// Active authorization (tracked for expiration)
type Authorization struct {
    ID          string      // Unique ID for this authorization
    Username    string      // Who was authorized
    SourceIP    net.IP      // Authorized source IP
    Port        uint16      // Authorized port
    CreatedAt   time.Time   // When authorized
    ExpiresAt   time.Time   // When to revoke
    RuleHandle  string      // iptables rule identifier
    PeerKey     *wgtypes.Key // WireGuard peer (if applicable)
}

// Replay cache entry
type NonceEntry struct {
    Nonce     uint32
    Timestamp time.Time
}

// Controller state
type SDPController struct {
    config        *Config
    clients       map[string]*ClientConfig
    authorizations sync.Map  // map[string]*Authorization
    nonceCache    *sync.Map  // map[uint32]NonceEntry
    packetSource  *pcap.Handle
    firewallMgr   *FirewallManager
    wireguardMgr  *WireGuardManager
    logger        *slog.Logger
}

5.3 Algorithm Overview

Main Processing Loop:

1. CAPTURE: Raw socket receives UDP packet on port 62201
2. PARSE: Extract SPA fields from packet bytes
3. VERIFY:
   a. Check signature (HMAC-SHA256 with client's shared secret)
   b. Verify timestamp within acceptable window (default: +/- 30 seconds)
   c. Verify nonce not in replay cache
   d. Verify source IP matches claimed IP in packet
   e. Verify client is authorized for requested port
4. AUTHORIZE (if all checks pass):
   a. Insert iptables rule for source IP + port
   b. Optionally add WireGuard peer
   c. Track authorization with expiration time
5. CLEANUP: Background goroutine removes expired authorizations

6. Phased Implementation Guide

Phase 1: SPA Packet Format and Generation (Days 1-3)

Goal: Create the SPA client that generates properly formatted, signed packets.

Deliverable: sdp-client CLI tool that generates and sends SPA packets.

Steps:

  1. Define the SPA packet structure in Go using encoding/binary
  2. Implement packet serialization with proper byte ordering (big-endian/network order)
  3. Implement HMAC-SHA256 signing
  4. Create UDP sender that transmits to server

Implementation Notes:

// Serialize SPA packet to bytes
func (p *SPAPacket) Marshal() ([]byte, error) {
    buf := new(bytes.Buffer)

    // All multi-byte fields use network byte order (big-endian)
    buf.Write(p.RandomData[:])
    buf.Write(p.Digest[:])
    buf.Write(p.Username[:])
    binary.Write(buf, binary.BigEndian, p.Timestamp)
    binary.Write(buf, binary.BigEndian, p.Version)
    binary.Write(buf, binary.BigEndian, p.MessageType)
    binary.Write(buf, binary.BigEndian, p.RequestedPort)
    buf.Write(p.ClientIP[:])
    binary.Write(buf, binary.BigEndian, p.Nonce)
    buf.Write(p.HMAC[:])

    return buf.Bytes(), nil
}

// Sign packet with HMAC-SHA256
func (p *SPAPacket) Sign(key []byte) {
    // HMAC covers everything except the HMAC field itself
    data := p.marshalWithoutHMAC()
    mac := hmac.New(sha256.New, key)
    mac.Write(data)
    copy(p.HMAC[:], mac.Sum(nil))
}

Verification:

# Generate and send SPA packet
./sdp-client --server 127.0.0.1 --port 443 --key /tmp/test.key --verbose

# Capture with tcpdump to verify packet format
sudo tcpdump -i lo udp port 62201 -X

Phase 2: Raw Packet Capture with gopacket/libpcap (Days 4-7)

Goal: Capture SPA packets without opening a listening port.

Deliverable: Packet capture module that receives and decodes SPA packets.

Steps:

  1. Set up gopacket with libpcap
  2. Create BPF filter for UDP port 62201
  3. Parse captured packets to extract SPA payload
  4. Verify packet structure and extract fields

Implementation Notes:

import (
    "github.com/google/gopacket"
    "github.com/google/gopacket/pcap"
)

func NewPacketCapture(iface string) (*PacketCapture, error) {
    // Open device with promiscuous mode off (we only want our traffic)
    handle, err := pcap.OpenLive(iface, 1600, false, pcap.BlockForever)
    if err != nil {
        return nil, err
    }

    // Set BPF filter: only UDP packets to our SPA port
    if err := handle.SetBPFFilter("udp and dst port 62201"); err != nil {
        return nil, err
    }

    return &PacketCapture{handle: handle}, nil
}

func (pc *PacketCapture) Start(ctx context.Context, handler func(pkt SPAPacket, srcIP net.IP)) {
    packetSource := gopacket.NewPacketSource(pc.handle, pc.handle.LinkType())

    for packet := range packetSource.Packets() {
        select {
        case <-ctx.Done():
            return
        default:
        }

        // Extract UDP payload
        if udpLayer := packet.Layer(layers.LayerTypeUDP); udpLayer != nil {
            udp, _ := udpLayer.(*layers.UDP)
            payload := udp.Payload

            // Parse SPA packet
            spa, err := ParseSPAPacket(payload)
            if err != nil {
                log.Printf("Invalid SPA packet: %v", err)
                continue
            }

            // Get source IP from IP layer
            if ipLayer := packet.Layer(layers.LayerTypeIPv4); ipLayer != nil {
                ip, _ := ipLayer.(*layers.IPv4)
                handler(spa, ip.SrcIP)
            }
        }
    }
}

Verification:

# Start capture (in separate terminal)
sudo ./sdp-controller --capture-only --interface eth0

# Send test packet
./sdp-client --server <your-ip> --port 443 --key /tmp/test.key

# Controller should log the received packet

Phase 3: SPA Verification (HMAC, Timestamps) (Days 8-12)

Goal: Implement complete SPA verification with anti-replay protection.

Deliverable: Verification module that accepts/rejects SPA packets with logging.

Steps:

  1. Implement timestamp verification with configurable window
  2. Implement nonce tracking with sliding window
  3. Implement HMAC verification using client’s shared secret
  4. Implement source IP verification
  5. Add comprehensive logging for debugging

Implementation Notes:

type SPAVerifier struct {
    clients     map[string]*ClientConfig
    nonceCache  *NonceCache
    maxClockSkew time.Duration
    logger      *slog.Logger
}

func (v *SPAVerifier) Verify(spa *SPAPacket, sourceIP net.IP) (*ClientConfig, error) {
    username := strings.TrimRight(string(spa.Username[:]), "\x00")

    // 1. Look up client configuration
    client, ok := v.clients[username]
    if !ok {
        return nil, fmt.Errorf("unknown user: %s", username)
    }

    // 2. Verify timestamp
    packetTime := time.Unix(int64(spa.Timestamp), 0)
    drift := time.Since(packetTime).Abs()
    if drift > v.maxClockSkew {
        return nil, fmt.Errorf("timestamp outside window: drift=%v", drift)
    }

    // 3. Check nonce for replay
    if v.nonceCache.Contains(spa.Nonce) {
        return nil, fmt.Errorf("nonce replay detected: %x", spa.Nonce)
    }

    // 4. Verify source IP matches claimed IP
    claimedIP := net.IP(spa.ClientIP[:])
    if !sourceIP.Equal(claimedIP) {
        return nil, fmt.Errorf("source IP mismatch: claimed=%v, actual=%v", claimedIP, sourceIP)
    }

    // 5. Verify HMAC signature
    expectedHMAC := spa.ComputeHMAC(client.SharedKey)
    if !hmac.Equal(spa.HMAC[:], expectedHMAC) {
        return nil, fmt.Errorf("HMAC verification failed")
    }

    // 6. Check if client is allowed to access requested port
    if !slices.Contains(client.AllowedPorts, spa.RequestedPort) {
        return nil, fmt.Errorf("port %d not authorized for user %s", spa.RequestedPort, username)
    }

    // All checks passed - record nonce
    v.nonceCache.Add(spa.Nonce, packetTime)

    return client, nil
}

Verification:

# Test valid packet
./sdp-client --server <server> --port 443 --key /tmp/alice.key
# Expected: Authorization successful

# Test replay (send same packet twice quickly)
./sdp-client --server <server> --port 443 --key /tmp/alice.key --nonce 12345
./sdp-client --server <server> --port 443 --key /tmp/alice.key --nonce 12345
# Expected: Second one rejected with "nonce replay detected"

# Test wrong key
./sdp-client --server <server> --port 443 --key /tmp/wrong.key
# Expected: "HMAC verification failed"

Phase 4: Dynamic iptables Rule Insertion (Days 13-17)

Goal: Insert and remove firewall rules based on SPA authorization.

Deliverable: Firewall manager that safely manipulates iptables.

Steps:

  1. Implement rule insertion with proper positioning
  2. Implement rule deletion by specification
  3. Add rule tagging with unique identifiers
  4. Implement startup cleanup (remove stale rules)
  5. Handle errors gracefully (iptables command failures)

Implementation Notes:

import "os/exec"

type FirewallManager struct {
    chain     string  // "INPUT"
    position  int     // Where to insert rules (before DROP)
    rulePrefix string // "sdp-" for identification
}

func (fm *FirewallManager) AllowAccess(auth *Authorization) error {
    // Build iptables command
    comment := fmt.Sprintf("%s%s-exp-%d", fm.rulePrefix, auth.ID, auth.ExpiresAt.Unix())

    args := []string{
        "-I", fm.chain, strconv.Itoa(fm.position),
        "-s", auth.SourceIP.String(),
        "-p", "tcp",
        "--dport", strconv.Itoa(int(auth.Port)),
        "-m", "comment", "--comment", comment,
        "-j", "ACCEPT",
    }

    cmd := exec.Command("iptables", args...)
    output, err := cmd.CombinedOutput()
    if err != nil {
        return fmt.Errorf("iptables insert failed: %v: %s", err, output)
    }

    auth.RuleHandle = comment
    return nil
}

func (fm *FirewallManager) RevokeAccess(auth *Authorization) error {
    // Delete rule by full specification (safer than by line number)
    args := []string{
        "-D", fm.chain,
        "-s", auth.SourceIP.String(),
        "-p", "tcp",
        "--dport", strconv.Itoa(int(auth.Port)),
        "-m", "comment", "--comment", auth.RuleHandle,
        "-j", "ACCEPT",
    }

    cmd := exec.Command("iptables", args...)
    output, err := cmd.CombinedOutput()
    if err != nil {
        return fmt.Errorf("iptables delete failed: %v: %s", err, output)
    }

    return nil
}

func (fm *FirewallManager) CleanupStaleRules() error {
    // Remove all rules with our prefix (from previous runs)
    cmd := exec.Command("sh", "-c",
        fmt.Sprintf(`iptables-save | grep -v "%s" | iptables-restore`, fm.rulePrefix))
    return cmd.Run()
}

Verification:

# Initial state
sudo iptables -L INPUT -n --line-numbers

# Send SPA
./sdp-client --server localhost --port 443 --key /tmp/alice.key

# Check rule was added
sudo iptables -L INPUT -n --line-numbers
# Should see new ACCEPT rule for your IP

# Wait for expiration or manually test removal
sudo ./sdp-controller --revoke-ip 203.0.113.50

# Verify rule was removed
sudo iptables -L INPUT -n --line-numbers

Phase 5: WireGuard Peer Management (Days 18-22)

Goal: Dynamically add and remove WireGuard peers.

Deliverable: WireGuard manager using wgctrl library.

Steps:

  1. Set up wgctrl client
  2. Implement peer addition with allowed IPs
  3. Implement peer removal
  4. Handle keypair enrollment process

Implementation Notes:

import (
    "golang.zx2c4.com/wireguard/wgctrl"
    "golang.zx2c4.com/wireguard/wgctrl/wgtypes"
)

type WireGuardManager struct {
    client    *wgctrl.Client
    device    string  // "wg0"
    subnet    net.IPNet
    nextIP    net.IP
    peerMap   map[wgtypes.Key]net.IP
}

func NewWireGuardManager(device string) (*WireGuardManager, error) {
    client, err := wgctrl.New()
    if err != nil {
        return nil, err
    }

    return &WireGuardManager{
        client:  client,
        device:  device,
        subnet:  net.IPNet{IP: net.ParseIP("10.200.0.0"), Mask: net.CIDRMask(24, 32)},
        nextIP:  net.ParseIP("10.200.0.2"),
        peerMap: make(map[wgtypes.Key]net.IP),
    }, nil
}

func (wm *WireGuardManager) AddPeer(pubKey wgtypes.Key, endpoint net.UDPAddr) (net.IP, error) {
    // Allocate IP for this peer
    peerIP := wm.allocateIP()

    peerConfig := wgtypes.PeerConfig{
        PublicKey: pubKey,
        Endpoint:  &endpoint,
        AllowedIPs: []net.IPNet{
            {IP: peerIP, Mask: net.CIDRMask(32, 32)},
        },
        ReplaceAllowedIPs: true,
    }

    err := wm.client.ConfigureDevice(wm.device, wgtypes.Config{
        Peers: []wgtypes.PeerConfig{peerConfig},
    })

    if err != nil {
        return nil, err
    }

    wm.peerMap[pubKey] = peerIP
    return peerIP, nil
}

func (wm *WireGuardManager) RemovePeer(pubKey wgtypes.Key) error {
    peerConfig := wgtypes.PeerConfig{
        PublicKey: pubKey,
        Remove:    true,
    }

    err := wm.client.ConfigureDevice(wm.device, wgtypes.Config{
        Peers: []wgtypes.PeerConfig{peerConfig},
    })

    delete(wm.peerMap, pubKey)
    return err
}

Verification:

# Check initial WireGuard state
sudo wg show

# Send SPA with WireGuard public key
./sdp-client --server <server> --port 443 --key /tmp/alice.key --wg-pubkey /tmp/alice.wg.pub

# Check peer was added
sudo wg show
# Should see new peer with allowed IPs

# Test WireGuard connection
ping 10.200.0.1  # Should work if your interface is configured

Phase 6: Automatic Rule Expiration (Days 23-28)

Goal: Implement background cleanup of expired authorizations.

Deliverable: Complete SDP controller with automatic expiration.

Steps:

  1. Implement expiration tracker goroutine
  2. Handle graceful shutdown
  3. Implement configuration reloading
  4. Add metrics and monitoring endpoints

Implementation Notes:

func (c *SDPController) startExpirationTracker(ctx context.Context) {
    ticker := time.NewTicker(10 * time.Second)
    defer ticker.Stop()

    for {
        select {
        case <-ctx.Done():
            // Graceful shutdown - remove all rules
            c.cleanupAllRules()
            return

        case <-ticker.C:
            now := time.Now()

            c.authorizations.Range(func(key, value interface{}) bool {
                auth := value.(*Authorization)

                if now.After(auth.ExpiresAt) {
                    c.logger.Info("authorization expired",
                        "user", auth.Username,
                        "source_ip", auth.SourceIP,
                        "port", auth.Port,
                    )

                    // Revoke firewall rule
                    if err := c.firewallMgr.RevokeAccess(auth); err != nil {
                        c.logger.Error("failed to revoke firewall rule", "error", err)
                    }

                    // Revoke WireGuard peer
                    if auth.PeerKey != nil {
                        if err := c.wireguardMgr.RemovePeer(*auth.PeerKey); err != nil {
                            c.logger.Error("failed to remove WireGuard peer", "error", err)
                        }
                    }

                    c.authorizations.Delete(key)
                }

                return true
            })
        }
    }
}

Verification:

# Send SPA with short timeout
./sdp-client --server <server> --port 443 --key /tmp/alice.key

# Verify access is granted
nmap -sS -Pn -p 443 <server>  # Should show "open"

# Wait for expiration (default 5 minutes, or configure shorter for testing)
sleep 300

# Verify access is revoked
nmap -sS -Pn -p 443 <server>  # Should show "filtered"

7. Testing Strategy

Unit Tests

Component Test Cases
SPA Packet Serialization/deserialization roundtrip; Invalid packet lengths; Wrong byte order
HMAC Verification Correct signature; Wrong key; Tampered packet
Timestamp Validation Within window; Outside window; Future timestamp
Nonce Cache First use accepted; Replay rejected; Expiration cleanup

Integration Tests

Scenario Expected Outcome
Valid SPA from authorized client Access granted, rule inserted
SPA with wrong signature Rejected, no rule change
Replay attack Rejected on second attempt
Access expiration Rule automatically removed
Controller restart Stale rules cleaned up

Security Tests

# Test 1: Port scan before SPA
nmap -sS -Pn -p 1-65535 <server>
# Expected: All ports filtered or closed

# Test 2: Replay attack
tcpdump -i eth0 -w /tmp/spa.pcap udp port 62201
./sdp-client --server <server> --port 443 --key /tmp/alice.key
tcpreplay -i eth0 /tmp/spa.pcap
# Expected: Replay rejected

# Test 3: Spoofed source IP
# (Requires network that allows IP spoofing - not typical)
# Expected: Source IP mismatch error

# Test 4: Timing attack
for i in {1..100}; do
    time ./sdp-client --server <server> --port 443 --key /tmp/wrong.key
done
# Expected: Constant-time rejection (no timing side channel)

8. Common Pitfalls & Debugging

Pitfall 1: CAP_NET_RAW Required

Symptom: “permission denied” when opening raw socket

Cause: Raw sockets require elevated privileges

Solution:

# Option 1: Run as root
sudo ./sdp-controller

# Option 2: Grant capability to binary
sudo setcap cap_net_raw+ep ./sdp-controller

Pitfall 2: BPF Filter Syntax

Symptom: No packets captured even when sending SPA

Cause: Incorrect BPF filter syntax

Solution:

// Test filter with tcpdump first
// $ sudo tcpdump -i eth0 'udp and dst port 62201'

// Common mistakes:
// Wrong: "udp dst port 62201"
// Right: "udp and dst port 62201"

Pitfall 3: iptables Rule Ordering

Symptom: Rules inserted but traffic still blocked

Cause: Rule inserted after the DROP rule

Solution:

# Check rule order
iptables -L INPUT --line-numbers

# Insert at specific position (before DROP)
iptables -I INPUT 3 -s 1.2.3.4 -p tcp --dport 443 -j ACCEPT

Pitfall 4: Clock Synchronization

Symptom: Valid packets rejected with “timestamp outside window”

Cause: Clock drift between client and server

Solution:

# Ensure NTP is running on both systems
timedatectl status
systemctl status chronyd  # or ntpd

# Check clock difference
date -u  # Compare on both systems

Pitfall 5: WireGuard Interface Not Up

Symptom: wgctrl operations fail

Cause: WireGuard interface doesn’t exist

Solution:

# Create interface first
ip link add dev wg0 type wireguard
ip address add dev wg0 10.200.0.1/24
ip link set up dev wg0

# Generate server keys
wg genkey | tee /etc/wireguard/privatekey | wg pubkey > /etc/wireguard/publickey
wg set wg0 private-key /etc/wireguard/privatekey

Pitfall 6: Nonce Cache Memory Growth

Symptom: Controller memory usage grows unbounded

Cause: Not expiring old nonces

Solution:

// Implement sliding window with cleanup
func (nc *NonceCache) cleanup() {
    cutoff := time.Now().Add(-nc.windowDuration)
    nc.entries.Range(func(key, value interface{}) bool {
        entry := value.(NonceEntry)
        if entry.Timestamp.Before(cutoff) {
            nc.entries.Delete(key)
        }
        return true
    })
}

9. Extensions & Challenges

Extension 1: Multi-Factor SPA

Extend SPA to require additional authentication factors:

+---------------------------------------------------------------+
| Standard SPA: Shared secret only                               |
| MFA SPA: Shared secret + TOTP code embedded in packet          |
+---------------------------------------------------------------+

Extension 2: SPA over ICMP

Hide SPA packets inside ICMP echo requests (ping):

  • Use payload of ICMP packet for SPA data
  • Even more covert than UDP (looks like normal ping)
  • Requires more complex capture filter

Extension 3: Mutual SPA

Both client and server exchange SPA packets:

  • Server proves identity to client before client connects
  • Prevents man-in-the-middle attacks
  • Requires two-way authorization

Extension 4: Integration with Identity Provider

Replace static shared secrets with OIDC/SAML authentication:

  • Client authenticates to IdP first
  • IdP provides signed token for SPA
  • Controller validates token with IdP

Extension 5: Kubernetes-Native SDP

Implement as Kubernetes operator:

  • CRD for SDP policies
  • Automatic pod-level firewall rules
  • Integration with service mesh

10. Books That Will Help

Topic Book Chapter/Section
Zero Trust fundamentals “Zero Trust Networks” by Gilman & Barth Ch. 10: “Constructing a Secure Access Plane”
SDP specification CSA SDP v2.0 Specification Entire document (free download)
Raw socket programming “The Linux Programming Interface” by Kerrisk Ch. 58-59: “Sockets”
Network packet structure “TCP/IP Illustrated, Vol. 1” by Stevens Ch. 3: “IP”, Ch. 11: “UDP”
Linux packet filtering “Linux iptables Pocket Reference” by Purdy Entire book
Cryptographic protocols “Serious Cryptography, 2nd Ed” by Aumasson Ch. 6: “Message Authentication”
WireGuard internals WireGuard whitepaper by Donenfeld Entire paper (free)
Security architecture “Security Engineering, 3rd Ed” by Anderson Ch. 21: “Network Security”
Netfilter/iptables internals “Linux Kernel Networking” by Rosen Ch. 9: “Netfilter”
BPF and packet capture “BPF Performance Tools” by Gregg Ch. 10: “Networking”

11. Interview Questions

After completing this project, you’ll be prepared to answer:

Architecture Questions

  1. “Explain the difference between SPA and port knocking.”
    • Expected: Stateless vs stateful, cryptographic authentication, replay protection
    • Bonus: Reference CSA SDP specification, explain why SPA is superior
  2. “How does SDP implement ‘deny all, permit by exception’?”
    • Expected: Default DROP rule, dynamic insertion after authentication
    • Bonus: Explain why this reduces attack surface even against 0-days
  3. “What happens if an attacker captures and replays an SPA packet?”
    • Expected: Explain timestamp, nonce, and source IP verification
    • Bonus: Describe sliding window implementation, memory efficiency

Implementation Questions

  1. “Why use raw sockets instead of a regular UDP socket for SPA capture?”
    • Expected: Port appears filtered, not open; passive capture
    • Bonus: Explain BPF filtering, kernel-level vs userspace
  2. “How do you safely modify iptables rules from a program?”
    • Expected: Rule ordering, tagging with comments, cleanup on startup
    • Bonus: Discuss nftables as modern alternative, atomic operations
  3. “How would you scale this to handle thousands of clients?”
    • Expected: Nonce cache efficiency (bloom filter), rule batching
    • Bonus: Distributed SDP controller architecture

Security Questions

  1. “What are the weaknesses of this SPA implementation?”
    • Expected: Shared secrets, clock sync requirements, UDP packet loss
    • Bonus: How to mitigate each, evolution to certificate-based auth
  2. “How would an attacker attempt to defeat this system?”
    • Expected: Replay attempts, timing attacks, key compromise
    • Bonus: Network-level attacks (IP spoofing requirements)

12. Self-Assessment Checklist

Before considering this project complete, verify:

Understanding

  • I can explain the “Dark Cloud” concept and why it reduces attack surface
  • I understand the difference between SPA and traditional port knocking
  • I can describe the SPA packet format from memory (major fields)
  • I can explain three anti-replay mechanisms and why each is necessary
  • I understand why raw sockets are required for passive SPA capture

Implementation

  • SPA client generates properly formatted, signed packets
  • Controller captures SPA packets without opening a listening port
  • HMAC verification correctly accepts valid and rejects invalid signatures
  • Replay attacks are detected and rejected
  • iptables rules are inserted in the correct position
  • Rules are automatically removed after timeout
  • Controller handles restart gracefully (cleanup stale rules)
  • WireGuard peers can be dynamically added/removed

Security

  • nmap scans show all ports as “filtered” before SPA
  • After valid SPA, only the authorized IP can access the port
  • Captured packets cannot be replayed successfully
  • Wrong key results in rejection without information leakage

Operations

  • Controller logs all authorization decisions
  • Errors are handled gracefully without crashes
  • Configuration can be reloaded without restart
  • Metrics are available for monitoring

13. Project Context

This is Project 7 in the Zero Trust Architecture Deep Dive series. It builds upon concepts from:

  • Project 1 (Identity-Aware Reverse Proxy): Identity as the new perimeter
  • Project 3 (Host-Level Micro-segmentation): Dynamic firewall rules
  • Project 4 (Mutual TLS Mesh): Cryptographic identity verification

And prepares you for:

  • Project 9 (ZTNA App Tunnel): Full application-level Zero Trust access
  • Project 10 (Capstone): Integrating all components into a complete ZT architecture

This guide was expanded from ZERO_TRUST_ARCHITECTURE_DEEP_DIVE.md. For the complete learning path, see the project index.