Skip to content

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:

Terminal window
chrlctl register --code chrl1-aeabky-3iovzg-…-osa6d4

The 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.

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:

  1. 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”.
  2. 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/watch adds 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.
  3. 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 chrlctl installs, so changing it later means registering the host again.
  4. Paste the provisioning code, if you did not pass one with --code.
  5. 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.

The client prints a progress trail on stderr as it works, so you can follow along:

  1. Resolves the application. In the guided flow this is your answer. Unattended, chrlctl walks 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.
  2. Builds the configuration. It hashes the application binary with SHA-256 and collects the unit name, executable path, and monitored paths into the upload.
  3. 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. chrlctl never trusts a server it cannot verify.
  4. 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.
  5. 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.
  6. 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.

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.

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.

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:

Terminal window
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.

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.

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.

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.