Deploy your fleet

Stand up a complete Crawdad fleet, console and relay, with a built-in internal CA, with one command. Then enroll your first device and set policy.

1 Install

curl -fsSL https://getcrawdad.dev/fleet-install.sh | sh

The fleet console is a single native binary, no Docker. The installer runs a preflight (ports free, disk space, curl present), downloads and SHA-256-verifies crawdad-fleet, installs a native OS service (systemd on Linux, launchd on macOS), and boots the console.

Root vs. per-user is decided by how you run it: as your own user it installs to ~/.local/bin with data in your home directory and a per-user service; with sudo it installs to /usr/local/bin with data in /var/lib/crawdad-fleet (Linux) or /usr/local/var/crawdad-fleet (macOS) and a system service.

On first boot it bootstraps the admin identity, creates the first operator, and prints your operator login key (a ck_… key, shown once) plus a ready-to-paste enroll command carrying the real --ca-fingerprint.

Environment overrides: CRAWDAD_FLEET_DATA_DIR (data directory), CRAWDAD_FLEET_INSTALL_DIR (binary location), CRAWDAD_FLEET_URL (download mirror), CRAWDAD_FLEET_VERSION (pin a version). The console binds port 9000 and the relay 8800, both fixed in the service unit.

What you get

Two planes on one host:

PlaneAddressPurpose
Consolehttps://localhost:9000Operator console UI/API with embedded dashboard and built-in internal CA (InternalCa). Binds 127.0.0.1 by default.
Relaywss://0.0.0.0:8800Opaque mTLS message router. Network-accessible so devices can reach it; every message is mTLS-gated.
Type the https:// prefix when you open the console. Without it, the browser sends plain HTTP to the TLS port and shows a confusing error (ERR_SSL_PROTOCOL_ERROR or binary garbage).

What the first boot does

After the service starts, the installer runs a first-boot sequence, then a verification pass. It is idempotent, safe to re-run:

  1. Wait for health, polls https://localhost:9000/health until the console answers. The console auto-initializes InternalCa on boot (root key + self-signed root cert, persisted in HardenedStore).
  2. Init admin identity, --init-admin (Ed25519 keypair + cert signed by InternalCa). Skipped if admin_cert.pem already exists.
  3. Init operator, --init-operator admin creates the first operator and prints the ck_… API key once. Skipped if operators already exist.
  4. Restart the service, picks up the admin identity.
  5. Verify, confirms the console responds, reads the CA fingerprint from /api/v1/enrollment/ca-fingerprint, and checks the service is active.

2 Log in to the console

Open https://localhost:9000 (note the https://) and log in by pasting the operator key (ck_…) from the installer output. The console uses the internal CA's self-signed certificate, so your browser shows a one-time warning, proceed past it, or trust the root permanently:

# macOS
sudo security add-trusted-cert -d -r trustRoot \
  -k /Library/Keychains/System.keychain \
  "<data-dir>/internal_ca_root_cert.pem"

# Linux
sudo cp "<data-dir>/internal_ca_root_cert.pem" \
  /usr/local/share/ca-certificates/crawdad-fleet.crt
sudo update-ca-certificates

The installer prints the exact <data-dir> (system install: /var/lib/crawdad-fleet on Linux; per-user install: under your home directory).

3 Create a scope and add a device

  1. In the console, create a scope (e.g. “Production”)
  2. Go to the Control lens
  3. Enter a device ID and click Add device
  4. The console generates a single-use token and shows a copy-paste enrollment command with the CA fingerprint included

4 Enroll the device

On the target machine (where the sidecar is installed):

curl -fsSL https://getcrawdad.dev/install.sh | sh

crawdad enroll \
  --token et_TOKEN \
  --console-url https://fleet-host:9000 \
  --ca-fingerprint sha256:FINGERPRINT

Replace with the values from the console’s copy-paste command. The --ca-fingerprint pin verifies the root CA before the device trusts anything, see Enrollment for the trust model.

