Silo — TLS and Reverse Proxy
Silo speaks plain HTTP on its listen port. Terminate TLS at a reverse proxy — this is the supported path. Inline HTTPS is not wired in the current build (see below).
Why reverse-proxy by default
- Battle-tested implementations (Caddy, nginx, Traefik, ALB) handle certificate rotation, OCSP stapling, HSTS, and weak-cipher pruning correctly.
- TLS in front of the cache means the cache process does not need root for port 443 and does not need access to private-key material.
- A reverse proxy can also add rate limiting, IP allowlists, and request-size caps as a first line of defense.
Caddy (easiest)
Automatic Let’s Encrypt:
silo.example.com {
reverse_proxy silo:8080
encode zstd gzip
# Stop body buffering — let Silo stream
request_body {
max_size 5GB
}
}
That’s it. Caddy fetches and renews certificates on its own. See examples/Caddyfile for a
docker-compose-friendly variant.
nginx
upstream silo_upstream {
server silo:8080;
keepalive 32;
}
server {
listen 443 ssl http2;
server_name silo.example.com;
ssl_certificate /etc/letsencrypt/live/silo.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/silo.example.com/privkey.pem;
# Strong defaults
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
add_header Strict-Transport-Security "max-age=63072000" always;
# Let large PUTs through
client_max_body_size 5g;
client_body_buffer_size 16k;
proxy_request_buffering off; # stream PUTs straight through
proxy_buffering off; # stream GETs straight back
# Keepalive to upstream
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
location / {
proxy_pass http://silo_upstream;
}
}
Key flags: proxy_request_buffering off and proxy_buffering off — without these, nginx
will spool the full body to disk before forwarding, which negates Silo’s streaming and hits
your nginx worker disk hard.
Traefik
Docker provider (labels)
# docker-compose.yml fragment
services:
silo:
image: ghcr.io/chrisjenx/silo:latest
labels:
- traefik.enable=true
- traefik.http.routers.silo.rule=Host(`silo.example.com`)
- traefik.http.routers.silo.entrypoints=websecure
- traefik.http.routers.silo.tls.certresolver=le
- traefik.http.services.silo.loadbalancer.server.port=8080
# IMPORTANT: do NOT enable the buffering middleware for the whole
# router — it spools the entire body before forwarding and defeats
# Silo's streaming. Cap size at Silo instead (it 413s via Expect:
# 100-continue). Only buffer if you must hard-limit at the edge:
- traefik.http.middlewares.silo-limit.buffering.maxRequestBodyBytes=5368709120
- traefik.http.middlewares.silo-limit.buffering.memRequestBodyBytes=1048576
- traefik.http.routers.silo.middlewares=silo-limit
Full static + dynamic config (file provider)
# traefik.yml — static config
entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint: { to: websecure, scheme: https }
websecure:
address: ":443"
transport:
respondingTimeouts:
readTimeout: "120s" # allow slow, large PUTs
idleTimeout: "180s"
certificatesResolvers:
le:
acme:
email: ops@example.com
storage: /acme/acme.json
tlsChallenge: {}
providers:
file:
filename: /etc/traefik/dynamic.yml
# dynamic.yml — routers/services
http:
routers:
silo:
rule: "Host(`silo.example.com`)"
entryPoints: [websecure]
service: silo
tls:
certResolver: le
services:
silo:
loadBalancer:
passHostHeader: true
servers:
- url: "http://silo:8080"
healthCheck:
path: /health
interval: "30s"
timeout: "3s"
Traefik streams request and response bodies by default (no proxy_buffering
equivalent to disable), so large GET/PUT pass straight through unless you
attach the buffering middleware above.
AWS ALB
Application Load Balancer terminates TLS with an ACM certificate and forwards plain HTTP to the Silo target group.
- Listener: HTTPS:443, ACM cert, forward to the target group. Add an HTTP:80 listener that 301-redirects to HTTPS.
- Target group: protocol HTTP, port 8080, health check
GET /health(200 expected),deregistration_delay = 30s. idle_timeout = 120son the LB — the 60s default can sever large PUTs on slow links.- ALB does not cap request body size, so Silo’s
max-entry-bytes(→ 413) is the size guard. ALB also doesn’t honourExpect: 100-continueend-to-end, so oversized PUTs upload fully before Silo rejects them — keep the cap sane. - Stickiness is not required — every request is content-addressed and self-routing across targets.
# Terraform sketch
resource "aws_lb_target_group" "silo" {
port = 8080
protocol = "HTTP"
vpc_id = var.vpc_id
health_check {
path = "/health"
matcher = "200"
interval = 30
timeout = 3
healthy_threshold = 2
unhealthy_threshold = 3
}
deregistration_delay = 30
}
resource "aws_lb_listener" "https" {
load_balancer_arn = aws_lb.silo.arn
port = 443
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06"
certificate_arn = var.acm_certificate_arn
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.silo.arn
}
}
Cloudflare Tunnel
cloudflared dials out from inside your network, so Silo needs no inbound
ports and no public IP. TLS terminates at the Cloudflare edge.
# config.yml for cloudflared
tunnel: <TUNNEL-UUID>
credentials-file: /etc/cloudflared/<TUNNEL-UUID>.json
ingress:
- hostname: silo.example.com
service: http://silo:8080
originRequest:
connectTimeout: 30s
# No response buffering — stream cache hits straight back.
noHappyEyeballs: false
- service: http_status:404
cloudflared tunnel route dns <TUNNEL-UUID> silo.example.com
cloudflared tunnel run <TUNNEL-UUID>
Caveats:
- The free plan caps request bodies at 100 MB. Large build-cache artifacts
will fail with
413from Cloudflare before reaching Silo — use a paid plan or a different proxy for big artifacts. - Cloudflare may buffer/transform; disable Caching and any compression for the Silo hostname so opaque cache bytes pass through untouched.
- Authenticate at Silo (HTTP Basic) and/or with Cloudflare Access in front —
Silo never trusts
X-Forwarded-*for auth.
Inline TLS (not currently wired)
Silo’s entrypoint starts Netty with a plain HTTP connector only, so the standard
Ktor ktor.deployment.ssl* keys are ignored by the shipped build — setting
them does nothing. Inline TLS is tracked as a future option; until then, terminate
TLS at a reverse proxy (above). Silo does not integrate ACME.
What Silo expects from the proxy
X-Forwarded-Proto: httpsis forwarded if you log it — but Silo does not need it for routing.X-Forwarded-Foris not trusted for auth decisions. Silo authenticates by HTTP Basic only.- Don’t strip
Expect: 100-continue— Silo uses it to early-reject oversized or unauthorized PUTs before the body is uploaded. - Don’t transcode the body or set
Content-Encoding. The body is opaque to the proxy.