Project 3: Host-Level Micro-segmentation (The Data Plane)

Project 3: Host-Level Micro-segmentation (The Data Plane)

Core Zero Trust Principle: “Assume breach.” This project teaches you to implement micro-segmentation at the host level - the security layer that contains attackers AFTER they’ve compromised a service, preventing lateral movement to other processes on the same machine.


Project Overview

Attribute Value
Difficulty Level 4: Expert
Time Estimate 2-4 weeks (40-80 hours)
Main Language C (or Python/Bash for orchestration)
Alternative Languages Rust (with aya for eBPF)
Knowledge Area Systems Programming / Linux Networking
Key Technologies iptables, nftables, eBPF, Netfilter, conntrack
Main Book “The Linux Programming Interface” by Michael Kerrisk

What You’re Building: A tool that dynamically manages firewall rules on a Linux server to enforce isolation between local processes. Instead of saying “Block Port 80,” it says “Only allow process nginx (running as user web-user) to talk to process redis (running as user db-user) on the loopback interface.”

Why It Matters: Traditional firewalls protect the “perimeter” - they stop attackers from getting IN. But once an attacker compromises a single service, they have free rein to move “East-West” within the host or cluster. Micro-segmentation stops this lateral movement by applying “Default Deny” to ALL traffic, including localhost.


Learning Objectives

By completing this project, you will be able to:

  1. Explain the Linux Netfilter architecture - Understand the 5 hook points, table types, chain ordering, and how packets flow through the kernel’s filtering subsystem
  2. Implement process-aware firewall rules - Use the iptables owner module to create rules that match traffic by UID/GID rather than just IP/port
  3. Design and enforce a “Default Deny” policy - Build a security model where only explicitly whitelisted communication paths are allowed
  4. Perform zero-downtime rule updates - Modify firewall rules without dropping active, legitimate connections using atomic nftables operations
  5. Debug network isolation issues - Use /proc/net/tcp, ss, conntrack, and kernel logs to troubleshoot blocked connections
  6. Understand East-West vs North-South security - Articulate why internal traffic is often MORE dangerous than external traffic
  7. Evaluate iptables vs nftables vs eBPF - Know the tradeoffs between legacy and modern filtering approaches

Deep Theoretical Foundation

The Linux Networking Stack and Netfilter

Before you can control traffic, you must understand how packets flow through the Linux kernel. The Netfilter framework is the kernel subsystem that provides packet filtering, NAT, and mangling capabilities.

+------------------------------------------------------------------+
|           PACKET FLOW THROUGH THE LINUX KERNEL                    |
+------------------------------------------------------------------+

INCOMING PACKET (from network interface)
         |
         v
+------------------+
|    PREROUTING    |  <-- Hook 1: Packet just arrived, before routing
|  (raw, mangle,   |      decision. Used for DNAT (destination NAT)
|   nat tables)    |
+--------+---------+
         |
         v
+------------------+
|  ROUTING DECISION|  <-- Kernel decides: Is this for ME or to FORWARD?
+--------+---------+
         |
    +----+----+
    |         |
    v         v
 LOCAL?    FORWARD?
    |         |
    v         v
+-------+  +------------------+
| INPUT |  |     FORWARD      |  <-- Hook 3: Packet passing through
| Hook 2|  |  (filter, mangle |      (not for this host)
+-------+  |   security)      |
    |      +--------+---------+
    |               |
    v               v
+----------+  +------------------+
| LOCAL    |  |   POSTROUTING    |  <-- Hook 5: Packet leaving
| PROCESS  |  |  (mangle, nat,   |      (after routing for forwards)
+----+-----+  |   raw)           |
     |        +------------------+
     |               |
     v               v
+------------------+ |
|     OUTPUT       | |  <-- Hook 4: Packet generated locally
|  (raw, mangle,   | |      (from a local process)
|   nat, filter,   | |
|   security)      | |
+--------+---------+ |
         |           |
         v           |
+------------------+ |
|   POSTROUTING    |<+  <-- Hook 5: Final hook before wire
|  (mangle, nat)   |
+--------+---------+
         |
         v
     [Network Interface - OUT]

+------------------------------------------------------------------+
|                        THE 5 NETFILTER HOOKS                       |
+------------------------------------------------------------------+
| Hook          | When                          | Typical Use Case  |
|---------------|-------------------------------|-------------------|
| PREROUTING    | Just arrived, pre-routing     | DNAT, conntrack   |
| INPUT         | Destined for local process    | Filtering inbound |
| FORWARD       | Passing through (router)      | Firewall/gateway  |
| OUTPUT        | Generated by local process    | Filtering outbound|
| POSTROUTING   | About to leave (post-routing) | SNAT, masquerade  |
+------------------------------------------------------------------+

Key Insight for Micro-segmentation: For process-to-process communication on the SAME host (localhost), packets flow through:

  1. OUTPUT chain (from sending process)
  2. POSTROUTING (briefly)
  3. PREROUTING (packet re-enters for local destination)
  4. INPUT chain (to receiving process)

This means you can filter localhost traffic using both OUTPUT and INPUT chains.

The iptables Chain Architecture

iptables organizes rules into tables and chains. Understanding this hierarchy is essential:

+------------------------------------------------------------------+
|                    IPTABLES TABLES AND CHAINS                      |
+------------------------------------------------------------------+

TABLES (organized by purpose):
==============================

1. RAW TABLE (Connection Tracking Bypass)
   +-------------+-------------+
   | PREROUTING  |   OUTPUT    |
   +-------------+-------------+
   Purpose: Mark packets to bypass connection tracking (NOTRACK)
   Use case: High-performance exemptions

2. MANGLE TABLE (Packet Modification)
   +-------------+-------------+-------------+-------------+-------------+
   | PREROUTING  |   INPUT     |   FORWARD   |   OUTPUT    | POSTROUTING |
   +-------------+-------------+-------------+-------------+-------------+
   Purpose: Modify packet headers (TTL, TOS, MARK)
   Use case: QoS, policy routing

3. NAT TABLE (Network Address Translation)
   +-------------+-------------+-------------+
   | PREROUTING  |   OUTPUT    | POSTROUTING |
   +-------------+-------------+-------------+
   Purpose: SNAT, DNAT, MASQUERADE
   Use case: Internet sharing, port forwarding

4. FILTER TABLE (Packet Filtering) <-- YOUR PRIMARY FOCUS
   +-------------+-------------+-------------+
   |    INPUT    |   FORWARD   |   OUTPUT    |
   +-------------+-------------+-------------+
   Purpose: Accept, drop, or reject packets
   Use case: Firewalling, access control

5. SECURITY TABLE (SELinux/Secmark)
   +-------------+-------------+-------------+
   |    INPUT    |   FORWARD   |   OUTPUT    |
   +-------------+-------------+-------------+
   Purpose: SELinux security context marking
   Use case: Mandatory Access Control integration

+------------------------------------------------------------------+
|               TABLE PROCESSING ORDER AT EACH HOOK                  |
+------------------------------------------------------------------+

PREROUTING:  raw -> mangle -> nat (DNAT)
INPUT:       mangle -> filter -> security
FORWARD:     mangle -> filter -> security
OUTPUT:      raw -> mangle -> nat (DNAT) -> filter -> security
POSTROUTING: mangle -> nat (SNAT)

+------------------------------------------------------------------+
|                    RULE EVALUATION ORDER                           |
+------------------------------------------------------------------+

Within a chain, rules are evaluated TOP-TO-BOTTOM:

   Chain OUTPUT (policy DROP)
   +-------------------------------------------------+
   | 1. -m owner --uid-owner 1001 -p tcp --dport 6379 -j ACCEPT  |
   | 2. -m owner --uid-owner 1001 -p tcp --dport 5432 -j ACCEPT  |
   | 3. -m state --state ESTABLISHED,RELATED -j ACCEPT           |
   | 4. -j LOG --log-prefix "BLOCKED: "                          |
   | 5. -j DROP (implicit from policy)                           |
   +-------------------------------------------------+

   FIRST MATCH WINS. If packet matches rule 1, rules 2-5 are skipped.

   POLICY: If NO rule matches, the chain's default POLICY applies.
           For security: Set policy to DROP (Default Deny)

The Owner Module: Process-Aware Filtering

The owner match module is the key to micro-segmentation. It allows you to match packets based on the LOCAL process that created them.

+------------------------------------------------------------------+
|                    THE IPTABLES OWNER MODULE                       |
+------------------------------------------------------------------+

The owner module works ONLY in the OUTPUT chain (and POSTROUTING
for some options), because that's where the kernel still knows
which process generated the packet.

AVAILABLE MATCH OPTIONS:
========================

--uid-owner UID      Match by User ID (numeric or name)
--gid-owner GID      Match by Group ID (numeric or name)
--pid-owner PID      Match by Process ID (DEPRECATED, use cgroups)
--sid-owner SID      Match by Session ID (DEPRECATED)
--cmd-owner CMD      Match by command name (REMOVED in modern kernels)

EXAMPLE RULES:
==============

# Allow user 'web-user' (UID 1001) to connect to Redis on port 6379
iptables -A OUTPUT -m owner --uid-owner 1001 -p tcp --dport 6379 -j ACCEPT

# Allow the 'docker' group to access the Docker socket
iptables -A OUTPUT -m owner --gid-owner docker -d 172.17.0.0/16 -j ACCEPT

# Block ALL network access for user 'sandbox' (UID 1005)
iptables -A OUTPUT -m owner --uid-owner 1005 -j DROP

# Allow root (UID 0) to do anything (be careful with this!)
iptables -A OUTPUT -m owner --uid-owner 0 -j ACCEPT

+------------------------------------------------------------------+
|                    HOW OWNER MATCHING WORKS                        |
+------------------------------------------------------------------+

When a process creates a socket and sends data:

1. Process (PID 1234, UID 1001) calls connect() or sendto()
2. Kernel creates a socket buffer (skb) for the packet
3. Kernel attaches socket metadata to skb, including:
   - sk_uid: Owner's User ID
   - sk_gid: Owner's Group ID (primary group)
   - skb->sk: Pointer to socket structure
4. Packet enters OUTPUT chain
5. The -m owner module reads sk_uid from skb->sk
6. Match succeeds if sk_uid == rule's --uid-owner value

IMPORTANT LIMITATION:
=====================

The owner module only works for packets with a LOCAL socket:
  - Works: Packets sent by local processes (OUTPUT chain)
  - Fails: Forwarded packets (no local socket)
  - Fails: Incoming packets (INPUT chain) - you can't match
          the DESTINATION process, only the SOURCE

For INPUT chain process matching, you need:
  - eBPF socket filters (see eBPF section below)
  - cgroups v2 net_cls integration
  - SELinux/AppArmor with network labels

