← back to writeups
Blue Team

A Six-Segment IDS Lab with Linux Namespaces & Suricata

Introduction

This writeup documents the design, implementation, and testing of a network-monitoring environment built entirely on one host. The goal was a functional lab of six isolated network segments with all traffic routed through a single Suricata sensor before it reaches a target service, and five distinct attacks detected using per-subnet Suricata rules.

Rather than deploying a single service, the target namespace hosts four — HTTP, FTP, SSH, and LDAP — so each source namespace can demonstrate a meaningfully different class of attack and a correspondingly different detection approach. That spread covers application-layer content inspection (SQL injection, anonymous FTP), connection-behaviour analysis (SSH brute force), and LDAP-specific keyword and response-code matching (honeypot enumeration and password spraying).

Every section below follows the same shape: environment setup, Suricata rule development with its rationale, attack execution, and the confirmed alert. Section 4 goes a step further and shows Suricata running inline as an IPS via Linux NFQUEUE, actively dropping traffic rather than only alerting on it.

Section 1 · Network Design & Setup

1.1 Environment

The base is a clean Ubuntu Server VM (a desktop environment was added for convenience but is not required), with the latest version of Suricata installed.

Ubuntu Server VM prepared as the lab host
A clean Ubuntu Server host — the whole lab lives inside it.
Installing the latest Suricata release on the host
Installing the latest Suricata build.

1.2 Architecture overview

The lab is seven Linux network namespaces: five source segments, one IDS segment, and one target. Each source namespace connects to ids through a dedicated veth pair. The ids namespace holds one interface per source subnet plus a separate interface toward target, which makes it the sole forwarding path every packet must traverse.

NamespaceRoleSubnet / address
ns1Source — SQL injection10.1.1.0/24 (host .2)
ns2Source — anonymous FTP10.2.2.0/24 (host .2)
ns3Source — SSH brute force10.3.3.0/24 (host .2)
ns4Source — LDAP honeypot enum10.4.4.0/24 (host .2)
ns5Source — LDAP password spray10.5.5.0/24 (host .2)
idsInline sensor / router.1 on every subnet above + 192.168.100.1
targetVictim services (HTTP/FTP/SSH/LDAP)192.168.100.2/24

1.3 Why Linux namespaces?

Linux network namespaces give complete kernel-level isolation: each namespace has its own interfaces, routing table, firewall rules, and ARP table, with zero shared state between them. Unlike virtual machines there is no hypervisor overhead — and these are the same primitive that Docker and Kubernetes use internally. That makes them ideal for simulating a multi-segment network entirely on one machine.

1.4 Step-by-step creation

1.4.1 Create all seven namespaces

ip netns add creating seven namespaces and ip netns list confirming them
Seven namespaces created and confirmed with ip netns list.

ip netns add registers a new, isolated network namespace with the kernel. At this point each namespace contains only an inactive loopback interface and has no external connectivity.

1.4.2 Create the veth pairs in the root namespace

Creating six veth pairs in the root namespace
Six veth pairs created in the root namespace before being distributed.

A veth (virtual Ethernet) pair behaves like a virtual patch cable — any packet sent into one end immediately appears on the other. Six pairs are created: one for each source-to-IDS link (veth1/ids1 through veth5/ids5) and one for the IDS-to-target link (veth-target/ids-target). All six are created in the root namespace first, then handed out.

1.4.3 Move each interface into its namespace

Moving veth interfaces into their respective namespaces with ip link set netns
Each interface is moved into its namespace with ip link set <iface> netns <ns>.

The move is permanent: afterwards veth1 is only visible inside ns1, ids1 only inside ids, and so on. The ids namespace ends up holding six interfaces — one facing each source subnet and one facing the target.

1.4.4 Assign IP addresses

Assigning a unique /24 subnet to each source namespace
Each source namespace lives on its own unique /24.

Giving every source its own subnet is deliberate: it lets Suricata tell exactly which source a packet came from by inspecting the source IP, which is what makes per-subnet rule sets possible later. The target segment uses 192.168.100.0/24 to keep it clearly distinct from source traffic.

1.4.5 Bring up the source-side interfaces

