Skip to content

Security Headers Deployment Guide#

Purpose: This guide explains how to configure security headers for SousChef when deploying with a reverse proxy (nginx, Apache, or cloud CDN). Security headers protect against common web vulnerabilities like XSS, clickjacking, and MIME type sniffing.


Overview#

SousChef includes a Streamlit UI (souschef/ui/app.py) that should be protected with security headers when deployed to production. Since Streamlit doesn't support custom HTTP headers directly, headers must be configured at the reverse proxy or CDN level.

Required Headers#

Header Value Purpose
X-Frame-Options DENY Prevents clickjacking attacks by disallowing iframe embedding
X-Content-Type-Options nosniff Prevents MIME type sniffing, forces declared content types
Content-Security-Policy default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self'; font-src 'self' data: Restricts resource loading to prevent XSS
Strict-Transport-Security max-age=31536000; includeSubDomains Forces HTTPS connections for 1 year
X-XSS-Protection 1; mode=block Legacy XSS filter (for older browsers)
Referrer-Policy strict-origin-when-cross-origin Controls referrer information leakage
Permissions-Policy geolocation=(), microphone=(), camera=() Restricts browser features

Deployment Options#

Why nginx? Lightweight, high-performance, excellent for reverse proxying Streamlit applications.

Configuration#

Create /etc/nginx/sites-available/souschef:

server {
    listen 443 ssl http2;
    server_name souschef.example.com;

    # SSL Configuration
    ssl_certificate /etc/ssl/certs/souschef.crt;
    ssl_certificate_key /etc/ssl/private/souschef.key;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;

    # Security Headers
    add_header X-Frame-Options "DENY" 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;
    add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;

    # HSTS (Strict-Transport-Security)
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    # Content Security Policy (CSP)
    # Note: Streamlit requires 'unsafe-inline' and 'unsafe-eval' for JavaScript
    add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' ws: wss:; font-src 'self' data:; frame-ancestors 'none'" always;

    # Proxy to Streamlit
    location / {
        proxy_pass http://localhost:8501;
        proxy_http_version 1.1;

        # WebSocket support for Streamlit
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        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;

        # Timeouts
        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
    }

    # Health check endpoint
    location /health {
        access_log off;
        return 200 "OK";
        add_header Content-Type text/plain;
    }
}

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

Enable Configuration#

# Link configuration
sudo ln -s /etc/nginx/sites-available/souschef /etc/nginx/sites-enabled/

# Test configuration
sudo nginx -t

# Reload nginx
sudo systemctl reload nginx

Option 2: Apache Reverse Proxy#

Why Apache? If you're already running Apache for other services, good integration with existing infrastructure.

Configuration#

Enable required Apache modules:

sudo a2enmod proxy
sudo a2enmod proxy_http
sudo a2enmod proxy_wstunnel  # For WebSocket support
sudo a2enmod headers
sudo a2enmod ssl

Create /etc/apache2/sites-available/souschef.conf:

<VirtualHost *:443>
    ServerName souschef.example.com

    # SSL Configuration
    SSLEngine on
    SSLCertificateFile /etc/ssl/certs/souschef.crt
    SSLCertificateKeyFile /etc/ssl/private/souschef.key
    SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1
    SSLCipherSuite HIGH:!aNULL:!MD5
    SSLHonorCipherOrder on

    # Security Headers
    Header always set X-Frame-Options "DENY"
    Header always set X-Content-Type-Options "nosniff"
    Header always set X-XSS-Protection "1; mode=block"
    Header always set Referrer-Policy "strict-origin-when-cross-origin"
    Header always set Permissions-Policy "geolocation=(), microphone=(), camera=()"
    Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
    Header always set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' ws: wss:; font-src 'self' data:; frame-ancestors 'none'"

    # Proxy Configuration
    ProxyPreserveHost On
    ProxyPass "/" "http://localhost:8501/"
    ProxyPassReverse "/" "http://localhost:8501/"

    # WebSocket support
    RewriteEngine On
    RewriteCond %{HTTP:Upgrade} =websocket [NC]
    RewriteRule /(.*)           ws://localhost:8501/$1 [P,L]

    # Logging
    ErrorLog ${APACHE_LOG_DIR}/souschef-error.log
    CustomLog ${APACHE_LOG_DIR}/souschef-access.log combined
