|
| 1 | +# STATBUS Cloud Deployment |
| 2 | + |
| 3 | +This directory contains configuration files for running STATBUS in a multi-tenant cloud environment with a shared pgAdmin instance. |
| 4 | + |
| 5 | +## Architecture Overview |
| 6 | + |
| 7 | +``` |
| 8 | + ┌─────────────────────────────────────┐ |
| 9 | + │ Host Server │ |
| 10 | + │ │ |
| 11 | + Internet │ ┌─────────────────────────────┐ │ |
| 12 | + │ │ │ Host-level Caddy │ │ |
| 13 | + │ │ │ │ │ |
| 14 | + ├── HTTPS ──────────────────┼─►│ :443 (HTTPS termination) │ │ |
| 15 | + │ ma.statbus.org/* │ │ ├── /pgadmin ───────────────┼─┬─► pgAdmin (:$PGADMIN_PORT) |
| 16 | + │ no.statbus.org/* │ │ │ (forward_auth first)│ │ │ |
| 17 | + │ al.statbus.org/* │ │ ├── /rest/* ────────────┼───┼─┼─► tenant REST |
| 18 | + │ │ │ └── /* ─────────────────┼───┼─┼─► tenant app |
| 19 | + │ │ │ │ │ │ |
| 20 | + └── PostgreSQL (TLS+SNI) ───┼─►│ :5432 (Layer4 SNI routing) │ │ │ |
| 21 | + ma.statbus.org │ │ ├── @ma ────────────────┼───┼─┼─► ma DB (:3025) |
| 22 | + no.statbus.org │ │ ├── @no ────────────────┼───┼─┼─► no DB (:3035) |
| 23 | + al.statbus.org │ │ └── @al ────────────────┼───┼─┼─► al DB (:3045) |
| 24 | + │ └─────────────────────────────┘ │ │ |
| 25 | + │ │ │ |
| 26 | + │ ┌─────────────────────────────┐ │ │ |
| 27 | + │ │ Shared pgAdmin (:$PGADMIN_PORT)│◄──┼─┘ |
| 28 | + │ │ (cloud/docker-compose) │ │ |
| 29 | + │ └─────────────────────────────┘ │ |
| 30 | + │ │ |
| 31 | + │ ┌─────────────────────────────┐ │ |
| 32 | + │ │ Tenant: ma (offset 2) │ │ |
| 33 | + │ │ ├── app :3022 │ │ |
| 34 | + │ │ ├── rest :3023 │ │ |
| 35 | + │ │ ├── db :3024 (plain) │ │ |
| 36 | + │ │ └── db :3025 (TLS) │ │ |
| 37 | + │ └─────────────────────────────┘ │ |
| 38 | + │ │ |
| 39 | + │ ┌─────────────────────────────┐ │ |
| 40 | + │ │ Tenant: no (offset 3) │ │ |
| 41 | + │ │ ├── app :3032 │ │ |
| 42 | + │ │ ├── rest :3033 │ │ |
| 43 | + │ │ ├── db :3034 (plain) │ │ |
| 44 | + │ │ └── db :3035 (TLS) │ │ |
| 45 | + │ └─────────────────────────────┘ │ |
| 46 | + │ │ |
| 47 | + │ ┌─────────────────────────────┐ │ |
| 48 | + │ │ Tenant: al (offset 4) │ │ |
| 49 | + │ │ ... │ │ |
| 50 | + │ └─────────────────────────────┘ │ |
| 51 | + └─────────────────────────────────────┘ |
| 52 | +``` |
| 53 | + |
| 54 | +## Key Concepts |
| 55 | + |
| 56 | +### Deployment Slots |
| 57 | +Each tenant runs in a separate "slot" with isolated ports: |
| 58 | +- **Slot offset** determines port numbers: `base = 3000 + (offset × 10)` |
| 59 | +- Services per slot: HTTP, HTTPS, app, rest, db (plain), db (TLS) |
| 60 | + |
| 61 | +| Tenant | Offset | App Port | REST Port | DB Plain | DB TLS | |
| 62 | +|--------|--------|----------|-----------|----------|--------| |
| 63 | +| local | 1 | 3012 | 3013 | 3014 | 3015 | |
| 64 | +| ma | 2 | 3022 | 3023 | 3024 | 3025 | |
| 65 | +| no | 3 | 3032 | 3033 | 3034 | 3035 | |
| 66 | +| al | 4 | 3042 | 3043 | 3044 | 3045 | |
| 67 | + |
| 68 | +### pgAdmin Authentication Flow |
| 69 | +1. User visits `https://ma.statbus.org/pgadmin` |
| 70 | +2. Host Caddy's `forward_auth` calls `localhost:3023/rpc/auth_gate` (ma's PostgREST) |
| 71 | +3. `auth_gate` checks for valid JWT in cookies: |
| 72 | + - Valid JWT → returns 200 OK → Caddy proxies to pgAdmin |
| 73 | + - No/invalid JWT → returns 401 → Caddy redirects to login page |
| 74 | +4. In pgAdmin, user connects to database with their STATBUS credentials |
| 75 | +5. pgAdmin connects via external hostname (e.g., `ma.statbus.org:5432`) |
| 76 | +6. Host Caddy Layer4 routes by SNI to tenant's TLS port |
| 77 | + |
| 78 | +### Why Shared pgAdmin? |
| 79 | +- **Resource efficiency**: One pgAdmin instance serves all tenants |
| 80 | +- **Simplified management**: Single place for updates and configuration |
| 81 | +- **Tenant isolation maintained**: Each connection goes through proper auth |
| 82 | + |
| 83 | +## Setup Instructions |
| 84 | + |
| 85 | +### 1. Configure Tenant Instances |
| 86 | +Each tenant needs its own STATBUS deployment. In each tenant directory: |
| 87 | + |
| 88 | +```bash |
| 89 | +# Edit .env.config for each tenant |
| 90 | +DEPLOYMENT_SLOT_CODE=ma |
| 91 | +DEPLOYMENT_SLOT_PORT_OFFSET=2 |
| 92 | +CADDY_DEPLOYMENT_MODE=private # Important: private mode for cloud |
| 93 | +ENABLE_PGADMIN=false # Disable per-instance pgAdmin |
| 94 | + |
| 95 | +# Generate configuration |
| 96 | +./devops/manage-statbus.sh generate-config |
| 97 | + |
| 98 | +# Start tenant services |
| 99 | +./devops/manage-statbus.sh start all |
| 100 | +``` |
| 101 | + |
| 102 | +### 2. Configure Shared pgAdmin |
| 103 | + |
| 104 | +```bash |
| 105 | +cd cloud |
| 106 | + |
| 107 | +# Create configuration from examples |
| 108 | +cp .env.example .env |
| 109 | +cp servers.json.example servers.json |
| 110 | + |
| 111 | +# Edit .env - set secure password |
| 112 | +nano .env |
| 113 | + |
| 114 | +# Edit servers.json - add your tenant entries |
| 115 | +nano servers.json |
| 116 | + |
| 117 | +# Start shared pgAdmin |
| 118 | +docker compose -f docker-compose.pgadmin.yml up -d |
| 119 | +``` |
| 120 | + |
| 121 | +### 3. Configure Host-level Caddy |
| 122 | + |
| 123 | +Install Caddy with Layer4 plugin on the host (not in Docker): |
| 124 | + |
| 125 | +```bash |
| 126 | +# Install xcaddy |
| 127 | +go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest |
| 128 | + |
| 129 | +# Build Caddy with layer4 |
| 130 | +xcaddy build --with github.com/mholt/caddy-l4 |
| 131 | + |
| 132 | +# Copy example Caddyfile and customize |
| 133 | +cp Caddyfile.example /etc/caddy/Caddyfile |
| 134 | +nano /etc/caddy/Caddyfile |
| 135 | + |
| 136 | +# Start Caddy |
| 137 | +sudo systemctl start caddy |
| 138 | +``` |
| 139 | + |
| 140 | +### 4. Set Environment Variables for Caddy |
| 141 | + |
| 142 | +Caddy needs to know the pgAdmin port. Either: |
| 143 | + |
| 144 | +```bash |
| 145 | +# Option A: Export before starting Caddy |
| 146 | +export PGADMIN_PORT=5050 |
| 147 | +sudo -E systemctl start caddy |
| 148 | + |
| 149 | +# Option B: Add to systemd environment file |
| 150 | +echo "PGADMIN_PORT=5050" | sudo tee -a /etc/caddy/environment |
| 151 | +sudo systemctl restart caddy |
| 152 | +``` |
| 153 | + |
| 154 | +### 5. Verify Setup |
| 155 | + |
| 156 | +```bash |
| 157 | +# Check pgAdmin is running (use port from .env) |
| 158 | +curl -I http://localhost:5050/pgadmin |
| 159 | + |
| 160 | +# Check tenant REST is accessible |
| 161 | +curl http://localhost:3023/ |
| 162 | + |
| 163 | +# Test full flow (should redirect to login if not authenticated) |
| 164 | +curl -I https://ma.statbus.org/pgadmin |
| 165 | +``` |
| 166 | + |
| 167 | +## Files in This Directory |
| 168 | + |
| 169 | +| File | Purpose | |
| 170 | +|------|---------| |
| 171 | +| `docker-compose.pgadmin.yml` | Shared pgAdmin Docker Compose | |
| 172 | +| `Caddyfile.example` | Full host-level Caddy configuration example | |
| 173 | +| `caddy-pgadmin.snippet` | Reusable Caddy snippet for pgAdmin routing | |
| 174 | +| `servers.json.example` | pgAdmin server definitions template | |
| 175 | +| `.env.example` | Environment variables template | |
| 176 | +| `README.md` | This documentation | |
| 177 | + |
| 178 | +## Customization |
| 179 | + |
| 180 | +### Adding a New Tenant |
| 181 | + |
| 182 | +1. **Deploy tenant STATBUS instance** with unique slot offset |
| 183 | +2. **Add to servers.json**: |
| 184 | + ```json |
| 185 | + "4": { |
| 186 | + "Name": "New Country STATBUS", |
| 187 | + "Group": "STATBUS Tenants", |
| 188 | + "Host": "xx.statbus.org", |
| 189 | + "Port": 5432, |
| 190 | + "SSLMode": "require", |
| 191 | + "SSLNegotiation": "direct", |
| 192 | + "SSLSNI": true |
| 193 | + } |
| 194 | + ``` |
| 195 | +3. **Add to host Caddyfile**: |
| 196 | + ```caddyfile |
| 197 | + # Layer4 SNI routing |
| 198 | + @xx tls sni xx.statbus.org |
| 199 | + route @xx { |
| 200 | + proxy localhost:30X5 # tenant's DB-TLS port |
| 201 | + } |
| 202 | +
|
| 203 | + # Site block |
| 204 | + xx.statbus.org { |
| 205 | + import pgadmin_route 30X3 # tenant's REST port |
| 206 | + import tenant_routes 30X2 30X3 |
| 207 | + import error_handlers |
| 208 | + } |
| 209 | + ``` |
| 210 | +4. **Reload services**: |
| 211 | + ```bash |
| 212 | + docker compose -f docker-compose.pgadmin.yml restart |
| 213 | + sudo systemctl reload caddy |
| 214 | + ``` |
| 215 | + |
| 216 | +### Removing pgAdmin Access for a Tenant |
| 217 | + |
| 218 | +Simply remove the `import pgadmin_route` line from that tenant's site block in the host Caddyfile. |
| 219 | + |
| 220 | +## Troubleshooting |
| 221 | + |
| 222 | +### pgAdmin shows 401 Unauthorized |
| 223 | +- Verify tenant's PostgREST is running: `curl localhost:30X3/` |
| 224 | +- Check JWT cookie is set: browser DevTools → Application → Cookies |
| 225 | +- Ensure `auth_gate` function exists in tenant database |
| 226 | + |
| 227 | +### Cannot connect to database in pgAdmin |
| 228 | +- Verify Layer4 SNI routing: `curl -v --resolve xx.statbus.org:5432:127.0.0.1 postgres://xx.statbus.org:5432/` |
| 229 | +- Check tenant's DB-TLS port is exposed |
| 230 | +- Ensure SSLSNI patch is applied (custom pgAdmin image) |
| 231 | + |
| 232 | +### pgAdmin not loading static assets |
| 233 | +- Verify `SCRIPT_NAME=/pgadmin` is set |
| 234 | +- Check Caddy is preserving the path correctly |
| 235 | + |
| 236 | +## Security Considerations |
| 237 | + |
| 238 | +1. **JWT-gated access**: Users must authenticate with STATBUS before accessing pgAdmin |
| 239 | +2. **Per-tenant isolation**: Each pgAdmin database connection uses the user's credentials, enforcing RLS |
| 240 | +3. **TLS everywhere**: All database connections use TLS via SNI routing |
| 241 | +4. **No shared credentials**: pgAdmin master password is only for its internal UI, not database access |
0 commit comments