Architecture
lockbox is three files on top of Ubuntu: an sshd config, an entrypoint script and a Python wrapper. This page follows a container from start to a finished SSH command.
The image
The Dockerfile starts from ubuntu:24.04 and installs openssh-server, python3 and ca-certificates, nothing else. It then:
- Creates the
lockboxuser and group with UID and GID1000(theLOCKBOX_UIDandLOCKBOX_GIDbuild arguments), reusing an existing user or group with that ID if the base image has one. - Creates
/work(owned bylockbox, mode755),/etc/lockbox/entrypoint.d/and an empty/etc/lockbox/authorized_keys. - Copies
allowed.jsonto/etc/lockbox/allowed.json,sshd_configto/etc/ssh/sshd_config,lockbox-wrapperto/usr/local/bin/andentrypoint.shto/. - Generates SSH host keys with
ssh-keygen -A. - Exposes port 22, with
ENTRYPOINT ["/entrypoint.sh"]andCMD ["/usr/sbin/sshd", "-D", "-e"].
Container start
entrypoint.sh runs as root with set -e, in this order:
- If
LOCKBOX_USERis notlockboxand thelockboxgroup still exists, renames the group and user, moves the home directory to/home/<name>, and rewritesAllowUsers lockboxandMatch User lockboxinsshd_config. - Writes the username to
/etc/lockbox/user, which the wrapper reads for its usage message. - Changes the group’s GID and the user’s UID when
LOCKBOX_GIDorLOCKBOX_UIDdiffer from the current values. - If
/etc/lockbox/host_keysexists, copies the host keys from it, or into it when it holds none. - Unlocks the account so sshd accepts public key logins, falling back to a
*password whenpasswd -urefuses. - Chowns the home directory and
/work(not recursively) to the user. - Runs each executable
/etc/lockbox/entrypoint.d/*.shin name order. - Appends
envto/etc/environment. - Execs the command,
sshd -D -eby default, which logs to the container’s stderr.
Steps 1 and 3 check before they change anything, so a restarted container starts cleanly.
A command, end to end
ssh myapp@host "list-files --json"
-> sshd: public key checked against /etc/lockbox/authorized_keys
-> ForceCommand: /usr/local/bin/lockbox-wrapper
SSH_ORIGINAL_COMMAND="list-files --json"
-> wrapper: load /etc/environment and /etc/lockbox/allowed.json
-> shlex.split -> ["list-files", "--json"]
-> built-in? yes -> cmd_list_files(["--json"]) -> JSON on stdout, exit 0
no -> in allowed.json? yes -> execv(path, [name, args...])
no -> "Error: '<name>' not allowed", exit 1
stdin and stdout are the SSH channel, which is how put and get move file data. When the command is an allowed binary, the wrapper process becomes that binary, so its output, exit code and stdin go straight to the client.
Repository layout
| Path | What it is |
|---|---|
Dockerfile | The image |
sshd_config | The sshd configuration |
entrypoint.sh | Container start |
lockbox-wrapper | The forced command and the built-ins |
allowed.json | The base image’s empty allowlist |
create_installer.sh | The installer generator |
test.sh | The integration tests, see Development |