Auto-managed credentials
When a crew declares a sidecar from a known datastore image (postgres, mysql, mariadb, mongo, redis, rabbitmq,
elasticsearch), Crewship generates and manages the sidecar’s auth
secret on your behalf. The operator never sees, types, or rotates
the value — crewship apply does it. This page explains how and
why.
Crewship-provisioned datastores are always authenticated. When a crew
declares a recognised datastore image (
postgres, mysql, mariadb, mongo,
redis, rabbitmq, elasticsearch) and leaves the service’s command: (and
auth env) unset, Crewship generates a strong, randomly generated auth secret at
apply time and delivers it to agents through the credential system as an env
credential (e.g. REDIS_PASSWORD). A recognised datastore image never boots
open: authentication, not just the crew-private bridge network, is the access
gate.Opting out is explicit and enforced. If you override the service’s
command: (or its auth env) you take ownership of the auth configuration — and
that override must itself provide authentication (e.g.
redis-server --requirepass <secret>, or a non-empty password env). If it does
not, crewship apply fails closed with an error naming the service and the
missing auth. The only way to intentionally run a recognised datastore open is
to set allow_unauthenticated: true on the service — a deliberate,
acknowledged opt-out. So command: ["redis-server"] with no --requirepass
is now a hard error unless you add the flag or set that opt-out. See
Redis below.TL;DR
crewship apply -f produces:
- One sidecar container (
postgres:16-alpine) on the crew-private bridge network withPOSTGRES_USER=postgresand a generatedPOSTGRES_PASSWORD. - One credential row in the workspace named
POSTGRES_PASSWORD, statusACTIVE, providerAUTO_MANAGED, taggedprovisioned_for_service=backend/postgres. - Every agent in the crew (here:
lead) getsPOSTGRES_PASSWORDadded to its env_refs automatically.
What images are recognised
Redis is delivered via a command arg, not an env var
The officialredis image does not read a password from an
environment variable — it takes --requirepass <value> as a command
argument. So Redis’s auto-credential injects the generated secret into
the sidecar’s command (redis-server --requirepass <generated>)
rather than its env. The value still reaches every agent as the
REDIS_PASSWORD env credential through the normal env_refs path, so
agents connect with redis://:$REDIS_PASSWORD@redis:6379.
If you declare your own command: on the Redis service and it already
passes --requirepass <secret>, CrewShip leaves it untouched: your
command wins, and no secret is generated, no credential row is created,
and no agent env_ref is appended — you own the auth.
If your override command: drops --requirepass (or supplies it
with an empty argument), the apply is rejected — a recognised datastore
may not boot open. Add the flag, or set allow_unauthenticated: true
on the service to run it open on purpose (see
Customising).
Images not in the catalog get no auto-credential — add an explicit
auto_credentials: block if your image needs one.
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-alpinedocker.io/library/postgres:17harbor.acme.io/library/postgres:16@sha256:...localhost:5000/postgres:latest
Customising the auto-credential
For unknown images or when you want explicit control: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.
Overriding a datastore’s command or auth env
When you override thecommand: (or the auth env) on a recognised
datastore image, you take ownership of its authentication — and that
override must carry auth. Crewship checks this at apply time:
- Command-injected datastores (Redis): your
command:must include the datastore’s auth flag followed by a non-empty argument (--requirepass <secret>for Redis). A command with no flag, or the flag with an empty argument, fails the apply. - Env-injected datastores (Postgres, MySQL, Mongo, …): the auth env
key you set (e.g.
POSTGRES_PASSWORD) must be non-empty. Setting it to""fails the apply — it is neither “auth” nor “let Crewship generate one,” it is an ambiguous half-config, so we reject it.
allow_unauthenticated (default false) is the only way to run a
recognised datastore image open. It is scoped to that one service and is
never needed on the default path (leave command:/auth env unset and
Crewship authenticates the datastore for you). It has no effect on images
outside the catalog — those are yours to configure entirely.
Threat model — why this is safe
The generated value lives in two places in the workspace database:credentials.encrypted_value— AES-256-GCM encrypted withENCRYPTION_KEY. Surfaces the row in the UI for audit, rotate actions, and “Created by” attribution.crews.services_json— plaintext, embedded in the sidecar env literal. The docker provider reads this column at sidecar start time and passes the value viadocker --env.
- Sidecars from
auto_credentialsMUST be crew-private (no host port published). The validator refusesports: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.
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)” —
systemis the v98 actor type, and the parenthesised slug is theprovisioned_for_servicetag. (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 undercredentials: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]
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.
Related
SPEC-4— Auto-managed sidecar credentials (internal design doc). This guide is the canonical user-facing reference.internal/manifest/known_sidecars.go(catalog implementation).- Manifests guide (the broader manifest layer).