Reverse Proxy Requirement¶
Bifolk must always be deployed behind a reverse proxy (such as Traefik, Nginx, or Caddy). Direct exposure to the internet without a proxy is not a supported configuration.
Target Audience: System administrators, DevOps engineers
Why a Reverse Proxy is Required¶
Bifolk deliberately delegates several security responsibilities to the reverse proxy layer rather than implementing them in Django:
- TLS termination — HTTPS is handled by the proxy, not by the Django application.
- HTTP security headers — Headers such as HSTS, CSP, and
X-Content-Type-Optionsare set by the proxy and cannot be guaranteed if the app is accessed directly. - Endpoint access control — Internal endpoints (such as
/health/) must be blocked from public access at the proxy level.
Danger
Accessing Bifolk directly without a reverse proxy removes all of these protections. The application itself does not enforce security headers.
Required Security Headers¶
Your reverse proxy must set the following headers on all responses:
| Header | Recommended Value |
|---|---|
Strict-Transport-Security |
max-age=31536000; includeSubDomains |
X-Content-Type-Options |
nosniff |
X-Frame-Options |
DENY |
Referrer-Policy |
strict-origin-when-cross-origin |
Content-Security-Policy |
See example below |
Permissions-Policy |
geolocation=(), microphone=(), camera=() |
Content-Security-Policy¶
Bifolk uses Chart.js (from jsDelivr CDN), Leaflet (from unpkg and jsDelivr CDN), Bootstrap, and serves its own static files. A baseline CSP that covers these sources:
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.jsdelivr.net https://unpkg.com;
style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://unpkg.com;
img-src 'self' data: https://*.tile.openstreetmap.org;
font-src 'self';
connect-src 'self';
frame-ancestors 'none';
Note
Test the CSP in report-only mode first (Content-Security-Policy-Report-Only) to catch violations before enforcing it.
Internal-Only Endpoints¶
The following endpoint must not be reachable from the public internet:
| Endpoint | Purpose | Restriction |
|---|---|---|
/health/ |
Container health check (exposes database connectivity status) | Internal/container network only |
Block this endpoint at the Traefik router level or equivalent.
Traefik Configuration Example¶
Security Headers Middleware¶
# traefik/dynamic/middlewares.yml
http:
middlewares:
bifolk-security-headers:
headers:
stsSeconds: 31536000
stsIncludeSubdomains: true
stsPreload: true
forceSTSHeader: true
contentTypeNosniff: true
frameDeny: true
referrerPolicy: "strict-origin-when-cross-origin"
permissionsPolicy: "geolocation=(), microphone=(), camera=()"
customResponseHeaders:
Content-Security-Policy: >-
default-src 'self';
script-src 'self' https://cdn.jsdelivr.net https://unpkg.com;
style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://unpkg.com;
img-src 'self' data: https://*.tile.openstreetmap.org;
font-src 'self';
connect-src 'self';
frame-ancestors 'none';
Blocking the Health Endpoint¶
# traefik/dynamic/routers.yml
http:
routers:
bifolk:
rule: "Host(`yourdomain.com`) && !PathPrefix(`/health/`)"
middlewares:
- bifolk-security-headers
service: bifolk
tls:
certResolver: letsencrypt
Trusted Proxies¶
Configure TRUSTED_PROXIES in your .env to match the Traefik container's IP so that X-Forwarded-For headers are trusted for access logging and rate limiting:
HTTPS Scheme and Email Links¶
When Traefik (or any proxy) terminates TLS, the Django application only sees plain HTTP internally. Without additional configuration, invitation links and other generated URLs will use http:// instead of https://.
Set TRUST_X_FORWARDED_PROTO=True so Django reads the X-Forwarded-Proto header that the proxy forwards:
SITE_DOMAIN sets the public hostname used in invitation emails, allauth emails (e.g. email verification), and QR codes. Without it the hostname falls back to the first entry in DJANGO_ALLOWED_HOSTS, which is often localhost in Docker setups.
Warning
Only enable TRUST_X_FORWARDED_PROTO when you control the reverse proxy and are certain it strips the X-Forwarded-Proto header from untrusted incoming requests. Leaving it enabled without a proxy allows clients to forge the scheme.
See Configuration for details on these settings.
Nginx Configuration Example¶
If you use Nginx as your reverse proxy:
server {
listen 443 ssl;
server_name yourdomain.com;
# TLS
ssl_certificate /etc/ssl/certs/yourdomain.pem;
ssl_certificate_key /etc/ssl/private/yourdomain.key;
# Security headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
# Block internal health endpoint
location /health/ {
deny all;
return 404;
}
location / {
proxy_pass http://bifolk-app: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;
}
}
Media File Serving¶
Bifolk serves user-uploaded media files (profile images, hive photos, and export files) through Django with authentication checks. The reverse proxy must route /media/ requests through the Django application — not serve them directly from the Docker volume.
Danger
If your reverse proxy serves /media/ directly from the storage volume (for example, using an Nginx alias or Traefik serveFile middleware), all authentication controls are bypassed. Anyone who guesses a file URL can download it without logging in.
What Django Enforces for Media¶
| File type | Protection |
|---|---|
| Profile images, hive photos | @login_required — unauthenticated requests are redirected to login |
Export files (/media/exports/) |
@login_required + organization membership check. Non-members receive 404. |
| All media | Path traversal prevention |
Correct Nginx Configuration¶
Route /media/ to the application, not to the volume:
location /media/ {
# Do NOT use alias or root here.
# Route through Django to enforce authentication.
proxy_pass http://bifolk-app:8000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
Correct Traefik Configuration¶
Do not add a serveFile or fileServer rule for /media/. The default proxy_pass to the app container is sufficient and correct.
Verifying Headers¶
After deployment, verify the security headers are present using a browser's developer tools or a command-line check:
You should see Strict-Transport-Security, X-Content-Type-Options, X-Frame-Options, and Referrer-Policy in the response headers.
Online tools such as securityheaders.com can also scan your domain and report missing or misconfigured headers.
Next Steps¶
- Installation - Full installation guide
- Configuration - Environment variable reference including
TRUSTED_PROXIES - Monitoring - Log file locations and access log format