Skip to content

Reverse Proxy

homelabforge edited this page Oct 18, 2026 · 8 revisions

Reverse Proxy Configuration

Configure reverse proxies (Traefik, Nginx, Caddy) to expose MyGarage with HTTPS, custom domains, and enhanced security.


Why Use a Reverse Proxy?

  • ✅ HTTPS/SSL - Encrypt traffic with Let's Encrypt certificates
  • ✅ Custom Domain - Access via mygarage.example.com instead of IP:8686
  • ✅ Authentication - Add external auth layer (Authelia, oauth2-proxy)
  • ✅ Load Balancing - Distribute traffic across multiple instances
  • ✅ Security Headers - HSTS, CSP, X-Frame-Options
  • ✅ Rate Limiting - Protect against brute-force attacks
  • ✅ Access Logs - Centralized logging and monitoring

Serving Under a Subpath

By default MyGarage is served at the root of its domain (https://mygarage.example.com/). To serve it under a subpath instead (e.g. https://home.example.com/mygarage/), set MYGARAGE_ROOT_PATH=/mygarage and put a prefix-stripping proxy in front. No image rebuild is needed — the app injects the base path at runtime, and OIDC redirects, the PWA, media, and deep links all follow the prefix.

nginx (subpath)

location = /mygarage { return 308 /mygarage/; }   # bare prefix -> trailing slash
location /mygarage/ {
    proxy_pass http://mygarage:8686/;             # trailing slash strips /mygarage
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-Host $host;
}

Traefik (subpath)

# Traefik dynamic config — the router MUST attach the strip middleware, plus a
# redirect so the bare /mygarage lands inside the /mygarage/ service-worker scope.
http:
  routers:
    mygarage:
      rule: "Host(`example.com`) && PathPrefix(`/mygarage`)"
      middlewares: ["mygarage-bare-redirect", "mygarage-strip"]
      service: mygarage
  middlewares:
    mygarage-bare-redirect:
      redirectRegex:
        regex: "^(https?://[^/]+)/mygarage$"
        replacement: "${1}/mygarage/"
        permanent: true
    mygarage-strip:
      stripPrefix:
        prefixes: ["/mygarage"]

OIDC: register the redirect URI with the prefix in the full path — https://example.com/mygarage/api/auth/oidc/callback.


Client IP and Rate Limits

Behind a reverse proxy, every request reaches MyGarage from the proxy. Unless you tell MyGarage which proxies to trust, its rate limits and audit logs see the proxy as the client: everyone shares one rate limit bucket, and every audit row records the proxy's address.

The Two Settings

  • MYGARAGE_TRUSTED_PROXIES: your reverse proxies' IPs or CIDRs, comma-separated, like 172.18.0.0/16 or 172.18.0.5,fd00::5. Empty (the default) trusts nobody. A request from a listed address gets its X-Forwarded-For read right to left, past every trusted hop, and the first address that isn't trusted is the client. A request from anywhere else has its X-Forwarded-For, X-Forwarded-Proto, X-Forwarded-Host and client IP header dropped before anything reads them. * and any /0 are refused, since they'd let every client pick its own address, and a bad entry stops MyGarage at startup with an error naming it.
  • MYGARAGE_CLIENT_IP_HEADER: optional. A single-value header your trusted proxy sets to the client's address, like CF-Connecting-IP or X-Real-IP. MyGarage only reads it from a trusted proxy, and uses it instead of X-Forwarded-For when it shows up exactly once; missing, repeated or not an address, MyGarage falls back to X-Forwarded-For. It can't be X-Forwarded-For, and it's ignored while MYGARAGE_TRUSTED_PROXIES is empty. Read Before You Set the Client IP Header first.

MYGARAGE_TRUSTED_HOSTS is a different setting: it's the private-IP allow-list for a self-hosted OIDC issuer, and putting your proxy there does nothing for client IPs.

Both are environment variables, so restart MyGarage after changing them. At startup it logs what it trusts:

Trusting proxy headers from 172.18.0.0/16; client IP header: none, using X-Forwarded-For

Finding the Address to Trust

Leave both unset and load MyGarage once through your proxy. The first request that carries X-Forwarded-For logs this:

Got X-Forwarded-For from 172.18.0.5, but MYGARAGE_TRUSTED_PROXIES is empty, so rate limits and audit logs see that address as the client. If it's your reverse proxy, set MYGARAGE_TRUSTED_PROXIES to it (see Reverse-Proxy in the wiki). Logged once.

That address is your proxy as MyGarage sees it (docker logs mygarage 2>&1 | grep "Got X-Forwarded-For"). No warning at all means your proxy isn't sending X-Forwarded-For, so fix that first (see What to Set for Your Proxy). With no proxy in front, a client sent the header itself: don't trust the address it names.

If the proxy is a container, docker network inspect shows its network's subnet and each container's address on it:

docker network inspect traefik-proxy --format '{{range .IPAM.Config}}{{.Subnet}} {{end}}'
docker network inspect traefik-proxy --format '{{range .Containers}}{{.Name}} {{.IPv4Address}}{{"\n"}}{{end}}'

A container's address can change when it's recreated, so the network's subnet is the steadier thing to trust. That trusts every container on the network, which is fine when they're all yours.

Once it's set, two more warnings can show up, each logged once per start:

Dropped proxy headers from 172.18.0.7, which isn't in MYGARAGE_TRUSTED_PROXIES. If it's your reverse proxy, add it. Logged once.

Something outside the list sent forwarding headers. If it's your proxy, add it. If it's a client, that's the protection working.

Trusted proxy 172.18.0.5 sent no X-Forwarded-For or client IP header, so rate limits and audit logs see it as the client. If it's your reverse proxy, have it send X-Forwarded-For (see Reverse-Proxy in the wiki); if it's a service calling MyGarage directly, ignore this. Logged once.

A trusted address sent nothing to go on. Either your proxy isn't forwarding (fix it), or that request didn't come through the proxy at all: a health check, or another container in a trusted range calling MyGarage directly. Those are harmless.

Never Trust an Address Clients Can Reach MyGarage From

A trusted address can tell MyGarage any client address it likes, so only trust addresses nothing but your proxy connects from.

The trap is a published port. With MyGarage's port published (the stock compose file has "8686:8686") and a proxy running on the host (like the basic nginx example, which proxies to localhost:8686), MyGarage doesn't see the proxy's own address. It sees the Docker network's gateway, like 172.18.0.1, so the gateway is the proxy's address there. Trust exactly the address the warning names, as a single address (172.18.0.1/32, not the network's /16), and only if no client can arrive from it:

  • Docker's userland proxy (on by default) carries connections to a published port on localhost, and IPv6 connections to a port published without a host IP when the Docker network is IPv4-only. Every one of those arrives from the gateway. With the proxy on the same host, publish on loopback only ("127.0.0.1:8686:8686") so nothing off the host can come in that way.
  • With rootless Docker's default networking, or Docker Desktop, every connection to a published port arrives from that same address, whoever made it. Trusting it trusts everyone: don't.

The layout the Traefik example uses avoids all of this: the proxy and MyGarage on one Docker network, and no ports: on MyGarage. The only way in is through the proxy, so you trust its address or that network's subnet.

Before You Set the Client IP Header

MyGarage believes MYGARAGE_CLIENT_IP_HEADER whenever a trusted proxy passes it along, so every path to that proxy has to go through the layer that sets it. A client that reaches the trusted proxy any other way sends its own value, and MyGarage believes it. Two setups where that bites:

  • Cloudflare proxied DNS (orange cloud) with no tunnel. Your origin answers anyone who finds its IP, and a client that goes straight there sends its own CF-Connecting-IP. Use the header only with the origin restricted to Cloudflare's IP ranges. Leaving it unset isn't a recipe on its own either: see Cloudflare Proxied DNS, No Tunnel.
  • nginx. nginx passes a client's own headers through wherever it doesn't set them, and a location with any proxy_set_header of its own inherits none from the server block. The basic example sets X-Real-IP only inside location /, so a second location that proxies to MyGarage would pass a client's own X-Real-IP straight through. Set the header in every location that proxies to MyGarage. The same goes for X-Forwarded-For: a location that doesn't set it hands MyGarage the client's own.

Cloudflare itself refuses any request that already carries CF-Connecting-IP (error 1000), so only a path that skips Cloudflare can choose it. A split-horizon setup has that hole on the LAN: a LAN client that hits your proxy directly, past Cloudflare, can send its own CF-Connecting-IP. Strip the header at the proxy on that path, or accept that LAN clients can pick their own address.

Your proxy also has to set X-Forwarded-Proto and X-Forwarded-Host itself: MyGarage only decides whose to believe, and a trusted proxy that passes a client's own value through is believed.

What to Set for Your Proxy

nginx

Trust the address the warning names. For nginx on the host with MyGarage's port published, that's the gateway as a /32, with the port published on 127.0.0.1 (see above). For nginx in a container on MyGarage's network, trust that network. In every location that proxies to MyGarage:

proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;

$proxy_add_x_forwarded_for appends the address nginx saw to whatever the client sent, and MyGarage reads from the right, so the client can't pick. Leave MYGARAGE_CLIENT_IP_HEADER unset.

MYGARAGE_TRUSTED_PROXIES=172.18.0.1/32   # the address the warning named

Traefik

Traefik sets X-Forwarded-For, X-Forwarded-Proto and X-Forwarded-Host itself, and throws away what the sender put in them unless the sender is in the entrypoint's forwardedHeaders.trustedIPs. Nothing to change in Traefik. Trust its Docker network and leave the header unset:

MYGARAGE_TRUSTED_PROXIES=172.18.0.0/16   # docker network inspect traefik-proxy

Caddy

reverse_proxy sets X-Forwarded-For, X-Forwarded-Proto and X-Forwarded-Host itself, and ignores the client's values unless the client is in Caddy's trusted_proxies. Don't override X-Forwarded-For with header_up. Trust Caddy's address and leave the header unset.

HAProxy

HAProxy sends no X-Forwarded-For unless you ask for it. Use plain option forwardfor, which appends the client's address after any the client sent (MyGarage reads from the right), or http-request set-header X-Forwarded-For %[src]. Never option forwardfor if-none: it keeps a client's own header and adds nothing, so the client picks its address. Set X-Forwarded-Proto and X-Forwarded-Host too, and run in mode http, or none of these rules apply. See the HAProxy example. Trust HAProxy's address and leave the header unset.

Cloudflare Tunnel, Straight to MyGarage

Put cloudflared and MyGarage on one Docker network with no ports: on MyGarage, so the tunnel is the only way in. Cloudflare sets CF-Connecting-IP (unless you turn on its Remove visitor IP headers Managed Transform, which strips it) and X-Forwarded-Proto on every request:

MYGARAGE_TRUSTED_PROXIES=172.18.0.0/16   # cloudflared's network, or its address
MYGARAGE_CLIENT_IP_HEADER=CF-Connecting-IP

X-Forwarded-For alone works here too, since Cloudflare appends the client's address to the end of it. Cloudflare passes a client's own X-Forwarded-Host through. cloudflared only replaces it (with the hostname the visitor asked for) when the route has an HTTP Host Header (httpHostHeader); set that to your public hostname so a client's own X-Forwarded-Host doesn't get through.

Cloudflare Tunnel to a Proxy

cloudflared to Traefik, nginx or Caddy, then MyGarage. Trust the proxy and read the header Cloudflare set, which all three pass through untouched:

MYGARAGE_TRUSTED_PROXIES=172.18.0.0/16   # the proxy's network, or its address
MYGARAGE_CLIENT_IP_HEADER=CF-Connecting-IP

X-Forwarded-For alone needs more: MyGarage has to trust cloudflared's address as well as the proxy's, and Traefik and Caddy have to trust it too (forwardedHeaders.trustedIPs, trusted_proxies), or they replace the chain with cloudflared's address. The header's precondition applies: if anything besides cloudflared can reach the proxy (a LAN client on a split-horizon setup, say), strip CF-Connecting-IP on that path or accept that it can pick its own address.

Cloudflare Proxied DNS, No Tunnel

DNS points at your origin with the orange cloud on, and your proxy takes requests from Cloudflare's edge. With only your proxy trusted, the X-Forwarded-For walk stops at the edge: nginx's $proxy_add_x_forwarded_for appends the edge's address, and Traefik without forwardedHeaders.trustedIPs replaces the chain with it (so does Caddy without trusted_proxies). Every limit becomes one bucket per Cloudflare edge address, every audit row records a Cloudflare address, and no warning fires. Two setups work:

  1. Trust Cloudflare's ranges too. List every range from Cloudflare's IPv4 and IPv6 lists in MYGARAGE_TRUSTED_PROXIES alongside your proxy, and the same ranges in Traefik's forwardedHeaders.trustedIPs (or Caddy's trusted_proxies). nginx with $proxy_add_x_forwarded_for and HAProxy with option forwardfor need nothing more. A client that skips Cloudflare and hits your origin directly is still the rightmost address your proxy appends, so it can't pick its own.
  2. Read CF-Connecting-IP with your origin firewalled so only Cloudflare's ranges can reach it: trust only your proxy and set MYGARAGE_CLIENT_IP_HEADER=CF-Connecting-IP.

Cloudflare publishes its ranges at cloudflare.com/ips. The list changes, so check it now and then. To build the value for the first setup (your proxy's address first):

echo "MYGARAGE_TRUSTED_PROXIES=172.18.0.5,$( (curl -s https://www.cloudflare.com/ips-v4; echo; curl -s https://www.cloudflare.com/ips-v6) | paste -sd, -)"

And in Traefik's static config:

entryPoints:
  https:
    address: ":443"
    forwardedHeaders:
      trustedIPs:
        - "173.245.48.0/20"
        # ...plus every other range from https://www.cloudflare.com/ips-v4 and /ips-v6

Either way, Cloudflare's ranges are shared by every Cloudflare account, and Cloudflare documents that a Worker can set the client address its own requests carry (CF-Connecting-IP follows the Worker's x-real-ip). So someone who points their own Cloudflare zone at your origin's IP can still choose the address MyGarage sees. Cloudflare's origin protection guide rates an IP allow-list "moderately secure" and "vulnerable to IP spoofing", and a tunnel or Authenticated Origin Pulls "very secure". Its headers reference says that proving traffic came from your own zone takes a tunnel or Authenticated Origin Pulls with your own certificate. If that matters to you, use a tunnel.

No Proxy

Leave both unset, and MyGarage uses each connection's own address. If the X-Forwarded-For warning shows up anyway, a client sent the header itself: don't trust the address it names. With rootless Docker's default networking, or Docker Desktop, every client arrives from one address (see above), and no setting here fixes that.

SSO-Only Installs

If everyone signs in through SSO and MyGarage sits behind a proxy, set MYGARAGE_TRUSTED_PROXIES. Unset, SSO sign-in shares one rate limit across every client, so one client can block it for everyone for a minute. A limited attempt lands back on the login page saying "Too many sign-in attempts. Wait a minute, then try again."


Traefik Configuration

Overview

Traefik is a modern reverse proxy designed for Docker environments.

Advantages:

  • Auto-discovery of Docker containers
  • Automatic Let's Encrypt certificates
  • Native Docker label configuration
  • Built-in dashboard

Docker Compose with Traefik Labels

Full Example:

services:
  mygarage:
    image: ghcr.io/homelabforge/mygarage:latest
    container_name: mygarage
    volumes:
      - ./data:/data
    networks:
      - traefik-proxy
    labels:
      # Enable Traefik
      - "traefik.enable=true"

      # HTTP Router
      - "traefik.http.routers.mygarage.rule=Host(`mygarage.example.com`)"
      - "traefik.http.routers.mygarage.entrypoints=https"
      - "traefik.http.routers.mygarage.tls=true"
      - "traefik.http.routers.mygarage.tls.certresolver=letsencrypt"

      # Service (backend)
      - "traefik.http.services.mygarage.loadbalancer.server.port=8686"

      # Middleware (optional - security headers)
      - "traefik.http.routers.mygarage.middlewares=mygarage-headers"
      - "traefik.http.middlewares.mygarage-headers.headers.stsSeconds=31536000"
      - "traefik.http.middlewares.mygarage-headers.headers.stsIncludeSubdomains=true"
      - "traefik.http.middlewares.mygarage-headers.headers.stsPreload=true"
      - "traefik.http.middlewares.mygarage-headers.headers.forceSTSHeader=true"

      # HTTP to HTTPS redirect
      - "traefik.http.routers.mygarage-http.rule=Host(`mygarage.example.com`)"
      - "traefik.http.routers.mygarage-http.entrypoints=http"
      - "traefik.http.routers.mygarage-http.middlewares=https-redirect"
      - "traefik.http.middlewares.https-redirect.redirectscheme.scheme=https"
      - "traefik.http.middlewares.https-redirect.redirectscheme.permanent=true"
    restart: unless-stopped

networks:
  traefik-proxy:
    external: true

Traefik Static Configuration (traefik.yml):

entryPoints:
  http:
    address: ":80"
  https:
    address: ":443"

certificatesResolvers:
  letsencrypt:
    acme:
      email: you@example.com
      storage: /letsencrypt/acme.json
      httpChallenge:
        entryPoint: http

providers:
  docker:
    exposedByDefault: false

Traefik with Authelia (SSO)

Add external authentication with Authelia:

labels:
  # Existing labels...

  # Add Authelia middleware
  - "traefik.http.routers.mygarage.middlewares=authelia@docker"

Users must authenticate via Authelia before accessing MyGarage (even if MyGarage uses auth_mode=none).


Nginx Configuration

Overview

Nginx is a battle-tested reverse proxy with extensive documentation.

Advantages:

  • Highly stable and performant
  • Rich ecosystem of modules
  • Fine-grained control
  • Proven at scale

Basic Nginx Configuration

/etc/nginx/sites-available/mygarage.conf:

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name mygarage.example.com;

    # SSL Configuration
    ssl_certificate /etc/letsencrypt/live/mygarage.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/mygarage.example.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;

    # Security Headers
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

    # Proxy to MyGarage
    location / {
        proxy_pass http://localhost:8686;
        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_set_header X-Forwarded-Host $host;

        # WebSocket support (if needed in future)
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";

        # Timeouts
        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
    }

    # Logs
    access_log /var/log/nginx/mygarage-access.log;
    error_log /var/log/nginx/mygarage-error.log;
}

# HTTP to HTTPS redirect
server {
    listen 80;
    listen [::]:80;
    server_name mygarage.example.com;
    return 301 https://$host$request_uri;
}

Enable Site:

sudo ln -s /etc/nginx/sites-available/mygarage.conf /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Nginx with Let's Encrypt (Certbot)

Install Certbot:

sudo apt install certbot python3-certbot-nginx

Obtain Certificate:

sudo certbot --nginx -d garage.example.com

Certbot automatically:

  • Obtains SSL certificate
  • Updates Nginx configuration
  • Sets up auto-renewal

Verify Auto-Renewal:

sudo certbot renew --dry-run

Caddy Configuration

Overview

Caddy is a modern web server with automatic HTTPS.

Advantages:

  • Automatic Let's Encrypt certificates (zero config)
  • Simple configuration syntax
  • Built-in security defaults
  • No separate certbot needed

Basic Caddy Configuration

Caddyfile:

garage.example.com {
    reverse_proxy localhost:8686
}

That's it! Caddy automatically:

  • Obtains Let's Encrypt certificate
  • Configures HTTPS
  • Redirects HTTP to HTTPS
  • Sets security headers

Advanced Caddy Configuration

Caddyfile with custom settings:

mygarage.example.com {
    # Reverse proxy
    reverse_proxy localhost:8686 {
        # Caddy sets X-Forwarded-For and X-Forwarded-Host itself. Don't
        # header_up X-Forwarded-For: {remote} replaces the chain with ip:port.
        header_up X-Real-IP {remote_host}
        header_up X-Forwarded-Proto {scheme}
    }

    # Security headers
    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
        X-Content-Type-Options "nosniff"
        X-Frame-Options "SAMEORIGIN"
        X-XSS-Protection "1; mode=block"
        Referrer-Policy "strict-origin-when-cross-origin"
    }

    # Logs
    log {
        output file /var/log/caddy/mygarage-access.log
        format json
    }
}

Caddy with Authelia

Caddyfile:

mygarage.example.com {
    # Forward auth to Authelia
    forward_auth authelia:9091 {
        uri /api/verify?rd=https://auth.example.com
        copy_headers Remote-User Remote-Groups Remote-Name Remote-Email
    }

    reverse_proxy localhost:8686
}

HAProxy Configuration

Overview

HAProxy is an enterprise-grade load balancer and reverse proxy.

Use Case: Load balancing across multiple MyGarage instances (high availability).

Basic HAProxy Configuration

/etc/haproxy/haproxy.cfg:

frontend mygarage-frontend
    mode http
    bind *:443 ssl crt /etc/haproxy/certs/mygarage.example.com.pem
    default_backend mygarage-backend

backend mygarage-backend
    mode http
    balance roundrobin
    option httpchk GET /health
    # Appends the client's address as the last X-Forwarded-For. Never add
    # "if-none": that keeps a client's own header and adds nothing.
    option forwardfor
    http-request set-header X-Forwarded-Proto https
    http-request set-header X-Forwarded-Host %[req.hdr(host)]
    server mygarage1 192.168.1.10:8686 check
    server mygarage2 192.168.1.11:8686 check

External Authentication (Authelia, oauth2-proxy)

Authelia with Traefik

Authelia Docker Compose:

services:
  authelia:
    image: authelia/authelia:latest
    container_name: authelia
    volumes:
      - ./authelia:/config
    environment:
      - TZ=America/New_York
    networks:
      - traefik-proxy
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.authelia.rule=Host(`auth.example.com`)"
      - "traefik.http.routers.authelia.entrypoints=https"
      - "traefik.http.routers.authelia.tls.certresolver=letsencrypt"
      - "traefik.http.middlewares.authelia.forwardauth.address=http://authelia:9091/api/verify?rd=https://auth.example.com"
      - "traefik.http.middlewares.authelia.forwardauth.trustForwardHeader=true"
      - "traefik.http.middlewares.authelia.forwardauth.authResponseHeaders=Remote-User,Remote-Groups,Remote-Name,Remote-Email"

  mygarage:
    # ... existing mygarage config
    labels:
      # Existing labels...
      - "traefik.http.routers.mygarage.middlewares=authelia@docker"

Authelia Configuration (authelia/configuration.yml):

jwt_secret: your-authelia-secret
default_redirection_url: https://mygarage.example.com

authentication_backend:
  file:
    path: /config/users_database.yml

access_control:
  default_policy: deny
  rules:
    - domain: mygarage.example.com
      policy: two_factor  # Require 2FA

session:
  secret: your-session-secret
  domain: example.com

Now users must authenticate via Authelia (with optional 2FA) before accessing MyGarage.

oauth2-proxy with Google

oauth2-proxy Docker Compose:

services:
  oauth2-proxy:
    image: quay.io/oauth2-proxy/oauth2-proxy:latest
    container_name: oauth2-proxy
    command:
      - --http-address=0.0.0.0:4180
      - --upstream=http://mygarage:8686
      - --provider=google
      - --client-id=<google-client-id>
      - --client-secret=<google-client-secret>
      - --cookie-secret=<random-32-char-secret>
      - --email-domain=example.com  # Only allow @example.com emails
      - --redirect-url=https://mygarage.example.com/oauth2/callback
    networks:
      - traefik-proxy
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.oauth2-proxy.rule=Host(`mygarage.example.com`)"
      - "traefik.http.routers.oauth2-proxy.entrypoints=https"
      - "traefik.http.routers.oauth2-proxy.tls.certresolver=letsencrypt"

Security Headers Explained

HSTS (HTTP Strict Transport Security)

Forces browsers to always use HTTPS:

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
  • max-age=31536000 - 1 year (browsers remember to use HTTPS)
  • includeSubDomains - Apply to all subdomains
  • preload - Allow inclusion in browser HSTS preload list

X-Content-Type-Options

Prevents MIME type sniffing:

add_header X-Content-Type-Options "nosniff" always;

X-Frame-Options

Prevents clickjacking attacks:

add_header X-Frame-Options "SAMEORIGIN" always;
  • SAMEORIGIN - Only allow framing from same domain
  • DENY - Never allow framing (more restrictive)

Content Security Policy (CSP)

Controls which resources can be loaded.

MyGarage sends its own Content-Security-Policy on every response, so you do not need to add one. A second policy set at the proxy is enforced on top of MyGarage's and can only block more, for example the map tiles.

Referrer-Policy

Controls how much referrer information is sent:

add_header Referrer-Policy "strict-origin-when-cross-origin" always;

Rate Limiting

Nginx Rate Limiting

Protect against brute-force login attacks:

# Define rate limit zone
limit_req_zone $binary_remote_addr zone=mygarage_login:10m rate=5r/m;

server {
    # ... existing config

    # Apply rate limit to auth endpoints
    location /api/auth/login {
        limit_req zone=mygarage_login burst=10 nodelay;
        proxy_pass http://localhost:8686;
        # Every location that proxies to MyGarage needs the full set
        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_set_header X-Forwarded-Host $host;
    }
}
  • rate=5r/m - 5 requests per minute
  • burst=10 - Allow bursts up to 10 requests
  • nodelay - Don't delay excess requests, reject immediately

Traefik Rate Limiting

labels:
  # Rate limit middleware
  - "traefik.http.middlewares.mygarage-ratelimit.ratelimit.average=100"
  - "traefik.http.middlewares.mygarage-ratelimit.ratelimit.burst=50"

  # Apply to router
  - "traefik.http.routers.mygarage.middlewares=mygarage-ratelimit"

IP Whitelisting

Nginx IP Whitelisting

Allow only specific IP addresses:

server {
    # ... existing config

    # Allow only these IPs
    allow 192.168.1.0/24;  # Local network
    allow 10.0.0.0/8;      # VPN
    deny all;              # Block everyone else

    location / {
        proxy_pass http://localhost:8686;
        # ... proxy headers
    }
}

Traefik IP Whitelisting

labels:
  # IP whitelist middleware
  - "traefik.http.middlewares.mygarage-ipwhitelist.ipwhitelist.sourcerange=192.168.1.0/24,10.0.0.0/8"

  # Apply to router
  - "traefik.http.routers.mygarage.middlewares=mygarage-ipwhitelist"

Troubleshooting

"502 Bad Gateway" (Nginx/Caddy)

Cause: MyGarage container not running or wrong port

Fix:

# Check MyGarage is running
docker ps | grep mygarage

# Verify MyGarage is listening on port 8686
curl http://localhost:8686/health

# Check reverse proxy can reach MyGarage
# If using Docker, ensure same network or use container name

"Invalid redirect URI" (OIDC with Reverse Proxy)

Cause: OIDC redirect URI doesn't match public URL

Fix: MyGarage builds the callback from the request, so make sure the proxy passes Host (or X-Forwarded-Host) and X-Forwarded-Proto. Then open Settings → System → Configure OIDC through your public URL and register the Callback URL it shows with your provider, e.g. https://mygarage.example.com/api/auth/oidc/callback (not localhost).

With MYGARAGE_TRUSTED_PROXIES set, look for Dropped proxy headers from <peer> in MyGarage's log (docker logs mygarage). It means that proxy isn't in the list, so its X-Forwarded-Proto and X-Forwarded-Host are ignored, and the callback URL (and the redirect back to the app after sign-in) gets built with MyGarage's internal scheme, usually http, and whatever Host the proxy sent. Add the proxy to MYGARAGE_TRUSTED_PROXIES. See Client IP and Rate Limits.

SSL Certificate Errors

Let's Encrypt Rate Limits:

  • 50 certificates per registered domain per week
  • 5 duplicate certificates per week

Fix:

  • Use staging environment for testing: --staging (Certbot) or acme_ca https://acme-staging-v02.api.letsencrypt.org/directory (Caddy)
  • Wait 7 days for rate limit to reset

"Too Many Redirects"

Cause: Redirect loop between reverse proxy and MyGarage

Fix:

  • Ensure X-Forwarded-Proto header is set correctly
  • Check both reverse proxy and MyGarage aren't forcing HTTPS redirects

Testing Your Configuration

SSL Labs Test

Test your HTTPS configuration:

https://www.ssllabs.com/ssltest/analyze.html?d=mygarage.example.com

Target Grade: A or A+

Security Headers Test

https://securityheaders.com/?q=https://mygarage.example.com

Target Grade: A or A+

DNS Propagation Check

https://www.whatsmydns.net/#A/mygarage.example.com

Verify your domain resolves to correct IP globally.


Need Help?


Next: Read the FAQ or check out Troubleshooting Guide.

Clone this wiki locally