Cronomicon · Bash Guide
📗 Usage 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 Bash guide — running shell scripts with Cronomicon Version B1.0

How to run Bash (and the rest of the shell family — perl, powershell, python) well inside Cronomicon: which of the two executors actually runs your script, how targets, bastions and host keys resolve, how env, run inputs and step outputs flow through a run, how secrets reach a script without ever touching the repo, and the failure modes worth knowing by name. It assumes you can write a shell script; what it teaches is the Cronomicon-specific shape around it.

Companion reading: the User Manual for the per-screen reference (Jobs §6, Scripts §8), the Administrator Manual for the SSH executor's internals (§4) and GitOps (§5), the Ansible Guide for playbooks, the PowerShell Guide for Windows fleets, the Python Guide for Python specifics, and the Runner Install Guide / Runner Config Guide / Runner Security Guide guides for the agent.

The one-sentence model. A shell script is a Script (run type bash), a Scope names the hosts it may touch, a Job binds the two to schedules and env — and the run executes over SSH, dialed either by the Cronomicon server itself (the in-app SSH executor) or by a runner sitting inside the target's network segment.


1 · Two executors, one script

Unlike Ansible (runner-only), a bash run can execute on either executor. Shell-family scripts default to ssh via the resolution chain (per-trigger override → job executor → global default → run-type default); both executors share the same lifecycle, logging, redaction, kill mechanism and concurrency cap — only where the SSH connection originates differs.

ssh — in-app SSH executorrunner — agent in the segment
Who dials the targetThe Cronomicon server (direct or via bastion)A registered cronomicon-runner host
ReachesAnything the server (or its bastions) can route toNetwork-isolated segments; only outbound HTTP to the server
Keys liveOn the server (named secrets / stored credentials)On the runner (key-dir, names-only manifests)
EnableOff by default — CRONOMICON_SSH_EXECUTOR_ENABLED=true (pool concurrency CRONOMICON_SSH_EXECUTOR_CONCURRENCY, default 4)Install a runner; the slim image / any host with a shell claims bash automatically

The classic first-day symptom: shell jobs queue forever. Shell runs default to the SSH executor type, but the executor pool is disabled until an operator turns it on. If CRONOMICON_SSH_EXECUTOR_ENABLED is unset, every ssh-tagged run sits queued indefinitely. Enable it — the server then logs a loud warning that it holds SSH keys and has outbound SSH — or route the job to a runner instead.

The life of an SSH run (Administrator Manual §4.2): claim the oldest queued run → open the log and load the scope's redactor → resolve the command (inline command, inline script, or a sandbox-checked scriptPath from the Git clone) → resolve targets (a pinned target_host wins over scope fan-out) → apply timeout_seconds → fan out across the hosts, up to 4 in parallel, each host's output prefixed [hostname] when more than one is involved → aggregate: all ok → success; all failed → failure; mixed → warning (partial).


2 · Getting scripts in


3 · Targets: scopes, host records & bastions

Every scope supports bash — it is the “bash floor”; the set can never exclude it. A bash run's blast radius is then decided in this order:

Each target resolves through its host record (Settings → SSH Targets, or imported from a synced inventory): address, port, user, via (bastion), and its auth key. A scope member with no matching host record becomes a reported per-host failure — never a silent skip.


4 · Env, run inputs & outputs

Environment layering

A run's env assembles as job env ← schedule env ← per-run overrides (override wins per key), plus your bound references (§5) and the dispatcher-owned CRONOMICON_RUN_* context set — CRONOMICON_RUN_ID, _JOB, _SCOPE, _TYPE, _TRIGGERED_BY, _EXECUTOR and friends — handy for logging and branching inside a script. The CRONOMICON_* namespace is injector-owned: operator env may reference those names but never define one (422).

Run inputs, not read -p

There is no TTY on either executor — a read -p hangs until the timeout kills the run. Declare operator questions as run inputs (spec.prompts — name, label, required, default, options) and read the answers from the env; the Run dialog asks, records provenance, and applies them via the per-run env path. prompt_enforcement: block makes a missing required answer reject the run instead of warning. See User Manual §6.5.

Exit codes & partial results

Exit 0 is success; anything else fails that host. On a multi-host fan-out the per-host results aggregate — all ok → success, all failed → failure, mixed → warning (partial) — so let your script's exit code mean something, and reserve stdout for information (and markers, below).

Passing data to downstream workflow steps (A12)

A script hands values to later workflow steps by printing a marker line on stdout:

echo "::cronomicon-output name=VERSION::1.4.2"

A downstream step declares an input bound to that output (in the Workflow Editor or Git YAML) and receives it as an env var. Markers are captured from the raw output before redaction — and if a captured value carries an injected secret, the executor drops all captured outputs and fails the run (output_secret_leak) rather than let the secret propagate. Details: User Manual §7.5.


5 · Secrets in scripts

