Authentication

Prax and TeamWork need a configured access layer before being exposed to users. This guide describes network access through Tailscale and identity-provider integration through an authentication proxy.

Synced from Prax at 35c345e5 View source ↗

← Guides

Prax and TeamWork need a configured access layer before being exposed to users. This guide describes network access through Tailscale and identity-provider integration through an authentication proxy.

Deployment limit: the current execution sandbox shares one filesystem. Authentication does not isolate mutually untrusted tenants. Use one deployment per untrusted tenant, with separate execution, data, and credentials. A trusted team sharing an environment is a different arrangement; its members must accept that shared boundary. See the single-tenant limitation and deployment topology.

Quick comparison

Approach Access model Configuration Best for
Tailscale Private network and tailnet policies Network setup One owner or a trusted team
OAuth2 Proxy + Google Google identity in front of the application Proxy and OAuth configuration Existing Google Workspace identity
OAuth2 Proxy + GitHub GitHub identity in front of the application Proxy and OAuth configuration Existing GitHub organization identity
Authentik (self-hosted) Self-hosted identity provider OIDC and proxy configuration Operators maintaining their own identity service

These are access patterns, not capacity or tenant-isolation claims. Verify the chosen proxy, application, and network configuration in your deployment.


Tailscale

Tailscale provides an encrypted private route governed by your tailnet access policy. It does not remove Prax’s existing host-published ports. Restrict those bindings or firewall them so traffic cannot bypass the intended access layer.

This is a useful starting point for one owner or a trusted team.

How it works

Public Internet ──X──> Your workstation (ports closed)

Your tailnet:
  Your laptop (100.64.0.1) ──> Workstation (100.64.0.2:3000) ──> TeamWork
  Colleague's laptop (100.64.0.3) ──> Same workstation ──> TeamWork

Tailscale assigns each device a stable IP on your tailnet (100.x.y.z). Traffic is encrypted end-to-end with WireGuard. No central server sees your data.

Step 1: Create a Tailscale account

Create an account at Tailscale. Check the current plan limits for your use case.

Step 2: Bring Prax onto the tailnet

You have two options here. Option 2a (recommended) runs tailscaled as a Docker sidecar in the Prax compose stack — no host tailscaled installation is needed, but the default host-published application ports still need restriction, and the Tailscale node identity is pinned to a Docker volume so container restarts don’t burn through device slots. Option 2b runs tailscaled on the host directly, which is fine if you already manage Tailscale system-wide.

Option 2a: Dockerized Tailscale sidecar (recommended)

  1. In the Tailscale admin console (Settings → Keys), generate a key with a non-ephemeral device identity for this persistent service. A reusable key supports re-enrollment; keep it private and scope it narrowly. Enable pre-approval only if your tailnet uses device approval. Persisted node state keeps the same identity across restarts. Ephemeral nodes are intended for short-lived workloads; their accounting depends on plan and lifetime, with nodes present for four hours treated as standard tagged devices. See Tailscale’s ephemeral-node documentation.
  2. Add the key to your Prax .env:
    TS_AUTHKEY=tskey-auth-...
    TS_HOSTNAME=prax
    COMPOSE_PROFILES=tailscale
    TEAMWORK_BASE_URL=https://prax.<your-tailnet>.ts.net
    
    Without the tailscale profile the sidecar does not start. Current Compose files still require a nonempty TS_AUTHKEY during interpolation; for local use with COMPOSE_PROFILES unset, use TS_AUTHKEY=unused. Replace that value with a real key before enabling this profile.
  3. docker compose up -d. The sidecar joins the tailnet automatically and serves https://prax.<tailnet>.ts.net/ (TeamWork) and https://prax.<tailnet>.ts.net:3001/ (Grafana, when the observability profile is also active).

Skip ahead to Step 3 below — you don’t need sudo tailscale up on the host.

Option 2b: Install on the host

# Install
curl -fsSL https://tailscale.com/install.sh | sh

# Start and authenticate
sudo tailscale up

# This prints a URL — open it in any browser to authenticate.
# Example: https://login.tailscale.com/a/abc123def456
# After authenticating, the node joins your tailnet.

# Verify
tailscale ip -4
# 100.64.0.2 (your tailnet IP)

tailscale status
# 100.64.0.2  workstation  you@github  linux  -

macOS:

# Install via App Store or Homebrew
brew install --cask tailscale

# Or download from https://tailscale.com/download/mac

# Start from menu bar icon, or:
sudo tailscale up

Step 3: Install on your client device (laptop, phone)

Install the Tailscale app on whatever device you’ll access TeamWork from:

Sign in with the same account. Both devices are now on your tailnet.

Step 4: Access TeamWork via tailnet IP

# Find your workstation's tailnet IP
tailscale ip -4
# 100.64.0.2

# Access TeamWork (replace with your IP)
open http://100.64.0.2:3000

That’s it. No port forwarding, no firewall rules, no certificates. If you’re not on the tailnet, you can’t reach port 3000.

Optional: Use a Tailscale hostname

Instead of remembering IPs, use MagicDNS:

# In Tailscale admin console (https://login.tailscale.com/admin/dns):
# Enable MagicDNS

# Now access via hostname:
open http://workstation.tail12345.ts.net:3000

Optional: HTTPS with Tailscale

Tailscale can provision TLS certificates for your tailnet hostnames:

# On your workstation
sudo tailscale cert workstation.tail12345.ts.net
# Creates workstation.tail12345.ts.net.crt and .key

# Configure your reverse proxy (nginx/Traefik/Caddy) to use these certs

Adding team members

# In Tailscale admin (https://login.tailscale.com/admin/users):
# 1. Click "Invite users"
# 2. They install Tailscale and join your tailnet
# 3. They can now reach your workstation's TeamWork

Access control (ACLs)

As your team grows, restrict who can access what:

// In Tailscale admin → Access Controls:
{
  "acls": [
    // Everyone can access TeamWork
    {"action": "accept", "src": ["group:team"], "dst": ["tag:prax:3000"]},
    // Only admins can access Prax API directly
    {"action": "accept", "src": ["group:admins"], "dst": ["tag:prax:5001"]},
    // Only admins can SSH
    {"action": "accept", "src": ["group:admins"], "dst": ["tag:prax:22"]},
  ]
}

Tailscale + Kubernetes

For K8s deployments, use the Tailscale Kubernetes operator:

helm repo add tailscale https://pkgs.tailscale.com/helmcharts
helm install tailscale tailscale/tailscale-operator -n tailscale --create-namespace \
  --set oauth.clientId=... --set oauth.clientSecret=...

Then annotate your TeamWork service:

apiVersion: v1
kind: Service
metadata:
  name: prax-teamwork
  annotations:
    tailscale.com/expose: "true"
    tailscale.com/hostname: "prax"
spec:
  # ...

TeamWork is now accessible at https://prax.tail12345.ts.net — only from your tailnet.


OAuth2 Proxy + Google OAuth

OAuth2 Proxy is a reverse proxy that requires authentication before any request reaches your app. It supports 20+ providers. This section covers Google — the most common for teams already using Google Workspace.

How it works

User ──> OAuth2 Proxy (:4180) ──> TeamWork (:8000)
              │
              └── Redirects to Google login
              └── Verifies token
              └── Sets cookie
              └── Forwards X-Forwarded-User, X-Forwarded-Email headers

TeamWork receives every request pre-authenticated. The user’s email is in the X-Forwarded-Email header — but note that TeamWork does not read X-Forwarded-* headers today (teamwork/src/teamwork/proxy_auth.py verifies a signed JWT assertion header, not these). OAuth2 Proxy is therefore a perimeter control: it only protects TeamWork if TeamWork is reachable exclusively through the proxy (Step 5 below). For an in-app check that also rejects requests which bypass the proxy, see Verified in-app check: PROXY_AUTH_*.

Step 1: Create a Google OAuth app

  1. Go to Google Cloud Console

  2. Create a project (or select an existing one):

    • Click the project dropdown at the top → “New Project”
    • Name: prax-auth (or whatever you like)
    • Click “Create”
  3. Enable the OAuth consent screen:

    • Navigate to APIs & Services → OAuth consent screen
    • Choose External (or Internal if you have Google Workspace and want to restrict to your org)
    • Fill in:
      • App name: Prax
      • User support email: your email
      • Authorized domains: your domain (e.g., example.com) or leave empty for testing
      • Developer contact: your email
    • Click “Save and Continue”
    • Scopes: click “Add or Remove Scopes” → select email and profile → “Update” → “Save and Continue”
    • Test users: add your email (required for External apps in testing mode)
    • Click “Save and Continue” → “Back to Dashboard”
  4. Create OAuth credentials:

    • Navigate to APIs & Services → Credentials
    • Click "+ Create Credentials" → “OAuth client ID”
    • Application type: Web application
    • Name: Prax OAuth
    • Authorized redirect URIs: add your callback URL:
      • For local: http://localhost:4180/oauth2/callback
      • For production: https://prax.yourdomain.com/oauth2/callback
    • Click “Create”
    • Copy the Client ID and Client Secret — you’ll need these next
