Guides: deploy & operate

How to serve TLS

On this page

This guide shows you how to put HTTPS in front of DeltaGlider Proxy. You can terminate TLS at the proxy itself or at a reverse proxy in front of it. The guide also shows how to avoid the reverse-proxy timeout that breaks large uploads.

S3 clients expect HTTPS. The UI (/_/*) and the S3 API (/) share one listener, so whichever option you pick, route the whole host. Do not write per-path rules.

Option A: terminate TLS at the proxy#

If you run the proxy directly on the edge, enable native TLS with your PEM pair. The proxy binds its listener once at startup, so a TLS change takes effect only after a restart.

In the admin UI:

  1. In the sidebar, open System → System (/_/admin/system).

  2. In the TLS card, turn on Enable TLS.

    The TLS card of the System page with TLS off; callout 2 marks the Enable TLS switch.The TLS card of the System page with TLS off; callout 2 marks the Enable TLS switch.

  3. Type the path of your certificate in Certificate path and the path of its key in Private key path. Both files must be PEM files that the proxy process can read. If you leave both fields empty, the proxy generates a self-signed certificate at startup. That certificate is fine for testing, but clients that verify certificates refuse it.

  4. Click Review & apply in the bar above the cards.

    TLS is on and both certificate paths are filled in; callout 3 marks the Certificate path and Private key path fields, and callout 4 marks Review & apply in the bar above the cards.TLS is on and both certificate paths are filled in; callout 3 marks the Certificate path and Private key path fields, and callout 4 marks Review & apply in the bar above the cards.

  5. Check the diff in the dialog, and then click Apply and Persist. The dialog says that a restart is required.

    The review dialog shows the TLS change and says that a restart is required; the arrow points at Apply and Persist.The review dialog shows the TLS change and says that a restart is required; the arrow points at Apply and Persist.

  6. Restart the proxy. After the restart, the listener speaks HTTPS on the same port.

The proxy writes the change into its config file, and the restart reads it from there. When the config file is mounted read-only, as the Compose example, the Helm chart and the Kubernetes operator mount it, the proxy cannot save the change. The restart then starts without TLS. In that case, set TLS in the YAML of your deployment instead. The admin UI shows a warning banner when the config file is read-only.

The same change in YAML#

The steps above write this configuration into the advanced section of deltaglider_proxy.yaml (why the UI and the file hold the same configuration):

# validate
advanced:
  tls:
    enabled: true
    cert_path: /etc/ssl/certs/proxy.pem
    key_path: /etc/ssl/private/proxy-key.pem

The environment variables DGP_TLS_ENABLED=true, DGP_TLS_CERT and DGP_TLS_KEY override these fields. When one of them is set, the admin UI shows the field as read-only with a from env badge. After you edit the file, restart the proxy. An apply through POST /_/api/admin/config/apply or deltaglider_proxy config apply stores the change, but the listener still needs the restart.

With TLS at the proxy, the admin session cookies carry the Secure flag automatically. This is true whether you enable TLS in the YAML file, in the admin UI, or with DGP_TLS_ENABLED.

When the proxy faces the internet directly, keep DGP_TRUST_PROXY_HEADERS=false (the default). Otherwise clients can spoof X-Forwarded-For and bypass rate limiting. DGP_TRUST_PROXY_HEADERS, DGP_TRUSTED_PROXY_CIDRS and DGP_SECURE_COOKIES exist only as environment variables. The YAML file and the admin UI cannot set them.

Option B: terminate TLS at a reverse proxy#

If you terminate TLS at nginx or Caddy on the same host, bind the proxy to 127.0.0.1:9000 and forward over the loopback. To bind it, set Listen address in the HTTP listener card of System → System to 127.0.0.1:9000, or set advanced.listen_addr: "127.0.0.1:9000" in the YAML file, and restart the proxy. The container image sets DGP_LISTEN_ADDR=0.0.0.0:9000, so in a container the field is read-only and the variable decides. If Traefik runs in Docker, it reaches the proxy container over the Docker network instead.

Traefik (Docker Compose labels):

# not-proxy-config: docker-compose service
deltaglider_proxy:
  image: beshultd/deltaglider_proxy:latest
  environment:
    DGP_TRUST_PROXY_HEADERS: "true"
    DGP_TRUSTED_PROXY_CIDRS: "172.16.0.0/12"  # the Docker networks: Traefik connects from its container address
  labels:
    traefik.enable: "true"
    traefik.http.routers.dgp.rule: "Host(`s3.acme.example`)"
    traefik.http.routers.dgp.entrypoints: "websecure"
    traefik.http.routers.dgp.tls.certresolver: "letsencrypt"
    traefik.http.services.dgp.loadbalancer.server.port: "9000"

nginx:

server {
    listen 443 ssl;
    server_name s3.acme.example;
    ssl_certificate /etc/ssl/certs/proxy.pem;
    ssl_certificate_key /etc/ssl/private/proxy-key.pem;
    client_body_timeout 30m;

    location / {
        proxy_pass http://127.0.0.1:9000;
        proxy_read_timeout 30m;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $host;
    }
}

Caddy (automatic TLS):

s3.acme.example {
    reverse_proxy localhost:9000
}

Set three env vars on the proxy when a reverse proxy is in front:

VariableValueWhy
DGP_TRUST_PROXY_HEADERStrueAccept X-Forwarded-For / X-Real-IP for rate limiting, the IP binding of admin sessions, and IAM IP conditions. Also accept X-Forwarded-Host and X-Forwarded-Proto for the same-origin check of admin requests, the OAuth callback address, and the Secure cookie flag. Set it to true only when a reverse proxy is really in front. Otherwise clients can spoof IPs.
DGP_TRUSTED_PROXY_CIDRSthe reverse proxy's address, for example 127.0.0.1/32The proxy reads those headers only on a connection from these networks. It is required with DGP_TRUST_PROXY_HEADERS=true: without it, the proxy refuses to start.
DGP_SECURE_COOKIEStrueThe listener is plain HTTP behind the reverse proxy, so the proxy cannot see the TLS itself. It sets the Secure flag on its own only when a trusted X-Forwarded-Proto: https header arrives. Setting this variable to true makes the admin session cookies HTTPS-only for every request, and false never sets the flag. Unset means automatic. When the variable is unset and an X-Forwarded-Proto: https header arrives from a proxy that is not trusted, the cookie gets no Secure flag, and the proxy logs one warning that names this variable.

Raise the reverse-proxy read timeout — mandatory for large uploads#

If you terminate TLS at a reverse proxy, you must raise its request read-timeout. Most reverse proxies default to 60 seconds. A 16 MB multipart part over a typical home uplink (1 to 5 MB/s, shared between concurrent parts) takes longer than that. The reverse proxy then closes the upstream connection in the middle of the body, and the client sees 502 (Traefik) or 504 (nginx). This affects any object over about 50 MB and every multipart upload.

Reverse proxyDefaultSettingRecommended
Traefik 3.x60 sentryPoints.<name>.transport.respondingTimeouts.readTimeout30m or 0 (no limit)
Caddy 2.x0 (no limit)read_timeout in servers blockleave at default
nginx60 sclient_body_timeout + proxy_read_timeout30m
AWS ALB60 s idle_timeouttarget-group attribute4000 (max)
HAProxy60 s timeout clientglobal / frontend30m

Traefik static config:

# not-proxy-config: traefik static config
entryPoints:
  websecure:
    address: ":443"
    transport:
      respondingTimeouts:
        readTimeout: "30m"
        writeTimeout: "30m"
        idleTimeout: "180s"

You can also set them as CLI flags on the Traefik container:

# not-proxy-config: docker-compose command
command:
  - '--entrypoints.websecure.transport.respondingTimeouts.readTimeout=30m'
  - '--entrypoints.websecure.transport.respondingTimeouts.writeTimeout=30m'

The proxy's own request timeout (DGP_REQUEST_TIMEOUT_SECS) defaults to 300 s and is a separate timer. Both timeouts must be long enough for large uploads to succeed.

Verify#

# TLS answers and the health endpoint is reachable
curl -s https://s3.acme.example/_/health

# A signed S3 call works over HTTPS
aws s3 ls --endpoint-url https://s3.acme.example

# Large-upload path survives the timeout (anything > 50 MB)
dd if=/dev/urandom of=/tmp/big.bin bs=1M count=100
aws s3 cp /tmp/big.bin s3://releases/ --endpoint-url https://s3.acme.example

If the large upload fails with 502/504 and the proxy log shows a request finishing at exactly 60 000 ms with status 400, the reverse-proxy timeout is still in effect. See Troubleshooting.