Declare the Secrets / Variables a job consumes as reference bindings (§11.6); they arrive in the run env as CRONOMICON_SECRET_<name> / CRONOMICON_VAR_<name>. On the in-app SSH executor the server resolves the bindings itself: resolution is fail-closed (a missing or out-of-scope binding fails the run without leaking why to the run log), injected values seed the run's redactor before any output is captured, and delivery to the remote command rides STDIN — never argv, so a secret never lands in /proc/<pid>/cmdline. On the runner path the same bindings require the per-runner Secret injection flag — un-flagged runners leave a binding-bearing run queued; see Ansible Guide §6.

Privilege escalation: sudo -S, and its one sharp edge

Passwordless sudo is the preferred arrangement, for shell runs as much as for Ansible. A NOPASSWD rule scoped to the exact commands a job needs is auditable on the target, survives a password rotation, and keeps the credential out of the run entirely. Everything below is the exception path — reach for it when a target's policy genuinely forbids passwordless escalation, not as the default shape of a job.

Bash has no become primitive: unlike an Ansible run, there is no flag Cronomicon can pass on your behalf, so escalation is whatever the script author writes. Declare the password as a reference binding and feed it to sudo -S, which reads from stdin:

set -euo pipefail
# CRONOMICON_SECRET_BECOME_PASSWORD arrives from a declared reference binding.
printf '%s\n' "$CRONOMICON_SECRET_BECOME_PASSWORD" | sudo -S -p '' systemctl restart myapp

⚠ The sharp edge: the process table on the target. sudo -S is safe because the value crosses a pipe. The moment you put it anywhere else on a command line — sudo -S <<< "$PASS" is fine, but echo "$PASS" | ... inside a bash -c string, or passing it as an argument to any helper — it lands in /proc/<pid>/cmdline on the target host, readable by any local user there for the life of the process. Cronomicon's redaction cannot reach that. Redaction masks injected values in the run log; it has no visibility into a remote process table. The same applies to writing the value to a file on the target without umask 077, and to set -x, which will happily echo the whole pipeline.

For Ansible runs none of this applies: a job names its become password with spec.become_password_secret, Cronomicon resolves it at dispatch and the agent hands it to ansible-playbook --become-password-file as a 0600 file that never enters the environment. See Ansible Guide §5.


6 · Best-practice checklist


7 · Troubleshooting

SymptomLikely cause → fix
Runs sit queued forever (executor ssh)The SSH executor pool is disabled. Set CRONOMICON_SSH_EXECUTOR_ENABLED=true — or give the job executor: runner.
Queued, “waiting for a bash-capable runner”Executor runner but no online runner claims bash — runner down, or a capabilityMask narrowed it. Check the Runners view.
Queued, status reason names a missing runner propertyA run that no online runner can claim records why: no runner is online, no online runner can run bash jobs, no online runner belongs to <agency>, no eligible runner is flagged for secret injection, or a named missing capability token. Read the reason before inspecting the fleet — it names the one gate that failed.
Queued: “only a runner with no agencies can claim it”The run has no scope, and an untagged run is claimable only by an untagged runner (the general pool is disjoint, not a fallback). Every online runner belongs to a department. Bind a scope to the run.
422 “this job consumes department-owned credentials”An unbound run (no scope on the job, none supplied at trigger) declaring a binding to a department-owned Secret/Variable/key. An unbound run carries an empty department set, so an owned row resolves for nobody. Supply a scope in the Run dialog. A binding to a global row is unaffected.
host key mismatch … possible MITMThe target's key changed (re-key or a real problem). Verify out-of-band; if legitimate, clear the stored host key to re-capture via TOFU.
cred_error on Test connectionWrong/passphrase-protected/mis-formatted key, or wrong user. The key must be an unencrypted private key.
conn_errorUnreachable address/port, a bastion hop failing, or a host-key mismatch — check the bastion's own key and reachability first.
422 scope_membershipA per-run host subset names a host outside the effective scope.
Per-host failure “no matching host record”A scope member has no SSH host record — add it in Settings → SSH Targets (or fix the inventory import).
unpinned_bastion_targetA secret-injecting run over a bastion to a target with no pinned host key. Probe the target (Test connection) or run once without secrets to capture it.
Run killed with cronomicon: job timed outtimeout_seconds is smaller than reality — or the script hung (interactive prompt, dead remote). Fix the hang first, then size the timeout.
Run failed, reason executor_lostThe server restarted mid-run and reconciled it at startup (plain Failed badge — distinct from a runner's amber Lost). Re-run.
Run failed, reason output_secret_leakA ::cronomicon-output value contained an injected secret; all outputs were dropped deliberately. Emit something derived instead of the secret itself.
Manual run rejected 409 / cron fire skippedThe global maxConcurrent cap is reached, or a Forbid policy suppressed the overlap (recorded once as a skipped run).

For SSH-executor internals, crash recovery and the stale-run reaper, see Administrator Manual §4; for anything on the agent side, the Runner Install Guide and Runner Config Guide guides.