Turning config files into templates, safely
JinjaTurtle started as a quick way to turn TOML and INI files into Jinja2 templates and Ansible defaults. It now handles seven config formats, generates loops from repeated structures, and neutralises template metacharacters to prevent injection from harvested config.
Why it exists
Templatising a config file by hand is tedious: parse the file, invent variable names, replace
each value with a {{ variable }} placeholder, and write a defaults file mapping those
variables back to the original values. It’s pure mechanical work, but it’s easy to make
mistakes - miss a value, mismatch a name, break the file structure.
It also takes a really long time. Specifically, I created JinjaTurtle after converting the CometBFT config.toml to Jinja2 and Ansible inventory, as part of making an Ansible role for it for one of my customers. I don’t want to have to do that again by hand!
JinjaTurtle automates the first pass. Point it at a config file, give it a role name, and it produces a Jinja2 template alongside an Ansible defaults YAML file. The original structure is preserved as much as possible, so the template looks like the source file with values swapped for variables.
Evolving the project
JinjaTurtle started with TOML and INI, with YAML and JSON added almost immediately. XML came next, using defusedxml for safe parsing.
I later refactored the format handlers into their own class, to make the codebase easier to extend by adding new handlers.
A significant step was the ability to generate Jinja2 loop constructs from repeated structures
in YAML, JSON, and XML. A list of homogeneous entries becomes a single template iterated with
{{ item.key }}, rather than a flat set of scalar variables. When the structure isn’t confident
enough to loop, it falls back to flattened scalars.
Bespoke format handlers have since been added for Postfix main.cf and systemd unit files
(*.service, *.socket, *.timer), and OpenSSH config support (ssh_config,
sshd_config, and detected snippet files).
Growing pains - type preservation and faithful output
A subtle early problem was type coercion. Naive string substitution would turn a JSON boolean
true into a Python True in the rendered output, producing invalid JSON. The renderers were
rewritten to use JSON-aware expressions and to preserve indentation, newlines, and operators like
< and > faithfully, rather than round-tripping through a serialiser that would reformat the
file.
The security model
JinjaTurtle is frequently pointed at config files harvested from real systems - exactly the use case that Enroll enables. As I developed Enroll and started having to think about the security risks with that project, there was a lot of boundary cross-over between Enroll and JinjaTurtle in terms of dealing with data. Some of that content may be influenced by an untrusted party: a hostname, a login banner, a GECOS comment. JinjaTurtle needs to guarantee that source content could not become executable template code.
Two guarantees have been established:
- Values are data, never code. Every config value is replaced with a
{{ variable }}placeholder, and the original value is stored in the defaults file. When the template renders, the placeholder prints the value as a literal string - Jinja2 doesn’t recursively render the contents of a variable, so a payload inside a value is inert. - Verbatim text is neutralised. Comments, blank lines, headers, and unrecognised lines are
copied into the template, but any Jinja2 metacharacters (
{{ }},{% %},{# #}) in that text are escaped so they render as literal characters.
Later hardening - done in lockstep with Enroll’s own security work - has added !unsafe tagging for
Ansible variables, safe XML parsing in API use, symlink avoidance, and defences against embedded
Jinja in original files during conversion.
Where it stands today
JinjaTurtle is a small, focused tool. It does one thing - turn config files into templates and variable data - and it does it for seven formats with loop generation, type preservation, and a security model that assumes the input might be hostile.
It pairs naturally with Enroll, which can detect structured configs and hand off to JinjaTurtle automatically - but it can equally be used as a standalone tool just fine.
The source is at git.mig5.net/mig5/jinjaturtle .
- Language: Python
- Formats: TOML, YAML, JSON, INI, XML, Postfix, systemd, SSH
- Output: Jinja2 templates + Ansible defaults
- Pairs with: Enroll