diff --git a/.env.dev.example b/.env.dev.example index ab3ea14..c2eab77 100644 --- a/.env.dev.example +++ b/.env.dev.example @@ -1,3 +1,5 @@ +APP_DOMAIN= +APP_BASE_URL= DJANGO_SECRET_KEY=change-me DJANGO_DEBUG=1 DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1 diff --git a/.env.prod.example b/.env.prod.example index 284485c..5604b8a 100644 --- a/.env.prod.example +++ b/.env.prod.example @@ -1,3 +1,5 @@ +APP_DOMAIN=workdock.example.com +APP_BASE_URL=https://workdock.example.com DJANGO_SECRET_KEY=change-me-long-random-value DJANGO_DEBUG=0 DJANGO_ALLOWED_HOSTS=workdock.example.com diff --git a/.env.test.example b/.env.test.example index e1c216c..b021ffb 100644 --- a/.env.test.example +++ b/.env.test.example @@ -1,3 +1,5 @@ +APP_DOMAIN= +APP_BASE_URL= DJANGO_SECRET_KEY=change-me-long-random-value DJANGO_DEBUG=1 DJANGO_ALLOWED_HOSTS=192.168.2.55,localhost,127.0.0.1 diff --git a/backend/config/settings.py b/backend/config/settings.py index 045551c..a3c70d9 100644 --- a/backend/config/settings.py +++ b/backend/config/settings.py @@ -1,13 +1,32 @@ import os import sys from pathlib import Path +from urllib.parse import urlsplit BASE_DIR = Path(__file__).resolve().parent.parent +def _split_csv_env(name: str, default: str = ''): + return [item.strip() for item in os.getenv(name, default).split(',') if item.strip()] + +def _append_unique(items, value): + if value and value not in items: + items.append(value) + +def _hostname_from_url(url: str) -> str: + try: + return (urlsplit(url).hostname or '').strip() + except ValueError: + return '' + SECRET_KEY = os.getenv('DJANGO_SECRET_KEY', 'unsafe-dev-key') DEBUG = os.getenv('DJANGO_DEBUG', '0') == '1' -ALLOWED_HOSTS = [h.strip() for h in os.getenv('DJANGO_ALLOWED_HOSTS', 'localhost,127.0.0.1').split(',') if h.strip()] -CSRF_TRUSTED_ORIGINS = [o.strip() for o in os.getenv('DJANGO_CSRF_TRUSTED_ORIGINS', '').split(',') if o.strip()] +APP_DOMAIN = os.getenv('APP_DOMAIN', '').strip() +APP_BASE_URL = os.getenv('APP_BASE_URL', '').strip().rstrip('/') +ALLOWED_HOSTS = _split_csv_env('DJANGO_ALLOWED_HOSTS', 'localhost,127.0.0.1') +CSRF_TRUSTED_ORIGINS = _split_csv_env('DJANGO_CSRF_TRUSTED_ORIGINS', '') +_append_unique(ALLOWED_HOSTS, APP_DOMAIN) +_append_unique(ALLOWED_HOSTS, _hostname_from_url(APP_BASE_URL)) +_append_unique(CSRF_TRUSTED_ORIGINS, APP_BASE_URL) # Security hardening SESSION_COOKIE_HTTPONLY = True diff --git a/backend/workflows/admin_config_views.py b/backend/workflows/admin_config_views.py index 91b4ef6..71284b0 100644 --- a/backend/workflows/admin_config_views.py +++ b/backend/workflows/admin_config_views.py @@ -406,5 +406,8 @@ def project_wiki_page_impl(request): def developer_handbook_page_impl(request): return render(request, 'workflows/developer_handbook.html') +def deployment_hosts_page_impl(request): + return render(request, 'workflows/deployment_hosts.html') + def release_checklist_page_impl(request): return render(request, 'workflows/release_checklist.html') diff --git a/backend/workflows/templates/workflows/deployment_hosts.html b/backend/workflows/templates/workflows/deployment_hosts.html new file mode 100644 index 0000000..0ed0232 --- /dev/null +++ b/backend/workflows/templates/workflows/deployment_hosts.html @@ -0,0 +1,78 @@ +{% extends 'workflows/base_shell.html' %} +{% load static i18n %} + +{% block title %}{% trans "Host & Domain Setup" %}{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block shell_body %} + {% include 'workflows/includes/app_header.html' with header_show_home=1 header_inside_shell=1 %} +
{% trans "Reference for configuring hostnames and origins correctly in Django deployments, including how to fix Invalid HTTP_HOST errors." %}
+ +{% trans "Django rejects requests whose Host header is not present in ALLOWED_HOSTS. This validation runs before normal page routing, so a broken hostname cannot show a friendly in-app error page on that same host." %}
+{% trans "Use this guide from a working hostname or IP address to correct the environment configuration." %}
+APP_DOMAIN: {% trans "canonical hostname without scheme, for example" %} workdock.bostame.deAPP_BASE_URL: {% trans "canonical external URL with scheme, for example" %} https://workdock.bostame.deDJANGO_ALLOWED_HOSTS: {% trans "comma-separated hostnames and IPs allowed to reach the app" %}DJANGO_CSRF_TRUSTED_ORIGINS: {% trans "comma-separated origins with scheme for POST and CSRF-safe requests" %}{% trans "The application automatically adds APP_DOMAIN and the hostname from APP_BASE_URL to the effective host allow-list. APP_BASE_URL is also added to trusted CSRF origins." %}
+APP_DOMAIN=workdock.bostame.de
+APP_BASE_URL=https://workdock.bostame.de
+DJANGO_ALLOWED_HOSTS=192.168.2.55,localhost,127.0.0.1
+DJANGO_CSRF_TRUSTED_ORIGINS=http://192.168.2.55:8088,https://workdock.bostame.de
+ APP_DOMAIN=workdock.example.com
+APP_BASE_URL=https://workdock.example.com
+DJANGO_ALLOWED_HOSTS=workdock.example.com
+DJANGO_CSRF_TRUSTED_ORIGINS=https://workdock.example.com
+ {% trans "Production should run with HTTPS, DEBUG disabled, secure cookies enabled, and SSL redirect enabled." %}
+/opt/workdock/.env.testcd /opt/workdock
+nano .env.test
+RUN_DJANGO_CHECK=0 DEPLOY_HEALTH_URL="http://127.0.0.1:8088/healthz/" ./scripts/deploy_stack.sh .env.test docker-compose.prod.yml
+ DJANGO_ALLOWED_HOSTS: {% trans "hostnames only, no scheme" %}DJANGO_CSRF_TRUSTED_ORIGINS: {% trans "must include scheme, for example" %} https://workdock.bostame.deAPP_DOMAIN: canonical hostname without schemeAPP_BASE_URL: canonical external URL including schemeDJANGO_ALLOWED_HOSTS: explicit host/IP allow-listDJANGO_CSRF_TRUSTED_ORIGINS: explicit origin allow-list with schemeAPP_DOMAIN and the hostname from APP_BASE_URL into the effective allowed-host configuration automatically.APP_BASE_URL is also appended to trusted CSRF origins automatically.APP_DOMAIN and APP_BASE_URL as the primary deployment-facing values instead of repeatedly editing long host/origin strings.DJANGO_ALLOWED_HOSTS and, if needed, in DJANGO_CSRF_TRUSTED_ORIGINS./admin-tools/deployment-hosts/.Invalid HTTP_HOST header failure happens before normal page routing, so a broken hostname cannot render a custom error page on that same broken host. Use a working host or IP to access the runbook and fix the env file.develop for the test deployment, main reserved for production.DJANGO_DEBUG=1 in .env.test because the security checks correctly reject insecure cookie settings when DEBUG=0 and the deployment is still plain HTTP. This is acceptable for the internal test box only. Production must run with HTTPS and DEBUG=0.
- docker-compose.prod.ymlweb and hard-refresh browserchardet==5.2.0 is installed in the rebuilt image and restart web/workerapp user..env, not in tracked files.{% trans "Runbook for ALLOWED_HOSTS, CSRF trusted origins, canonical domain variables, and how to resolve Invalid HTTP_HOST errors safely." %}
+