Sign inSign up

psyb0t/lockbox

By psyb0t

•Updated 3 months ago

Image
0

4.6K

psyb0t/lockbox repository overview

⁠docker-lockbox

CI version license Docker Pulls

Locked-down SSH container with sandboxed file operations. Use as a base image to build your own dedicated tool containers - just provide a list of allowed commands and install your binaries. No shell access, no injection bullshit.

Skip the HTTP API wrapper. Any CLI tool instantly becomes a secure, remotely accessible service over SSH - no REST endpoints, no request parsing, no serialization layer. Just ssh myapp@host "tool --flag arg" and you're done. Even better - route it through Cloudflare Tunnel or Tailscale and you've got secure remote access without exposing ports.

⁠Table of Contents

⁠What You Get

  • SSH key auth only - no passwords, no keyboard-interactive
  • File ops over SSH - put, get, list-files, remove-file, create-dir, remove-dir, remove-dir-recursive, move-file, copy-file, file-info, file-exists, file-hash, disk-usage, search-files, append-file - all locked to /work
  • No shell access - Python wrapper validates every command, no shell involved at any point
  • No injection - &&, ;, |, $() are just literal arguments. No shell means shell metacharacters are meaningless
  • No forwarding - TCP forwarding, tunneling, agent forwarding, X11 - all disabled
  • Extensible - downstream images add their own allowed commands via a JSON config file

⁠Security

  • No shell access - every command goes through a Python wrapper that parses with shlex.split() and executes via os.execv() - no shell involved at any point
  • Whitelist only - if the command isn't in the allowed list, it doesn't run
  • No injection - &&, ;, |, $() become literal arguments to the binary. No shell means shell metacharacters are meaningless
  • SSH key auth only - passwords disabled, keyboard-interactive disabled
  • No forwarding - TCP forwarding, tunneling, agent forwarding, X11 - all disabled
  • Path sandboxing - all file operations resolve and validate paths stay within /work

⁠Quick Start

⁠As a Base Image (Primary Use Case)

Create allowed.json with your commands:

{
  "ffmpeg": "/usr/bin/ffmpeg",
  "ffprobe": "/usr/bin/ffprobe"
}

Create your Dockerfile:

FROM psyb0t/lockbox

ENV LOCKBOX_USER=myapp

RUN apt-get update && \
    apt-get install -y --no-install-recommends ffmpeg && \
    apt-get clean && rm -rf /var/lib/apt/lists/*

COPY allowed.json /etc/lockbox/allowed.json

Build and run:

docker build -t myapp .
cat ~/.ssh/id_rsa.pub > authorized_keys
mkdir -p work host_keys

docker run -d \
  --name myapp \
  --restart unless-stopped \
  -p 2222:22 \
  -e "LOCKBOX_UID=$(id -u)" \
  -e "LOCKBOX_GID=$(id -g)" \
  -v $(pwd)/authorized_keys:/etc/lockbox/authorized_keys:ro \
  -v $(pwd)/host_keys:/etc/lockbox/host_keys \
  -v $(pwd)/work:/work \
  myapp

ssh -p 2222 myapp@localhost "ffmpeg -version"

⁠File Operations

All paths are relative to /work. You can't escape it - traversal attempts get blocked, absolute paths get remapped under /work.

CommandDescription
list-filesList /work or a subdirectory (--json for JSON output)
putUpload file from stdin
getDownload file to stdout
append-fileAppend stdin to an existing file
remove-fileDelete a file (not directories)
create-dirCreate directory (recursive)
remove-dirRemove empty directory
remove-dir-recursiveRemove directory and everything in it recursively
move-fileMove/rename a file or directory within /work
copy-fileCopy a file within /work
file-infoJSON metadata for a path (size, mode, owner, etc)
file-existsPrint true or false
file-hashSHA-256 hex digest of a file
disk-usageTotal bytes used in /work or a subpath
search-filesGlob pattern search (recursive) within /work
⁠Examples
# Upload a file
ssh lockbox@host "put input.txt" < input.txt

# Download a file
ssh lockbox@host "get output.txt" > output.txt

# List files
ssh lockbox@host "list-files"
ssh lockbox@host "list-files subdir"

# List files as JSON
ssh lockbox@host "list-files --json"

# Create a directory
ssh lockbox@host "create-dir project1"

# Delete a file
ssh lockbox@host "remove-file input.txt"

# Delete an empty directory
ssh lockbox@host "remove-dir project1"

# Nuke a directory and everything in it
ssh lockbox@host "remove-dir-recursive project1"

# Move/rename a file
ssh lockbox@host "move-file old.txt new.txt"

# Copy a file
ssh lockbox@host "copy-file original.txt backup.txt"

# Get file metadata as JSON
ssh lockbox@host "file-info output.txt"

# Check if a file exists
ssh lockbox@host "file-exists output.txt"

# Get SHA-256 hash
ssh lockbox@host "file-hash output.txt"

# Check disk usage (bytes)
ssh lockbox@host "disk-usage"
ssh lockbox@host "disk-usage subdir"

# Search for files by glob pattern
ssh lockbox@host "search-files **/*.txt"

# Append to an existing file
echo "more data" | ssh lockbox@host "append-file output.txt"

⁠Configuration

⁠Allowed Commands (/etc/lockbox/allowed.json)

A JSON object mapping command names to binary paths. The base image ships with {} (no external commands allowed, only file operations). Downstream images override this:

{
  "mytool": "/usr/bin/mytool",
  "another": "/usr/local/bin/another"
}
⁠Entrypoint Extensions (/etc/lockbox/entrypoint.d/*.sh)

Drop executable .sh scripts in /etc/lockbox/entrypoint.d/ to run custom init logic before sshd starts. Scripts run in sorted order. Example - a font cache rebuild hook:

COPY --chmod=755 10-fontcache.sh /etc/lockbox/entrypoint.d/10-fontcache.sh
⁠SSH Username (LOCKBOX_USER env var)

Sets the SSH username for the container. The entrypoint renames the system user and updates sshd config at startup. Defaults to lockbox. Set it in your downstream Dockerfile:

ENV LOCKBOX_USER=myapp

Users then connect with ssh myapp@host instead of ssh lockbox@host.

⁠Environment Variables
VariableDefaultDescription
LOCKBOX_UID1000UID for the lockbox user (match host)
LOCKBOX_GID1000GID for the lockbox user (match host)
LOCKBOX_USERlockboxSSH username (renames system user at startup)
⁠Volumes
PathDescription
/workInput/output files - your workspace
/etc/lockbox/authorized_keysSSH public keys (mount read-only)
/etc/lockbox/host_keysSSH host keys (persists across container recreates)

⁠Installer Generator

If you're building a downstream image, create_installer.sh generates a complete install.sh for your project from a YAML config. Grab it and feed it your config:

curl -fsSL https://raw.githubusercontent.com/psyb0t/docker-lockbox/main/create_installer.sh | bash -s installer.yml > install.sh

Example config:

name: myapp
image: psyb0t/myapp
repo: psyb0t/docker-myapp

volumes:
  - flag: -d
    env: DATA_DIR
    mount: /data:ro
    default: ./data
    description: Data directory

environment:
  - flag: -w
    env: WORKERS
    container_env: APP_WORKERS
    default: 4
    description: Number of workers
  - flag: --log-level
    env: LOG_LEVEL
    container_env: APP_LOG_LEVEL
    default: info
    description: Log level (debug, info, warn, error)
FieldDescription
nameCLI command name, home dir name (~/.myapp/), compose service name
imageDocker image to pull
repoGitHub repo for curl | bash upgrades
volumesExtra volumes with CLI flags for runtime configuration
environmentEnvironment variables passed into the container

Each volumes entry:

KeyDescription
flagCLI flag for the start command (e.g. -d)
envVariable name in .env file, prefixed with uppercase app name (e.g. MYAPP_DATA_DIR)
mountContainer mount path, with optional :ro suffix
defaultDefault host path relative to app home
descriptionHelp text shown in CLI usage

Each environment entry:

KeyDescription
flagCLI flag for the start command (e.g. -w, --log-level)
envVariable name in .env file, prefixed with uppercase app name (e.g. MYAPP_WORKERS)
container_envActual env var name inside the container (e.g. APP_WORKERS)
defaultDefault value
descriptionHelp text shown in CLI usage
⁠What Gets Generated

The generated install.sh gives users a one-liner install (curl | sudo bash) that sets up ~/.myapp/ with docker-compose, authorized_keys, host_keys, work dir, and a CLI wrapper with start, stop, upgrade, uninstall, status, and logs commands. Resource limits (--cpus, --memory, --swap) are always available - defaults to unlimited if not specified.

Using the example config above, the generated .env:

MYAPP_PORT=2222
MYAPP_DATA_DIR=$MYAPP_HOME/data
MYAPP_WORKERS=4
MYAPP_LOG_LEVEL=info
MYAPP_PROCESSING_UNIT=cpu
MYAPP_GPUS=all
MYAPP_CPUS=0
MYAPP_MEMORY=0
MYAPP_SWAP=0

The generated docker-compose.yml:

services:
  myapp:
    image: psyb0t/myapp
    ports:
      - "${MYAPP_PORT:-2222}:22"
    environment:
      - LOCKBOX_UID=${REAL_UID}
      - LOCKBOX_GID=${REAL_GID}
      - APP_WORKERS=${MYAPP_WORKERS:-4}
      - APP_LOG_LEVEL=${MYAPP_LOG_LEVEL:-info}
      - PROCESSING_UNIT=${MYAPP_PROCESSING_UNIT:-cpu}
    volumes:
      - ./authorized_keys:/etc/lockbox/authorized_keys:ro
      - ./host_keys:/etc/lockbox/host_keys
      - ./work:/work
      - ${MYAPP_DATA_DIR:-./data}:/data:ro
    cpus: ${MYAPP_CPUS:-0}
    mem_limit: ${MYAPP_MEMORY:-0}
    memswap_limit: ${MYAPP_MEMSWAP:-0}
    restart: unless-stopped

Every generated installer includes GPU/processing unit support. Two compose overlays are generated alongside the base docker-compose.yml. The CLI wrapper's compose() function automatically merges the right one based on --processing-unit:

  • --processing-unit cuda → merges docker-compose.cuda.yml (nvidia driver, NVIDIA_VISIBLE_DEVICES)
  • --processing-unit rocm → merges docker-compose.rocm.yml (AMD /dev/kfd, /dev/dri, HIP_VISIBLE_DEVICES)
  • --processing-unit cpu (default) → no overlay

The --gpus flag controls which GPUs are exposed — maps to the vendor-specific env var in each overlay.

The generated CLI wrapper:

myapp start -d                                          # start detached (cpu mode)
myapp start -d --processing-unit cuda --gpus 0          # NVIDIA GPU 0
myapp start -d --processing-unit cuda --gpus all        # all NVIDIA GPUs
myapp start -d --processing-unit rocm --gpus 0          # AMD GPU 0
myapp start -d -w 8 --log-level debug                   # 8 workers, debug logging
myapp start -d --port 3333 --cpus 4 --memory 8g         # custom port, resource limits
myapp stop                                              # stop
myapp status                                            # show container status
myapp logs -f                                           # follow logs
myapp upgrade                                           # pull latest image and re-install
myapp uninstall                                         # stop and remove everything

⁠Building

make build
make test    # builds test image and runs integration tests

⁠Built on Lockbox

ImageDescription
psyb0t/mediaproc⁠FFmpeg, Sox, ImageMagick, 2200+ fonts - media processing over SSH
psyb0t/qwenspeak⁠Qwen3-TTS text-to-speech over SSH - preset voices, voice cloning, voice design

⁠License

This project is licensed under WTFPL⁠ - Do What The Fuck You Want To Public License.

Tag summary

Content type

Image

Digest

sha256:f1975a06a…

Size

44.7 MB

Last updated

3 months ago

docker pull psyb0t/lockbox