Skip to main content
Running Heimdall behind a reverse proxy provides TLS termination, load balancing, and additional security layers.

Nginx Configuration

Basic Setup

1

Install Nginx

sudo apt update
sudo apt install nginx
2

Create site configuration

sudo nano /etc/nginx/sites-available/heimdall
3

Enable site

sudo ln -s /etc/nginx/sites-available/heimdall /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Production Configuration

# /etc/nginx/sites-available/heimdall

upstream heimdall {
    server 127.0.0.1:8080 max_fails=3 fail_timeout=30s;
    keepalive 32;
}

server {
    listen 80;
    server_name heimdall.example.com;
    
    # Redirect HTTP to HTTPS
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name heimdall.example.com;
    
    # TLS Configuration
    ssl_certificate /etc/ssl/certs/heimdall.pem;
    ssl_certificate_key /etc/ssl/private/heimdall.key;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384';
    ssl_prefer_server_ciphers off;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;
    ssl_stapling on;
    ssl_stapling_verify on;
    
    # Security Headers
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    
    # Logging
    access_log /var/log/nginx/heimdall_access.log;
    error_log /var/log/nginx/heimdall_error.log;
    
    # Rate Limiting
    limit_req_zone $binary_remote_addr zone=heimdall_limit:10m rate=10r/s;
    limit_req zone=heimdall_limit burst=20 nodelay;
    
    # Max Upload Size (for repository zip uploads)
    client_max_body_size 500M;
    
    # General Proxy Settings
    location / {
        proxy_pass http://heimdall;
        proxy_http_version 1.1;
        
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        
        proxy_set_header Connection "";
        proxy_buffering off;
        proxy_request_buffering off;
        
        # Timeouts
        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
    }
    
    # SSE (Server-Sent Events) - Critical for scan progress
    location /api/scans/ {
        proxy_pass http://heimdall;
        proxy_http_version 1.1;
        
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        
        # SSE requires these settings
        proxy_set_header Connection "";
        proxy_buffering off;
        proxy_cache off;
        proxy_read_timeout 3600s;
        
        # Disable compression for SSE
        gzip off;
        
        # Chunked transfer encoding
        chunked_transfer_encoding on;
    }
    
    # Health Check
    location /health {
        proxy_pass http://heimdall;
        access_log off;
    }
}
Critical: SSE (Server-Sent Events) endpoints require proxy_buffering off and proxy_cache off to work correctly. Without these settings, scan progress updates will not stream in real-time.

TLS/SSL Configuration

Obtaining Certificates

SSL Best Practices

1

Use strong protocols

ssl_protocols TLSv1.2 TLSv1.3;
Disable TLSv1.0 and TLSv1.1 — they are deprecated and insecure.
2

Configure secure ciphers

ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384';
ssl_prefer_server_ciphers off;
3

Enable HSTS

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
4

Enable OCSP stapling

ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
Test your SSL configuration:
# SSL Labs test
open https://www.ssllabs.com/ssltest/analyze.html?d=heimdall.example.com

# Local test
openssl s_client -connect heimdall.example.com:443 -tls1_3

SSE (Server-Sent Events) Configuration

Heimdall uses SSE for real-time scan progress updates. Proper proxy configuration is critical.

Why SSE Requires Special Configuration

SSE streams data incrementally over a long-lived HTTP connection. Default proxy buffering breaks this behavior.

Required Nginx Settings

location /api/scans/ {
    # Disable buffering (critical)
    proxy_buffering off;
    proxy_cache off;
    
    # Long timeout for streaming
    proxy_read_timeout 3600s;
    
    # Disable compression
    gzip off;
    
    # Enable chunked transfer
    chunked_transfer_encoding on;
    
    proxy_pass http://heimdall;
}

Testing SSE

# Test SSE endpoint
curl -N -H "Accept: text/event-stream" \
  https://heimdall.example.com/api/scans/123/progress/stream

# Should output streaming events:
# event: scan_update
# data: {"stage":"ingest","status":"running"}
#
# event: scan_update
# data: {"stage":"tyr","status":"completed"}
If you see buffered output or timeouts, check:
  • proxy_buffering off is set
  • proxy_cache off is set
  • No intermediate proxies or CDNs are buffering

Security Hardening

Rate Limiting

Protect against abuse:
# Define rate limit zones
http {
    limit_req_zone $binary_remote_addr zone=heimdall_limit:10m rate=10r/s;
    limit_req_zone $binary_remote_addr zone=heimdall_login:10m rate=5r/m;
    
    server {
        # General rate limit
        limit_req zone=heimdall_limit burst=20 nodelay;
        
        # Stricter limit for auth endpoints
        location ~ ^/api/auth/(login|register) {
            limit_req zone=heimdall_login burst=3 nodelay;
            proxy_pass http://heimdall;
        }
    }
}

IP Whitelisting

Restrict access to specific IPs:
# Allow specific IPs
allow 192.168.1.0/24;
allow 10.0.0.0/8;
deny all;

Block Malicious Bots

# Block common bot patterns
if ($http_user_agent ~* (bot|crawler|spider|scraper)) {
    return 403;
}

DDoS Protection

# Connection limiting
limit_conn_zone $binary_remote_addr zone=addr:10m;
limit_conn addr 10;

# Request body size
client_body_buffer_size 128k;
client_max_body_size 500M;  # For zip uploads

# Timeouts
client_body_timeout 30s;
client_header_timeout 30s;
send_timeout 30s;

Load Balancing

For high availability, run multiple Heimdall instances:
upstream heimdall {
    least_conn;  # Use least connections algorithm
    
    server 127.0.0.1:8080 max_fails=3 fail_timeout=30s;
    server 127.0.0.1:8081 max_fails=3 fail_timeout=30s;
    server 127.0.0.1:8082 max_fails=3 fail_timeout=30s;
    
    keepalive 32;
}

server {
    location / {
        proxy_pass http://heimdall;
        
        # Health checks
        proxy_next_upstream error timeout invalid_header http_500 http_502 http_503;
        proxy_next_upstream_tries 2;
    }
}
When running multiple instances, ensure they share the same database and filesystem for repository storage.

Health Checks

Monitor backend health:
location /health {
    proxy_pass http://heimdall;
    access_log off;
    
    # Return 503 if backend is down
    proxy_intercept_errors on;
    error_page 502 503 504 = @healthcheck_failed;
}

location @healthcheck_failed {
    return 503 "Heimdall is currently unavailable";
}
Integrate with monitoring:
# Prometheus blackbox exporter
- job_name: 'heimdall'
  metrics_path: /probe
  params:
    module: [http_2xx]
  static_configs:
    - targets:
      - https://heimdall.example.com/health

Logging and Monitoring

Access Logs

# Custom log format
log_format heimdall_log '$remote_addr - $remote_user [$time_local] '
                        '"$request" $status $body_bytes_sent '
                        '"$http_referer" "$http_user_agent" '
                        'rt=$request_time uct="$upstream_connect_time" '
                        'uht="$upstream_header_time" urt="$upstream_response_time"';

access_log /var/log/nginx/heimdall_access.log heimdall_log;

Error Logs

error_log /var/log/nginx/heimdall_error.log warn;

Log Rotation

# /etc/logrotate.d/nginx
/var/log/nginx/*.log {
    daily
    missingok
    rotate 14
    compress
    delaycompress
    notifempty
    create 0640 www-data adm
    sharedscripts
    postrotate
        [ -f /var/run/nginx.pid ] && kill -USR1 `cat /var/run/nginx.pid`
    endscript
}

Troubleshooting

SSE Not Working

1

Check proxy buffering

# Must be off for SSE
proxy_buffering off;
proxy_cache off;
2

Check timeouts

# Must be long enough for scan duration
proxy_read_timeout 3600s;
3

Test directly

# Bypass Nginx
curl -N http://localhost:8080/api/scans/123/progress/stream

# Through Nginx
curl -N https://heimdall.example.com/api/scans/123/progress/stream

502 Bad Gateway

# Check Heimdall is running
sudo systemctl status heimdall
curl http://localhost:8080/health

# Check Nginx error logs
sudo tail -f /var/log/nginx/error.log

# Test upstream
curl -v http://127.0.0.1:8080/health

SSL Certificate Errors

# Verify certificate
openssl x509 -in /etc/ssl/certs/heimdall.pem -text -noout

# Check certificate chain
openssl s_client -connect heimdall.example.com:443 -showcerts

# Test SSL configuration
nginx -t

High Load

# Check Nginx status
sudo systemctl status nginx

# Check worker processes
ps aux | grep nginx

# Monitor connections
ss -s
netstat -an | grep :443 | wc -l

Alternative: Caddy

Caddy provides automatic HTTPS and simpler configuration:
# Caddyfile

heimdall.example.com {
    reverse_proxy localhost:8080 {
        # SSE support (automatic)
        flush_interval -1
    }
    
    # Security headers (automatic)
    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains"
        X-Frame-Options "SAMEORIGIN"
        X-Content-Type-Options "nosniff"
    }
    
    # Rate limiting
    rate_limit {
        zone dynamic {
            key {remote_host}
            events 10
            window 1s
        }
    }
}
Run Caddy:
sudo caddy run --config /etc/caddy/Caddyfile

Next Steps

Production Deployment

Complete production deployment guide

Docker Deployment

Docker and Docker Compose setup

Build docs developers (and LLMs) love