# 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) ```nginx 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) ```yaml # 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](#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](Authentication#self-hosted-provider-on-a-private-ip), 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](#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: ```bash 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](#basic-nginx-configuration), 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](#docker-compose-with-traefik-labels) 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](https://www.cloudflare.com/ips/). Leaving it unset isn't a recipe on its own either: see [Cloudflare Proxied DNS, No Tunnel](#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](#basic-nginx-configuration) 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: ```nginx 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. ```bash 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: ```bash 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](#basic-haproxy-configuration). 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: ```bash 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: ```bash 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](https://www.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): ```bash 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: ```yaml 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](https://developers.cloudflare.com/fundamentals/security/protect-your-origin-server/) rates an IP allow-list "moderately secure" and "vulnerable to IP spoofing", and a tunnel or Authenticated Origin Pulls "very secure". Its [headers reference](https://developers.cloudflare.com/fundamentals/reference/http-headers/) 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**: ```yaml 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`): ```yaml 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: ```yaml 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`**: ```nginx 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**: ```bash 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**: ```bash sudo apt install certbot python3-certbot-nginx ``` **Obtain Certificate**: ```bash sudo certbot --nginx -d garage.example.com ``` Certbot automatically: - Obtains SSL certificate - Updates Nginx configuration - Sets up auto-renewal **Verify Auto-Renewal**: ```bash 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**: ```yaml 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`): ```yaml 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**: ```yaml 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= - --client-secret= - --cookie-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: ```nginx 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: ```nginx add_header X-Content-Type-Options "nosniff" always; ``` ### X-Frame-Options Prevents clickjacking attacks: ```nginx 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: ```nginx add_header Referrer-Policy "strict-origin-when-cross-origin" always; ``` --- ## Rate Limiting ### Nginx Rate Limiting Protect against brute-force login attacks: ```nginx # 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 ```yaml 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: ```nginx 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 ```yaml 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**: ```bash # 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 ` 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](#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? - **Reverse Proxy Issues** - See [Troubleshooting Guide](Troubleshooting.md#reverse-proxy-problems) - **SSL Certificate Problems** - See [FAQ](FAQ.md#ssl-certificates) - **Community** - [GitHub Discussions](https://github.com/homelabforge/mygarage/discussions) --- **Next**: Read the [FAQ](FAQ) or check out [Troubleshooting Guide](Troubleshooting).