Skip to content

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-Options are 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:

TRUSTED_PROXIES=172.18.0.2

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=bee.example.org
TRUST_X_FORWARDED_PROTO=True

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:

curl -I https://yourdomain.com

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