Nginx Proxy Workaround for CSP (Python Apps)¶
Last resort solution for Python applications with unavoidable CSP violations
⚠️ Security Warning
This workaround weakens your application's security posture by removing or modifying Content Security Policy headers. Only use this approach when:
- You have exhausted all CSP-compliant alternatives
- The CSP violation comes from a critical library with no alternatives
- You understand and accept the security trade-offs
This should be your absolute last resort.
When to Use This Workaround¶
✅ Valid Use Cases¶
- Streamlit Applications
- Streamlit's core framework generates inline scripts
- Some Streamlit components violate CSP
-
No CSP-compliant alternative exists for Streamlit
-
Government-Specific Libraries
- Specialized data visualization libraries required for government work
- Libraries mandated by agency policy
-
No alternative libraries available
-
Python Web Frameworks with Inline Scripts
- Dash applications
- Plotly Dash
- Other Python frameworks that inject inline scripts
❌ Do NOT Use If¶
- You're building a Node.js/React/Vue application (these can be CSP-compliant)
- Alternative CSP-compliant libraries exist
- You haven't tried other solutions first
- Application handles sensitive data (reconsider architecture instead)
How It Works¶
The nginx proxy workaround uses a multi-stage Docker build:
┌─────────────┐ ┌──────────┐ ┌─────────────┐
│ Browser │ ───> │ Nginx │ ───> │ Python App │
│ │ │ (Port │ │ (Port 8000)│
│ │ │ 3000) │ │ │
└─────────────┘ └──────────┘ └─────────────┘
│
│ Modifies/removes
│ CSP headers
▼
Key points:
- Nginx listens on port 3000 (configured in
airbase.json) - Python app runs on internal port 8000
- Nginx proxies requests to Python app
- Nginx intercepts responses and removes/modifies CSP headers
- Browser receives responses without strict CSP enforcement
Prerequisites¶
Before proceeding:
- You've confirmed CSP violations exist (checked browser console)
- You've tried CSP-compliant alternatives
- You've searched for updated versions of the problematic library
- You understand the security implications
Implementation Steps¶
Step 1: Modify Your Dockerfile¶
Update your Dockerfile to use a multi-stage build with nginx:
# Stage 1: Python application
FROM gdssingapore/airbase:python-3.13 AS python-app
ENV PYTHONUNBUFFERED=TRUE
COPY --chown=app:app requirements.txt ./
RUN pip install -r requirements.txt
COPY --chown=app:app . ./
# Stage 2: Nginx proxy
FROM gdssingapore/airbase:nginx-1.28
# Copy Python app files
COPY --from=python-app --chown=app:app /app /app
# Copy nginx configuration
COPY --chown=app:app nginx.conf /etc/nginx/conf.d/default.conf
# Install Python in nginx container
USER root
RUN apt-get update && \
apt-get install -y python3 python3-pip python3-venv && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Switch to app user
USER app
# Start both nginx and Python app
CMD ["/bin/sh", "-c", "python3 /app/app.py & nginx -g 'daemon off;'"]
Key changes:
- Multi-stage build: Python app → Nginx container
- Nginx listens on port 3000 (Airbase nginx image default)
- Python app runs on port 8000 internally
- Both services start via CMD
Step 2: Create nginx.conf¶
Create an nginx.conf file in your project root:
# Remove CSP headers from upstream responses
proxy_hide_header Content-Security-Policy;
server {
listen 3000;
server_name _;
# Proxy to Python application
location / {
proxy_pass http://127.0.0.1:8000;
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;
# Remove CSP headers
proxy_hide_header Content-Security-Policy;
proxy_hide_header X-Content-Security-Policy;
# WebSocket support (for Streamlit)
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 86400;
}
}
What this does:
- Removes
Content-Security-Policyheaders from Python app responses - Proxies all traffic to
127.0.0.1:8000(Python app) - Configures WebSocket support for Streamlit
- Nginx listens on port 3000
Step 3: Update Python Application Port¶
Ensure your Python app listens on port 8000 (not 3000):
For Streamlit (app.py):
Run command in Dockerfile CMD:
CMD ["/bin/sh", "-c", "streamlit run /app/app.py --server.port=8000 --server.address=127.0.0.1 & nginx -g 'daemon off;'"]
For Flask:
from flask import Flask
app = Flask(__name__)
if __name__ == '__main__':
app.run(host='127.0.0.1', port=8000)
For FastAPI:
import uvicorn
from fastapi import FastAPI
app = FastAPI()
if __name__ == '__main__':
uvicorn.run(app, host='127.0.0.1', port=8000)
Step 4: Verify airbase.json Configuration¶
Ensure airbase.json specifies port 3000 (nginx port):
Important: The port must be 3000 (where nginx listens), not 8000.
Step 5: Update Project Structure¶
Your project should look like this:
streamlit-app/
├── airbase.json ← Port 3000
├── Dockerfile ← Multi-stage with nginx
├── nginx.conf ← CSP header removal
├── requirements.txt
├── app.py ← Runs on port 8000
└── .env
Complete Working Examples¶
Example 1: Streamlit Dashboard¶
Directory structure:
airbase.json:
{
"framework": "container",
"handle": "data-team/analytics-dashboard",
"port": 3000,
"instanceType": "b.small"
}
requirements.txt:
app.py:
import streamlit as st
import pandas as pd
import plotly.express as px
st.set_page_config(page_title="Analytics Dashboard", layout="wide")
st.title("📊 Analytics Dashboard")
# Sample data
data = pd.DataFrame({
'Month': ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'],
'Revenue': [45000, 52000, 48000, 61000, 58000, 67000],
'Expenses': [32000, 35000, 31000, 42000, 39000, 44000]
})
# Display metrics
col1, col2, col3 = st.columns(3)
col1.metric("Total Revenue", f"${data['Revenue'].sum():,}")
col2.metric("Total Expenses", f"${data['Expenses'].sum():,}")
col3.metric("Net Profit", f"${data['Revenue'].sum() - data['Expenses'].sum():,}")
# Interactive chart
st.subheader("Revenue vs Expenses")
fig = px.line(data, x='Month', y=['Revenue', 'Expenses'],
title='Monthly Trends')
st.plotly_chart(fig, use_container_width=True)
# Data table
st.subheader("Raw Data")
st.dataframe(data, use_container_width=True)
nginx.conf:
proxy_hide_header Content-Security-Policy;
server {
listen 3000;
server_name _;
location / {
proxy_pass http://127.0.0.1:8000;
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_hide_header Content-Security-Policy;
proxy_hide_header X-Content-Security-Policy;
# WebSocket support for Streamlit
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 86400;
}
}
Dockerfile:
# Stage 1: Python dependencies
FROM gdssingapore/airbase:python-3.13 AS python-app
ENV PYTHONUNBUFFERED=TRUE
COPY --chown=app:app requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY --chown=app:app . ./
# Stage 2: Nginx + Python runtime
FROM gdssingapore/airbase:nginx-1.28
# Copy Python app
COPY --from=python-app --chown=app:app /app /app
# Copy nginx config
COPY --chown=app:app nginx.conf /etc/nginx/conf.d/default.conf
# Install Python
USER root
RUN apt-get update && \
apt-get install -y python3 python3-pip && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
USER app
# Start Streamlit and Nginx
CMD ["/bin/sh", "-c", "streamlit run /app/app.py --server.port=8000 --server.address=127.0.0.1 & nginx -g 'daemon off;'"]
Example 2: Plotly Dash Application¶
app.py:
import dash
from dash import html, dcc
import plotly.express as px
import pandas as pd
app = dash.Dash(__name__)
df = pd.DataFrame({
'Category': ['A', 'B', 'C', 'D'],
'Values': [10, 25, 15, 30]
})
fig = px.bar(df, x='Category', y='Values')
app.layout = html.Div([
html.H1('Dashboard'),
dcc.Graph(figure=fig)
])
if __name__ == '__main__':
app.run(host='127.0.0.1', port=8000, debug=False)
Dockerfile:
FROM gdssingapore/airbase:python-3.13 AS python-app
ENV PYTHONUNBUFFERED=TRUE
COPY --chown=app:app requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY --chown=app:app . ./
FROM gdssingapore/airbase:nginx-1.28
COPY --from=python-app --chown=app:app /app /app
COPY --chown=app:app nginx.conf /etc/nginx/conf.d/default.conf
USER root
RUN apt-get update && \
apt-get install -y python3 python3-pip && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
USER app
CMD ["/bin/sh", "-c", "python3 /app/app.py & nginx -g 'daemon off;'"]
Build and Deploy¶
Build¶
Deploy¶
Verify¶
- Visit your application URL
- Open browser console (F12)
- You should see NO CSP errors (headers are removed)
- Verify functionality works
Security Implications¶
What You're Giving Up¶
By removing CSP headers, you lose protection against:
- Cross-Site Scripting (XSS) Attacks
- Malicious scripts can be injected into your app
-
User data can be stolen via injected JavaScript
-
Data Injection Attacks
- Attackers can inject malicious content
-
Forms can be hijacked
-
Clickjacking Protection
- Your app can be embedded in malicious iframes
- Users can be tricked into unwanted actions
Mitigation Strategies¶
Even without CSP, you can improve security:
-
Input Validation
-
Output Encoding
-
Authentication and Authorization
- Implement strong authentication
- Use Airbase IP whitelisting in
airbase.json -
Require SGTS/TechPass login
-
Limited Exposure
- Deploy to staging, not production
- Restrict access via IP whitelisting
-
Use for internal tools only
-
Regular Security Audits
- Monitor application logs
- Review dependencies regularly
- Update libraries promptly
When NOT to Use This¶
Absolutely avoid this workaround if:
- Application handles PII (Personally Identifiable Information)
- Application handles authentication credentials
- Application processes financial data
- Application is public-facing
- Application is production-critical
Consider alternative architecture instead:
- Rebuild with CSP-compliant framework
- Use API backend + static CSP-compliant frontend
- Use alternative libraries
- Request security exception from security team
Troubleshooting¶
Issue: Nginx fails to start¶
Error:
Cause: Port 3000 already in use or nginx config error
Solution:
-
Check nginx.conf syntax:
-
Ensure Python app uses port 8000, not 3000
-
Verify Dockerfile CMD starts both services correctly
Issue: Python app not accessible¶
Symptom: Nginx starts but returns 502 Bad Gateway
Cause: Python app not running or wrong port
Solution:
-
Check Python app listens on 127.0.0.1:8000:
-
Verify both services start in CMD:
-
Check Container logs in Airbase Console for Python errors
Issue: Still seeing CSP errors¶
Symptom: CSP violations still appear in browser console
Cause: CSP headers not being removed properly
Solution:
-
Verify
proxy_hide_headerdirectives in nginx.conf: -
Check if Python app sets CSP headers directly:
-
Clear browser cache and hard reload (Ctrl+Shift+R)
Issue: WebSocket connection fails (Streamlit)¶
Symptom: Streamlit app loads but doesn't update dynamically
Cause: WebSocket proxy not configured
Solution:
Add WebSocket configuration to nginx.conf:
location / {
# ... other proxy settings ...
# WebSocket support
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 86400;
}
Issue: High memory usage¶
Symptom: Application crashes or becomes unresponsive
Cause: Running both nginx and Python in nano instance
Solution:
Upgrade to b.small instance in airbase.json:
Then redeploy:
Alternative Approaches to Consider¶
Before using this workaround, consider these alternatives:
1. Refactor to API + Static Frontend¶
Architecture:
┌──────────────┐ ┌──────────────┐
│ Static │ API │ Python │
│ Frontend │ ─────> │ Backend │
│ (CSP-OK) │ Calls │ (No CSP) │
└──────────────┘ └──────────────┘
- Build CSP-compliant React/Vue frontend
- Python provides API only
- Frontend makes API calls
- Deploy frontend and backend separately
2. Use CSP-Compatible Libraries¶
Instead of Streamlit, consider:
- Build custom Flask app with vanilla JavaScript
- Use React + Plotly for charts
- Use vanilla HTML/CSS/JS
Trade-off: More development time, but better security
3. Request Security Exception¶
For critical government applications:
- Document why CSP workaround is necessary
- Detail security mitigations in place
- Request formal security exception from agency security team
- Implement additional security controls
See Also¶
- How-To: Write CSP-Compliant Code - Try this first
- How-To: Troubleshoot CSP Violations - Identify violations
- How-To: Test CSP Locally - Test before deploying
- Reference: Content Security Policy - CSP specification
- Example: Python Streamlit - Streamlit deployment
- Reference: Dockerfile Requirements - Multi-stage builds
Summary¶
This workaround:
- ✅ Allows Python apps with CSP violations to work on Airbase
- ✅ Uses nginx proxy to remove CSP headers
- ✅ Supports Streamlit, Dash, and other Python frameworks
- ⚠️ Weakens security posture significantly
- ⚠️ Should only be used as last resort
- ⚠️ Requires understanding of security trade-offs
Remember: Always try CSP-compliant alternatives first. Only use this workaround when absolutely necessary and with appropriate security controls in place.