Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion deploy/orchestrator/orchestrator.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ DV_SECRET_KEY=change-me
DV_DEBUG=false
# Hostnames the UI is served on (comma separated)
DV_ALLOWED_HOSTS=devicevault.example.com
# Full origins, required when the UI is served over HTTPS by a front proxy
# Full origins the UI is served on. Required when the front proxy does not send X-Forwarded-Proto: https
DV_CSRF_TRUSTED_ORIGINS=https://devicevault.example.com
DV_TIMEZONE=Australia/Sydney
DV_LOG_LEVEL=INFO
Expand Down
15 changes: 12 additions & 3 deletions docker/nginx.conf.template
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ map $http_upgrade $connection_upgrade {
'' close;
}

# The scheme the browser used. This server speaks plain HTTP behind a TLS proxy or load
# balancer, which says so in X-Forwarded-Proto; without one, it is this server's own scheme.
# The API builds its absolute URLs (OIDC redirects), its CSRF origin check and Secure cookies
# from it. A client that sends the header itself only changes how its own request is seen.
map $http_x_forwarded_proto $dv_forwarded_proto {
default $scheme;
"~*^https(,|$)" https;
}

server {
listen 8080;
server_name _;
Expand All @@ -26,7 +35,7 @@ server {
proxy_pass $devicevault_api;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto $dv_forwarded_proto;
proxy_set_header X-Forwarded-Host $http_host;
proxy_read_timeout 120s;
}
Expand All @@ -45,7 +54,7 @@ server {
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto $dv_forwarded_proto;
# Flower must not see the session cookie or any API credentials.
proxy_set_header Cookie "";
proxy_set_header Authorization "";
Expand All @@ -64,7 +73,7 @@ server {
proxy_set_header X-Original-URI $request_uri;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto $dv_forwarded_proto;
proxy_set_header X-Forwarded-Host $http_host;
}

Expand Down
11 changes: 11 additions & 0 deletions docs/DEPLOYMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,17 @@ use and `DV_SESSION_MAX_HOURS` after sign-in. Repeated failed sign-ins lock out
username or client address for `DV_LOGIN_LOCKOUT_MIN`; the client address is the
last `X-Forwarded-For` hop, which the `web` service adds.

The TLS proxy must tell the `web` service that the browser used HTTPS by sending
`X-Forwarded-Proto: https`, and pass the `Host` header through unchanged (nginx:
`proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Host $http_host;`;
HAProxy: `http-request set-header X-Forwarded-Proto https if { ssl_fc }`; Traefik,
Caddy and cloud load balancers do both by default). The orchestrator builds its own
URLs from them: the OIDC redirect URI it sends to the identity provider, the origin
its CSRF check expects, and whether the Flower cookie is `Secure`. Without the
header it takes the site to be plain HTTP, so OIDC sign-in is sent back to an
`http://` address, and unsafe requests are refused unless `DV_CSRF_TRUSTED_ORIGINS`
lists the HTTPS origin.

**API tokens** let scripts and monitoring call the API without a session: send
`Authorization: Bearer dvt_...`. A token acts as its owner and never beyond their
grants, narrowed by its scopes: `api` (the whole API, except managing tokens,
Expand Down
22 changes: 22 additions & 0 deletions orchestrator/accounts/tests/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,28 @@ def test_signing_out_ends_the_session(alice):
assert AuditLog.objects.filter(action='auth.logout', actor=alice).exists()


def test_behind_an_https_proxy_the_site_is_https(alice):
"""What the web tier passes on from the TLS proxy (X-Forwarded-Proto) decides the origin and URLs."""
from django.test import RequestFactory

proxied = {'HTTP_X_FORWARDED_PROTO': 'https', 'HTTP_X_FORWARDED_HOST': 'vault.example.com'}
client = APIClient(enforce_csrf_checks=True, **proxied)
client.get('/api/auth/csrf/')
token = client.cookies[settings.CSRF_COOKIE_NAME].value
body = {'username': 'alice', 'password': PASSWORD}

def post(origin):
return client.post('/api/auth/login/', body, format='json', HTTP_X_CSRFTOKEN=token, HTTP_ORIGIN=origin)

assert post('http://vault.example.com').status_code == 403
assert post('https://vault.example.com').status_code == 200

request = RequestFactory().get('/', **proxied)
assert request.build_absolute_uri('/api/auth/oidc/entra/callback/') == (
'https://vault.example.com/api/auth/oidc/entra/callback/'
)


def test_csrf_is_enforced_on_sign_in_and_unsafe_requests(alice):
client = APIClient(enforce_csrf_checks=True)

Expand Down