ATAuth Homelab Deployment Guide#
ATAuth provides OIDC-based authentication for your homelab using AT Protocol identities. Users sign in with their @handle (Bluesky or self-hosted PDS) and your apps receive standard OIDC tokens.
Prerequisites#
- Docker and Docker Compose
- A domain name with DNS pointing to your server
- A reverse proxy that terminates TLS (Caddy, Traefik, or nginx)
- Optional: Your own PDS for fully self-hosted identity
Deploy#
git clone https://github.com/Cache8063/atauth.git
cd atauth
cp gateway/.env.example .env
Edit .env:
- Replace every
auth.yourdomain.comwith your actual domain - Set
CORS_ORIGINSto the domains of apps that will use ATAuth - Generate three secrets (
openssl rand -hex 32for each) and fill inADMIN_TOKEN,OIDC_KEY_SECRET, andMFA_ENCRYPTION_KEY
See gateway/.env.example for all available options including email, forward-auth proxy, and passkey configuration.
Start the gateway:
docker compose up -d
The gateway runs on 127.0.0.1:3100. Set up a reverse proxy with TLS (see Reverse Proxy Configuration below).
Verify it's running:
curl https://your-atauth-domain/.well-known/openid-configuration
Register Apps#
Option 1: Admin Dashboard (Recommended)#
Open https://your-atauth-domain/admin/login, enter your admin token, then use the Setup Wizard to register apps. It includes presets for:
- Audiobookshelf, Jellyfin, Gitea/Forgejo, Nextcloud, Immich
- Grafana, Wiki.js, Portainer, Outline, Mealie
- Paperless-ngx, Vaultwarden, Miniflux, Mattermost, Vikunja
- Plane, GoToSocial, Stirling-PDF, Tandoor Recipes, FreshRSS
Each preset auto-fills the correct redirect URI, scopes, and grant types.
Option 2: Admin API#
curl -X POST https://your-atauth-domain/admin/oidc/clients \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"id": "jellyfin",
"name": "Jellyfin",
"redirect_uris": ["https://jellyfin.example.com/sso/OID/redirect/ATAuth"],
"grant_types": ["authorization_code", "refresh_token"],
"scopes": ["openid", "profile", "email"],
"require_pkce": true,
"token_endpoint_auth_method": "client_secret_basic"
}'
Save the returned client_secret -- it is only shown once.
Configure Your App#
Point your app's OIDC settings to ATAuth's discovery URL:
https://your-atauth-domain/.well-known/openid-configuration
This auto-discovers all endpoints. Most apps need:
| Setting | Value |
|---|---|
| Discovery URL | https://your-atauth-domain/.well-known/openid-configuration |
| Client ID | The ID you chose when registering |
| Client Secret | The secret returned at registration |
| Scopes | openid profile email |
Forward-Auth Proxy#
For apps without OIDC support, ATAuth provides nginx auth_request based SSO. Enable it in .env:
FORWARD_AUTH_ENABLED=true
FORWARD_AUTH_SESSION_SECRET=<generate-with-openssl-rand-hex-32>
Then:
- Register the origin in the admin dashboard under Proxy Origins
- Add access rules under Access Rules
- Use the Proxy Setup Wizard to generate nginx config snippets
Example nginx config:
location / {
auth_request /auth/verify;
auth_request_set $auth_did $upstream_http_x_auth_did;
auth_request_set $auth_handle $upstream_http_x_auth_handle;
proxy_set_header X-Auth-DID $auth_did;
proxy_set_header X-Auth-Handle $auth_handle;
proxy_pass http://your-app;
}
location = /auth/verify {
internal;
proxy_pass http://atauth:3100;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
}
Reverse Proxy Configuration#
Caddy#
auth.example.com {
reverse_proxy localhost:3100
}
Traefik#
labels:
- "traefik.enable=true"
- "traefik.http.routers.atauth.rule=Host(`auth.example.com`)"
- "traefik.http.routers.atauth.entrypoints=websecure"
- "traefik.http.routers.atauth.tls.certresolver=letsencrypt"
- "traefik.http.services.atauth.loadbalancer.server.port=3100"
nginx#
server {
listen 443 ssl http2;
server_name auth.example.com;
ssl_certificate /etc/ssl/certs/auth.example.com.crt;
ssl_certificate_key /etc/ssl/private/auth.example.com.key;
location / {
proxy_pass http://localhost:3100;
proxy_http_version 1.1;
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;
}
}
Self-Hosted PDS#
With your own PDS, ATAuth becomes fully independent:
- Users get handles like
@alice.your-domain.com - No dependency on Bluesky
- Works on air-gapped networks
The gateway auto-discovers the user's PDS from their handle. No special configuration needed -- just ensure the PDS is reachable.
Access Control#
ATAuth supports per-user access rules:
- Allow/deny by DID: Target specific AT Protocol identities
- Allow/deny by handle pattern:
*.your-domain.commatches all handles on a PDS domain - Deny overrides: Deny rules always win regardless of order
- Per-origin scoping: Rules can target a specific origin or apply globally
Manage rules via the admin dashboard or API.
Backup#
# Backup the SQLite database
docker compose exec atauth cp /app/data/gateway.db /app/data/gateway.db.backup
# Or backup the entire data volume
docker run --rm -v atauth_atauth-data:/data -v $(pwd):/backup alpine \
tar czf /backup/atauth-backup.tar.gz -C /data .
Updating#
Using the pre-built image from GHCR:
docker pull ghcr.io/cache8063/atauth-gateway:latest
docker compose up -d
Or if building locally:
git pull
docker compose build
docker compose up -d
Troubleshooting#
OIDC discovery returns 404#
Ensure OAUTH_CLIENT_ID in .env matches your public URL and the gateway is running. Check logs with docker compose logs atauth.
Users can't authenticate#
- Check the PDS is reachable from the ATAuth container:
docker compose exec atauth wget -qO- https://bsky.social/xrpc/_health - Verify the user's handle resolves correctly
- Check logs:
docker compose logs atauth
"invalid_redirect_uri"#
The redirect URI in the OIDC request must exactly match one registered for the client (including scheme, host, port, and path).
CORS errors in browser console#
Add the app's origin to CORS_ORIGINS in .env and restart: docker compose restart atauth.