This document explains the setup and execution of all the challenges from the Security Engineering lab. The lab was implemented using Docker containers instead of virtual machines to achieve lightweight and scriptable isolation (and because of our toaster computers).
All the code and configurations are available in the repository, hosted on GitHub at this address : https://github.com/ThePension/security-engineering-lab
Made by : Cy02, Cy04, Cy05, Cy06, Cy10
How to run the lab:
-
Clone the repository:
git clone <repository-url>
-
Navigate into the directory:
cd <repository-name>
-
Build the Docker containers:
docker-compose up -d
-
Access the containers:
- Use
docker exec -it <container-name> /bin/bashto access each container. - For example, to access the firewall:
docker exec -it firewall-1 /bin/bash - Use
Simulate a basic network topology with:
- An internal client (
client-1) - A firewall (
firewall-1) bridging two networks - An external client (
external-client) simulating the Internet
Docker provides a lightweight, fast, and scriptable alternative to full virtual machines. Each container:
- Boots in seconds
- Can be assigned to specific networks and IPs
- Uses
iptables/iproute2like any Linux system
This allowed full control over networking while avoiding the overhead of virtualization.
-
internal-net(192.168.102.0/24):client-1: 192.168.102.20firewall-1: 192.168.102.254
-
external-net(10.20.20.0/24):external-client: 10.20.20.100firewall-1: 10.20.20.254
All traffic between internal and external passes through firewall-1.
We initially considered using pfSense as a software firewall:
- pfSense is FreeBSD-based and cannot run in Docker
- It requires systemd, kernel modules, and full virtualization
- Not lightweight or scriptable for this setup
Result: We used a custom Debian container (firewall-1) with full control over iptables or ufw.
Add:
- An
nginx-servercontainer (192.168.102.50) running a web server - Configure
firewall-1to:- Only allow port 80 from external to internal
- Block all other traffic
In firewall-1:
-
iptables:- Enabled IP forwarding
- Configured
MASQUERADEfor NAT
-
ufw:- Default deny all routed traffic
- Allow routed TCP traffic from external to 192.168.102.50:80
- Allow ICMP (ping) for debugging
external-clientcancurl http://192.168.102.50successfully- All other ports or protocols from
external-clientare dropped client-1can ping or reach internal services freely
The objective of Challenge 3 is to redesign the network to introduce a DMZ (Demilitarized Zone) to securely expose public services (e.g., a web server), while isolating internal resources from external clients.
Security Objectives:
- The web server (
nginx-server) must be accessible from both the internal and external networks. - The internal client (
client-1) must be able to access both the external and DMZ networks. - The external client (
external-client) must not be able to access the internal network. - All routing and filtering is performed by the firewall (
firewall-1).
To implement network separation, we introduced a third Docker bridge network for the DMZ. Each container is assigned to its respective network(s), and firewall-1 is the only container bridging all three zones.
| Container | Network(s) | IP Address | Role |
|---|---|---|---|
client-1 |
internal-net |
192.168.102.20 | Internal workstation |
external-client |
external-net |
10.20.20.100 | Simulated WAN attacker |
nginx-server |
dmz-net |
172.16.0.50 | Public web service |
firewall-1 |
all three | 192.168.102.254 10.20.20.254 172.16.0.254 |
Routing and firewall |
| Network | Subnet | Gateway IP | Purpose |
|---|---|---|---|
| internal-net | 192.168.102.0/24 | 192.168.102.254 | Internal network |
| external-net | 10.20.20.0/24 | 10.20.20.254 | Simulated Internet |
| dmz-net | 172.16.0.0/24 | 172.16.0.254 | DMZ for public access |
Each client replaces its default route with the IP of firewall-1 in its respective network.
Firewall logic is implemented using ufw (Uncomplicated Firewall), backed by iptables. IP forwarding is enabled, and NAT is manually configured for outbound traffic. The firewall enforces the following rules:
- Deny all routed traffic
- Allow all outgoing traffic
- Deny all incoming traffic
| Source Network | Destination Network | Rule Type | Action |
|---|---|---|---|
| internal-net | external-net | Route | Allow all |
| internal-net | dmz-net | Route | Allow all |
| external-net | dmz-net (port 80) | Route | Allow HTTP only |
| external-net | internal-net | Route | Deny all |
Additionally, ICMP (ping) from external to internal is blocked explicitly using:
external-clientcan accessnginx-serveron port 80 (HTTP)client-1can accessexternal-clientandnginx-serverexternal-clientcannot accessclient-1(ICMP blocked)- All traffic is routed through
firewall-1
Traffic was confirmed using tcpdump inside the firewall container, validating proper routing and enforcement.
To improve the availability and scalability of the web service hosted in the DMZ, we implemented a load balancing and monitoring setup using HAProxy.
We duplicated the original webserver-1 container to a new container called webserver-2. Both webservers are placed in the DMZ network. A new container dmz-proxy-1 was added to act as a frontend proxy for load balancing HTTP traffic between both backend servers.
We also adjusted the firewall rules in firewall-1 to route HTTP traffic to the proxy instead of a single webserver.
| Container | Role | Network(s) | IP Address |
|---|---|---|---|
| client-1 | Internal Client | internal-net | 192.168.102.20 |
| firewall-1 | Firewall/NAT Router | internal-net, external-net | 192.168.102.1 / 10.20.20.1 |
| external-client | External Attacker | external-net | 10.20.20.100 |
| webserver-1 | DMZ Web Server | dmz-net | 172.16.0.50 |
| webserver-2 | DMZ Web Server | dmz-net | 172.16.0.51 |
| dmz-proxy-1 | HAProxy Load Balancer | dmz-net | 172.16.0.60 |
The following haproxy.cfg file was used in the dmz-proxy-1 container to load balance traffic and expose monitoring on port 8404:
global
log stdout format raw local0
maxconn 100
defaults
log global
mode http
timeout connect 5s
timeout client 30s
timeout server 30s
frontend http_front
bind *:80
default_backend web_servers
backend web_servers
balance roundrobin
server web1 172.16.0.50:80 check
server web2 172.16.0.51:80 check
listen stats
bind *:8404
stats enable
stats uri /stats
stats refresh 5s
stats show-node
stats show-legends
stats auth admin:admin-
frontendhandles incoming traffic on port 80 and forwards to the backend group. -
backendbalances requests using a round-robin strategy. -
listenstats exposes the HAProxy statistics at /stats (port 8404) with basic auth.
On firewall-1, we updated UFW rules to forward external HTTP traffic to the proxy instead of the single webserver.
iptables -t nat -A PREROUTING -i eth1 -p tcp --dport 80 -j DNAT --to-destination 172.16.0.60:80
ufw route allow proto tcp from any to 172.16.0.60 port 80To validate the load balancing mechanism, we created a script in external-client that makes 100 HTTP requests to the proxy and counts how many times each backend responded.
#!/bin/bash
# Run 100 curl requests to the proxy
for i in $(seq 1 100); do
curl -s http://172.16.0.60/
doneWe accessed the HAProxy stats UI via:
- http://172.16.0.60:8404/stats
- Username: admin
- Password: admin
From there, we could confirm:
- Both backend servers are healthy and reachable
- Load was distributed as expected
This concludes the implementation and verification of HAProxy load balancing and monitoring.
The objective of this challenge was to harden the web servers in the DMZ by deploying the ModSecurity Web Application Firewall (WAF) with the OWASP Core Rule Set (CRS). The WAF must block specific malicious patterns, including a custom rule to block requests containing the query parameter ?testparam=test.
To support ModSecurity natively and simplify CRS integration, the two DMZ web servers (webserver-1, webserver-2) were migrated from Nginx to Apache:
The Docker image now uses the apache2 package.
The Apache module mod_security2 is installed and enabled.
ModSecurity was installed using the package libapache2-mod-security2. The following modifications were applied:
Activate ModSecurity:
The recommended config file was renamed:
mv /etc/modsecurity/modsecurity.conf-recommended /etc/modsecurity/modsecurity.confSwitch from Detection to Blocking Mode:
In /etc/modsecurity/modsecurity.conf, the following line:
SecRuleEngine DetectionOnlywas replaced with:
SecRuleEngine OnThis ensures ModSecurity actively blocks malicious requests rather than just logging them.
Custom Rule to Block ?testparam=test:
A new rule was added in /etc/modsecurity/custom-rules.conf:
SecRule ARGS:testparam "@streq test" "id:1000001,deny,status:403,log,msg:'Blocked testparam=test'"This rule explicitly blocks requests that include the parameter testparam=test.
To enhance protection, the latest CRS from OWASP was downloaded and activated:
CRS Installation:
git clone https://github.com/coreruleset/coreruleset.git /opt/owasp-crs
cp /opt/owasp-crs/crs-setup.conf.example /opt/owasp-crs/crs-setup.conf
ln -s /opt/owasp-crs /etc/modsecurity/owasp-crsApache Configuration Updated:
The default CRS loader was disabled to avoid rule ID duplication:
sed -i 's|IncludeOptional /usr/share/modsecurity-crs/\*.load|# Removed default CRS loader|' /etc/apache2/mods-enabled/security2.confThen the CRS was explicitly loaded via:
IncludeOptional /etc/modsecurity/owasp-crs/crs-setup.conf
IncludeOptional /etc/modsecurity/owasp-crs/rules/*.confAfter deploying the updated container, a request to the webserver (dmz reverse proxy) with the query param testparam=test is denied. With another value for this query param, we can successfully access the website (screenshots below).
Due to the complexity of setting up all the infrastructure in Docker instead of using virtual machines, we ran out of time and couldn’t complete all the challenges. Building the network, configuring firewall rules, routing, proxies, and the WAF took a lot of effort to make everything work properly.
However, the lab allowed us to simulate a real-world network with internal, DMZ, and external zones, and to test important concepts like firewalls, load balancing, and web application protection.


