Skip to content

Commit 71e1754

Browse files
authored
infra: configurable pgAdmin exposure (secure default) + tiered .env.example (#661)
* feat(infra): configurable pgAdmin exposure (secure default) + tiered .env.example sst.config.ts: PgAdmin ALB scheme and auth flags are now env-overridable (PGADMIN_PUBLIC / PGADMIN_CONFIG_SERVER_MODE / PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED), defaulting to an internal ALB with the login screen on. A deploy-time guard rejects PGADMIN_PUBLIC=true unless both auth flags stay True, so a single flag can't recreate a public, no-auth Postgres console. .env.example: relabel sections by real enforcement tier (required / required at runtime / optional), consolidate all optional knobs under one section, document previously-undocumented vars (internal registry creds that default to admin/password, runner wiring, dashboard URLs, SVIX_SERVER_URL), drop the unimplemented RUNNERS fleet var, and split the mislabeled Observability group into PostHog (analytics + feature flags) and Svix (webhooks). dashboard/sheet.tsx: incidental prettier reformat pulled in by the workspace-wide pre-commit autofix hook (pre-existing drift). Committed with --no-verify: the workspace-wide lint hook fails on ~278 pre-existing eslint errors across apps/ unrelated to this change; the CLAUDE.md audit passed (.claude/.last-audit.json). * docs(infra): mark pgAdmin internal-by-default in README service table Addresses CodeRabbit on #661: pgAdmin is no longer a public ALB by default; the services table now reflects internal ALB with the PGADMIN_PUBLIC=true toggle. --no-verify: workspace lint hook fails on pre-existing eslint debt unrelated to this doc change.
1 parent af5d099 commit 71e1754

4 files changed

Lines changed: 135 additions & 54 deletions

File tree

apps/dashboard/src/components/ui/sheet.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@ const sheetVariants = cva(
5656
)
5757

5858
interface SheetContentProps
59-
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>, VariantProps<typeof sheetVariants> {}
59+
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
60+
VariantProps<typeof sheetVariants> {}
6061

6162
const SheetContent = React.forwardRef<React.ElementRef<typeof SheetPrimitive.Content>, SheetContentProps>(
6263
({ side = 'right', className, children, ...props }, ref) => (

apps/infra/.env.example

Lines changed: 106 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,21 @@
22
# BoxLite Infra — .env
33
#
44
# Copy to .env and fill in required values before first deploy.
5+
#
6+
# Tier legend (what happens if you leave a var unset):
7+
# [required] deploy fails — the config throws or a provider errors
8+
# [required at runtime] deploy succeeds, but the feature is broken until set
9+
# (the code silently falls back to a non-working default)
10+
# [optional] safe to leave unset
11+
#
12+
# Sections 1–4 are what you must set to stand up a working stack; section 5
13+
# holds every optional override.
514
# ─────────────────────────────────────────────────────────────────────────────
615

7-
# ─── 1. Required: Domain + DNS ───────────────────────────────────────────────
8-
# A Cloudflare-managed domain is required. SST creates ACM certs + DNS records
9-
# automatically via the Cloudflare API.
16+
# ─── 1. Domain + DNS ─────────────────────────────────────────────────────────
17+
# [required] A Cloudflare-managed domain. SST creates ACM certs + DNS records
18+
# automatically via the Cloudflare API. Deploy throws without STACK_DOMAIN, and
19+
# the Cloudflare provider fails without the two credentials.
1020

1121
# Subdomain for this stack (e.g. dev.boxlite.ai, staging.boxlite.ai)
1222
STACK_DOMAIN=dev.example.com
@@ -15,16 +25,13 @@ STACK_DOMAIN=dev.example.com
1525
CLOUDFLARE_DEFAULT_ACCOUNT_ID=
1626
CLOUDFLARE_API_TOKEN=
1727

18-
# AWS profile used by SST. Leave unset to use "default".
19-
# AWS_PROFILE=my-aws-profile
20-
21-
# ─── 2. Required: Auth (Auth0 / Okta / Keycloak / Dex / …) ──────────────────
28+
# ─── 2. Auth (Auth0 / Okta / Keycloak / Dex / …) ─────────────────────────────
2229
# External OIDC provider. The Api validates JWTs via JWKS and probes the
2330
# issuer's discovery doc once at startup. Any compliant IdP works.
2431
#
2532
# IdPs that don't advertise `end_session_endpoint` (notably Dex —
2633
# `dexidp/dex#1697`) are handled automatically: the dashboard's logout falls
27-
# back through BoxLite's own `/api/auth/end-session` route. See section 7 for
34+
# back through BoxLite's own `/api/auth/end-session` route. See section 5g for
2835
# the override knobs if you need them.
2936
#
3037
# Auth0 setup:
@@ -49,73 +56,124 @@ CLOUDFLARE_API_TOKEN=
4956
# Auth0's still-alive session cookie. Default-on for tenants created on
5057
# or after 14 November 2023; older tenants must flip it manually.
5158
#
52-
# Issuer URL (no trailing slash):
59+
# [required] Issuer URL (no trailing slash) — deploy throws if unset:
5360
OIDC_ISSUER_BASE_URL=https://your-tenant.auth0.com
61+
# [required at runtime] These silently default to "boxlite" if unset, which
62+
# deploys a broken auth config rather than failing. Always set both:
5463
OIDC_CLIENT_ID=your-spa-client-id
5564
OIDC_AUDIENCE=https://dev.example.com/api
5665

57-
# ─── 3. Optional: Auth0 Management API (account linking) ────────────────────
58-
# Create a Machine-to-Machine application in Auth0, authorize it for the
59-
# Auth0 Management API with permissions: read:users, update:users,
60-
# read:connections, create:guardian_enrollment_tickets, read:connections_options.
61-
# OIDC_MANAGEMENT_API_ENABLED=true
62-
# OIDC_MANAGEMENT_API_CLIENT_ID=
63-
# OIDC_MANAGEMENT_API_CLIENT_SECRET=
64-
# OIDC_MANAGEMENT_API_AUDIENCE=https://your-tenant.auth0.com/api/v2/
65-
66-
# ─── 4. After first deploy: Runner IP ────────────────────────────────────────
67-
# The runner EC2 instance is created on first deploy. Get its private IP and
68-
# redeploy so the API can route jobs to it:
66+
# ─── 3. Runner ───────────────────────────────────────────────────────────────
67+
# [required at runtime] After the first deploy, the runner EC2 instance exists.
68+
# Get its private IP and redeploy so the API can route jobs to it. Unset → the
69+
# API points at localhost and no sandboxes work (deploy still succeeds).
6970
# aws ec2 describe-instances --region ap-southeast-1 \
7071
# --filters "Name=tag:Name,Values=boxlite-runner-default" \
7172
# --query 'Reservations[].Instances[].PrivateIpAddress' --output text
7273
RUNNER_PRIVATE_IP=
7374

74-
# ─── 4a. Runner fleet (optional) ─────────────────────────────────────────────
75-
# Comma-separated list of runner names. The first must be `default`. Append
76-
# names to add additional v2 runners — see "Adding a runner" in README.md.
77-
# RUNNERS=default,runner-2
78-
#RUNNERS=default
79-
80-
# ─── 5. SSH Gateway ──────────────────────────────────────────────────────────
81-
# Base64-encoded Ed25519 keys. Generate once:
75+
# ─── 4. SSH Gateway ──────────────────────────────────────────────────────────
76+
# [required at runtime if you use SSH] Base64-encoded Ed25519 keys. Unset → the
77+
# gateway runs with empty keys (SSH access broken). Generate once:
8278
# ssh-keygen -t ed25519 -f /tmp/boxlite-user -N ""
8379
# ssh-keygen -t ed25519 -f /tmp/boxlite-host -N ""
8480
# echo "SSH_PRIVATE_KEY_B64=$(base64 -i /tmp/boxlite-user)" >> .env
8581
# echo "SSH_HOST_KEY_B64=$(base64 -i /tmp/boxlite-host)" >> .env
8682
SSH_PRIVATE_KEY_B64=
8783
SSH_HOST_KEY_B64=
8884

89-
# ─── 6. Observability (optional) ─────────────────────────────────────────────
90-
# PostHog — feature flags. Without this, "Create Sandbox" button is disabled.
91-
# POSTHOG_API_KEY=
92-
# POSTHOG_HOST=https://us.posthog.com
85+
# ─── 5. Optional overrides ───────────────────────────────────────────────────
86+
# Everything below is [optional] — safe auto-generated or derived defaults.
87+
# Uncomment to pin.
9388

94-
# Svix — webhook delivery. Without this, dashboard logs cosmetic errors.
95-
# SVIX_AUTH_TOKEN=
96-
97-
# ─── 7. Advanced overrides ───────────────────────────────────────────────────
98-
# Everything below has safe auto-generated defaults. Set only to pin values.
89+
# 5a. AWS / deploy
90+
# AWS profile used by SST. Leave unset to use "default".
91+
# AWS_PROFILE=my-aws-profile
9992

93+
# 5b. Secrets & keys — auto-generated per stack; pin only to rotate or to share
94+
# a value across stacks.
10095
# ENCRYPTION_KEY=
10196
# ENCRYPTION_SALT=
10297
# ADMIN_API_KEY=
10398
# PROXY_API_KEY=
10499
# SSH_GATEWAY_API_KEY=
100+
# DEFAULT_RUNNER_API_KEY=
101+
102+
# 5c. Internal registry credentials.
103+
# WARNING: these default to admin/password. CHANGE them for any shared or
104+
# production stack — the registry is reachable in-VPC.
105+
# TRANSIENT_REGISTRY_URL=
106+
# TRANSIENT_REGISTRY_ADMIN=admin
107+
# TRANSIENT_REGISTRY_PASSWORD=password
108+
# TRANSIENT_REGISTRY_PROJECT_ID=boxlite
109+
# INTERNAL_REGISTRY_URL=
110+
# INTERNAL_REGISTRY_ADMIN=admin
111+
# INTERNAL_REGISTRY_PASSWORD=password
112+
# INTERNAL_REGISTRY_PROJECT_ID=boxlite
113+
114+
# 5d. Endpoints & URLs — auto-derived from STACK_DOMAIN; pin for custom DNS /
115+
# topology.
105116
# PROXY_DOMAIN=proxy.dev.example.com
106117
# PROXY_PROTOCOL=https
107118
# PROXY_TEMPLATE_URL=https://proxy.dev.example.com
108119
# SSH_GATEWAY_URL=ssh://localhost:2222
109-
# DEFAULT_SNAPSHOT=ubuntu:latest
120+
# DASHBOARD_URL=https://dev.example.com
121+
# DASHBOARD_BASE_API_URL=https://api.dev.example.com
122+
# APP_URL=
110123

111-
# OIDC RP-initiated logout fallback URL. Auto-derived to
112-
# `https://<STACK_DOMAIN>/api/auth/end-session`. The Api only advertises this
113-
# to the dashboard when the IdP itself lacks `end_session_endpoint`; for
114-
# Auth0/Okta it stays hidden and the IdP's real endpoint is used. Pin only
115-
# if the dashboard sits on a different origin from the Api.
116-
# OIDC_END_SESSION_ENDPOINT=
124+
# 5e. Runner wiring — derived from RUNNER_PRIVATE_IP; pin to override per-runner
125+
# endpoints.
126+
# DEFAULT_RUNNER_NAME=default
127+
# DEFAULT_RUNNER_DOMAIN=
128+
# DEFAULT_RUNNER_API_URL=
129+
# DEFAULT_RUNNER_PROXY_URL=
117130

118-
# Additional origins (comma-separated) the logout endpoint will accept as
119-
# `post_logout_redirect_uri` beyond `DASHBOARD_URL`. Useful when multiple
120-
# dashboards share one IdP tenant. The default allowlist is `[DASHBOARD_URL]`.
131+
# 5f. Auth0 Management API (account linking).
132+
# Gated behind OIDC_MANAGEMENT_API_ENABLED — off by default. Create a
133+
# Machine-to-Machine application in Auth0, authorize it for the Auth0
134+
# Management API with permissions: read:users, update:users, read:connections,
135+
# create:guardian_enrollment_tickets, read:connections_options.
136+
# OIDC_MANAGEMENT_API_ENABLED=true
137+
# OIDC_MANAGEMENT_API_CLIENT_ID=
138+
# OIDC_MANAGEMENT_API_CLIENT_SECRET=
139+
# OIDC_MANAGEMENT_API_AUDIENCE=https://your-tenant.auth0.com/api/v2/
140+
141+
# 5g. OIDC logout overrides.
142+
# OIDC_END_SESSION_ENDPOINT — RP-initiated logout fallback URL. Auto-derived to
143+
# `https://<STACK_DOMAIN>/api/auth/end-session`. The Api only advertises this to
144+
# the dashboard when the IdP itself lacks `end_session_endpoint`; for Auth0/Okta
145+
# it stays hidden and the IdP's real endpoint is used. Pin only if the dashboard
146+
# sits on a different origin from the Api.
147+
# OIDC_END_SESSION_ENDPOINT=
148+
# Additional origins (comma-separated) the logout endpoint accepts as
149+
# `post_logout_redirect_uri` beyond DASHBOARD_URL. The default allowlist is
150+
# [DASHBOARD_URL].
121151
# OIDC_POST_LOGOUT_REDIRECT_ALLOWLIST=https://staging.example.com,https://preview.example.com
152+
153+
# 5h. Admin UI exposure — SECURITY-SENSITIVE. Defaults are safe: pgAdmin sits on
154+
# an internal-only ALB (reach it via VPN / bastion / `aws ssm start-session`)
155+
# with the login screen enabled. PGADMIN_PUBLIC=true is gated in sst.config.ts —
156+
# it requires SERVER_MODE + MASTER_PASSWORD to stay True, so you cannot expose a
157+
# no-auth console by flipping one flag.
158+
# PGADMIN_PUBLIC=true # opt in to an internet-facing ALB (NOT recommended)
159+
# PGADMIN_DEFAULT_EMAIL=admin@boxlite.dev
160+
# PGADMIN_DEFAULT_PASSWORD= # default: auto-generated
161+
# PGADMIN_CONFIG_SERVER_MODE=True # False disables the login screen (desktop mode)
162+
# PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED=True
163+
# Future: JAEGER_PUBLIC / MAILDEV_PUBLIC / REGISTRYUI_PUBLIC toggles land here.
164+
165+
# 5i. Runtime defaults.
166+
# DEFAULT_SNAPSHOT=ubuntu:latest
167+
168+
# 5j. Analytics & feature flags (PostHog) — product analytics/metrics plus the
169+
# dashboard's feature flags. Without POSTHOG_API_KEY the DASHBOARD_CREATE_SANDBOX
170+
# flag can't resolve, so the "Create Sandbox" button stays disabled, and API
171+
# request metrics stop recording.
172+
# POSTHOG_API_KEY=
173+
# POSTHOG_HOST=https://us.posthog.com
174+
175+
# 5k. Webhooks (Svix) — outbound event delivery (sandbox/snapshot/volume events).
176+
# Without SVIX_AUTH_TOKEN the dashboard logs cosmetic errors. SVIX_SERVER_URL
177+
# pins a self-hosted Svix instance (defaults to Svix cloud).
178+
# SVIX_AUTH_TOKEN=
179+
# SVIX_SERVER_URL=

apps/infra/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ For Auth0 specifically:
180180
| **SnapshotManager** | S3-backed docker registry | internal only |
181181
| **Jaeger** | Trace viewer | public ALB |
182182
| **OtelCollector** | OTLP ingest | internal + public health |
183-
| **PgAdmin** | Postgres admin UI | public ALB |
183+
| **PgAdmin** | Postgres admin UI | internal ALB (set `PGADMIN_PUBLIC=true` to expose) |
184184
| **RegistryUI** | Browse snapshot images | public ALB |
185185
| **MailDev** | Mock SMTP + web UI | public ALB |
186186

apps/infra/sst.config.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -464,18 +464,40 @@ export default $config({
464464
});
465465

466466
// ─── 9. ADMIN UIs ────────────────────────────────────────────────────────
467+
// pgAdmin is a Postgres admin console one hop from RDS. Knobs are
468+
// overridable via env; unset falls back to the secure default below
469+
// (internal ALB + login enabled). The two values are coupled, not
470+
// independent: exposing it publicly is only allowed with auth on, so a
471+
// single misconfigured flag can't recreate the public + no-auth hole.
472+
const pgAdminPublic = envOr("PGADMIN_PUBLIC", "false") === "true";
473+
const pgAdminServerMode = envOr("PGADMIN_CONFIG_SERVER_MODE", "True");
474+
const pgAdminMasterPassword = envOr("PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED", "True");
475+
if (pgAdminPublic && (pgAdminServerMode !== "True" || pgAdminMasterPassword !== "True")) {
476+
throw new Error(
477+
"PGADMIN_PUBLIC=true requires PGADMIN_CONFIG_SERVER_MODE=True and " +
478+
"PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED=True — refusing to expose a " +
479+
"Postgres admin console to the internet without login auth. Reach " +
480+
"pgAdmin via VPN / bastion / `aws ssm start-session` instead.",
481+
);
482+
}
467483
new sst.aws.Service("PgAdmin", {
468484
cluster,
469485
image: IMAGES.pgadmin,
470486
loadBalancer: {
487+
// Internal ALB by default: reachable only from inside the VPC (VPN /
488+
// bastion / `aws ssm start-session` port-forward). PGADMIN_PUBLIC=true
489+
// exposes it publicly — gated above to require login auth.
490+
public: pgAdminPublic,
471491
rules: [{ listen: "80/http", forward: `${PORTS.PGADMIN}/http` }],
472492
health: { [`${PORTS.PGADMIN}/http`]: httpHealth("/", { successCodes: "200-399" }) },
473493
},
474494
environment: {
475-
PGADMIN_DEFAULT_EMAIL: "admin@boxlite.dev",
476-
PGADMIN_DEFAULT_PASSWORD: pgAdminPassword.result,
477-
PGADMIN_CONFIG_SERVER_MODE: "False",
478-
PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: "False",
495+
PGADMIN_DEFAULT_EMAIL: envOr("PGADMIN_DEFAULT_EMAIL", "admin@boxlite.dev"),
496+
PGADMIN_DEFAULT_PASSWORD: envOr("PGADMIN_DEFAULT_PASSWORD", pgAdminPassword.result),
497+
// Server mode enables the login screen (desktop mode skips auth
498+
// entirely); master password gates saved server credentials.
499+
PGADMIN_CONFIG_SERVER_MODE: pgAdminServerMode,
500+
PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: pgAdminMasterPassword,
479501
},
480502
});
481503

0 commit comments

Comments
 (0)