# Random 32-byte secret for session cookies
python3 -c "import os,base64; print(base64.urlsafe_b64encode(os.urandom(32)).decode())"
# Example output: aBcDeFgHiJkLmNoPqRsTuVwXyZ0123456789_-AB

Step 3: Add OAuth2 Proxy to docker-compose

Create a docker-compose.override.yml (this layers on top of your existing docker-compose.yml without modifying it):

# docker-compose.override.yml
services:
  oauth2-proxy:
    image: quay.io/oauth2-proxy/oauth2-proxy:v7.7.1
    ports:
      - "4180:4180"
    environment:
      # Google OAuth
      OAUTH2_PROXY_PROVIDER: google
      OAUTH2_PROXY_CLIENT_ID: "YOUR_CLIENT_ID.apps.googleusercontent.com"
      OAUTH2_PROXY_CLIENT_SECRET: "YOUR_CLIENT_SECRET"
      OAUTH2_PROXY_COOKIE_SECRET: "YOUR_COOKIE_SECRET_FROM_STEP_2"

      # Where to send authenticated traffic (TeamWork's API is served by the
      # all-in-one `prax` container on port 8000)
      OAUTH2_PROXY_UPSTREAMS: "http://prax:8000"
      OAUTH2_PROXY_HTTP_ADDRESS: "0.0.0.0:4180"

      # Who can log in (restrict to your domain, or * for any Google account)
      OAUTH2_PROXY_EMAIL_DOMAINS: "*"
      # To restrict to your org: OAUTH2_PROXY_EMAIL_DOMAINS: "yourcompany.com"

      # Pass user identity to TeamWork
      OAUTH2_PROXY_SET_XAUTHREQUEST: "true"
      OAUTH2_PROXY_PASS_USER_HEADERS: "true"

      # Cookie settings
      OAUTH2_PROXY_COOKIE_SECURE: "false"  # Set to true with HTTPS
      OAUTH2_PROXY_COOKIE_HTTPONLY: "true"
      OAUTH2_PROXY_COOKIE_SAMESITE: "lax"

      # Session settings
      OAUTH2_PROXY_SESSION_STORE_TYPE: "cookie"
      OAUTH2_PROXY_SKIP_PROVIDER_BUTTON: "true"  # Go straight to Google login

      # Redirect after login
      OAUTH2_PROXY_REDIRECT_URL: "http://localhost:4180/oauth2/callback"
    depends_on:
      prax:
        condition: service_healthy

Step 4: Start with auth

# Normal start — docker-compose.override.yml is auto-loaded
docker compose up -d

# Access via OAuth2 Proxy port (not TeamWork directly)
open http://localhost:4180
# You'll be redirected to Google login → then back to TeamWork

Step 5: Restrict direct access to TeamWork

Remove the TeamWork port mapping from docker-compose so users can only reach it through the proxy:

# docker-compose.override.yml — add this to hide TeamWork's direct port
services:
  teamwork:
    ports: !override []  # Remove the 3000:8000 and 8000:8000 mappings

Or if you prefer, just don’t expose port 3000 in your firewall.

Kubernetes setup

For K8s, add OAuth2 Proxy as a sidecar or separate deployment:

# In your values override:
# k8s/my-values.yaml
auth:
  enabled: true
  provider: google
  clientId: "YOUR_CLIENT_ID"
  clientSecret: "YOUR_CLIENT_SECRET"
  cookieSecret: "YOUR_COOKIE_SECRET"
  emailDomains: "yourcompany.com"

Or deploy OAuth2 Proxy via its own Helm chart:

helm repo add oauth2-proxy https://oauth2-proxy.github.io/manifests
helm install oauth2-proxy oauth2-proxy/oauth2-proxy \
  --set config.clientID="YOUR_CLIENT_ID" \
  --set config.clientSecret="YOUR_CLIENT_SECRET" \
  --set config.cookieSecret="YOUR_COOKIE_SECRET" \
  --set extraArgs.provider=google \
  --set extraArgs.upstream="http://prax-teamwork:8000" \
  --set extraArgs.email-domain="*" \
  --set ingress.enabled=true \
  --set ingress.hosts[0]=prax.yourdomain.com \
  -n prax

