Skip to main content

Auto-managed credentials

When a crew declares a sidecar from a known database image (postgres, mysql, mariadb, mongo, rabbitmq, elasticsearch), Crewship generates and manages the sidecar’s root password on your behalf. The operator never sees, types, or rotates the value — crewship apply does it. This page explains how and why.

TL;DR

apiVersion: crewship/v1
kind: Crew
metadata: { name: Backend, slug: backend }
spec:
  services:
    - { name: postgres, image: postgres:16-alpine }
  agents:
    - { slug: lead, name: Lead, agent_role: LEAD }
crewship apply -f produces:
  • One sidecar container (postgres:16-alpine) on the crew-private bridge network with POSTGRES_USER=postgres and a generated POSTGRES_PASSWORD.
  • One credential row in the workspace named POSTGRES_PASSWORD, status ACTIVE, provider AUTO_MANAGED, tagged provisioned_for_service=backend/postgres.
  • Every agent in the crew (here: lead) gets POSTGRES_PASSWORD added to its env_refs automatically.
Zero PENDING credentials. Zero env files. The operator opens the URL, finishes onboarding, and the crew’s database is reachable from day one.

What images are recognised

Image prefixSugar envAuto-credential
postgresPOSTGRES_USER=postgresPOSTGRES_PASSWORD
mariadbMARIADB_ROOT_PASSWORD
mysqlMYSQL_ROOT_PASSWORD
mongoMONGO_INITDB_ROOT_USERNAME=rootMONGO_INITDB_ROOT_PASSWORD
rabbitmqRABBITMQ_DEFAULT_USER=adminRABBITMQ_DEFAULT_PASS
elasticsearchdiscovery.type=single-nodeELASTIC_PASSWORD
Images not in the catalog (e.g. redis) get no auto-credential at all — for default Redis the crew-private bridge isolation is enough; add an explicit auto_credentials: block if your image needs auth. The catalog lives in internal/manifest/known_sidecars.go. New entries are PR additions, not “any image we recognise” magic — the behaviour stays auditable. The image-name match strips registry hosts and tags, so all of these resolve to the same postgres entry:
  • postgres:16-alpine
  • docker.io/library/postgres:17
  • harbor.acme.io/library/postgres:16@sha256:...
  • localhost:5000/postgres:latest

Customising the auto-credential

For unknown images or when you want explicit control:
services:
  - name: cache
    image: ghcr.io/example/redis-with-auth:v1
    auto_credentials:
      - name: REDIS_PASSWORD
        inject_as_env: REDIS_AUTH       # default = name
        inject_to_agents: false          # default = true; here only the
                                         # sidecar sees the value
        length: 48                       # default = 32 bytes (64 hex chars)
        description: Auto-generated AUTH token for the cache sidecar.
Operator entries with the same name as a sugar default override the sugar entirely. A crew can also add credentials beyond the sugar set — both land in the resolved output.

Threat model — why this is safe

The generated value lives in two places in the workspace database:
  1. credentials.encrypted_value — AES-256-GCM encrypted with ENCRYPTION_KEY. Surfaces the row in the UI for audit, rotate actions, and “Created by” attribution.
  2. crews.services_json — plaintext, embedded in the sidecar env literal. The docker provider reads this column at sidecar start time and passes the value via docker --env.
The plaintext-in-services_json is intentional and bounded:
  • Sidecars from auto_credentials MUST be crew-private (no host port published). The validator refuses ports: that publish to the host on a service with auto-credentials — that combination requires a T2 manual credential and an explicit security review.
  • The crew-private bridge network is the actual security boundary. An attacker who can read the workspace DB also already controls bridge isolation and the threat model is “host root,” under which a separate encrypted column doesn’t help.
  • The duplicated value is bounded to the same DB file the encrypted column lives in. A backup of the workspace state carries both consistently.
A future migration moves the literal value to an encrypted sibling column once the sidecar env-refs runtime path is wired (Service. EnvRefs resolution at sidecar start). Until then, the duplication is the trade-off for shipping the friction-zero default today.

What the UI shows

Auto-managed rows appear under Credentials with:
  • Source column reads “system (backend/postgres)” — system is the v98 actor type, and the parenthesised slug is the provisioned_for_service tag. (A future PR pins the row to the crew’s lead agent and renders “agent: trapper” instead.)
  • “Reveal value” and “Edit” actions are hidden.
  • “Rotate” is available as a follow-up action (P1).
  • Audit timeline shows the create event with the manifest apply as the actor user.

When auto_credentials is the WRONG choice

  • The sidecar publishes a port to the host (docker run -p 5432:5432- style). The bridge is no longer the security boundary; declare the credential manually under credentials: and accept the operator-input friction.
  • The credential needs to be readable by something outside the crew (other crews, an external monitor, a backup job). T1 rows are crew-scoped by design — workspace-wide credentials with external consumers belong to T2 / T3.
  • You want to bring your own value from a secrets manager (Vault, AWS Secrets Manager, Doppler). That’s the T3 tier — declare a credential with a source: block instead.

Migration: existing manifests with env_refs: [POSTGRES_PASSWORD]

# Before (v97 and earlier)
credentials:
  - { env: POSTGRES_PASSWORD, provider: NONE, type: GENERIC_SECRET }

services:
  - name: postgres
    image: postgres:16
    env: { POSTGRES_DB: uo, POSTGRES_USER: postgres }
    env_refs: [POSTGRES_PASSWORD]
# After (v98+)
services:
  - { name: postgres, image: postgres:16, env: { POSTGRES_DB: uo } }
Drop the credentials: block, drop the env_refs:, drop the explicit POSTGRES_USER. Re-apply. Existing user-managed credential rows are NOT touched — the dispatch refuses to overwrite a name collision unless provider=AUTO_MANAGED was already there. Migrate by deleting the manual row first if you want the auto-managed flavour.