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
- 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.
- Auto-detection (e.g., detect self-signed cert and relax automatically): Fragile, hard to reason about, and could lead to surprising behavior in production.
- 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
Is your feature request related to a problem? Please describe.
There is currently no single
DEV_MODEswitch. Several security-related settings are either: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.curl --insecureis hardcoded (not conditionally guarded),DB_SSLMODE=disableby default,POSTFIX_RELAYHOST_TLS_LEVEL=may, weak default credentials are committed, and self-signed certs are the default.envconfiguration.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/falseenvironment variable (defaultfalse).When
DEV_MODE=true:Strict-Transport-Securityheader removed or set tomax-age=0)Content-Security-Policyto allow local originsSESSION_COOKIE_SECURE/CSRF_COOKIE_SECURE/SECURE_REFERRER_POLICY--insecureflag tocurlcalls (or switch to--cacertwith a dev CA)DEBUG_MODE=True,EMAIL_HOST_TLS=FalseCurrent findings across the repository
Category A: Too strict for development (should be relaxable)
images/openwisp_nginx/openwisp.ssl.template.conf:25Strict-Transport-Security "max-age=31536000" always;images/openwisp_nginx/openwisp.ssl.template.conf:17-26X-XSS-Protection,X-Content-Type-Options,Permissions-Policy, restrictiveContent-Security-Policy,ssl_protocols TLSv1.2 TLSv1.3, strict ciphersimages/openwisp_nginx/openwisp.ssl.80.template.conf:12return 301 https://$host:$NGINX_SSL_PORT$request_uri;images/openwisp_nginx/openwisp.template.conf:55NGINX_HTTPS_ALLOWED_IPS, everything else redirectedreturn 301 https://$host$request_uri;inside@denyimages/common/openwisp/settings.py:75-78HTTPS_SCHEME == "https"(which is any non-FalseSSL_CERT_MODE, including SelfSigned)SESSION_COOKIE_SECURE = True,CSRF_COOKIE_SECURE = True,SECURE_REFERRER_POLICY = "strict-origin-when-cross-origin"images/common/openwisp/utils.py:33-38request_scheme()returns"https"for SelfSigned mode, forcing Django to believe it runs under HTTPS even with self-signed certsif os.environ["SSL_CERT_MODE"] in ["False", ...]: return "http"else return"https"images/common/openwisp/settings.py:182-187CERT_REQUIREDwhenREDIS_USE_TLS=Truessl_cert_reqs: ssl.CERT_REQUIREDCategory B: Too relaxed for production (should be guarded)
images/common/utils.sh:233,241,243,250curl --insecurehardcoded -- no cert verification on OpenVPN config/checksum/CRL downloadscurl --insecure ...(4 occurrences).env:32+images/common/utils.sh:44-61SSL_CERT_MODE=SelfSignedwith self-signed cert generationopenssl req -x509 ... -days 365 -subj '/CN=OpenWISP'images/openwisp_base/Dockerfile:116+ othersDB_SSLMODE=disable(set in base, openvpn, freeradius Dockerfiles)images/openwisp_postfix/Dockerfile:30POSTFIX_RELAYHOST_TLS_LEVEL=mayimages/openwisp_base/Dockerfile:140+.env:21DJANGO_SECRET_KEYdefaults,build.pyprovideschange-secret-keybut not enforced.env:16-19+DockerfileENVsDB_PASS=admin,DB_USER=admin,INFLUXDB_PASS=admin,INFLUXDB_USER=adminimages/openwisp_base/Dockerfile:132EMAIL_HOST_TLS=Falseimages/openwisp_nginx/Dockerfile:31-32NGINX_ADMIN_ALLOW_NETWORK=all,NGINX_HTTPS_ALLOWED_IPS=allDescribe alternatives you've considered
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.docker-compose.dev.yml): Already partially viable but doesn't address the hardcoded--insecureor the Django/nginx-level security headers.Additional context
There is a work-in-progress branch
nginx-secure-header-disable(not merged) that introducesNGINX_HSTS_ENABLEDas 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_MODEapproach would:DEV_MODE=true docker compose up)DEV_MODEone at a timecurl --insecurewithcurl ${CURL_OPTS:-}whereCURL_OPTSdefaults to empty (strict) in production and--insecurein dev mode