Skip to main content

splent_feature_nginx

Nginx reverse proxy for SPLENT products (config archetype). It ships no Python behaviour of its own: it brings an nginx container that sits in front of the product's web service and answers on the product's HTTP and HTTPS ports.

What it provides

  • Docker:
    • docker/docker-compose.dev.yml, nginx in front of the development server.
    • docker/docker-compose.prod.yml, built from docker/Dockerfile.prod, with the certbot_webroot and letsencrypt volumes for TLS certificates. Both join splent_network, where they reach the product as NGINX_UPSTREAM_HOST:NGINX_UPSTREAM_PORT.
  • Server templates in docker/nginx/templates/, one per environment, rendered by the nginx image at start. Both do the same things:
    • The web container is resolved per request through Docker's DNS (resolver 127.0.0.11 valid=10s, proxy_pass through a variable), so nginx starts before the web container exists, answers 502 while it is away and follows it when it is recreated with another address.
    • Only NGINX_SERVER_NAME reaches the app. A default_server takes every other Host, and requests without one: production closes the connection (444), development answers 421 with a sentence saying what to add.
    • The site's other names, NGINX_REDIRECT_SERVER_NAMES (the bare domain of a www. site), get a 301 to the first name of NGINX_SERVER_NAME, the canonical one, path and query included, and never reach the app.
    • The app is told the host as X-Forwarded-Host: $host, the name the request matched a server by. A trusted terminator chooses the scheme and names the client, but not the host, so the app only ever hears a name of NGINX_SERVER_NAME.
    • A server on 127.0.0.1:8080, inside the container only, answers /healthz with 200, which the compose healthcheck asks.
    • The requests that publish to Zenodo or build a dataset's ZIP get NGINX_LONG_REQUEST_TIMEOUT (600 s), everything else 120 s.
    • /login, /signup, /reset/forgot and /captcha/* are rate limited per client address (NGINX_AUTH_RATE_LIMIT, NGINX_AUTH_RATE_BURST), with 429 above the limit.
    • Every request gets an id, $request_id or the one a trusted proxy sent, which goes to the app and back to the browser as X-Request-ID.
    • The access log uses the splent format: the combined layout with the tokens of the reset, confirmation and sign-up links, the secret query parameters (token, api_key, code, state, ...) and the same in the Referer replaced by ***, then the request id, the peer and the times. nginx's error log has no format, so the request line of a request nginx itself failed (the app down, a 429) is still written there as it came. tests/unit/test_server_templates.py renders both as the image does and checks each of these.
  • docker/nginx/locations/, where a product drops extra location blocks. protected-files.conf.example shows how to serve files the app has authorised through X-Accel-Redirect.
  • An entrypoint script, 15-splent-trusted-proxies.sh, that trusts X-Forwarded-Proto, X-Request-ID and the client in X-Forwarded-For only from NGINX_TRUSTED_PROXY_CIDRS, and another, 16-splent-redirects.sh, that writes the redirect server for NGINX_REDIRECT_SERVER_NAMES and stops the container on a name that is not a host.
  • An nginx entry in the Administration section of the menu registry, for administrators only. It opens the site through the proxy, at NGINX_SERVER_NAME (its first name that is a host) on NGINX_HTTP_HOST_PORT, in a new tab. It needs splent_framework>=1.21.0.
  • For one more minor, the same link in the signed-in sidebar slot (layout.authenticated_sidebar), at localhost as before, for the shells that still draw slots instead of the registry.

Configuration

Env var Default Purpose
NGINX_UPSTREAM_HOST __PRODUCT___web The product's web container, resolved to the product name by product:env --merge and product:build.
NGINX_UPSTREAM_PORT 5000 Port the web container listens on.
NGINX_HTTP_HOST_PORT 80 Host port for HTTP. Gets the product's port offset.
NGINX_HTTPS_HOST_PORT 443 Host port for HTTPS. Gets the product's port offset.
NGINX_SERVER_NAME localhost, asked for in production server_name of the site, several names separated by spaces. The first is the canonical one. Any other Host is refused.
NGINX_REDIRECT_SERVER_NAMES empty Other names of the site, separated by spaces, redirected for good to the first name of NGINX_SERVER_NAME.
NGINX_TRUSTED_PROXY_CIDRS empty Peers allowed to set the forwarded headers and name the client. Empty trusts nobody. Set it only behind a TLS terminator on the host.
NGINX_BIND_HOST 0.0.0.0 Host interface the ports are published on. 127.0.0.1 behind a TLS terminator on the host.
NGINX_AUTH_RATE_LIMIT 60r/m Sustained rate per client address on the account forms.
NGINX_AUTH_RATE_BURST 30 Requests above that rate served at once before the 429s.
NGINX_LONG_REQUEST_TIMEOUT 600s Read timeout of the Zenodo publications and the ZIP downloads. Keep GUNICORN_TIMEOUT a little above it.

docker/.env.prod.example is the production copy of docker/.env.example: it asks for NGINX_SERVER_NAME, since localhost there would refuse the real site, and marks the rest as the operator's.

Behind a TLS terminator

The server listens on HTTP only, so HTTPS comes from a terminator on the host (nginx, Caddy, Traefik) that forwards to NGINX_HTTP_HOST_PORT. Then:

  1. NGINX_BIND_HOST=127.0.0.1, so the terminator is the only way in.
  2. NGINX_TRUSTED_PROXY_CIDRS=172.16.0.0/12, the docker range the terminator arrives from (the peer= field of the access log shows the address; add 192.168.65.0/24 on Docker Desktop).
  3. The terminator keeps the Host it received and sets X-Forwarded-For, X-Forwarded-Proto and, when it has one, X-Request-ID. Whatever X-Forwarded-Host it sends is not passed on: nginx forwards the Host the request matched.
  4. The app keeps SPLENT_TRUSTED_PROXIES=1. nginx is the only hop it talks to: nginx decides whether to believe the terminator and forwards one X-Forwarded-For (the client), one X-Forwarded-Proto and one X-Forwarded-Host. With 2 the app looks for two values in each, finds one, and ignores them: http:// links, cookies without Secure, no HSTS.

When nginx is the edge, leave the first two as they are and keep SPLENT_TRUSTED_PROXIES=1 as well.

One canonical host

A site reached by more than one name should answer by one, so that the links it mails, the addresses a search engine keeps and the OAuth callbacks all name the same host. Put the canonical name in NGINX_SERVER_NAME and the others in NGINX_REDIRECT_SERVER_NAMES:

NGINX_SERVER_NAME=www.hub.example.org
NGINX_REDIRECT_SERVER_NAMES=hub.example.org

The app then only ever sees www.hub.example.org. Behind nginx it should refuse any other Host too (Flask's TRUSTED_HOSTS) and build the links it makes outside a request with SERVER_NAME, which is the product's to set.

Use it in a product

splent feature:add splent_feature_nginx

Metadata

Release files for splent-feature-nginx 1.5.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for splent-feature-nginx 1.5.0
File Size Uploaded Provenance
splent_feature_nginx-1.5.0.tar.gz 21.0 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for splent-feature-nginx 1.5.0
File Interpreter ABI Platform
splent_feature_nginx-1.5.0-py3-none-any.whl Python 3 none any Details

Total release size: 42.8 kB

Release files / splent_feature_nginx-1.5.0.tar.gz

Download URL splent_feature_nginx-1.5.0.tar.gz
Size 21.0 kB
Tags Source
SHA-256 checksum
How to use checksums
6073380fe263bc6c7001282eba0ad6e586deec3a0d74ba7b06db2c8426b1a772
BLAKE2b-256 checksum
How to use checksums
a0194d9377d2f5156d72649f80417ae77495e11806f1aed2e36491bfc0dc7456
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.1.0 CPython/3.13.16

Release files / splent_feature_nginx-1.5.0-py3-none-any.whl

Download URL splent_feature_nginx-1.5.0-py3-none-any.whl
Size 21.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
8d0a295a2d65d3e6c5a7fea4bd9d8e5b8384edc94f7a5ac72722619283f8ab89
BLAKE2b-256 checksum
How to use checksums
1b13df371930886c0133dab9c9891c28a1afcd100de7d0ce28f6de0e685c836a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.1.0 CPython/3.13.16

Security for splent-feature-nginx 1.5.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page