Skip to main content

Overview

The Nginx redirector provides a critical layer of operational security by:
  • Performing TLS termination to hide backend server fingerprints
  • Filtering traffic based on User-Agent, Content-Type, and HTTP method
  • Serving a fake website for non-beacon requests
  • Spoofing the Server header to impersonate Apache
  • Logging all access attempts for incident response
The redirector is essential for OPSEC. Running the C2 server without Nginx exposes it to fingerprinting and allows blue teams to identify the framework by HTTP response patterns.

Deployment Methods

Two deployment methods are supported:
MethodUse CaseConfiguration
Docker Compose (recommended)Reproducible deploymentnginx_docker.conf, automatic environment
Bare-MetalDirect controlnginx_example.conf, manual systemd setup
This guide focuses on Docker Compose deployment. For bare-metal, see the deployment guide.

Nginx Configuration Files

The redirector uses two Nginx configuration files:

nginx_main.conf

Loads the headers-more module required for Server header spoofing:
load_module modules/ngx_http_headers_more_filter_module.so;

user  nginx;
worker_processes  auto;

error_log  /var/log/nginx/error.log notice;
pid        /var/run/nginx.pid;

events {
    worker_connections  1024;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;
    sendfile      on;
    keepalive_timeout  65;
    include /etc/nginx/conf.d/*.conf;
}

nginx_docker.conf

Defines the C2 server virtual host and traffic filtering rules:
log_format c2_access '$remote_addr "$request" "$http_user_agent" '
                     'req=$request_length status=$status';

server {
    listen 443 ssl;
    server_name c2.lab.internal;

    access_log /var/log/nginx/c2_access.log c2_access;
    error_log  /var/log/nginx/c2_error.log warn;

    # Hide nginx version
    server_tokens off;

    # TLS — same cert and key used by the backend server
    ssl_certificate     /etc/nginx/certs/server.crt;
    ssl_certificate_key /etc/nginx/certs/server.key;

    # Enforce modern TLS only
    ssl_protocols             TLSv1.2 TLSv1.3;
    ssl_ciphers               HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;

    # Suppress nginx Server header and replace with Apache
    more_clear_headers Server;
    more_set_headers 'Server: Apache/2.4.54';

    # HSTS - force HTTPS to prevent downgrade attacks
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    # Block directory traversal attempts
    if ($request_uri ~ "\.\.") {
        return 403;
    }

    # Block common shell metadata paths
    if ($request_uri ~ "(wp-login\.php|wp-admin|admin\.php|login\.php)") {
        return 403;
    }

    # Forward beacon traffic to backend server
    location = /beacon {
        # Only POST is valid — reject everything else at this location
        limit_except POST {
            deny all;
        }

        if ($http_user_agent !~* "Mozilla") {
            return 404;
        }

        if ($content_type != "application/octet-stream") {
            return 404;
        }

        proxy_pass         http://c2-server:8443/beacon;
        proxy_http_version 1.1;

        # Pass real client IP so server logs show agent IP not 127.0.0.1
        proxy_set_header   X-Real-IP        $remote_addr;
        proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;
        proxy_set_header   Host             $host;

        # Forward raw binary payload without buffering or modification
        proxy_request_buffering  off;
        proxy_buffering          off;

        # Match agent request timeout
        proxy_connect_timeout    10s;
        proxy_send_timeout       10s;
        proxy_read_timeout       10s;

        # Pass large payloads — must match MAX_BEACON_SIZE in server_main.py
        client_max_body_size 256k;
    }

    # Return fake normal website for all other paths 
    location / {
        root /var/www/html;
        index index.html;
    }
}

# Redirect plain HTTP to HTTPS
server {
    listen 80;
    server_name c2.lab.internal;
    return 301 https://$host$request_uri;
}

Traffic Filtering Rules

Beacon Endpoint (/beacon)

The /beacon endpoint enforces strict validation:
CheckRuleAction on Failure
HTTP MethodMust be POST404
User-AgentMust contain “Mozilla”404
Content-TypeMust be “application/octet-stream”404
PathMust be exactly /beacon404 or serve fake site
The agent’s User-Agent is set to Mozilla/5.0 to pass the filter. See transport/beacon_transport.py:47 for implementation.

Blocked Paths

Common attack paths are blocked at the Nginx layer:
  • Directory traversal: /../, /../
  • WordPress admin: /wp-login.php, /wp-admin
  • Generic admin: /admin.php, /login.php
These return 403 Forbidden without reaching the backend.

Decoy Website

All requests to paths other than /beacon serve static HTML from /var/www/html. This makes the C2 server appear as a legitimate website to casual scanners.

Docker Nginx Image

The Nginx container is built from redirector/Dockerfile.nginx:
FROM nginx:stable-alpine
RUN apk add --no-cache nginx-mod-http-headers-more
The nginx-mod-http-headers-more module is required for the more_set_headers directive used to spoof the Server header.

Configuration Steps

1

Verify certificate paths

Confirm nginx_docker.conf references Docker-mounted certificate paths:
ssl_certificate     /etc/nginx/certs/server.crt;
ssl_certificate_key /etc/nginx/certs/server.key;
These paths correspond to the Docker Compose volume mount:
volumes:
  - ./certs:/etc/nginx/certs:ro
2

Verify backend proxy target

Confirm nginx_docker.conf uses the Docker service name:
proxy_pass http://c2-server:8443/beacon;
Docker Compose automatically resolves c2-server to the backend container IP on the c2-internal network.
3

Deploy fake website

Place static HTML files in redirector/site/:
mkdir -p redirector/site
cat > redirector/site/index.html <<EOF
<!DOCTYPE html>
<html>
<head><title>Welcome</title></head>
<body><h1>Welcome to our site</h1></body>
</html>
EOF
This directory is mounted into the container at /var/www/html.
4

Start the stack

Deploy both Nginx and the C2 server:
docker compose up -d

Testing the Redirector

Test Beacon Endpoint Returns 400

A 400 response confirms Nginx forwarded the request to the backend:
curl -k --resolve c2.lab.internal:443:127.0.0.1 \
     -X POST https://c2.lab.internal/beacon \
     -H 'Content-Type: application/octet-stream' \
     -d 'test' -o /dev/null -w '%{http_code}\n'
Expected: 400 (backend rejected invalid protocol message)
A 502 means the c2-server container is not running. A 404 means the location block did not match — check the proxy_pass configuration.

Test Invalid User-Agent Returns 404

curl -k --resolve c2.lab.internal:443:127.0.0.1 \
     -X POST https://c2.lab.internal/beacon \
     -H 'Content-Type: application/octet-stream' \
     -H 'User-Agent: curl/7.68.0' \
     -d 'test' -o /dev/null -w '%{http_code}\n'
Expected: 404 (User-Agent does not contain “Mozilla”)

Test Fake Website is Served

curl -k --resolve c2.lab.internal:443:127.0.0.1 \
     https://c2.lab.internal/ -o /dev/null -w '%{http_code}\n'
Expected: 200 (decoy site served)

Test Server Header Spoofing

curl -k --resolve c2.lab.internal:443:127.0.0.1 \
     -I https://c2.lab.internal/
Expected header:
Server: Apache/2.4.54

Test HTTP to HTTPS Redirect

curl -I --resolve c2.lab.internal:80:127.0.0.1 \
     http://c2.lab.internal/
Expected:
HTTP/1.1 301 Moved Permanently
Location: https://c2.lab.internal/

Viewing Nginx Logs

Docker Compose Logs

View live Nginx logs:
docker compose logs -f nginx
View access log only:
docker exec c2-nginx tail -f /var/log/nginx/c2_access.log
View error log only:
docker exec c2-nginx tail -f /var/log/nginx/c2_error.log

Log Format

Access logs use a custom format that includes:
log_format c2_access '$remote_addr "$request" "$http_user_agent" '
                     'req=$request_length status=$status';
Example log entry:
192.168.100.20 "POST /beacon HTTP/1.1" "Mozilla/5.0" req=1024 status=200

Troubleshooting

SymptomCauseFix
502 Bad GatewayBackend not runningStart c2-server before Nginx; check server logs
404 on valid beaconUser-Agent or Content-Type mismatchVerify agent sends Mozilla UA and application/octet-stream
403 on all requestsIP blocked by allow/deny rulesRemove IP restrictions in nginx_docker.conf
nginx: [emerg] module ... not foundheaders-more module missingRebuild nginx image with Dockerfile.nginx
Permission denied on cert keyWrong volume mount permissionsRun chmod 640 certs/server.key
Server header shows nginxheaders-more module not loadedCheck nginx_main.conf includes load_module

Security Best Practices

  • Never expose port 8443 to the host network — only Nginx should be reachable
  • Always use TLS 1.2 or higher — disable TLS 1.0 and 1.1
  • Rotate TLS certificates regularly — self-signed certs should be regenerated every 90 days
  • Monitor access logs for anomalies — unexpected IPs or scanning patterns indicate detection
  • Use domain fronting in production — resolve c2.lab.internal to a CDN or legitimate domain

Advanced Configuration

Custom User-Agent Filtering

Modify the User-Agent check to match your agent’s custom header:
if ($http_user_agent !~* "YourCustomUA") {
    return 404;
}

IP Whitelisting

Restrict beacon endpoint to known agent IPs:
location = /beacon {
    allow 192.168.100.0/24;
    deny all;
    
    # ... rest of config
}

Rate Limiting

Prevent brute-force attacks:
http {
    limit_req_zone $binary_remote_addr zone=beacon_limit:10m rate=10r/m;
}

server {
    location = /beacon {
        limit_req zone=beacon_limit burst=5;
        # ... rest of config
    }
}

Next Steps

Build docs developers (and LLMs) love