Running chrlctl
chrlctl, the Churchill Host CLI, is a single binary and the only Churchill executable that lives on the host’s disk. There is no config file and no environment variables to set: the provisioning code carries the control plane’s address and its pinned server key, so chrlctl knows where to connect and how to verify what answers. The registration wizard’s connect step supplies a per-architecture download link and a hash to verify the binary against before you run it. On a new host, enrollment is then one command, run as root:
chrlctl register --code chrl1-aeabky-3iovzg-…-osa6d4The value you pass to --code is a single-use provisioning code issued from the dashboard: one chrl1-… string of about 160 characters, which you paste whole. See Provisioning codes. Before you run the command, confirm the host meets the platform baseline. See Host requirements.
The guided flow
Section titled “The guided flow”Run on a terminal without --unit or --exec-path, chrlctl register walks you through the registration before it sends anything. The output is plain numbered menus and y/N prompts, so it works over SSH and on a serial console. In order, it asks you to:
- Pick the application. It lists the unit-backed applications it discovered, one line each with the unit, the executable, and the pid, and marks the one it would have picked on its own as the default. Services that cannot register at all (systemd template instances, whose names the control plane refuses) are listed separately as “Not selectable”.
- Choose what to monitor. The application binary is pinned by its SHA-256 and always covered, so it is shown but not offered as a choice. Everything else is a checklist you toggle by number, and
a /path/to/watchadds a path of your own. A path the application writes to while it runs is flagged, because monitoring it reports the application’s own writes as violations. - Answer the session-recording question. “Record interactive login sessions on this host? [y/N]”. Recording covers every account on the host, not only the one the application runs as: it makes the session recorder the login shell and streams terminal output to the control plane. Keystrokes are not captured. Declining is the default and leaves the host untouched. The answer travels with the poller
chrlctlinstalls, so changing it later means registering the host again. - Paste the provisioning code, if you did not pass one with
--code. - Confirm the summary. It prints the application, unit, executable, monitored paths, code, scan cadence, and the recording answer, and asks “Register this application? [y/N]”. Anything but yes cancels, and nothing is registered.
chrlctl skips the flow, and auto-selects the application instead, whenever it is run unattended: with --non-interactive, with --unit or --exec-path (which already say what to register), or when its input is not a terminal, as in a script or a systemd unit. On that path --code is required.
What happens, in order
Section titled “What happens, in order”The client prints a progress trail on stderr as it works, so you can follow along:
- Resolves the application. In the guided flow this is your answer. Unattended,
chrlctlwalks the running systemd services, filters out system services and Churchill’s own machinery, and requires exactly one candidate. Zero or several candidates stop the run rather than risk a wrong pick. See the flags below. - Builds the configuration. It hashes the application binary with SHA-256 and collects the unit name, executable path, and monitored paths into the upload.
- Dials the control plane. The connection is outbound from the host over TLS (port 62203 by default), and the server’s ed25519 identity is pinned.
chrlctlnever trusts a server it cannot verify. - Establishes the host identity. On first run it generates a persistent host identity key at
/etc/churchill/identity/host-identity.v1. The private key never leaves the host. A corrupt or tampered record is never silently repaired or regenerated; re-establishing trust takes a fresh provisioning code. This is by design. - Redeems the code. The control plane pairs the host, assigns its host ID, and issues a per-host certificate, stored at
/etc/churchill/identity/host-cert.pem. - Uploads the configuration. The discovered application configuration goes up over the same authenticated session.
On success, exactly one line lands on stdout:
status=registered host_id=<uuid> cert=true manifest_sha256=<hex>cert=true confirms the per-host certificate was stored, and manifest_sha256 is the fingerprint of the configuration the control plane recorded. After that line, chrlctl finishes the host setup. On SELinux hosts it installs Churchill’s SELinux module (this needs policycoreutils). Then it copies itself to /opt/churchill/bootstrap-client and installs, enables, and starts churchill-deploy.service, the poller described below. If the host is SELinux-enforcing and the module install fails, the run stops there: the host is registered, but the poller is deliberately not set up until the module is in place. Non-enforcing hosts continue with a warning.
Exit codes
Section titled “Exit codes”The success line is the only thing printed to stdout, so register scripts cleanly. Its exit code answers the one question that matters afterwards: is the provisioning code still worth anything?
| Code | Meaning |
|---|---|
0 |
Success. |
1 |
The run failed before the control plane redeemed the code. The code was not consumed and the same one can be used again. |
2 |
Usage or local validation failure: a bad flag, a malformed code, a checksum mismatch. Nothing was sent. |
3 |
The run failed after the code was redeemed. The code is spent. Issue a new one. |
chrlctl deploy redeems no code, so it reports 0, 1 (a poll, staging, or adoption step failed and the resident loop re-polls), and 2 (a missing or unusable flag) only.
What gets written on the host
Section titled “What gets written on the host”| Path | What it is |
|---|---|
/etc/churchill/identity/host-identity.v1 |
The host identity key record. Created on first register, reused after that. |
/etc/churchill/identity/host-cert.pem |
The per-host certificate issued at pairing. |
/etc/churchill/identity/tenant_id |
The tenant this host joined. Every certificate in a delivered bundle is checked against it, so a host that has no record of its tenant refuses to deploy. |
/opt/churchill/bootstrap-client |
A copy of chrlctl itself. This is the binary the poller unit runs. |
/etc/systemd/system/churchill-deploy.service |
The deploy poller unit, installed, enabled, and started after a successful register. It carries the control plane’s address and key pin, taken from the provisioning code, because the code is spent by the time the poller first runs. |
| SELinux module | Churchill’s policy module, installed on SELinux hosts. |
All of these persist across reboots. The protection runtime that arrives later does not appear in this list on purpose: it lives only in memory and does not survive a reboot.
Choosing the application yourself
Section titled “Choosing the application yourself”Naming the application on the command line skips the guided flow entirely. Unattended discovery is fail-closed: chrlctl registers an application only when the host has exactly one candidate. When it finds none or several, name the service yourself:
chrlctl register --code chrl1-aeabky-3iovzg-…-osa6d4 --unit myapp.service| Flag | What it does |
|---|---|
--code |
The provisioning code. Required unattended; the guided flow asks for it when it is omitted. |
--unit |
The systemd unit of the application to register. The unit must be running unless you also pass --exec-path. Skips the guided flow. |
--exec-path |
The application’s executable path, for when it cannot be resolved from the unit. Skips the guided flow. |
--paths |
Monitored paths, comma-separated. The list you pass replaces the discovered set, and the guided flow skips its checklist. |
--scan-cadence |
How often the host sentinel re-verifies the monitored files. Defaults to 6h. |
--session-recording |
Record every interactive login on this host. It pre-answers the guided flow’s question; unattended, its absence is the opt-out. |
--non-interactive |
Never prompt. Auto-discover the application instead of running the guided flow. |
--env-name |
An optional environment name binding. Some control planes require it and say so. |
--identity-dir |
Where the identity record, certificate, and tenant id live. Defaults to /etc/churchill/identity. |
--no-install-poller-unit |
Skip installing churchill-deploy.service. |
To check which client build you have, run chrlctl --version. The flag goes before the subcommand, not after it.
There is no --addr and no certificate-authority flag to set in normal use: the provisioning code carries the control plane’s address and the pinned server key, and the dial trusts that pin and nothing else. Hidden development-only overrides (--addr, --server-spki-pin, --server-name) exist for lab stacks and are left out of --help on purpose.
Common errors
Section titled “Common errors”chrlctl checks the code itself before it dials anything, and it exits 2 without consuming the code when the check fails. provisioning code: checksum mismatch means a character was mistyped or lost in copying: copy the code from the dashboard again. provisioning code: not a chrl1- code means the value is not a provisioning code at all.
Rejections from the control plane print as server register-rejected: <reason>. Here is what each message means and what to do about it:
| Message | Meaning | What to do |
|---|---|---|
missing required flag: --code |
An unattended register run had no code. The guided flow prompts for one instead. | Issue a code from the dashboard and pass it with --code. |
unknown_code |
The control plane has no record of this code. Typos are caught locally by the checksum, so the code was minted by a different control plane or tenant, or was never issued here. | Confirm the code came from this control plane’s dashboard, and reissue. |
already_redeemed |
Provisioning codes are single-use, and this one has been spent. | Issue a new code. |
expired |
The code’s validity window has passed. | Issue a new code. |
revoked |
An operator revoked the code. | Issue a new code. |
cidr_scope |
The host connected from outside the network the code is scoped to. The code is not consumed. | Fix the host’s network path or issue a code scoped to the right network, then retry. |
no protected-app candidate discovered on this host; pass --unit |
Discovery found no eligible running service. On a terminal the guided flow says the same thing and stops. | Start your application under systemd, or name it with --unit and --exec-path. |
multiple app candidates discovered (…); pass --unit to choose |
An unattended run found more than one eligible service. The guided flow lists them and asks instead. | Name the one you mean with --unit, or run chrlctl on a terminal and pick. |
could not resolve exec path for unit "x" (is it running?); pass --exec-path |
The named unit has no running process to resolve a binary from. | Start the service, or supply the binary’s path with --exec-path. |
paired but upload failed: … |
The host is paired (its identity and certificate are in place) but the configuration upload did not complete. | Run register again with a fresh code. The identity on disk is reused, not regenerated. |
The deploy poller
Section titled “The deploy poller”Registration pairs the host. Protection arrives later, once the application’s registration has been approved. That delivery is the job of churchill-deploy.service. It is a resident poller: it starts at enrollment, stays running through the whole CAB gap, and polls the control plane about every 15 seconds (--poll-interval). Most polls find nothing and report status=deploy-none. That is the healthy idle state, not an error, and it is printed only when the status changes, so a week of idling is one line rather than thousands.
When CAB approval publishes a protection bundle for the host, the poller fetches it, verifies every signature and hash before anything touches the host, stages it into a sealed read-only in-memory runtime, reports adoption back to the control plane, and becomes the host sentinel in the same process.
To inspect a pending directive without applying anything, run chrlctl deploy --verify-only. It polls, verifies what it finds, reports the result, and stops. It never stages or adopts.
Confirming it worked
Section titled “Confirming it worked”The status=registered line tells you the host side succeeded. To confirm the control plane agrees, and to watch the host come alive in the console, see Verifying enrollment.