1
0
Fork
You've already forked jexec-cdist
0
Two tiny Python wrappers that let https://www.cdi.st/ manage FreeBSD jails on a remote host via jexec, without running any agent, daemon, or Python interpreter inside the jail itself.
  • Python 100%
2026年04月12日 10:05:11 +02:00
jexec-scp.py fix parameter handling for scp 2026年04月12日 10:05:11 +02:00
jexec-ssh.py initial commit 2026年04月11日 21:52:07 +02:00
README.md renaming 2026年04月11日 21:54:10 +02:00

jexec-cdist

Two tiny Python wrappers that let cdist manage FreeBSD jails on a remote host via jexec, without running any agent, daemon, or Python interpreter inside the jail itself.

The control machine runs cdist. The host runs a normal ssh daemon and already has jls/jexec in base. The jails run... nothing extra. That is the entire point.

Why

cdist is already minimal: the target only needs POSIX sh, and the control machine speaks ssh. But cdist expects one ssh endpoint per target, and a FreeBSD jail is not normally reachable as its own ssh host. The usual workarounds are:

  1. Run sshd inside every jail — defeats half the reason to use jails.
  2. Add Python + an agent-style runtime — defeats the other half.
  3. Write a shim that tunnels cdist through the host.

This repo is option 3, in the smallest form I could come up with: two short scripts that plug into cdist's --remote-exec and --remote-copy hooks.

How it works

┌──────────────────┐ ┌──────────────────┐
│ cdist (Linux / │ ssh │ FreeBSD host │
│ *BSD / macOS) │──────── │ jls / jexec │
│ jexec-ssh.py │ scp │ │
│ jexec-scp.py │ │ │
└──────────────────┘ └───────┬──────────┘
 │
 ┌───────┴──────┐
 │ jail │ no ssh,
 │ (POSIX sh) │ no python,
 │ │ no agent.
 └──────────────┘
  • jexec-ssh.py takes cdist's command line, strips the ssh-style options that cdist prepends, SSHes to $JAIL_HOST, and runs the payload through jexec -u <user> <jail> /bin/sh -c '...'. It execvps into the real ssh so the exit code flows back to cdist unchanged.
  • jexec-scp.py walks cdist's scp-style argv looking for <jail>:/path arguments. For each one, it asks the host for the jail's filesystem root (jls -j <jail> path) and rewrites the path to a normal <host>:<jailroot>/path. Then it execvps into the system scp.

Requirements

On the control machine:

  • Python 3.6+ (for f-strings)
  • ssh and scp (OpenSSH)
  • cdist

On the FreeBSD host:

  • A reachable ssh account (either root or a user with doas/sudo access to jls and jexec)
  • Jails visible to jls

Inside the jails:

  • Nothing.

Install

git clone https://codeberg.org/Larvitz/jexec-cdist.git
cd jexec-cdist
chmod +x jexec-ssh.py jexec-scp.py

That is the whole installation. There is nothing to package, nothing to compile, and no virtualenv to create.

Use

export JAIL_HOST=root@radon.example.com
cdist config -c ./conf \
 --remote-exec ./jexec-ssh.py \
 --remote-copy ./jexec-scp.py \
 jail1 jail2 jail3

The positional arguments are jail names as they appear in jls on the host. cdist walks its usual workflow once per jail and applies whatever manifest you have in ./conf/manifest/init.

Environment variables

Variable Default Description
JAIL_HOST required ssh target that runs the jails (user@host)
JAIL_USER root user passed to jexec -u inside the jail

JAIL_HOST is the only thing you need to set; SSH options, identity files, jump hosts and so on belong in your ~/.ssh/config for the host, where they apply uniformly to both wrappers.

Example manifest

The conf/manifest/init in this repo is a one-liner that drops a sentinel file inside each jail, as a smoke test:

__file /tmp/cdist-wrapper-success --state present

After a successful run:

[root@radon ~]# jexec jail1 ls -l /tmp/cdist-wrapper-success
-rw------- 1 root wheel 0 Apr 11 21:02 /tmp/cdist-wrapper-success
[root@radon ~]# jexec jail2 ls -l /tmp/cdist-wrapper-success
-rw------- 1 root wheel 0 Apr 11 21:18 /tmp/cdist-wrapper-success

Replace the one-liner with any other cdist types (__package_pkgng, __file, __line, __service, etc.) to drive real configuration.

Running as a non-root user on the host

If you would rather not let cdist ssh to the host as root, create an unprivileged account and grant it jls/jexec through doas:

# /usr/local/etc/doas.conf
permit nopass cdist as root cmd jls
permit nopass cdist as root cmd jexec

Then wrap the two commands with a tiny shim (or open an issue — a proper JAIL_DOAS=1 env var is on the roadmap).

Performance Tip: SSH Multiplexing

Because these wrappers initiate a new ssh connection for every jls, scp and jexec command cdist wants to run, a single cdist config invocation can stack up a lot of TCP handshakes. Over a high-latency link a trivial run can take 15 seconds or more, almost all of it spent re-authenticating.

Enable ssh multiplexing on the control machine and the same run drops to near-instant. Add something like this to your ~/.ssh/config:

Host radon.example.com
 ControlMaster auto
 ControlPath ~/.ssh/sockets/%r@%h-%p
 ControlPersist 10m

Make sure ~/.ssh/sockets exists (mkdir -p ~/.ssh/sockets). OpenSSH opens one master connection on the first call, keeps it alive for ten minutes after the last use, and every subsequent ssh/scp from the wrappers rides that existing channel with zero connection overhead.

Limitations

  • One jail host per cdist invocation. JAIL_HOST is process-wide. If you need to manage jails on several hosts in a single logical run, call cdist once per host. Failure isolation usually makes this the right call anyway.
  • Filesystem path assumption. The scp wrapper reaches into the jail by writing to its root on the host. Jails that use overlay mounts, nullfs unions or similar non-trivial setups may need the path-rewriting rule adjusted.
  • No built-in privilege escalation on the host. See the doas snippet above; anything more elaborate is not yet wired in.

License

Released under CC0-1.0 (public domain dedication). Do anything you like with it; there is no warranty and no expectation of support.

See also

  • cdist — the configuration management system these scripts plug into
  • ansible_jailexec — the same idea, but for Ansible users
  • jexec(8), jls(8) — the FreeBSD primitives doing the actual work