Reverse Proxy & TLS¶
Configure a TLS-terminating reverse proxy in front of Repod to enable HTTPS, expose services on standard ports, and harden the public attack surface.
Why a reverse proxy?¶
Repod exposes three services over plain HTTP by default:
| Service | Default port | Description |
|---|---|---|
| Web interface | 3003 |
React SPA |
| Backend API | 8000 |
FastAPI β called directly by browsers |
| Repository | 80 |
Served by internal Nginx |
Without a reverse proxy:
- Credentials and API tokens travel over the network in plaintext.
- Modern browsers flag plain HTTP sites.
- HSTS cannot be enabled.
- APT/DNF clients are vulnerable to man-in-the-middle attacks.
With a TLS reverse proxy you gain:
- End-to-end encryption (TLS 1.2/1.3).
- HSTS to enforce HTTPS in browsers.
- Standard ports 80/443 β no port suffix in URLs.
- Centralized certificate management (Let's Encrypt or internal CA).
Target architecture¶
βββββββββββββββββββββββββββββββββββββββββ
β Reverse Proxy β
Internet β :443 ββΊ β / β frontend-ui :3003 β
β /api/* β backend-api :8000 β
β :80 ββΊ β redirect to HTTPS β
βββββββββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββββββββ
β Repository (HTTP OK) β
LAN clients β :80 ββΊ depot-apt / depot-rpm :80 β
βββββββββββββββββββββββββββββββββββββββββ
Repository over HTTP
The APT/RPM repository can remain on plain HTTP β packages are signed by GPG. APT and DNF verify the signature; HTTP integrity is handled at the package level. Migrate to HTTPS only if your security policy requires it.
Before you start¶
Set BIND_HOST=127.0.0.1 in your .env file so services bind only to the
loopback interface. The reverse proxy then handles all external exposure:
Rebuild and restart after changing .env:
Option A β Nginx + Let's Encrypt (recommended)¶
Install Nginx and Certbot¶
Obtain the TLS certificate¶
Certbot modifies the Nginx configuration and configures automatic renewal.
Full Nginx configuration¶
Create /etc/nginx/sites-available/repod (Debian/Ubuntu) or
/etc/nginx/conf.d/repod.conf (RHEL/AlmaLinux):
# HTTP β HTTPS redirect
server {
listen 80;
server_name repo.example.com;
return 301 https://$host$request_uri;
}
# Frontend + API over HTTPS
server {
listen 443 ssl http2;
server_name repo.example.com;
ssl_certificate /etc/letsencrypt/live/repo.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/repo.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
# HSTS β enable only after verifying HTTPS works end-to-end
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# Frontend (React SPA)
location / {
proxy_pass http://127.0.0.1:3003;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Backend API β strip the /api prefix before forwarding
location /api/ {
rewrite ^/api/(.*) /$1 break;
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 300s; # allow time for CVE scans
client_max_body_size 512m; # allow large package uploads
# Required for Server-Sent Events (sync / import progress)
proxy_buffering off;
proxy_cache off;
}
}
Enable and reload:
# Debian / Ubuntu
ln -s /etc/nginx/sites-available/repod /etc/nginx/sites-enabled/repod
# All platforms
nginx -t
sudo systemctl reload nginx
Variant β single domain with /api/ path prefix¶
If you do not want the frontend and API on separate ports, the location /api/
block above routes API calls correctly. Update REACT_APP_API_URL accordingly
and rebuild the frontend:
Option B β Nginx + internal CA / self-signed certificate¶
Useful for intranets or air-gapped environments without public Internet access.
Generate a self-signed certificate¶
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout /etc/ssl/private/repod.key \
-out /etc/ssl/certs/repod.crt \
-subj "/CN=repo.example.com" \
-addext "subjectAltName=DNS:repo.example.com,IP:192.168.1.100"
Replace the ssl_certificate* lines in the Nginx configuration above:
Distribute the certificate to clients¶
Option C β Traefik (Docker-native)¶
Traefik integrates natively with Docker and automatically obtains Let's Encrypt certificates via labels on containers.
Add Traefik to docker-compose.yaml¶
services:
traefik:
image: traefik:v3.0
command:
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.le.acme.tlschallenge=true"
- "[email protected]"
- "--certificatesresolvers.le.acme.storage=/letsencrypt/acme.json"
ports:
- "80:80"
- "443:443"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "./letsencrypt:/letsencrypt"
restart: unless-stopped
Add labels to Repod services¶
frontend-ui:
labels:
- "traefik.enable=true"
- "traefik.http.routers.repod-ui.rule=Host(`repo.example.com`)"
- "traefik.http.routers.repod-ui.entrypoints=websecure"
- "traefik.http.routers.repod-ui.tls.certresolver=le"
- "traefik.http.services.repod-ui.loadbalancer.server.port=3003"
backend-api:
labels:
- "traefik.enable=true"
- "traefik.http.routers.repod-api.rule=Host(`repo.example.com`) && PathPrefix(`/api/`)"
- "traefik.http.routers.repod-api.entrypoints=websecure"
- "traefik.http.routers.repod-api.tls.certresolver=le"
- "traefik.http.routers.repod-api.middlewares=strip-api"
- "traefik.http.middlewares.strip-api.stripprefix.prefixes=/api"
- "traefik.http.services.repod-api.loadbalancer.server.port=8000"
| Advantage | Disadvantage |
|---|---|
| Zero manual certificate management | Requires Docker socket access |
| Auto-renewal | Verbose label syntax |
| Auto-discovery of services | Extra container to manage |
Option D β Caddy (minimal configuration)¶
Caddy handles HTTPS automatically with minimal configuration.
repo.example.com {
# Frontend
reverse_proxy / http://127.0.0.1:3003
# Backend API β strip /api prefix
handle /api/* {
uri strip_prefix /api
reverse_proxy http://127.0.0.1:8000 {
header_up X-Forwarded-Proto {scheme}
transport http { read_timeout 300s }
}
}
request_body { max_size 512MB }
}
Caddy obtains and renews Let's Encrypt certificates automatically.
Update Repod configuration after enabling HTTPS¶
After enabling the reverse proxy, update environment variables and rebuild the frontend.
.env¶
BIND_HOST=127.0.0.1
REACT_APP_API_URL=https://repo.example.com/api
REACT_APP_REPO_URL=http://repo.example.com
backend.env¶
For Traefik inside Docker (bridge network), also include the Docker subnet:
Rebuild the frontend¶
REACT_APP_* variables are baked into the JavaScript bundle at build time:
Verify the configuration¶
# Certificate chain
curl -vI https://repo.example.com 2>&1 | grep -E "SSL|TLS|certificate|issuer"
# HSTS header
curl -sI https://repo.example.com | grep -i strict-transport
# HTTP β HTTPS redirect
curl -I http://repo.example.com
# Expected: 301 Moved Permanently β https://...
# API over HTTPS
curl -s https://repo.example.com/api/health/live | jq .
Certificate renewal¶
| Proxy | Renewal | Action required |
|---|---|---|
| Certbot | Systemd timer every 12 h | None β runs automatically |
| Traefik | Built-in ACME client | None |
| Caddy | Built-in ACME client | None |
| Self-signed | Manual or cron | Schedule annual renewal |
Check Certbot timer status:
Post-configuration checklist¶
| Action | File / Command |
|---|---|
BIND_HOST=127.0.0.1 |
.env |
REACT_APP_API_URL updated |
.env β rebuild frontend |
CORS_ORIGINS updated |
backend.env |
TRUSTED_PROXIES set |
backend.env |
| Frontend rebuilt | docker compose build frontend-ui |
| HSTS header verified | curl -sI https://repo.example.com |
| Port 8000 not exposed | sudo ufw status or firewall-cmd --list-all |