Skip to content

Commit d5096ad

Browse files
committed
cloud: Add multi-tenant pgAdmin deployment configuration
Adds cloud/ directory with configuration for running a shared pgAdmin instance that serves multiple STATBUS tenants. Key features: - System-level docker-compose for pgAdmin (independent of tenant instances) - Host-level Caddyfile with per-tenant forward_auth and SNI routing - PGADMIN_PORT environment variable for configurable port binding - Comprehensive documentation with architecture diagrams
1 parent f5e1b36 commit d5096ad

File tree

7 files changed

+581
-0
lines changed

7 files changed

+581
-0
lines changed

cloud/.env.example

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Cloud pgAdmin environment configuration
2+
# Copy to .env and customize
3+
4+
# pgAdmin port - host Caddy proxies to this
5+
# Must match the port in Caddyfile's pgadmin_route snippet
6+
PGADMIN_PORT=5050
7+
8+
# pgAdmin master credentials
9+
# These are for pgAdmin's internal authentication only
10+
# Actual database access uses the user's STATBUS credentials
11+
PGADMIN_DEFAULT_EMAIL=admin@statbus.local
12+
PGADMIN_DEFAULT_PASSWORD=change-this-to-a-secure-password
13+
14+
# Generate a secure password with:
15+
# openssl rand -base64 32

cloud/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Ignore actual configuration files (contain credentials/tenant-specific data)
2+
# Only .example files should be committed
3+
.env
4+
servers.json

cloud/Caddyfile.example

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
# Example Host-level Caddyfile for STATBUS Cloud Deployment
2+
#
3+
# This Caddyfile runs at the HOST level (not inside Docker) and handles:
4+
# 1. TLS termination for all tenant subdomains
5+
# 2. Routing to each tenant's Docker Compose services
6+
# 3. Shared pgAdmin with per-tenant authentication
7+
# 4. Layer4 SNI routing for PostgreSQL connections
8+
#
9+
# Customize this for your deployment by:
10+
# 1. Adding/removing tenant blocks
11+
# 2. Updating port numbers to match your DEPLOYMENT_SLOT_PORT_OFFSET values
12+
# 3. Configuring your domain and TLS settings
13+
#
14+
# Port calculation: base_port = 3000 + (slot_offset × 10)
15+
# HTTP: base_port + 0, HTTPS: base_port + 1, App: base_port + 2
16+
# REST: base_port + 3, DB: base_port + 4, DB-TLS: base_port + 5
17+
#
18+
# Environment variables (set these or replace with values):
19+
# PGADMIN_PORT - pgAdmin listen port (default: 5050)
20+
21+
# Global options
22+
{
23+
# Use Let's Encrypt for production
24+
# For staging/testing, uncomment:
25+
# acme_ca https://acme-staging-v02.api.letsencrypt.org/directory
26+
27+
# Layer4 for PostgreSQL SNI routing
28+
layer4 {
29+
# PostgreSQL with SNI-based routing (port 5432)
30+
:5432 {
31+
@ma tls sni ma.statbus.org
32+
route @ma {
33+
proxy localhost:3025 # ma DB-TLS port
34+
}
35+
36+
@no tls sni no.statbus.org
37+
route @no {
38+
proxy localhost:3035 # no DB-TLS port
39+
}
40+
41+
@al tls sni al.statbus.org
42+
route @al {
43+
proxy localhost:3045 # al DB-TLS port
44+
}
45+
}
46+
}
47+
}
48+
49+
# Reusable snippets
50+
(pgadmin_route) {
51+
# pgAdmin route with tenant-specific authentication
52+
# {args[0]} = tenant's REST port for auth_gate
53+
handle /pgadmin* {
54+
forward_auth localhost:{args[0]} {
55+
uri /rpc/auth_gate
56+
copy_headers Cookie
57+
}
58+
reverse_proxy localhost:{$PGADMIN_PORT:5050} {
59+
header_up Host {host}
60+
header_up X-Forwarded-Proto {scheme}
61+
header_up X-Forwarded-Host {host}
62+
}
63+
}
64+
}
65+
66+
(tenant_routes) {
67+
# Standard tenant routes
68+
# {args[0]} = app port, {args[1]} = rest port
69+
70+
# PostgREST API
71+
handle /rest/* {
72+
uri strip_prefix /rest
73+
reverse_proxy localhost:{args[1]}
74+
}
75+
76+
# Auth gate endpoint (for pgAdmin forward_auth)
77+
handle /rest/rpc/auth_gate {
78+
reverse_proxy localhost:{args[1]}
79+
}
80+
81+
# Next.js application (catch-all)
82+
handle {
83+
reverse_proxy localhost:{args[0]}
84+
}
85+
}
86+
87+
(error_handlers) {
88+
handle_errors {
89+
@unauthorized expression {http.error.status_code} == 401
90+
handle @unauthorized {
91+
redir /?login=required&redirect={uri} 302
92+
}
93+
}
94+
}
95+
96+
# =============================================================================
97+
# TENANT SITE BLOCKS
98+
# =============================================================================
99+
100+
# Morocco tenant (slot offset 2: ports 3020-3025)
101+
ma.statbus.org {
102+
import pgadmin_route 3023
103+
import tenant_routes 3022 3023
104+
import error_handlers
105+
}
106+
107+
# Norway tenant (slot offset 3: ports 3030-3035)
108+
no.statbus.org {
109+
import pgadmin_route 3033
110+
import tenant_routes 3032 3033
111+
import error_handlers
112+
}
113+
114+
# Albania tenant (slot offset 4: ports 3040-3045)
115+
al.statbus.org {
116+
import pgadmin_route 3043
117+
import tenant_routes 3042 3043
118+
import error_handlers
119+
}
120+
121+
# =============================================================================
122+
# OPTIONAL: Root domain redirect
123+
# =============================================================================
124+
125+
# statbus.org {
126+
# redir https://www.statbus.org{uri} permanent
127+
# }
128+
129+
# www.statbus.org {
130+
# # Marketing site or redirect to documentation
131+
# reverse_proxy localhost:8080
132+
# }

cloud/README.md

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
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

Comments
 (0)