Unofficial
On this page
  1. 01What the jail gives
  2. 02Before you start
  3. 03The profile
  4. 04The blocks
  5. 05Build, sign, install
  6. 06Check that it is confined
  7. 07Stop, remove, and what stays

Run your package's service in a jail: its own account, no root, only the network you allow

A QPKG package's service script runs as root. With the service builder's Start in a jail block, the programs it starts run as an account of their own inside a sandbox: no capabilities, no way to gain them, a filter on dangerous system calls, and a network of their own. The example serves a page and reports, on that page, what the sandbox lets it do.

  1. Profilewhat runs, what it sees
  2. Blocksaccount, folders, the jail
  3. Buildbvs-jail and bwrap come with it
  4. Signcase 1, then install
  5. Checkthe page reports itself

The pictures come from Bucket & Code Server IDE 1.0.0-49, and the example was built, signed, installed and checked on a QTS 6.1 test NAS. The account is called ola.

01What the jail gives

  • The services run as the account the profile names, never root, with no capabilities and NoNewPrivs set. A seccomp filter refuses mounts, new namespaces, kernel modules, bpf, io_uring, ptrace and the other calls kernel exploits go through.
  • They see the app read-only, their data folders read-write, a private /tmp, /proc and /dev, and the NAS's glibc. No other share, no /etc/config, no other package.
  • Their network is a loopback of their own. Each inbound port is relayed from the NAS to a port inside. Outbound connections go through an HTTP proxy that refuses private, loopback and the NAS's own addresses.
  • A memory limit, a CPU share and a process limit hold the whole jail.

The package carries the jail itself: bin/bvs-jail and a static bin/bwrap (bubblewrap), staged by the build for each architecture.

02Before you start

  • A QPKG project made with New project, QDK on the Toolchains page and the Ubuntu 24.04 builder environment. Your own App Center walks through them; this example used the Empty service preset.
  • A code-signing certificate that QNAP issued to you, as files. App Center installs only signed packages.

03The profile

The profile is a JSON file of the package. Here it is shared/etc/jail/jailbox.json in the project, which the package installs as etc/jail/jailbox.json in its install folder.

# shared/etc/jail/jailbox.json
{
  "name": "jailbox",
  "user": "jailbox",
  "app": "@QPKG_ROOT@",
  "data": ["@QPKG_DATA@"],
  "services": [
    {"name": "checks", "cmd": ["@QPKG_ROOT@/bin/busybox", "sh", "@QPKG_ROOT@/share/jailbox/checks.sh", "@QPKG_ROOT@", "@QPKG_DATA@"], "oneshot": true},
    {"name": "web", "cmd": ["@QPKG_ROOT@/bin/busybox", "httpd", "-f", "-p", "127.0.0.1:8080", "-h", "@QPKG_DATA@/www"], "wait_port": 8080}
  ],
  "inbound": [{"listen": "0.0.0.0:19975", "port": 8080}],
  "egress": {},
  "limits": {"memory_mb": 256},
  "stop_seconds": 10
}
  • @QPKG_ROOT@ and @QPKG_DATA@ stand for the install and data folders, which are known only on the NAS. Each start writes them out into a copy that only root can change, and the jail runs that copy. Unknown keys are refused.
  • app is bound read-only. Here it is the whole install folder, so the services can use the package's own BusyBox; a real app points it at its own tree, for example @QPKG_ROOT@/app.
  • services start in order. A oneshot runs to its end first; wait_port waits until the service listens before the next one starts. At least one service keeps running.
  • inbound relays port 19975 of the NAS to port 8080 inside. egress gives the services a proxy at 127.0.0.1:3128 for ports 80 and 443 of public addresses; an allow list lets named destinations through whatever their address.
  • limits.memory_mb is 2048 unless you set it; stop_seconds is how long the jail waits for its services to stop.

The first service runs this script, also in the package, as shared/share/jailbox/checks.sh. It writes what the sandbox allows into the folder the second service serves.

