run.bash can provision a Fedora Server or Fedora Cloud box end-to-end with
zero interactive prompts, driven entirely by RUN_BASH_* environment variables.
This is the path for IaC / cloud-init / CI provisioning where no human is at the
keyboard.
It is the same run.bash used interactively — headless mode simply supplies every
answer up front from the environment, runs on the box it provisions
(connection: local), self-updates the repo to the branch-latest source, and lets the
Ansible layer auto-detect the server profile (skipping all GNOME/desktop plays,
per Plan 00061). Fedora Cloud resolves to the server profile too — no separate scope.
The authoritative, always-current contract is built into the script:
./run.bash --help-run-headlessThis document is the narrative companion; if the two ever disagree, the
--helpoutput wins.
Headless is ON when any of these hold:
--headlessflag is passed, orRUN_BASH_HEADLESS=1is set; or- stdin is not a TTY and at least one
RUN_BASH_*config var is set.
Force it off with --interactive (useful for piped-stdin smoke tests).
-
Run as the non-root target user. cloud-init
runcmdis root; drop to the user (sudo -u <user> -i …). -
ALL-scoped sudo, held either of two ways — exactly one is required:NOPASSWD:ALL(the default cloud image user has it), or- ordinary password sudo plus
RUN_BASH_SUDO_PASSWORD_FILEpointing at a0600file holding this user's password.run.bashproves it authenticates during preflight, not at the firstdnf.
A command-scoped sudoers rule is not supported by either route: it passes the
sudo -k -n trueprobe and then fails ondnf. The probe cannot detect that — a known limitation, stated rather than implied. -
GitHub: a single account + scoped token, or
none. SetRUN_BASH_GITHUB_ACCOUNTSto a single GitHub username (then also provide a scoped token and SSH passphrase, see below) or tononeto provision with no GitHub identity at all — an HTTPS-only clone, no token or SSH key needed.noneis incompatible withRUN_BASH_CONFIG_SOURCE(non-none) andRUN_BASH_RESTORE_PROJECTS=1, since both need a GitHub identity to pull from. Unset fails fast either way.
Any missing or unsafe input aborts immediately with a big, specific error naming the exact fix — a headless run never blocks waiting on a prompt that can't be answered.
| Variable | Meaning | Required |
|---|---|---|
RUN_BASH_HEADLESS=1 |
Force headless mode. | — |
RUN_BASH_USER_EMAIL |
Git email. | Yes |
RUN_BASH_GITHUB_ACCOUNTS |
Single GitHub username (v1). alias:user also accepted. |
Yes |
RUN_BASH_USER_LOGIN |
System login. | No (current user) |
RUN_BASH_USER_NAME |
Full name. | No (= login) |
RUN_BASH_HOSTNAME |
Hostname to set when the box is still named fedora. |
No (leaves default) |
RUN_BASH_CONFIG_SOURCE |
hosts/<name>.yml to import from the private config repo, or none. |
No (none = fresh) |
RUN_BASH_PROVISIONING_PROFILE |
Force desktop/server. |
No (auto-detect) |
RUN_BASH_OPTIONAL_PLAYBOOKS |
Space/comma list of optional plays (play-foo.yml/foo), or none. The keyword server-recommended expands to a curated, generic dev/server bundle (manifest); combines with explicit plays. |
No (none) |
RUN_BASH_RESTORE_PROJECTS |
1 to restore projects from the config manifest. |
No (off) |
RUN_BASH_REBOOT |
1 to reboot at the end. |
No (off) |
Provide each secret as a path to a 0600 file (recommended) — the secret bytes
never enter the environment, process listings, or cloud-init user-data:
| Variable | Secret | Required |
|---|---|---|
RUN_BASH_VAULT_PASSWORD_FILE |
Ansible vault password | Yes, always — ansible.cfg needs a readable vault-pass.secret to start at all, whether or not anything ends up vault-encrypted |
RUN_BASH_GITHUB_TOKEN_FILE |
Scoped GitHub PAT | When RUN_BASH_GITHUB_ACCOUNTS is not none |
RUN_BASH_GITHUB_SSH_PASSPHRASE_FILE |
SSH key passphrase | When RUN_BASH_GITHUB_ACCOUNTS is not none |
RUN_BASH_SUDO_PASSWORD_FILE |
This user's sudo password | Only when the user lacks NOPASSWD:ALL |
Literal equivalents (RUN_BASH_GITHUB_TOKEN, RUN_BASH_GITHUB_SSH_PASSPHRASE,
RUN_BASH_VAULT_PASSWORD, RUN_BASH_SUDO_PASSWORD) are accepted but:
- Refused on a detected cloud box — cloud-init persists user-data in the metadata
service, world-readable indefinitely. Use the
*_FILEform there. - Warned loudly otherwise, and setting both a literal and its
*_FILEis an error.
GitHub token scope: the full vars/github-required-scopes.yml set plus
admin:public_key. The login SSH key stays passphrase-protected (it is loaded
non-interactively via ssh-agent + a transient SSH_ASKPASS helper), which is why
the passphrase file is required.
Run as the non-root target user, branch-latest repo, full setup:
RUN_BASH_HEADLESS=1 \
RUN_BASH_USER_EMAIL=name@example.com \
RUN_BASH_GITHUB_ACCOUNTS=<gh-username> \
RUN_BASH_GITHUB_TOKEN_FILE=/run/secrets/gh-token \
RUN_BASH_GITHUB_SSH_PASSPHRASE_FILE=/run/secrets/ssh-pass \
RUN_BASH_VAULT_PASSWORD_FILE=/run/secrets/vault-pass \
./run.bash/run/secrets is tmpfs (RAM-backed, wiped on reboot) — a good home for the 0600
secret files.
write_files embeds its content inside user-data, which the metadata service
serves forever — so never put secret bytes there. Fetch them out-of-band inside
runcmd, immediately above the run.bash line:
runcmd:
- [ sh, -c, 'aws secretsmanager get-secret-value --secret-id vault
--query SecretString --output text > /run/secrets/vault-pass' ]
- [ sh, -c, 'aws secretsmanager get-secret-value --secret-id gh-token
--query SecretString --output text > /run/secrets/gh-token' ]
- [ sh, -c, 'sudo -u <user> -i env RUN_BASH_HEADLESS=1
RUN_BASH_USER_EMAIL=name@example.com RUN_BASH_GITHUB_ACCOUNTS=<gh-username>
RUN_BASH_GITHUB_TOKEN_FILE=/run/secrets/gh-token
RUN_BASH_VAULT_PASSWORD_FILE=/run/secrets/vault-pass
/home/<user>/run.bash' ]Replace <user> with the box's non-root distro user. When fetching run.bash itself,
pin it to a commit SHA (not HEAD) and inspect before running — do not pipe it
straight into a shell.
A headless run never hangs and never half-provisions silently:
- Any missing required value or unmet precondition aborts in preflight, before any provisioning action, with a message naming the exact fix.
- Any failure during provisioning (token rejected, SSH key load, vault mismatch, main playbook failure, a requested optional play missing or failing) aborts with a big red banner naming the step, the concrete reason, and a debug pointer — then exits non-zero. It never reports success on failure and never continues past a main-playbook failure.
./run.bash --helpand./run.bash --help-run-headless— the built-in, authoritative contract- Installation Guide — the interactive desktop install
- GitHub Multi-Account Setup — the account model referenced by
github_accounts