Bringing up loopback and veth interfaces in the source namespaces; ids addr output still DOWN
Loopback and veth brought up in each source namespace. On the right, the ids side shows its IPs assigned but still DOWN — its interfaces haven't been brought up yet.

Interfaces are DOWN by default after being moved into a namespace, so both the loopback (lo) and the veth interface must be brought up explicitly.

1.4.6 Bring up the IDS-side interfaces

IDS interfaces transitioning to UP with the LOWER_UP flag set
ids1/ids2 transition to BROADCAST,MULTICAST,UP,LOWER_UP — LOWER_UP confirms the veth link is established end to end.

1.4.7 Bring up the target interface

Target interface up with 192.168.100.2 and ids-target peer confirmed
ids-target is UP,LOWER_UP with 192.168.100.1/24, and link-netns target confirms the peer sits in the target namespace.

1.4.8 Add default routes

Adding default routes in each namespace pointing at the ids namespace
Default routes so every segment reaches the others only through ids.

Each source namespace needs a default gateway pointing at ids so that all traffic — including packets for 192.168.100.2 — is forwarded through ids rather than dropped. The target namespace needs a default route via 192.168.100.1 so response packets can get back. Without that return route, connections would be one-way only.

1.4.9 Enable IP forwarding on the IDS and verify

Enabling net.ipv4.ip_forward and a ping showing ttl=63 with zero packet loss
Forwarding enabled, then verified: ttl=63 and 0% loss end to end.

By default Linux drops packets that arrive on one interface but are destined for a different network. Setting net.ipv4.ip_forward=1 turns the ids namespace into a router, letting packets arriving on ids1–ids5 be forwarded out ids-target toward target. This is the step that completes the forwarding path.

A ttl=63 (decremented from 64) confirms the packet was forwarded through one hop — the ids namespace — exactly as intended. 0% packet loss confirms the full bidirectional path ns1 → ids → target → ids → ns1 works.

1.5 Network diagram

Diagram of five source namespaces routing through the ids namespace to the target
Every source namespace must pass through ids — there is no direct path to target.

This inline placement is exactly what lets Suricata both detect and, in the next phase, block traffic.

Section 2 · SQL Injection (ns1)

2.1 Setting up the vulnerable environment

A deliberately vulnerable PHP web application was deployed inside the target namespace (192.168.100.2) to simulate a login service. It authenticates against an SQLite database and concatenates user input straight into the query, so it is vulnerable to SQL injection. It is the target for traffic from ns1.

2.1.1 Enter the target namespace and verify identity

sudo ip netns exec target bash
ip a
Inside the target namespace, ip a shows veth-target with 192.168.100.2/24
veth-target holds 192.168.100.2/24 and is up.

2.1.2 Create the vulnerable PHP application

The page takes user and pass from HTTP GET parameters and inserts them directly into the SQL query — no parameterisation, so it is injectable.

<?php
$db = new SQLite3('users.db');
$username = $_GET['user'];
$password = $_GET['pass'];
$query = "SELECT * FROM users WHERE username='$username' AND password='$password'";
echo "<b>Executing Query:</b><br>";
echo htmlspecialchars($query);
echo "<hr>";
$result = $db->query($query);
if ($result->fetchArray()) {
    echo "Login Successful";
} else {
    echo "Login Failed";
}
?>
The vulnerable PHP login page source
The injectable login page.

2.1.3 Create and populate the SQLite database

sqlite3 users.db
CREATE TABLE users(username TEXT, password TEXT);
INSERT INTO users VALUES('admin','admin123');
INSERT INTO users VALUES('satej','satej123');
INSERT INTO users VALUES('rudra','rudra123');
.quit
Creating the users table and inserting three accounts in SQLite
Three accounts seeded — the data the login page queries against.

2.1.4 Start the PHP built-in web server

php -S 192.168.100.2:80
Starting PHP's built-in web server bound to 192.168.100.2:80
The service listening on 192.168.100.2:80.
Valid credentials return Login Successful, invalid return Login Failed
Baseline behaviour: valid credentials succeed, invalid ones fail — so a later bypass is provably the result of exploitation, not misconfiguration.

2.2 Suricata rule development

A custom rule was added to /etc/suricata/rules/local.rules to inspect HTTP traffic bound for the app and alert on a common injection pattern.

alert http 10.1.1.0/24 any -> 192.168.100.2 80 (
    msg:"NS1 SQL Injection";
    content:"OR";
    sid:100001;
    rev:1;
)
The NS1 SQL injection rule in local.rules
The sid:100001 rule, scoped to the ns1 subnet.

The rule watches HTTP from 10.1.1.0/24 to 192.168.100.2:80 and fires if the string OR appears in the inspected payload. It was then validated before deployment:

sudo suricata -T -c /etc/suricata/suricata.yaml -S /etc/suricata/rules/local.rules
This signature is intentionally simplified for the demo. Matching only the bare string OR would be noisy in production because the keyword appears in plenty of legitimate requests. A robust signature would inspect SQL metacharacters, URL-encoded payloads, and contextual patterns instead.

2.3 IDS deployment and monitoring

Suricata was launched inside the ids namespace on ids1, the interface that receives all ns1 traffic.

sudo ip netns exec ids suricata -i ids1 -S /etc/suricata/rules/local.rules -v
Suricata starting on interface ids1 and beginning live capture
Suricata initialised and monitoring live traffic on ids1.

2.4 Attack execution and detection

2.4.1 Executing the attack

curl "http://192.168.100.2/sqli/?user=admin&pass=%27%20OR%201=1--"
curl sending the URL-encoded OR 1=1 payload from ns1
The payload %27%20OR%201=1-- decodes to ' OR 1=1--.

The injected OR 1=1 makes the WHERE clause always true, so the app returns Login Successful with no valid password — authentication bypassed.

2.4.2 Alert generation

fast.log entry showing the NS1 SQL Injection alert with sid 100001
The alert recorded in fast.log.

The entry shows source 10.1.1.2 (ns1) → 192.168.100.2:80, message NS1 SQL Injection, rule 100001. The attack produced its intended effect (bypass) and tripped the IDS — detection confirmed.

Section 3 · Anonymous FTP Access (ns2)

3.1 Setting up the FTP environment

3.1.1 Install and configure vsftpd

Installing the vsftpd FTP server on the target
vsftpd installed on the target.

The config (/etc/vsftpd.conf) was set to allow anonymous access:

listen=YES
listen_ipv6=NO
anonymous_enable=YES
local_enable=YES
write_enable=YES
anon_root=/srv/ftp
vsftpd.conf with anonymous_enable set to YES and anon_root /srv/ftp
Anonymous access enabled; /srv/ftp presented to anonymous users.

3.1.2 Start the FTP service

/usr/sbin/vsftpd /etc/vsftpd.conf
Starting the vsftpd service
vsftpd running.
Anonymous login succeeding and listing test.txt
Anonymous login works and the test file is visible.

3.2 Suricata rule development

The NS2 FTP access rule in local.rules
The sid:100002 FTP rule.
alert ftp 10.2.2.0/24 any -> 192.168.100.2 21 (
    msg:"NS2 FTP ACCESS";
    ftp.command_data;
    content:"anonymous";
    sid:100002;
    rev:1;
)

The rule inspects FTP control traffic from 10.2.2.0/24 and fires whenever the username anonymous appears during authentication.

3.3 Start Suricata on ids2

Suricata starting on interface ids2
Suricata now inspecting FTP traffic on the ns2 path.

3.4 Detecting the access

Anonymous FTP session from ns2 and the resulting fast.log alert
The anonymous login from ns2 and its matching alert.

The transmitted username matched content:"anonymous"; Suricata logged source 10.2.2.2 (ns2) → 192.168.100.2:21, message NS2 FTP ACCESS, rule 100002 — protocol-aware inspection of the FTP authentication exchange.

Section 4 · SSH Brute Force & Inline Blocking (ns3)

4.1 Setting up the vulnerable environment

4.1.1 Create a test user

sudo ip netns exec target bash
useradd -m satejssh
passwd satejssh
Creating the satejssh user account in the target namespace
A target account for the brute-force simulation.

4.1.2 Configure and start SSH