+------------------------------------------------------------------+
|              DETERMINING UID FROM /proc/net/tcp                    |
+------------------------------------------------------------------+

The /proc/net/tcp file shows all active TCP connections with their
owning UID:

$ cat /proc/net/tcp
  sl  local_address rem_address   st tx_queue rx_queue ... uid ...
   0: 0100007F:1F90 0100007F:E0C4 01 00000000:00000000 ... 1001 ...
   |      |     |        |     |  |                        |
   |      |     |        |     |  |                        +-- UID!
   |      |     |        |     |  +-- Connection state (01=ESTABLISHED)
   |      |     |        +-----+-- Remote address:port (hex)
   |      +-----+-- Local address:port (hex, little-endian)
   +-- Slot number

Decoding addresses:
  0100007F = 127.0.0.1 (01=127, 00=0, 00=0, 7F=1... wait, reversed!)
  Actually: 7F000001 in network order = 127.0.0.1
  1F90 hex = 8080 decimal

Use `ss -tulnp` for human-readable output with process names.

eBPF and Modern Filtering

eBPF (extended Berkeley Packet Filter) is the future of network filtering. It allows you to attach custom programs to kernel hooks with performance approaching native code.

+------------------------------------------------------------------+
|                    eBPF FOR MICRO-SEGMENTATION                     |
+------------------------------------------------------------------+

WHY eBPF?
=========

iptables Limitations:
  - Can't match by process name (binary path)
  - Can't see inside the packet payload
  - Rule evaluation is O(n) - slows down with many rules
  - Can't make complex decisions (if-then-else logic)

eBPF Advantages:
  - Full programmability (C-like language compiled to bytecode)
  - O(1) lookups using eBPF maps (hash tables)
  - Can inspect packet contents (deep packet inspection)
  - Can access process context (comm, cgroup, namespace)
  - Minimal performance overhead (JIT compiled)

eBPF PROGRAM TYPES FOR NETWORKING:
==================================

1. XDP (eXpress Data Path)
   - Runs at driver level, before skb allocation
   - Fastest possible filtering (millions of pps)
   - Use case: DDoS mitigation

2. TC (Traffic Control)
   - Runs at ingress/egress of network interface
   - Has access to full skb (more metadata than XDP)
   - Use case: Per-interface policy

3. Socket Filters (SO_ATTACH_BPF)
   - Attached to individual sockets
   - Can filter/observe per-socket traffic
   - Use case: Application-specific filtering

4. cgroup/sock_ops
   - Attached to cgroups
   - Controls socket operations (connect, bind, etc.)
   - Use case: Container networking policy

+------------------------------------------------------------------+
|              eBPF MICRO-SEGMENTATION ARCHITECTURE                  |
+------------------------------------------------------------------+

                      User Space
           +------------------------------+
           |   Policy Manager (Your Tool) |
           |  +------------------------+  |
           |  | YAML Config Parser     |  |
           |  | eBPF Map Loader        |  |
           |  | Audit Log Writer       |  |
           |  +------------------------+  |
           +-------------|----------------+
                         | libbpf / aya
    =====================|===========================================
                         v              Kernel Space
           +------------------------------+
           |         eBPF Maps            |
           |  +------------------------+  |
           |  | allowed_paths: hashmap |  |  Key: (src_uid, dst_port)
           |  | blocked_count: percpu  |  |  Value: ALLOW/DENY
           |  | audit_events: ringbuf  |  |
           |  +------------------------+  |
           +-------------|----------------+
                         |
           +-------------|----------------+
           |     eBPF Program (TC/cgroup) |
           |  +------------------------+  |
           |  | 1. Get current UID     |  |
           |  | 2. Get destination port|  |
           |  | 3. Lookup in map       |  |
           |  | 4. Return ACCEPT/DROP  |  |
           |  +------------------------+  |
           +------------------------------+

RUST + AYA EXAMPLE:
===================

// Kernel-side eBPF program (simplified)
#[classifier]
pub fn filter_egress(ctx: TcContext) -> i32 {
    let uid = bpf_get_current_uid_gid() as u32;
    let dport = unsafe { (*ctx.skb.data.tcp).dest };

    let key = (uid, dport);
    match ALLOWED_PATHS.get(&key) {
        Some(_) => TC_ACT_OK,     // Allow
        None => {
            BLOCKED_COUNT.add(1);  // Increment counter
            TC_ACT_SHOT           // Drop
        }
    }
}

+------------------------------------------------------------------+
|                iptables vs nftables vs eBPF                        |
+------------------------------------------------------------------+

|                | iptables     | nftables      | eBPF           |
|----------------|--------------|---------------|----------------|
| Kernel API     | Netfilter    | Netfilter     | BPF syscall    |
| Rule syntax    | CLI args     | Custom DSL    | C/Rust code    |
| Atomicity      | Per-rule     | Per-ruleset   | Per-map-update |
| Performance    | O(n) linear  | O(n) + sets   | O(1) hashmap   |
| Flexibility    | Low          | Medium        | Very High      |
| Learning curve | Low          | Medium        | High           |
| Process match  | UID/GID only | UID/GID/marks | Full context   |
| Container-aware| No (manual)  | Partial       | Yes (cgroups)  |

RECOMMENDATION:
===============

For this project:
  1. START with iptables (understand the concepts)
  2. MIGRATE to nftables (production readiness)
  3. EXPLORE eBPF (advanced extension)

iptables is deprecated but universally supported.
nftables is the production replacement.
eBPF is the future for complex policies.

East-West vs North-South Traffic

Understanding traffic patterns is crucial for security design:

+------------------------------------------------------------------+
|              EAST-WEST vs NORTH-SOUTH TRAFFIC                      |
+------------------------------------------------------------------+

NORTH-SOUTH TRAFFIC (Vertical)
==============================
Traffic entering or leaving the network boundary.

          Internet (North)
              |
              v
        +----------+
        | Firewall |  <-- Traditional security focus
        +----------+
              |
              v
        +----------+
        | DMZ/Edge |
        +----------+
              |
              v
        [Internal Network]
              |
              v
          Data Center (South)

Characteristics:
  - Crosses trust boundaries
  - Typically encrypted (TLS)
  - Well-monitored at edge
  - IDS/IPS inspection common

EAST-WEST TRAFFIC (Horizontal)
==============================
Traffic between services INSIDE the network or host.

        [Internal Network / Single Host]

        +--------+     +--------+     +--------+
        | App A  |<--->| App B  |<--->| App C  |
        +--------+     +--------+     +--------+
             ^              ^              ^
             |              |              |
             +------+-------+-------+------+
                    |               |
               +--------+     +--------+
               |  DB 1  |     |  DB 2  |
               +--------+     +--------+

Characteristics:
  - Often UNENCRYPTED (assumes trusted network)
  - Rarely monitored (blind spot!)
  - Same host = localhost, no firewall
  - Attackers LOVE this

+------------------------------------------------------------------+
|              WHY EAST-WEST IS MORE DANGEROUS                       |
+------------------------------------------------------------------+

SCENARIO: Attacker compromises a low-value web app.

WITHOUT MICRO-SEGMENTATION:
===========================

  [Attacker] ---(RCE exploit)---> [Web App]
                                      |
       "I'm inside! Let me look around..."
                                      |
                  +-------------------+-------------------+
                  |                   |                   |
                  v                   v                   v
             [Database]         [Payment API]      [Admin Panel]
                  |                   |                   |
             "SELECT *..."      "curl internal"     "reset password"
                  |                   |                   |
                  v                   v                   v
             DATA BREACH         FRAUD              FULL TAKEOVER

The web app can talk to EVERYTHING on the network/host.
Attacker inherits all the app's network privileges.

WITH MICRO-SEGMENTATION:
========================

  [Attacker] ---(RCE exploit)---> [Web App]
                                      |
       "I'm inside! Let me look around..."
                                      |
                  +-------------------+-------------------+
                  |                   |                   |
                  v                   v                   v
             [Database]         [Payment API]      [Admin Panel]
                  |                   |                   |
              ALLOWED              DENIED               DENIED
            (Only SELECT on    (Connection           (Connection
             specific tables)    refused)              refused)
                  |
                  v
           Limited data only    ATTACK CONTAINED

The web app can ONLY talk to specifically allowed paths.
Attacker is "trapped" in a box with no lateral movement.

+------------------------------------------------------------------+
|              THE "ASSUME BREACH" MENTAL MODEL                      |
+------------------------------------------------------------------+

Traditional security: "Build walls to keep attackers OUT"
Zero Trust security:  "Assume attackers are ALREADY IN"

Questions to ask yourself:
  1. If App X is compromised, what can the attacker reach?
  2. What's the MINIMUM set of connections App X needs?
  3. Can I detect when App X tries to reach something unusual?

MICRO-SEGMENTATION IS THE "SEAT BELTS" OF SECURITY:
  - Firewalls are airbags (prevent initial crash)
  - Micro-seg is seat belts (minimize damage when crash happens)
  - You need BOTH

Connection Tracking (conntrack)

Stateful firewalling depends on tracking connections. Understanding conntrack is essential:

+------------------------------------------------------------------+
|                    CONNECTION TRACKING (CONNTRACK)                 |
+------------------------------------------------------------------+

Connection tracking allows the firewall to understand the STATE
of a connection, not just individual packets.

THE CONNTRACK TABLE:
====================

$ sudo conntrack -L
tcp      6 431999 ESTABLISHED src=192.168.1.5 dst=93.184.216.34 sport=54321 dport=443 src=93.184.216.34 dst=192.168.1.5 sport=443 dport=54321 [ASSURED] mark=0 use=1
udp     17 29 src=192.168.1.5 dst=8.8.8.8 sport=12345 dport=53 src=8.8.8.8 dst=192.168.1.5 sport=53 dport=12345 mark=0 use=1

                ^                    ^                        ^
                |                    |                        |
         Protocol/State       Original direction         Reply direction

CONNECTION STATES:
==================

+------------------------------------------------------------------+
|   State        | Meaning                    | Firewall Action    |
|----------------|----------------------------|---------------------|
| NEW            | First packet of connection | Evaluate rules      |
| ESTABLISHED    | Seen traffic both ways     | Usually ACCEPT      |
| RELATED        | Related to existing conn   | Usually ACCEPT      |
|                | (e.g., FTP data channel)   |                     |
| INVALID        | Doesn't match any state    | Usually DROP        |
| UNTRACKED      | Bypasses conntrack         | Evaluate rules      |
+------------------------------------------------------------------+

THE "ESTABLISHED,RELATED" TRICK:
================================

This is the most important rule for any firewall:

iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT

WHY? Because you only need to define rules for NEW connections.
Once established, the reply packets are automatically allowed.

WITHOUT THIS RULE:
  1. Client sends SYN     (OUTPUT: allowed by your rule)
  2. Server sends SYN-ACK (INPUT: BLOCKED! No rule matches)
  3. Connection fails

