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#
Option 1: nginx Reverse Proxy (Recommended)#
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:
Target: A+ ratingMozilla Observatory:
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:
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:
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:
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#
- OWASP Secure Headers Project
- MDN Web Security
- nginx Security Headers Guide
- Streamlit Deployment Best Practices
- Content Security Policy Reference
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