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.
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.
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.Two planes on one host:
| Plane | Address | Purpose |
|---|---|---|
| Console | https://localhost:9000 | Operator console UI/API with embedded dashboard and built-in internal CA (InternalCa). Binds 127.0.0.1 by default. |
| Relay | wss://0.0.0.0:8800 | Opaque mTLS message router. Network-accessible so devices can reach it; every message is mTLS-gated. |
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).After the service starts, the installer runs a first-boot sequence, then a verification pass. It is idempotent, safe to re-run:
https://localhost:9000/health until the console answers. The console auto-initializes InternalCa on boot (root key + self-signed root cert, persisted in HardenedStore).--init-admin (Ed25519 keypair + cert signed by InternalCa). Skipped if admin_cert.pem already exists.--init-operator admin creates the first operator and prints the ck_… API key once. Skipped if operators already exist./api/v1/enrollment/ca-fingerprint, and checks the service is active.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).
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.
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.
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:
| Value | What it is | Where it comes from |
|---|---|---|
ENROLLMENT_KEY | Reusable key (ek_…) bound to a client scope; enrolls many devices | Console: 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_URL | Your console's URL, e.g. https://fleet.example.com:9000 | The host you installed the console on |
CA_FINGERPRINT | Root CA pin (sha256:…) the device verifies before trusting anything | curl -fsk https://<console>:9000/api/v1/enrollment/ca-fingerprint, the installer's verification line, or crawdad-fleet ca export |
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
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 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.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.
push_kdl_policy command; the device loads it and governs at the wire after the push, surviving restart.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.
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.
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.
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.
POST /api/v1/msp/provision and /api/v1/msp/customers/provision create the scope, a Default group, a reusable enrollment key, and optional policy templates.POST /api/v1/msp/customers/:id/rollout mints a reusable enrollment key and returns a ready-to-run install package, the crawdad enroll command, the unattended installer invocations, the ENROLLMENT_KEY/CONSOLE_URL/CA_FINGERPRINT contract, and the CA fingerprint pin, so a client’s whole fleet self-enrolls with real signed certificates and no per-device manual signing.GET /api/v1/msp/billing/metered previews the current count and amount with no Stripe call; POST /api/v1/msp/billing/report-usage runs the live cycle and is admin-gated.GET /api/v1/msp/rollup returns per-client device counts, open governance holds, governance and threat activity, and the metered cost, plus fleet totals, respecting operator visibility.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
rm -rf <data-dir> command if you truly want to erase it.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>
The installer's preflight checks these before touching anything, and stops if any fails:
9000 (console) and 8800 (relay) freecurl availableNo Docker, no Compose, no external database, one binary and one data directory.
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).
| Flag | What it does | When to run |
|---|---|---|
--init-admin | Generates Ed25519 admin signing identity (key + cert signed by InternalCa) | Once, before first console start |
--init-operator NAME | Creates the first operator with a random API key. Refuses if operators exist. | Once, after --init-admin |