Install · Configuration · Help · Website
CaaL provides disposable SSH login environments using OCI containers. Users connect with a normal SSH client and get a fresh, isolated shell that is destroyed when the session ends. No VMs, no persistent state, and minimal host setup required.
CaaL is made of three binaries that work together:
caalsh– the login shell. Replaces the user's shell in/etc/passwd. Every SSH login goes through it.caald– a background daemon that tracks active sessions and allows sessions managment.caalctl– an admin CLI for inspecting and managing live sessions.
When a user logs in via SSH, caalsh:
- Reads
/etc/caal/caal.tomland verifies the user is configured and enabled - Checks with
caaldthat the session limit hasn't been reached - Clears the entire environment – no SSH vars or host state leaks into the container
- Creates a per-session overlay filesystem over the container's rootfs, backed by a loop-mounted ext4 image – writes go there, the base image stays untouched
- Execs crun to start the OCI container
- Registers the session with
caald - Optionally enforces a session timeout (kills the container after N seconds)
- On exit, tears down the overlay, wipes the loop image, and unregisters from
caald– the host is left exactly as it was
Each session is fully ephemeral: nothing persists between logins.
CaaL is useful for:
- disposable SSH lab environments
- student shells
- honeypots
- shared demo systems
- temporary contractor access
- isolated CI/debug environments
- SSH access without persistent host accounts
Warning
caalsh requires root privileges (setuid or direct root execution)
to perform mounts and launch containers.
- Linux host with overlay filesystem support (modern kernels)
crun– lightweight OCI container runtimeskopeo+umoci– for pulling and unpacking OCI imagese2fsprogs– providesmkfs.ext4for session disk creationgit– required by the one-liner installer- Root access for install and user setup
gcc,make
The setup script handles installing all of these automatically on supported distros (except git).
Note
max_sessions is bounded by your kernel's available loopback devices.
The default is 8 (/dev/loop0–loop7). You can raise it with max_loop=N
in your kernel parameters or via modprobe loop max_loop=N.
# one-liner (clones to /tmp, runs setup, then cleans up):
curl -sSL caal.douxx.tech/get | sudo bash
# or manually:
git clone https://github.com/douxxtech/caal
cd caal
sudo bash scripts/setup.shThe setup script will:
- Detect your package manager and install dependencies (Debian/Ubuntu, Fedora/RHEL, CentOS, Arch, openSUSE supported)
- Build and install
caalsh,caald, andcaalctlto/usr/local/bin/ - Register
caalshas a valid shell in/etc/shells - Install and optionally enable/start the
caaldsystemd service - Pull a default busybox bundle to
/opt/caal/bundles/default - Create a base config at
/etc/caal/caal.toml
caald is a small background daemon that keeps track of active sessions over a Unix socket (/run/caald.sock). caalsh registers each session with it on login and unregisters on logout.
Its main jobs are:
- Providing an accurate session count so
max_sessionsis enforced reliably - Giving
caalctla live view of running sessions
Managing the daemon:
systemctl status caald # check if it's running
systemctl start caald # start it
systemctl enable caald # enable on boot
journalctl -u caald # view logsNote
caalsh has a fallback for when caald isn't running, session count is less accurate but keeps logins working.
Running without caald is not recommended for production.
caalctl talks to caald to give you a live view of what's running and let you kill sessions remotely.
# List all active sessions
caalctl list
# Print the number of active sessions
caalctl count
# Kill a specific session by its container ID
caalctl kill caalsh-1234-1718000000
# Kill all sessions for a user
caalctl killuser bobExample output of caalctl list:
USERNAME CONTAINER ID PID STARTED
-------- ------------ --- -------
bob caalsh-1234-1718000000 1234 2026-01-15 14:23:01
alice caalsh-5678-1718000120 5678 2026-01-15 14:25:21
Note
caalctl requires caald to be running. If it can't connect, it will say so.
Use newcaal to set up a new user – it handles the system account, the config entry, and the SSH hardening in one shot:
# Interactive setup
sudo newcaal bob
# Or use all defaults (busybox bundle, no timeout, 1GB disk space, enabled)
sudo newcaal bob -ynewcaal will:
- Create the system user with
caalshas their login shell - Prompt you to set a password
- Write the user's entry in
caal.toml - Write a per-user
sshd_config.ddrop-in that disables TCP/agent/X11 forwarding and forcescaalsheven if the user somehow has a different shell
Once created, the user can log in immediately:
ssh bob@host
bob@host's password:
/ # id
uid=0(root) gid=0(root) groups=10(wheel)
/ # busybox | head -1
BusyBox v1.37.0 (2024-09-26 21:31:42 UTC) multi-call binary.Note
uid=0 here is container root, not host root. The container runs in an
isolated namespace – it has no privileges on the host.
sudo delcaal bob # interactive
sudo delcaal bob -y # skip confirmationThis removes the system user account, the [bob] entry from caal.toml, and the per-user sshd drop-in at /etc/ssh/sshd_config.d/caal-bob.conf. sshd is reloaded automatically.
Note
delcaal does not kill active sessions for the user. If bob is currently logged in,
his session will run until it ends naturally. Use caalctl killuser bob first
if you want to terminate active sessions immediately.
CaaL's config lives at /etc/caal/caal.toml. Every user that should be allowed in must have an entry – no entry means access denied, regardless of whether the system account exists.
# general config
max_sessions = 4 # max simultaneous sessions across all users
[bob]
bundle = "/opt/caal/bundles/default" # absolute path to the OCI bundle
timeout = 0 # session lifetime in seconds, 0 = unlimited
disk = 1024 # session disk size in MB
enabled = true # set to false to lock out without deletingNotes:
max_sessionsis capped by available loopback devices (see Requirements)bundlemust be an absolute pathtimeoutis enforced viaSIGKILL– the container is forcibly terminated when it expiresdisksets the session ext4 image size; space is reserved upfront viafallocate, so it counts against real disk immediately – not just when writtendiskcan't be 0 – it'll be replaced by the default (1024)- Setting
enabled = falseblocks new logins for that user without removing their account or config. It does not kill active sessions – usecaalctl killuser <user>for that
The default bundle uses busybox, but you can point any user at any OCI bundle. To create a bundle from a different image:
# Pull the image
skopeo copy docker://alpine:latest oci:/tmp/alpine-oci:latest
# Unpack it as an OCI bundle
umoci unpack --image /tmp/alpine-oci:latest /opt/caal/bundles/alpine
# Point a user at it in caal.toml
# bundle = "/opt/caal/bundles/alpine"The bundle directory must follow the OCI Runtime Bundle spec: a config.json and a rootfs/ directory.
Note
Umoci default bundles are very minimal. Before using a bundle in production, you likely want to:
- Fix
/etc/passwdand/etc/groupso the container has the users it expects - Set correct permissions on
/tmp,/var, and any runtime directories - Add timezone data if needed (
/etc/localtime,/usr/share/zoneinfo) - Configure resource limits (memory, CPU) in
config.jsonunderlinux.resources - Update other configurations such as hostname, namespaces, etc.
The OCI config is at /opt/caal/bundles/<image>/config.json.
By default, OCI bundles are usually configured to launch /bin/sh or /bin/bash. However, you can edit the bundle's config.json to make the container run a specific program as the "init" process. When that program exits, the container stops and the SSH session closes.
Here is an example on how to turn a CaaL login into a dedicated htop monitor:
- Open the
config.jsonfor your bundle:sudo nano /opt/caal/bundles/default/config.json - Locate the
processobject and modify theargsarray:
"process": {
"terminal": true,
"user": {
"uid": 0,
"gid": 0
},
"args": [
"/usr/bin/htop"
],
"env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"TERM=xterm-256color"
],
"cwd": "/"
}
Tip
Why do this?
- Restricted Access: The user can only interact with the specified application. They cannot "break out" to a shell to explore the container filesystem unless the app itself has a shell-escape feature.
- App-as-a-Service: You can provide disposable instances of interactive CLI tools (like
psql,redis-cli, or custom internal management scripts) over SSH. - Automatic Cleanup: Since CaaL tears down the environment when the process exits, quitting the application (e.g., pressing
F10inhtop) automatically closes the SSH connection and wipes the session disk.
This demo walks through building a developer-focused Fedora bundle, creating a CaaL user for it, and showing ephemeral sessions in action.
sudo skopeo copy docker://fedora:latest oci:/tmp/fedora-oci:latest
sudo umoci unpack --image /tmp/fedora-oci:latest /opt/caal/bundles/fedoraMount the required pseudo-filesystems, then enter the rootfs:
ROOTFS="/opt/caal/bundles/fedora/rootfs"
sudo mount --bind /dev "$ROOTFS/dev"
sudo mount --bind /proc "$ROOTFS/proc"
sudo mount --bind /sys "$ROOTFS/sys"
sudo mount --bind /run "$ROOTFS/run"
sudo chroot "$ROOTFS" /bin/bashTune DNF for faster installs:
tee /etc/dnf/dnf.conf > /dev/null <<'EOF'
[main]
gpgcheck=True
installonly_limit=3
clean_requirements_on_remove=True
best=True
skip_if_unavailable=True
fastestmirror=True
max_parallel_downloads=20
keepcache=True
deltarpm=False
defaultyes=True
EOFUpdate and install a small developer toolset:
dnf update
dnf install \
bash-completion btop curl nano fd-find fzf \
gcc gcc-c++ gh git htop iproute jq make \
neovim openssh-clients procps-ng python3 python3-pip \
ripgrep rsync strace tar tmux tree unzip \
util-linux vim wget which zipCustomize the shell environment (optional):
nano /root/.bashrcThen exit the chroot and unmount:
exit
sudo umount "$ROOTFS/dev"
sudo umount "$ROOTFS/proc"
sudo umount "$ROOTFS/sys"
sudo umount "$ROOTFS/run"Edit /opt/caal/bundles/fedora/config.json and apply these changes:
- Remove the network namespace entry from
linux.namespaces– this gives the container access to the host network (needed forgit,gh, etc.) - Set
hostnameto something likefedodev - Set
process.cwdto/root - Remove
CAP_NET_BIND_SERVICEfrom capabilities – users shouldn't bind privileged ports - Add
CAP_DAC_OVERRIDE– required so the container can write files as expected - Add
linux.resources.cpuandlinux.resources.memoryif you wish to apply cgroups limitations
sudo newcaal fedora
# bundle: /opt/caal/bundles/fedora
# timeout: 43200 (12 hours)
# disk: 4096 (4 GB)Log in and do some real work: authenticate with GitHub, clone a repo, make a commit, and push it:
Then log out and log back in. The session is fully ephemeral: no ~/.config/gh auth, no cloned repo, nothing persists:
Each session starts from the same clean image. The disk, any installed tools, cloned repos, and credentials are all wiped on logout. The next session gets a fresh environment identical to the first.
caalshclears the entire environment before launching the container – nothing from the SSH session leaks in- The overlay filesystem ensures the container rootfs is always clean – a user cannot permanently modify it
- The SSH drop-in written by
newcaaldisables agent forwarding, TCP forwarding, and X11 for the user - Users are created with
/tmpas their home directory – they have no persistent home on the host caalshmust be setuid root (or run as root) to perform mounts – the Makefile handles this
Have an issue running CaaL ? Feel free to open a new issue
CaaL is free software, distributed under the GPLv3.0.
Copyright (C) 2026 douxxtech