Repository navigation
Hand declared config strings to Ansible as data, never as templates (#174) - #179
Merged
Merged
Conversation
A component's service.command, healthCheck.command and release.command
reached the cloudfall_deploy role through the rendered inventory, and
Ansible templated them, so a value holding {{ lookup('pipe', ...) }} ran
on the management host during cloudfall health or deploy, and an honest
docker inspect --format {{.State.Health.Status}} failed to template.
inventory render and the observe overlay now mark every string unsafe
({"__ansible_unsafe": ...}) where they are written for Ansible, so every
playbook that reads them, the #164 command health check gate included,
gets the text as declared.
Fixes #174
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 30, 2026
Contributor
Author
|
@romamo Decision needed: the security fix for #174 changes how the inventory file that Every string in that JSON file becomes
Reply here with a number or your own answer; shipmill takes this up on the tick after your reply. Nothing on this PR merges until then. |
The engine reads the rendered inventory itself to stage the owner role passwords services.yml sets. With every string marked unsafe, serviceKind read as a mapping, so no owned database was found: no password was staged and a missing secret no longer stopped the run before Ansible (S-006-6, S-006-7). Read marked strings back as text with a shared object_hook, and build the test inventory the way inventory render writes it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
Author
|
Decision by @romamo, relayed by Claude Code
|
shipmill-romamo Bot
added a commit
that referenced
this pull request
Oct 31, 2026
#181) (#189) An approved operation's inputs reached ansible-playbook as plain --extra-vars JSON, and Ansible templates extra vars: an input holding {{ lookup('pipe', ...) }} ran on the management host with the operator's rights, and an honest input holding {{ broke the run. run_command now marks every input string unsafe with as_ansible_data, as #179 did for the rendered inventory; integers and booleans keep their types. The replay reads the argv back with object_hook=unmarked, so its key matches the unmarked inputs a recording keeps, from before this change and after. The decision record's inputs are unchanged. Fixes #181 Co-authored-by: shipmill-romamo[bot] <338557544+shipmill-romamo[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A component's
service.command,healthCheck.commandandrelease.commandreached thecloudfall_deployrole through the rendered inventory, and Ansible templated them:[/bin/true, "{{ lookup('pipe', 'touch /tmp/pwned') }}"]ran on the management host whilehealth.ymlvalidated the role's arguments, outside any gate. It also broke honest values such asdocker inspect --format {{.State.Health.Status}} app(a template syntax error).The fix sits at the hand-off, as agreed on the issue: every string Cloudfall writes for Ansible is marked unsafe in the JSON form Ansible's loader reads,
{"__ansible_unsafe": ...}.cloudfall.ansible_data.as_ansible_datamarks every string of a document unsafe (keys, numbers, booleans and null pass through; anything else is aTypeError)cloudfall-engine inventory rendermarks the whole rendered inventory, both the file it writes and the JSON it prints. That one file is what every engine playbook reads (deploy,health,restart,rollback,services,domains,data,backup, ...), so the marking also covers the Gate a deploy on the command health check and receipt it (S-009-2..9, 11, 12) #164 command health check gate once it landscloudfall observe's overlay (cloudfall_server_typeandcloudfall_server_id) is marked the same way, since a server type also holds free strings (a packageversion, for example)The audit of the other paths:
copy: src:artifact build; they never reach AnsibleResourceId,ReleaseId, declared database names,PostgresSchemaNamefrom Copy only the named schemas in data copy (#124) #134, which is left as it is) and SDK-made absolute paths. They are not marked in this PRBehaviour change: a declared value holding
{{ ... }}is now literal. The rendered inventory's strings now read as{"__ansible_unsafe": "..."}in its JSON and inansible-inventory --list.Plan
sdk/src/cloudfall/ansible_data.py(new),engine/src/cloudfall_engine/cli.py,sdk/src/cloudfall/observe.py,engine/tests/test_unsafe_config_values.py(new),engine/tests/test_ansible_inventory.py,engine/tests/test_examples_render_unchanged.pywith its golden inventory,sdk/tests/test_observe.py,changelog.d/174-no-templating-config-values.md{{ lookup('pipe', 'touch <marker>') }}value inhealthCheck.command,service.command,release.commandand a server type's package version, run throughansible-playbookas the playbooks hand it over), then mark every string of the rendered inventory and of the observe overlay unsafe where they are written for Ansible, then the tests that read the rendered JSON, then the changelog fragment{{.Status}}fails to template) and pass with the fix (no marker; the literal text reaches the unit file'sExecStart=, the release command's argv and the health check command); the golden example's role templates still render byte-identicalTest plan
engine/tests/test_unsafe_config_values.py(new, 5 tests). Each one renders the inventory throughcloudfall-engine inventory renderand runsansible-playbookagainst local hosts:health.ymlwith a command health check holding the payload: no markercloudfall_deploy_component: "{{ cloudfall_component_item }}"): arrives as declared, withdocker inspect --format {{.State.Health.Status}} appin itcomponent.service.j2:ExecStart=carries the payload and{{.Status}}literallyrelease_commandtasks with a stand-insystemd-run: the argv ends with the payload as writtencloudfall observeoverlay with a package version holding the payload: no marker, literal valueobserve.pyand failed with the marker createdtest_engine_cli_emits_ansible_json,test_ansible_merges_the_overlay_over_the_team_inventoryand the regenerated goldenansible-inventory.json. The golden test reads the values back as Ansible's loader does, so the nftables and PostgreSQL templates still render byte-identicaluv run pytest engine/tests: 237 passed, 1 skippeduv run pytest sdk/tests/test_observe.py sdk/tests/test_lifecycle.py sdk/tests/test_domains_apply.py sdk/tests/test_owned_postgresql_converge.py: passeduv run ruff check .,uv run mypyandansible-lint engine/ansible(with CI'sANSIBLE_CONFIG) are cleanansible_host/ansible_userwere exercised only through local connectionsFixes #174
🤖 Generated with Claude Code