Introduction
"To defend, you must first understand the attack. Without knowing how to protect, how can you expect to break through?"
As a beginner learning penetration testing, I’ve realized that defensive thinking is essential even for attackers. When switching roles to a site administrator, the core question becomes: How can I maximize website protection at the lowest possible cost?
So I ran an experiment using CDN, Nginx, SafeLine WAF, and Docker to build a practical and budget-friendly defense stack.
Goal
Use Docker containers for everything and set up a full traffic flow:
User → www.example.com → Cloudflare (CDN)
→ Nginx Proxy Manager (Reverse Proxy)
→ SafeLine WAF (Traffic inspection & protection)
→ Local App (Docker container)
Prerequisites
Install the following components on your server:
Core Components
- Docker + Docker Compose
SafeLine WAF
Developed by Chaitin Tech over a decade, SafeLine is a next-gen WAF with 17.1k stars on GitHub. No need to explain—just results.
🔧 Install Guide
🔗 Web Panel:https://<your-ip>:9443
Nginx Proxy Manager (NPM)
A GUI tool to manage reverse proxies & SSL certs in one place
🌐 Official Site
🔗 Web Panel:http://<your-ip>:81
Step-by-Step Setup
Step 1: Deploy Test Website (Nginx in Docker)
Pull Nginx container:
docker pull nginx
Create configuration folders and files:
(Before deploying the Nginx Docker container, it's important to first create the necessary configuration directories (such as config files, static content, and log folders) on the host machine. These will be mounted into the container at runtime, allowing for flexible control and persistent data management.)
mkdir -p ~/nginx/{conf,html,logs}
cd ~/nginx
Create Nginx config:
vim ~/nginx/conf/nginx.conf
Paste the following:
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
server_tokens off;
log_format main '$remote_addr - $request';
access_log /var/log/nginx/access.log main;
sendfile on;
keepalive_timeout 65;
server {
listen 12080; # Important! Use a custom port (NOT 80) for SafeLine
server_name _;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}
}
Make any necessary changes to the config, and don’t forget to save it.
Upload HTML files to ~/nginx/html/
.
Start the container:
docker run -d --name my-nginx \
-p 12080:12080 \
-v $(pwd)/conf/nginx.conf:/etc/nginx/nginx.conf:ro \
-v $(pwd)/html:/usr/share/nginx/html \
-v $(pwd)/logs:/var/log/nginx \
nginx:alpine
That’s it for the web setup!
Try visiting http://<your-ip>:<your-port>
in your browser.
If the page loads, everything is working as expected.
Step 2: Configure Nginx Proxy Manager (NPM)
SSL Certificate via Cloudflare
- Go to SSL Certificates > Add SSL Certificate
- Enter Domain Names:
*.example.com
- Check Use DNS Challenge
- Choose Cloudflare as provider
- In Credentials File Content, paste your Cloudflare API Token
- Agree to Let's Encrypt TOS
- Click Save
Wait until the wildcard cert is issued.
Add Proxy Host
Go to: Dashboard → Proxy Hosts → Add Proxy Host
-
Domain Names:
www.example.com
-
Scheme:
http
(This is for internal forwarding, not external access) -
Forward Hostname/IP:
172.17.0.1
(Useip addr show docker0
to confirm; most likely it's this) -
Forward Port: Use a random unused port, e.g.,
10080
⚠️ Since the stack is:
NPM → SafeLine → Web Service
,
SafeLine will listen on this random port and forward to Nginx (port 12080).
Here’s how I configured it:
Attach SSL:
- Tab: SSL
- Choose the cert you just created
- Enable Force SSL
- Click Save
Step 3: Configure SafeLine WAF
Go to Applications → Add Application
-
Domain: Your full domain (e.g.
www.example.com
) -
Port: The NPM forwarding port (
10080
) - SSL: Leave unchecked
-
Upstream Server:
http://127.0.0.1:12080
(local Nginx port) - App Name: Any name you like
Click Submit.
Here’s how I configured it:
To properly get the real client IP:
Go to Applications → Advanced Config → Get Attack IP From
Choose "The 2nd Rightmost IP In X-Forwarded-For "
(Since Cloudflare and NPM both act as proxies, the 2nd Rightmost IP is the attacker’s real IP)
Step 4: Secure Ports with iptables
Until now, the Nginx container's port is still exposed. An attacker can bypass your domain and scan open IP:PORT directly. Let’s fix that.
At this stage, both my domain and the exposed IP:port are still accessible from the public internet.
Source Code
Test site source:
🔗 github.com/imbyter/homepage
Understanding iptables Rule Types and Matching Logic
The port restriction via iptables
in this setup can be divided into two categories:
1. Restrictions on Docker-exposed ports:
- Managed via the
FORWARD
chain - Managed via the
DOCKER-USER
chain
2. Restrictions on locally hosted ports:
- Managed via the
INPUT
chain
Rule Matching Logic
-
iptables
rules are matched top to bottom, in the order they are written. - Always add accept rules first for trusted traffic.
- Drop rules should come last as a fallback to block any unmatched packets.
- Use numeric rule positions (e.g.,
-I CHAIN NUM
) to precisely control execution order.
🔒 FORWARD Chain (Docker)
# Allow local host (127.0.0.1) to access port 12080
iptables -I FORWARD 1 -s 127.0.0.1 -p tcp --dport 12080 -j ACCEPT
# Allow traffic from Docker bridge network (default: 172.17.0.0/16)
iptables -I FORWARD 2 -s 172.17.0.0/16 -p tcp --dport 12080 -j ACCEPT
# Drop all other traffic to port 12080
iptables -A FORWARD -p tcp --dport 12080 -j DROP
🔒 DOCKER-USER Chain (Docker)
# Allow local host to access port 12080
iptables -I DOCKER-USER 1 -s 127.0.0.1 -p tcp --dport 12080 -j ACCEPT
# Allow Docker bridge network to access port 12080
iptables -I DOCKER-USER 2 -s 172.17.0.0/16 -p tcp --dport 12080 -j ACCEPT
# Drop all other traffic to port 12080
iptables -A DOCKER-USER -p tcp --dport 12080 -j DROP
🔒 INPUT Chain (Local)
# Allow local host to access port 12080
iptables -I INPUT 1 -s 127.0.0.1 -p tcp --dport 12080 -j ACCEPT
# Allow Docker bridge network to access port 12080
iptables -I INPUT 2 -s 172.17.0.0/16 -p tcp --dport 12080 -j ACCEPT
# Drop all other traffic to port 12080
iptables -A INPUT -p tcp --dport 12080 -j DROP
Testing the Setup
Cloudflare Test
Try pinging your domain:
NPM + iptables Test
- Domain access: ✅ Allowed
- IP + port access: ❌ Blocked
SafeLine WAF Test
- Normal access: ✅ Allowed
- Simulated attack: 🚫 Blocked
Success! Protection is active.
Final Thoughts
With just Docker, Cloudflare, Nginx Proxy Manager, and SafeLine WAF, I’ve built a solid, layered security setup that can handle real-world attacks—all without spending a penny.
If you're running a homelab or building self-hosted services, this setup gives you serious defense on a budget.
Top comments (0)