5 Set policy

In the console, create a policy template on the scope (Control lens → policy section). The device inherits it immediately. The device is now enrolled, connected via mTLS, and receiving fleet commands.

Bulk enrollment for MSPs (RMM / MDM push)

The per-device token above enrolls one machine at a time. To roll out a whole client fleet unattended, use a reusable enrollment key plus the CA fingerprint, pushed through your RMM. Three values travel together, the enrollment trio:

ValueWhat it isWhere it comes from
ENROLLMENT_KEYReusable key (ek_…) bound to a client scope; enrolls many devicesConsole: provision a customer, or POST /api/v1/enrollment/keys. The turnkey POST /api/v1/msp/customers/:id/rollout mints one and returns the whole install package.
CONSOLE_URLYour console's URL, e.g. https://fleet.example.com:9000The host you installed the console on
CA_FINGERPRINTRoot CA pin (sha256:…) the device verifies before trusting anythingcurl -fsk https://<console>:9000/api/v1/enrollment/ca-fingerprint, the installer's verification line, or crawdad-fleet ca export

macOS / Linux

The sidecar installer reads the trio from the environment and auto-enrolls after install, with no interaction:

curl -fsSL https://getcrawdad.dev/install.sh | \
  ENROLLMENT_KEY=ek_YOURKEY \
  CONSOLE_URL=https://fleet.example.com:9000 \
  CA_FINGERPRINT=sha256:YOURFINGERPRINT sh

Or enroll an already-installed device directly:

crawdad enroll \
  --key ek_YOURKEY \
  --console-url https://fleet.example.com:9000 \
  --ca-fingerprint sha256:YOURFINGERPRINT

Windows (RMM)

Set the same three as environment variables, then run the installer as Administrator, the form to drop into an RMM script:

$env:ENROLLMENT_KEY = "ek_YOURKEY"
$env:CONSOLE_URL = "https://fleet.example.com:9000"
$env:CA_FINGERPRINT = "sha256:YOURFINGERPRINT"
irm https://getcrawdad.dev/install.ps1 | iex
Key vs. token. --key ek_… is a reusable enrollment key for self-registration (fleet / bulk); --token et_… is a single-use per-device token (targeted, from the console's Add device). Both take --console-url and --ca-fingerprint. The fingerprint pin is required for secure enrollment, --insecure skips it and is dev-only.

Governance at fleet scale

Beyond policy inheritance, the console rolls up Contextual Agency Governance across your fleet. Each device governs its agents by an operator-declared charter (an allowlist over the tools, data, and effects an agent’s job needs, enforced on the observed action) and watches the whole session for staged compromise with a trajectory layer. The console is the fleet view and control point for those decisions.

Every governance decision rides the same sealed telemetry and signed command channel every other fleet operation uses. See the Fleet Console page for the full story.

Enforcement mode across a fleet

The 1.6 Enforce controls, the OS egress lock, the Maximum sealed-VM tier, fail_mode, and break-glass, are configured per device today: through the device admin API (POST /api/v1/mode/enforcement) or protection_mode.json, and recovered with the root + admin-token crawdad enforce CLI. See the Enforce mode guide. A device can be pinned to Enforce through a tamper-independent fleet.json breadcrumb, so a pinned device that is tampered coerces itself back up to Enforce (fail-closed) and refuses a local downgrade to Monitor.

The console now surfaces this enforcement posture read-only — see Fleet enforcement posture below — including each device’s Monitor/Enforce state, egress lock, tier, fail_mode, fleet-pin, and any open break-glass window. And it now commands that surface fleet-wide from the Fleet actions view: flip a device or fleet Monitor↔Enforce, set the tier and fail_mode, and apply or lift the survives-tamper fleet-pin. Strengthening actions (to Enforce, Maximum, fail-closed, pinned) take a light path — an operator plus one confirmation, kept safe by a server-side validator that rejects any weakening — while any reduce action is destructive and runs behind the full ceremony: a typed scope name, a single-use step-up confirm-token, a second operator, and a cool-down for the largest scopes. Opening break-glass stays device-local by design — that is still the root crawdad enforce CLI on the device, and the console only surfaces an open window. Alongside this, the console manages detection-strictness protection modes, dispositions, charters, and per-tool rules across the fleet.

Fleet enforcement posture

The console’s Fleet posture view rolls up each device’s 1.6 runtime-enforcement posture across the scope — Monitor/Enforce, egress lock, tier, fail_mode, credential broker, fleet-pin, and break-glass — from the flat enforcement_* fields each 1.6 device forwards on its posture report. It is read-only and comes in Simple and Rich modes. A device on a pre-1.6 agent forwards none of these fields, and its row reads “enforcement not reported” — an explicit state, never inferred as enforcing or not.

Green means green. A device shows green only when it is enforcing with the OS egress lock engaged. A clean Monitoring device is a deliberate, healthy, non-green neutral state — named honestly, never as “not yet governed.” Any degradation (tier fallback, open break-glass, Enforce without a lock, fail-open under Enforce, policy drift) is amber; a device pinned to Enforce but reporting Monitor is red. The roll-up reads green only when every device does.

Honest limits. The Drift chip is attention, not confirmed tamper — it fires when a device reports no fleet_policy.json, which includes a legitimately standalone device not yet under a fleet policy. A Windows device under Enforce reads amber (“Broker-only”) by design: Windows has no OS egress lock, and the mandatory credential broker is the enforcement ceiling there. And this view is verified by construction and tests against the exact wire shape the device forwards; the live device→console end-to-end proof across a multi-device fleet is a ship-checklist item.

MSP provisioning and billing

The console is MSP-native. One tenant model carries the MSP root, its client organizations, and their device groups, with role-based access that inherits down the tree and reads that respect each operator’s scope.

Day-2 operations

The console runs as a native OS service, no Docker. Manage it with your platform's service tools:

# Status + logs — Linux (system install, run with sudo)
systemctl status crawdad-fleet
journalctl -u crawdad-fleet -f

# Status + logs — Linux (per-user install)
systemctl --user status crawdad-fleet
journalctl --user -u crawdad-fleet -f

# Status + logs — macOS
launchctl list com.crawdad.fleet
tail -f <log-dir>/crawdad-fleet.log

# Backup / restore (restore stops the service first)
crawdad-fleet backup /path/to/backup
crawdad-fleet restore /path/to/backup

# Upgrade in place (data, operators, and CA preserved)
curl -fsSL https://getcrawdad.dev/fleet-install.sh | sh -s -- --upgrade

# Uninstall — removes binary + service; DATA DIRECTORY IS PRESERVED
curl -fsSL https://getcrawdad.dev/fleet-install.sh | sh -s -- --uninstall
Uninstall preserves your data (operators, CA, enrollment records) so you can reinstall against the same fleet. The uninstaller prints the exact rm -rf <data-dir> command if you truly want to erase it.

Operator key management

The operator key (ck_…) is shown once during install. Only its SHA-256 hash is stored, a lost key cannot be recovered, only rotated. Run these on the console host (the binary is crawdad-fleet):

# List operators
crawdad-fleet operator list

# Reset a key (prints a new key once)
crawdad-fleet operator reset-key --id <operator-id>

# Create a new operator
crawdad-fleet operator create --name <name>

Prerequisites

The installer's preflight checks these before touching anything, and stops if any fails:

No Docker, no Compose, no external database, one binary and one data directory.

Manual deployment

For custom infrastructure or deploying components separately, see the Console Guide and the full deployment reference (covers manual relay and console setup, including the built-in internal CA).

Console bootstrap flags

FlagWhat it doesWhen to run
--init-adminGenerates Ed25519 admin signing identity (key + cert signed by InternalCa)Once, before first console start
--init-operator NAMECreates the first operator with a random API key. Refuses if operators exist.Once, after --init-admin

Security notes