#!/bin/sh
# Runs inside the jail and writes what the sandbox allows into the page folder.
R=$1 D=$2 B=$R/bin/busybox V=$($B dirname "$D")
{
  $B grep -E '^(Uid|CapEff|NoNewPrivs|Seccomp):' /proc/self/status
  if $B touch "$R/probe" 2>/dev/null; then echo 'write the install folder: allowed'; else echo 'write the install folder: refused'; fi
  if $B ls /etc/config >/dev/null 2>&1; then echo 'read /etc/config: allowed'; else echo 'read /etc/config: refused'; fi
  echo "what $V holds here: $($B ls -A "$V" | $B tr '\n' ' ')"
  echo "network interfaces: $($B tail -n +3 /proc/net/dev | $B cut -d: -f1 | $B tr -d ' ' | $B tr '\n' ' ')"
} >"$D/www/checks.txt"
echo 'Served from inside the jail.' >"$D/www/index.html"

04The blocks

In the project's page, the Service and package card, tab Blocks: Add a block under At start, five times, in this order.

Add a block, the group The program: Create a group, Create an account, Owner and mode, Start the program, Start in a jail, Wait for a port, Wait for a socket
  1. Data folder on the volume, name jailbox: the folder $QPKG_DATA.
  2. Create an account, name jailbox: the account the profile names.
  3. Create a folder, $QPKG_DATA/www, owned by jailbox:jailbox, mode 750: the only folder the services write.
  4. Start in a jail, ID jailbox, profile $QPKG_ROOT/etc/jail/jailbox.json. Its seconds to wait at stop must be at least the profile's stop_seconds plus 15; the default 90 is enough here. The Start in a jail block: ID jailbox, Profile $QPKG_ROOT/etc/jail/jailbox.json, Seconds to wait at stop 90, Restart it when it stops off, Undo it at stop on, and the lines it writes: qpkg_jail and, at stop, qpkg_daemon_stop with bin/bvs-jail
  5. Wait for a port, 19975: the start ends when the relayed port answers.

Under At a status check, Is it running? with the ID jailbox and Is the port open? with 19975 make the service script's status verb answer for the jail. Save shows each file's diff, and Commit writes them to the project's repository.

Commit these changes?: service.json with the start blocks, and shared/jailbox.sh with the lines qpkg_data_dir, qpkg_user, qpkg_dir, qpkg_jail and qpkg_wait_port

05Build, sign, install

Build in the project's page runs QDK's qbuild. Its log names what the package carries for the jail.

The build log: the x86_64 package: bin/busybox, bin/chpst, share/busybox/, bin/bvs-jail, bin/bwrap and share/bubblewrap/; built: build/jailbox_0.1.0_x86_64.qpkg, without a code signature

Without a certificate the package is built unsigned, and the project's page says that test deploy and the repository need a signed one.

The project's pipeline: Build built; Test deploy needs a code-signed package; Publish: unsigned, the repository accepts signed packages only
  1. Put the certificate's files, certificate, private_key and ca_certs, next to qpkg.cfg in the project folder, and build again: the package comes out signed. The template's .gitignore keeps the files out of git. In CI they come from the repository's secrets, as Your own App Center shows.
  2. Install the signed package: Test deploy on the project's page (admin), App Center's Install Manually, or your NAS's own App Center repository.

06Check that it is confined

Open http://<NAS address>:19975/: the page says “Served from inside the jail.”, through the relayed port. checks.txt beside it holds what the first service found:

# http://192.168.1.20:19975/checks.txt
Uid:	222	222	222	222
CapEff:	0000000000000000
NoNewPrivs:	1
Seccomp:	2
write the install folder: refused
read /etc/config: refused
what /share/CACHEDEV1_DATA holds here: .qpkg jailbox
network interfaces: lo
  • The volume folder holds only the package's own folders: the install folder under .qpkg and the data folder. No share, no other package.
  • From an SSH session as admin, grep -E '^(Uid|CapEff|NoNewPrivs|Seccomp):' /proc/<pid>/status of the service shows the same. The two bubblewrap processes that watch the jail from outside have no capabilities and NoNewPrivs too, without the filter.
  • The jail's log is log/jailbox.log in the data folder: each service's start, its port, the stop. While the data folder belongs to root, the log notes it; the services write only in the folder the Create a folder block gave the account.

07Stop, remove, and what stays

  • /etc/init.d/jailbox.sh stop, or App Center's stop, ends the services in reverse order and the jail logs ended (0). No process of the account, no cgroup and no listening port stays.
  • Removing the package stops the jail and removes the install folder and the service script. The account and the data folder stay, because no block removes them: Your own shell lines At removal can take them along.
  • The jail's run folder, /var/run/bvs-jail/jailbox, stays until the next start rewrites it. It is on QTS's memory file system and goes at the next reboot.