Skip to content

Reverse Proxy and TLS for APT Repo Manager

This guide explains how to place APT Repo Manager behind a reverse proxy to enable HTTPS, strengthen security, and expose the application on standard ports (80/443).


1. Why a reverse proxy?

APT Repo Manager exposes three ports over plain HTTP by default:

Service Port Description
Frontend 3003 React SPA
Backend API 8000 FastAPI, called directly by browsers
APT repo 80 Served by internal Nginx

Without a reverse proxy, the application runs over cleartext HTTP. This means:

  • Credentials and API tokens are transmitted in plaintext over the network.
  • Modern browsers flag or block plain HTTP sites.
  • HSTS (HTTP Strict Transport Security) cannot be enabled.
  • apt 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.
  • Exposure on standard ports 80/443 (no port number in the URL).
  • Centralized certificate management (Let's Encrypt or internal CA).
  • Ability to filter, rate-limit, and log requests.

2. Target architecture

                        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                        β”‚           Reverse Proxy              β”‚
Internet ──── :443 ───► β”‚  /         β†’ frontend  :3003         β”‚
                        β”‚  /api/     β†’ backend   :8000         β”‚
         ──── :80  ───► β”‚  apt.*     β†’ apt-repo  :80 (HTTP OK) β”‚
                        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
  • The frontend and API are served over HTTPS on the same domain (repo.example.com).
  • The APT repo can remain on HTTP (packages are signed by GPG) or be migrated to HTTPS if apt clients support it.
  • In production, set BIND_HOST=127.0.0.1 in .env so services only listen on the loopback interface.

3. Prerequisites and DNS

System prerequisites

  • A Linux server with Docker and Docker Compose installed.
  • Ports 80 and 443 open in the firewall.
  • A domain name pointing to the server's public IP.
Record Value Purpose
repo.example.com A β†’ SERVER_IP Frontend + API
apt.example.com A β†’ SERVER_IP APT repo (optional)

If you use a single domain with path-based routing (/api/), a single A record is sufficient.

Firewall ports

# ufw
ufw allow 80/tcp
ufw allow 443/tcp

# iptables
iptables -A INPUT -p tcp --dport 80 -j ACCEPT
iptables -A INPUT -p tcp --dport 443 -j ACCEPT

4.1 Installation

apt update
apt install nginx certbot python3-certbot-nginx

4.2 Obtaining the certificate

certbot --nginx -d repo.example.com
# If using a separate subdomain for apt:
certbot --nginx -d repo.example.com -d apt.example.com

Certbot automatically modifies the Nginx configuration and sets up auto-renewal.

4.3 Complete Nginx configuration

Create the file /etc/nginx/sites-available/repod:

# /etc/nginx/sites-available/repod

# HTTP β†’ HTTPS redirect (frontend + API)
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 (1 year β€” do not enable before 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
    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 long CVE scans
        client_max_body_size 512m;   # allow large .deb package uploads
    }
}

# APT repo (HTTP β€” apt clients typically don't have custom CAs)
server {
    listen 80;
    server_name apt.example.com;

    location / {
        proxy_pass http://127.0.0.1:80;
        proxy_set_header Host $host;
    }
}

4.4 Enabling the configuration

ln -s /etc/nginx/sites-available/repod /etc/nginx/sites-enabled/repod
nginx -t          # syntax check
systemctl reload nginx

4.5 Variant: single domain with /api/ path prefix

If you do not want a separate subdomain for the API, the location /api/ block above handles it. Update REACT_APP_API_URL accordingly:

REACT_APP_API_URL=https://repo.example.com/api

5. Option B: Nginx + internal CA / self-signed certificate

Useful for intranets or development environments without public Internet access.

5.1 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"

To include Subject Alternative Names (recommended for modern browsers):

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"

5.2 Adapt the Nginx configuration

Replace the ssl_certificate / ssl_certificate_key lines with:

ssl_certificate     /etc/ssl/certs/repod.crt;
ssl_certificate_key /etc/ssl/private/repod.key;

Remove or comment out the HSTS header if the certificate is not publicly trusted.

5.3 Distributing the certificate to clients

For browsers and apt clients to accept the certificate:

# On each client machine (Ubuntu/Debian)
cp repod.crt /usr/local/share/ca-certificates/repod.crt
update-ca-certificates

# For apt specifically
echo 'Acquire::https::repo.example.com::CaInfo "/etc/ssl/certs/repod.crt";' \
  > /etc/apt/apt.conf.d/99repod-tls

6. Option C: Traefik (Docker-native)

Traefik integrates natively with Docker and automatically obtains Let's Encrypt certificates via labels on containers.

6.1 Traefik service in 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.letsencrypt.acme.tlschallenge=true"
      - "[email protected]"
      - "--certificatesresolvers.letsencrypt.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

6.2 Labels on repod services

  frontend:
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.repod-frontend.rule=Host(`repo.example.com`)"
      - "traefik.http.routers.repod-frontend.entrypoints=websecure"
      - "traefik.http.routers.repod-frontend.tls.certresolver=letsencrypt"
      - "traefik.http.services.repod-frontend.loadbalancer.server.port=3003"

  backend:
    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=letsencrypt"
      - "traefik.http.routers.repod-api.middlewares=strip-api-prefix"
      - "traefik.http.middlewares.strip-api-prefix.stripprefix.prefixes=/api"
      - "traefik.http.services.repod-api.loadbalancer.server.port=8000"

