Traefik Local Auth Proxy

Updated January 28, 2026 6 minutes

If you need to access an HTTP service that requires authentication (Bearer or Basic auth), but you don’t want to put the API token anywhere near your code, you can use this script to create a localhost-only proxy for that service. The proxy accepts unauthenticated requests originating only from 127.0.0.1, and it will inject the API token into your requests, and forward them to the upstream server.

This kind of thing might also be useful for integration with third party software that doesn’t support authentication. As a pilot case, this script was designed for the integration of Ollama with Home Assistant, but HA doesn’t have an option to add a Bearer auth token for Ollama. HA can now talk to the local proxy and the proxy injects the API token into the request.

Get the script

Download the script:

wget https://raw.githubusercontent.com/EnigmaCurry/blog.rymcg.tech/master/src/traefik/traefik_local_auth_proxy.sh

Make the script executable:

chmod a+x traefik_local_auth_proxy.sh

Install Traefik

If you have Nix installed, Traefik will be installed automatically by the script. If not, you can download the latest binary of Traefik, and install it in your PATH (e.g., /usr/local/bin/traefik).

Examples

  # Run in foreground
  ./traefik_local_auth_proxy.sh \
    --upstream https://api.example.com \
    --token 'abc123'
  # Install & enable as user service
  ./traefik_local_auth_proxy.sh \
    --upstream https://api.example.com \
    --token 'abc123' \
    --install-user-service
  # Basic auth style header (example)
  ./traefik_local_auth_proxy.sh \
    --upstream https://api.example.com \
    --token 'dXNlcjpwYXNz' \
    --auth-header Authorization \
    --auth-prefix 'Basic' \
    --install-user-service

The script

#!/usr/bin/env bash
set -euo pipefail

# traefik_local_auth_proxy.sh
#
# Localhost-only Traefik proxy that forwards to a remote upstream endpoint,
# injecting an Authorization header on the forwarded request.
#
# This is useful when your upstream requires an HTTP Authorization token but
# your local client (e.g. Home Assistant integration) cannot set headers.
#
# Traefik resolution:
#   1) If `traefik` is in PATH, use it.
#   2) Else, if `nix` is in PATH, run Traefik via `nix run`.
#   3) Else, error.
#
# Also supports installing/enabling a systemd *user* service:
#   --install-user-service installs unit + config under ~/.config and enables it
#   --uninstall-user-service removes them (and disables)
#
# SECURITY NOTE:
#   Your token is written into a config file on disk (permissions restricted).
#   Anyone who can read your user files could potentially recover it.
#
# Default listen: 127.0.0.1:11435
# Default auth scheme: Bearer
# Default TLS verify: ON (use --insecure-skip-verify for self-signed upstreams)

usage() {
  cat <<'EOF'
Usage:
  traefik_local_auth_proxy.sh [options] [--install-user-service|--uninstall-user-service]

Options:
  --upstream URL              Remote upstream base URL (e.g. https://host:port) (or env AUTH_PROXY_UPSTREAM)
  --token TOKEN               Token to inject (or env AUTH_PROXY_TOKEN)
  --auth-header NAME          Header name to inject (default: Authorization)
  --auth-prefix STR           Prefix before token (default: "Bearer")
  --listen ADDR:PORT          Local listen address (default: 127.0.0.1:11435)
  --log-level LEVEL           Traefik log level (default: INFO)
  --insecure-skip-verify      Skip TLS cert verification to upstream (self-signed). Default: OFF
  --nix-traefik-ref REF       Nix ref to run Traefik (default: nixpkgs#traefik)

Service actions (systemd --user):
  --install-user-service      Install + enable + start user service (name: traefik-local-auth-proxy)
  --uninstall-user-service    Disable + stop + remove user service files

Other:
  -h, --help                  Show help

Examples:
  # Run in foreground
  ./traefik_local_auth_proxy.sh \
    --upstream https://api.example.com \
    --token 'abc123'

  # Install & enable as user service
  ./traefik_local_auth_proxy.sh \
    --upstream https://api.example.com \
    --token 'abc123' \
    --install-user-service

  # Basic auth style header (example)
  ./traefik_local_auth_proxy.sh \
    --upstream https://api.example.com \
    --token 'dXNlcjpwYXNz' \
    --auth-header Authorization \
    --auth-prefix 'Basic' \
    --install-user-service

Notes:
  - The local proxy does NOT require auth; it injects the auth header to the upstream.
  - Listens on 127.0.0.1 by default (not accessible remotely).
EOF
}

# Defaults
LISTEN_ADDR="127.0.0.1:11435"
LOG_LEVEL="INFO"
INSECURE_SKIP_VERIFY="false"
AUTH_HEADER="Authorization"
AUTH_PREFIX="Bearer"
NIX_TRAEFIK_REF="nixpkgs#traefik"

# Inputs (can come from env)
UPSTREAM="${AUTH_PROXY_UPSTREAM:-}"
TOKEN="${AUTH_PROXY_TOKEN:-}"

# Service actions
DO_INSTALL_USER_SERVICE="false"
DO_UNINSTALL_USER_SERVICE="false"

# Parse args
while [[ $# -gt 0 ]]; do
  case "$1" in
    --upstream) UPSTREAM="${2:-}"; shift 2;;
    --token) TOKEN="${2:-}"; shift 2;;
    --auth-header) AUTH_HEADER="${2:-}"; shift 2;;
    --auth-prefix) AUTH_PREFIX="${2:-}"; shift 2;;
    --listen) LISTEN_ADDR="${2:-}"; shift 2;;
    --log-level) LOG_LEVEL="${2:-}"; shift 2;;
    --insecure-skip-verify) INSECURE_SKIP_VERIFY="true"; shift 1;;
    --nix-traefik-ref) NIX_TRAEFIK_REF="${2:-}"; shift 2;;
    --install-user-service) DO_INSTALL_USER_SERVICE="true"; shift 1;;
    --uninstall-user-service) DO_UNINSTALL_USER_SERVICE="true"; shift 1;;
    -h|--help) usage; exit 0;;
    *) echo "Unknown argument: $1" >&2; usage; exit 2;;
  esac
done

if [[ "$DO_INSTALL_USER_SERVICE" == "true" && "$DO_UNINSTALL_USER_SERVICE" == "true" ]]; then
  echo "ERROR: Choose only one of --install-user-service or --uninstall-user-service" >&2
  exit 2
fi

need_inputs="true"
if [[ "$DO_UNINSTALL_USER_SERVICE" == "true" ]]; then
  need_inputs="false"
fi

if [[ "$need_inputs" == "true" ]]; then
  if [[ -z "$UPSTREAM" ]]; then
    echo "ERROR: --upstream (or env AUTH_PROXY_UPSTREAM) is required" >&2
    usage
    exit 2
  fi
  if [[ -z "$TOKEN" ]]; then
    echo "ERROR: --token (or env AUTH_PROXY_TOKEN) is required" >&2
    usage
    exit 2
  fi
fi

# Decide how to run Traefik
TRAEFIK_MODE=""
if command -v traefik >/dev/null 2>&1; then
  TRAEFIK_MODE="path"
elif command -v nix >/dev/null 2>&1; then
  TRAEFIK_MODE="nix"
else
  echo "ERROR: traefik not found in PATH, and nix not found either." >&2
  echo "Install traefik, or install nix so the script can run Traefik via nix." >&2
  exit 127
fi

# Service install paths (systemd --user)
SERVICE_NAME="traefik-local-auth-proxy"
USER_SYSTEMD_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
USER_CFG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/${SERVICE_NAME}"
USER_CFG_FILE="${USER_CFG_DIR}/dynamic.yml"
USER_ENV_FILE="${USER_CFG_DIR}/env"
USER_UNIT_FILE="${USER_SYSTEMD_DIR}/${SERVICE_NAME}.service"

write_dynamic_config_file() {
  local cfg_path="$1"
  umask 077
  cat > "$cfg_path" <<EOF
http:
  routers:
    local_auth_proxy:
      rule: "PathPrefix(\`/\`)"
      entryPoints: ["local"]
      service: "upstream"
      middlewares: ["inject-auth"]

  middlewares:
    inject-auth:
      headers:
        customRequestHeaders:
          ${AUTH_HEADER}: "${AUTH_PREFIX} ${TOKEN}"
EOF

  if [[ "$INSECURE_SKIP_VERIFY" == "true" ]]; then
    cat >> "$cfg_path" <<'EOF'

  serversTransports:
    upstream-transport:
      insecureSkipVerify: true
EOF
  fi

  cat >> "$cfg_path" <<EOF

  services:
    upstream:
      loadBalancer:
        passHostHeader: false
EOF

  if [[ "$INSECURE_SKIP_VERIFY" == "true" ]]; then
    cat >> "$cfg_path" <<'EOF'
        serversTransport: "upstream-transport"
EOF
  fi

  cat >> "$cfg_path" <<EOF
        servers:
          - url: "${UPSTREAM}"
EOF
  chmod 600 "$cfg_path"
}

install_user_service() {
  if ! command -v systemctl >/dev/null 2>&1; then
    echo "ERROR: systemctl not found. Is systemd available?" >&2
    exit 127
  fi

  mkdir -p "$USER_SYSTEMD_DIR" "$USER_CFG_DIR"
  chmod 700 "$USER_CFG_DIR"

  # Write env file (still stores secret on disk; permissions restricted)
  umask 077
  cat > "$USER_ENV_FILE" <<EOF
AUTH_PROXY_UPSTREAM=${UPSTREAM}
AUTH_PROXY_TOKEN=${TOKEN}
AUTH_HEADER=${AUTH_HEADER}
AUTH_PREFIX=${AUTH_PREFIX}
INSECURE_SKIP_VERIFY=${INSECURE_SKIP_VERIFY}
LISTEN_ADDR=${LISTEN_ADDR}
LOG_LEVEL=${LOG_LEVEL}
NIX_TRAEFIK_REF=${NIX_TRAEFIK_REF}
EOF
  chmod 600 "$USER_ENV_FILE"

  # Write dynamic config (bakes in header value)
  write_dynamic_config_file "$USER_CFG_FILE"

  # ExecStart: pin absolute traefik path if present; else use nix run
  local execstart=""
  if command -v traefik >/dev/null 2>&1; then
    execstart="$(command -v traefik)"
  else
    execstart="$(command -v nix) run ${NIX_TRAEFIK_REF} --"
  fi

  umask 077
  cat > "$USER_UNIT_FILE" <<EOF
[Unit]
Description=Traefik localhost auth-injecting proxy
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
EnvironmentFile=%h/.config/${SERVICE_NAME}/env
ExecStart=${execstart} \\
  --log.level=\${LOG_LEVEL} \\
  --entrypoints.local.address=\${LISTEN_ADDR} \\
  --providers.file.filename=%h/.config/${SERVICE_NAME}/dynamic.yml \\
  --providers.file.watch=true
Restart=on-failure
RestartSec=1

# Basic hardening (user service)
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=read-only
ProtectControlGroups=true
ProtectKernelModules=true
ProtectKernelTunables=true
LockPersonality=true
MemoryDenyWriteExecute=true
RestrictRealtime=true
RestrictSUIDSGID=true

ReadWritePaths=%h/.config/${SERVICE_NAME}
ReadWritePaths=%h/.cache/nix
ReadWritePaths=%h/.local/state/nix

[Install]
WantedBy=default.target
EOF
  chmod 600 "$USER_UNIT_FILE"

  systemctl --user daemon-reload
  systemctl --user enable --now "${SERVICE_NAME}.service"

  echo "Installed and started user service: ${SERVICE_NAME}.service"
  echo "Local endpoint: http://${LISTEN_ADDR}"
  echo "Test: curl -s http://${LISTEN_ADDR}/"
  echo "Logs: journalctl --user -u ${SERVICE_NAME}.service"
}

uninstall_user_service() {
  if ! command -v systemctl >/dev/null 2>&1; then
    echo "ERROR: systemctl not found. Is systemd available?" >&2
    exit 127
  fi

  systemctl --user disable --now "${SERVICE_NAME}.service" >/dev/null 2>&1 || true
  rm -f "$USER_UNIT_FILE"
  rm -rf "$USER_CFG_DIR"
  systemctl --user daemon-reload

  echo "Uninstalled user service: ${SERVICE_NAME}.service"
}

if [[ "$DO_UNINSTALL_USER_SERVICE" == "true" ]]; then
  uninstall_user_service
  exit 0
fi

if [[ "$DO_INSTALL_USER_SERVICE" == "true" ]]; then
  install_user_service
  exit 0
fi

# Foreground run mode: write temp config and exec Traefik (or nix run traefik)
umask 077
CFG="$(mktemp -t traefik-local-auth-proxy.XXXXXX.yml)"
cleanup() { rm -f "$CFG"; }
trap cleanup EXIT INT TERM

write_dynamic_config_file "$CFG"

echo "Starting Traefik localhost auth proxy:"
echo "  Listen : http://${LISTEN_ADDR}"
echo "  Upstream: ${UPSTREAM}"
echo "  Inject : ${AUTH_HEADER}: ${AUTH_PREFIX} <redacted>"
echo "  TLS skip-verify: ${INSECURE_SKIP_VERIFY}"
echo "  Config: ${CFG}"
echo
echo "Test:"
echo "  curl -s http://${LISTEN_ADDR}/"
echo

if [[ "$TRAEFIK_MODE" == "path" ]]; then
  exec traefik \
    --log.level="$LOG_LEVEL" \
    --entrypoints.local.address="$LISTEN_ADDR" \
    --providers.file.filename="$CFG" \
    --providers.file.watch=true
else
  exec nix run "$NIX_TRAEFIK_REF" -- \
    --log.level="$LOG_LEVEL" \
    --entrypoints.local.address="$LISTEN_ADDR" \
    --providers.file.filename="$CFG" \
    --providers.file.watch=true
fi


You can discuss this blog on Matrix (Element): #blog-rymcg-tech:enigmacurry.com

This blog is copyright EnigmaCurry and dual-licensed CC-BY-SA and MIT. The source is on github: enigmacurry/blog.rymcg.tech and PRs are welcome. ❤️