Skip to content

A component's command values are Ansible-templated on the management host #174

Closed

Description

@shipmill-romamo

Found in the review of #170.

The problem

A component's service.command and healthCheck.command reach the cloudfall_deploy role as Ansible variables, and Ansible templates them. A value holding {{ ... }} is evaluated on the management host while the role checks its arguments, before any task runs on a server.

Reproduce

Declare a component with service.command: [/bin/true, "{{ lookup('pipe', 'touch /tmp/pwned') }}"] (or the same in a type: command healthCheck.command), then run cloudfall health for it. /tmp/pwned appears on the management host. The reviewer reproduced it on health.yml for both fields.

Why it matters

Anyone who can change the project's config, an agent included, runs commands on the management host with the operator's rights, outside any gate (Q-2). It also breaks honest commands: a health check like docker inspect --format {{.State.Health.Status}} app can't be declared.

#134 fixed the same class of bug for data copy --source-schema names by refusing braces.

Proposed fix

Pass these values to Ansible so they are never templated (mark them unsafe in the rendered inventory or extra vars, for example with !unsafe), so a literal {{ reaches the unit file and the check as written. Refusing braces in the schema is the fallback if marking them unsafe can't hold on every path. Check every other config string that reaches Ansible the same way (environment values, build steps, release commands).

Related: #164 (the command health check's engine side), spec docs/specs/009-command-health-check.md.

Activity

  1. shipmill-romamo commented on Oct 30, 2026

    @shipmill-romamo
    ContributorAuthor

    Triage: implement. A security fix: a config value runs as a Jinja expression on the management host, outside any gate.

    Checked on main (745c129): nothing marks service.command or healthCheck.command unsafe before they reach the cloudfall_deploy role, and #134 closed the same class of bug only for --source-schema names.

    The plan:

    • Pass every config string that reaches Ansible as data, never as a template: mark it unsafe where the SDK hands it over (the rendered inventory, host vars or extra vars), so {{ reaches the unit file and the check as written. This keeps honest commands such as docker inspect --format {{.State.Health.Status}} app working, which refusing braces would not
    • Audit every path the same way: service.command, healthCheck.command, environment values, build steps, release commands, and any other declared string that reaches a role
    • A regression test per path that renders a value holding {{ lookup('pipe', 'touch <marker>') }} through ansible-playbook (or the role's templating) and proves the marker file is never created and the literal text reaches its target

    Behaviour change: a declared value holding {{ ... }} was templated before and reaches the server literally now. No doc or spec says these values are templated, so this restores the declared meaning. If marking a value unsafe can't hold on some path, that path is reported back rather than switched to refusing braces, since a refusal changes what users can declare.

    I'll link the PR here.

  2. shipmill-romamo commented on Oct 30, 2026

    @shipmill-romamo
    ContributorAuthor

    The fix is in #179: the inventory that cloudfall-engine inventory render writes, and the overlay cloudfall observe writes, mark every string as Ansible data ({"__ansible_unsafe": ...}), so a declared {{ ... }} reaches the server as written. Every engine playbook reads that inventory, so it covers the #164 command health check too. Five real ansible-playbook tests (health.yml, the health check command, the unit file, the release command tasks, the observe overlay) fail on main and pass with it.

    Trade-off: the rendered inventory's JSON now shows the wrapper on every string, which a tool outside Ansible reading that file would see. Environment values and build steps never pass through Jinja, so they needed nothing. One path is left: an approved catalog operation's inputs still reach the team's playbook as templated --extra-vars; that changes the team's playbook contract, so it's its own issue, #181. This issue closes when #179 merges.

  3. shipmill-romamo commented on Oct 30, 2026

    @shipmill-romamo
    ContributorAuthor

    Update: #179 passed review with one fix pushed (the engine reads the rendered inventory itself to stage owned-database passwords, and the marked strings had made it skip them; it now reads them back as text, with tests building the inventory as inventory render writes it). CI is green on 8a939f6. It waits on a maintainer decision posted on the PR: the rendered inventory file's new format.

  4. shipmill-romamo commented on Oct 31, 2026

    @shipmill-romamo
    ContributorAuthor

    Released in v0.8.0rc14 (pre-release).
    A plain install gets it with the next stable release.
    If you filed this: react 👍 on this comment once it works for you, or reply if it doesn't.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions