Skip to content

Security: ankit-kl0/cicd-agent

Security

docs/security.md

Security notes

Secrets

  • All server secrets are read exclusively via process.env through packages/server/src/env.ts, validated with zod at boot - the process refuses to start if anything required is missing. .env.example documents every variable; the real .env is gitignored and must never be committed.
  • Harbor push credentials (HARBOR_USERNAME/HARBOR_PASSWORD, plus HARBOR_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 as secrets.HARBOR_USERNAME / secrets.HARBOR_PASSWORD / secrets.HARBOR_REGISTRY / secrets.HARBOR_PROJECT (see packages/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} or secrets.X - literal secret values are never rendered into a generated file.

Exception: read-only Harbor browsing credentials

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.

Exception: read+write Unleash credentials

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.

Two distinct trust boundaries

  1. GitHub -> central service (/webhooks/github): verified via HMAC SHA-256 against GITHUB_WEBHOOK_SECRET (X-Hub-Signature-256), computed over the raw request body captured before JSON parsing, compared with a constant-time comparison.
  2. Our own workflow -> central service (/api/deploy-events, /api/environments/:id/containers): authenticated with a per-app shared secret (deployCallbackSecret, surfaced in the dashboard as CENTRAL_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.

Logging

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.

The dashboard is not authenticated

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.

PR-based file generation

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.

There aren't any published security advisories