6.3 Pros and cons

Advantage Disadvantage
Zero manual certificate management Traefik needs access to the Docker socket
Automatic renewal Verbose labels for complex routing rules
Built-in dashboard Less suitable for non-Docker deployments

7. Option D: Caddy (minimal configuration)

Caddy handles HTTPS automatically with almost no configuration.

7.1 Installation

apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \
  | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \
  | tee /etc/apt/sources.list.d/caddy-stable.list
apt update && apt install caddy

7.2 Caddyfile

# /etc/caddy/Caddyfile

repo.example.com {
    # Frontend
    reverse_proxy / http://127.0.0.1:3003

    # Backend API (strip the /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
            }
        }
    }

    # Maximum upload size for .deb packages
    request_body {
        max_size 512MB
    }
}

apt.example.com {
    reverse_proxy http://127.0.0.1:80
}

7.3 Starting Caddy

systemctl enable --now caddy
caddy reload --config /etc/caddy/Caddyfile

Caddy automatically obtains and renews Let's Encrypt certificates in the background. No further action is required.


8. Updating repod configuration after enabling HTTPS

After enabling the reverse proxy, several environment variables need to be updated.

8.1 .env file (project root)

# Restrict binding to loopback β€” the RP handles external exposure
BIND_HOST=127.0.0.1

# Public URLs (used at frontend build time)
PUBLIC_URL=https://repo.example.com
REACT_APP_API_URL=https://repo.example.com/api
REACT_APP_REPO_URL=http://apt.example.com

8.2 backend.env file

# Only allow requests from the new HTTPS domain
CORS_ORIGINS=https://repo.example.com

# Trust only the local reverse proxy
TRUSTED_PROXIES=127.0.0.1

For Traefik inside Docker (bridge network), also include the Docker subnet:

TRUSTED_PROXIES=127.0.0.1,172.16.0.0/12

8.3 Application settings

In the web UI: Settings > General > app_url β†’ https://repo.example.com

8.4 Rebuilding the frontend

The React frontend is compiled with environment variables at build time. After any change to REACT_APP_* or PUBLIC_URL, rebuild the image:

docker compose build frontend
docker compose up -d frontend

9. Verification and testing

9.1 SSL verification

# Check the certificate chain
curl -vI https://repo.example.com 2>&1 | grep -E "SSL|TLS|certificate|issuer|expire"

# Check with openssl
openssl s_client -connect repo.example.com:443 -servername repo.example.com < /dev/null \
  | openssl x509 -noout -dates -subject -issuer

9.2 API test

# Verify the API responds over HTTPS
curl -s https://repo.example.com/api/health | jq .

# Check the HSTS header
curl -sI https://repo.example.com | grep -i strict-transport

9.3 HTTP β†’ HTTPS redirect test

curl -I http://repo.example.com
# Expected: HTTP/1.1 301 Moved Permanently + Location: https://...

9.4 Browser developer tools check

  1. Open https://repo.example.com in Chrome or Firefox.
  2. Open DevTools β†’ Network tab β†’ click the first request.
  3. Verify in Response Headers:
  4. strict-transport-security: max-age=31536000; includeSubDomains
  5. x-forwarded-proto: https (visible in backend logs if logging is enabled)

9.5 APT client test

# Add the repository and test
echo "deb http://apt.example.com/ubuntu focal main" \
  > /etc/apt/sources.list.d/repod.list
apt update

9.6 SSL quality rating (optional)

Test your TLS configuration at https://www.ssllabs.com/ssltest/ to obtain an A+ rating.


10. Certificate renewal

10.1 Let's Encrypt (Certbot)

Certbot automatically configures a systemd timer or cron job for renewal:

# Check the systemd timer
systemctl status certbot.timer

# Dry-run renewal test
certbot renew --dry-run

# Force renewal (if needed)
certbot renew --force-renewal

The default cron added by certbot:

0 0,12 * * * certbot renew --quiet

After renewal, Nginx reloads its configuration automatically if the post-deploy hook is in place (certbot adds it by default).

10.2 Traefik

Traefik handles renewal automatically. No manual action is required. Certificates are stored in ./letsencrypt/acme.json.

10.3 Caddy

Caddy renews certificates automatically in the background. No manual action is required.

10.4 Internal / self-signed certificates

For a self-signed certificate with a 1-year validity period, schedule a cron job for renewal:

# Example renewal script
cat > /usr/local/bin/renew-repod-cert.sh << 'EOF'
#!/bin/bash
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"
systemctl reload nginx
EOF
chmod +x /usr/local/bin/renew-repod-cert.sh

# Annual cron (330 days to stay ahead of expiry)
echo "0 3 1 1 * root /usr/local/bin/renew-repod-cert.sh" \
  > /etc/cron.d/repod-cert-renewal

Post-deployment checklist

Action Command / File
Restrict binding BIND_HOST=127.0.0.1 in .env
Update public URLs PUBLIC_URL, REACT_APP_API_URL in .env
Update CORS CORS_ORIGINS=https://... in backend.env
Configure trusted proxies TRUSTED_PROXIES=127.0.0.1 in backend.env
Update app_url Settings > General in the web UI
Rebuild frontend docker compose build frontend && docker compose up -d frontend
Verify HSTS header Browser DevTools β†’ Response Headers