</VirtualHost>

# HTTP to HTTPS redirect
<VirtualHost *:80>
    ServerName souschef.example.com
    Redirect permanent / https://souschef.example.com/
</VirtualHost>

Enable Configuration#

# Enable site
sudo a2ensite souschef

# Test configuration
sudo apache2ctl configtest

# Reload Apache
sudo systemctl reload apache2

Option 3: Docker with nginx Sidecar#

Why Docker? Consistent deployment across environments, easy to version control infrastructure.

docker-compose.yml#

version: '3.8'

services:
  souschef:
    build: .
    image: souschef:latest
    container_name: souschef-app
    ports:
      - "8501:8501"
    environment:
      - SOUSCHEF_DEBUG=0
      - SOUSCHEF_DB_HOST=postgres
    networks:
      - souschef-net
    restart: unless-stopped

  nginx:
    image: nginx:alpine
    container_name: souschef-nginx
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./ssl:/etc/ssl/certs:ro
    depends_on:
      - souschef
    networks:
      - souschef-net
    restart: unless-stopped

networks:
  souschef-net:
    driver: bridge

nginx.conf (for Docker)#

events {
    worker_connections 1024;
}

http {
    upstream souschef {
        server souschef-app:8501;
    }

    server {
        listen 80;
        server_name _;
        return 301 https://$host$request_uri;
    }

    server {
        listen 443 ssl http2;
        server_name _;

        ssl_certificate /etc/ssl/certs/cert.pem;
        ssl_certificate_key /etc/ssl/certs/key.pem;
        ssl_protocols TLSv1.2 TLSv1.3;

        # Security Headers (see Option 1 for full list)
        add_header X-Frame-Options "DENY" always;
        add_header X-Content-Type-Options "nosniff" always;
        add_header X-XSS-Protection "1; mode=block" always;
        add_header Strict-Transport-Security "max-age=31536000" always;
        add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' ws: wss:; font-src 'self' data:; frame-ancestors 'none'" always;

        location / {
            proxy_pass http://souschef;
            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;

            # WebSocket support
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
        }
    }
}

Option 4: Cloud CDN (AWS CloudFront, Cloudflare, Azure CDN)#

Why CDN? Global distribution, DDoS protection, managed SSL certificates.

AWS CloudFront Example#

// CloudFront Distribution (Terraform)
resource "aws_cloudfront_distribution" "souschef" {
  enabled = true

  origin {
    domain_name = "souschef-alb.us-east-1.elb.amazonaws.com"
    origin_id   = "souschef-origin"

    custom_origin_config {
      http_port              = 80
      https_port             = 443
      origin_protocol_policy = "https-only"
      origin_ssl_protocols   = ["TLSv1.2"]
    }
  }

  default_cache_behavior {
    allowed_methods  = ["GET", "HEAD", "OPTIONS", "PUT", "POST", "PATCH", "DELETE"]
    cached_methods   = ["GET", "HEAD", "OPTIONS"]
    target_origin_id = "souschef-origin"

    forwarded_values {
      query_string = true
      headers = ["Host", "Origin"]

      cookies {
        forward = "all"
      }
    }

    viewer_protocol_policy = "redirect-to-https"
  }

  # Custom Response Headers (Lambda@Edge function)
  lambda_function_association {
    event_type   = "origin-response"
    lambda_arn   = aws_lambda_function.security_headers.qualified_arn
    include_body = false
  }

  viewer_certificate {
    acm_certificate_arn = aws_acm_certificate.souschef.arn
    ssl_support_method  = "sni-only"
  }
}

// Lambda@Edge for Security Headers
resource "aws_lambda_function" "security_headers" {
  function_name = "souschef-security-headers"
  handler       = "index.handler"
  runtime       = "nodejs18.x"
  role          = aws_iam_role.lambda_edge.arn
  publish       = true

  filename = "security_headers.zip"
}

Lambda@Edge Function (security_headers/index.js)#

