Skip to content

feat(install): Install into a home directory without root - #2

Merged
ananos merged 1 commit into
mainfrom
feat/install-user-mode
Sep 21, 2026
Merged

ananos merged 1 commit into
mainfrom
feat/install-user-mode

Conversation

@ananos

@ananos ananos commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

PR #1 leaves the prefix baked into the generated configs, so a published tarball
cannot serve an unprivileged user: the only route into a home directory is to
rebuild the bundle for that prefix. This is the install.sh --user mode its
"Out of scope" section points at.

It is mostly a text rewrite. No binary in the bundle carries the path -- brig,
urunc, nerdctl and containerd all come back empty from
strings | grep /var/lib/brig -- so exactly nine generated files decide where a
tree thinks it lives: bin/brig-ctl, bin/brig-pool-up,
bin/brig-rootless-setup.sh, etc/brig-env.sh, etc/containerd.toml,
etc/nerdctl.toml, etc/urunc.toml and the two units under etc/systemd/.
pins.env now records the layout the bundle was built with, and retarget_tree
rewrites those files after unpacking, then greps for the old prefix and fails if
any of it survived. A tenth file added later stops the install rather than
half-working on a host.

A non-root caller gets a user install by default, and --system / --user
force it either way. The tree goes to ~/.local/share/brig, the launchers to
~/.local/bin, the socket to $XDG_RUNTIME_DIR. No group, no thin pool
(losetup and dmsetup are not available in a user namespace, so a user
install is overlayfs only), and no /etc/urunc symlink, which the user unit
already replaces with URUNC_CONFIG_FILE. The last stage runs
brig-rootless-setup.sh, so the per-user work stays in the one place that
already did it. brig-uninstall.sh keys off the stamp, so a home install comes
down as its own user, taking the user unit, the per-user config and the
containerd data root with it.

The preflight is in two halves. What nothing can fix without an admin stops the
install: no subuid range, no newuidmap, no systemd user session. What
brig-rootless-setup.sh asks sudo for once is reported and left to it, the
AppArmor profile and the device ACLs. Failing hard on those blocked the install
before the script that fixes them had been unpacked.

Tested

On an Ubuntu 24.04 amd64 host, with a stub sudo that refuses every call and
logs it, so "no root" is measured rather than assumed. That log came back empty
across the install, the run and the uninstall.

  • installed from a bundle built with the default prefix, retargeted all nine
    files, and brig doctor comes up green from the home prefix
  • a guest boots on the microVM backend: Linux urunc 6.18.0urunc, with the
    project bind-mounted at /work, virtiofs rw in the guest, read from and
    written to, and the guest's write landing on the host owned by the invoking
    user
  • the shim, virtiofsd and the monitor all run as that user out of the home
    prefix, served by a containerd on the user's own config
  • the image pull over slirp4netns works
  • brig-ctl uninstall as that user removes the tree, the per-user config, the
    containerd data root, the launchers and the user unit, and leaves a system
    install on the same host running
  • an unprepared prefix, whose rootlesskit has no AppArmor profile, is refused:
    it warns before unpacking anything, stops at the sudo it cannot make, and
    exits 1

CI's lint steps pass locally: sh -n and shellcheck -s sh over install.sh,
build-bundle.sh, the generated uninstaller and the generated setup. The bundle
job gains one assertion, that pins.env records PREFIX, DATA_DIR and
RUN_DIR, since the retarget reads them from there.

Based on #1

Stacked on feat/rootless-containerd, because install.sh --user runs
brig-rootless-setup.sh, which arrives in that branch. Rebase onto main once
#1 merges, or squash the two if one commit reads better.

Out of scope

qemu still comes from the host rather than the bundle, so a user with no root at
all needs it installed alongside the host prerequisites above. A home install is
also one copy of the tree per user where a root install is shared, so
brig-ctl rootless against a shared install stays the better trade with root
and several users.

@ananos
ananos force-pushed the feat/rootless-containerd branch from ceabe45 to 3107b7b Compare September 21, 2026 21:08
@ananos
ananos force-pushed the feat/install-user-mode branch from 194a0a4 to 67a6eea Compare September 21, 2026 21:08
@ananos
ananos force-pushed the feat/rootless-containerd branch from 3107b7b to 1d90bff Compare September 21, 2026 21:10
@ananos
ananos force-pushed the feat/install-user-mode branch from 67a6eea to c685345 Compare September 21, 2026 21:10
@ananos
ananos force-pushed the feat/rootless-containerd branch from 1d90bff to c42790f Compare September 21, 2026 22:04
@ananos
ananos force-pushed the feat/install-user-mode branch from c685345 to 761fd08 Compare September 21, 2026 22:04
@ananos
ananos marked this pull request as ready for review September 21, 2026 22:09
install.sh needed root because the layout was fixed at build time: the
generated wrappers, configs and units carry /var/lib/brig/data as literal
paths, so the only way into a home directory was to rebuild the bundle for
that prefix. A published tarball could not serve an unprivileged user at all.

Nothing else stood in the way. No binary in the bundle carries the path --
brig, urunc, nerdctl and containerd all come back empty -- so exactly nine
generated text files decide where a tree thinks it lives, and install.sh can
rewrite them after unpacking. pins.env now records the layout it was built
with, and retarget_tree greps for it afterwards: a tenth file added later
fails the install instead of half-working on the host.

A non-root caller now gets a user install by default, and --system / --user
force it either way:

- the tree in ~/.local/share/brig, launchers in ~/.local/bin, socket in
  $XDG_RUNTIME_DIR
- no group, no thin pool (losetup and dmsetup are not available in a user
  namespace, so a user install is overlayfs only), and no /etc/urunc symlink,
  which the user unit replaces with URUNC_CONFIG_FILE
- the last stage runs brig-rootless-setup.sh, which already did the per-user
  work for a shared install
- a preflight for the host prerequisites, split between what stops the install
  and what brig-rootless-setup.sh asks sudo for once
- brig-uninstall.sh keys off the stamp, so a user install comes down as that
  user, taking the user unit, the per-user config and the containerd data root
  with it

Verified end to end on an Ubuntu 24.04 host with a stub sudo that refuses
every call: the install completes, brig doctor is green from the home prefix,
and a guest boots on 6.18.0urunc with a project bind-mounted at /work, read
and written from inside the VM. The shim, virtiofsd and qemu all run as the
invoking user out of the home prefix. An unprepared prefix is refused and
exits 1 rather than half-installing.

The preflight also has to ask the right question about the devices. Testing
/dev/kvm for read and write tests it from a login shell, where this user's kvm
group still applies -- and that group is precisely what the user namespace
drops, so the check passes on exactly the hosts whose first sandbox then dies
with EPERM. It now looks for what the setup grants: this user named in the
ACL, or a mode that covers everyone. setfacl joins the list of things only an
admin can supply, beside newuidmap, since the grant is made with it. And the
warning counts what it found instead of always saying "two", which otherwise
reads as a second problem the reader has failed to notice.

Signed-off-by: Anastassios Nanos <ananos@nofire.ai>
@ananos
ananos changed the base branch from feat/rootless-containerd to main September 21, 2026 22:10
@ananos
ananos force-pushed the feat/install-user-mode branch from 761fd08 to 7910264 Compare September 21, 2026 22:10
@ananos
ananos merged commit e4f2049 into main Sep 21, 2026
3 checks passed
@ananos
ananos deleted the feat/install-user-mode branch September 25, 2026 12:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant