OAuth2 in Front of Every Service, for Free
Series: You Don’t Need a VPC | Post 5 of 5
The Problem
Most self-hosted services have their own auth: Home Assistant has its own login, VS Code Server has a password, etc. Managing N separate credential sets is annoying, and some services have weak auth or none at all.
The pattern that scales: put oauth2-proxy in front of everything. One login (Google), one session cookie, one place to manage who’s allowed in.
oauth2-proxy as a Sidecar
For each protected service, run an oauth2-proxy container that handles the auth flow and proxies through to the real service:
homeassistant-proxy:
image: quay.io/oauth2-proxy/oauth2-proxy:latest
environment:
- OAUTH2_PROXY_UPSTREAMS=http://192.168.50.249:8123
- OAUTH2_PROXY_EMAIL_DOMAINS=*
- OAUTH2_PROXY_AUTHENTICATED_EMAILS_FILE=/etc/oauth2-proxy/allowed-emails.txt
- VIRTUAL_HOST=homeassistant.mukulhase.com
volumes:
- ./allowed-emails.txt:/etc/oauth2-proxy/allowed-emails.txt:roThe VIRTUAL_HOST label registers it with nginx-proxy. All traffic hits oauth2-proxy first; only authenticated, allowlisted users get through to the real service.
Email Allowlist
Instead of managing users in Auth0 or Google Workspace, a flat file works fine for a household:
[email protected]
[email protected]
Mount it read-only into every proxy container. Adding someone: edit the file, redeploy. Removing someone: same.
Auth0 for Richer Identity
Some services need more than “is this a valid Google user” — they need the user’s identity in a JWT claim for downstream authorization. For the home MCP server, Auth0 sits between Google and the service:
User → Auth0 (Google as IdP) → JWT with email claim → home-mcp
An Auth0 Action adds the email to the access token:
exports.onExecutePostLogin = async (event, api) => {
api.accessToken.setCustomClaim('email', event.user.email);
};This Action is managed in Terraform — not manually configured in the Auth0 dashboard:
resource "auth0_action" "add_email_claim" {
name = "Add email to access token"
runtime = "node18"
deploy = true
code = file("${path.module}/auth0_action.js")
supported_triggers {
id = "post-login"
version = "v3"
}
}The home-mcp service validates the JWT and checks the email claim against its own allowlist. Auth0 handles the Google OAuth dance; the service handles authorization.
Two Users, Two Environments
The Claude Code (openclaw) setup has two separate instances — one for each person in the household. Each gets its own:
- Container with its own workspace volume
- oauth2-proxy with its own allowed-emails list
- Subdomain
openclaw:
environment:
- VIRTUAL_HOST=openclaw.mukulhase.com
openclaw-auro:
environment:
- VIRTUAL_HOST=openclaw-auro.mukulhase.com
openclaw-proxy:
environment:
- OAUTH2_PROXY_AUTHENTICATED_EMAILS_FILE=/etc/oauth2-proxy/allowed-emails.txt
openclaw-proxy-auro:
environment:
- OAUTH2_PROXY_AUTHENTICATED_EMAILS_FILE=/etc/oauth2-proxy/allowed-emails-auro.txtSame infrastructure, different access control. Neither user can reach the other’s environment.
The Result
Every service in the stack is behind auth. No service needs to implement auth itself. The access control layer is a flat file and a Terraform file — both version-controlled, both auditable.
This is what a VPC security group + IAM policy gives you in AWS. Here it’s oauth2-proxy + a text file.
Series Wrap-Up
The full stack:
- Cloudflare Tunnel replaces public IP exposure + cert management
- nginx-proxy + docker-gen replaces load balancer + service discovery
- Rootless Podman + SELinux replaces IAM instance roles + security groups
- 1Password CLI replaces Secrets Manager
- oauth2-proxy + Auth0 replaces Cognito + API Gateway authorizers
Total monthly cost: $0 (Cloudflare free tier, 1Password personal, hardware you already own).
You don’t need a VPC.