mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-08 19:26:48 +00:00
feat(settings): WEBVIRTCLOUD_HTTPS enables secure cookies, HTTPS redirect and HSTS
This commit is contained in:
parent
7405217466
commit
c1381318f1
4 changed files with 105 additions and 2 deletions
14
README.md
14
README.md
|
|
@ -394,6 +394,12 @@ When the panel is served over HTTPS on a hostname other than localhost, add it t
|
|||
environment=CSRF_TRUSTED_ORIGINS="https://webvirtcloud.example.com"
|
||||
```
|
||||
|
||||
For a panel served over HTTPS, also set `WEBVIRTCLOUD_HTTPS=1` (same place). It
|
||||
makes the session and CSRF cookies HTTPS-only, redirects HTTP to HTTPS (except
|
||||
`/datasource/`, which VMs fetch over HTTP) and sends HSTS for one year. TLS can
|
||||
end at the bundled nginx or at a proxy in front of it; such a proxy must send
|
||||
`X-Forwarded-Proto: https`.
|
||||
|
||||
## How To Update
|
||||
|
||||
Back up `db.sqlite3` before updating (in Docker it lives in the `data` volume).
|
||||
|
|
@ -428,6 +434,14 @@ requirement. If provisioning cannot write a new private password file, it stops
|
|||
without creating an admin account or printing its password; fix the directory
|
||||
permissions and rerun migrations.
|
||||
|
||||
HTTPS deployments: copy the `SECURE_PROXY_SSL_HEADER` / `WEBVIRTCLOUD_HTTPS` block
|
||||
of `webvirtcloud/settings.py.template` into your `webvirtcloud/settings.py`, set
|
||||
`WEBVIRTCLOUD_HTTPS=1` in the service environment, copy the `map` block and the
|
||||
`X-Forwarded-Proto` line of `conf/nginx/webvirtcloud.conf` into
|
||||
`/etc/nginx/conf.d/webvirtcloud.conf`, and reload nginx
|
||||
(`sudo nginx -t && sudo systemctl reload nginx`). Re-running the installer
|
||||
replaces that file, including any TLS directives you added to it.
|
||||
|
||||
```bash
|
||||
# Go to Installation Directory
|
||||
cd /srv/webvirtcloud
|
||||
|
|
|
|||
|
|
@ -1,3 +1,11 @@
|
|||
# "https" from a TLS-terminating proxy in front of this nginx is kept; otherwise
|
||||
# the scheme of this server is used. A client claiming https over plain HTTP
|
||||
# only changes how its own request is handled (no redirect, secure cookies).
|
||||
map $http_x_forwarded_proto $wvc_forwarded_proto {
|
||||
default $scheme;
|
||||
https https;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
|
||||
|
|
@ -14,7 +22,7 @@ server {
|
|||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Proto $wvc_forwarded_proto;
|
||||
proxy_connect_timeout 1800;
|
||||
proxy_read_timeout 1800;
|
||||
proxy_send_timeout 1800;
|
||||
|
|
|
|||
|
|
@ -35,9 +35,20 @@ CSRF_TRUSTED_ORIGINS = ["http://localhost", "http://127.0.0.1", "http://localhos
|
|||
if extra_origins := os.getenv("CSRF_TRUSTED_ORIGINS"):
|
||||
CSRF_TRUSTED_ORIGINS.extend([origin.strip() for origin in extra_origins.split(",") if origin.strip()])
|
||||
|
||||
# Honor X-Forwarded-Proto header for request.is_secure() when behind a reverse proxy (Nginx, Traefik, etc.)
|
||||
# Honor X-Forwarded-Proto for request.is_secure(). gunicorn listens on
|
||||
# 127.0.0.1 only, so the bundled nginx is the one client that sets it.
|
||||
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
||||
|
||||
# Production profile for a panel served over HTTPS: secure cookies, HTTP to
|
||||
# HTTPS redirect and HSTS. Off by default (plain HTTP).
|
||||
if os.getenv("WEBVIRTCLOUD_HTTPS") == "1":
|
||||
SESSION_COOKIE_SECURE = True
|
||||
CSRF_COOKIE_SECURE = True
|
||||
SECURE_SSL_REDIRECT = True
|
||||
# VMs fetch cloud-init metadata over plain HTTP
|
||||
SECURE_REDIRECT_EXEMPT = [r"^datasource/"]
|
||||
SECURE_HSTS_SECONDS = 31536000
|
||||
|
||||
# Application definition
|
||||
INSTALLED_APPS = [
|
||||
"django.contrib.auth",
|
||||
|
|
|
|||
70
webvirtcloud/test_settings_env.py
Normal file
70
webvirtcloud/test_settings_env.py
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
"""Settings read from the environment. Each case imports the settings in a
|
||||
subprocess, so the environment of one case cannot leak into another."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from django.conf import settings
|
||||
from django.test import TestCase, override_settings
|
||||
|
||||
# Env vars that change the settings; removed from the inherited environment so
|
||||
# every case starts from the defaults.
|
||||
CONFIG_VARS = ["WEBVIRTCLOUD_HTTPS"]
|
||||
|
||||
|
||||
def _env(env):
|
||||
base = {k: v for k, v in os.environ.items() if k not in CONFIG_VARS}
|
||||
return {**base, "DJANGO_SETTINGS_MODULE": "webvirtcloud.settings", **env}
|
||||
|
||||
|
||||
def load_settings(env, names):
|
||||
code = (
|
||||
"import json, django; django.setup(); from django.conf import settings as s; "
|
||||
f"print(json.dumps({{n: getattr(s, n) for n in {names!r}}}, default=str))"
|
||||
)
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-c", code], env=_env(env), cwd=settings.BASE_DIR,
|
||||
capture_output=True, text=True,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise AssertionError(result.stderr)
|
||||
return json.loads(result.stdout)
|
||||
|
||||
|
||||
def run_manage(env, *args):
|
||||
result = subprocess.run(
|
||||
[sys.executable, "manage.py", *args], env=_env(env), cwd=settings.BASE_DIR,
|
||||
capture_output=True, text=True,
|
||||
)
|
||||
output = result.stdout + result.stderr
|
||||
if result.returncode != 0:
|
||||
raise AssertionError(output)
|
||||
return output
|
||||
|
||||
|
||||
# TestCase: the request cases may touch the database (sessions, axes).
|
||||
class HttpsProfileTestCase(TestCase):
|
||||
def test_https_profile_passes_deploy_checks(self):
|
||||
output = run_manage({"WEBVIRTCLOUD_HTTPS": "1"}, "check", "--deploy")
|
||||
for check_id in ("security.W004", "security.W008", "security.W012", "security.W016"):
|
||||
self.assertNotIn(check_id, output)
|
||||
|
||||
def test_plain_http_is_the_default(self):
|
||||
names = ["SESSION_COOKIE_SECURE", "CSRF_COOKIE_SECURE", "SECURE_SSL_REDIRECT", "SECURE_HSTS_SECONDS"]
|
||||
self.assertEqual(load_settings({}, names), dict.fromkeys(names[:3], False) | {"SECURE_HSTS_SECONDS": 0})
|
||||
|
||||
def test_datasource_is_not_redirected(self):
|
||||
exempt = load_settings({"WEBVIRTCLOUD_HTTPS": "1"}, ["SECURE_REDIRECT_EXEMPT"])["SECURE_REDIRECT_EXEMPT"]
|
||||
with override_settings(SECURE_SSL_REDIRECT=True, SECURE_REDIRECT_EXEMPT=exempt):
|
||||
self.assertNotEqual(self.client.get("/datasource/openstack/").status_code, 301)
|
||||
response = self.client.get("/accounts/login/")
|
||||
self.assertEqual(response.status_code, 301)
|
||||
self.assertTrue(response["Location"].startswith("https://"))
|
||||
|
||||
def test_proxied_https_request_is_not_redirected(self):
|
||||
# TLS ends at a proxy, which tells Django through X-Forwarded-Proto
|
||||
with override_settings(SECURE_SSL_REDIRECT=True):
|
||||
response = self.client.get("/accounts/login/", HTTP_X_FORWARDED_PROTO="https")
|
||||
self.assertNotEqual(response.status_code, 301)
|
||||
Loading…
Add table
Add a link
Reference in a new issue