- All server secrets are read exclusively via
process.envthroughpackages/server/src/env.ts, validated withzodat boot - the process refuses to start if anything required is missing..env.exampledocuments every variable; the real.envis gitignored and must never be committed. - Harbor push credentials (
HARBOR_USERNAME/HARBOR_PASSWORD, plusHARBOR_REGISTRY/HARBOR_PROJECT) are never held by the central service at all - they live only as GitHub Actions secrets on each onboarded repo, referenced by the generated workflow assecrets.HARBOR_USERNAME/secrets.HARBOR_PASSWORD/secrets.HARBOR_REGISTRY/secrets.HARBOR_PROJECT(seepackages/server/src/templates/workflows/deploy.yml.tmpl, the source of truth for exact secret names). The Harbor robot account should be scoped to push-only on the specific project(s) it needs - provision this in Harbor, not enforced by this codebase. - Generated templates (Dockerfile/compose/workflow) only ever reference
${VAR}orsecrets.X- literal secret values are never rendered into a generated file.
Unlike push credentials (which remain exclusively GitHub Actions secrets,
unchanged), the central service does hold HARBOR_ROBOT_USER_READONLY/
HARBOR_ROBOT_SECRET_READONLY (packages/server/src/harbor/client.ts) for
the dashboard's project/repository/tag/vulnerability browsing UI. This is a
deliberate, narrowly-scoped exception to the "central service never holds
Harbor credentials" rule above - provision a distinct robot account
scoped read-only (pull/list, no push), so a leak of the browsing credential
can never grant push access. Never rendered into generated templates, same
rule as every other secret.
Unlike every other exception above (all read-only), the central service holds
UNLEASH_ADMIN_TOKEN (packages/server/src/unleash/client.ts) with write
access - toggling a flag or editing a strategy/variant from the dashboard
calls Unleash's Admin API directly. This is a deliberate, higher-blast-radius
exception to "the central service never holds write credentials to external
systems": a leaked token can flip flags in whatever projects it's scoped to.
Mitigate by provisioning a token scoped to only the project(s) this dashboard
should manage (Unleash supports project-scoped admin tokens), not an
instance-wide admin token, where your Unleash version supports it. Same
logging-redaction and "never rendered into generated templates" rules as
every other secret apply.
- GitHub -> central service (
/webhooks/github): verified via HMAC SHA-256 againstGITHUB_WEBHOOK_SECRET(X-Hub-Signature-256), computed over the raw request body captured before JSON parsing, compared with a constant-time comparison. - Our own workflow -> central service (
/api/deploy-events,/api/environments/:id/containers): authenticated with a per-app shared secret (deployCallbackSecret, surfaced in the dashboard asCENTRAL_SERVICE_SHARED_SECRET), not the GitHub webhook secret - this is a different caller (our own CI run) with a different trust boundary.
GET /api/events/stream (SSE) and the Harbor/runner/assistant routes added
alongside the dashboard redesign use neither boundary - they're read-only
dashboard-only endpoints, same trust level as the existing /api/dashboard/*
routes. /api/events/stream in particular broadcasts every domain event to
every connected client with no per-user/per-app scoping - acceptable under
the "dashboard is unauthenticated, private-network-only" assumption below,
but worth revisiting if auth is ever added.
The Unleash routes (routes/unleash.ts) are dashboard-only in the same
sense - no additional auth boundary of their own - but unlike Harbor's GET-
only routes, several of them mutate live Unleash state (toggle/strategy/
variant/create/archive). Anyone who can reach the dashboard's HTTP API can
flip a flag; this is a direct consequence of the dashboard being
unauthenticated (see below), not a separate gap introduced by Unleash.
pino is configured (logging/logger.ts) to redact known-sensitive
header/field names (Authorization, X-Deploy-Secret,
X-Hub-Signature-256, and any field literally named *secret*/*token*/
*password*/*privateKey*). This is structural redaction only - a
general-purpose scanner that finds secret-shaped values anywhere in
arbitrary log payloads is an explicit non-goal of this MVP. Avoid logging
raw request bodies or third-party API responses that might carry
unstructured secrets.
This MVP does not implement dashboard login/authz - it assumes the service
runs on a private network reachable only by the team, since GET /api/apps/:id intentionally returns deployCallbackSecret so it can be
copy-pasted into GitHub. Do not expose this service to the public internet
without adding auth in front of it.
Nothing generated by this service is ever pushed directly to develop or
main. Proposed files are always opened as a PR on a dedicated branch
(ai-devops-agent/setup-pipeline-<app>) for a human to review and merge -
see docs/architecture.md for the full flow.