Verifying user identity in TeamWork

Once OAuth2 Proxy is in front of TeamWork, every request includes:

X-Forwarded-User: alice
X-Forwarded-Email: alice@yourcompany.com
X-Forwarded-Preferred-Username: alice
X-Forwarded-Groups: engineering,admins

TeamWork does not consume these headers as of 2026-09 — nothing in teamwork/src/teamwork/ reads X-Forwarded-User / X-Forwarded-Email, and the app has no per-user workspace routing (see Multi-user workspace routing, which is a design sketch, not shipped code). They are available to a future routing layer; today they are informational only, and identity is enforced at the proxy.


OAuth2 Proxy + GitHub OAuth

Same architecture as Google, but using GitHub as the identity provider. Better for dev teams and open source projects.

Step 1: Create a GitHub OAuth App

  1. Go to GitHub Developer Settings
  2. Click “OAuth Apps” → “New OAuth App”
  3. Fill in:
    • Application name: Prax
    • Homepage URL: http://localhost:4180 (or your domain)
    • Authorization callback URL: http://localhost:4180/oauth2/callback
  4. Click “Register application”
  5. Copy the Client ID
  6. Click “Generate a new client secret” → Copy the Client Secret

Step 2: Configure OAuth2 Proxy

# docker-compose.override.yml
services:
  oauth2-proxy:
    image: quay.io/oauth2-proxy/oauth2-proxy:v7.7.1
    ports:
      - "4180:4180"
    environment:
      OAUTH2_PROXY_PROVIDER: github
      OAUTH2_PROXY_CLIENT_ID: "YOUR_GITHUB_CLIENT_ID"
      OAUTH2_PROXY_CLIENT_SECRET: "YOUR_GITHUB_CLIENT_SECRET"
      OAUTH2_PROXY_COOKIE_SECRET: "YOUR_COOKIE_SECRET"
      OAUTH2_PROXY_UPSTREAMS: "http://prax:8000"
      OAUTH2_PROXY_HTTP_ADDRESS: "0.0.0.0:4180"
      OAUTH2_PROXY_EMAIL_DOMAINS: "*"
      OAUTH2_PROXY_SET_XAUTHREQUEST: "true"
      OAUTH2_PROXY_PASS_USER_HEADERS: "true"
      OAUTH2_PROXY_COOKIE_SECURE: "false"
      OAUTH2_PROXY_SKIP_PROVIDER_BUTTON: "true"
      OAUTH2_PROXY_REDIRECT_URL: "http://localhost:4180/oauth2/callback"
      # Optional: restrict to a GitHub org
      # OAUTH2_PROXY_GITHUB_ORG: "your-org"
      # Optional: restrict to a specific team
      # OAUTH2_PROXY_GITHUB_TEAM: "engineering"
    depends_on:
      prax:
        condition: service_healthy

Everything else is the same as the Google setup.


Authentik (self-hosted OIDC)

For full control over authentication — custom branding, LDAP/Active Directory integration, SCIM user provisioning, fine-grained policies, and audit logging. Runs as 3 containers alongside Prax.

When to use Authentik

  • You need to support multiple identity sources (Google + GitHub + LDAP)
  • You want branded login pages
  • You need audit trails for compliance
  • You need self-hosted identity policies for users of a trusted environment; untrusted tenants still require separate Prax execution and data boundaries
  • You don’t want to depend on external OAuth providers

Architecture

User ──> Authentik Proxy ──> TeamWork
              │
              └── Authentik Server (OIDC provider)
              └── PostgreSQL (auth database)
              └── Redis (sessions)

Setup overview

  1. Deploy Authentik (docker-compose or Helm chart)
  2. Create an OAuth2/OIDC application in Authentik’s admin UI
  3. Point OAuth2 Proxy at Authentik’s OIDC endpoints (or use Authentik’s built-in proxy provider)
  4. Configure allowed users/groups

Authentik’s own docs are excellent: https://docs.goauthentik.io

