Skip to content

Latest commit

 

History

History
178 lines (125 loc) · 4.45 KB

File metadata and controls

178 lines (125 loc) · 4.45 KB

PostgREST JWT Gateway (Nginx + auth_request)

Author: Mr. Watson 🦄 Date: 2026-02-15

Goal

Add a self-hosted JWT access layer in front of PostgREST for monetizable endpoints, without breaking existing routes.

What was configured

  • New local auth service validates X-API-Key and signs short-lived JWTs
  • Nginx uses auth_request to call that service
  • Protected routes inject Authorization: Bearer <jwt> to PostgREST
  • Existing beachlab.org/api/telemetry/... remains unchanged

Architecture

Client
  -> Nginx (api.beachlab.org)
      -> /telemetry/public/*  -> PostgREST (anon)
      -> /telemetry/pro/*     -> auth_request -> auth-jwt service
                                   (if valid key => signed JWT)
                                -> PostgREST (JWT role)

Files

  • /usr/local/bin/auth_jwt_service.py
  • /etc/auth-jwt-service.env
  • /etc/auth-jwt-keys.csv
  • /etc/systemd/system/auth-jwt.service
  • /etc/postgrest-telemetry.conf
  • /etc/nginx/sites-available/api.beachlab.org

Routes

Public (no key):

  • https://api.beachlab.org/telemetry/public/telemetry_latest?limit=1

Protected (requires X-API-Key):

  • https://api.beachlab.org/telemetry/pro/telemetry_latest?limit=1

Auth health:

  • https://api.beachlab.org/auth/health

Secrets and keys

Current runtime secrets are stored in:

  • /etc/auth-jwt-service.env (JWT_SECRET, TTL, host/port)
  • /etc/auth-jwt-keys.csv (api_key,role,plan,enabled,expires_at_utc)

Generate a strong JWT secret (example):

openssl rand -hex 32

Permissions:

sudo chmod 600 /etc/auth-jwt-service.env /etc/auth-jwt-keys.csv

Operations

# service health
systemctl status auth-jwt postgrest-telemetry nginx

# auth service logs
journalctl -u auth-jwt -n 80 --no-pager

# local tests (force host header to local nginx)
curl -ks -H 'Host: api.beachlab.org' https://127.0.0.1/auth/health
curl -ks -o /dev/null -w '%{http_code}\n' \
  -H 'Host: api.beachlab.org' \
  https://127.0.0.1/telemetry/public/telemetry_latest?limit=1
curl -ks -o /dev/null -w '%{http_code}\n' \
  -H 'Host: api.beachlab.org' \
  https://127.0.0.1/telemetry/pro/telemetry_latest?limit=1
curl -ks -o /dev/null -w '%{http_code}\n' \
  -H 'Host: api.beachlab.org' \
  -H 'X-API-Key: <YOUR_KEY>' \
  https://127.0.0.1/telemetry/pro/telemetry_latest?limit=1

Expected:

  • public: 200
  • protected without key: 401
  • protected with valid key: 200

Add/rotate API keys

Current plan mapping in this setup:

  • free -> web_anon
  • free_trial (30 days) -> web_trial
  • pro -> web_paid

Append keys:

# free
FREE_KEY=$(openssl rand -hex 24)
echo "$FREE_KEY,web_anon,free,1," | sudo tee -a /etc/auth-jwt-keys.csv

# trial (30 days)
TRIAL_KEY=$(openssl rand -hex 24)
TRIAL_EXP=$(date -u -d '+30 days' '+%Y-%m-%dT%H:%M:%SZ')
echo "$TRIAL_KEY,web_trial,free_trial,1,$TRIAL_EXP" | sudo tee -a /etc/auth-jwt-keys.csv

# paid
PAID_KEY=$(openssl rand -hex 24)
echo "$PAID_KEY,web_paid,pro,1," | sudo tee -a /etc/auth-jwt-keys.csv

sudo systemctl restart auth-jwt

Disable a key by setting enabled to 0.

Promote from free to paid roles

This host already has role skeletons:

  • web_anon
  • web_trial
  • web_paid

Next hardening step (when monetization starts):

  1. Restrict paid objects to web_paid
  2. Keep free objects on web_anon/web_trial
  3. Add RLS if tenant/data partitioning is needed

Rollback

Restore backups and reload:

# restore previous nginx file (pick your timestamp)
sudo cp /etc/nginx/sites-available/api.beachlab.org.bak.<timestamp> /etc/nginx/sites-available/api.beachlab.org

# restore previous PostgREST config (pick your timestamp)
sudo cp /etc/postgrest-telemetry.conf.bak.<timestamp> /etc/postgrest-telemetry.conf

sudo nginx -t && sudo systemctl reload nginx
sudo systemctl restart postgrest-telemetry
sudo systemctl disable --now auth-jwt

Notes

  • This design is fully self-hosted, no SaaS/free-tier dependency.
  • Keep PostgREST bound to localhost and expose only through Nginx.
  • Add rate limits in Nginx as next step if needed (limit_req).