Skip to content

[feature] Introduce DEV_MODE toggle for security feature toggles #635

Description

@nemesifier

Is your feature request related to a problem? Please describe.

There is currently no single DEV_MODE switch. Several security-related settings are either:

  • Too strict for local development: HSTS headers with max-age=31536000, forced HTTPS redirects, strict CSP/CORS, and Django secure-cookie settings make it painful to develop with self-signed certificates or plain HTTP.
  • Too relaxed for production: curl --insecure is hardcoded (not conditionally guarded), DB_SSLMODE=disable by default, POSTFIX_RELAYHOST_TLS_LEVEL=may, weak default credentials are committed, and self-signed certs are the default .env configuration.

Existing per-feature toggles (SSL_CERT_MODE, NGINX_HTTP_ALLOW, NGINX_ADMIN_ALLOW_NETWORK, DEBUG_MODE) are inconsistent and require users to know about each one individually. There is no single environment variable that a developer can set to say "I am running locally, relax the sharp edges."

Describe the solution you'd like

Introduce a single DEV_MODE=true/false environment variable (default false).

When DEV_MODE=true:

  • Disable HSTS (Strict-Transport-Security header removed or set to max-age=0)
  • Allow HTTP access without forced HTTPS redirect
  • Relax Content-Security-Policy to allow local origins
  • Disable Django SESSION_COOKIE_SECURE / CSRF_COOKIE_SECURE / SECURE_REFERRER_POLICY
  • Add --insecure flag to curl calls (or switch to --cacert with a dev CA)
  • Optionally set DEBUG_MODE=True, EMAIL_HOST_TLS=False

Current findings across the repository

Category A: Too strict for development (should be relaxable)

# Location Issue Current value
A1 images/openwisp_nginx/openwisp.ssl.template.conf:25 HSTS header forces HTTPS-only for 1 year Strict-Transport-Security "max-age=31536000" always;
A2 images/openwisp_nginx/openwisp.ssl.template.conf:17-26 Strict security headers + TLS restrictions break localdev HSTS, X-XSS-Protection, X-Content-Type-Options, Permissions-Policy, restrictive Content-Security-Policy, ssl_protocols TLSv1.2 TLSv1.3, strict ciphers
A3 images/openwisp_nginx/openwisp.ssl.80.template.conf:12 Unconditional HTTP-to-HTTPS redirect return 301 https://$host:$NGINX_SSL_PORT$request_uri;
A4 images/openwisp_nginx/openwisp.template.conf:55 HTTP access restricted to NGINX_HTTPS_ALLOWED_IPS, everything else redirected return 301 https://$host$request_uri; inside @deny
A5 images/common/openwisp/settings.py:75-78 Django secure cookies only when HTTPS_SCHEME == "https" (which is any non-False SSL_CERT_MODE, including SelfSigned) SESSION_COOKIE_SECURE = True, CSRF_COOKIE_SECURE = True, SECURE_REFERRER_POLICY = "strict-origin-when-cross-origin"
A6 images/common/openwisp/utils.py:33-38 request_scheme() returns "https" for SelfSigned mode, forcing Django to believe it runs under HTTPS even with self-signed certs if os.environ["SSL_CERT_MODE"] in ["False", ...]: return "http" else return "https"
A7 images/common/openwisp/settings.py:182-187 Celery Redis TLS requires CERT_REQUIRED when REDIS_USE_TLS=True ssl_cert_reqs: ssl.CERT_REQUIRED

Category B: Too relaxed for production (should be guarded)

# Location Issue Current value
B1 images/common/utils.sh:233,241,243,250 curl --insecure hardcoded -- no cert verification on OpenVPN config/checksum/CRL downloads curl --insecure ... (4 occurrences)
B2 .env:32 + images/common/utils.sh:44-61 Default SSL_CERT_MODE=SelfSigned with self-signed cert generation openssl req -x509 ... -days 365 -subj '/CN=OpenWISP'
B3 images/openwisp_base/Dockerfile:116 + others Database SSL verification disabled by default DB_SSLMODE=disable (set in base, openvpn, freeradius Dockerfiles)
B4 images/openwisp_postfix/Dockerfile:30 Postfix relay TLS opportunistic, not enforced POSTFIX_RELAYHOST_TLS_LEVEL=may
B5 images/openwisp_base/Dockerfile:140 + .env:21 Hardcoded weak Django secret key DJANGO_SECRET_KEY defaults, build.py provides change-secret-key but not enforced
B6 .env:16-19 + Dockerfile ENVs Weak default credentials committed DB_PASS=admin, DB_USER=admin, INFLUXDB_PASS=admin, INFLUXDB_USER=admin
B7 images/openwisp_base/Dockerfile:132 SMTP TLS disabled by default EMAIL_HOST_TLS=False
B8 images/openwisp_nginx/Dockerfile:31-32 Admin panel and HTTP access open to all IPs by default NGINX_ADMIN_ALLOW_NETWORK=all, NGINX_HTTPS_ALLOWED_IPS=all

Describe alternatives you've considered

  1. Per-feature env vars (current approach -- NGINX_HSTS_ENABLED, SSL_CERT_MODE, etc.): Works but requires users to know about and set each one individually. No single "make it work locally" switch.
  2. Auto-detection (e.g., detect self-signed cert and relax automatically): Fragile, hard to reason about, and could lead to surprising behavior in production.
  3. Separate Docker Compose override files (docker-compose.dev.yml): Already partially viable but doesn't address the hardcoded --insecure or the Django/nginx-level security headers.

Additional context

There is a work-in-progress branch nginx-secure-header-disable (not merged) that introduces NGINX_HSTS_ENABLED as a per-feature toggle for the HSTS header specifically. This is a proof-of-concept for one aspect of the dev-mode problem but doesn't address the broader set of toggles.

A DEV_MODE approach would:

  • Provide a single entry point for developers (DEV_MODE=true docker compose up)
  • Not change any defaults (production behavior is unchanged)
  • Allow gradual migration: individual features can be refactored to check DEV_MODE one at a time
  • Replace curl --insecure with curl ${CURL_OPTS:-} where CURL_OPTS defaults to empty (strict) in production and --insecure in dev mode
  • Potentially warn or fail on startup if weak credentials are detected in production mode

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Fields

    No fields configured for Feature.

    Projects

    Status
    To do
    Status
    To do

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions