Skip to content

karany97/pingate

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

pingate

The simplest signed-cookie PIN gate
for any local AI tool you don't want to expose raw.

License: MIT Auth: HMAC-SHA256 cookie Surface: FastAPI


You have a local AI tool — Open WebUI, Grafana, an internal dashboard, a chat backend — that you want to expose on the public internet via a Cloudflare Tunnel (or Tailscale Funnel, or a reverse-proxy). But you don't want to wire SSO yet, you don't want HTTP basic auth showing up in browser bars, and you don't want strangers hitting it.

pingate is one small FastAPI service that sits in front, asks for a PIN, verifies it via HMAC-SHA256 signed cookies, and reverse-proxies authenticated traffic to your upstream. Bonus: optional same-origin LiteLLM proxy (so your LLM master key never ships in a browser bundle), optional CSS theme injection, WebSocket passthrough.

        public internet
              │
              ▼
       ┌──────────────┐    cookie ok?    ┌─────────────────────┐
       │   pingate    │ ───── yes ─────▶ │  your upstream      │
       │   :3060      │                  │  (OWUI / Grafana /  │
       │              │ ───── no ──────▶ │   anything HTTP)    │
       │ PIN form +   │     /pin form    └─────────────────────┘
       │ signed cookie│
       └──────────────┘
              │
              │ (optional)
              ▼
       ┌──────────────────────┐
       │  /api/llm/* same-    │ ──── cookie ok + Origin ok ────▶
       │  origin LiteLLM      │     inject Bearer ${LITELLM_KEY}
       │  proxy mode          │     → LiteLLM proxy
       └──────────────────────┘     (key never leaves server)

Install

Docker (recommended)

docker run --rm -d \
  --name pingate \
  -p 3060:3060 \
  -e PINGATE_PIN=0000 \
  -e PINGATE_SECRET="$(openssl rand -hex 32)" \
  -e PINGATE_UPSTREAM=http://your-upstream:3050 \
  ghcr.io/karany97/pingate:latest

From source

git clone https://github.com/karany97/pingate.git
cd pingate
pip install -r requirements.txt
PINGATE_PIN=0000 \
PINGATE_SECRET="$(openssl rand -hex 32)" \
PINGATE_UPSTREAM=http://localhost:3050 \
PINGATE_PORT=3060 \
  python app.py

Verify

# Open in browser → PIN form appears
open http://localhost:3060/

# Enter PIN → cookie set → upstream rendered
# Refresh in next 30 days → upstream rendered (cookie persists)

How it works

  1. GET /pin → 1-field PIN form (vanilla HTML, no JS frameworks)
  2. POST /pin/check with pin=0000
  3. If PIN matches PINGATE_PIN, set Set-Cookie: pingate=<value>.<sig> where value is <unix_timestamp_30d_from_now> and sig is hmac_sha256(PINGATE_SECRET, value).hexdigest()
  4. Redirect to ?next= (defaults to /)
  5. Every subsequent request: verify the signature, check the timestamp hasn't expired, reverse-proxy to PINGATE_UPSTREAM with optional X-Trusted-User-Email: ${PINGATE_TRUSTED_EMAIL} header

The cookie is SameSite=Lax, Secure (when behind HTTPS), HttpOnly. 30-day expiry default (rotate PINGATE_SECRET to invalidate all outstanding cookies in one shot).

Why this exists

Cloudflare Access does this for $7/user/month + you have to give Cloudflare your auth provider. Caddy's basicauth directive does this without sessions (every request re-auths). Authelia / Authentik are overkill for "I just want a PIN in front of OWUI." pingate is the missing 500 LOC between "expose it raw" and "stand up a real IdP."

We've shipped this pattern in production for a year. It's solid. Now it's MIT.

Optional features

Same-origin LiteLLM proxy (the security win)

If you set LITELLM_MASTER_KEY in the environment, pingate mounts an extra route /api/llm/{path:path} that:

  1. Validates the gate cookie (same as the main proxy)
  2. Validates the request's Origin matches LITELLM_ALLOWED_ORIGIN
  3. Strips any Authorization header the client sent
  4. Injects Authorization: Bearer ${LITELLM_MASTER_KEY}
  5. Forwards to LITELLM_INTERNAL (defaults to http://127.0.0.1:8008)
  6. Streams SSE for chat/completions, handles CORS preflight

Your browser-side chat bundle calls fetch('/api/llm/v1/chat/completions', { credentials: 'include' }) with NO Bearer key. The key never appears in any JS your users can DevTools-inspect. Critical for any chat frontend you want to make fully public.

CSS theme injection

If you set PINGATE_THEME_CSS_PATH=/path/to/your.css, pingate injects the CSS into every HTML response from the upstream (just before </head>). This is the simplest path to re-theme an open-source tool without forking it.

WebSocket connections pass through untouched (no CSS injection on streams).

Trusted-header injection

Set PINGATE_TRUSTED_EMAIL and (optionally) PINGATE_TRUSTED_NAME — pingate adds them as X-Trusted-User-Email / X-Trusted-User-Name headers on every proxied request. Works directly with Open WebUI's WEBUI_AUTH_TRUSTED_EMAIL_HEADER auto-login, Grafana's auth.proxy, and anything else that does header-based SSO.

WebSocket proxy

Per-request WebSocket upgrades transparently. The cookie is checked once on the upgrade handshake; subsequent frames flow.

Configure

Env var Default What it does
PINGATE_PIN 0000 (CHANGE IT — sample only) The PIN users enter
PINGATE_SECRET dev placeholder — CHANGE IT HMAC-SHA256 key for signing cookies
PINGATE_UPSTREAM http://127.0.0.1:3050 What pingate reverse-proxies to
PINGATE_PORT 3060 pingate's listen port
PINGATE_COOKIE_NAME pingate Cookie name (lets multiple gates coexist)
PINGATE_TRUSTED_EMAIL (empty) Optional X-Trusted-User-Email value
PINGATE_TRUSTED_NAME (empty) Optional X-Trusted-User-Name value
PINGATE_EXTRA_HEADERS {} JSON dict of extra headers (e.g. {"X-Webauth-User":"admin"} for Grafana)
PINGATE_THEME_CSS_PATH (empty) Path to a CSS file to inject into HTML responses
LITELLM_MASTER_KEY (empty) Enables /api/llm/* same-origin proxy when set
LITELLM_INTERNAL http://127.0.0.1:8008 LiteLLM endpoint to inject the key into
LITELLM_PROXY_PATH /api/llm Mount point for the LiteLLM proxy
LITELLM_ALLOWED_ORIGIN (empty) Origin allowlist for CORS preflight

What's supported

Surface Status
HTTP reverse-proxy (GET/POST/PUT/PATCH/DELETE/HEAD/OPTIONS)
WebSocket reverse-proxy (Socket.IO friendly)
Same-origin LiteLLM proxy with key injection
CORS preflight with Origin allowlist
Streaming SSE pass-through
Theme CSS injection
Trusted-header SSO
Multi-instance (different cookie names)

What's NOT supported

  • Multi-user with per-user PINs. This is "one PIN for everyone with access." For multi-user, point pingate at a real IdP (Authelia, Authentik, Keycloak).
  • OAuth / OIDC. Same reason.
  • Rate-limiting. Put Caddy/nginx in front.

Compose with our other tools

You're using Plug pingate in like
Open WebUI cloudflared → pingate → OWUI. Set PINGATE_TRUSTED_EMAIL so OWUI auto-logs in.
Grafana cloudflared → pingate → Grafana. Set PINGATE_EXTRA_HEADERS='{"X-Webauth-User":"admin"}'.
Destiny Atelier The auth-proxy pattern Destiny Atelier ships is the same pattern as pingate's LITELLM_MASTER_KEY mode.
tooltalk + moa-router Stack: pingate → moa-router → tooltalk → Gemma 4. PIN at the edge, voting in the middle, tool-call shape at the inside.

License

MIT. Fork it, sell it, ship it.

Acknowledgements

Pattern extracted from the Destiny Atelier chat stack — the same gate that's been protecting atelier.example.com for a year, generalized into its own MIT repo because the same problem keeps coming up: "I want my local AI thing on the internet but I don't want a real IdP yet."

About

The simplest signed-cookie PIN gate for any local AI tool. HMAC-SHA256 cookie auth + reverse proxy + WebSocket passthrough + optional LiteLLM proxy + CSS theme injection. 544 LOC FastAPI. MIT.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors