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.
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.mdD1). Cronomicon never decrypts or transmits private-key bytes. The runner holds its own keys; the server only ships host references. Seerunner-security.htmlfor 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:
- Server URL — the reachable base URL of the Cronomicon server (
https://cronomicon.example.com). If it presents a private/internal CA certificate, also have that CA bundle (PEM) on hand (§5). - A registration token — single-use, one per install (§1).
- A capability decision — usually none: by default the agent auto-detects the host's toolchains at startup and claims the matching run-types; you only pass
-cto narrow the set. For a container deployment you still pick slim vs fat, since the image decides which toolchains exist to detect (§2). - For a systemd install:
rooton a Linux host (Debian/Ubuntu or RHEL-family) and ancronomicon-runnerbinary — either pre-built (§2) or fetched from the server itself with the installer's--downloadflag (§2/§3; container-deployed servers bundle the binaries). known_hostsfor the targets — optional pre-seeding. Host-key verification is strict (the agent refuses an unknown/changed key), but--known-hostsis not required up front: you can scan & approve a target's key from the Runners view when the first run fails verification (human-approved TOFU — see the Security Guide §8 and Runner Config Guide). Pre-seed a curatedknown_hostswith--known-hostsif you'd rather trust hosts ahead of time; either way, the runner's own keys still go in via--key-dir/--key-map.
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 Runnerbutton. 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 bashPaste 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 withbash: 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'spoll/registercalls 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… | Capabilities | Image | Why |
|---|---|---|---|
| Run shells over SSH (incl. bastion/ProxyJump), or reach a network-isolated segment (T-b) | bash,perl,powershell,python | slim: the static binary on a distroless base | The agent only needs an SSH client; it's a tiny static/distroless image. |
| Run ansible / terraform locally (T-a) | ansible,terraform | fat: the binary plus Ansible, Python and Terraform | These toolchains run on the executor; the fat image carries ansible+Python and terraform. |
Run full ansible project checkouts (roles, vars_files, .j2 templates) — opt-in | ansible + -allow-checkout | fat + provisioning | The 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.
| Image | Carries | Built 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:
- Download (container-deployed server): the server image bundles cross-compiled agent binaries (linux amd64/arm64) plus a
SHA256SUMSfile, served unauthenticated at/agents/cronomicon-runner-linux-{amd64,arm64}. The §3 installer's--downloadflag detects the host architecture, fetches the binary, and verifies its checksum before installing — so a host needs nothing butcurland reachability to the Cronomicon server. (A source-built/bare-metal server returns a clean 404 here — build instead, or pointCRONOMICON_AGENT_DIRat a directory holding the files.) - Build:
cd backend
CGO_ENABLED=0 go build -o cronomicon-runner ./cmd/cronomicon-runner3 · 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).
| Flag | Required | Default | Meaning |
|---|---|---|---|
-s, --server | yes | — | Cronomicon server URL (prepends https:// if no scheme). |
-t, --token | yes | — | Registration token (crn_reg_*). |
-n, --name | no | $(hostname) | Runner display name (keep it stable). |
-c, --capabilities | no | — (auto-detect) | Comma-separated run-types claimed. Omit ⇒ the agent auto-detects the host's toolchains at startup; set only to narrow. |
-b, --binary | no | ./cronomicon-runner, ./bin/…, backend/bin/… | Path to the pre-built binary. |
--download | no | off | Fetch 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-hosts | no | — | 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-dir | no | — | 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-map | no | — | 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-key | no | — | <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-cert | no | — | Private CA bundle (PEM) — installed to /etc/cronomicon-runner/ca.pem and wired via CRONOMICON_RUNNER_CA_CERT (§5). |
--inventory | no | cronomicon | cronomicon or local (§7); local requires --local-inventory. |
--local-inventory | no | — | Local inventory JSON (with --inventory local) — installed to /etc/cronomicon-runner/inventory.json and wired via CRONOMICON_RUNNER_LOCAL_INVENTORY (§7). |
--allow-checkout | no | off | Opt into runner-side pinned playbook checkout — writes CRONOMICON_RUNNER_ALLOW_CHECKOUT=true (§12). |
--checkout-repos | no | — | Comma-separated allowlist of repo clone URLs the runner may check out — writes CRONOMICON_RUNNER_CHECKOUT_REPOS (§12). |
--checkout-token-file | no | — | 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-file | no | — | 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 inpsand shell history. A hidden prompt needs a real terminal, so a pipedcurl … | sudo bashinstall 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-hostsand--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 wiresCRONOMICON_RUNNER_KEY_DIRto it, so if you omit the key flags you need only drop a key file into that directory (named after itsauthKeyEnvVar) or re-run with--generate-key NAME— norunner.envedit for the key path. The agent still refuses SSH withoutknown_hosts(§8), so the script prints a loud "before the first SSH run" block when--known-hostswas omitted, andknown_hostscan 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 annotatedrunner.env, the matching §3 one-liner, or the §10docker runvariant. 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).
| Flag | Env | Config-file key | Default | Meaning |
|---|---|---|---|---|
-server | CRONOMICON_RUNNER_SERVER | serverUrl | — (required) | Cronomicon server base URL. |
-ca-cert | CRONOMICON_RUNNER_CA_CERT | caCertPath | system store | PEM CA bundle to trust for TLS. |
-registration-token | CRONOMICON_RUNNER_REGISTRATION_TOKEN | registrationToken | — | Shared crn_reg_* token (first run only). |
-name | CRONOMICON_RUNNER_NAME | name | — (required) | Display name; stable across restarts. |
-os | CRONOMICON_RUNNER_OS | os | Linux | Linux or Windows. |
-capabilities | CRONOMICON_RUNNER_CAPABILITIES | capabilities | — (auto-detect) | Comma-separated run-types claimed. Unset ⇒ probed from the host's toolchains at startup; set to narrow. |
-max-concurrent | CRONOMICON_RUNNER_MAX_CONCURRENT | maxConcurrent | 5 | Max simultaneous runs. |
-inventory | CRONOMICON_RUNNER_INVENTORY | inventory | cronomicon | cronomicon or local (D8 — see §7). |
-identity-file | CRONOMICON_RUNNER_IDENTITY_FILE | identityFile | cronomicon-runner-identity.json | Where {id, apiKey} persists (§6). |
-poll-interval | CRONOMICON_RUNNER_POLL_INTERVAL | pollInterval | 60s | Heartbeat/poll cadence (D7). |
-key-dir | CRONOMICON_RUNNER_KEY_DIR | keyDir | — | Dir searched for keys by name (§8). |
-key-map | CRONOMICON_RUNNER_KEY_MAP | keyMap | — | NAME=path,NAME2=path2 key map (§8). |
-known-hosts | CRONOMICON_RUNNER_KNOWN_HOSTS | knownHostsFile | — | OpenSSH known_hosts for target host-key verification (§8). |
-local-inventory | CRONOMICON_RUNNER_LOCAL_INVENTORY | localInventoryFile | — | Local inventory file for local mode (§7). |
-state-dir | CRONOMICON_RUNNER_STATE_DIR | stateDir | OS temp dir | Writable dir for transient run artifacts (falls back to systemd's STATE_DIRECTORY, then the OS temp dir). |
-log-retry-budget | CRONOMICON_RUNNER_LOG_RETRY_BUDGET | logRetryBudget | 5 | Log-resume attempts before log_stream_lost. |
-fan-out | CRONOMICON_RUNNER_FAN_OUT | fanOut | 4 | Max parallel SSH targets per run. |
-config | CRONOMICON_RUNNER_CONFIG | — | — | Path to a JSON config file (the lowest layer). |
Declared vs local config.
name,os,capabilities,maxConcurrent, andinventoryare 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.jsonThe 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.jsoninventory.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:
- Put the identity file on durable storage: a named volume in containers (
/var/lib/cronomicon-runner), a real path under systemdStateDirectory. - Keep
-namestable across restarts. - A present-but-corrupt identity file is a hard error (the agent will not silently re-register and orphan the existing row) — restore or delete it deliberately.
(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:
cronomicon(default; suits T-a and reachable/semi-reachable targets): the server resolves the scope (scope_hosts → ssh_hosts) and the manifest carries fully-resolved target references. Use this when Cronomicon's inventory is the source of truth and the runner is just an execution location closer to the targets (or one carrying the ansible/terraform toolchain).local(suits T-b network-isolated segments): the manifest carries only the scope name; the agent resolves it against its own-local-inventory. Use this when the targets live where Cronomicon can't see them, so it shouldn't (and can't) hold their topology. Keeps the isolated segment's host list out of the control plane entirely.
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
- Capabilities (
-capabilities) are the run-types this runner will claim. Unset by default ⇒ auto-detected: the agent probes the host forbash,perl,pwsh,python3/python,ansible-playbook, andterraformat startup and claims what it finds (zero findings is a startup error). Set the flag/env explicitly only to narrow the claim set. A run for a type no online runner declares stays queued — waiting for a {type}-capable runner — rather than failing. - Keys (model b). The agent resolves a target's
authKeyEnvVarname to a local private key, in order: (1)-key-mapentryNAME=path; (2) an env var of that NAME holding a PEM; (3) a file<NAME>,<NAME>.pem, or<NAME>.keyin-key-dir. The server never sends key bytes. Key files are read at dial time, so rotating the file contents in place takes effect on the next run. - Host-key verification. Point
-known-hostsat an OpenSSHknown_hostsfile. The agent verifies the target host key against it and refuses to connect when no known_hosts is configured — there is no trust-on-first-use fall-open on the target hop. Populate known_hosts before first run (ssh-keyscanthe targets, or copy from a trusted source). The same applies to bastions reached viavia. The file is read at dial time, so adding a host to it does not need a restart.
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 -fThe 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.2The 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:
- Logs —
journalctl -u cronomicon-runner -f(systemd) ordocker logs -f cronomicon-runner-01(container) should showregistered with serveron the first start (orresumed runner identityon a restart), then quietpollactivity on the cadence. Noregister: server returned …errors. - Runners view — the runner appears online, showing its capabilities, load, version, and an inventory chip (
cronomicon/local). - Version —
cronomicon-runner versionprints the build (a plain localgo buildreportsdev; release images stamp the real version/commit). - (optional) Trigger a small
bashrun scoped to a host this runner serves and watch the log stream back, to confirm key custody +known_hostsare 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.
| Step | Skip 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 password | Binding-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 reach | First 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
-allow-checkoutopts the runner in. Without it, a checkout manifest is refused loudly.-checkout-reposis the allowlist of repo clone URLs this runner may check out. A checkout manifest whose repo is not in the list is refused — a compromised server cannot point the runner at an arbitrary repo. Empty ⇒ no repo allowed.-checkout-token-file(preferred) /-checkout-tokenholds a read-only GitLab deploy token (read_repositoryonly), provisioned likesecrets.envand never shipped by the server. A bare token uses theoauth2:<token>form; auser:tokenvalue is used verbatim (deploy-token form). Worst-case leak is read access to one repo from one runner — revoke it per-runner.-mirror-diris where persistent bare git mirrors live (default<state-dir>/mirrors). Mirrors are fetch-updated; each run gets a freshgit archivetree (no.git) torn down on exit.
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
| Flag | Env | Config-file key | Default | Meaning |
|---|---|---|---|---|
-allow-checkout | CRONOMICON_RUNNER_ALLOW_CHECKOUT | allowCheckout | false | Opt into runner-side project checkout. |
-checkout-repos | CRONOMICON_RUNNER_CHECKOUT_REPOS | checkoutRepos | — | Comma-separated allowlist of repo clone URLs. |
-checkout-token-file | CRONOMICON_RUNNER_CHECKOUT_TOKEN_FILE | checkoutTokenFile | — | File holding the read-only deploy token (preferred). |
-checkout-token | CRONOMICON_RUNNER_CHECKOUT_TOKEN | checkoutToken | — | Deploy token literal (prefer the file form). |
-mirror-dir | CRONOMICON_RUNNER_MIRROR_DIR | mirrorDir | <state-dir>/mirrors | Persistent bare-mirror directory. |
-galaxy-server | CRONOMICON_RUNNER_GALAXY_SERVER | galaxyServer | public galaxy | ansible-galaxy source URL for per-run installs. |
-vault-password-file | CRONOMICON_RUNNER_VAULT_PASSWORD_FILE | vaultPasswordFile | — | Ansible Vault password file (advertises vault). |
-env-base-extra | CRONOMICON_RUNNER_ENV_BASE_EXTRA | envBaseExtra | — | Extra env-var NAMES forwarded to the scoped child env. |
-exclude-ssh-auth-sock | CRONOMICON_RUNNER_EXCLUDE_SSH_AUTH_SOCK | excludeSshAuthSock | false | Drop SSH_AUTH_SOCK from the base safe-list. |
-no-sandbox | CRONOMICON_RUNNER_NO_SANDBOX | noSandbox | false | Disable the per-run systemd-run scope wrapper. |
-sandbox-memory-max | CRONOMICON_RUNNER_SANDBOX_MEMORY_MAX | sandboxMemoryMax | — | Per-run cgroup MemoryMax (e.g. 2G). |
-sandbox-cpu-quota | CRONOMICON_RUNNER_SANDBOX_CPU_QUOTA | sandboxCpuQuota | — | Per-run cgroup CPUQuota (e.g. 150%). |
-sandbox-tasks-max | CRONOMICON_RUNNER_SANDBOX_TASKS_MAX | sandboxTasksMax | — | 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).