exports.handler = async (event) => {
    const response = event.Records[0].cf.response;
    const headers = response.headers;

    headers['x-frame-options'] = [{ key: 'X-Frame-Options', value: 'DENY' }];
    headers['x-content-type-options'] = [{ key: 'X-Content-Type-Options', value: 'nosniff' }];
    headers['x-xss-protection'] = [{ key: 'X-XSS-Protection', value: '1; mode=block' }];
    headers['strict-transport-security'] = [{ 
        key: 'Strict-Transport-Security', 
        value: 'max-age=31536000; includeSubDomains' 
    }];
    headers['content-security-policy'] = [{ 
        key: 'Content-Security-Policy', 
        value: "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' ws: wss:; font-src 'self' data:; frame-ancestors 'none'"
    }];
    headers['referrer-policy'] = [{ 
        key: 'Referrer-Policy', 
        value: 'strict-origin-when-cross-origin' 
    }];

    return response;
};

Verification#

1. Test Security Headers Locally#

# Test nginx configuration
curl -I https://souschef.example.com | grep -E "(X-Frame-Options|X-Content-Type-Options|Strict-Transport-Security)"

# Expected output:
# X-Frame-Options: DENY  
# X-Content-Type-Options: nosniff
# Strict-Transport-Security: max-age=31536000; includeSubDomains

2. Online Security Header Scanners#

Use automated tools to verify header configuration:

SecurityHeaders.com:

https://securityheaders.com/?q=https://souschef.example.com
Target: A+ rating

Mozilla Observatory:

https://observatory.mozilla.org/analyze/souschef.example.com
Target: A rating (90+/100)

3. Manual Browser Testing#

Open browser DevTools (F12) → Network tab → Reload page → Check response headers:

X-Frame-Options: DENY
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Strict-Transport-Security: max-age=31536000; includeSubDomains
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; ...
Referrer-Policy: strict-origin-when-cross-origin

Troubleshooting#

Issue: Streamlit App Doesn't Load#

Symptom: Blank page, console errors: "Refused to execute inline script"

Cause: CSP header too restrictive, Streamlit requires 'unsafe-inline' and 'unsafe-eval'

Fix: Update CSP to allow inline scripts:

Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; ...

Issue: WebSocket Connection Fails#

Symptom: "WebSocket connection failed" in browser console

Cause: nginx not configured for WebSocket proxying

Fix: Add WebSocket headers to nginx config:

proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";

Issue: Headers Not Applied to Static Files#

Symptom: Security scanner reports missing headers on CSS/JS files

Cause: Headers only added to HTML responses

Fix: Add always directive to nginx headers:

add_header X-Frame-Options "DENY" always;


Security Best Practices#

1. SSL/TLS Configuration#

  • Use TLS 1.2+ only: Disable SSLv3, TLS 1.0, TLS 1.1
  • Strong cipher suites: Prefer modern ciphers (AES-GCM, ChaCha20)
  • Certificate management: Use Let's Encrypt or corporate PKI
  • HSTS preload: Consider adding your domain to HSTS preload list

2. CSP Gradual Rollout#

Start with report-only mode to avoid breaking functionality:

# Phase 1: Report violations without blocking
add_header Content-Security-Policy-Report-Only "default-src 'self'; report-uri /csp-report" always;

# Phase 2: After reviewing reports, enforce policy
add_header Content-Security-Policy "default-src 'self'; ..." always;

3. Monitoring#

Log and monitor security header violations:

# nginx: Log CSP violations
location /csp-report {
    access_log /var/log/nginx/csp-violations.log;
    return 204;
}

Compliance Mapping#

Requirement Headers Standard
Prevent clickjacking X-Frame-Options: DENY OWASP A01, CWE-1021
Prevent MIME sniffing X-Content-Type-Options: nosniff OWASP A04, CWE-79
Enforce HTTPS Strict-Transport-Security OWASP A02, CWE-319
Restrict content sources Content-Security-Policy OWASP A03, CWE-79
Limit browser features Permissions-Policy Privacy regulations

References#


Maintenance Checklist#

Quarterly Review: - [ ] Run SecurityHeaders.com scan - [ ] Review CSP violation logs - [ ] Update SSL certificates (if not automated) - [ ] Test WebSocket connectivity - [ ] Verify HSTS is active - [ ] Check for new security header recommendations

After Major Streamlit Upgrades: - [ ] Test CSP compatibility - [ ] Verify WebSocket connections - [ ] Check browser console for errors - [ ] Update CSP if Streamlit adds new resource requirements