Cronomicon · Runner Install Guide
🔧 Runner Guide

Cronomicon

The self-hosted Script Orchestrator — schedule and run Bash, Ansible, Terraform, PowerShell, Perl and Python jobs across your fleet from one auditable web console.

Guide Runner install & configure guide Version R7.2

A step-by-step walk-through of installing and configuring an Cronomicon runner. A runner is an out-of-process worker that registers with the server, long-polls for runs the server assigned to it (executor='runner'), executes them, and streams logs back. It is the second execution method alongside the in-app SSH executor — it exists to run ansible/terraform (which need a local toolchain the app's distroless image lacks) and to reach network-isolated targets the server can't.

Scope. This guide ends once the runner is registered and online. For day-2 operations — health checks, draining, updating the agent, re-syncing config to the server, and removing a runner — see Runner Config Guide.

Credential model (b), locked (runners-update.md D1). Cronomicon never decrypts or transmits private-key bytes. The runner holds its own keys; the server only ships host references. See runner-security.html for the full posture and blast-radius record.

The agent binary is cronomicon-runner (built from backend/cmd/cronomicon-runner). Its complete config surface lives in internal/agent/config.go; the var names in this guide are the real ones.


0 · Before you begin

Have these ready before you start:

The fastest route on a Linux VM is the runner-install.sh script (§3), which does §2/§4/§9 in one shot. Read §1 first to get a token, then jump to §3.


1 · Add a runner (one click) — or mint a token by hand

Fast path — the + Add Runner button. On the Runners view, click + Add Runner, optionally label it, and press Mint & build command. You get a single flagless one-liner:

curl -fsSL https://cronomicon.example.com/install/crn_reg_… | sudo bash

Paste it on the host as root — done. The server URL, the token, and the agent-binary download are baked into the script by the /install/<token> endpoint, so nothing rides the command line; capabilities auto-detect on the host (§8). A download-inspect-run variant is offered for shops that don't pipe curl into sudo (the saved file is already personalized, so it too runs with no arguments). This is the recommended path; the manual token/flag flow below still exists for pre-authored installs and config management.

Behind an SSO / forward-auth proxy? If curl …/install/<token> fails with bash: syntax error near unexpected token `<' / `<!doctype html>', the reverse proxy redirected the request to its login page and returned HTML instead of the script. The runner endpoints are unauthenticated by design (the token IS the credential; the runner authenticates the server with its own bearer) and must be bypassed at the proxy — the same bypass the runner's poll/register calls need. Add /install/* (and /agents/*, /runner-install.sh) to the proxy's auth-bypass allowlist; see the administrator manual (section 8.3, runner provisioning behind SSO) for the exact Authelia rule.

In the UI, open the Runners view (top-level nav) and find the Register New Runner card: click Mint token (optionally giving it a label, e.g. the intended runner name) to get a fresh crn_reg_* bearer (24h expiry). Tokens are single-use: each one registers exactly one runner and dies on that first successful registration — the card's token list shows which runner consumed which token. The plaintext is shown only once at mint — Reveal/Copy it immediately; it is stored only as a hash and can never be re-revealed. Installing several runners? Mint one token per host — minting does not invalidate other tokens. Registration mints a long-lived per-runner key (crn_run_*, 365d), persisted to the runner's identity file; the registration token is dead from then on and is never needed again (config changes propagate via Resync, which uses the runner's own key). An unused token can be revoked from the list at any time.

The card's Install Command is a real, copy-pasteable §3 invocation: it curls /runner-install.sh from the Cronomicon server itself and pipes it into sudo bash with this server's URL and the token filled in (the freshly minted token is substituted while its plaintext is revealed — otherwise a <TOKEN> placeholder). A download-inspect-run variant is available under the command for orgs that don't pipe curl into sudo. Run it on the target host next to a pre-built cronomicon-runner binary (§2); capabilities are auto-detected from the host's toolchains at startup — add -c only to narrow them.

(A registration credential can also be seeded out-of-band via CRONOMICON_RUNNER_BOOTSTRAP_TOKEN on the server — unlike minted tokens it is multi-use; see env-matrix.md.)


2 · Choose slim vs fat (capability-driven)

The image you deploy is dictated by the capabilities the runner will declare:

You need…CapabilitiesImageWhy
Run shells over SSH (incl. bastion/ProxyJump), or reach a network-isolated segment (T-b)bash,perl,powershell,pythonslim: the static binary on a distroless baseThe agent only needs an SSH client; it's a tiny static/distroless image.
Run ansible / terraform locally (T-a)ansible,terraformfat: the binary plus Ansible, Python and TerraformThese toolchains run on the executor; the fat image carries ansible+Python and terraform.
Run full ansible project checkouts (roles, vars_files, .j2 templates) — opt-inansible + -allow-checkoutfat + provisioningThe runner fetches the playbooks repo at a server-pinned commit and runs it from a tree; needs a deploy credential + provisioned collections. See §12.

The toolchains are only in the fat image — deliberately. The app's own image is distroless and ships no ansible/terraform (a conscious minimal-control-plane posture), and the slim runner stays slim for the SSH-onward case. Capability routing makes this safe: the server's claim filter (run_type IN caps) guarantees only an ansible-capable runner ever claims an ansible run, so you can run a fleet of slim shell runners and a few fat ansible/terraform runners side by side.

Both images are published to the GitHub Container Registry from every release tag, built for linux/amd64. Pull the tag that matches your server's version (the sidebar footer, or GET /version): the server refuses an agent older than its protocol, and the server and runner images of one release always match.

ImageCarriesBuilt from
ghcr.io/resetsmith/cronomicon-runner:<version>slim: the static agent on distroless/static. No shell, no toolchains.backend/Dockerfile.runner
ghcr.io/resetsmith/cronomicon-runner-fat:<version>fat: the same agent on Debian with ansible (Python), terraform, git and an OpenSSH client.backend/Dockerfile.runner.fat

Each is also tagged <major>.<minor> and latest; pin the full version in production. The Runners view's docker run helper names the image for your server's version.

To build your own instead (a different base, extra collections, another architecture), use the same Dockerfiles with the repository root as the build context:

docker build -f backend/Dockerfile.runner     -t cronomicon-runner:slim .
docker build -f backend/Dockerfile.runner.fat -t cronomicon-runner:fat .

The fat image pins its ansible and terraform versions as build args (ANSIBLE_VERSION, TERRAFORM_VERSION); override them with --build-arg, and add the collections your playbooks need in a derived image.

Or get just the binary for a systemd install — download it from the server, or build it:

cd backend
CGO_ENABLED=0 go build -o cronomicon-runner ./cmd/cronomicon-runner

3 · Fast path — runner-install.sh (systemd, Linux VM)

For a single-host systemd install, deploy/runner-install.sh — served by the app at /runner-install.sh, which is where the Runners view's Download button and the panel's install command fetch it, so the target host never needs to reach anything but the Cronomicon server — automates §2's binary placement, the service account and directories, the env file, and the hardened unit — everything in §9 — in one root-run command, and can also install the runner's known_hosts, key custody, CA trust, and local inventory (§5/§7/§8) so an SSH-capable runner is usable the moment the script exits. It detects Debian/Ubuntu vs RHEL-family from /etc/os-release.

sudo ./runner-install.sh \
  --server   https://cronomicon.example.com \
  --token    crn_reg_xxxxxxxxxxxx \
  --name     runner-dc1-01 \
  --download \
  --known-hosts ./known_hosts \
  --key-dir  ./keys \
  --ca-cert  ./internal-ca.pem   # only for a private/internal CA (§5)

Capabilities are auto-detected. With no -c, the installer writes no CRONOMICON_RUNNER_CAPABILITIES line at all and the agent probes the host's toolchains at startup (bash, perl, pwsh, python3/python, ansible-playbook, terraform) to claim the matching run-types — install a toolchain later and a service restart propagates the new capability to the registry automatically (the config-drift resync). Pass -c only to narrow what the runner claims (e.g. the host has python but must not run python jobs).

FlagRequiredDefaultMeaning
-s, --serveryes—Cronomicon server URL (prepends https:// if no scheme).
-t, --tokenyes—Registration token (crn_reg_*).
-n, --nameno$(hostname)Runner display name (keep it stable).
-c, --capabilitiesno— (auto-detect)Comma-separated run-types claimed. Omit ⇒ the agent auto-detects the host's toolchains at startup; set only to narrow.
-b, --binaryno./cronomicon-runner, ./bin/…, backend/bin/…Path to the pre-built binary.
--downloadnooffFetch the agent binary from the server's /agents/ endpoint (arch auto-detected, SHA-256 verified — a checksum mismatch aborts, never falls back). Falls back to -b/auto-search when the deployment doesn't bundle binaries (§2). Uses --ca-cert for the fetch when given.
--known-hostsno—OpenSSH known_hosts file — installed to /var/lib/cronomicon-runner/known_hosts (0640, owner cronomicon-runner) and wired via CRONOMICON_RUNNER_KNOWN_HOSTS (§8).
--key-dirno—Directory of private keys — copied into /var/lib/cronomicon-runner/keys (0700 dir, 0600 files, owner cronomicon-runner) and wired via CRONOMICON_RUNNER_KEY_DIR (§8). Mutually exclusive with --key-map.
--key-mapno—NAME=path,NAME2=path2 — each key is copied to /var/lib/cronomicon-runner/keys/<NAME> and the rewritten map wired via CRONOMICON_RUNNER_KEY_MAP (§8). Mutually exclusive with --key-dir.
--generate-keyno—<NAME> — mint a fresh passphrase-less ed25519 key at /var/lib/cronomicon-runner/keys/<NAME> (owned by cronomicon-runner) and print its public key to add to each target's authorized_keys. The private key never leaves the host. Refuses to overwrite an existing key. Reference it in Cronomicon host config by <NAME> (the authKeyEnvVar).
--ca-certno—Private CA bundle (PEM) — installed to /etc/cronomicon-runner/ca.pem and wired via CRONOMICON_RUNNER_CA_CERT (§5).
--inventorynocronomiconcronomicon or local (§7); local requires --local-inventory.
--local-inventoryno—Local inventory JSON (with --inventory local) — installed to /etc/cronomicon-runner/inventory.json and wired via CRONOMICON_RUNNER_LOCAL_INVENTORY (§7).
--allow-checkoutnooffOpt into runner-side pinned playbook checkout — writes CRONOMICON_RUNNER_ALLOW_CHECKOUT=true (§12).
--checkout-reposno—Comma-separated allowlist of repo clone URLs the runner may check out — writes CRONOMICON_RUNNER_CHECKOUT_REPOS (§12).
--checkout-token-fileno—Path to a read-only deploy-token file — installed to /etc/cronomicon-runner/checkout-token (0640 root:cronomicon-runner) and wired via CRONOMICON_RUNNER_CHECKOUT_TOKEN_FILE (§12).
--checkout-token -no—Prompt for the deploy token on stdin (input hidden) and write it to the same path. Never pass the token as a flag value — it would leak via ps/shell history; the installer refuses.
--vault-pass-fileno—Path to an Ansible Vault password file — installed to /etc/cronomicon-runner/vault-pass (0640 root:cronomicon-runner) and wired via CRONOMICON_RUNNER_VAULT_PASSWORD_FILE (§8/§12).
--vault-pass -no—Prompt for the vault password on stdin (input hidden) and write it to the same path (same no-secret-as-value rule).

Secrets are never a flag value. The checkout deploy token and vault password are placed either from a file you staged (--checkout-token-file / --vault-pass-file) or typed at a hidden prompt (--checkout-token - / --vault-pass -) — the installer refuses a secret passed as a flag value, since it would be visible in ps and shell history. A hidden prompt needs a real terminal, so a piped curl … | sudo bash install must use the file form (the script's own stdin is the piped source). This makes a checkout+vault Ansible runner install in one invocation — no post-install file placement, no env merge.

What it does: creates the cronomicon-runner system user/group, 0750 /var/lib/cronomicon-runner (state) and /etc/cronomicon-runner (config), installs the binary to /usr/local/bin/cronomicon-runner (with --download, fetching and checksum-verifying it from the server first), installs whatever the optional flags supplied (known_hosts, keys, CA bundle, local inventory — with the ownership/modes in the table above), writes runner.env (mode 0640) wiring it all up, writes the hardened cronomicon-runner.service, then daemon-reload + enable + restart. It prints the runner name, capabilities, inventory mode, installed paths, and systemctl is-active when done.

SSH-capable runner? Pass the custody flags. With --known-hosts and --key-dir/--key-map/--generate-key, the runner is ready for its first SSH run when the script exits — nothing to hand-edit. The installer always creates /var/lib/cronomicon-runner/keys (0700) and wires CRONOMICON_RUNNER_KEY_DIR to it, so if you omit the key flags you need only drop a key file into that directory (named after its authKeyEnvVar) or re-run with --generate-key NAME — no runner.env edit for the key path. The agent still refuses SSH without known_hosts (§8), so the script prints a loud "before the first SSH run" block when --known-hosts was omitted, and known_hosts can be seeded then via scan & approve. See §4 for the full surface and §11 to verify.

If you'd rather wire systemd up by hand (or need a non-default layout), skip the script and follow §9 instead.


4 · Configure (the full surface)

Most installs use + Add Runner (§1); this table is for pre-authoring. For a fully-specified install — config management, containers, or a locked-down one-liner — the Runners view's Advanced: pre-authored install helper authors the artifacts for you: pick a profile (SSH task runner surfaces key/known_hosts custody; Ansible control node surfaces the checkout/vault secret-file inputs; Custom shows everything), then copy a complete annotated runner.env, the matching §3 one-liner, or the §10 docker run variant. Max jobs, sandbox caps, and checkout policy are not helper inputs; they are tuned on the runner's row after it registers (runner-manage.html §4.5). The table below remains the reference for hand-authoring and for the knobs the helper doesn't surface (poll cadence, tuning, scoped child env).

Config is resolved from three layers, low → high precedence: an optional JSON config file < CRONOMICON_RUNNER_* env vars < command-line flags. Pick whichever fits your deployment (env file for systemd/containers; flags for ad-hoc).

FlagEnvConfig-file keyDefaultMeaning
-serverCRONOMICON_RUNNER_SERVERserverUrl— (required)Cronomicon server base URL.
-ca-certCRONOMICON_RUNNER_CA_CERTcaCertPathsystem storePEM CA bundle to trust for TLS.
-registration-tokenCRONOMICON_RUNNER_REGISTRATION_TOKENregistrationToken—Shared crn_reg_* token (first run only).
-nameCRONOMICON_RUNNER_NAMEname— (required)Display name; stable across restarts.
-osCRONOMICON_RUNNER_OSosLinuxLinux or Windows.
-capabilitiesCRONOMICON_RUNNER_CAPABILITIEScapabilities— (auto-detect)Comma-separated run-types claimed. Unset ⇒ probed from the host's toolchains at startup; set to narrow.
-max-concurrentCRONOMICON_RUNNER_MAX_CONCURRENTmaxConcurrent5Max simultaneous runs.
-inventoryCRONOMICON_RUNNER_INVENTORYinventorycronomiconcronomicon or local (D8 — see §7).
-identity-fileCRONOMICON_RUNNER_IDENTITY_FILEidentityFilecronomicon-runner-identity.jsonWhere {id, apiKey} persists (§6).
-poll-intervalCRONOMICON_RUNNER_POLL_INTERVALpollInterval60sHeartbeat/poll cadence (D7).
-key-dirCRONOMICON_RUNNER_KEY_DIRkeyDir—Dir searched for keys by name (§8).
-key-mapCRONOMICON_RUNNER_KEY_MAPkeyMap—NAME=path,NAME2=path2 key map (§8).
-known-hostsCRONOMICON_RUNNER_KNOWN_HOSTSknownHostsFile—OpenSSH known_hosts for target host-key verification (§8).
-local-inventoryCRONOMICON_RUNNER_LOCAL_INVENTORYlocalInventoryFile—Local inventory file for local mode (§7).
-state-dirCRONOMICON_RUNNER_STATE_DIRstateDirOS temp dirWritable dir for transient run artifacts (falls back to systemd's STATE_DIRECTORY, then the OS temp dir).
-log-retry-budgetCRONOMICON_RUNNER_LOG_RETRY_BUDGETlogRetryBudget5Log-resume attempts before log_stream_lost.
-fan-outCRONOMICON_RUNNER_FAN_OUTfanOut4Max parallel SSH targets per run.
-configCRONOMICON_RUNNER_CONFIG——Path to a JSON config file (the lowest layer).

Declared vs local config. name, os, capabilities, maxConcurrent, and inventory are declared to the server at registration — changing them later does not propagate on a plain restart (the agent resumes its stored identity without re-registering). To push a change you must resync — one click (Runners → Resync). The rest (keys, known_hosts, CA, poll cadence, fan-out, local inventory) are runner-local and take effect on the next run or restart. See Runner Config Guide "Resync" for the full rules.

A config file uses camelCase keys (unknown keys are a loud error):

{
  "serverUrl": "https://cronomicon.example.com",
  "name": "runner-dc1-01",
  "capabilities": ["bash", "perl", "powershell", "python"],
  "inventory": "cronomicon",
  "knownHostsFile": "/var/lib/cronomicon-runner/known_hosts",
  "identityFile": "/var/lib/cronomicon-runner/identity.json"
}

Run cronomicon-runner version to print the build version/commit.

Worked example A — SSH-onward bash/perl/powershell/python runner, inventory=cronomicon

A reachable-segment runner that executes bash/perl/powershell/python over SSH; the server resolves the targets and ships fully-resolved host references.

cronomicon-runner \
  -server https://cronomicon.example.com \
  -registration-token crn_reg_xxxxxxxxxxxx \
  -name runner-dc1-01 \
  -capabilities bash,perl,powershell,python \
  -inventory cronomicon \
  -key-dir /var/lib/cronomicon-runner/keys \
  -known-hosts /var/lib/cronomicon-runner/known_hosts \
  -identity-file /var/lib/cronomicon-runner/identity.json

The manifest carries each target's address/port/user/via plus an authKeyEnvVar name; the agent resolves that name to a local key in -key-dir (or -key-map/env) and dials — directly or through the via bastion.

Worked example B — network-isolated runner, inventory=local

A runner living inside a segment Cronomicon can't see (T-b). The manifest carries only the scope name; the agent expands it against its own inventory and uses its own keys.

cronomicon-runner \
  -server https://cronomicon.example.com \
  -registration-token crn_reg_xxxxxxxxxxxx \
  -name runner-isolated-seg-a \
  -capabilities bash,ansible \
  -inventory local \
  -local-inventory /etc/cronomicon-runner/inventory.json \
  -key-map PROD_KEY=/var/lib/cronomicon-runner/keys/prod.pem \
  -known-hosts /var/lib/cronomicon-runner/known_hosts \
  -identity-file /var/lib/cronomicon-runner/identity.json

inventory.json maps a scope name → host targets (host references only — the agent supplies the key by authKeyEnvVar name, never the bytes):

{
  "prod": [
    {
      "name": "app-1",
      "address": "10.20.0.11",
      "port": 22,
      "user": "deploy",
      "authKeyEnvVar": "PROD_KEY",
      "via": "bastion.seg-a.local:22"
    }
  ]
}

An unknown scope is an error (not a silent empty), mirroring the server's "unmatched host is a failure, not a skip" rule.


5 · TLS / CA trust

The agent connects to the server over HTTPS. If the server presents a certificate signed by a private/internal CA, point the agent at the CA bundle with -ca-cert / CRONOMICON_RUNNER_CA_CERT (a PEM file). With a publicly-trusted cert, leave it unset to use the system trust store. (In containers, the slim/fat images ship ca-certificates for the public case.)


6 · Identity-file persistence (so restarts resume identity)

On first start the agent registers and writes {id, apiKey} to the identity file (0600, atomic write). On every subsequent start it resumes that identity instead of registering again — without this, each restart would mint a fresh runner row and orphan the old one.

Set this up correctly at install time:

(The day-2 side of this — re-registration after a server reap, and forcing a re-register to push config — lives in Runner Config Guide.)


7 · The two inventory modes (D8) — when to use each

Selected per-runner at registration via -inventory:

A local-mode runner requires -local-inventory; resolving a scope that isn't in the file is an error.

Because inventory is a declared field (set at registration), switching a runner between cronomicon and local requires a resync (one click) — see Runner Config Guide "Resync".


8 · Declaring capabilities, keys & host-key verification


9 · Manual systemd install

This is the long-form of what §3's runner-install.sh automates — use it when you need a custom layout or want to see each step.

# Build/copy the binary.
cd backend && CGO_ENABLED=0 go build -o cronomicon-runner ./cmd/cronomicon-runner
sudo install -m 0755 cronomicon-runner /usr/local/bin/

# Service account + dirs.
sudo useradd --system --home-dir /var/lib/cronomicon-runner \
             --shell /usr/sbin/nologin cronomicon-runner
sudo install -d -o cronomicon-runner -g cronomicon-runner -m 0750 /var/lib/cronomicon-runner
sudo install -d -m 0750 /etc/cronomicon-runner

# Config + unit (edit runner.env first — see cronomicon-runner.env.example).
sudo install -m 0640 deploy/cronomicon-runner.env.example /etc/cronomicon-runner/runner.env
sudo install -m 0644 deploy/cronomicon-runner.service /etc/systemd/system/

sudo systemctl daemon-reload
sudo systemctl enable --now cronomicon-runner
sudo systemctl status cronomicon-runner
journalctl -u cronomicon-runner -f

The unit runs the agent as a non-root, sandboxed service (NoNewPrivileges, ProtectSystem=strict, a single writable StateDirectory, a restricted syscall set). See cronomicon-runner.service for the full hardening set.


10 · Container install

# Persist identity + keys across restarts on a named volume.
docker volume create cronomicon-runner-data

docker run -d --name cronomicon-runner-01 --restart unless-stopped \
  -e CRONOMICON_RUNNER_SERVER=https://cronomicon.example.com \
  -e CRONOMICON_RUNNER_REGISTRATION_TOKEN=crn_reg_xxxxxxxxxxxx \
  -e CRONOMICON_RUNNER_NAME=runner-dc1-01 \
  -e CRONOMICON_RUNNER_CAPABILITIES=bash,perl,powershell,python \
  -e CRONOMICON_RUNNER_INVENTORY=cronomicon \
  -e CRONOMICON_RUNNER_KEY_DIR=/var/lib/cronomicon-runner/keys \
  -e CRONOMICON_RUNNER_KNOWN_HOSTS=/var/lib/cronomicon-runner/known_hosts \
  -v cronomicon-runner-data:/var/lib/cronomicon-runner \
  ghcr.io/resetsmith/cronomicon-runner:2.0.2

The explicit CRONOMICON_RUNNER_CAPABILITIES is optional here too — omit the line and the agent auto-detects the image's toolchains at startup (the image choice becomes the capability decision). Use the tag that matches your server's version (§2). For an ansible/terraform runner, swap in ghcr.io/resetsmith/cronomicon-runner-fat and either omit the var (detect) or set CRONOMICON_RUNNER_CAPABILITIES=ansible,terraform to narrow. Mount the runner's keys and (for local mode) its inventory.json into the volume / a bind mount. The images set CRONOMICON_RUNNER_IDENTITY_FILE=/var/lib/cronomicon-runner/identity.json by default, so the named volume gives you identity persistence for free.


11 · Verify the install

Confirm the runner came up and registered:

  1. Logs — journalctl -u cronomicon-runner -f (systemd) or docker logs -f cronomicon-runner-01 (container) should show registered with server on the first start (or resumed runner identity on a restart), then quiet poll activity on the cadence. No register: server returned … errors.
  2. Runners view — the runner appears online, showing its capabilities, load, version, and an inventory chip (cronomicon / local).
  3. Version — cronomicon-runner version prints the build (a plain local go build reports dev; release images stamp the real version/commit).
  4. (optional) Trigger a small bash run scoped to a host this runner serves and watch the log stream back, to confirm key custody + known_hosts are wired correctly.

If the runner doesn't appear, or shows offline, see Runner Config Guide "Check status & health".

The new-runner checklist — an agency is a provisioning domain

A registered, online runner is not yet a useful one. Cronomicon ships names, not values — key names, become-password names, lookup('env',…) names — so every name a run may reference must resolve on every runner that can claim that run. With a fleet of one that is trivially true. With five it drifts, and each gate that saves correctness presents as the same symptom: the run sits queued.

The supported answer is to treat an agency as a provisioning domain: a department's runners carry that department's key-dir, secrets and sidecar templates, and agency claim-gating guarantees a run only lands where its names resolve. The alternative — syncing every secret to every runner — reintroduces exactly the blast radius the names-only design exists to avoid.

StepSkip it and…
Enroll it in its agencies (Runners → the runner's row, or Scopes → Agencies → expand the agency)An untagged runner serves only the general pool. It cannot claim any departmental run, and the department's runs wait for a runner that never comes.
Flag it for Secret injection if its runs carry reference bindings or a become passwordBinding-bearing runs are invisible to an un-flagged runner. This is the step most often forgotten, because enrolling in the agency feels like the whole job.
Provision its department's key-dir / secrets.env (or the Vault Agent sidecar)The run claims, then fails on an unresolvable name — “env passthrough var(s) not set” or a key-resolution error.
Check its ansible-core version if the department uses job-level become passwords--become-password-file landed in 2.12. A runner below that does not advertise become-file, and those runs correctly wait rather than being handed to a runner that would ignore the flag.
Scan & approve the targets it will reachFirst contact fails soft with host_key_unverified per host.

The version pin is a routing fact, not a fleet ceiling. A runner held at ansible-core 2.16 to serve RHEL 8 targets (platform python 3.6; core 2.17 dropped 3.6 target support) does not cap the rest of the fleet. Keep scopes with RHEL 8 targets claimable by a 2.16-line runner and let newer runners serve newer targets — but note such a runner is below no threshold that matters here: 2.16 is comfortably past become-file's 2.12.

A run that no online runner can claim records why, so a mis-provisioned new runner shows up as a named reason on the waiting run rather than as an unexplained queue — see Administrator Manual §11.


12 · Ansible checkout projects & the Tier 2 sandbox (opt-in)

By default an ansible runner receives one self-contained playbook body per run. Checkout mode lets a runner execute a full playbook project — roles, vars_files, include_vars, .j2 templates — by fetching the playbooks repo at a server-pinned commit SHA and running the entry playbook from a materialized tree. It is opt-in per job (a kind: Script wrapper with project_root/entry) and per runner (the flags below). Body-only runs are unchanged; a runner without -allow-checkout refuses any checkout run.

Enable checkout + the deploy credential

Provision the toolchain (fat image)

Install ansible-core and your organization-standard collections on the runner host (the fat image carries ansible+Python; add collections in your image build or a provisioning step). The agent detects and advertises what's installed as capability tokens (ansible, collection:<fqcn>) so the server only lands a run on a runner that has its toolchain. A project's requirements.yml is installed per run into the ephemeral workdir; a collection already present at the exact pinned version is not re-downloaded (so a pre-provisioned runner runs offline). Point -galaxy-server at a private mirror/hub to keep installs off the public galaxy.ansible.com (the default).

Vault (opt-in)

A job that declares requires: [vault] needs -vault-password-file configured — a runner-local file the agent passes as --vault-password-file. The password never travels; a vault run reaching a runner without the file is refused loudly. Also register that password as an Cronomicon stored secret so the log redactor masks it — and note that values your playbook decrypts from vault at run time bypass redaction, so gate them with no_log: true (see runner-security.html §8).

Env scoping

A local-toolchain child does not inherit the runner's full environment. It sees an explicit base safe-list + the run's env + an allowlist of NAMES (from inventory lookup('env',…), target auth-key vars, and the job's env_passthrough). Add operator-needed names with -env-base-extra; drop SSH_AUTH_SOCK from the base with -exclude-ssh-auth-sock for runners that use per-host key files.

Tier 2 sandbox

Each local-toolchain run is wrapped in a per-run systemd-run --scope cgroup for resource caps — -sandbox-memory-max (e.g. 2G), -sandbox-cpu-quota (e.g. 150%), -sandbox-tasks-max (e.g. 512) — while the filesystem/syscall hardening is inherited from the runner's own systemd unit. -no-sandbox opts out. Availability is probed at startup: where no usable systemd manager exists, runs execute unsandboxed and are reported as such (a loud startup warning + a per-run provenance line when -allow-checkout is set). A sandbox-capable runner advertises the sandboxed token, so a job may requires: [sandboxed] to avoid unsandboxed hosts.

Checkout config surface

FlagEnvConfig-file keyDefaultMeaning
-allow-checkoutCRONOMICON_RUNNER_ALLOW_CHECKOUTallowCheckoutfalseOpt into runner-side project checkout.
-checkout-reposCRONOMICON_RUNNER_CHECKOUT_REPOScheckoutRepos—Comma-separated allowlist of repo clone URLs.
-checkout-token-fileCRONOMICON_RUNNER_CHECKOUT_TOKEN_FILEcheckoutTokenFile—File holding the read-only deploy token (preferred).
-checkout-tokenCRONOMICON_RUNNER_CHECKOUT_TOKENcheckoutToken—Deploy token literal (prefer the file form).
-mirror-dirCRONOMICON_RUNNER_MIRROR_DIRmirrorDir<state-dir>/mirrorsPersistent bare-mirror directory.
-galaxy-serverCRONOMICON_RUNNER_GALAXY_SERVERgalaxyServerpublic galaxyansible-galaxy source URL for per-run installs.
-vault-password-fileCRONOMICON_RUNNER_VAULT_PASSWORD_FILEvaultPasswordFile—Ansible Vault password file (advertises vault).
-env-base-extraCRONOMICON_RUNNER_ENV_BASE_EXTRAenvBaseExtra—Extra env-var NAMES forwarded to the scoped child env.
-exclude-ssh-auth-sockCRONOMICON_RUNNER_EXCLUDE_SSH_AUTH_SOCKexcludeSshAuthSockfalseDrop SSH_AUTH_SOCK from the base safe-list.
-no-sandboxCRONOMICON_RUNNER_NO_SANDBOXnoSandboxfalseDisable the per-run systemd-run scope wrapper.
-sandbox-memory-maxCRONOMICON_RUNNER_SANDBOX_MEMORY_MAXsandboxMemoryMax—Per-run cgroup MemoryMax (e.g. 2G).
-sandbox-cpu-quotaCRONOMICON_RUNNER_SANDBOX_CPU_QUOTAsandboxCpuQuota—Per-run cgroup CPUQuota (e.g. 150%).
-sandbox-tasks-maxCRONOMICON_RUNNER_SANDBOX_TASKS_MAXsandboxTasksMax—Per-run cgroup TasksMax (e.g. 512).

These capabilities are advertised at registration. -allow-checkout, the installed collections, -vault-password-file, and sandbox availability all become capability tokens the server claim-gates against. Changing them (or provisioning a new collection) only reaches the server on a resync (one click; installed collections are re-detected at resync time, while flag/probe-derived tokens need a restart first) — see Runner Config Guide "Resync". The Runners page shows each runner's detected toolchains (ansible-core version, collections, checkout/vault/sandbox).


See also: Runner Config Guide (day-2: manage, update, resync, remove), runner-security.html (credential-model posture & blast radius, checkout security model), cronomicon-runner.env.example (annotated env template), env-matrix.md (server-side vars).