jexec, without running any agent, daemon, or Python interpreter inside the jail itself.
| jexec-scp.py | fix parameter handling for scp | |
| jexec-ssh.py | initial commit | |
| README.md | renaming | |
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:
- Run
sshdinside every jail — defeats half the reason to use jails. - Add Python + an agent-style runtime — defeats the other half.
- 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.pytakes cdist's command line, strips the ssh-style options that cdist prepends, SSHes to$JAIL_HOST, and runs the payload throughjexec -u <user> <jail> /bin/sh -c '...'. Itexecvps into the realsshso the exit code flows back to cdist unchanged.jexec-scp.pywalks cdist's scp-style argv looking for<jail>:/patharguments. 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 itexecvps into the systemscp.
Requirements
On the control machine:
- Python 3.6+ (for f-strings)
sshandscp(OpenSSH)- cdist
On the FreeBSD host:
- A reachable ssh account (either
rootor a user withdoas/sudoaccess tojlsandjexec) - 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_HOSTis 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
doassnippet 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