Try to prevent what could lead to execution of embedded jinja in original files when converting

This commit is contained in:
Miguel Jacq 2026-06-23 14:20:00 +10:00
parent f5de32b778
commit a5ca5f2974
Signed by: mig5
GPG key ID: 03906B4110AAD3B8
13 changed files with 640 additions and 101 deletions

View file

@ -115,9 +115,8 @@ ERB mode the file is intended to be Puppet Hiera data rather than Ansible role
defaults.
JinjaTurtle does **not** generate Puppet classes or `file` resources. A Puppet
module, or another tool such as Enroll, should still declare the class
parameters and call the template, for example with Puppet's `template()`
function.
module, should still declare the class parameters and call the template, for
example with Puppet's `template()` function.
## Using `--puppet-class`
@ -340,15 +339,53 @@ JinjaTurtle also templates some common bespoke config formats:
For ambiguous extensions like `*.conf`, JinjaTurtle uses lightweight content
sniffing. You can always force a specific handler with `--format`.
## Relationship with Enroll
## Security model
JinjaTurtle can be used directly, but it is also useful as a helper for tools
that generate configuration-management code.
JinjaTurtle is frequently pointed at config files that were *harvested* from
real systems, where some content may be influenced by an untrusted party (a
hostname, a login banner, a `GECOS` comment, a "Managed by ..." note). It is
therefore designed so that source content cannot turn into executable template
code.
Two guarantees matter:
1. **Values are data, never code.** Every config *value* is replaced with a
`{{ variable }}` placeholder in the template, and the original value is stored
separately in the defaults/Hiera data. When the template is later rendered,
the placeholder prints the value as a literal string; Jinja2 does not
recursively render the *contents* of a variable, so a payload sitting inside
a value (for example `motd = {{ salt['cmd.run']('id') }}`) is inert.
2. **Verbatim text is neutralised.** To preserve formatting, JinjaTurtle copies
comments, blank lines, headers and any unrecognised lines from the source
into the template. Any template metacharacters in that copied text
(`{{ }}`, `{% %}`, `{# #}` for Jinja2; `<% %>` for ERB) are escaped so they
render as the literal characters the author wrote, rather than executing.
### Consumer responsibilities
The value guarantee above relies on the downstream renderer being single-pass,
which is the normal case:
- **Ansible**: rendering a template with `template:`/`ansible.builtin.template`
is single-pass. For defence in depth, treat the generated defaults as
untrusted input — Ansible already does not re-template variable *contents* by
default. If you build your own var structures from this data and pass them
through additional templating, mark untrusted values with the `!unsafe` tag so
they are never re-evaluated.
- **Salt**: use the generated file as a `file.managed` template
(`template: jinja`). Do **not** place JinjaTurtle output where Salt would
render it a second time as part of SLS/pillar rendering, which is a separate
Jinja pass and would re-evaluate any embedded expressions.
- **Puppet/ERB**: render with the standard `template()`/`epp()` flow.
In short: render JinjaTurtle output exactly once. Do not feed it back through
another templating pass.
**IMPORTANT**: Always review both the original config files, then the resulting
templates generated by JinjaTurtle, before integrating them into your config
management system!
Enroll can call JinjaTurtle when it is available on the `PATH`. In that setup,
Enroll decides where Puppet, Ansible, or Salt files belong, while JinjaTurtle
concentrates on converting harvested config files into templates plus variable
data.
## Found a bug, have a suggestion?