WITH THIS RULE:
  1. Client sends SYN     (OUTPUT: allowed by your rule)
  2. conntrack: "NEW connection: 192.168.1.5:54321 -> 93.184.216.34:443"
  3. Server sends SYN-ACK (INPUT: matches ESTABLISHED, ACCEPT)
  4. Connection succeeds!

CONNTRACK AND MICRO-SEGMENTATION:
=================================

PROBLEM: You add a new DENY rule, but existing connections still work!

$ iptables -A OUTPUT -m owner --uid-owner 1001 -p tcp --dport 6379 -j DROP

Expectation: web-user can't reach Redis anymore
Reality: Existing connections keep working for hours

WHY? The ESTABLISHED rule (above) matches existing connections
     before your new DROP rule is evaluated.

SOLUTIONS:

1. Flush the conntrack table (disrupts ALL connections):
   $ sudo conntrack -D -p tcp --dport 6379

2. Add INVALID/UNTRACKED handling:
   $ iptables -I OUTPUT 1 -m conntrack --ctstate INVALID -j DROP

3. Use connection timeout:
   # Wait for TCP timeout (default: 5 days for ESTABLISHED)
   # Or configure: /proc/sys/net/netfilter/nf_conntrack_tcp_timeout_established

4. Mark and kill connections:
   $ ss -K dst 127.0.0.1:6379  # Kill matching sockets

FOR ZERO-DOWNTIME UPDATES:
==========================

1. Add new ACCEPT rules first
2. Remove old ACCEPT rules
3. Existing connections keep working (ESTABLISHED)
4. New connections use new rules

This is why nftables' atomic updates are valuable.

Default Deny Philosophy

The “Default Deny” principle is foundational to Zero Trust:

+------------------------------------------------------------------+
|                    DEFAULT DENY PHILOSOPHY                         |
+------------------------------------------------------------------+

THE PRINCIPLE:
==============

"Everything is DENIED unless EXPLICITLY allowed."

This is the opposite of traditional networking where:
  - Default: ALLOW
  - Exception: Deny specific bad things

Zero Trust flips this:
  - Default: DENY
  - Exception: Allow specific good things

IMPLEMENTATION IN IPTABLES:
===========================

STEP 1: Set chain policies to DROP

$ iptables -P INPUT DROP
$ iptables -P OUTPUT DROP
$ iptables -P FORWARD DROP

WARNING: If you run this without ESTABLISHED rule,
         you'll immediately lose SSH access!

STEP 2: Allow essential traffic FIRST

# Allow loopback (required for many apps)
$ iptables -A INPUT -i lo -j ACCEPT
$ iptables -A OUTPUT -o lo -j ACCEPT

# Allow established connections (critical!)
$ iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
$ iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT

# Allow SSH (don't lock yourself out!)
$ iptables -A INPUT -p tcp --dport 22 -j ACCEPT
$ iptables -A OUTPUT -p tcp --sport 22 -j ACCEPT

STEP 3: Add specific allow rules for your apps

# web-user can reach Redis
$ iptables -A OUTPUT -m owner --uid-owner web-user -p tcp --dport 6379 -j ACCEPT

# web-user can reach PostgreSQL
$ iptables -A OUTPUT -m owner --uid-owner web-user -p tcp --dport 5432 -j ACCEPT

STEP 4: Log and drop everything else

$ iptables -A INPUT -j LOG --log-prefix "INPUT DENIED: "
$ iptables -A INPUT -j DROP

$ iptables -A OUTPUT -j LOG --log-prefix "OUTPUT DENIED: "
$ iptables -A OUTPUT -j DROP

+------------------------------------------------------------------+
|              SAFE TESTING ORDER (DON'T LOCK YOURSELF OUT!)         |
+------------------------------------------------------------------+

ALWAYS TEST IN THIS ORDER:

1. Backup current rules:
   $ iptables-save > /tmp/backup.rules

2. Set a timeout to restore (in another terminal):
   $ sleep 300 && iptables-restore < /tmp/backup.rules

3. Apply rules one at a time, testing SSH after each

4. Only set policy to DROP after confirming SSH works

5. If locked out, wait for the timeout to restore

PRO TIP: Use `iptables -I` (insert) instead of `-A` (append)
         to add rules at the TOP of the chain during testing.

+------------------------------------------------------------------+
|              WHAT "DEFAULT DENY" LOOKS LIKE IN PRACTICE            |
+------------------------------------------------------------------+

BEFORE (Default Allow - Dangerous):
===================================

$ iptables -L OUTPUT -n
Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination
DROP       tcp  --  0.0.0.0/0            0.0.0.0/0            tcp dpt:31337

"Allow everything except the known bad port 31337"
Problem: Attacker uses port 31338. Allowed!

AFTER (Default Deny - Secure):
==============================

$ iptables -L OUTPUT -n
Chain OUTPUT (policy DROP)
target     prot opt source               destination
ACCEPT     all  --  0.0.0.0/0            0.0.0.0/0            state ESTABLISHED
ACCEPT     tcp  --  0.0.0.0/0            0.0.0.0/0            owner UID match 1001 tcp dpt:6379
ACCEPT     tcp  --  0.0.0.0/0            0.0.0.0/0            owner UID match 1001 tcp dpt:5432
LOG        all  --  0.0.0.0/0            0.0.0.0/0            LOG flags 0 level 4 prefix "OUTPUT DENIED: "
DROP       all  --  0.0.0.0/0            0.0.0.0/0

"Deny everything except these specific allowed paths"
Problem: Attacker uses port 31338. DENIED!

Process Isolation Mechanisms

Understanding how Linux identifies processes is crucial:

+------------------------------------------------------------------+
|              LINUX PROCESS ISOLATION MECHANISMS                    |
+------------------------------------------------------------------+

CREDENTIALS:
============

Every process has a set of credentials:

$ cat /proc/self/status | grep -E '^(Uid|Gid|Groups)'
Uid:    1000    1000    1000    1000    (real, effective, saved, fs)
Gid:    1000    1000    1000    1000
Groups: 4 24 27 30 46 116 1000

- Real UID: Who started the process
- Effective UID: What permissions apply NOW (for setuid binaries)
- Saved UID: Can be restored after dropping privileges
- FS UID: Used for filesystem access checks

The iptables owner module matches EFFECTIVE UID.

NAMESPACES:
===========

Linux namespaces provide isolation:

+------------------------------------------------------------------+
| Namespace | Isolates                        | Relevant to Network |
|-----------|----------------------------------|---------------------|
| net       | Network interfaces, routes, fw  | VERY (isolated fw!) |
| pid       | Process ID numbers               | No                  |
| mnt       | Filesystem mounts                | No                  |
| user      | UID/GID mappings                 | YES (uid mismatch!) |
| uts       | Hostname and domain              | No                  |
| ipc       | System V IPC, POSIX queues       | No                  |
| cgroup    | cgroup hierarchy visibility      | YES (cgroup filters)|
| time      | System time (kernel 5.6+)        | No                  |
+------------------------------------------------------------------+

NAMESPACE IMPACT ON FIREWALL RULES:
===================================

Each network namespace has its OWN iptables rules!

$ ip netns exec container1 iptables -L
(shows rules for container1's network namespace)

$ iptables -L
(shows rules for the HOST network namespace)

This is why Docker/K8s networking is complex:
  - Container has one set of rules
  - Host has another set of rules
  - Traffic crosses namespace boundaries via veth pairs

For micro-segmentation on the HOST namespace:
  - Your rules apply to all processes in that namespace
  - Container traffic appears as traffic from the container network
  - Use cgroup matching for container-aware filtering

CGROUPS:
========

Control groups (cgroups) organize processes into hierarchies:

/sys/fs/cgroup/
├── user.slice/
│   ├── user-1000.slice/
│   │   ├── session-3.scope/  <-- Your terminal session
│   │   └── app.service/      <-- Systemd service
├── system.slice/
│   ├── nginx.service/
│   └── redis.service/
└── docker/
    ├── container-abc123/
    └── container-def456/

CGROUP-BASED FIREWALLING:
=========================

nftables and eBPF can match traffic by cgroup:

# nftables example
nft add rule inet filter output socket cgroupv2 level 2 "system.slice/nginx.service" accept

# eBPF can read cgroup ID:
u64 cgroup_id = bpf_get_current_cgroup_id();

This allows per-SERVICE rules, not just per-USER rules.
Much more granular than iptables owner module!

Complete Project Specification

Functional Requirements

ID Requirement Acceptance Criteria
FR-1 Parse YAML configuration file Tool reads rules from YAML, validates syntax
FR-2 Apply Default Deny policy Chain policies set to DROP, all traffic blocked by default
FR-3 Allow traffic by UID/GID Rules created with -m owner matching specified users
FR-4 Preserve SSH access SSH (port 22) always allowed to prevent lockout
FR-5 Log blocked attempts Blocked packets logged to /var/log/kern.log with prefix
FR-6 Flush rules safely Tool can remove all custom rules without breaking system
FR-7 Persist rules across reboot Rules saved to /etc/iptables/rules.v4 or equivalent
FR-8 Report current state Tool can display active rules and their hit counts
FR-9 Support ESTABLISHED connections Existing connections continue working after rule changes
FR-10 Resolve usernames to UIDs Tool accepts “web-user” and converts to numeric UID

Non-Functional Requirements

ID Requirement Target
NFR-1 Rule application time < 100ms for 100 rules
NFR-2 Minimal network latency < 0.1ms added per packet
NFR-3 No packet drops during update Zero dropped packets during rule change
NFR-4 Idempotent operations Running tool twice produces same result
NFR-5 Audit trail All rule changes logged with timestamp
NFR-6 Graceful error handling Invalid rules don’t crash or partial-apply

Configuration File Format

# /etc/zt-segment/rules.yaml
version: "1.0"
settings:
  default_policy: deny          # deny or allow
  log_blocked: true
  log_prefix: "ZT-BLOCKED: "
  preserve_ssh: true            # Safety: always allow SSH
  preserve_established: true    # Allow existing connections

rules:
  # Web application can reach Redis
  - name: "web-to-redis"
    description: "Allow web app to connect to cache"
    from:
      user: "web-user"          # Username (resolved to UID)
      # uid: 1001              # Alternative: specify UID directly
    to:
      host: "127.0.0.1"         # Optional: default is any
      port: 6379
      protocol: tcp             # tcp, udp, or both
    action: allow

  # Web application can reach PostgreSQL
  - name: "web-to-postgres"
    from:
      user: "web-user"
    to:
      port: 5432
      protocol: tcp
    action: allow

  # Database user can reach nothing (isolated)
  - name: "db-no-egress"
    from:
      user: "postgres"
    to:
      host: "0.0.0.0/0"         # All destinations
    action: deny
    log: true                   # Extra logging for this rule

  # Allow DNS for all users (required for hostname resolution)
  - name: "allow-dns"
    from:
      user: "*"                 # Any user
    to:
      port: 53
      protocol: udp
    action: allow

Architecture Diagram

+------------------------------------------------------------------+
|              ZT-SEGMENT TOOL ARCHITECTURE                          |
+------------------------------------------------------------------+

                         +------------------+
                         |   rules.yaml     |
                         +--------+---------+
                                  |
                                  v
+------------------------------------------------------------------+
|                       ZT-SEGMENT TOOL                              |
|  +-------------------+  +-------------------+  +-----------------+ |
|  | Config Parser     |  | Rule Translator   |  | State Manager   | |
|  |                   |  |                   |  |                 | |
|  | - YAML parsing    |  | - YAML -> iptables|  | - Current rules | |
|  | - Validation      |  | - UID resolution  |  | - Diff detection| |
|  | - User lookup     |  | - Rule ordering   |  | - Hit counters  | |
|  +-------------------+  +-------------------+  +-----------------+ |
|           |                      |                     |           |
|           v                      v                     v           |
|  +----------------------------------------------------------+     |
|  |                    Firewall Backend                       |     |
|  |  +------------------+  +------------------+               |     |
|  |  | iptables Driver  |  | nftables Driver  | (extensible) |     |
|  |  +------------------+  +------------------+               |     |
|  +----------------------------------------------------------+     |
|                              |                                     |
+------------------------------|-------------------------------------+
                               |
       ========================|===================================
                               v           KERNEL SPACE
                        +-------------+
                        |  Netfilter  |
                        |  Subsystem  |
                        +------+------+
                               |
              +----------------+----------------+
              |                |                |
       +------+------+  +------+------+  +------+------+
       |   INPUT     |  |   OUTPUT    |  |   FORWARD   |
       |   Chain     |  |   Chain     |  |   Chain     |
       +-------------+  +-------------+  +-------------+
                               |
                               v
                      +----------------+
                      | Audit Logger   |
                      | (via LOG tgt)  |
                      +----------------+
                               |
                               v
                        /var/log/kern.log

+------------------------------------------------------------------+
|                    COMMAND FLOW EXAMPLES                           |
+------------------------------------------------------------------+

$ sudo ./zt-segment --config ./rules.yaml

1. Parse rules.yaml
2. Validate all rules (users exist, ports valid)
3. Generate iptables commands
4. Apply in atomic batch (if using nftables)
5. Verify rules applied correctly
6. Save rules for persistence

$ sudo ./zt-segment --status

1. Query current iptables rules
2. Match against expected rules
3. Show hit counters (packets matched)
4. Report any drift from config

$ sudo ./zt-segment --flush

1. Remove all ZT-SEGMENT rules
2. Restore default ACCEPT policy
3. Keep SSH rules (safety)
4. Log the flush operation

Real World Outcome

When you complete this project, you will have a host-level security tool that locks down your server using the “Default Deny” principle. You will be able to prove that even with a root-level vulnerability in one app, the attacker is “trapped” and cannot touch other local services.

Terminal Setup

# Terminal 1: Create test users and services
$ sudo useradd -r -s /bin/false web-user
$ sudo useradd -r -s /bin/false db-user
$ sudo useradd -r -s /bin/false attacker-user

# Start services as different users
$ sudo -u web-user python3 -m http.server 8080 &
[1] 12345
$ sudo -u db-user redis-server --port 6379 --bind 127.0.0.1 &
[2] 12346
# Terminal 2: Apply micro-segmentation
$ sudo ./zt-segment --config ./rules.yaml
[INFO] 2024-12-27T10:00:00Z Loading micro-segmentation rules from ./rules.yaml
[INFO] 2024-12-27T10:00:00Z Validating rules...
[INFO] 2024-12-27T10:00:00Z   Rule 'web-to-redis': web-user (UID 1001) -> 127.0.0.1:6379/tcp
[INFO] 2024-12-27T10:00:00Z   Rule 'preserve-ssh': * -> *:22/tcp
[INFO] 2024-12-27T10:00:00Z   Rule 'established': ESTABLISHED,RELATED -> ACCEPT
[INFO] 2024-12-27T10:00:00Z Applying Default Deny to OUTPUT chain...
[INFO] 2024-12-27T10:00:00Z Applied 5 iptables rules
[INFO] 2024-12-27T10:00:00Z Rules verified successfully
[INFO] 2024-12-27T10:00:00Z Micro-segmentation ACTIVE

$ sudo ./zt-segment --status
+------------------------------------------------------------------+
|                    ZT-SEGMENT STATUS                               |
+------------------------------------------------------------------+
| Policy: DEFAULT DENY                                               |
| Active Rules: 5                                                    |
| Last Updated: 2024-12-27T10:00:00Z                                |
+------------------------------------------------------------------+
| #  | Rule Name        | From         | To              | Packets |
|----|------------------|--------------|-----------------|---------|
| 1  | established      | *            | ESTABLISHED     | 1,234   |
| 2  | preserve-ssh     | *            | *:22/tcp        | 89      |
| 3  | web-to-redis     | web-user     | 127.0.0.1:6379  | 456     |
| 4  | log-denied       | *            | LOG             | 23      |
| 5  | default-deny     | *            | DROP            | 23      |
+------------------------------------------------------------------+

Test Case 1: Authorized Path (web-user to Redis)

$ sudo -u web-user curl http://localhost:6379
-ERR unknown command 'GET', with args beginning with: '/',

# Redis responds! This proves:
# 1. web-user CAN reach port 6379
# 2. The connection was ALLOWED by our rule

# Check the iptables counters
$ sudo iptables -L OUTPUT -v -n | grep 6379
  456   27360 ACCEPT  tcp  --  *  *  0.0.0.0/0  127.0.0.1  owner UID match 1001 tcp dpt:6379

# 456 packets have matched this rule

Test Case 2: Unauthorized Path (attacker-user to Redis)

$ sudo -u attacker-user curl http://localhost:6379
curl: (7) Failed to connect to localhost port 6379: Connection refused

# Connection REFUSED! The attacker is blocked.

# Check the kernel log
$ sudo tail -1 /var/log/kern.log
Dec 27 10:01:00 hostname kernel: ZT-BLOCKED: IN= OUT=lo SRC=127.0.0.1 DST=127.0.0.1 PROTO=TCP SPT=54321 DPT=6379 UID=1003

# The log shows:
# - UID=1003 (attacker-user) attempted connection
# - DPT=6379 (Redis port) was blocked
# - This is your audit trail!

Test Case 3: Lateral Movement Block (web-user to Internet)

# Even if an attacker compromises web-user, they can't reach the internet

$ sudo -u web-user ping 8.8.8.8
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
ping: sendmsg: Operation not permitted

$ sudo -u web-user curl https://evil-c2-server.com
curl: (7) Couldn't connect to server

# The compromised web app cannot:
# - Ping external hosts
# - Download malware
# - Connect to C2 servers
# - Exfiltrate data

# Log entry:
$ sudo tail -1 /var/log/kern.log
Dec 27 10:02:00 hostname kernel: ZT-BLOCKED: IN= OUT=eth0 SRC=192.168.1.100 DST=8.8.8.8 PROTO=ICMP UID=1001

Test Case 4: SSH Still Works (Safety Check)

# From another machine, verify SSH still works
$ ssh user@192.168.1.100
user@hostname:~$

# You didn't lock yourself out!

The Core Question You’re Answering

“How can each host in my network defend itself, even if the perimeter is breached, by enforcing identity-based access at the kernel level?”

This project transforms you from someone who relies on perimeter firewalls to someone who understands that every host must be its own fortress. You will learn that the kernel itself is your last line of defense, and that filtering traffic by process identity (not just IP addresses) is what separates modern Zero Trust architectures from legacy security models.


Concepts You Must Understand First

Before diving into implementation, ensure you can answer these foundational questions:

1. Linux Netfilter and iptables Architecture

Question to answer: When a packet arrives at a Linux host, what path does it take through the kernel, and at which points can you intercept and filter it?

The Netfilter framework is the foundation of all Linux firewalling. Understanding its hook architecture (PREROUTING, INPUT, FORWARD, OUTPUT, POSTROUTING) is essential because you must know WHERE to place your filtering rules. Misunderstanding this leads to rules that never match or, worse, rules that block the wrong traffic.

Book Reference:

  • “Understanding Linux Network Internals” by Christian Benvenuti, Chapters 10-12 (Netfilter Architecture)
  • “The Linux Programming Interface” by Michael Kerrisk, Chapter 58-61 (Sockets and TCP/IP fundamentals)

2. eBPF (extended Berkeley Packet Filter)

Question to answer: Why is eBPF considered the future of network filtering, and what can it do that iptables cannot?

eBPF allows you to run custom programs inside the kernel at various hook points, with near-native performance. Unlike iptables which has fixed matching capabilities, eBPF can inspect packet contents, access process metadata (like the executable path, not just UID), and use O(1) hashmap lookups instead of O(n) rule chains. Understanding eBPF positions you to build next-generation security tools.

Book Reference:

  • “BPF Performance Tools” by Brendan Gregg, Chapters 1-3 (BPF introduction and architecture)
  • “Linux Observability with BPF” by David Calavera and Lorenzo Fontana

3. Network Namespaces

Question to answer: How do network namespaces provide isolation, and why does each namespace have its own independent firewall rules?

Network namespaces are a fundamental Linux isolation primitive. Each namespace has its own network interfaces, routing tables, AND iptables rules. This is how containers achieve network isolation. Understanding this concept is critical because your micro-segmentation tool must decide: do you apply rules in the host namespace, the container namespace, or both?

Book Reference:

  • “Container Security” by Liz Rice, Chapter 4 (Namespaces)
  • “How Linux Works, 3rd Edition” by Brian Ward, Chapter 9 (Network Configuration)

4. TCP/IP Packet Flow in the Kernel

Question to answer: What happens to a TCP SYN packet from the moment it arrives at the network interface until it reaches the application socket?

Deep understanding of packet flow reveals why certain filtering approaches work and others fail. You need to know about socket buffers (skb), the routing decision point, and how the kernel associates packets with sockets. This knowledge is essential for understanding why the owner module works in OUTPUT but not INPUT, and how conntrack maintains connection state.

Book Reference:

  • “TCP/IP Illustrated, Volume 1” by W. Richard Stevens, Chapters 17-18 (TCP Connection Management)
  • “Understanding Linux Network Internals” by Christian Benvenuti, Chapters 13-14 (Packet Reception and Transmission)

5. East-West vs North-South Traffic

Question to answer: Why is internal (East-West) traffic often more dangerous than external (North-South) traffic, and what assumptions about trust does this expose?

Traditional security focuses on the perimeter (North-South), assuming everything inside the network is trusted. This is the core fallacy that Zero Trust corrects. East-West traffic between internal services is often unencrypted, unmonitored, and unrestricted, making it the ideal attack vector after initial compromise. Your micro-segmentation tool directly addresses this blind spot.

Book Reference:

  • “Zero Trust Networks” by Evan Gilman and Doug Barth, Chapter 5 (Micro-segmentation)
  • NIST SP 800-207: Zero Trust Architecture

6. Connection Tracking (conntrack) and Stateful Filtering

Question to answer: How does the kernel remember that a packet is part of an existing connection, and why is the ESTABLISHED,RELATED rule so critical?

Stateless filtering treats each packet independently, which makes reply traffic hard to handle. Connection tracking (conntrack) maintains state for each connection, enabling rules like “allow reply packets for connections I initiated.” Misunderstanding conntrack leads to broken connections or security bypasses where existing connections continue working after a DENY rule is added.

Book Reference:

  • “Linux Firewalls, 4th Edition” by Steve Suehring, Chapters 3-4 (iptables and connection tracking)
  • “The Linux Programming Interface” by Michael Kerrisk, Chapter 61 (Advanced Socket Topics)

Questions to Guide Your Design

Before writing code, work through these design questions:

Policy Representation

  • How will you represent the relationship “user X can talk to port Y on host Z”?
  • Should your policy file be explicit (list every allowed path) or implicit (list exceptions to a default)?
  • How do you handle wildcards (any user, any port, any host)?

Rule Translation

  • What iptables command corresponds to “allow UID 1001 to TCP port 6379”?
  • In what order must rules appear in the chain? Why does order matter?
  • How do you ensure ESTABLISHED,RELATED is always evaluated first?

Safety Mechanisms

  • What happens if your tool crashes mid-application? Will the host be locked down or wide open?
  • How do you ensure SSH access is never accidentally blocked?
  • What timeout or fallback mechanisms prevent permanent lockout?

State Management

  • How do you detect drift between your policy file and the actual iptables rules?
  • How do you handle rules that were manually added outside your tool?
  • Should your tool be idempotent (running twice produces the same result)?

eBPF Considerations (Advanced)

  • If you migrate to eBPF, how do you handle the fact that eBPF programs have limited stack space (512 bytes)?
  • How do you update policy without reloading the eBPF program? (Hint: eBPF maps)
  • How do you match by process binary name instead of just UID?

Thinking Exercise

Paper exercise before coding: Trace a TCP connection through the Netfilter stack.

Grab a piece of paper and draw the following scenario:

  1. Process A (UID 1001, PID 5000, executable /usr/bin/webapp) wants to connect to 127.0.0.1:6379 (Redis)
  2. Process B (UID 999, PID 6000, executable /usr/bin/redis-server) is listening on 127.0.0.1:6379

Trace the packet flow:

  1. Process A calls connect(): What system call does this translate to? What happens in the kernel?

  2. Kernel creates SYN packet: At this point, which fields are available: source IP, source port, destination IP, destination port, UID of Process A, PID of Process A, executable path?

  3. SYN packet enters OUTPUT chain: Draw the tables (raw, mangle, nat, filter, security) and their order. Where would your filtering rule be?

  4. SYN packet exits via loopback: Since destination is 127.0.0.1, what happens? Does it go through a physical interface?

  5. SYN packet re-enters via PREROUTING: The loopback interface receives its own packet. Trace through PREROUTING.

  6. Routing decision: Kernel sees destination is local. Packet goes to INPUT chain, not FORWARD.

  7. INPUT chain filtering: Can you match by “destination UID” (Process B’s UID)? Why or why not?

  8. Packet delivered to redis-server: The socket accept() call returns. Connection established.

  9. SYN-ACK packet generated: Now trace the reply. Does it go through OUTPUT? Does it match your rules?

  10. ESTABLISHED state: After the 3-way handshake, what state does conntrack record? How does this affect future packets?

Questions to answer from your trace:

  • At which point can you identify the source process (UID, PID)?
  • At which point can you identify the destination process?
  • Why does the owner module only work in OUTPUT?
  • What information would you need eBPF to access that iptables cannot?

Hints in Layers

If you get stuck, reveal these hints progressively:

Layer 1: Basic iptables Structure

Hint: Rule application order matters

iptables evaluates rules top-to-bottom, first match wins. Your tool must ensure:

  1. ESTABLISHED,RELATED rule comes FIRST (allows reply traffic)
  2. Safety rules (SSH) come SECOND (prevent lockout)
  3. User-defined ALLOW rules come NEXT
  4. LOG rule comes before DROP (for audit trail)
  5. DROP is the policy or final rule

A common mistake is appending a DROP rule, then appending ALLOW rules after it. The DROP will match first, and your ALLOW rules will never be evaluated.

Use iptables -I OUTPUT 1 to insert at position 1 (top), or calculate positions carefully when using -A (append).

Layer 2: The Owner Module

Hint: Matching traffic by process identity

The owner module is your key to process-aware filtering:

# Match outgoing traffic from UID 1001
iptables -A OUTPUT -m owner --uid-owner 1001 -j ACCEPT

# Match outgoing traffic from group "web"
iptables -A OUTPUT -m owner --gid-owner web -j ACCEPT

Important limitations:

  • Only works in OUTPUT (and POSTROUTING for some options)
  • Cannot match destination process on INPUT chain
  • Uses effective UID, not real UID
  • Cannot match by process name or executable path (eBPF can)

To resolve username to UID in your code:

import pwd
uid = pwd.getpwnam("www-data").pw_uid  # Returns 33 on Debian

Layer 3: Connection Tracking Integration

Hint: Handling stateful connections correctly

The ESTABLISHED,RELATED rule is critical:

# This MUST be the first rule in OUTPUT and INPUT
iptables -A OUTPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

Why? When your webapp (UID 1001) connects to Redis (port 6379):

  1. SYN packet matches your ALLOW rule (OUTPUT chain)
  2. SYN-ACK from Redis would be DROPPED without ESTABLISHED rule (INPUT chain)
  3. The ESTABLISHED rule recognizes this as a reply to an existing connection

Gotcha: If you add a DENY rule for an existing connection, the connection continues working because ESTABLISHED matches before your DENY. To immediately terminate:

# Delete conntrack entry for this connection
conntrack -D -p tcp --dport 6379
# Or kill the socket directly
ss -K dst 127.0.0.1:6379

Layer 4: Atomic Updates with nftables

Hint: Zero-downtime rule changes

iptables applies rules one at a time. Between commands, you may have an inconsistent state. nftables solves this:

# Create a complete ruleset file
table inet zt_segment {
    chain output {
        type filter hook output priority 0; policy drop;

        ct state established,related accept
        tcp sport 22 accept
        meta skuid 1001 tcp dport 6379 accept
        log prefix "ZT-BLOCKED: "
        drop
    }
}

Apply atomically:

nft -f /path/to/rules.nft

The entire table is replaced in a single kernel operation. No gap where traffic could leak through.

Migration path: Start with iptables for learning, then refactor to nftables for production. The concepts are identical; only the syntax differs.

Layer 5: eBPF for Advanced Matching

Hint: Moving beyond UID-based filtering

eBPF unlocks capabilities impossible with iptables:

Match by cgroup (container-aware):

u64 cgroup_id = bpf_get_current_cgroup_id();
// Look up policy for this container

Match by executable path:

char comm[16];
bpf_get_current_comm(&comm, sizeof(comm));
// comm now contains "webapp", "redis-server", etc.

O(1) policy lookup:

// Instead of O(n) rule chain traversal
struct policy_key key = { .uid = uid, .dport = dport };
struct policy_value *val = bpf_map_lookup_elem(&policies, &key);

Rust + Aya framework:

// Kernel-side eBPF program
#[classifier]
pub fn zt_filter(ctx: TcContext) -> i32 {
    let uid = bpf_get_current_uid_gid() as u32;
    match ALLOWED.get(&uid) {
        Some(_) => TC_ACT_OK,
        None => TC_ACT_SHOT,
    }
}

eBPF requires kernel 4.4+ (ideally 5.x), and the learning curve is steep. Start with iptables, graduate to eBPF when you need its power.


Solution Architecture

Component Breakdown

+------------------------------------------------------------------+
|                    SOLUTION COMPONENTS                             |
+------------------------------------------------------------------+

1. CONFIGURATION LAYER
   +------------------+
   | ConfigParser     |
   +------------------+
   - Parse YAML configuration
   - Validate rule syntax
   - Resolve usernames to UIDs (getpwnam)
   - Handle defaults and inheritance

   Key Functions:
   - parse_config(path) -> Config
   - validate_rule(rule) -> Result<Rule, Error>
   - resolve_user(name) -> uid_t

2. TRANSLATION LAYER
   +------------------+
   | RuleTranslator   |
   +------------------+
   - Convert abstract rules to iptables commands
   - Handle rule ordering (ESTABLISHED first!)
   - Generate LOG rules for denied traffic
   - Support multiple backends (iptables, nftables)

   Key Functions:
   - translate_rule(rule) -> Vec<IptablesCommand>
   - order_rules(rules) -> Vec<Rule>
   - generate_log_rule(prefix) -> IptablesCommand

3. EXECUTION LAYER
   +------------------+
   | FirewallBackend  |
   +------------------+
   - Execute iptables/nftables commands
   - Atomic updates (nftables: whole ruleset swap)
   - Rollback on failure
   - Verification after application

   Key Functions:
   - apply(commands) -> Result<(), Error>
   - rollback(backup) -> Result<(), Error>
   - verify() -> bool

4. STATE MANAGEMENT
   +------------------+
   | StateManager     |
   +------------------+
   - Track current rules
   - Detect configuration drift
   - Report hit counters
   - Manage persistence

   Key Functions:
   - get_current_state() -> State
   - diff(config, state) -> Changes
   - persist_rules(path) -> Result<(), Error>

5. AUDIT/LOGGING
   +------------------+
   | AuditLogger      |
   +------------------+
   - Log all rule changes
   - Parse kernel log for blocked packets
   - Generate security reports
   - Alert on anomalies

   Key Functions:
   - log_change(before, after)
   - parse_kernel_log() -> Vec<BlockedPacket>
   - generate_report() -> Report

Rule Processing Pipeline

+------------------------------------------------------------------+
|                    RULE PROCESSING PIPELINE                        |
+------------------------------------------------------------------+

INPUT: rules.yaml
         |
         v
+-------------------+
| 1. PARSE          |  Parse YAML, create rule objects
+-------------------+
         |
         v
+-------------------+
| 2. VALIDATE       |  Check users exist, ports valid, no conflicts
+-------------------+
         |
         v
+-------------------+
| 3. RESOLVE        |  Username -> UID, hostname -> IP
+-------------------+
         |
         v
+-------------------+
| 4. ORDER          |  Put rules in correct evaluation order:
|                   |  1. ESTABLISHED,RELATED
|                   |  2. Safety rules (SSH)
|                   |  3. User rules (by priority)
|                   |  4. LOG denied
|                   |  5. Default DENY
+-------------------+
         |
         v
+-------------------+
| 5. TRANSLATE      |  Rule -> iptables command(s)
|                   |
|                   |  Example:
|                   |  web-to-redis rule becomes:
|                   |  iptables -A OUTPUT
|                   |    -m owner --uid-owner 1001
|                   |    -p tcp
|                   |    -d 127.0.0.1
|                   |    --dport 6379
|                   |    -j ACCEPT
+-------------------+
         |
         v
+-------------------+
| 6. DIFF           |  Compare with current state
|                   |  Only apply changes (idempotent)
+-------------------+
         |
         v
+-------------------+
| 7. BACKUP         |  Save current rules before changes
|                   |  iptables-save > /tmp/backup.rules
+-------------------+
         |
         v
+-------------------+
| 8. APPLY          |  Execute iptables commands
|                   |  If using nftables: atomic swap
+-------------------+
         |
         v
+-------------------+
| 9. VERIFY         |  Confirm rules match expected state
|                   |  Test critical paths (SSH)
+-------------------+
         |
         v
+-------------------+
| 10. PERSIST       |  Save for boot:
|                   |  - Debian: /etc/iptables/rules.v4
|                   |  - RHEL: /etc/sysconfig/iptables
|                   |  - Systemd: iptables-restore service
+-------------------+
         |
         v
OUTPUT: Rules applied, logged, persisted

Phased Implementation Guide

Phase 1: Manual iptables Exploration (2-3 hours)

Goal: Understand iptables commands by hand before automating.

Deliverables:

  • Successfully block yourself from google.com
  • Allow only specific users to reach specific ports
  • Read and understand kernel logs

Steps:

# 1. Start with a clean slate
$ sudo iptables -F        # Flush all rules
$ sudo iptables -X        # Delete custom chains
$ sudo iptables -P INPUT ACCEPT
$ sudo iptables -P OUTPUT ACCEPT

# 2. View current (empty) rules
$ sudo iptables -L -v -n
Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes)

# 3. Block yourself from Google
$ sudo iptables -A OUTPUT -d 8.8.8.8 -j REJECT
$ ping 8.8.8.8
ping: sendmsg: Operation not permitted  # BLOCKED!

# 4. Remove the rule
$ sudo iptables -D OUTPUT -d 8.8.8.8 -j REJECT

# 5. Create test users
$ sudo useradd -r test-user1
$ sudo useradd -r test-user2
$ id test-user1
uid=1001(test-user1) gid=1001(test-user1) groups=1001(test-user1)

# 6. Allow only test-user1 to reach port 443
$ sudo iptables -A OUTPUT -m owner --uid-owner test-user1 -p tcp --dport 443 -j ACCEPT
$ sudo iptables -A OUTPUT -p tcp --dport 443 -j REJECT

# Test:
$ sudo -u test-user1 curl -I https://google.com  # Works!
$ sudo -u test-user2 curl -I https://google.com  # Blocked!

# 7. Add logging
$ sudo iptables -I OUTPUT 2 -p tcp --dport 443 -j LOG --log-prefix "HTTPS-BLOCKED: "
$ sudo tail -f /var/log/kern.log

Verification:

  • Can block specific destinations
  • Can allow/deny by UID
  • Can see blocked packets in kernel log
  • Can restore to clean state

Phase 2: Basic YAML Config Parser (4-6 hours)

Goal: Read rules from YAML and validate them.

Deliverables:

  • YAML parser that loads rules.yaml
  • Validation that users exist
  • Validation that ports are valid

Python Prototype:

import yaml
import pwd
from dataclasses import dataclass
from typing import List, Optional

@dataclass
class Rule:
    name: str
    from_user: str
    from_uid: int
    to_host: Optional[str]
    to_port: int
    protocol: str
    action: str

def parse_config(path: str) -> List[Rule]:
    with open(path) as f:
        config = yaml.safe_load(f)

    rules = []
    for rule_config in config.get('rules', []):
        # Resolve username to UID
        username = rule_config['from'].get('user')
        if username and username != '*':
            try:
                uid = pwd.getpwnam(username).pw_uid
            except KeyError:
                raise ValueError(f"User '{username}' does not exist")
        else:
            uid = -1  # Match any user

        rule = Rule(
            name=rule_config['name'],
            from_user=username,
            from_uid=uid,
            to_host=rule_config['to'].get('host'),
            to_port=rule_config['to']['port'],
            protocol=rule_config['to'].get('protocol', 'tcp'),
            action=rule_config['action']
        )
        rules.append(rule)

    return rules

# Test
rules = parse_config('rules.yaml')
for rule in rules:
    print(f"{rule.name}: UID {rule.from_uid} -> {rule.to_port}/{rule.protocol}")

Verification:

  • Parses valid YAML without errors
  • Raises error for non-existent users
  • Raises error for invalid port numbers
  • Converts usernames to UIDs correctly

Phase 3: Rule Application Logic (6-8 hours)

Goal: Generate and execute iptables commands from parsed rules.

Deliverables:

  • Function to convert Rule to iptables command
  • Function to apply commands in correct order
  • Backup and rollback capability

Key Code:

import subprocess
from typing import List

def rule_to_iptables(rule: Rule) -> str:
    """Convert a Rule to an iptables command string."""
    cmd = ["iptables", "-A", "OUTPUT"]

    # Match by owner (if not wildcard)
    if rule.from_uid != -1:
        cmd.extend(["-m", "owner", "--uid-owner", str(rule.from_uid)])

    # Match protocol
    cmd.extend(["-p", rule.protocol])

    # Match destination
    if rule.to_host:
        cmd.extend(["-d", rule.to_host])
    cmd.extend(["--dport", str(rule.to_port)])

    # Action
    action = "ACCEPT" if rule.action == "allow" else "DROP"
    cmd.extend(["-j", action])

    return " ".join(cmd)

def apply_rules(rules: List[Rule]) -> None:
    """Apply rules in the correct order."""
    # 1. Backup current rules
    backup = subprocess.check_output(["iptables-save"])

    try:
        # 2. Flush OUTPUT chain
        subprocess.run(["iptables", "-F", "OUTPUT"], check=True)

        # 3. Add ESTABLISHED,RELATED rule first
        subprocess.run([
            "iptables", "-A", "OUTPUT",
            "-m", "state", "--state", "ESTABLISHED,RELATED",
            "-j", "ACCEPT"
        ], check=True)

        # 4. Add SSH safety rule
        subprocess.run([
            "iptables", "-A", "OUTPUT",
            "-p", "tcp", "--sport", "22",
            "-j", "ACCEPT"
        ], check=True)

        # 5. Add user rules
        for rule in rules:
            cmd = rule_to_iptables(rule)
            subprocess.run(cmd.split(), check=True)

        # 6. Add LOG and DROP
        subprocess.run([
            "iptables", "-A", "OUTPUT",
            "-j", "LOG", "--log-prefix", "ZT-BLOCKED: "
        ], check=True)

        subprocess.run(["iptables", "-A", "OUTPUT", "-j", "DROP"], check=True)

        # 7. Set policy
        subprocess.run(["iptables", "-P", "OUTPUT", "DROP"], check=True)

    except subprocess.CalledProcessError as e:
        # Rollback on failure
        subprocess.run(["iptables-restore"], input=backup, check=True)
        raise RuntimeError(f"Failed to apply rules: {e}")

Verification:

  • Rules appear in iptables -L OUTPUT
  • Order is correct (ESTABLISHED first, DROP last)
  • Failed application triggers rollback
  • SSH access preserved after application

Phase 4: Logging and Auditing (4-6 hours)

Goal: Track blocked connections and generate reports.

Deliverables:

  • Parse kernel log for blocked packets
  • Generate human-readable reports
  • Alert on suspicious patterns

Log Parser:

import re
from datetime import datetime
from collections import defaultdict

def parse_kernel_log(log_path: str = "/var/log/kern.log") -> List[dict]:
    """Parse blocked packets from kernel log."""
    pattern = r"ZT-BLOCKED:.*SRC=(\S+).*DST=(\S+).*PROTO=(\w+).*DPT=(\d+).*UID=(\d+)"

    blocked = []
    with open(log_path) as f:
        for line in f:
            match = re.search(pattern, line)
            if match:
                blocked.append({
                    'src': match.group(1),
                    'dst': match.group(2),
                    'proto': match.group(3),
                    'dport': int(match.group(4)),
                    'uid': int(match.group(5)),
                    'raw': line.strip()
                })
    return blocked

def generate_report(blocked: List[dict]) -> str:
    """Generate a summary report of blocked connections."""
    by_uid = defaultdict(list)
    for b in blocked:
        by_uid[b['uid']].append(b)

    report = []
    report.append("=" * 60)
    report.append("ZT-SEGMENT BLOCKED CONNECTIONS REPORT")
    report.append("=" * 60)
    report.append(f"Total blocked: {len(blocked)}")
    report.append("")

    for uid, events in sorted(by_uid.items()):
        try:
            username = pwd.getpwuid(uid).pw_name
        except KeyError:
            username = f"UID:{uid}"

        report.append(f"User: {username} ({len(events)} blocked)")

        # Group by destination port
        by_port = defaultdict(int)
        for e in events:
            by_port[e['dport']] += 1

        for port, count in sorted(by_port.items(), key=lambda x: -x[1]):
            report.append(f"  -> Port {port}: {count} attempts")
        report.append("")

    return "\n".join(report)

Verification:

  • Blocked packets appear in parsed output
  • Report shows per-user statistics
  • Can identify suspicious patterns (many attempts to same port)

Phase 5: Zero-Downtime Rule Updates (6-8 hours)

Goal: Update rules without dropping active connections.

Deliverables:

  • Diff current state against desired state
  • Apply only changed rules
  • Maintain ESTABLISHED connections

Implementation Strategy:

+------------------------------------------------------------------+
|              ZERO-DOWNTIME UPDATE ALGORITHM                        |
+------------------------------------------------------------------+

CURRENT STATE:                    DESIRED STATE:
+----------------------+          +----------------------+
| 1. ESTABLISHED->ACC  |          | 1. ESTABLISHED->ACC  |
| 2. SSH->ACCEPT       |          | 2. SSH->ACCEPT       |
| 3. web->6379 ACCEPT  |          | 3. web->6379 ACCEPT  |
| 4. LOG               |   -->    | 4. web->5432 ACCEPT  | NEW!
| 5. DROP              |          | 5. LOG               |
+----------------------+          | 6. DROP              |
                                  +----------------------+

NAIVE APPROACH (WRONG):
1. Flush all rules
2. Apply new rules
PROBLEM: Gap between flush and apply = all traffic allowed!

CORRECT APPROACH:
1. Insert new rule at correct position:
   iptables -I OUTPUT 4 -m owner --uid-owner 1001 -p tcp --dport 5432 -j ACCEPT

2. Existing connections continue (ESTABLISHED matches first)
3. New connections use new rule
4. No gap, no dropped packets

USING NFTABLES (BETTER):
1. Create new ruleset in memory
2. Atomic swap: `nft -f new-rules.nft`
3. Kernel swaps entire ruleset in single operation
4. Zero race conditions

nftables Migration:

# /etc/nftables/zt-segment.nft
table inet zt_segment {
    chain output {
        type filter hook output priority 0; policy drop;

        # Established connections
        ct state established,related accept

        # SSH safety
        tcp sport 22 accept

        # User rules
        meta skuid 1001 tcp dport 6379 accept
        meta skuid 1001 tcp dport 5432 accept

        # Log and drop
        log prefix "ZT-BLOCKED: "
        drop
    }
}

# Apply atomically
$ nft -f /etc/nftables/zt-segment.nft

Phase 6: (Optional) eBPF Migration (10-15 hours)

Goal: Replace iptables with eBPF for advanced features.

Deliverables:

  • eBPF program that matches by cgroup/binary
  • User-space controller for map management
  • Performance comparison with iptables

Rust + Aya Example:

// xt-segment-ebpf/src/main.rs (kernel side)
#![no_std]
#![no_main]

use aya_bpf::{
    bindings::TC_ACT_OK,
    bindings::TC_ACT_SHOT,
    macros::{classifier, map},
    maps::HashMap,
    programs::TcContext,
};

#[map]
static ALLOWED_PATHS: HashMap<(u32, u16), u8> = HashMap::with_max_entries(1024, 0);

#[classifier]
pub fn zt_segment(ctx: TcContext) -> i32 {
    match try_filter(ctx) {
        Ok(ret) => ret,
        Err(_) => TC_ACT_OK, // Fail open on error (or fail closed)
    }
}

fn try_filter(ctx: TcContext) -> Result<i32, i64> {
    // Get current UID
    let uid = unsafe { bpf_get_current_uid_gid() } as u32;

    // Get destination port (simplified, needs proper parsing)
    let dport: u16 = 0; // Parse from packet

    // Check if allowed
    let key = (uid, dport);
    if ALLOWED_PATHS.get(&key).is_some() {
        return Ok(TC_ACT_OK);
    }

    // Log and drop
    Ok(TC_ACT_SHOT)
}

Testing Strategy

Unit Tests

Component Test Case Expected Result
YAML Parser Valid config Rules parsed correctly
YAML Parser Missing user ValueError raised
YAML Parser Invalid port (>65535) ValueError raised
UID Resolver Existing user Returns correct UID
UID Resolver Non-existent user Raises KeyError
Rule Translator ACCEPT rule Correct iptables command
Rule Translator DROP rule Correct iptables command
Rule Translator Wildcard user No –uid-owner in command

Integration Tests

#!/bin/bash
# test_integration.sh

set -e

echo "Setting up test environment..."
sudo useradd -r -s /bin/false test-allowed || true
sudo useradd -r -s /bin/false test-denied || true

# Start a simple TCP server on port 9999
python3 -m http.server 9999 &
SERVER_PID=$!
sleep 1

echo "Applying rules..."
sudo ./zt-segment --config test-rules.yaml

echo "Test 1: Allowed user can connect..."
sudo -u test-allowed curl -s http://localhost:9999 > /dev/null
echo "PASS: Allowed user connected"

echo "Test 2: Denied user cannot connect..."
if sudo -u test-denied curl -s --connect-timeout 1 http://localhost:9999 > /dev/null 2>&1; then
    echo "FAIL: Denied user connected"
    exit 1
fi
echo "PASS: Denied user blocked"

echo "Test 3: SSH still works..."
ssh -o ConnectTimeout=5 localhost exit
echo "PASS: SSH works"

echo "Cleaning up..."
sudo ./zt-segment --flush
kill $SERVER_PID

echo "All tests passed!"

Security Tests

Attack Vector Test Method Expected Defense
Bypass via raw socket sudo -u test nc localhost 6379 Still blocked (owner module tracks UID)
UID spoofing Process cannot change UID Kernel prevents without setuid
Rule ordering exploit Add DROP before ESTABLISHED Tool reorders automatically
Lockout via flush Flush rules without SSH safety Tool preserves SSH
Conntrack bypass New connection after DENY conntrack flush or wait for timeout

Common Pitfalls and Debugging

Pitfall 1: Locking Yourself Out

Symptom: SSH connection drops, can’t reconnect.

Cause: Applied DROP policy without SSH exemption.

Prevention:

# ALWAYS add this BEFORE setting DROP policy:
iptables -A OUTPUT -p tcp --sport 22 -j ACCEPT
iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT

# Use a timeout safety net:
$ (sleep 300 && iptables -P OUTPUT ACCEPT) &
# If you get locked out, wait 5 minutes

Recovery:

  1. Cloud console / BMC access
  2. Boot into single-user mode
  3. iptables -P OUTPUT ACCEPT && iptables -F

Pitfall 2: Rule Ordering Issues

Symptom: Traffic blocked even though ACCEPT rule exists.

Cause: DROP rule evaluated before ACCEPT rule.

Debugging:

# Show rule numbers
$ sudo iptables -L OUTPUT -n -v --line-numbers
num   pkts bytes target     prot opt in     out     source      destination
1     0    0     DROP       all  --  *      *       0.0.0.0/0   0.0.0.0/0
2     0    0     ACCEPT     tcp  --  *      *       0.0.0.0/0   127.0.0.1  dpt:6379

# Rule 1 (DROP) matches before rule 2 (ACCEPT)!

Fix:

# Delete the misplaced DROP
$ sudo iptables -D OUTPUT 1

# Insert ACCEPT at the right position
$ sudo iptables -I OUTPUT 1 -p tcp -d 127.0.0.1 --dport 6379 -j ACCEPT

Pitfall 3: Connection Tracking Gotchas

Symptom: Existing connections work after adding DENY rule.

Cause: ESTABLISHED rule matches before your new DENY.

Explanation:

Packet arrives from existing connection:
  1. Check rule 1: ESTABLISHED? YES -> ACCEPT
  2. Never reaches your new DENY rule!

Fix:

# Option 1: Kill existing connections
$ sudo conntrack -D -p tcp --dport 6379

# Option 2: Use INVALID state to catch suspicious packets
$ sudo iptables -I OUTPUT 1 -m conntrack --ctstate INVALID -j DROP

# Option 3: Wait for connection timeout (could be hours/days)

Pitfall 4: ESTABLISHED Connections Bypassing New Rules

Symptom: User continues accessing resource after being denied.

Root Cause: This is actually a FEATURE, not a bug. Zero-downtime updates preserve existing connections.

Behavior:

1. User connects at 10:00 AM (allowed)
2. Rule changes at 10:30 AM (user now denied)
3. Existing connection CONTINUES working
4. NEW connection attempts are BLOCKED

If Immediate Revocation Required:

# Kill all connections matching criteria
$ sudo ss -K dst 127.0.0.1:6379

# Or kill connections for specific UID (requires custom logic)
$ for pid in $(pgrep -u web-user); do
    sudo nsenter -t $pid -n ss -K dst 127.0.0.1:6379
done

Debugging Toolkit

# 1. View rules with hit counters
$ sudo iptables -L OUTPUT -v -n --line-numbers

# 2. Watch blocked packets in real-time
$ sudo tail -f /var/log/kern.log | grep ZT-BLOCKED

# 3. View active connections
$ sudo ss -tulnp

# 4. View connections by UID
$ cat /proc/net/tcp | awk '{print $8}' | sort | uniq -c

# 5. Trace specific user's connections
$ sudo strace -f -e network -u web-user curl http://localhost:6379

# 6. View conntrack table
$ sudo conntrack -L -p tcp | grep 6379

# 7. Test rule matching
$ sudo iptables -C OUTPUT -m owner --uid-owner 1001 -p tcp --dport 6379 -j ACCEPT
# Returns 0 if rule exists, 1 if not

Extensions and Challenges

Extension 1: Container/Namespace-Aware Isolation

Challenge: Extend your tool to work with Docker containers.

What to Build:

  • Detect container network namespaces
  • Apply rules inside container network namespace
  • Use cgroup-based matching for container processes
  • Integrate with Docker events API

Hints:

# List container network namespaces
$ docker inspect -f '{{.State.Pid}}' container_name
# Use nsenter to apply rules inside:
$ sudo nsenter -t $PID -n iptables -L

# Or use cgroup matching with nftables:
$ nft add rule inet filter output meta cgroup "docker/container_id" drop

Extension 2: eBPF-Based Filtering

Challenge: Replace iptables with eBPF for richer matching.

What to Build:

  • TC classifier that checks process binary name
  • eBPF map for policy storage
  • Real-time policy updates without rule reload
  • Per-CPU counters for performance stats

Why It’s Harder:

  • Kernel programming (even with Aya/libbpf)
  • Limited stack size (512 bytes)
  • No dynamic memory allocation
  • Verification requirements

Extension 3: Integration with PDP (Project 2)

Challenge: Consult the Policy Decision Engine for dynamic rules.

What to Build:

  • API client that queries PDP for permissions
  • Cache decisions locally (with TTL)
  • Real-time rule updates on policy change
  • Fallback behavior when PDP unavailable

Architecture:

[Process makes connection]
       |
       v
[eBPF program checks cache] --miss--> [User-space queries PDP]
       |                                      |
     hit                                      v
       |                               [PDP returns ALLOW/DENY]
       v                                      |
[ALLOW or DROP]  <---cache update-------------+

Extension 4: Kubernetes NetworkPolicy Implementation

Challenge: Implement Kubernetes NetworkPolicy semantics.

What to Build:

  • Watch Kubernetes API for NetworkPolicy resources
  • Translate policies to iptables/nftables rules
  • Handle pod-to-pod traffic (calico/cilium-like)
  • Namespace isolation

Example Policy to Support:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: web-deny-all
spec:
  podSelector:
    matchLabels:
      app: web
  policyTypes:
  - Egress
  egress:
  - to:
    - podSelector:
        matchLabels:
          app: db
    ports:
    - port: 5432

Real-World Connections

Cilium

Cilium is the industry-leading eBPF-based networking and security for Kubernetes.

What It Does:

  • eBPF-based network policy enforcement
  • L3/L4/L7 visibility and control
  • Service mesh without sidecars
  • DNS-aware policies

How Your Project Relates: Your micro-segmentation tool is a simplified version of Cilium’s host-level policies. Cilium extends this to:

  • Pod-level granularity
  • Cross-node policies
  • Identity-based (not just UID-based) enforcement

Calico

Project Calico is a networking and security solution for Kubernetes.

What It Does:

  • IP-based and identity-based policies
  • Can use iptables or eBPF dataplane
  • Works on VMs, bare metal, and containers

How Your Project Relates: Calico’s host-protection policies are exactly what you’re building. Your YAML config format is similar to Calico’s HostEndpoint policies.

AWS Security Groups

AWS Security Groups are cloud-native micro-segmentation.

What They Do:

  • Allow/deny traffic by source/destination security group
  • Stateful (like conntrack)
  • Instance-level granularity

How Your Project Relates: Your iptables rules are the on-premise equivalent of Security Groups. The difference:

  • Security Groups: identity = “security group membership”
  • Your tool: identity = “UID/process owner”

Kubernetes Network Policies

Kubernetes NetworkPolicy is the standard API for pod networking policy.

What They Do:

  • Define ingress/egress rules for pods
  • Use labels for pod selection
  • Require a CNI that implements them (Calico, Cilium, etc.)

How Your Project Relates: NetworkPolicy is the declaration; your tool (if extended) would be the enforcement. NetworkPolicy says “what,” and tools like yours do the “how.”

Service Meshes (Istio, Linkerd)

Service meshes implement L7 micro-segmentation.

What They Do:

  • mTLS between services (identity via certificate)
  • Authorization policies (who can call what API)
  • Traffic routing and observability

How Your Project Relates: Your tool operates at L3/L4 (IP, port, UID). Service meshes operate at L7 (HTTP, gRPC). They’re complementary:

  • Your tool: Prevents TCP connections between untrusted processes
  • Service mesh: Authorizes specific API calls within allowed connections

Interview Questions

Question 1: What is “Lateral Movement” and how does micro-segmentation prevent it?

Brief Answer: Lateral movement is when an attacker, after compromising one system, moves sideways to attack other systems on the same network. Micro-segmentation prevents this by applying firewall rules between every host (or process), not just at the network edge. Even if an attacker compromises a web server, they cannot reach the database because there’s no allowed path.

Deeper Discussion: Mention East-West traffic, the “assume breach” model, and how traditional firewalls only protect North-South traffic.

Question 2: Explain the Netfilter architecture and the 5 hook points.

Brief Answer:

  1. PREROUTING: Packet just arrived, before routing decision (DNAT happens here)
  2. INPUT: Packet destined for a local process
  3. FORWARD: Packet passing through (this host is a router)
  4. OUTPUT: Packet generated by a local process
  5. POSTROUTING: Packet about to leave (SNAT happens here)

Follow-up: For localhost traffic, packets go through OUTPUT, briefly through routing, then INPUT.

Question 3: How does the iptables owner module work?

Brief Answer: The owner module matches packets based on the UID/GID of the local process that created the socket. It works in the OUTPUT chain because that’s where the kernel still knows which process is sending the packet. It reads the sk_uid field from the socket buffer’s associated socket structure.

Limitation: Can’t match destination process on INPUT chain (no socket association for incoming packets to unknown port).

Question 4: What are the tradeoffs between iptables, nftables, and eBPF?

Brief Answer: | | iptables | nftables | eBPF | |–|–|–|–| | Atomic updates | No (per-rule) | Yes (per-ruleset) | Yes (per-map) | | Performance | O(n) rules | O(n) + sets | O(1) hashmap | | Flexibility | Low | Medium | Very high | | Maturity | Very high | High | Growing |

When to use each: iptables for simple/legacy systems, nftables for modern Linux servers, eBPF for complex or high-performance requirements.

Question 5: How do you handle existing connections when adding a DENY rule?

Brief Answer: The ESTABLISHED,RELATED rule (which should come first) continues to match existing connections. Options:

  1. Wait: Connection will eventually time out
  2. Kill: Use conntrack -D to delete connection tracking entries
  3. Force: Use ss -K to kill sockets

Security Implication: This is a feature for zero-downtime updates but can be a bug if immediate revocation is required.

Question 6: What happens if you set a DROP policy without an ESTABLISHED rule?

Brief Answer: Disaster. Every reply packet (like TCP SYN-ACK) would be dropped because there’s no rule allowing return traffic. All connections would immediately break, including SSH to the server.

Prevention: Always add ESTABLISHED,RELATED rule BEFORE setting DROP policy.

Question 7: How would you implement micro-segmentation in Kubernetes?

Brief Answer: Use a CNI that supports NetworkPolicy (Calico, Cilium). Define NetworkPolicy resources that specify:

  • Which pods the policy applies to (via labels)
  • Allowed ingress sources
  • Allowed egress destinations

Deeper: Mention that NetworkPolicy is just the API; enforcement happens via iptables/eBPF/ipvs depending on CNI.

Question 8: Why is stateless filtering insufficient for modern security?

Brief Answer: Stateless filtering (checking each packet independently) can’t:

  1. Allow reply packets without also allowing new connections on the same port
  2. Track multi-packet protocols (FTP, SIP)
  3. Detect connection hijacking or injection attacks

Connection tracking (conntrack) adds statefulness, enabling the “ESTABLISHED” rule pattern.

Question 9: What’s the “Default Deny” philosophy and why is it critical?

Brief Answer: Default Deny means all traffic is blocked unless explicitly allowed. This is the opposite of Default Allow (block only known-bad traffic).

Why critical:

  • Attackers use novel ports/protocols - Default Allow misses them
  • Reduces attack surface to only explicitly needed paths
  • Forces conscious decisions about every allowed connection
  • Aligns with Zero Trust “never trust, always verify”

Question 10: How would you debug “connection refused” when you expect traffic to be allowed?

Brief Answer:

  1. Check rule order: iptables -L OUTPUT -n --line-numbers
  2. Check hit counters: Is the ACCEPT rule being matched?
  3. Check conntrack: Is there an existing DENY decision cached?
  4. Check kernel log: Is there a LOG entry for the dropped packet?
  5. Check process UID: ps aux | grep process - is it running as expected user?
  6. Trace with strace: strace -e network -p PID

Resources and Self-Assessment

Books

Topic Book Chapters
Linux Networking “The Linux Programming Interface” by Michael Kerrisk Ch. 58-61 (Sockets, TCP/IP)
Systems Programming “How Linux Works, 3rd Edition” by Brian Ward Ch. 9-10 (Network, Firewall)
Network Internals “Understanding Linux Network Internals” by Christian Benvenuti Ch. 10-12 (Netfilter)
eBPF “BPF Performance Tools” by Brendan Gregg Ch. 1-3 (BPF intro)
Security “Zero Trust Networks” by Gilman & Barth Ch. 5 (Micro-segmentation)
Network Security “Computer Networks, 5th Ed” by Tanenbaum Ch. 8 (Security)

RFCs and Standards

Document Topic
NIST SP 800-207 Zero Trust Architecture
RFC 3704 Ingress Filtering (BCP38)
RFC 2267 Network Ingress Filtering

Tools

Tool Purpose
iptables Legacy packet filtering
nftables Modern packet filtering
bpftool eBPF program management
ss Socket statistics
conntrack Connection tracking
tcpdump Packet capture
wireshark Packet analysis

Self-Assessment Checklist

Before considering this project complete, verify you can:

Conceptual Understanding:

  • Explain the 5 Netfilter hooks and when each is triggered
  • Describe how iptables tables and chains are organized
  • Explain why East-West traffic is often more dangerous than North-South
  • Articulate the “Default Deny” philosophy and its importance
  • Describe how connection tracking enables stateful filtering

Implementation Verification:

  • Your tool parses YAML config and applies rules
  • Rules are applied in correct order (ESTABLISHED first)
  • SSH access is preserved (never lock yourself out)
  • Blocked packets are logged with source UID
  • Rules persist across reboot
  • Tool can flush all rules safely

Security Verification:

  • Allowed users can reach specified ports
  • Denied users cannot reach any ports (Default Deny)
  • Users cannot reach the internet (lateral movement blocked)
  • Existing connections continue working after rule updates
  • New connections respect new rules

Debugging Skills:

  • Can diagnose “connection refused” with iptables counters
  • Can parse kernel log for blocked packets
  • Can identify rule ordering issues
  • Can recover from accidental lockout

Advanced Understanding:

  • Can explain iptables vs nftables vs eBPF tradeoffs
  • Understand how to extend this to containers
  • Know how this relates to Kubernetes NetworkPolicy
  • Can describe integration with PDP for dynamic policies

Next Steps

After completing this project, you have built the Data Plane enforcement for Zero Trust - the component that actually blocks traffic. Your next options:

  1. Project 4: Mutual TLS Mesh - Add cryptographic identity to network traffic. Your micro-segmentation uses UIDs; mTLS adds certificate-based identity that works across hosts.

  2. Project 2: Policy Decision Engine - Build the “brain” that decides what rules to apply. Your current tool uses static YAML; the PDP enables dynamic, context-aware policies.

  3. Project 7: SDP Controller - Extend micro-segmentation to make infrastructure “dark” using Single Packet Authorization. Attackers can’t even see your ports until they prove identity.

Remember: This project teaches the “data plane” of Zero Trust - the actual enforcement of isolation. Combined with identity (Projects 1, 4) and policy (Project 2), you have the complete picture of Zero Trust network security.


Appendix: Minimal Working Example

For testing, here’s a complete minimal implementation:

rules.yaml:

version: "1.0"
settings:
  default_policy: deny
  log_blocked: true

rules:
  - name: "web-to-redis"
    from:
      user: "www-data"
    to:
      port: 6379
      protocol: tcp
    action: allow

apply.sh:

#!/bin/bash
set -e

# Backup
iptables-save > /tmp/backup.rules

# Safety timeout
(sleep 300 && iptables-restore < /tmp/backup.rules) &
TIMEOUT_PID=$!

# Parse config and apply
WEB_UID=$(id -u www-data)

# Flush
iptables -F OUTPUT

# ESTABLISHED (critical!)
iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT

# SSH safety
iptables -A OUTPUT -p tcp --sport 22 -j ACCEPT

# Allow loopback for some system functions
iptables -A OUTPUT -o lo -j ACCEPT

# User rules
iptables -A OUTPUT -m owner --uid-owner $WEB_UID -p tcp --dport 6379 -j ACCEPT

# Log and drop
iptables -A OUTPUT -j LOG --log-prefix "ZT-BLOCKED: "
iptables -A OUTPUT -j DROP

# Set policy
iptables -P OUTPUT DROP

# If we got here, rules work! Kill the timeout
kill $TIMEOUT_PID 2>/dev/null || true

echo "Rules applied successfully!"
iptables -L OUTPUT -v -n

This minimal example demonstrates all core concepts. Expand from here.