Password authentication was enabled, and MaxAuthTries was reduced to 1 so each attempt opens a fresh connection (which is what the behaviour-based rule counts).

sshd_config edited to allow password authentication
sshd_config — password auth on.
sshd_config MaxAuthTries set to 1
MaxAuthTries 1.
Starting sshd and verifying login from ns3 with valid credentials
Daemon started and reachability verified from ns3.

4.2 Suricata rule development

The threshold-based NS3 SSH brute force rule
The sid:100003 threshold rule.
alert tcp 10.3.3.0/24 any -> 192.168.100.2 22 (
    msg:"NS3 SSH BRUTE FORCE";
    flags:S;
    flow:to_server;
    threshold:type threshold, track by_src, count 3, seconds 60;
    sid:100003;
    rev:1;
)
SSH encrypts authentication once the connection is up, so Suricata can't read usernames, passwords, or failures directly. Detection therefore relies on connection behaviour: flags:S matches only TCP SYN packets (new-connection attempts), and the threshold fires after three from the same source IP within 60 seconds — enough to flag a brute force while keeping false positives down.

4.3 Start Suricata on ids3

Suricata starting on ids3 with the brute-force rule loaded
Rule loaded, engine up on ids3.

4.4 Detecting the brute force

fast.log entry for NS3 SSH BRUTE FORCE after three attempts
Alert after three attempts inside the window.

Source 10.3.3.2 (ns3) → 192.168.100.2:22, message NS3 SSH BRUTE FORCE, rule 100003. Requiring multiple attempts before alerting is what makes the signature representative of real-world detection.

4.5 NFQUEUE setup for blocking (IPS mode)

With detection working, Suricata was switched to IPS mode using Linux NFQUEUE so it can drop matching traffic. An iptables rule in the ids namespace diverts SSH packets on the forwarding path into the queue:

iptables -I FORWARD -p tcp --dport 22 -j NFQUEUE --queue-num 0
iptables FORWARD rule sending SSH traffic to NFQUEUE queue 0
SSH packets routed into NFQUEUE for inspect-then-verdict handling.

The companion rule uses the drop action rather than alert:

drop tcp 10.3.3.0/24 any -> 192.168.100.2 22 (
    msg:"NS3 SSH BLOCKED";
    flags:S;
    flow:to_server;
    threshold:type threshold, track by_src, count 3, seconds 60;
    sid:200003;
    rev:1;
)
Suricata started in NFQUEUE/IPS mode with the drop rule loaded
Suricata running inline with the blocking rule.
Suricata drop event generated for blocked SSH traffic
A drop event — the packet never reaches the target.
This threshold rule drops the packet that trips it but does not blacklist the host, so later attempts may still get through — in practice roughly every fourth attempt is dropped. For durable blocking, a small script could tail fast.log and dynamically insert an iptables rule to ban the source IP for a set period.

Section 5 · LDAP Honeypot Enumeration (ns4)

5.1 LDAP environment setup

An OpenLDAP service was deployed in the target namespace. LDAP is a good choice here because attackers routinely query it during internal recon to enumerate users, computers, and other directory objects.

Installing OpenLDAP packages on the target
OpenLDAP packages installed.
slapd
ss -tulpn | grep 389
slapd running and listening on port 389
The directory service listening on 389.
A test LDAP query from ns4 succeeding against the target
Connectivity from ns4 confirmed; the default dc=nodomain tree was kept.

5.2 Create the honeypot object

A decoy computer object, FINANCE-PC, was added as bait:

dn: cn=FINANCE-PC,dc=nodomain
objectClass: top
objectClass: device
cn: FINANCE-PC
description: Decoy Workstation
ldapadd -x -D "cn=admin,dc=nodomain" -W -f decoy.ldif
Importing the decoy FINANCE-PC object with ldapadd
The decoy imported into the directory.
ip netns exec ns4 ldapsearch -x -H ldap://192.168.100.2 \
  -b "dc=nodomain" "(cn=FINANCE-PC)"
ldapsearch returning the FINANCE-PC decoy object and its attributes
The decoy is discoverable through normal LDAP enumeration.

5.3 Rule development and deployment

alert tcp 10.4.4.0/24 any -> 192.168.100.2 389 (
    msg:"NS4 LDAP DECOY ENUMERATION";
    content:"FINANCE-PC";
    nocase;
    sid:100004;
    rev:1;
)
The NS4 LDAP decoy enumeration rule
The sid:100004 rule matches the honeypot name in any query.
sudo ip netns exec ids suricata -i ids4 -S /etc/suricata/rules/local.rules -v
Suricata started on interface ids4
Monitoring ns4 traffic on ids4.

5.4 Detecting the enumeration

ldapsearch for FINANCE-PC from ns4 triggering the Suricata match
The search filter carries FINANCE-PC across ids4.
fast.log entry for NS4 LDAP DECOY ENUMERATION
The alert: source 10.4.4.2 → 192.168.100.2:389, rule 100004.
Any touch of a decoy like FINANCE-PC is inherently high-signal: legitimate users have no reason to query a planted reconnaissance trap, so a single hit is worth escalating.

Section 6 · LDAP Password Spray (ns5)

6.1 Create LDAP user accounts

Several accounts — ordinary users and service accounts — were added so a single password could be sprayed across them. An abbreviated view of the LDIF:

dn: ou=People,dc=nodomain
objectClass: organizationalUnit
ou: People

dn: uid=jsmith,ou=People,dc=nodomain
objectClass: inetOrgPerson
cn: John Smith
sn: Smith
uid: jsmith
mail: john.smith@company.local
title: IT Administrator
userPassword: Summer2025!

# ...adoe, rpatel, mjones, finance.svc, backup.svc — all with userPassword: Summer2025!
ldapadd -x -D "cn=admin,dc=nodomain" -W -f users.ldif
Importing the six user accounts with ldapadd
Six accounts imported under ou=People.

6.2 Build the spray

users.txt containing the target usernames
users.txt — the target list.
while read user
do
  ldapwhoami \
    -x \
    -D "uid=$user,ou=People,dc=nodomain" \
    -w Winter2025! \
    -H ldap://192.168.100.2
  sleep 2
done < users.txt
The password-spray bind loop running against each account
One password (Winter2025!) tried against every account — the opposite of a per-account brute force, and a common way to dodge lockout policies.

6.3 Rule development and deployment

alert tcp 192.168.100.2 389 -> 10.5.5.0/24 any (
    msg:"NS5 LDAP Password Spraying Detected";
    ldap.responses.result_code:invalid_credentials;
    threshold:type threshold, track by_dst, count 5, seconds 60;
    sid:100005;
    rev:1;
)
The NS5 LDAP password spraying rule keyed on invalid_credentials responses
The sid:100005 rule watches the server's invalid_credentials responses, not the requests.
sudo ip netns exec ids suricata -i ids5 -S /etc/suricata/rules/local.rules -v
Suricata started on interface ids5
Monitoring ns5 traffic on ids5.

6.4 Detecting the spray

The password spray executing from ns5 against multiple accounts
Repeated failed binds across accounts with the same password.
fast.log entry for NS5 LDAP Password Spraying Detected
Alert once five invalid_credentials responses land inside the window.
Keying on the server's result codes rather than individual login attempts is what lets the rule distinguish a spray — many accounts, same password — from the occasional genuine mistyped login, keeping false positives low.

Conclusion

The lab stood up a complete monitoring environment from nothing but Linux primitives: seven namespaces modelling five isolated source networks, one inspection point, and a target host, with every packet between source and target forced through the ids namespace for centralised monitoring and enforcement.

Five attack scenarios were built and detected, each with a purpose-written, subnet-scoped rule:

  • SQL injection against a vulnerable PHP app — HTTP content inspection.
  • Anonymous FTP access — protocol-aware control-channel matching.
  • SSH brute force — connection-behaviour thresholding, then inline blocking via NFQUEUE.
  • LDAP honeypot enumeration — decoy-object keyword matching.
  • LDAP password spray — server-side response-code thresholding.

Together they show the same sensor covering three detection styles — application-layer content, connection behaviour, and protocol response codes — and the step from passive IDS to active IPS, all on a single host.