Without a container¶
How to run Evey directly on a host (systemd, an init script, or by hand) instead of the Docker image. The image is the recommended path: it ships the plugin sandbox, the hardening and the plugin runtimes already wired. Going bare metal means reproducing that posture yourself: the right binaries side by side, a dedicated non-root user, the sandbox backend your kernel supports, and owner-only secret directories. This page walks through it in order.
Build and install the binaries¶
Build the two Go binaries and install them in the same directory: the
plugin runner looks for the launcher in $EVEY_PLUGIN_LAUNCHER first, then
next to its own executable, then on $PATH.
# from the repository root
CGO_ENABLED=0 go build -ldflags="-s -w -X main.version=$(git describe --tags --always) -X main.commit=$(git rev-parse HEAD)" -o evey ./cmd/evey
CGO_ENABLED=0 go build -ldflags="-s -w" -o evey-plugin-launcher ./cmd/evey-plugin-launcher
sudo install -m 0755 evey evey-plugin-launcher /opt/evey/
# optional: shell completion and the manual page, both printed by the binary
/opt/evey/evey completion bash | sudo tee /etc/bash_completion.d/evey >/dev/null
/opt/evey/evey man | sudo tee /usr/local/share/man/man1/evey.1 >/dev/null
evey version prints the version, commit and build date without any
configuration.
The seccomp shim¶
Build libevey-sandbox.so (one C file, needs a C compiler) and install it
at the path the runner expects, /app/libevey-sandbox.so:
gcc -shared -fPIC -nostartfiles -o libevey-sandbox.so sandbox/sandbox.c
sudo install -D -m 0644 libevey-sandbox.so /app/libevey-sandbox.so
The path is fixed in the runner. Without the file, plugins refuse to start
(fail closed) unless the server runs in dev mode. The shim adds a seccomp
denylist for dynamically linked plugins, on top of Landlock or bubblewrap.
The image keeps it under /app with the SDK; a symlink works too.
Runtimes and services¶
Install whatever your plugins' entrypoint: invokes. The runner recognises
python3, python, node, php, ruby, perl, lua, bash and sh;
python3 covers most community plugins. Then the services the server talks
to:
| Dependency | Why |
|---|---|
| ffmpeg and ffprobe | transcoding, probing; the image also bundles VAAPI drivers and fpcalc (chromaprint) for intro detection |
PostgreSQL (server plus a database) and pg_dump |
the database; pg_dump writes the automatic pre-migration dump |
| Meilisearch | the search index; without it search falls back to SQL |
bwrap (bubblewrap) |
only for the bubblewrap sandbox backend |
setcap (libcap) |
to grant the launcher its file capability |
The embedded database and search engine are a feature of the image's
entrypoint, not of the binary: on bare metal you run both yourself and point
EVEY_DB_HOST and EVEY_MEILI_URL at them.
Run as a dedicated non-root user¶
Never run the server as root. Create a group and two unprivileged users, as the image does:
sudo groupadd --gid 1000 evey
sudo useradd --system --uid 1000 --gid evey --shell /usr/sbin/nologin evey
sudo useradd --system --uid 1001 --gid evey --shell /usr/sbin/nologin evey-plugin
evey(UID 1000) runs the server.evey-plugin(UID 1001) is the user plugins are dropped to when uid separation is active, so a compromised plugin lands as a different unprivileged user that cannot read the server's secrets by plain file permissions.
Warning
The group id matters: the launcher drops a plugin to UID 1001 and to the
server's own group, and refuses any uid or gid below 1000. A group
created by useradd --system alone gets a system id below 1000, and
plugins in basic mode are then refused.
The server marks itself non-dumpable at startup (PR_SET_DUMPABLE=0), so a
co-resident plugin cannot read /proc/<server-pid>/{mem,environ}.
Pick the sandbox backend for your kernel¶
Set EVEY_PLUGINS_SANDBOX_MODE. auto (the default) resolves in the order
landlock, bubblewrap, basic and picks the strongest the host supports. Set
a mode explicitly when you want fail-closed behaviour.
Landlock (recommended)¶
A per-path filesystem allowlist (a plugin sees its own directory and the socket directory, never your data or secrets) plus a Landlock v4 TCP egress filter. It needs no privilege, no mount and no namespace: the launcher applies it to itself, then executes the plugin.
Landlock must be compiled in and in the active LSM list. The runner probes
landlock_create_ruleset; the probe fails when the syscall is absent
(kernel older than 5.13) or when Landlock is not in the active list. Check:
If landlock is missing from that list, add it on the kernel command line
and reboot (keep the existing entries, the list varies per distribution):
Then either keep auto or pin it:
EVEY_PLUGINS_SANDBOX_MODE=landlock # fail closed: a plugin is refused when Landlock cannot be enforced
landlock passes -require-landlock to the launcher, which exits non-zero
rather than run a plugin unconfined.
bubblewrap¶
When Landlock is not available, install bwrap. It puts each plugin in
user, pid, uts, ipc and network namespaces with a mount allowlist. It needs
unprivileged user namespaces and the mount syscall, true on most bare-metal
Linux but blocked inside an unprivileged container, which is why Landlock is
the container default. auto only selects bubblewrap when Landlock is
absent and a throwaway bwrap probe succeeds.
EVEY_PLUGINS_SANDBOX_MODE=bubblewrap # namespaces
EVEY_PLUGINS_SANDBOX_MODE=strict # namespaces plus the seccomp shim
Check that the host permits the namespace and mount setup:
basic (last resort)¶
EVEY_PLUGINS_SANDBOX_MODE=basic is a uid drop plus the seccomp shim; the
launcher, when present, still installs its kernel seccomp denylist and
best-effort Landlock. There is no per-path filesystem confinement: a plugin
runs at UID 1001 with seccomp only. Use it on old or locked-down kernels
where neither Landlock nor bubblewrap works, and rely on the uid separation
below to protect the secrets.
A plugin granted the subprocess permission requires the launcher even in
basic mode: the shim does not survive an exec of a child process, only
the launcher's kernel seccomp does. Without the launcher such a plugin
refuses to start rather than run unconfined.
Keep secrets out of a plugin's reach¶
Uid separation runs each plugin under evey-plugin (UID 1001), distinct
from the server (UID 1000), so a plugin cannot read jwt.key, the backups
or the logs by plain file permissions. It matters in basic mode; under
Landlock or bubblewrap the filesystem is already restricted, so it is
redundant there.
EVEY_PLUGINS_UID_SEPARATION:
| Value | Behaviour |
|---|---|
auto (default) |
on only when the sandbox resolves to basic |
on |
always, as an extra independent layer |
off |
never |
The drop is done by the launcher, not the server. The server stays non-root with no capability; the launcher carries one file capability and switches to UID 1001 just before executing the plugin:
The launcher refuses any target uid or gid below 1000. When it cannot drop
(capability missing, no_new_privs set), a plugin in basic mode is
refused, since the uid is its only protection there; under Landlock it logs a
warning and runs at the server's uid.
Warning
Uid separation is incompatible with no_new_privs. If the server runs
under systemd NoNewPrivileges=yes, the file capability is ignored on
exec: plugins are refused in basic mode, and under Landlock uid
separation degrades to same-uid. Pick one.
If the server runs as root instead (not recommended), it drops plugins to 1001 directly and the capability is not needed. The plain-language picture per scenario is in Plugin sandbox setup.
Secret file permissions¶
Mirror the image: secret-bearing directories are owner-only, so a plugin dropped to UID 1001 cannot read them even without Landlock.
sudo chown -R evey:evey /var/lib/evey
# the server owns plugins/ (it extracts installs there); group evey,
# which evey-plugin belongs to, only reads and executes the installed files:
sudo chmod 750 /var/lib/evey/plugins
# owner-only on the secret directories:
sudo chmod 700 /var/lib/evey/keys /var/lib/evey/logs /var/lib/evey/backups
# the JWT signing key itself (generated 0600 on first boot):
sudo chmod 600 /var/lib/evey/jwt.key
Adjust the paths to wherever you point the EVEY_*_DIR variables and
EVEY_JWT_PRIVATE_KEY_PATH.
Configure the environment¶
All configuration is EVEY_* environment variables; Configuration
has the full table. Points specific to a bare-metal run:
- Point
EVEY_DB_HOSTandEVEY_MEILI_URLat your PostgreSQL and Meilisearch.EVEY_DB_PASSWORDmust not be the defaultevey. - The Go defaults for paths were chosen for the container: set
EVEY_LOG_DIR(defaults to the relativelogs),EVEY_BACKUP_DIR(/data/backups),EVEY_TRANSCODING_CACHE_DIR(/data/transcode),EVEY_TRICKPLAY_CACHE_DIR(/cache/trickplay) andEVEY_JWT_PRIVATE_KEY_PATH(/data/jwt.key) to your own directories. - The server serves plain HTTP: put a reverse proxy in front for TLS, with an
https://External URL and the proxy in Trusted proxies (Admin -> Remote Access;EVEY_SERVER_BASE_URLandEVEY_TRUSTED_PROXIESare the fallbacks). See Reverse proxy. - The server opens two ports, and only one may be exposed.
EVEY_SERVER_PORT(default9090) is the native gRPC listener; the HTTP API your clients and reverse proxy talk to is that port plus one (9091). The remote-access gate, which decides whether clients outside your LAN may connect, is wired into the HTTP chain and does not cover the gRPC listener, which therefore binds to loopback only by default (EVEY_GRPC_BIND=127.0.0.1). If you have a reason to publish it, setEVEY_GRPC_BIND=0.0.0.0; the server warns at boot that the gate does not apply to it, and firewalling it is then on you. - Migrations run at startup; there is no separate migrate step.
- The plugin resource caps (
EVEY_PLUGINS_MEMORY_MB,_PIDS_MAX,_DISK_MAX_MB,_CPU_NICE) and the sandbox mode are editable at runtime from Admin -> Plugins -> Resource limits; the environment values are the initial defaults. - The admin panel's server restart relies on the image's supervisor and reports itself unavailable in a bare run: restart the service yourself.
Verify the sandbox¶
The server logs the backend it resolved. After the first plugin starts, look for:
- once, for
auto:plugin sandbox auto-detection complete selected_mode=landlock(orbubblewrap,basic); - at every plugin start:
plugin sandbox applied sandbox_mode=landlock plugin_id=...; - the warning you do not want when you expected Landlock:
plugin sandbox auto-detection fell back to basic mode: no per-path filesystem isolation available ..., meaning neither Landlock nor a workingbwrapwas found. Re-check the backend section above.
Confirm the kernel side independently:
cat /sys/kernel/security/lsm # lists "landlock"
grep Seccomp: /proc/<plugin-pid>/status # 2 = a seccomp filter is active on the plugin
A Seccomp: 2 line on a live plugin process confirms the kernel filter is
installed. With EVEY_PLUGINS_SANDBOX_MODE=landlock and Landlock not
enforced, the plugin refuses to start: that is the fail-closed contract
working.
For backups, updates and the recovery commands, the container pages apply
as they are: Backups and updates, Recovery
and the command line describe the same evey admin
binary.