The useful line for AI in systemd management is not the line between a correct unit and an incorrect unit. It is the line between a boundary that has already been reviewed and the parameters that may vary inside it. When a developer asks a free model to produce a full systemd service file, the revision surface grows from a small set of variables to every line in the file. That tradeoff can make sense when the model knows the target host; it rarely makes sense when the unit will run on a free server with unknown state. I accept the opposite premise: AI should not write systemd units. It should fill a template that a human has already audited.
The free model access and free server option from MonkeyCode make repeated parameter generation very cheap. Disclosure: This article was prepared as part of MonkeyCode's product outreach. That low cost is exactly where the model adds value: it can propose ten variations of a service name, state directory, restart policy, or environment file without touching the sandbox settings. It is not a reason to let the model choose the sandbox settings itself.
Why a complete unit is the wrong artifact
A complete systemd unit mixes two different decisions: what the service should do and which security boundaries should protect it. The first decision changes often and is partly mechanical. The second decision changes rarely and should be governed by policy. When a model writes the entire unit, it treats both decisions as equally available, so the generated file may contain NoNewPrivileges=true in one revision and omit it in the next. A human reviewer then has to re-derive the policy from a diff, which is slow and error-prone. If the policy lives in a versioned template, the model can only affect decisions that are safe to vary, not the boundary that contains them.
This also avoids the recursive review problem. If you have to inspect every line of a generated unit, the model has not saved you time. Direct unit generation can fail because of an invented directive, a distribution-specific order, or an undeclared path. A parameter-only output can fail because a parameter is missing, malformed, or points to a nonexistent path. That smaller set is much easier to check and reject automatically.
A reproducible template workflow
This workflow starts from a deliberately narrow base unit template, not from a blank prompt.
- Keep a base template in version control. This example uses simple placeholders and hardcodes protections that should not vary per service:
[Unit]
Description=$description
AssertPathExists=$config_path
[Service]
User=$service_user
Group=$service_user
ExecStart=$binary --config $config_path
WorkingDirectory=$state_dir
StateDirectory=$state_name
Restart=on-failure
NoNewPrivileges=true
ProtectSystem=full
- Ask the model for only the parameters, not the unit. Requiring JSON output makes the boundary explicit and makes validation easy before rendering:
{
"description": "Model Context Protocol worker",
"config_path": "/etc/mcp/config.yaml",
"service_user": "mcp",
"binary": "/usr/local/bin/mcp-worker",
"state_dir": "/var/lib/mcp",
"state_name": "mcp"
}
- Render the template with a small script and fail on missing keys instead of silently passing unknown values:
#!/usr/bin/env python3
import json
import sys
from string import Template
def render(template_path: str, params_path: str) -> str:
template = Template(open(template_path).read())
params = json.load(open(params_path))
return template.substitute(params) # raises KeyError for missing fields
if __name__ == "__main__":
sys.stdout.write(render(sys.argv[1], sys.argv[2]))
- Validate the host-side parameters before writing the unit. This is not a review of every line, only the small set the model was allowed to change:
test -x /usr/local/bin/mcp-worker && echo "binary ok"
id mcp >/dev/null 2>&1 && echo "user ok"
test -r /etc/mcp/config.yaml && echo "config readable"
- Render the final file and verify its syntax and security posture:
python3 render.py worker.service.tpl worker.params.json > worker.service
systemd-analyze verify ./worker.service
systemd-analyze security ./worker.service
The review surface shrinks from the entire generated unit to the rendered parameter values. A missing path, a nonexistent user, or an unreadable config file can be rejected before systemd ever sees the service file.
The opinion in practice
If you already maintain systemd templates in an Ansible role or a dotfiles repository, this is the same idea restated. If you do not, the first hour of work is not writing a perfect template; it is writing a deliberately narrow one and refusing prompts that ask for a full unit. The template should encode your default ProtectSystem, PrivateTmp, NoNewPrivileges, and restart policy. Every field left blank is a field you are willing to discuss in review. That is a much more honest position than reacting to a full generated file after the fact.
The difference in review cost is visible even for a single service.
| Item | Full unit generation | Template plus parameters |
|---|---|---|
| Review surface | every directive in the file | only rendered parameter values |
| Security policy | re-created per generation | fixed in versioned template |
| Invalid state failure | can hide in any directive | limited to binary, user, config, state_dir |
| Cost of a bad revision | re-audit whole unit | reject or correct one parameter |
Limitations
The template approach does not remove systemd expertise; it concentrates that expertise in one file that someone must maintain. When a new systemd option becomes relevant, or a service legitimately needs a different sandbox, the template must be updated deliberately. The model can still return a plausible but wrong parameter, such as a binary path that exists only in the model's training data or a user name that is not present on this host. Those problems are easier to catch than a full unit's problems, but they are not eliminated. A template also cannot verify dynamic behavior, such as whether the service actually works after startup, whether it writes outside the allowed directories at runtime, or whether the chosen restart policy is appropriate under load.
Developers who already have configuration management that generates units from structured variables should implement the same boundary there rather than running a standalone Python renderer. Developers who need to prototype a service with a very unusual permission model may find the template too restrictive and still need to write a unit by hand. The point is not that a template covers every case; the point is that it keeps the AI-model contribution small enough to inspect.
Bottom line
Treat the systemd template as the security policy and the model as a parameter proposer. If the template is versioned and checked, you can test ten generated parameter sets in the time it used to take to review one generated unit. The next time a model offers a complete service file, ask it to return the JSON that fits your template instead. Share one field you refuse to let AI touch, and why that boundary matters on an unmanaged server.
Top comments (0)