March 2026 · 7 min read
Internal tools are the worst-protected services in most developer stacks. Grafana with no password. An admin panel on a port that "only we know about." A metrics endpoint accessible from any machine on the VPN because nobody got around to adding auth.
This is how internal tools usually get protected: not at all, or with basic HTTP auth that's been in a Confluence doc since 2019.
Here's a practical approach that takes about 60 seconds per service.
Rather than adding auth to each service individually — which means code changes, library additions, and different implementations per service — put an auth proxy in front of every service. The proxy handles authentication. Your service just handles its actual job.
This is how large organizations handle it at scale (with tools like Envoy or OAuth2 Proxy). For small to mid-size teams, those tools are often overkill. Here's a simpler path.
Gate is a single Go binary that sits in front of any HTTP service. Set two environment variables, run it, and your service is protected:
curl -fsSL https://stockyard.dev/gate/install.sh | sh GATE_UPSTREAM=http://localhost:3000 \ GATE_ADMIN_KEY=your-secret-admin-key \ gate Proxy: http://localhost:8780 → http://localhost:3000 Admin API: http://localhost:8780/gate/api Dashboard: http://localhost:8780/ui
Every request to :8780 now requires authentication. Requests without a valid key get a 401 before they reach your service.
Issue a key for each consumer — a developer, a CI system, another service:
curl -s -X POST http://localhost:8780/gate/api/keys \
-H 'Authorization: Bearer your-secret-admin-key' \
-H 'Content-Type: application/json' \
-d '{"name":"alice"}'
# Returns: {"key":"sk-gate-...","note":"Save this — shown once"}
Consumers pass the key as a bearer token: Authorization: Bearer sk-gate-.... No key, no access. Revoking access is a single DELETE call — no code changes to the upstream service.
If the service is a web app (Grafana, an admin dashboard, a metrics page), you can add session-based login. Gate's POST /gate/login endpoint accepts username and password and sets an HTTP-only session cookie:
curl -s -X POST http://localhost:8780/gate/login \
-H 'Content-Type: application/json' \
-d '{"username":"alice","password":"secret"}'
For browser access, set GATE_CORS_ORIGINS to your allowed origins. The session cookie handles subsequent requests automatically.
Set GATE_RPM to enforce a requests-per-minute limit across all consumers. Gate Pro adds per-key rate limits if you need different limits for different callers.
Gate logs every request — method, path, status, latency, which key was used, and source IP. Query it via the admin API or browse the dashboard at /ui:
curl -s http://localhost:8780/gate/api/logs \ -H 'Authorization: Bearer your-secret-admin-key' | jq .
Gate uses the same binary in production. Set DATA_DIR to a persistent path, put it behind your reverse proxy (nginx, Caddy), and let TLS terminate at the reverse proxy layer. The Gate binary itself listens on HTTP.
For multiple services, run a separate Gate instance for each upstream. Each maintains its own SQLite database and key set. Gate Pro ($2.99/mo) removes the 1-upstream and 5-user limits on the free tier.
Gate handles API key auth and basic session auth well. It doesn't do SSO, 2FA, LDAP, or SAML. If you need those, look at Authelia or a full identity provider. Gate is for the 80% case: add solid auth to an internal service without standing up infrastructure.
Single binary. No code changes. Free to start.