Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
d0ca7d7
Default ALLOWED_HOSTS to localhost in production
avilches Jul 2, 2026
70c4a20
Default chart allowedHosts to localhost so unset deployments fail closed
avilches Jul 2, 2026
94673dc
Merge branch 'main' into security/gateway-allowed-hosts-default
avilches Jul 2, 2026
75b20b3
Fail closed when ALLOWED_HOSTS is unset in production
avilches Jul 2, 2026
356013d
Add ALLOWED_HOSTS settings regression tests
avilches Jul 2, 2026
3c0f87d
Merge remote-tracking branch 'origin/main' into security/gateway-allo…
avilches Jul 3, 2026
c793c08
Merge branch 'main' into security/gateway-allowed-hosts-default
avilches Jul 3, 2026
6c2d090
Merge remote-tracking branch 'origin/main' into security/gateway-allo…
avilches Jul 6, 2026
7c02a6d
Merge branch 'main' into security/gateway-allowed-hosts-default
avilches Jul 6, 2026
bc78100
Merge remote-tracking branch 'origin/main' into security/gateway-allo…
avilches Jul 7, 2026
60976ac
Merge branch 'main' into security/gateway-allowed-hosts-default
avilches Jul 8, 2026
fcf3a74
Merge branch 'main' into security/gateway-allowed-hosts-default
avilches Jul 13, 2026
6aac0b5
Merge branch 'main' into security/gateway-allowed-hosts-default
avilches Jul 14, 2026
edd886b
Merge branch 'main' into security/gateway-allowed-hosts-default
korgan00 Jul 16, 2026
dad93e4
Merge remote-tracking branch 'origin/main' into security/gateway-allo…
avilches Jul 27, 2026
7dbee82
Merge branch 'main' into security/gateway-allowed-hosts-default
avilches Jul 27, 2026
5f54f6f
Merge branch 'main' into security/gateway-allowed-hosts-default
ElePT Aug 18, 2026
72e661c
Merge branch 'main' into security/gateway-allowed-hosts-default
avilches Aug 20, 2026
5e4d240
Merge branch 'main' into security/gateway-allowed-hosts-default
korgan00 Aug 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion charts/qiskit-serverless/charts/gateway/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,10 @@ application:
runtimeApi:
url: "https://quantum.cloud.ibm.com"
cacheTtl: "1"
allowedHosts: "*"
# Set this to the host(s) the gateway serves. It is empty by default so a
# deployment that forgets to set it fails closed (the gateway refuses to boot)
# instead of accepting any Host header. Set "*" only for local development.
allowedHosts: ""
trustedOrigins: "http://localhost"
corsOrigins: "http://localhost"
logsMaximumSize: "52428800" # 50Mb in bytes
Expand Down
2 changes: 2 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ services:
user: "root" # root user is needed to write on volumes
environment:
- DEBUG=0
- ALLOWED_HOSTS=*
# Local-only dev secret. With DEBUG=0 the app fails closed without this.
- DJANGO_SECRET_KEY=django-insecure-local-compose-only-change-me
- RAY_HOST=http://ray-head:8265
Expand Down Expand Up @@ -78,6 +79,7 @@ services:
entrypoint: "./entrypoint-scheduler.sh"
environment:
- DEBUG=0
- ALLOWED_HOSTS=*
# Local-only dev secret. With DEBUG=0 the app fails closed without this.
- DJANGO_SECRET_KEY=django-insecure-local-compose-only-change-me
- DATABASE_HOST=postgres
Expand Down
12 changes: 11 additions & 1 deletion gateway/main/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,17 @@
LOG_FORMAT = "json" if os.environ.get("LOG_FORMAT", "simple") == "json" else "simple"

# It must be a full url without protocol: mydomain.com
ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "*").split(",")
# Accepting any Host ("*") enables Host header attacks (cache poisoning and
# password-reset links pointing at an attacker domain). In production (DEBUG off)
# the process fails closed if ALLOWED_HOSTS is not set, instead of defaulting to
# a value. A hardcoded "*" is only used for local development and tests.
_allowed_hosts = os.environ.get("ALLOWED_HOSTS")
if not _allowed_hosts:
if DEBUG or IS_TEST:
_allowed_hosts = "*"
else:
raise ImproperlyConfigured("ALLOWED_HOSTS environment variable must be set when DEBUG is disabled.")
ALLOWED_HOSTS = _allowed_hosts.split(",")
Comment on lines +65 to +71

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we check if _allowed_hosts is *?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, probably yes 🤔

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I get your point, I guess you're suggesting something like this:

if DEBUG or IS_TEST:
    if not _allowed_hosts:
        _allowed_hosts = "*"
else:
    if not _allowed_hosts:
        raise ImproperlyConfigured("ALLOWED_HOSTS environment variable must be set when DEBUG is disabled.")
    else:
        if _allowed_hosts == "*":
            raise ImproperlyConfigured("ALLOWED_HOSTS environment variable can't be * in production environment")

Right? so it rejects an explicit * with DEBUG/IS_TEST off, which sounds ok... but this is something different of the main purpose of this PR, which it just fails when ALLOWED_HOSTS is missing in production, not checking the content...

If we want validate the content, forbidding * in production, it's more than just check if _allowed_host is *:

  • docker-compose.yaml in this PR sets DEBUG=0 and ALLOWED_HOSTS=* on purpose for the gateway and scheduler services, so that would need to change first (a real host, or drop back to DEBUG=1 there), otherwise this check crashes both containers on startup. Do we want DEBUG=1 or force localhost?
  • The staging/production deployments charts don't set allowedHosts explicitly and currently fallback to the chart's own wildcard default rather than an explicit value, so this check wouldn't catch them as-is.

So, I would keep this PR scoped to "fail closed on missing ALLOWED_HOSTS" as it is right now... and handle "reject a * value in production" PR as a follow-up, because it will require configuring the production/staging environment with the right value first before rejecting the *...


# It must be a full url: https://mydomain.com
CSRF_TRUSTED_ORIGINS = os.environ.get("CSRF_TRUSTED_ORIGINS", "http://localhost").split(",")
Expand Down
34 changes: 34 additions & 0 deletions gateway/tests/main/test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import importlib
import os
import sys
from unittest.mock import patch

import pytest
from django.conf import settings
Expand Down Expand Up @@ -93,6 +94,39 @@ def test_debug_enabled_sets_debug_log_level(monkeypatch):
assert main.settings.LOG_LEVEL == "DEBUG"


def test_allowed_hosts_required_when_debug_off(monkeypatch):
"""Unset ALLOWED_HOSTS with DEBUG off fails closed in production."""
monkeypatch.setenv("DEBUG", "0")
monkeypatch.delenv("ALLOWED_HOSTS", raising=False)

# Hide pytest from sys.modules only for this reload so IS_TEST is False
# and the production guard is actually exercised.
with patch.dict("sys.modules"):
sys.modules.pop("pytest", None)
with pytest.raises(ImproperlyConfigured):
importlib.reload(main.settings)


def test_allowed_hosts_wildcard_when_debug_on(monkeypatch):
"""Unset ALLOWED_HOSTS with DEBUG on defaults to the wildcard."""
monkeypatch.setenv("DEBUG", "1")
monkeypatch.delenv("ALLOWED_HOSTS", raising=False)

importlib.reload(main.settings)

assert main.settings.ALLOWED_HOSTS == ["*"]


def test_allowed_hosts_uses_set_value(monkeypatch):
"""A set ALLOWED_HOSTS value is used as-is, split on commas."""
monkeypatch.setenv("DEBUG", "0")
monkeypatch.setenv("ALLOWED_HOSTS", "example.com")

importlib.reload(main.settings)

assert main.settings.ALLOWED_HOSTS == ["example.com"]


class TestAuthMechanism:
"""Tests for SETTINGS_AUTH_MECHANISM handling in main.settings."""

Expand Down
3 changes: 3 additions & 0 deletions gateway/tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ setenv =
LANGUAGE=en_US
LC_ALL=en_US.utf-8
PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python
# Settings fail closed without ALLOWED_HOSTS when DEBUG is off; lint and import
# checks import the settings module, so give them a local value.
ALLOWED_HOSTS=localhost
# Local-only dev secret so static analysis tools (pylint-django, import-linter)
# can import Django settings, which now fail closed with DEBUG off and no key.
DJANGO_SECRET_KEY=django-insecure-tox-only-change-me
Expand Down
Loading