# docker-compose.override.yml — Authentik services
services:
  authentik-server:
    image: ghcr.io/goauthentik/server:latest
    command: server
    environment:
      AUTHENTIK_SECRET_KEY: "generate-a-long-random-string"
      AUTHENTIK_REDIS__HOST: authentik-redis
      AUTHENTIK_POSTGRESQL__HOST: authentik-db
      AUTHENTIK_POSTGRESQL__USER: authentik
      AUTHENTIK_POSTGRESQL__PASSWORD: authentik-password
      AUTHENTIK_POSTGRESQL__NAME: authentik
    ports:
      - "9000:9000"  # Authentik UI + OIDC endpoints
    depends_on:
      - authentik-db
      - authentik-redis

  authentik-worker:
    image: ghcr.io/goauthentik/server:latest
    command: worker
    environment:
      AUTHENTIK_SECRET_KEY: "same-secret-as-server"
      AUTHENTIK_REDIS__HOST: authentik-redis
      AUTHENTIK_POSTGRESQL__HOST: authentik-db
      AUTHENTIK_POSTGRESQL__USER: authentik
      AUTHENTIK_POSTGRESQL__PASSWORD: authentik-password
      AUTHENTIK_POSTGRESQL__NAME: authentik

  authentik-db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: authentik
      POSTGRES_PASSWORD: authentik-password
      POSTGRES_DB: authentik
    volumes:
      - authentik-db-data:/var/lib/postgresql/data

  authentik-redis:
    image: redis:7-alpine
    volumes:
      - authentik-redis-data:/data

volumes:
  authentik-db-data:
  authentik-redis-data:

After starting, access http://localhost:9000/if/flow/initial-setup/ to create the admin account, then configure an OAuth2 provider for Prax.


Verified in-app check: PROXY_AUTH_* (IAP / Cloudflare Access)

The one authenticating-proxy integration TeamWork actually implements is the PROXY_AUTH_* middleware (teamwork/src/teamwork/proxy_auth.py, settings in teamwork/src/teamwork/config.py). It is default off (the middleware is not even added). When PROXY_AUTH_ENABLED=true, every request except the exempt paths (PROXY_AUTH_EXEMPT_PATHS, default /health,/healthz) must carry a valid signed JWT assertion from the fronting proxy, verified against the provider’s JWKS — so a request that reaches the bound port without going through the proxy is rejected by the app itself (401), not only by the firewall. It is fail-closed: misconfiguration refuses to start.

PROXY_AUTH_ENABLED=true
PROXY_AUTH_PROVIDER=iap                 # preset: header x-goog-iap-jwt-assertion, ES256, Google JWKS
PROXY_AUTH_AUDIENCE=<IAP backend-service audience>   # REQUIRED when enabled
# or:
# PROXY_AUTH_PROVIDER=cloudflare_access   # preset: header cf-access-jwt-assertion, RS256
# PROXY_AUTH_AUDIENCE=<Access application AUD tag>
# PROXY_AUTH_ISSUER=https://<team>.cloudflareaccess.com   # JWKS URL derived from this

PROXY_AUTH_HEADER, PROXY_AUTH_JWKS_URL, PROXY_AUTH_ALGORITHMS override the preset for other providers. OAuth2 Proxy and Authentik (above) do not emit a JWT assertion header in these presets, so with them the perimeter (Step 5) is the control. Full deployment scenarios: network-exposure (Scenario B).

Multi-user workspace routing

Status (2026-09): design sketch, not implemented. TeamWork reads no identity header and Prax has no per-email workspace mapping; the flow below is what such a layer would look like.

Once authentication is in place and you’re ready for multi-user, the app needs to map authenticated users to workspaces. The flow:

OAuth2 Proxy ──> X-Forwarded-Email: alice@company.com ──> TeamWork
                                                              │
                                                              ├── Workspace: /workspaces/alice/
                                                              ├── Qdrant collection: prax-alice
                                                              ├── Neo4j namespace: prax_alice
                                                              └── Sandbox: prax-sandbox-2

With the Kubernetes operator, this mapping is automatic — creating a PraxWorkspace CR provisions everything. Without the operator, TeamWork would need a small middleware to read the header and route accordingly.


Security checklist

Item Single user Multi-user
Don’t expose TeamWork port publicly Use Tailscale or firewall Use OAuth2 Proxy
API keys in environment, not code .env file, chmod 600 K8s Secrets or cloud secrets manager
HTTPS Tailscale auto-certs or Let’s Encrypt cert-manager + ingress
Session cookies N/A COOKIE_SECURE=true, COOKIE_HTTPONLY=true
Restrict login Tailscale ACLs EMAIL_DOMAINS or GITHUB_ORG
Audit logging Prax health telemetry Authentik audit log
API key rotation Manual Automated via secrets manager

References