Repositories / jai.git
jai.git
Clone (read-only): git clone http://git.guha-anderson.com/git/jai.git
@@ -1,6 +1,6 @@  -# JAI - An ultra lightweight jail for AI CLIs on linux 6.13 and later +# JAI - An ultra lightweight jail for AI CLIs on modern linux `jai` strives to be the easiest container in the world to configure--so easy that you never again need to run a code assistant @@ -39,6 +39,11 @@ following: * Per-command configuration files. +jai emphasizes security over portability. It heavily leverages modern +linux APIs to isolate processes and avoid time-of-check-to-time-of-use +race conditions. It will not work with kernels older than 6.13 or +operating systems other than linux. + See the [man page](jai.1.md) for more documentation. See the [INSTALL](INSTALL) file for installation instructions.
@@ -13,8 +13,9 @@ const std::string default_conf = # overlay mount. To change the default, you can uncomment one of the # following: -# casual -# strict +# mode casual +# mode bare +# mode strict # You can use use "name NAME" to specify different sandboxes. For # casual sandboxes, the sandboxed home directory will be in @@ -38,10 +39,11 @@ const std::string default_conf = command "$0" "$@"; exit $? # Masked files are deleted when an overlayfs is first created, but -# have no effect on existing overlays. To delete files from an -# existing overlay, delete them under /run/jai/$USER/default.home. -# Otherwise, to apply new mask directives you can run "jai -u" to -# unmount any existing overlays. +# have no effect on existing overlays or on strict/bare jails. To +# delete files from an existing overlay, delete them under +# /run/jai/$USER/default.home. Otherwise, to apply new mask +# directives after editing this file, you can run "jai -u" to unmount +# any existing overlays. mask .jai mask .ssh
@@ -31,7 +31,7 @@ everything else. This is known as _casual mode_, because *cmd* can read most sensitive files on the system, so jai prevents *cmd* from clobbering all your files but doesn't provide any confidentiality. -If you run `jai --strict` *cmd* [*arg*]...", then *cmd* will be run +If you run `jai -mstrict` *cmd* [*arg*]...", then *cmd* will be run with an empty home directory as an unprivileged user id, but with the current working directory mapped to its place and fully exposed. While the rest of the system outside the user's home directory is @@ -43,6 +43,8 @@ Before using `jai`, if your home directory is on NFS, make `$HOME/.jai` a symbolic link to a directory you own on a local file system that supports extended attributes. Otherwise, overlay mounts may not work and you may only be able to use strict mode (see below). +Note that strict mode will not work with a home directory on NFS, but +you can use bare mode. If you want to grant access to directories other than the current working directory, you can specify addition directories with the `-d` @@ -60,7 +62,7 @@ jai allows the use of multiple sandboxed home directories. To use a home directory other than the default, just give it a name with the `-n` option and it will be created on demand. When you specify a home directory with `-n`, strict mode becomes the default. However, you -can have multiple home overlays by specifying `--casual` with `-n`. +can have multiple home overlays by specifying `-mcasual` with `-n`. # CONFIGURATION @@ -120,7 +122,7 @@ virtual environment before running the command. is read at the exact point of the `conf` directive, so that it overrides previous lines and is overridden by subsequent lines. -`-d` *dir*, `--dir `*dir* +`-d` *dir*, `--dir` *dir* : Grant full access to directory *dir* and everything below in the jail. You must own the directory. You can supply this option multiple times. Note that on the command line, relative paths are @@ -134,9 +136,9 @@ virtual environment before running the command. home directory will be copy-on-write and nothing will be directly exported. -`--casual` -: Enables casual mode, in which the user's home directory is made - available as an overlay mount. Casual mode protects against +`-m casual`|`bare`|`strict`, `--mode casual`|`bare`|`strict` +: Set the execution mode. In casual mode, the user's home directory + is made available as an overlay mount. Casual mode protects against destruction of files outside of granted directories, but does not protect confidentiality: sandboxed code can read most files accessible to the user. You can hide specific files with the @@ -144,15 +146,19 @@ virtual environment before running the command. but because casual mode makes everything readable by default, it cannot protect all sensitive files. -`--strict` -: Enables strict mode. In strict mode, the user's home directory is - replaced by an empty directory, and sandboxed code runs with a - different user id, `jai`. Id-mapped mounts are used to map `jai` to - the invoking user in granted directories. Strict mode is the + In strict mode, the user's home directory is replaced by an empty + directory (`$HOME/.jai/`*name*`.home`), and sandboxed code runs with + a different user id, `jai`. Id-mapped mounts are used to map `jai` + to the invoking user in granted directories. Strict mode is the default when you name a sandbox (see `--name`), but not for the default sandbox. -`-n` *name*, `--name `*name* + Bare mode uses an empty directory like strict mode, but runs with + the invoking user's credentials. It is inferior to strict mode, but + can be used for NFS-mounted home directories, since NFS does not + support id-mapped mounts. + +`-n` *name*, `--name` *name* : jai allows you to have multiple sandboxed home directories, which may be useful when sandboxing multiple tools that should not have access to each other's API keys. This option specifies which home
@@ -17,10 +17,11 @@ path prog; constexpr const char *kUnstrustedUser = UNTRUSTED_USER; +constexpr const char *kUntrustedGecos = "JAI sandbox untrusted user"; constexpr const char *kRunRoot = "/run/jai"; struct Config { - enum Mode { kInvalidMode, kCasual, kStrict }; + enum Mode { kInvalidMode, kCasual, kBare, kStrict }; Mode mode_{kInvalidMode}; PathSet grant_directories_; @@ -179,18 +180,14 @@ Config::init_credentials() untrusted_cred_ = user_cred_ = Credentials::get_user(pw); if (PwEnt u = PwEnt::get_nam(kUnstrustedUser)) { - if (u->pw_uid && !strcmp(u->pw_gecos, "JAI sandbox untrusted user") && + if (u->pw_uid && !strcmp(u->pw_gecos, kUntrustedGecos) && !strcmp(u->pw_dir, "/")) untrusted_cred_ = Credentials::get_user(u); else - warn(R"(Ignoring user {} because uid is 0, home dir is not "/" or -GECOS field is not "JAI sandbox untrusted user")", - kUnstrustedUser); + warn(R"(Ignoring user {} because uid is 0, home dir is not "/", or +GECOS field is not "{}")", + kUnstrustedUser, kUntrustedGecos); } - else - warn(R"(Could not find credentials for untrusted {} user. -Try running "sudo systemd-sysusers".)", - kUnstrustedUser); // Paranoia about ptrace, because we will drop privileges to access // the file system as the user. @@ -409,6 +406,9 @@ Config::make_mnt_ns() Fd oldns = xopenat(-1, "/proc/self/ns/mnt", O_RDONLY | O_CLOEXEC); Defer _restore_ns{[fd = *oldns] { xsetns(fd, CLONE_NEWNS); }}; + if (mode_ == kStrict && untrusted_cred_ == user_cred_) + err("Cannot use strict mode: invalid user {}", kUnstrustedUser); + if (mode_ == kInvalidMode) mode_ = sandbox_name_.empty() ? kCasual : kStrict; if (sandbox_name_.empty()) @@ -426,10 +426,12 @@ Config::make_mnt_ns() if (mode_ == kCasual) home = clone_tree(*make_home_overlay()); else { - sbcred = &untrusted_cred_; - mapns = make_idmap_ns(); - attr.attr_set |= MOUNT_ATTR_IDMAP; - attr.userns_fd = *mapns; + if (mode_ == kStrict) { + sbcred = &untrusted_cred_; + mapns = make_idmap_ns(); + attr.attr_set |= MOUNT_ATTR_IDMAP; + attr.userns_fd = *mapns; + } home = clone_tree(*ensure_udir(home_jai(), cat(sandbox_name_, ".home"))); } xmnt_setattr(*tmp, attr); @@ -474,6 +476,7 @@ Config::make_mnt_ns() xsetns(*oldns, CLONE_NEWNS); auto restore_root = asuser(); Fd src = xopenat(-1, d, O_DIRECTORY | O_PATH | O_CLOEXEC); + check_user(*src, d); restore_root.reset(); src = clone_tree(*src); // Should it be recursive? xmnt_setattr(*src, attr); @@ -661,7 +664,7 @@ Config::exec(int nsfd, char **argv) _exit(1); } - if (mode_ == kCasual) + if (mode_ == kCasual || mode_ == kBare) user_cred_.make_real(); else untrusted_cred_.make_real(); @@ -692,12 +695,24 @@ Config::opt_parser() auto ret = std::make_unique<Options>(); Options &opts = *ret; opts( - "--casual", [this] { mode_ = kCasual; }, - "Enable casual mode (copy-on-write overlay home directory)"); - opts( - "--strict", [this] { mode_ = kStrict; }, - std::format("Enable strict mode (run with uid {} and empty home)", - kUnstrustedUser)); + "-m", "--mode", + [this](std::string_view m) { + static const std::map<std::string, Mode, std::less<>> modemap{ + {"default", kInvalidMode}, + {"casual", kCasual}, + {"bare", kBare}, + {"strict", kStrict}}; + if (auto it = modemap.find(m); it != modemap.end()) + mode_ = it->second; + else + err<Options::Error>(R"(invalid mode {})", m); + }, + std::format(R"(Set execution mode to one of the following: + casual - run as invoking UID with overlay home directory + bare - run as invoking UID with bare home directory + strict - run as UID {} with bare home directory)", + kUnstrustedUser), + "casual|bare|strict"); opts( "-d", "--dir", [this](path d) {