crewship admin
cmd/crewship/cmd_admin.go exposes the operator-on-the-host recovery surface. Every subcommand under admin opens the local SQLite database directly (no HTTP, no auth token) — the server doesn’t even need to be running. Use these when a user is locked out of the UI.
The “credential” for these commands is shell access to the host. That matches what GitLab (gitlab-rake gitlab:password:reset), Gitea (gitea admin user change-password), Nextcloud (occ user:resetpassword), and Mattermost (mmctl user change-password) all do — if you can SSH to the box, you ARE the admin.
The credential for these commands is shell access to the host — there
is no
crewship login token. Requirements:- Read+write access to the data directory (default
~/.crewship). The binary opens~/.crewship/crewship.dbdirectly viaopenAdminDB. - The
crewshipbinary must run on the same host as the data directory. - The host’s filesystem permissions are the authentication.
Subcommands
crewship admin reencrypt
Re-encrypt stored secret envelopes to the server’s current master-key version.
This is the server-side step after configuring a new key version; the command
requires authentication, an active workspace, and a running server.
key_version, totals for
Re-encrypted, Skipped, and Failed, followed by per-table/per-column
counts when present. JSON output contains key_version, reencrypted,
skipped, failed, and columns. A non-zero exit means at least one value
could not be decrypted with the configured keys; do not retire the old key.
crewship admin reset-password
Rotate a user’s password.
crewship admin list-users
Dump every row in the users table, ordered by created_at ASC. Output columns: EMAIL, NAME, CREATED (created_at), LOCKED (derived from locked_until), FAILS (failed_login_count), ROLES (role@workspace-slug, comma-joined across workspaces).
The lockout column reflects the
users.locked_until timestamp set by the failed-login throttle — list-users --locked-only followed by reset-password is the canonical “unlock this user” pair.
crewship admin promote
Grant a workspace role to an existing user. Roles ascend VIEWER < MEMBER < MANAGER < ADMIN < OWNER; the command updates the user’s existing row in workspace_members (the user must already be a member — a non-member errors with user is not a member of workspace).
This is the post-
init step for bootstrapping a second admin: the first user from crewship init is OWNER automatically; everyone else starts as a regular member and needs admin promote to gain elevated access without going through the UI.
crewship admin invalidate-sessions
Force-logout every active session for one user without changing their password. The user can still sign in normally afterwards — they just have to re-authenticate on every device they were already on.
Each revoked row is stamped
revoked_reason='admin_invalidate' so the audit trail distinguishes this from the side-effect revokes that fire during reset-password (those carry revoked_reason='password_change').
When to reach for this instead of reset-password
When to reach for this instead of reset-password
- Laptop stolen / recovered, the password is believed safe but a cached cookie might still be on the device.
- Suspected token leak via Slack screenshot, browser history dump, etc.
- Periodic compliance sweep — “log everyone out of yesterday’s sessions”.
crewship admin sessions list
Forensic read of user_sessions for one user — mirrors crewship session list but for arbitrary users (the user-side command is self-only). Admin-only via direct DB access.
Output columns: ID, STATUS (derived from
revoked_at + expires_at; revoked rows show revoked:<reason>), CREATED, LAST USED (last_used_at), EXPIRES, IP, UA (user_agent). Sorted created_at DESC. A typo’d email reports no user with email <addr> rather than silently returning zero rows.
Pair with admin invalidate-sessions to act on what sessions list surfaces.
crewship admin prune-legacy
Removes orphaned pre-C1 (slug-only) crew docker resources — volumes and
containers named <prefix>-{home,tools,team}-<slug> (e.g.
crewship-3-tools-engineering) left over from before the C1 naming change
(2026-06 audit, which re-keyed crew resources to include the crew id).
Why this exists
A legacy slug-only volume survivescrewship seed --nuke: the nuke clears
the database, and crew teardown removes the id-scoped volumes — but never the
orphaned slug-only ones, because no crew row references them. While they exist,
the runtime’s legacy-resource guard refuses to start the crew’s container, and
the failure reaches users only as a generic “failed to start agent
container”. Every agent in the affected crew fails; a seed --smoke-test
reports N/N agents failed.
crewship doctor surfaces this proactively: the legacy crew resources check
calls the authenticated GET /api/v1/admin/legacy-resources endpoint and
WARNs with this command as the remediation when orphaned resources are
present. (Detection runs on this admin endpoint, not on the unauthenticated
/healthz hot path, so a slow docker daemon can never stall health probes.)
The prune is instance-wide: legacy docker names carry no workspace or crew
id, so detection (what crewship doctor surfaces) and prune enumerate the same
full crew set — otherwise the doctor could WARN on a slug the prune can never
reach. It removes only the orphaned legacy names; the id-scoped resources
the live runtime uses are excluded (slug/id-collision safe) and never touched.
It reports each resource removed:
OWNER or ADMIN role. Returns 503 when the server’s container
provider is not docker (nothing to prune).
crewship admin reap-orphan-containers
Finds — and, with --apply, reaps — crew containers orphaned by an
internal-token master rotation. When crewshipd restarts with a different
internal-token master while a per-crew agent container survives, that container
keeps the crew-bound token (crwv1.<ws>.<crew>.<mac>) it was minted with, and
the new process rejects it forever:
docker ps still shows it “healthy”, and it re-emits the warning every reap
interval (log-noise that drowns real signal). This command clears those
orphans.
Reading the result: “none found” vs “could not tell”
The detector compares the token fingerprint a container’s sidecar advertises on/health against the one the server would mint for that crew today. It fails
safe: a container that advertises no fingerprint is never classified as an
orphan, so a reap only ever removes something positively proven stale.
That safety has a corollary — a sidecar too old to advertise token_fp at all
makes the detector structurally unable to find anything, while still
reporting zero orphans. So the sweep also reports its own coverage:
--format json carries inspected, identified and detector_inert. A server
predating these fields omits them, and the CLI then falls back to the older,
non-committal wording rather than inferring a coverage figure it wasn’t given.
One-time step after upgrading to the stable-master build
The stable internal-token master (#1385) stops future restarts from ever orphaning a container. But the deploy that first introduces it rotates the master once — from the old per-boot random to the value derived fromENCRYPTION_KEY. Any container that
outlived that one deploy is orphaned exactly once and does not self-heal.
After rolling out the new build, do one of:
- Recreate the surviving containers — a normal redeploy / crew re-dispatch starts a fresh container that re-mints a valid token; or
- Run
crewship admin reap-orphan-containers --applyto stop+remove just the orphaned ones (the next dispatch to each crew recreates them fresh).
CREWSHIP_INTERNAL_TOKEN or the ENCRYPTION_KEY it derives
from.
How detection stays safe
For each of the workspace’s crews the command probes the running container’s sidecar/health for the token fingerprint it advertises (token_fp, #1385)
and compares it against the fingerprint of the token the server would mint
today. A container is listed only on a definite, non-empty mismatch —
an unreachable sidecar, a pre-#1385 sidecar that reports no fingerprint, or an
unconfigured master are all treated as unknown and are never reaped. A
healthy container is never touched. Any agent inside an orphaned container is
already broken (its credentials can’t sync), so recreation restores it rather
than interrupting healthy work.
Dry-run prints the orphans it found; --apply reports what it reaped:
OWNER or ADMIN role. Returns 503 when the server’s container
provider is not docker.
See also
crewship init— first-user bootstrap on a fresh database. Afterinit,admin promoteis how you add a second OWNER.crewship doctor— thelegacy crew resourcescheck WARNs when a prune is needed.crewship session— user-scoped self-service of the sameuser_sessionstable.crewship doctor— adjacent host-side diagnostics (data dir, schema version, container runtime).
crewship admin health
ENCRYPTION_KEY came from. generated means
Crewship auto-created it into <dataDir>/secrets.env, next to the database it
protects — so a copied disk, a restored snapshot or a stray backup carries the
ciphertext and what opens it. external means the operator injected it.
Log level shows the live level, and says (temporary) when a timed
override is in effect — a level that reverts on its own is a different fact
from one someone set for good.
crewship admin stats
crewship admin gdpr
Answers the two requests a person can make about their own data.
--reason is not paperwork. It is the audit trail, and it is what answers “why
was this person’s data removed” a year later — so delete refuses without it,
and refuses again without --yes. Both commands write an append-only row and
return an action id: that id is the receipt for the request you were
answering, so record it against the request.
export writes to stdout by default (pipe it, redirect it); --out writes a
file with 0600, since the file is every row about a person.
crewship admin security-posture
Report how this instance is postured: encryption at rest, the private-egress ceiling, signup policy, rate limiting, and whether email/OAuth are configured.
No secret value is ever returned — only whether something is configured. That is the whole contract, and it’s what lets this be a plain admin
GET with nothing worth stealing in the response. A regression test asserts no secret-shaped env value reaches the wire, including via a warning message.What it reports
Each value is read through the same accessor the runtime enforces with, not re-parsed locally. A posture that parses a flag its own way eventually disagrees with the code that acts on it, and a security report that confidently states the wrong thing is worse than no report.
Intent vs. effect
The rate limiter is reported twice on purpose. In production the limiter runs regardless of the disable flag (MustNotDisableRateLimitInProd), so:
Warnings
Beyond the raw state, the report derives warnings that name the consequence:plaintext_secrets_allowed(high) — anyone with the database file can read every stored secret.encryption_key_missing(medium, high when combined with the above) — the combination means new credentials are being written in the clear.rate_limit_disabled(medium) — credential-stuffing and the/credentials/testoracle are unthrottled.rate_limit_disabled_ignored_in_prod(info) — a disable flag is set but the production guard keeps the limiter running. Not an exposure; it means your config and the running state disagree, which matters the first time that config lands somewhere the guard doesn’t apply.signup_open(medium, high in production) — anyone who can reach the instance can create an account.private_endpoints_ceiling_open(info) — crews that also opt in can reach RFC1918/loopback. Link-local and cloud-metadata stay blocked regardless.
encryption_key_generated(medium) — Crewship bootstrapped the master key itself instead of an operator supplying one, so the key lives with the data it protects.privileged_credentials_enabled(high) — at least one workspace hasallow_privileged_credentialson, which removes the fail-closed boundary between privileged crews and stored secrets.private_endpoints_in_use(medium) — crews have opted into private-endpoint egress and the instance ceiling is open, so the reach is live rather than merely permitted.seed_account_default_password(high) — thecrewship seeddemo account still authenticates with the password printed in these docs.no_backup_recorded(medium) — nobackup.*action appears in the audit trail. Reported only when that query succeeds: a failed probe is unknown state, not a missing backup.
crewship admin memory-config
Read or adjust workspaces.memory_config — currently the retention window for memory_versions rows, consumed by the per-workspace retention sweep.
The command also accepts the alias crewship admin memcfg.
GET/PATCH /api/v1/admin/memory/config) exists so retention can be changed without editing SQLite by hand. Until this command there was no client for it, so the hand-edit stayed the only route — which is what the endpoint was built to avoid.
memory-config get
Prints the resolved value and, importantly, whether it was chosen:
30 days on its own doesn’t tell you whether anyone picked it. That matters before changing it: overriding a built-in default is routine, overriding somebody’s deliberate policy is not.
When the stored document carries keys this CLI version doesn’t model, they are printed verbatim underneath rather than hidden — otherwise get would look authoritative when it isn’t.
memory-config set --retention-days N
Writes are manage-tier and emit
memory.config_updated to the journal, so a compliance audit can trace when retention policy changed and who changed it.
Sent as a partial PATCH carrying only the key being changed. The server merges it into the stored document and preserves settings this CLI doesn’t know about, so an older binary can’t silently drop a newer knob by writing a whole document back.
The same data backs the Admin → Runtime → Memory configuration card in the dashboard.
crewship admin ratelimits
Inspect and tune the instance’s rate limiters at runtime — the same surface as the dashboard’s Infrastructure → Rate Limiters tab. Every limiter that was once a hardcoded constant (the per-IP HTTP buckets, the login lockout, the notification anti-storm bucket, crew provisioning, agent webhooks) is listed and overridable without a redeploy.
Aliases: crewship admin rate-limits and crewship admin ratelimit.
list shows each limiter’s group, key, current value, shipped default, unit, and whether the value is a default or an override:
set <key> <value> overrides a limiter; reset <key> reverts it to the shipped default. Overrides persist in the instance-global rate_limit_overrides table and take effect immediately — the per-IP HTTP buckets are retuned on the live limiter, the rest read their value on next use.
Writes are manage-tier (OWNER/ADMIN) and logged at WARN with the actor. A value outside a limiter’s allowed range is rejected (
400); an unknown key is 404.
Raising
http.auth_per_min is the fix for an instance whose users get logged out under heavy refresh behind a shared NAT. On current builds the read-only session polls that caused that no longer touch this bucket, so most instances never need to.