Compare commits

...

36 Commits

Author SHA1 Message Date
Ophestra Umiker de0d78daae
release: 0.2.1
release / release (push) Successful in 1m4s Details
test / test (push) Successful in 20s Details
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-19 21:03:50 +09:00
Ophestra Umiker 6bf33ce507
fortify: use resolved username
test / test (push) Successful in 21s Details
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-19 21:03:09 +09:00
Ophestra Umiker 9faf3b3596
app: validate username
test / test (push) Successful in 23s Details
This value is used for passwd generation. Bad input can cause very confusing issues. This is not a security issue, however validation will improve user experience.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-19 21:01:41 +09:00
Ophestra Umiker d99c8b1fb4
release: 0.2.0
release / release (push) Successful in 44s Details
test / test (push) Successful in 22s Details
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-19 18:15:09 +09:00
Ophestra Umiker 6e4870775f
update README document
test / test (push) Successful in 20s Details
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-19 18:14:06 +09:00
Ophestra Umiker 0a546885e3
nix: update options doc
test / test (push) Successful in 22s Details
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-19 18:12:35 +09:00
Ophestra Umiker 653d69da0a
nix: module descriptions
test / test (push) Successful in 24s Details
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-19 18:10:57 +09:00
Ophestra Umiker f8256137ae
nix: separate module options from implementation
test / test (push) Successful in 25s Details
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-19 17:08:22 +09:00
Ophestra Umiker 54b47b0315
nix: copy pixmaps directory to share package
test / test (push) Successful in 21s Details
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-18 14:46:08 +09:00
Ophestra Umiker ae2628e57a
cmd/fshim/ipc: install signal handler on shim start
test / test (push) Successful in 20s Details
Getting killed at this point will result in inconsistent state.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-18 13:33:46 +09:00
Ophestra Umiker c026a4b5dc
fortify: permissive defaults resolve home directory from os
test / test (push) Successful in 21s Details
When starting with the permissive defaults "run" command, attempt to resolve home directory from os by default and fall back to /var/empty.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-18 13:01:07 +09:00
Ophestra Umiker 748a0ae2c8
nix: wrap program from libexec
test / test (push) Successful in 24s Details
This avoids renaming the fortify binary.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-18 12:58:47 +09:00
Ophestra Umiker 8f3f0c7bbf
nix: integrate dynamic users
test / test (push) Successful in 21s Details
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-18 02:49:48 +09:00
Ophestra Umiker 05b7dbf066
app: alternative inner home path
test / test (push) Successful in 24s Details
Support binding home to an alternative path in the mount namespace.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-18 00:18:21 +09:00
Ophestra Umiker 866270ff05
fmsg: add to wg prior to enqueue
test / test (push) Successful in 27s Details
Adding after channel write is racy.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-17 23:50:02 +09:00
Ophestra Umiker c1fad649e8
app/start: check for cleanup and abort condition
test / test (push) Successful in 21s Details
Dirty fix. Will rewrite after fsu integration complete.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-17 23:41:52 +09:00
Ophestra Umiker b5f01ef20b
app: append # for ChangeHosts message with numerical uid
test / test (push) Successful in 21s Details
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-17 23:40:37 +09:00
Ophestra Umiker 2e23cef7bb
cmd/fuserdb: generate group entries
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-17 23:31:06 +09:00
Ophestra Umiker 6a6d30af1f
cmd/fuserdb: systemd userdb drop-in entries generator
test / test (push) Successful in 20s Details
This provides user records via nss-systemd. Static drop-in entries are generated to reduce complexity and attack surface.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-17 02:16:02 +09:00
Ophestra Umiker df33123bd7
app: integrate fsu
test / test (push) Successful in 21s Details
This removes the dependency on external user switchers like sudo/machinectl and decouples fortify user ids from the passwd database.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-16 21:19:45 +09:00
Ophestra Umiker 1a09b55bd4
nix: remove portal paths from default
test / test (push) Successful in 27s Details
Despite presenting itself as a generic desktop integration interface, xdg-desktop portal is highly flatpak-centric and only supports flatpak and snap in practice. It is a significant attack surface to begin with as it is a privileged process which accepts input from unprivileged processes, and the lack of support for anything other than fortify also introduces various information leaks when exposed to fortify as it treats fortified programs as unsandboxed, privileged programs in many cases.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-10 22:24:17 +09:00
Ophestra Umiker 9a13b311ac
app/config: rename map_real_uid from use_real_uid
test / test (push) Successful in 19s Details
This option only changes mapped uid in the user namespace.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-09 12:01:34 +09:00
Ophestra Umiker 45fead18c3
cmd/fshim: set no_new_privs flag
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-09 11:50:56 +09:00
Ophestra Umiker 431aa32291
nix: remove absolute Exec paths
test / test (push) Successful in 26s Details
Absolute paths set for Exec causes the program to be launched as the privileged user.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-08 02:05:47 +09:00
Ophestra Umiker 3962705126
nix: keep fshim and finit names
test / test (push) Successful in 22s Details
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-06 14:59:28 +09:00
Ophestra Umiker ad80be721b
nix: improve start script
test / test (push) Successful in 23s Details
Zsh store path in shebang. Replace writeShellScript with writeScript since runtimeShell is not overridable.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-06 14:09:41 +09:00
Ophestra Umiker f831948bca
release: 0.1.0
release / release (push) Successful in 28s Details
test / test (push) Successful in 21s Details
This release significantly changes the command line interface, and updates the NixOS module to finally produce meaningful sandbox configuration.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-06 04:37:43 +09:00
Ophestra Umiker 2e31b3d3a1
update README document
test / test (push) Successful in 32s Details
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-06 04:35:52 +09:00
Ophestra Umiker 4d90e73366
nix: generate strict sandbox configuration
test / test (push) Successful in 22s Details
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-06 04:25:15 +09:00
Ophestra Umiker 3dfc1fcd56
app: support full /dev access
test / test (push) Successful in 22s Details
Also moved /dev/fortify to /fortify since it is impossible to create new directories in /dev from the init namespace and bind mounting its contents has undesirable side effects.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-06 03:49:39 +09:00
Ophestra Umiker 89bafd0c22
fortify: root check before command handling
test / test (push) Successful in 22s Details
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-05 12:57:03 +09:00
Ophestra Umiker 861bb1274f
fortify: override default usage function
test / test (push) Successful in 23s Details
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-05 00:12:31 +09:00
Ophestra Umiker 714818c8aa
fortify: implement cleaner argument structure
test / test (push) Successful in 24s Details
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-05 00:07:36 +09:00
Ophestra Umiker 69cc64ef56
linux: provide access to stdout
test / test (push) Successful in 22s Details
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-04 22:55:46 +09:00
Ophestra Umiker fc25ac2523
app: separate auto etc from permissive defaults
test / test (push) Successful in 23s Details
Populating /etc with symlinks is quite useful even outside the permissive defaults usage pattern.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-04 22:18:05 +09:00
Ophestra Umiker d909b1190a
app/config: UseRealUID as true in template
test / test (push) Successful in 24s Details
The template is based on a Chromium setup, which this workaround was created for.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-04 19:45:31 +09:00
36 changed files with 2142 additions and 1177 deletions

View File

@ -33,9 +33,9 @@ jobs:
go build -v -ldflags '-s -w go build -v -ldflags '-s -w
-X git.ophivana.moe/security/fortify/internal.Version=${{ github.ref_name }} -X git.ophivana.moe/security/fortify/internal.Version=${{ github.ref_name }}
-X git.ophivana.moe/security/fortify/internal.Fsu=/usr/bin/fsu -X git.ophivana.moe/security/fortify/internal.Fsu=/usr/bin/fsu
-X git.ophivana.moe/security/fortify/internal.Fshim=/usr/libexec/fortify/fshim
-X git.ophivana.moe/security/fortify/internal.Finit=/usr/libexec/fortify/finit -X git.ophivana.moe/security/fortify/internal.Finit=/usr/libexec/fortify/finit
-X main.Fmain=/usr/bin/fortify' -X main.Fmain=/usr/bin/fortify
-X main.Fshim=/usr/libexec/fortify/fshim'
-o bin/ ./... && -o bin/ ./... &&
(cd bin && sha512sum --tag -b * > sha512sums) (cd bin && sha512sum --tag -b * > sha512sums)

View File

@ -36,8 +36,8 @@ jobs:
go build -v -ldflags '-s -w go build -v -ldflags '-s -w
-X git.ophivana.moe/security/fortify/internal.Version=${{ github.ref_name }} -X git.ophivana.moe/security/fortify/internal.Version=${{ github.ref_name }}
-X git.ophivana.moe/security/fortify/internal.Fsu=/usr/bin/fsu -X git.ophivana.moe/security/fortify/internal.Fsu=/usr/bin/fsu
-X git.ophivana.moe/security/fortify/internal.Fshim=/usr/libexec/fortify/fshim
-X git.ophivana.moe/security/fortify/internal.Finit=/usr/libexec/fortify/finit -X git.ophivana.moe/security/fortify/internal.Finit=/usr/libexec/fortify/finit
-X main.Fmain=/usr/bin/fortify' -X main.Fmain=/usr/bin/fortify
-X main.Fshim=/usr/libexec/fortify/fshim'
-o bin/ ./... && -o bin/ ./... &&
(cd bin && sha512sum --tag -b * > sha512sums) (cd bin && sha512sum --tag -b * > sha512sums)

145
README.md
View File

@ -2,6 +2,7 @@ Fortify
======= =======
[![Go Reference](https://pkg.go.dev/badge/git.ophivana.moe/security/fortify.svg)](https://pkg.go.dev/git.ophivana.moe/security/fortify) [![Go Reference](https://pkg.go.dev/badge/git.ophivana.moe/security/fortify.svg)](https://pkg.go.dev/git.ophivana.moe/security/fortify)
[![Go Report Card](https://goreportcard.com/badge/git.ophivana.moe/security/fortify)](https://goreportcard.com/report/git.ophivana.moe/security/fortify)
Lets you run graphical applications as another user in a confined environment with a nice NixOS Lets you run graphical applications as another user in a confined environment with a nice NixOS
module to configure target users and provide launchers and desktop files for your privileged user. module to configure target users and provide launchers and desktop files for your privileged user.
@ -31,7 +32,9 @@ nix run git+https://git.ophivana.moe/security/fortify -- -h
## Module usage ## Module usage
The NixOS module currently requires home-manager and impermanence to function correctly. The NixOS module currently requires home-manager to function correctly.
Full module documentation can be found [here](options.md).
To use the module, import it into your configuration with To use the module, import it into your configuration with
@ -68,21 +71,21 @@ This adds the `environment.fortify` option:
{ {
environment.fortify = { environment.fortify = {
enable = true; enable = true;
user = "nixos"; stateDir = "/var/lib/persist/module/fortify";
stateDir = "/var/lib/persist/module"; users = {
target = { alice = 0;
chronos = { nixos = 10;
launchers = {
weechat.method = "sudo";
claws-mail.capability.pulse = false;
discord = {
command = "vesktop --ozone-platform-hint=wayland";
share = pkgs.vesktop;
}; };
chromium.dbus = { apps = [
configSystem = { {
name = "chromium";
id = "org.chromium.Chromium";
packages = [ pkgs.chromium ];
userns = true;
mapRealUid = true;
dbus = {
system = {
filter = true; filter = true;
talk = [ talk = [
"org.bluez" "org.bluez"
@ -90,8 +93,9 @@ This adds the `environment.fortify` option:
"org.freedesktop.UPower" "org.freedesktop.UPower"
]; ];
}; };
config = { session =
filter = true; f:
f {
talk = [ talk = [
"org.freedesktop.DBus" "org.freedesktop.DBus"
"org.freedesktop.FileManager1" "org.freedesktop.FileManager1"
@ -106,74 +110,63 @@ This adds the `environment.fortify` option:
"org.mpris.MediaPlayer2.org.chromium.Chromium.*" "org.mpris.MediaPlayer2.org.chromium.Chromium.*"
"org.mpris.MediaPlayer2.chromium.*" "org.mpris.MediaPlayer2.chromium.*"
]; ];
call = { call = { };
"org.freedesktop.portal.*" = "*"; broadcast = { };
};
broadcast = {
"org.freedesktop.portal.*" = "@/org/freedesktop/portal/*";
}; };
}; };
}
{
name = "claws-mail";
id = "org.claws_mail.Claws-Mail";
packages = [ pkgs.claws-mail ];
gpu = false;
capability.pulse = false;
}
{
name = "weechat";
packages = [ pkgs.weechat ];
capability = {
wayland = false;
x11 = false;
dbus = true;
pulse = false;
}; };
}
{
name = "discord";
id = "dev.vencord.Vesktop";
packages = [ pkgs.vesktop ];
share = pkgs.vesktop;
command = "vesktop --ozone-platform-hint=wayland";
userns = true;
mapRealUid = true;
capability.x11 = true;
dbus = {
session =
f:
f {
talk = [ "org.kde.StatusNotifierWatcher" ];
own = [ ];
call = { };
broadcast = { };
}; };
packages = with pkgs; [ system.filter = true;
weechat };
claws-mail }
vesktop {
chromium name = "looking-glass-client";
]; groups = [ "plugdev" ];
persistence.directories = [ extraPaths = [
".config/weechat" {
".claws-mail" src = "/dev/shm/looking-glass";
".config/vesktop" write = true;
}
]; ];
extraConfig = { extraConfig = {
programs.looking-glass-client.enable = true; programs.looking-glass-client.enable = true;
}; };
}; }
}; ];
}; };
} }
``` ```
* `enable` determines whether the module should be enabled or not. Useful when sharing configurations between graphical
and headless systems. Defaults to `false`.
* `user` specifies the privileged user with access to fortified applications.
* `stateDir` is the path to your persistent storage location. It is directly passed through to the impermanence module.
* `target` is an attribute set of submodules, where the attribute name is the username of the unprivileged target user.
The available options are:
* `packages`, the list of packages to make available in the target user's environment.
* `persistence`, user persistence attribute set passed to impermanence.
* `extraConfig`, extra home-manager configuration for the target user.
* `launchers`, attribute set where the attribute name is the name of the launcher.
The available options are:
* `command`, the command to run as the target user. Defaults to launcher name.
* `dbus.config`, D-Bus proxy custom configuration.
* `dbus.configSystem`, D-Bus system bus custom configuration, null to disable.
* `dbus.id`, D-Bus application id, has no effect if `dbus.config` is set.
* `dbus.mpris`, whether to enable MPRIS defaults, has no effect if `dbus.config` is set.
* `capability.wayland`, whether to share the Wayland socket.
* `capability.x11`, whether to share the X11 socket and allow connection.
* `capability.dbus`, whether to proxy D-Bus.
* `capability.pulse`, whether to share the PulseAudio socket and cookie.
* `share`, package containing desktop/icon files. Defaults to launcher name.
* `method`, the launch method for the sandboxed program, can be `"fortify"`, `"fortify-sudo"`, `"sudo"`.

View File

@ -103,7 +103,7 @@ func main() {
if err := cmd.Start(); err != nil { if err := cmd.Start(); err != nil {
fmsg.Fatalf("cannot start %q: %v", payload.Argv0, err) fmsg.Fatalf("cannot start %q: %v", payload.Argv0, err)
} }
fmsg.Withhold() fmsg.Suspend()
// close setup pipe as setup is now complete // close setup pipe as setup is now complete
if err := setup.Close(); err != nil { if err := setup.Close(); err != nil {

View File

@ -5,6 +5,8 @@ import (
"net" "net"
"os" "os"
"os/exec" "os/exec"
"os/signal"
"strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
"syscall" "syscall"
@ -12,6 +14,7 @@ import (
"git.ophivana.moe/security/fortify/acl" "git.ophivana.moe/security/fortify/acl"
shim0 "git.ophivana.moe/security/fortify/cmd/fshim/ipc" shim0 "git.ophivana.moe/security/fortify/cmd/fshim/ipc"
"git.ophivana.moe/security/fortify/internal"
"git.ophivana.moe/security/fortify/internal/fmsg" "git.ophivana.moe/security/fortify/internal/fmsg"
) )
@ -24,24 +27,26 @@ type Shim struct {
cmd *exec.Cmd cmd *exec.Cmd
// uid of shim target user // uid of shim target user
uid uint32 uid uint32
// whether to check shim pid // string representation of application id
checkPid bool aid string
// user switcher executable path // string representation of supplementary group ids
executable string supp []string
// path to setup socket // path to setup socket
socket string socket string
// shim setup abort reason and completion // shim setup abort reason and completion
abort chan error abort chan error
abortErr atomic.Pointer[error] abortErr atomic.Pointer[error]
abortOnce sync.Once abortOnce sync.Once
// fallback exit notifier with error returned killing the process
killFallback chan error
// wayland mediation, nil if disabled // wayland mediation, nil if disabled
wl *shim0.Wayland wl *shim0.Wayland
// shim setup payload // shim setup payload
payload *shim0.Payload payload *shim0.Payload
} }
func New(executable string, uid uint32, socket string, wl *shim0.Wayland, payload *shim0.Payload, checkPid bool) *Shim { func New(uid uint32, aid string, supp []string, socket string, wl *shim0.Wayland, payload *shim0.Payload) *Shim {
return &Shim{uid: uid, executable: executable, socket: socket, wl: wl, payload: payload, checkPid: checkPid} return &Shim{uid: uid, aid: aid, supp: supp, socket: socket, wl: wl, payload: payload}
} }
func (s *Shim) String() string { func (s *Shim) String() string {
@ -68,9 +73,11 @@ func (s *Shim) AbortWait(err error) {
<-s.abort <-s.abort
} }
type CommandBuilder func(shimEnv string) (args []string) func (s *Shim) WaitFallback() chan error {
return s.killFallback
}
func (s *Shim) Start(f CommandBuilder) (*time.Time, error) { func (s *Shim) Start() (*time.Time, error) {
var ( var (
cf chan *net.UnixConn cf chan *net.UnixConn
accept func() accept func()
@ -87,26 +94,51 @@ func (s *Shim) Start(f CommandBuilder) (*time.Time, error) {
} }
// start user switcher process and save time // start user switcher process and save time
s.cmd = exec.Command(s.executable, f(shim0.Env+"="+s.socket)...) var fsu string
s.cmd.Env = []string{} if p, ok := internal.Check(internal.Fsu); !ok {
fmsg.Fatal("invalid fsu path, this copy of fshim is not compiled correctly")
panic("unreachable")
} else {
fsu = p
}
s.cmd = exec.Command(fsu)
s.cmd.Env = []string{
shim0.Env + "=" + s.socket,
"FORTIFY_APP_ID=" + s.aid,
}
if len(s.supp) > 0 {
fmsg.VPrintf("attaching supplementary group ids %s", s.supp)
s.cmd.Env = append(s.cmd.Env, "FORTIFY_GROUPS="+strings.Join(s.supp, " "))
}
s.cmd.Stdin, s.cmd.Stdout, s.cmd.Stderr = os.Stdin, os.Stdout, os.Stderr s.cmd.Stdin, s.cmd.Stdout, s.cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
s.cmd.Dir = "/" s.cmd.Dir = "/"
fmsg.VPrintln("starting shim via user switcher:", s.cmd) fmsg.VPrintln("starting shim via fsu:", s.cmd)
fmsg.Withhold() // withhold messages to stderr fmsg.Suspend() // withhold messages to stderr
if err := s.cmd.Start(); err != nil { if err := s.cmd.Start(); err != nil {
return nil, fmsg.WrapErrorSuffix(err, return nil, fmsg.WrapErrorSuffix(err,
"cannot start user switcher:") "cannot start fsu:")
} }
startTime := time.Now().UTC() startTime := time.Now().UTC()
// kill shim if something goes wrong and an error is returned // kill shim if something goes wrong and an error is returned
s.killFallback = make(chan error, 1)
killShim := func() { killShim := func() {
if err := s.cmd.Process.Signal(os.Interrupt); err != nil { if err := s.cmd.Process.Signal(os.Interrupt); err != nil {
fmsg.Println("cannot terminate shim on faulted setup:", err) s.killFallback <- err
} }
} }
defer func() { killShim() }() defer func() { killShim() }()
// take alternative exit path on signal
sig := make(chan os.Signal, 2)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
go func() {
v := <-sig
fmsg.Printf("got %s after program start", v)
s.killFallback <- nil
signal.Ignore(syscall.SIGINT, syscall.SIGTERM)
}()
accept() accept()
var conn *net.UnixConn var conn *net.UnixConn
select { select {
@ -132,7 +164,7 @@ func (s *Shim) Start(f CommandBuilder) (*time.Time, error) {
err = errors.New("compromised fortify build") err = errors.New("compromised fortify build")
s.Abort(err) s.Abort(err)
return &startTime, err return &startTime, err
} else if s.checkPid && cred.Pid != int32(s.cmd.Process.Pid) { } else if cred.Pid != int32(s.cmd.Process.Pid) {
fmsg.Printf("process %d tried to connect to shim setup socket, expecting shim %d", fmsg.Printf("process %d tried to connect to shim setup socket, expecting shim %d",
cred.Pid, s.cmd.Process.Pid) cred.Pid, s.cmd.Process.Pid)
err = errors.New("compromised target user") err = errors.New("compromised target user")

View File

@ -58,7 +58,7 @@ func main() {
// dial setup socket // dial setup socket
var conn *net.UnixConn var conn *net.UnixConn
if c, err := net.DialUnix("unix", nil, &net.UnixAddr{Name: socketPath, Net: "unix"}); err != nil { if c, err := net.DialUnix("unix", nil, &net.UnixAddr{Name: socketPath, Net: "unix"}); err != nil {
fmsg.Fatal("cannot dial setup socket:", err) fmsg.Fatal(err.Error())
panic("unreachable") panic("unreachable")
} else { } else {
conn = c conn = c
@ -67,7 +67,7 @@ func main() {
// decode payload gob stream // decode payload gob stream
var payload shim.Payload var payload shim.Payload
if err := gob.NewDecoder(conn).Decode(&payload); err != nil { if err := gob.NewDecoder(conn).Decode(&payload); err != nil {
fmsg.Fatal("cannot decode shim payload:", err) fmsg.Fatalf("cannot decode shim payload: %v", err)
} else { } else {
fmsg.SetVerbose(payload.Verbose) fmsg.SetVerbose(payload.Verbose)
} }
@ -80,7 +80,7 @@ func main() {
wfd := -1 wfd := -1
if payload.WL { if payload.WL {
if fd, err := receiveWLfd(conn); err != nil { if fd, err := receiveWLfd(conn); err != nil {
fmsg.Fatal("cannot receive wayland fd:", err) fmsg.Fatalf("cannot receive wayland fd: %v", err)
} else { } else {
wfd = fd wfd = fd
} }
@ -102,7 +102,10 @@ func main() {
} else { } else {
// no argv, look up shell instead // no argv, look up shell instead
var ok bool var ok bool
if ic.Argv0, ok = os.LookupEnv("SHELL"); !ok { if payload.Bwrap.SetEnv == nil {
fmsg.Fatal("no command was specified and environment is unset")
}
if ic.Argv0, ok = payload.Bwrap.SetEnv["SHELL"]; !ok {
fmsg.Fatal("no command was specified and $SHELL was unset") fmsg.Fatal("no command was specified and $SHELL was unset")
} }
@ -125,7 +128,7 @@ func main() {
// share config pipe // share config pipe
if r, w, err := os.Pipe(); err != nil { if r, w, err := os.Pipe(); err != nil {
fmsg.Fatal("cannot pipe:", err) fmsg.Fatalf("cannot pipe: %v", err)
} else { } else {
conf.SetEnv[init0.Env] = strconv.Itoa(3 + len(extraFiles)) conf.SetEnv[init0.Env] = strconv.Itoa(3 + len(extraFiles))
extraFiles = append(extraFiles, r) extraFiles = append(extraFiles, r)
@ -134,7 +137,7 @@ func main() {
go func() { go func() {
// stream config to pipe // stream config to pipe
if err = gob.NewEncoder(w).Encode(&ic); err != nil { if err = gob.NewEncoder(w).Encode(&ic); err != nil {
fmsg.Fatal("cannot transmit init config:", err) fmsg.Fatalf("cannot transmit init config: %v", err)
} }
}() }()
} }
@ -142,7 +145,7 @@ func main() {
helper.BubblewrapName = payload.Exec[0] // resolved bwrap path by parent helper.BubblewrapName = payload.Exec[0] // resolved bwrap path by parent
if b, err := helper.NewBwrap(conf, nil, finitPath, if b, err := helper.NewBwrap(conf, nil, finitPath,
func(int, int) []string { return make([]string, 0) }); err != nil { func(int, int) []string { return make([]string, 0) }); err != nil {
fmsg.Fatal("malformed sandbox config:", err) fmsg.Fatalf("malformed sandbox config: %v", err)
} else { } else {
cmd := b.Unwrap() cmd := b.Unwrap()
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
@ -154,7 +157,7 @@ func main() {
// run and pass through exit code // run and pass through exit code
if err = b.Start(); err != nil { if err = b.Start(); err != nil {
fmsg.Fatal("cannot start target process:", err) fmsg.Fatalf("cannot start target process: %v", err)
} else if err = b.Wait(); err != nil { } else if err = b.Wait(); err != nil {
fmsg.VPrintln("wait:", err) fmsg.VPrintln("wait:", err)
} }

View File

@ -1,10 +1,12 @@
package main package main
import ( import (
"bufio" "bytes"
"fmt"
"log" "log"
"os" "os"
"path" "path"
"slices"
"strconv" "strconv"
"strings" "strings"
"syscall" "syscall"
@ -15,9 +17,15 @@ const (
fsuConfFile = "/etc/fsurc" fsuConfFile = "/etc/fsurc"
envShim = "FORTIFY_SHIM" envShim = "FORTIFY_SHIM"
envAID = "FORTIFY_APP_ID" envAID = "FORTIFY_APP_ID"
envGroups = "FORTIFY_GROUPS"
PR_SET_NO_NEW_PRIVS = 0x26
) )
var Fmain = compPoison var (
Fmain = compPoison
Fshim = compPoison
)
func main() { func main() {
log.SetFlags(0) log.SetFlags(0)
@ -33,12 +41,17 @@ func main() {
log.Fatal("this program must not be started by root") log.Fatal("this program must not be started by root")
} }
var fmain string var fmain, fshim string
if p, ok := checkPath(Fmain); !ok { if p, ok := checkPath(Fmain); !ok {
log.Fatal("invalid fortify path, this copy of fsu is not compiled correctly") log.Fatal("invalid fortify path, this copy of fsu is not compiled correctly")
} else { } else {
fmain = p fmain = p
} }
if p, ok := checkPath(Fshim); !ok {
log.Fatal("invalid fshim path, this copy of fsu is not compiled correctly")
} else {
fshim = p
}
pexe := path.Join("/proc", strconv.Itoa(os.Getppid()), "exe") pexe := path.Join("/proc", strconv.Itoa(os.Getppid()), "exe")
if p, err := os.Readlink(pexe); err != nil { if p, err := os.Readlink(pexe); err != nil {
@ -61,84 +74,76 @@ func main() {
uid += fid * 10000 uid += fid * 10000
} }
// allowed aid range 0 to 9999
if as, ok := os.LookupEnv(envAID); !ok {
log.Fatal("FORTIFY_APP_ID not set")
} else if aid, err := parseUint32Fast(as); err != nil || aid < 0 || aid > 9999 {
log.Fatal("invalid aid")
} else {
uid += aid
}
// pass through setup path to shim // pass through setup path to shim
var shimSetupPath string var shimSetupPath string
if s, ok := os.LookupEnv(envShim); !ok { if s, ok := os.LookupEnv(envShim); !ok {
log.Fatal("FORTIFY_SHIM not set") // fortify requests target uid
// print resolved uid and exit
fmt.Print(uid)
os.Exit(0)
} else if !path.IsAbs(s) { } else if !path.IsAbs(s) {
log.Fatal("FORTIFY_SHIM is not absolute") log.Fatal("FORTIFY_SHIM is not absolute")
} else { } else {
shimSetupPath = s shimSetupPath = s
} }
// allowed aid range 0 to 9999 // supplementary groups
if as, ok := os.LookupEnv(envAID); !ok { var suppGroups, suppCurrent []int
log.Fatal("FORTIFY_APP_ID not set")
} else if aid, err := strconv.Atoi(as); err != nil || aid < 0 || aid > 9999 { if gs, ok := os.LookupEnv(envGroups); ok {
log.Fatal("invalid aid") if cur, err := os.Getgroups(); err != nil {
log.Fatalf("cannot get groups: %v", err)
} else { } else {
uid += aid suppCurrent = cur
} }
// parse space-separated list of group ids
gss := bytes.Split([]byte(gs), []byte{' '})
suppGroups = make([]int, len(gss)+1)
for i, s := range gss {
if gid, err := strconv.Atoi(string(s)); err != nil {
log.Fatalf("cannot parse %q: %v", string(s), err)
} else if gid > 0 && gid != uid && gid != os.Getgid() && slices.Contains(suppCurrent, gid) {
suppGroups[i] = gid
} else {
log.Fatalf("invalid gid %d", gid)
}
}
suppGroups[len(suppGroups)-1] = uid
} else {
suppGroups = []int{uid}
}
// careful! users in the allowlist is effectively allowed to drop groups via fsu
if err := syscall.Setresgid(uid, uid, uid); err != nil { if err := syscall.Setresgid(uid, uid, uid); err != nil {
log.Fatalf("cannot set gid: %v", err) log.Fatalf("cannot set gid: %v", err)
} }
if err := syscall.Setgroups(suppGroups); err != nil {
log.Fatalf("cannot set supplementary groups: %v", err)
}
if err := syscall.Setresuid(uid, uid, uid); err != nil { if err := syscall.Setresuid(uid, uid, uid); err != nil {
log.Fatalf("cannot set uid: %v", err) log.Fatalf("cannot set uid: %v", err)
} }
if err := syscall.Exec(fmain, []string{"fortify", "shim"}, []string{envShim + "=" + shimSetupPath}); err != nil { if _, _, errno := syscall.AllThreadsSyscall(syscall.SYS_PRCTL, PR_SET_NO_NEW_PRIVS, 1, 0); errno != 0 {
log.Fatalf("cannot set no_new_privs flag: %s", errno.Error())
}
if err := syscall.Exec(fshim, []string{"fshim"}, []string{envShim + "=" + shimSetupPath}); err != nil {
log.Fatalf("cannot start shim: %v", err) log.Fatalf("cannot start shim: %v", err)
} }
panic("unreachable") panic("unreachable")
} }
func parseConfig(p string, puid int) (fid int, ok bool) {
// refuse to run if fsurc is not protected correctly
if s, err := os.Stat(p); err != nil {
log.Fatal(err)
} else if s.Mode().Perm() != 0400 {
log.Fatal("bad fsurc perm")
} else if st := s.Sys().(*syscall.Stat_t); st.Uid != 0 || st.Gid != 0 {
log.Fatal("fsurc must be owned by uid 0")
}
if r, err := os.Open(p); err != nil {
log.Fatal(err)
return -1, false
} else {
s := bufio.NewScanner(r)
var line int
for s.Scan() {
line++
// <puid> <fid>
lf := strings.SplitN(s.Text(), " ", 2)
if len(lf) != 2 {
log.Fatalf("invalid entry on line %d", line)
}
var puid0 int
if puid0, err = strconv.Atoi(lf[0]); err != nil || puid0 < 1 {
log.Fatalf("invalid parent uid on line %d", line)
}
ok = puid0 == puid
if ok {
// allowed fid range 0 to 99
if fid, err = strconv.Atoi(lf[1]); err != nil || fid < 0 || fid > 99 {
log.Fatalf("invalid fortify uid on line %d", line)
}
return
}
}
if err = s.Err(); err != nil {
log.Fatalf("cannot read fsurc: %v", err)
}
return -1, false
}
}
func checkPath(p string) (string, bool) { func checkPath(p string) (string, bool) {
return p, p != compPoison && p != "" && path.IsAbs(p) return p, p != compPoison && p != "" && path.IsAbs(p)
} }

77
cmd/fsu/parse.go Normal file
View File

@ -0,0 +1,77 @@
package main
import (
"bufio"
"errors"
"fmt"
"log"
"os"
"strings"
"syscall"
)
func parseUint32Fast(s string) (int, error) {
sLen := len(s)
if sLen < 1 {
return -1, errors.New("zero length string")
}
if sLen > 10 {
return -1, errors.New("string too long")
}
n := 0
for i, ch := range []byte(s) {
ch -= '0'
if ch > 9 {
return -1, fmt.Errorf("invalid character '%s' at index %d", string([]byte{ch}), i)
}
n = n*10 + int(ch)
}
return n, nil
}
func parseConfig(p string, puid int) (fid int, ok bool) {
// refuse to run if fsurc is not protected correctly
if s, err := os.Stat(p); err != nil {
log.Fatal(err)
} else if s.Mode().Perm() != 0400 {
log.Fatal("bad fsurc perm")
} else if st := s.Sys().(*syscall.Stat_t); st.Uid != 0 || st.Gid != 0 {
log.Fatal("fsurc must be owned by uid 0")
}
if r, err := os.Open(p); err != nil {
log.Fatal(err)
return -1, false
} else {
s := bufio.NewScanner(r)
var line int
for s.Scan() {
line++
// <puid> <fid>
lf := strings.SplitN(s.Text(), " ", 2)
if len(lf) != 2 {
log.Fatalf("invalid entry on line %d", line)
}
var puid0 int
if puid0, err = parseUint32Fast(lf[0]); err != nil || puid0 < 1 {
log.Fatalf("invalid parent uid on line %d", line)
}
ok = puid0 == puid
if ok {
// allowed fid range 0 to 99
if fid, err = parseUint32Fast(lf[1]); err != nil || fid < 0 || fid > 99 {
log.Fatalf("invalid fortify uid on line %d", line)
}
return
}
}
if err = s.Err(); err != nil {
log.Fatalf("cannot read fsurc: %v", err)
}
return -1, false
}
}

69
cmd/fuserdb/main.go Normal file
View File

@ -0,0 +1,69 @@
package main
import (
"bytes"
"errors"
"flag"
"fmt"
"os"
"path"
"strconv"
"git.ophivana.moe/security/fortify/internal/fmsg"
)
func main() {
fmsg.SetPrefix("fuserdb")
const varEmpty = "/var/empty"
out := flag.String("o", "userdb", "output directory")
homeDir := flag.String("d", varEmpty, "parent of home directories")
shell := flag.String("s", "/sbin/nologin", "absolute path to subordinate user shell")
flag.Parse()
type user struct {
name string
fid int
}
users := make([]user, len(flag.Args()))
for i, s := range flag.Args() {
f := bytes.SplitN([]byte(s), []byte{':'}, 2)
if len(f) != 2 {
fmsg.Fatalf("invalid entry at index %d", i)
}
users[i].name = string(f[0])
if fid, err := strconv.Atoi(string(f[1])); err != nil {
fmsg.Fatal(err.Error())
} else {
users[i].fid = fid
}
}
if err := os.MkdirAll(*out, 0755); err != nil && !errors.Is(err, os.ErrExist) {
fmsg.Fatalf("cannot create output: %v", err)
}
for _, u := range users {
fidString := strconv.Itoa(u.fid)
for aid := 0; aid < 10000; aid++ {
userName := fmt.Sprintf("u%d_a%d", u.fid, aid)
uid := 1000000 + u.fid*10000 + aid
us := strconv.Itoa(uid)
realName := fmt.Sprintf("Fortify subordinate user %d (%s)", aid, u.name)
var homeDirectory string
if *homeDir != varEmpty {
homeDirectory = path.Join(*homeDir, fidString, strconv.Itoa(aid))
} else {
homeDirectory = varEmpty
}
writeUser(userName, uid, us, realName, homeDirectory, *shell, *out)
writeGroup(userName, uid, us, nil, *out)
}
}
fmsg.Printf("created %d entries", len(users)*2*10000)
fmsg.Exit(0)
}

64
cmd/fuserdb/payload.go Normal file
View File

@ -0,0 +1,64 @@
package main
import (
"encoding/json"
"os"
"path"
"git.ophivana.moe/security/fortify/internal/fmsg"
)
type payloadU struct {
UserName string `json:"userName"`
Uid int `json:"uid"`
Gid int `json:"gid"`
MemberOf []string `json:"memberOf,omitempty"`
RealName string `json:"realName"`
HomeDirectory string `json:"homeDirectory"`
Shell string `json:"shell"`
}
func writeUser(userName string, uid int, us string, realName, homeDirectory, shell string, out string) {
userFileName := userName + ".user"
if f, err := os.OpenFile(path.Join(out, userFileName), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644); err != nil {
fmsg.Fatalf("cannot create %s: %v", userName, err)
} else if err = json.NewEncoder(f).Encode(&payloadU{
UserName: userName,
Uid: uid,
Gid: uid,
RealName: realName,
HomeDirectory: homeDirectory,
Shell: shell,
}); err != nil {
fmsg.Fatalf("cannot serialise %s: %v", userName, err)
} else if err = f.Close(); err != nil {
fmsg.Printf("cannot close %s: %v", userName, err)
}
if err := os.Symlink(userFileName, path.Join(out, us+".user")); err != nil {
fmsg.Fatalf("cannot link %s: %v", userName, err)
}
}
type payloadG struct {
GroupName string `json:"groupName"`
Gid int `json:"gid"`
Members []string `json:"members,omitempty"`
}
func writeGroup(groupName string, gid int, gs string, members []string, out string) {
groupFileName := groupName + ".group"
if f, err := os.OpenFile(path.Join(out, groupFileName), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644); err != nil {
fmsg.Fatalf("cannot create %s: %v", groupName, err)
} else if err = json.NewEncoder(f).Encode(&payloadG{
GroupName: groupName,
Gid: gid,
Members: members,
}); err != nil {
fmsg.Fatalf("cannot serialise %s: %v", groupName, err)
} else if err = f.Close(); err != nil {
fmsg.Printf("cannot close %s: %v", groupName, err)
}
if err := os.Symlink(groupFileName, path.Join(out, gs+".group")); err != nil {
fmsg.Fatalf("cannot link %s: %v", groupName, err)
}
}

135
config.go
View File

@ -1,135 +0,0 @@
package main
import (
"encoding/json"
"flag"
"fmt"
"git.ophivana.moe/security/fortify/dbus"
"git.ophivana.moe/security/fortify/internal/app"
"git.ophivana.moe/security/fortify/internal/fmsg"
"git.ophivana.moe/security/fortify/internal/system"
)
var (
printTemplate bool
confPath string
dbusConfigSession string
dbusConfigSystem string
dbusID string
mpris bool
dbusVerbose bool
userName string
enablements [system.ELen]bool
launchMethodText string
)
func init() {
flag.BoolVar(&printTemplate, "template", false, "Print a full config template and exit")
// config file, disables every other flag here
flag.StringVar(&confPath, "c", "nil", "Path to full app configuration, or \"nil\" to configure from flags")
flag.StringVar(&dbusConfigSession, "dbus-config", "builtin", "Path to D-Bus proxy config file, or \"builtin\" for defaults")
flag.StringVar(&dbusConfigSystem, "dbus-system", "nil", "Path to system D-Bus proxy config file, or \"nil\" to disable")
flag.StringVar(&dbusID, "dbus-id", "", "D-Bus ID of application, leave empty to disable own paths, has no effect if custom config is available")
flag.BoolVar(&mpris, "mpris", false, "Allow owning MPRIS D-Bus path, has no effect if custom config is available")
flag.BoolVar(&dbusVerbose, "dbus-log", false, "Force logging in the D-Bus proxy")
flag.StringVar(&userName, "u", "chronos", "Passwd name of user to run as")
flag.BoolVar(&enablements[system.EWayland], "wayland", false, "Share Wayland socket")
flag.BoolVar(&enablements[system.EX11], "X", false, "Share X11 socket and allow connection")
flag.BoolVar(&enablements[system.EDBus], "dbus", false, "Proxy D-Bus connection")
flag.BoolVar(&enablements[system.EPulse], "pulse", false, "Share PulseAudio socket and cookie")
}
func init() {
methodHelpString := "Method of launching the child process, can be one of \"sudo\""
if os.SdBooted() {
methodHelpString += ", \"systemd\""
}
flag.StringVar(&launchMethodText, "method", "sudo", methodHelpString)
}
func tryTemplate() {
if printTemplate {
if s, err := json.MarshalIndent(app.Template(), "", " "); err != nil {
fmsg.Fatalf("cannot generate template: %v", err)
panic("unreachable")
} else {
fmt.Println(string(s))
}
fmsg.Exit(0)
}
}
func loadConfig() *app.Config {
if confPath == "nil" {
// config from flags
return configFromFlags()
} else {
// config from file
c := new(app.Config)
if f, err := os.Open(confPath); err != nil {
fmsg.Fatalf("cannot access config file %q: %s", confPath, err)
panic("unreachable")
} else if err = json.NewDecoder(f).Decode(&c); err != nil {
fmsg.Fatalf("cannot parse config file %q: %s", confPath, err)
panic("unreachable")
} else {
return c
}
}
}
func configFromFlags() (config *app.Config) {
// initialise config from flags
config = &app.Config{
ID: dbusID,
User: userName,
Command: flag.Args(),
Method: launchMethodText,
}
// enablements from flags
for i := system.Enablement(0); i < system.Enablement(system.ELen); i++ {
if enablements[i] {
config.Confinement.Enablements.Set(i)
}
}
// parse D-Bus config file from flags if applicable
if enablements[system.EDBus] {
if dbusConfigSession == "builtin" {
config.Confinement.SessionBus = dbus.NewConfig(dbusID, true, mpris)
} else {
if c, err := dbus.NewConfigFromFile(dbusConfigSession); err != nil {
fmsg.Fatalf("cannot load session bus proxy config from %q: %s", dbusConfigSession, err)
} else {
config.Confinement.SessionBus = c
}
}
// system bus proxy is optional
if dbusConfigSystem != "nil" {
if c, err := dbus.NewConfigFromFile(dbusConfigSystem); err != nil {
fmsg.Fatalf("cannot load system bus proxy config from %q: %s", dbusConfigSystem, err)
} else {
config.Confinement.SystemBus = c
}
}
// override log from configuration
if dbusVerbose {
config.Confinement.SessionBus.Log = true
config.Confinement.SystemBus.Log = true
}
}
return
}

View File

@ -42,6 +42,33 @@
with nixpkgsFor.${system}; with nixpkgsFor.${system};
self.packages.${system}.fortify.buildInputs ++ [ self.packages.${system}.fortify ]; self.packages.${system}.fortify.buildInputs ++ [ self.packages.${system}.fortify ];
}; };
generateDoc =
let
pkgs = nixpkgsFor.${system};
inherit (pkgs) lib;
doc =
let
eval = lib.evalModules {
specialArgs = {
inherit pkgs;
};
modules = [ ./options.nix ];
};
cleanEval = lib.filterAttrsRecursive (n: v: n != "_module") eval;
in
pkgs.nixosOptionsDoc { inherit (cleanEval) options; };
docText = pkgs.runCommand "fortify-module-docs.md" { } ''
cat ${doc.optionsCommonMark} > $out
sed -i '/*Declared by:*/,+1 d' $out
'';
in
nixpkgsFor.${system}.mkShell {
shellHook = ''
exec cat ${docText} > options.md
'';
};
}); });
}; };
} }

View File

@ -53,7 +53,7 @@ func (a *app) String() string {
} }
if a.seal != nil { if a.seal != nil {
return "(sealed fortified app as uid " + a.seal.sys.user.Uid + ")" return "(sealed fortified app as uid " + a.seal.sys.user.us + ")"
} }
return "(unsealed fortified app)" return "(unsealed fortified app)"

View File

@ -2,6 +2,7 @@ package app_test
import ( import (
"fmt" "fmt"
"io"
"io/fs" "io/fs"
"os/user" "os/user"
"strconv" "strconv"
@ -18,9 +19,12 @@ var testCasesNixos = []sealTestCase{
{ {
"nixos permissive defaults no enablements", new(stubNixOS), "nixos permissive defaults no enablements", new(stubNixOS),
&app.Config{ &app.Config{
User: "chronos",
Command: make([]string, 0), Command: make([]string, 0),
Method: "sudo", Confinement: app.ConfinementConfig{
AppID: 0,
Username: "chronos",
Outer: "/home/chronos",
},
}, },
app.ID{ app.ID{
0x4a, 0x45, 0x0b, 0x65, 0x4a, 0x45, 0x0b, 0x65,
@ -28,11 +32,11 @@ var testCasesNixos = []sealTestCase{
0xbd, 0x01, 0x78, 0x0e, 0xbd, 0x01, 0x78, 0x0e,
0xb9, 0xa6, 0x07, 0xac, 0xb9, 0xa6, 0x07, 0xac,
}, },
system.New(150). system.New(1000000).
Ensure("/tmp/fortify.1971", 0701). Ensure("/tmp/fortify.1971", 0711).
Ephemeral(system.Process, "/tmp/fortify.1971/4a450b6596d7bc15bd01780eb9a607ac", 0701). Ephemeral(system.Process, "/tmp/fortify.1971/4a450b6596d7bc15bd01780eb9a607ac", 0711).
Ensure("/tmp/fortify.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir", acl.Execute). Ensure("/tmp/fortify.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir", acl.Execute).
Ensure("/tmp/fortify.1971/tmpdir/150", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/150", acl.Read, acl.Write, acl.Execute). Ensure("/tmp/fortify.1971/tmpdir/0", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/0", acl.Read, acl.Write, acl.Execute).
Ensure("/run/user/1971/fortify", 0700).UpdatePermType(system.User, "/run/user/1971/fortify", acl.Execute). Ensure("/run/user/1971/fortify", 0700).UpdatePermType(system.User, "/run/user/1971/fortify", acl.Execute).
Ensure("/run/user/1971", 0700).UpdatePermType(system.User, "/run/user/1971", acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset Ensure("/run/user/1971", 0700).UpdatePermType(system.User, "/run/user/1971", acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset
Ephemeral(system.Process, "/run/user/1971/fortify/4a450b6596d7bc15bd01780eb9a607ac", 0700).UpdatePermType(system.Process, "/run/user/1971/fortify/4a450b6596d7bc15bd01780eb9a607ac", acl.Execute). Ephemeral(system.Process, "/run/user/1971/fortify/4a450b6596d7bc15bd01780eb9a607ac", 0700).UpdatePermType(system.Process, "/run/user/1971/fortify/4a450b6596d7bc15bd01780eb9a607ac", acl.Execute).
@ -42,6 +46,7 @@ var testCasesNixos = []sealTestCase{
Net: true, Net: true,
UserNS: true, UserNS: true,
Clearenv: true, Clearenv: true,
Chdir: "/home/chronos",
SetEnv: map[string]string{ SetEnv: map[string]string{
"HOME": "/home/chronos", "HOME": "/home/chronos",
"SHELL": "/run/current-system/sw/bin/zsh", "SHELL": "/run/current-system/sw/bin/zsh",
@ -54,11 +59,11 @@ var testCasesNixos = []sealTestCase{
DieWithParent: true, DieWithParent: true,
AsInit: true, AsInit: true,
}).SetUID(65534).SetGID(65534). }).SetUID(65534).SetGID(65534).
Procfs("/proc").DevTmpfs("/dev").Mqueue("/dev/mqueue"). Procfs("/proc").
Tmpfs("/dev/fortify", 4096). Tmpfs("/fortify", 4096).
DevTmpfs("/dev").Mqueue("/dev/mqueue").
Bind("/bin", "/bin", false, true). Bind("/bin", "/bin", false, true).
Bind("/boot", "/boot", false, true). Bind("/boot", "/boot", false, true).
Bind("/etc", "/dev/fortify/etc").
Bind("/home", "/home", false, true). Bind("/home", "/home", false, true).
Bind("/lib", "/lib", false, true). Bind("/lib", "/lib", false, true).
Bind("/lib64", "/lib64", false, true). Bind("/lib64", "/lib64", false, true).
@ -102,88 +107,90 @@ var testCasesNixos = []sealTestCase{
Bind("/run/wrappers", "/run/wrappers", false, true). Bind("/run/wrappers", "/run/wrappers", false, true).
Bind("/run/zed.pid", "/run/zed.pid", false, true). Bind("/run/zed.pid", "/run/zed.pid", false, true).
Bind("/run/zed.state", "/run/zed.state", false, true). Bind("/run/zed.state", "/run/zed.state", false, true).
Symlink("/dev/fortify/etc/alsa", "/etc/alsa"). Bind("/etc", "/fortify/etc").
Symlink("/dev/fortify/etc/bashrc", "/etc/bashrc"). Symlink("/fortify/etc/alsa", "/etc/alsa").
Symlink("/dev/fortify/etc/binfmt.d", "/etc/binfmt.d"). Symlink("/fortify/etc/bashrc", "/etc/bashrc").
Symlink("/dev/fortify/etc/dbus-1", "/etc/dbus-1"). Symlink("/fortify/etc/binfmt.d", "/etc/binfmt.d").
Symlink("/dev/fortify/etc/default", "/etc/default"). Symlink("/fortify/etc/dbus-1", "/etc/dbus-1").
Symlink("/dev/fortify/etc/ethertypes", "/etc/ethertypes"). Symlink("/fortify/etc/default", "/etc/default").
Symlink("/dev/fortify/etc/fonts", "/etc/fonts"). Symlink("/fortify/etc/ethertypes", "/etc/ethertypes").
Symlink("/dev/fortify/etc/fstab", "/etc/fstab"). Symlink("/fortify/etc/fonts", "/etc/fonts").
Symlink("/dev/fortify/etc/fuse.conf", "/etc/fuse.conf"). Symlink("/fortify/etc/fstab", "/etc/fstab").
Symlink("/dev/fortify/etc/host.conf", "/etc/host.conf"). Symlink("/fortify/etc/fuse.conf", "/etc/fuse.conf").
Symlink("/dev/fortify/etc/hostid", "/etc/hostid"). Symlink("/fortify/etc/host.conf", "/etc/host.conf").
Symlink("/dev/fortify/etc/hostname", "/etc/hostname"). Symlink("/fortify/etc/hostid", "/etc/hostid").
Symlink("/dev/fortify/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM"). Symlink("/fortify/etc/hostname", "/etc/hostname").
Symlink("/dev/fortify/etc/hosts", "/etc/hosts"). Symlink("/fortify/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM").
Symlink("/dev/fortify/etc/inputrc", "/etc/inputrc"). Symlink("/fortify/etc/hosts", "/etc/hosts").
Symlink("/dev/fortify/etc/ipsec.d", "/etc/ipsec.d"). Symlink("/fortify/etc/inputrc", "/etc/inputrc").
Symlink("/dev/fortify/etc/issue", "/etc/issue"). Symlink("/fortify/etc/ipsec.d", "/etc/ipsec.d").
Symlink("/dev/fortify/etc/kbd", "/etc/kbd"). Symlink("/fortify/etc/issue", "/etc/issue").
Symlink("/dev/fortify/etc/libblockdev", "/etc/libblockdev"). Symlink("/fortify/etc/kbd", "/etc/kbd").
Symlink("/dev/fortify/etc/locale.conf", "/etc/locale.conf"). Symlink("/fortify/etc/libblockdev", "/etc/libblockdev").
Symlink("/dev/fortify/etc/localtime", "/etc/localtime"). Symlink("/fortify/etc/locale.conf", "/etc/locale.conf").
Symlink("/dev/fortify/etc/login.defs", "/etc/login.defs"). Symlink("/fortify/etc/localtime", "/etc/localtime").
Symlink("/dev/fortify/etc/lsb-release", "/etc/lsb-release"). Symlink("/fortify/etc/login.defs", "/etc/login.defs").
Symlink("/dev/fortify/etc/lvm", "/etc/lvm"). Symlink("/fortify/etc/lsb-release", "/etc/lsb-release").
Symlink("/dev/fortify/etc/machine-id", "/etc/machine-id"). Symlink("/fortify/etc/lvm", "/etc/lvm").
Symlink("/dev/fortify/etc/man_db.conf", "/etc/man_db.conf"). Symlink("/fortify/etc/machine-id", "/etc/machine-id").
Symlink("/dev/fortify/etc/modprobe.d", "/etc/modprobe.d"). Symlink("/fortify/etc/man_db.conf", "/etc/man_db.conf").
Symlink("/dev/fortify/etc/modules-load.d", "/etc/modules-load.d"). Symlink("/fortify/etc/modprobe.d", "/etc/modprobe.d").
Symlink("/fortify/etc/modules-load.d", "/etc/modules-load.d").
Symlink("/proc/mounts", "/etc/mtab"). Symlink("/proc/mounts", "/etc/mtab").
Symlink("/dev/fortify/etc/nanorc", "/etc/nanorc"). Symlink("/fortify/etc/nanorc", "/etc/nanorc").
Symlink("/dev/fortify/etc/netgroup", "/etc/netgroup"). Symlink("/fortify/etc/netgroup", "/etc/netgroup").
Symlink("/dev/fortify/etc/NetworkManager", "/etc/NetworkManager"). Symlink("/fortify/etc/NetworkManager", "/etc/NetworkManager").
Symlink("/dev/fortify/etc/nix", "/etc/nix"). Symlink("/fortify/etc/nix", "/etc/nix").
Symlink("/dev/fortify/etc/nixos", "/etc/nixos"). Symlink("/fortify/etc/nixos", "/etc/nixos").
Symlink("/dev/fortify/etc/NIXOS", "/etc/NIXOS"). Symlink("/fortify/etc/NIXOS", "/etc/NIXOS").
Symlink("/dev/fortify/etc/nscd.conf", "/etc/nscd.conf"). Symlink("/fortify/etc/nscd.conf", "/etc/nscd.conf").
Symlink("/dev/fortify/etc/nsswitch.conf", "/etc/nsswitch.conf"). Symlink("/fortify/etc/nsswitch.conf", "/etc/nsswitch.conf").
Symlink("/dev/fortify/etc/opensnitchd", "/etc/opensnitchd"). Symlink("/fortify/etc/opensnitchd", "/etc/opensnitchd").
Symlink("/dev/fortify/etc/os-release", "/etc/os-release"). Symlink("/fortify/etc/os-release", "/etc/os-release").
Symlink("/dev/fortify/etc/pam", "/etc/pam"). Symlink("/fortify/etc/pam", "/etc/pam").
Symlink("/dev/fortify/etc/pam.d", "/etc/pam.d"). Symlink("/fortify/etc/pam.d", "/etc/pam.d").
Symlink("/dev/fortify/etc/pipewire", "/etc/pipewire"). Symlink("/fortify/etc/pipewire", "/etc/pipewire").
Symlink("/dev/fortify/etc/pki", "/etc/pki"). Symlink("/fortify/etc/pki", "/etc/pki").
Symlink("/dev/fortify/etc/polkit-1", "/etc/polkit-1"). Symlink("/fortify/etc/polkit-1", "/etc/polkit-1").
Symlink("/dev/fortify/etc/profile", "/etc/profile"). Symlink("/fortify/etc/profile", "/etc/profile").
Symlink("/dev/fortify/etc/protocols", "/etc/protocols"). Symlink("/fortify/etc/protocols", "/etc/protocols").
Symlink("/dev/fortify/etc/qemu", "/etc/qemu"). Symlink("/fortify/etc/qemu", "/etc/qemu").
Symlink("/dev/fortify/etc/resolv.conf", "/etc/resolv.conf"). Symlink("/fortify/etc/resolv.conf", "/etc/resolv.conf").
Symlink("/dev/fortify/etc/resolvconf.conf", "/etc/resolvconf.conf"). Symlink("/fortify/etc/resolvconf.conf", "/etc/resolvconf.conf").
Symlink("/dev/fortify/etc/rpc", "/etc/rpc"). Symlink("/fortify/etc/rpc", "/etc/rpc").
Symlink("/dev/fortify/etc/samba", "/etc/samba"). Symlink("/fortify/etc/samba", "/etc/samba").
Symlink("/dev/fortify/etc/sddm.conf", "/etc/sddm.conf"). Symlink("/fortify/etc/sddm.conf", "/etc/sddm.conf").
Symlink("/dev/fortify/etc/secureboot", "/etc/secureboot"). Symlink("/fortify/etc/secureboot", "/etc/secureboot").
Symlink("/dev/fortify/etc/services", "/etc/services"). Symlink("/fortify/etc/services", "/etc/services").
Symlink("/dev/fortify/etc/set-environment", "/etc/set-environment"). Symlink("/fortify/etc/set-environment", "/etc/set-environment").
Symlink("/dev/fortify/etc/shadow", "/etc/shadow"). Symlink("/fortify/etc/shadow", "/etc/shadow").
Symlink("/dev/fortify/etc/shells", "/etc/shells"). Symlink("/fortify/etc/shells", "/etc/shells").
Symlink("/dev/fortify/etc/ssh", "/etc/ssh"). Symlink("/fortify/etc/ssh", "/etc/ssh").
Symlink("/dev/fortify/etc/ssl", "/etc/ssl"). Symlink("/fortify/etc/ssl", "/etc/ssl").
Symlink("/dev/fortify/etc/static", "/etc/static"). Symlink("/fortify/etc/static", "/etc/static").
Symlink("/dev/fortify/etc/subgid", "/etc/subgid"). Symlink("/fortify/etc/subgid", "/etc/subgid").
Symlink("/dev/fortify/etc/subuid", "/etc/subuid"). Symlink("/fortify/etc/subuid", "/etc/subuid").
Symlink("/dev/fortify/etc/sudoers", "/etc/sudoers"). Symlink("/fortify/etc/sudoers", "/etc/sudoers").
Symlink("/dev/fortify/etc/sysctl.d", "/etc/sysctl.d"). Symlink("/fortify/etc/sysctl.d", "/etc/sysctl.d").
Symlink("/dev/fortify/etc/systemd", "/etc/systemd"). Symlink("/fortify/etc/systemd", "/etc/systemd").
Symlink("/dev/fortify/etc/terminfo", "/etc/terminfo"). Symlink("/fortify/etc/terminfo", "/etc/terminfo").
Symlink("/dev/fortify/etc/tmpfiles.d", "/etc/tmpfiles.d"). Symlink("/fortify/etc/tmpfiles.d", "/etc/tmpfiles.d").
Symlink("/dev/fortify/etc/udev", "/etc/udev"). Symlink("/fortify/etc/udev", "/etc/udev").
Symlink("/dev/fortify/etc/udisks2", "/etc/udisks2"). Symlink("/fortify/etc/udisks2", "/etc/udisks2").
Symlink("/dev/fortify/etc/UPower", "/etc/UPower"). Symlink("/fortify/etc/UPower", "/etc/UPower").
Symlink("/dev/fortify/etc/vconsole.conf", "/etc/vconsole.conf"). Symlink("/fortify/etc/vconsole.conf", "/etc/vconsole.conf").
Symlink("/dev/fortify/etc/X11", "/etc/X11"). Symlink("/fortify/etc/X11", "/etc/X11").
Symlink("/dev/fortify/etc/zfs", "/etc/zfs"). Symlink("/fortify/etc/zfs", "/etc/zfs").
Symlink("/dev/fortify/etc/zinputrc", "/etc/zinputrc"). Symlink("/fortify/etc/zinputrc", "/etc/zinputrc").
Symlink("/dev/fortify/etc/zoneinfo", "/etc/zoneinfo"). Symlink("/fortify/etc/zoneinfo", "/etc/zoneinfo").
Symlink("/dev/fortify/etc/zprofile", "/etc/zprofile"). Symlink("/fortify/etc/zprofile", "/etc/zprofile").
Symlink("/dev/fortify/etc/zshenv", "/etc/zshenv"). Symlink("/fortify/etc/zshenv", "/etc/zshenv").
Symlink("/dev/fortify/etc/zshrc", "/etc/zshrc"). Symlink("/fortify/etc/zshrc", "/etc/zshrc").
Bind("/tmp/fortify.1971/tmpdir/150", "/tmp", false, true). Bind("/tmp/fortify.1971/tmpdir/0", "/tmp", false, true).
Tmpfs("/tmp/fortify.1971", 1048576). Tmpfs("/tmp/fortify.1971", 1048576).
Tmpfs("/run/user", 1048576). Tmpfs("/run/user", 1048576).
Tmpfs("/run/user/65534", 8388608). Tmpfs("/run/user/65534", 8388608).
Bind("/home/chronos", "/home/chronos", false, true).
Bind("/tmp/fortify.1971/4a450b6596d7bc15bd01780eb9a607ac/passwd", "/etc/passwd"). Bind("/tmp/fortify.1971/4a450b6596d7bc15bd01780eb9a607ac/passwd", "/etc/passwd").
Bind("/tmp/fortify.1971/4a450b6596d7bc15bd01780eb9a607ac/group", "/etc/group"). Bind("/tmp/fortify.1971/4a450b6596d7bc15bd01780eb9a607ac/group", "/etc/group").
Tmpfs("/var/run/nscd", 8192), Tmpfs("/var/run/nscd", 8192),
@ -192,9 +199,12 @@ var testCasesNixos = []sealTestCase{
"nixos permissive defaults chromium", new(stubNixOS), "nixos permissive defaults chromium", new(stubNixOS),
&app.Config{ &app.Config{
ID: "org.chromium.Chromium", ID: "org.chromium.Chromium",
User: "chronos",
Command: []string{"/run/current-system/sw/bin/zsh", "-c", "exec chromium "}, Command: []string{"/run/current-system/sw/bin/zsh", "-c", "exec chromium "},
Confinement: app.ConfinementConfig{ Confinement: app.ConfinementConfig{
AppID: 9,
Groups: []string{"video"},
Username: "chronos",
Outer: "/home/chronos",
SessionBus: &dbus.Config{ SessionBus: &dbus.Config{
Talk: []string{ Talk: []string{
"org.freedesktop.Notifications", "org.freedesktop.Notifications",
@ -228,7 +238,6 @@ var testCasesNixos = []sealTestCase{
}, },
Enablements: system.EWayland.Mask() | system.EDBus.Mask() | system.EPulse.Mask(), Enablements: system.EWayland.Mask() | system.EDBus.Mask() | system.EPulse.Mask(),
}, },
Method: "systemd",
}, },
app.ID{ app.ID{
0xeb, 0xf0, 0x83, 0xd1, 0xeb, 0xf0, 0x83, 0xd1,
@ -236,11 +245,11 @@ var testCasesNixos = []sealTestCase{
0x82, 0xd4, 0x13, 0x36, 0x82, 0xd4, 0x13, 0x36,
0x9b, 0x64, 0xce, 0x7c, 0x9b, 0x64, 0xce, 0x7c,
}, },
system.New(150). system.New(1000009).
Ensure("/tmp/fortify.1971", 0701). Ensure("/tmp/fortify.1971", 0711).
Ephemeral(system.Process, "/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c", 0701). Ephemeral(system.Process, "/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c", 0711).
Ensure("/tmp/fortify.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir", acl.Execute). Ensure("/tmp/fortify.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir", acl.Execute).
Ensure("/tmp/fortify.1971/tmpdir/150", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/150", acl.Read, acl.Write, acl.Execute). Ensure("/tmp/fortify.1971/tmpdir/9", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/9", acl.Read, acl.Write, acl.Execute).
Ensure("/run/user/1971/fortify", 0700).UpdatePermType(system.User, "/run/user/1971/fortify", acl.Execute). Ensure("/run/user/1971/fortify", 0700).UpdatePermType(system.User, "/run/user/1971/fortify", acl.Execute).
Ensure("/run/user/1971", 0700).UpdatePermType(system.User, "/run/user/1971", acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset Ensure("/run/user/1971", 0700).UpdatePermType(system.User, "/run/user/1971", acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset
Ephemeral(system.Process, "/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c", 0700).UpdatePermType(system.Process, "/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c", acl.Execute). Ephemeral(system.Process, "/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c", 0700).UpdatePermType(system.Process, "/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c", acl.Execute).
@ -285,6 +294,7 @@ var testCasesNixos = []sealTestCase{
(&bwrap.Config{ (&bwrap.Config{
Net: true, Net: true,
UserNS: true, UserNS: true,
Chdir: "/home/chronos",
Clearenv: true, Clearenv: true,
SetEnv: map[string]string{ SetEnv: map[string]string{
"DBUS_SESSION_BUS_ADDRESS": "unix:path=/run/user/65534/bus", "DBUS_SESSION_BUS_ADDRESS": "unix:path=/run/user/65534/bus",
@ -304,11 +314,11 @@ var testCasesNixos = []sealTestCase{
DieWithParent: true, DieWithParent: true,
AsInit: true, AsInit: true,
}).SetUID(65534).SetGID(65534). }).SetUID(65534).SetGID(65534).
Procfs("/proc").DevTmpfs("/dev").Mqueue("/dev/mqueue"). Procfs("/proc").
Tmpfs("/dev/fortify", 4096). Tmpfs("/fortify", 4096).
DevTmpfs("/dev").Mqueue("/dev/mqueue").
Bind("/bin", "/bin", false, true). Bind("/bin", "/bin", false, true).
Bind("/boot", "/boot", false, true). Bind("/boot", "/boot", false, true).
Bind("/etc", "/dev/fortify/etc").
Bind("/home", "/home", false, true). Bind("/home", "/home", false, true).
Bind("/lib", "/lib", false, true). Bind("/lib", "/lib", false, true).
Bind("/lib64", "/lib64", false, true). Bind("/lib64", "/lib64", false, true).
@ -353,88 +363,90 @@ var testCasesNixos = []sealTestCase{
Bind("/run/zed.pid", "/run/zed.pid", false, true). Bind("/run/zed.pid", "/run/zed.pid", false, true).
Bind("/run/zed.state", "/run/zed.state", false, true). Bind("/run/zed.state", "/run/zed.state", false, true).
Bind("/dev/dri", "/dev/dri", true, true, true). Bind("/dev/dri", "/dev/dri", true, true, true).
Symlink("/dev/fortify/etc/alsa", "/etc/alsa"). Bind("/etc", "/fortify/etc").
Symlink("/dev/fortify/etc/bashrc", "/etc/bashrc"). Symlink("/fortify/etc/alsa", "/etc/alsa").
Symlink("/dev/fortify/etc/binfmt.d", "/etc/binfmt.d"). Symlink("/fortify/etc/bashrc", "/etc/bashrc").
Symlink("/dev/fortify/etc/dbus-1", "/etc/dbus-1"). Symlink("/fortify/etc/binfmt.d", "/etc/binfmt.d").
Symlink("/dev/fortify/etc/default", "/etc/default"). Symlink("/fortify/etc/dbus-1", "/etc/dbus-1").
Symlink("/dev/fortify/etc/ethertypes", "/etc/ethertypes"). Symlink("/fortify/etc/default", "/etc/default").
Symlink("/dev/fortify/etc/fonts", "/etc/fonts"). Symlink("/fortify/etc/ethertypes", "/etc/ethertypes").
Symlink("/dev/fortify/etc/fstab", "/etc/fstab"). Symlink("/fortify/etc/fonts", "/etc/fonts").
Symlink("/dev/fortify/etc/fuse.conf", "/etc/fuse.conf"). Symlink("/fortify/etc/fstab", "/etc/fstab").
Symlink("/dev/fortify/etc/host.conf", "/etc/host.conf"). Symlink("/fortify/etc/fuse.conf", "/etc/fuse.conf").
Symlink("/dev/fortify/etc/hostid", "/etc/hostid"). Symlink("/fortify/etc/host.conf", "/etc/host.conf").
Symlink("/dev/fortify/etc/hostname", "/etc/hostname"). Symlink("/fortify/etc/hostid", "/etc/hostid").
Symlink("/dev/fortify/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM"). Symlink("/fortify/etc/hostname", "/etc/hostname").
Symlink("/dev/fortify/etc/hosts", "/etc/hosts"). Symlink("/fortify/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM").
Symlink("/dev/fortify/etc/inputrc", "/etc/inputrc"). Symlink("/fortify/etc/hosts", "/etc/hosts").
Symlink("/dev/fortify/etc/ipsec.d", "/etc/ipsec.d"). Symlink("/fortify/etc/inputrc", "/etc/inputrc").
Symlink("/dev/fortify/etc/issue", "/etc/issue"). Symlink("/fortify/etc/ipsec.d", "/etc/ipsec.d").
Symlink("/dev/fortify/etc/kbd", "/etc/kbd"). Symlink("/fortify/etc/issue", "/etc/issue").
Symlink("/dev/fortify/etc/libblockdev", "/etc/libblockdev"). Symlink("/fortify/etc/kbd", "/etc/kbd").
Symlink("/dev/fortify/etc/locale.conf", "/etc/locale.conf"). Symlink("/fortify/etc/libblockdev", "/etc/libblockdev").
Symlink("/dev/fortify/etc/localtime", "/etc/localtime"). Symlink("/fortify/etc/locale.conf", "/etc/locale.conf").
Symlink("/dev/fortify/etc/login.defs", "/etc/login.defs"). Symlink("/fortify/etc/localtime", "/etc/localtime").
Symlink("/dev/fortify/etc/lsb-release", "/etc/lsb-release"). Symlink("/fortify/etc/login.defs", "/etc/login.defs").
Symlink("/dev/fortify/etc/lvm", "/etc/lvm"). Symlink("/fortify/etc/lsb-release", "/etc/lsb-release").
Symlink("/dev/fortify/etc/machine-id", "/etc/machine-id"). Symlink("/fortify/etc/lvm", "/etc/lvm").
Symlink("/dev/fortify/etc/man_db.conf", "/etc/man_db.conf"). Symlink("/fortify/etc/machine-id", "/etc/machine-id").
Symlink("/dev/fortify/etc/modprobe.d", "/etc/modprobe.d"). Symlink("/fortify/etc/man_db.conf", "/etc/man_db.conf").
Symlink("/dev/fortify/etc/modules-load.d", "/etc/modules-load.d"). Symlink("/fortify/etc/modprobe.d", "/etc/modprobe.d").
Symlink("/fortify/etc/modules-load.d", "/etc/modules-load.d").
Symlink("/proc/mounts", "/etc/mtab"). Symlink("/proc/mounts", "/etc/mtab").
Symlink("/dev/fortify/etc/nanorc", "/etc/nanorc"). Symlink("/fortify/etc/nanorc", "/etc/nanorc").
Symlink("/dev/fortify/etc/netgroup", "/etc/netgroup"). Symlink("/fortify/etc/netgroup", "/etc/netgroup").
Symlink("/dev/fortify/etc/NetworkManager", "/etc/NetworkManager"). Symlink("/fortify/etc/NetworkManager", "/etc/NetworkManager").
Symlink("/dev/fortify/etc/nix", "/etc/nix"). Symlink("/fortify/etc/nix", "/etc/nix").
Symlink("/dev/fortify/etc/nixos", "/etc/nixos"). Symlink("/fortify/etc/nixos", "/etc/nixos").
Symlink("/dev/fortify/etc/NIXOS", "/etc/NIXOS"). Symlink("/fortify/etc/NIXOS", "/etc/NIXOS").
Symlink("/dev/fortify/etc/nscd.conf", "/etc/nscd.conf"). Symlink("/fortify/etc/nscd.conf", "/etc/nscd.conf").
Symlink("/dev/fortify/etc/nsswitch.conf", "/etc/nsswitch.conf"). Symlink("/fortify/etc/nsswitch.conf", "/etc/nsswitch.conf").
Symlink("/dev/fortify/etc/opensnitchd", "/etc/opensnitchd"). Symlink("/fortify/etc/opensnitchd", "/etc/opensnitchd").
Symlink("/dev/fortify/etc/os-release", "/etc/os-release"). Symlink("/fortify/etc/os-release", "/etc/os-release").
Symlink("/dev/fortify/etc/pam", "/etc/pam"). Symlink("/fortify/etc/pam", "/etc/pam").
Symlink("/dev/fortify/etc/pam.d", "/etc/pam.d"). Symlink("/fortify/etc/pam.d", "/etc/pam.d").
Symlink("/dev/fortify/etc/pipewire", "/etc/pipewire"). Symlink("/fortify/etc/pipewire", "/etc/pipewire").
Symlink("/dev/fortify/etc/pki", "/etc/pki"). Symlink("/fortify/etc/pki", "/etc/pki").
Symlink("/dev/fortify/etc/polkit-1", "/etc/polkit-1"). Symlink("/fortify/etc/polkit-1", "/etc/polkit-1").
Symlink("/dev/fortify/etc/profile", "/etc/profile"). Symlink("/fortify/etc/profile", "/etc/profile").
Symlink("/dev/fortify/etc/protocols", "/etc/protocols"). Symlink("/fortify/etc/protocols", "/etc/protocols").
Symlink("/dev/fortify/etc/qemu", "/etc/qemu"). Symlink("/fortify/etc/qemu", "/etc/qemu").
Symlink("/dev/fortify/etc/resolv.conf", "/etc/resolv.conf"). Symlink("/fortify/etc/resolv.conf", "/etc/resolv.conf").
Symlink("/dev/fortify/etc/resolvconf.conf", "/etc/resolvconf.conf"). Symlink("/fortify/etc/resolvconf.conf", "/etc/resolvconf.conf").
Symlink("/dev/fortify/etc/rpc", "/etc/rpc"). Symlink("/fortify/etc/rpc", "/etc/rpc").
Symlink("/dev/fortify/etc/samba", "/etc/samba"). Symlink("/fortify/etc/samba", "/etc/samba").
Symlink("/dev/fortify/etc/sddm.conf", "/etc/sddm.conf"). Symlink("/fortify/etc/sddm.conf", "/etc/sddm.conf").
Symlink("/dev/fortify/etc/secureboot", "/etc/secureboot"). Symlink("/fortify/etc/secureboot", "/etc/secureboot").
Symlink("/dev/fortify/etc/services", "/etc/services"). Symlink("/fortify/etc/services", "/etc/services").
Symlink("/dev/fortify/etc/set-environment", "/etc/set-environment"). Symlink("/fortify/etc/set-environment", "/etc/set-environment").
Symlink("/dev/fortify/etc/shadow", "/etc/shadow"). Symlink("/fortify/etc/shadow", "/etc/shadow").
Symlink("/dev/fortify/etc/shells", "/etc/shells"). Symlink("/fortify/etc/shells", "/etc/shells").
Symlink("/dev/fortify/etc/ssh", "/etc/ssh"). Symlink("/fortify/etc/ssh", "/etc/ssh").
Symlink("/dev/fortify/etc/ssl", "/etc/ssl"). Symlink("/fortify/etc/ssl", "/etc/ssl").
Symlink("/dev/fortify/etc/static", "/etc/static"). Symlink("/fortify/etc/static", "/etc/static").
Symlink("/dev/fortify/etc/subgid", "/etc/subgid"). Symlink("/fortify/etc/subgid", "/etc/subgid").
Symlink("/dev/fortify/etc/subuid", "/etc/subuid"). Symlink("/fortify/etc/subuid", "/etc/subuid").
Symlink("/dev/fortify/etc/sudoers", "/etc/sudoers"). Symlink("/fortify/etc/sudoers", "/etc/sudoers").
Symlink("/dev/fortify/etc/sysctl.d", "/etc/sysctl.d"). Symlink("/fortify/etc/sysctl.d", "/etc/sysctl.d").
Symlink("/dev/fortify/etc/systemd", "/etc/systemd"). Symlink("/fortify/etc/systemd", "/etc/systemd").
Symlink("/dev/fortify/etc/terminfo", "/etc/terminfo"). Symlink("/fortify/etc/terminfo", "/etc/terminfo").
Symlink("/dev/fortify/etc/tmpfiles.d", "/etc/tmpfiles.d"). Symlink("/fortify/etc/tmpfiles.d", "/etc/tmpfiles.d").
Symlink("/dev/fortify/etc/udev", "/etc/udev"). Symlink("/fortify/etc/udev", "/etc/udev").
Symlink("/dev/fortify/etc/udisks2", "/etc/udisks2"). Symlink("/fortify/etc/udisks2", "/etc/udisks2").
Symlink("/dev/fortify/etc/UPower", "/etc/UPower"). Symlink("/fortify/etc/UPower", "/etc/UPower").
Symlink("/dev/fortify/etc/vconsole.conf", "/etc/vconsole.conf"). Symlink("/fortify/etc/vconsole.conf", "/etc/vconsole.conf").
Symlink("/dev/fortify/etc/X11", "/etc/X11"). Symlink("/fortify/etc/X11", "/etc/X11").
Symlink("/dev/fortify/etc/zfs", "/etc/zfs"). Symlink("/fortify/etc/zfs", "/etc/zfs").
Symlink("/dev/fortify/etc/zinputrc", "/etc/zinputrc"). Symlink("/fortify/etc/zinputrc", "/etc/zinputrc").
Symlink("/dev/fortify/etc/zoneinfo", "/etc/zoneinfo"). Symlink("/fortify/etc/zoneinfo", "/etc/zoneinfo").
Symlink("/dev/fortify/etc/zprofile", "/etc/zprofile"). Symlink("/fortify/etc/zprofile", "/etc/zprofile").
Symlink("/dev/fortify/etc/zshenv", "/etc/zshenv"). Symlink("/fortify/etc/zshenv", "/etc/zshenv").
Symlink("/dev/fortify/etc/zshrc", "/etc/zshrc"). Symlink("/fortify/etc/zshrc", "/etc/zshrc").
Bind("/tmp/fortify.1971/tmpdir/150", "/tmp", false, true). Bind("/tmp/fortify.1971/tmpdir/9", "/tmp", false, true).
Tmpfs("/tmp/fortify.1971", 1048576). Tmpfs("/tmp/fortify.1971", 1048576).
Tmpfs("/run/user", 1048576). Tmpfs("/run/user", 1048576).
Tmpfs("/run/user/65534", 8388608). Tmpfs("/run/user/65534", 8388608).
Bind("/home/chronos", "/home/chronos", false, true).
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/passwd", "/etc/passwd"). Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/passwd", "/etc/passwd").
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/group", "/etc/group"). Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/group", "/etc/group").
Bind("/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c/wayland", "/run/user/65534/wayland-0"). Bind("/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c/wayland", "/run/user/65534/wayland-0").
@ -501,23 +513,12 @@ func (s *stubNixOS) Executable() (string, error) {
return "/home/ophestra/.nix-profile/bin/fortify", nil return "/home/ophestra/.nix-profile/bin/fortify", nil
} }
func (s *stubNixOS) Lookup(username string) (*user.User, error) { func (s *stubNixOS) LookupGroup(name string) (*user.Group, error) {
if s.usernameErr != nil { switch name {
if err, ok := s.usernameErr[username]; ok { case "video":
return nil, err return &user.Group{Gid: "26", Name: "video"}, nil
}
}
switch username {
case "chronos":
return &user.User{
Uid: "150",
Gid: "101",
Username: "chronos",
HomeDir: "/home/chronos",
}, nil
default: default:
return nil, user.UnknownUserError(username) return nil, user.UnknownGroupError(name)
} }
} }
@ -579,8 +580,8 @@ func (s *stubNixOS) Exit(code int) {
panic("called exit on stub with code " + strconv.Itoa(code)) panic("called exit on stub with code " + strconv.Itoa(code))
} }
func (s *stubNixOS) FshimPath() string { func (s *stubNixOS) Stdout() io.Writer {
return "/nix/store/00000000000000000000000000000000-fortify-0.0.10/bin/.fshim" panic("requested stdout")
} }
func (s *stubNixOS) Paths() linux.Paths { func (s *stubNixOS) Paths() linux.Paths {
@ -591,6 +592,10 @@ func (s *stubNixOS) Paths() linux.Paths {
} }
} }
func (s *stubNixOS) Uid(aid int) (int, error) {
return 1000000 + 0*10000 + aid, nil
}
func (s *stubNixOS) SdBooted() bool { func (s *stubNixOS) SdBooted() bool {
return true return true
} }

View File

@ -1,23 +1,22 @@
package app package app
import ( import (
"os" "errors"
"git.ophivana.moe/security/fortify/dbus" "git.ophivana.moe/security/fortify/dbus"
"git.ophivana.moe/security/fortify/helper/bwrap" "git.ophivana.moe/security/fortify/helper/bwrap"
"git.ophivana.moe/security/fortify/internal/linux"
"git.ophivana.moe/security/fortify/internal/system" "git.ophivana.moe/security/fortify/internal/system"
) )
const fTmp = "/fortify"
// Config is used to seal an *App // Config is used to seal an *App
type Config struct { type Config struct {
// D-Bus application ID // D-Bus application ID
ID string `json:"id"` ID string `json:"id"`
// username of the target user to switch to
User string `json:"user"`
// value passed through to the child process as its argv // value passed through to the child process as its argv
Command []string `json:"command"` Command []string `json:"command"`
// string representation of the child's launch method
Method string `json:"method"`
// child confinement configuration // child confinement configuration
Confinement ConfinementConfig `json:"confinement"` Confinement ConfinementConfig `json:"confinement"`
@ -25,6 +24,16 @@ type Config struct {
// ConfinementConfig defines fortified child's confinement // ConfinementConfig defines fortified child's confinement
type ConfinementConfig struct { type ConfinementConfig struct {
// numerical application id, determines uid in the init namespace
AppID int `json:"app_id"`
// list of supplementary groups to inherit
Groups []string `json:"groups"`
// passwd username in the sandbox, defaults to chronos
Username string `json:"username,omitempty"`
// home directory in sandbox, empty for outer
Inner string `json:"home_inner"`
// home directory in init namespace
Outer string `json:"home"`
// bwrap sandbox confinement configuration // bwrap sandbox confinement configuration
Sandbox *SandboxConfig `json:"sandbox"` Sandbox *SandboxConfig `json:"sandbox"`
@ -47,10 +56,12 @@ type SandboxConfig struct {
UserNS bool `json:"userns,omitempty"` UserNS bool `json:"userns,omitempty"`
// share net namespace // share net namespace
Net bool `json:"net,omitempty"` Net bool `json:"net,omitempty"`
// share all devices
Dev bool `json:"dev,omitempty"`
// do not run in new session // do not run in new session
NoNewSession bool `json:"no_new_session,omitempty"` NoNewSession bool `json:"no_new_session,omitempty"`
// map target user uid to privileged user uid in the user namespace // map target user uid to privileged user uid in the user namespace
UseRealUID bool `json:"use_real_uid"` MapRealUID bool `json:"map_real_uid"`
// mediated access to wayland socket // mediated access to wayland socket
Wayland bool `json:"wayland,omitempty"` Wayland bool `json:"wayland,omitempty"`
@ -60,6 +71,8 @@ type SandboxConfig struct {
Filesystem []*FilesystemConfig `json:"filesystem"` Filesystem []*FilesystemConfig `json:"filesystem"`
// symlinks created inside the sandbox // symlinks created inside the sandbox
Link [][2]string `json:"symlink"` Link [][2]string `json:"symlink"`
// automatically set up /etc symlinks
AutoEtc bool `json:"auto_etc"`
// paths to override by mounting tmpfs over them // paths to override by mounting tmpfs over them
Override []string `json:"override"` Override []string `json:"override"`
} }
@ -79,13 +92,16 @@ type FilesystemConfig struct {
// Bwrap returns the address of the corresponding bwrap.Config to s. // Bwrap returns the address of the corresponding bwrap.Config to s.
// Note that remaining tmpfs entries must be queued by the caller prior to launch. // Note that remaining tmpfs entries must be queued by the caller prior to launch.
func (s *SandboxConfig) Bwrap(uid int) *bwrap.Config { func (s *SandboxConfig) Bwrap(os linux.System) (*bwrap.Config, error) {
if s == nil { if s == nil {
return nil return nil, errors.New("nil sandbox config")
} }
if !s.UseRealUID { var uid int
if !s.MapRealUID {
uid = 65534 uid = 65534
} else {
uid = os.Geteuid()
} }
conf := (&bwrap.Config{ conf := (&bwrap.Config{
@ -99,11 +115,21 @@ func (s *SandboxConfig) Bwrap(uid int) *bwrap.Config {
AsInit: true, AsInit: true,
// initialise map // initialise map
Chmod: make(map[string]os.FileMode), Chmod: make(bwrap.ChmodConfig),
}). }).
SetUID(uid).SetGID(uid). SetUID(uid).SetGID(uid).
Procfs("/proc").DevTmpfs("/dev").Mqueue("/dev/mqueue"). Procfs("/proc").
Tmpfs("/dev/fortify", 4*1024) Tmpfs(fTmp, 4*1024)
if !s.Dev {
conf.DevTmpfs("/dev").Mqueue("/dev/mqueue")
} else {
conf.Bind("/dev", "/dev", false, true, true)
}
if !s.AutoEtc {
conf.Dir("/etc")
}
for _, c := range s.Filesystem { for _, c := range s.Filesystem {
if c == nil { if c == nil {
@ -121,14 +147,35 @@ func (s *SandboxConfig) Bwrap(uid int) *bwrap.Config {
conf.Symlink(l[0], l[1]) conf.Symlink(l[0], l[1])
} }
return conf if s.AutoEtc {
conf.Bind("/etc", fTmp+"/etc")
// link host /etc contents to prevent passwd/group from being overwritten
if d, err := os.ReadDir("/etc"); err != nil {
return nil, err
} else {
for _, ent := range d {
name := ent.Name()
switch name {
case "passwd":
case "group":
case "mtab":
conf.Symlink("/proc/mounts", "/etc/"+name)
default:
conf.Symlink(fTmp+"/etc/"+name, "/etc/"+name)
}
}
}
}
return conf, nil
} }
// Template returns a fully populated instance of Config. // Template returns a fully populated instance of Config.
func Template() *Config { func Template() *Config {
return &Config{ return &Config{
ID: "org.chromium.Chromium", ID: "org.chromium.Chromium",
User: "chronos",
Command: []string{ Command: []string{
"chromium", "chromium",
"--ignore-gpu-blocklist", "--ignore-gpu-blocklist",
@ -136,13 +183,19 @@ func Template() *Config {
"--enable-features=UseOzonePlatform", "--enable-features=UseOzonePlatform",
"--ozone-platform=wayland", "--ozone-platform=wayland",
}, },
Method: "sudo",
Confinement: ConfinementConfig{ Confinement: ConfinementConfig{
AppID: 9,
Groups: []string{"video"},
Username: "chronos",
Outer: "/var/lib/persist/home/org.chromium.Chromium",
Inner: "/var/lib/fortify",
Sandbox: &SandboxConfig{ Sandbox: &SandboxConfig{
Hostname: "localhost", Hostname: "localhost",
UserNS: true, UserNS: true,
Net: true, Net: true,
NoNewSession: true, NoNewSession: true,
MapRealUID: true,
Dev: true,
Wayland: false, Wayland: false,
// example API credentials pulled from Google Chrome // example API credentials pulled from Google Chrome
// DO NOT USE THESE IN A REAL BROWSER // DO NOT USE THESE IN A REAL BROWSER
@ -152,12 +205,15 @@ func Template() *Config {
"GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT", "GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT",
}, },
Filesystem: []*FilesystemConfig{ Filesystem: []*FilesystemConfig{
{Src: "/nix"}, {Src: "/nix/store"},
{Src: "/storage/emulated/0", Write: true, Must: true}, {Src: "/run/current-system"},
{Src: "/data/user/0", Dst: "/data/data", Write: true, Must: true}, {Src: "/run/opengl-driver"},
{Src: "/var/tmp", Write: true}, {Src: "/var/db/nix-channels"},
{Src: "/home/chronos", Write: true, Must: true},
{Src: "/dev/dri", Device: true},
}, },
Link: [][2]string{{"/dev/fortify/etc", "/etc"}}, Link: [][2]string{{"/run/user/65534", "/run/user/150"}},
AutoEtc: true,
Override: []string{"/var/run/nscd"}, Override: []string{"/var/run/nscd"},
}, },
SystemBus: &dbus.Config{ SystemBus: &dbus.Config{

View File

@ -1,57 +0,0 @@
package app
import (
"strings"
"git.ophivana.moe/security/fortify/internal/fmsg"
)
func (a *app) commandBuilderMachineCtl(shimEnv string) (args []string) {
args = make([]string, 0, 9+len(a.seal.sys.bwrap.SetEnv))
// shell --uid=$USER
args = append(args, "shell", "--uid="+a.seal.sys.user.Username)
// --quiet
if !fmsg.Verbose() {
args = append(args, "--quiet")
}
// environ
envQ := make([]string, 0, len(a.seal.sys.bwrap.SetEnv)+1)
for k, v := range a.seal.sys.bwrap.SetEnv {
envQ = append(envQ, "-E"+k+"="+v)
}
// add shim payload to environment for shim path
envQ = append(envQ, "-E"+shimEnv)
args = append(args, envQ...)
// -- .host
args = append(args, "--", ".host")
// /bin/sh -c
if sh, err := a.os.LookPath("sh"); err != nil {
// hardcode /bin/sh path since it exists more often than not
args = append(args, "/bin/sh", "-c")
} else {
args = append(args, sh, "-c")
}
// build inner command expression ran as target user
innerCommand := strings.Builder{}
// apply custom environment variables to activation environment
innerCommand.WriteString("dbus-update-activation-environment --systemd")
for k := range a.seal.sys.bwrap.SetEnv {
innerCommand.WriteString(" " + k)
}
innerCommand.WriteString("; ")
// launch fortify shim
innerCommand.WriteString("exec " + a.os.FshimPath())
// append inner command
args = append(args, innerCommand.String())
return
}

View File

@ -1,30 +0,0 @@
package app
import (
"git.ophivana.moe/security/fortify/internal/fmsg"
)
const (
sudoAskPass = "SUDO_ASKPASS"
)
func (a *app) commandBuilderSudo(shimEnv string) (args []string) {
args = make([]string, 0, 8)
// -Hiu $USER
args = append(args, "-Hiu", a.seal.sys.user.Username)
// -A?
if _, ok := a.os.LookupEnv(sudoAskPass); ok {
fmsg.VPrintln(sudoAskPass, "set, adding askpass flag")
args = append(args, "-A")
}
// shim payload
args = append(args, shimEnv)
// -- $@
args = append(args, "--", a.os.FshimPath())
return
}

View File

@ -2,9 +2,10 @@ package app
import ( import (
"errors" "errors"
"fmt"
"io/fs" "io/fs"
"os/user"
"path" "path"
"regexp"
"strconv" "strconv"
shim "git.ophivana.moe/security/fortify/cmd/fshim/ipc" shim "git.ophivana.moe/security/fortify/cmd/fshim/ipc"
@ -15,26 +16,15 @@ import (
"git.ophivana.moe/security/fortify/internal/system" "git.ophivana.moe/security/fortify/internal/system"
) )
const (
LaunchMethodSudo uint8 = iota
LaunchMethodMachineCtl
)
var method = [...]string{
LaunchMethodSudo: "sudo",
LaunchMethodMachineCtl: "systemd",
}
var ( var (
ErrConfig = errors.New("no configuration to seal") ErrConfig = errors.New("no configuration to seal")
ErrUser = errors.New("unknown user") ErrUser = errors.New("invalid aid")
ErrLaunch = errors.New("invalid launch method") ErrHome = errors.New("invalid home directory")
ErrName = errors.New("invalid username")
ErrSudo = errors.New("sudo not available")
ErrSystemd = errors.New("systemd not available")
ErrMachineCtl = errors.New("machinectl not available")
) )
var posixUsername = regexp.MustCompilePOSIX("^[a-z_]([A-Za-z0-9_-]{0,31}|[A-Za-z0-9_-]{0,30}\\$)$")
// appSeal seals the application with child-related information // appSeal seals the application with child-related information
type appSeal struct { type appSeal struct {
// app unique ID string representation // app unique ID string representation
@ -51,15 +41,11 @@ type appSeal struct {
// persistent process state store // persistent process state store
store state.Store store state.Store
// uint8 representation of launch method sealed from config
launchOption uint8
// process-specific share directory path // process-specific share directory path
share string share string
// process-specific share directory path local to XDG_RUNTIME_DIR // process-specific share directory path local to XDG_RUNTIME_DIR
shareLocal string shareLocal string
// path to launcher program
toolPath string
// pass-through enablement tracking from config // pass-through enablement tracking from config
et system.Enablements et system.Enablements
@ -98,39 +84,11 @@ func (a *app) Seal(config *Config) error {
seal.fid = config.ID seal.fid = config.ID
seal.command = config.Command seal.command = config.Command
// parses launch method text and looks up tool path
switch config.Method {
case method[LaunchMethodSudo]:
seal.launchOption = LaunchMethodSudo
if sudoPath, err := a.os.LookPath("sudo"); err != nil {
return fmsg.WrapError(ErrSudo,
"sudo not found")
} else {
seal.toolPath = sudoPath
}
case method[LaunchMethodMachineCtl]:
seal.launchOption = LaunchMethodMachineCtl
if !a.os.SdBooted() {
return fmsg.WrapError(ErrSystemd,
"system has not been booted with systemd as init system")
}
if machineCtlPath, err := a.os.LookPath("machinectl"); err != nil {
return fmsg.WrapError(ErrMachineCtl,
"machinectl not found")
} else {
seal.toolPath = machineCtlPath
}
default:
return fmsg.WrapError(ErrLaunch,
"invalid launch method")
}
// create seal system component // create seal system component
seal.sys = new(appSealSys) seal.sys = new(appSealSys)
// mapped uid // mapped uid
if config.Confinement.Sandbox != nil && config.Confinement.Sandbox.UseRealUID { if config.Confinement.Sandbox != nil && config.Confinement.Sandbox.MapRealUID {
seal.sys.mappedID = a.os.Geteuid() seal.sys.mappedID = a.os.Geteuid()
} else { } else {
seal.sys.mappedID = 65534 seal.sys.mappedID = 65534
@ -138,16 +96,51 @@ func (a *app) Seal(config *Config) error {
seal.sys.mappedIDString = strconv.Itoa(seal.sys.mappedID) seal.sys.mappedIDString = strconv.Itoa(seal.sys.mappedID)
seal.sys.runtime = path.Join("/run/user", seal.sys.mappedIDString) seal.sys.runtime = path.Join("/run/user", seal.sys.mappedIDString)
// look up user from system // validate uid and set user info
if u, err := a.os.Lookup(config.User); err != nil { if config.Confinement.AppID < 0 || config.Confinement.AppID > 9999 {
if errors.As(err, new(user.UnknownUserError)) { return fmsg.WrapError(ErrUser,
return fmsg.WrapError(ErrUser, "unknown user", config.User) fmt.Sprintf("aid %d out of range", config.Confinement.AppID))
} else { } else {
// unreachable seal.sys.user = appUser{
panic(err) aid: config.Confinement.AppID,
as: strconv.Itoa(config.Confinement.AppID),
data: config.Confinement.Outer,
home: config.Confinement.Inner,
username: config.Confinement.Username,
} }
if seal.sys.user.username == "" {
seal.sys.user.username = "chronos"
} else if !posixUsername.MatchString(seal.sys.user.username) {
return fmsg.WrapError(ErrName,
fmt.Sprintf("invalid user name %q", seal.sys.user.username))
}
if seal.sys.user.data == "" || !path.IsAbs(seal.sys.user.data) {
return fmsg.WrapError(ErrHome,
fmt.Sprintf("invalid home directory %q", seal.sys.user.data))
}
if seal.sys.user.home == "" {
seal.sys.user.home = seal.sys.user.data
}
// invoke fsu for full uid
if u, err := a.os.Uid(seal.sys.user.aid); err != nil {
return fmsg.WrapErrorSuffix(err,
"cannot obtain uid from fsu:")
} else { } else {
seal.sys.user = u seal.sys.user.uid = u
seal.sys.user.us = strconv.Itoa(u)
}
// resolve supplementary group ids from names
seal.sys.user.supp = make([]string, len(config.Confinement.Groups))
for i, name := range config.Confinement.Groups {
if g, err := a.os.LookupGroup(name); err != nil {
return fmsg.WrapError(err,
fmt.Sprintf("unknown group %q", name))
} else {
seal.sys.user.supp[i] = g.Gid
}
}
} }
// map sandbox config to bwrap // map sandbox config to bwrap
@ -159,6 +152,7 @@ func (a *app) Seal(config *Config) error {
UserNS: true, UserNS: true,
Net: true, Net: true,
NoNewSession: true, NoNewSession: true,
AutoEtc: true,
} }
// bind entries in / // bind entries in /
if d, err := a.os.ReadDir("/"); err != nil { if d, err := a.os.ReadDir("/"); err != nil {
@ -173,9 +167,8 @@ func (a *app) Seal(config *Config) error {
case "/run": case "/run":
case "/tmp": case "/tmp":
case "/mnt": case "/mnt":
case "/etc": case "/etc":
b = append(b, &FilesystemConfig{Src: p, Dst: "/dev/fortify/etc", Write: false, Must: true})
default: default:
b = append(b, &FilesystemConfig{Src: p, Write: true, Must: true}) b = append(b, &FilesystemConfig{Src: p, Write: true, Must: true})
} }
@ -208,35 +201,14 @@ func (a *app) Seal(config *Config) error {
if config.Confinement.Enablements.Has(system.EX11) || config.Confinement.Enablements.Has(system.EWayland) { if config.Confinement.Enablements.Has(system.EX11) || config.Confinement.Enablements.Has(system.EWayland) {
conf.Filesystem = append(conf.Filesystem, &FilesystemConfig{Src: "/dev/dri", Device: true}) conf.Filesystem = append(conf.Filesystem, &FilesystemConfig{Src: "/dev/dri", Device: true})
} }
// link host /etc to prevent passwd/group from being overwritten
if d, err := a.os.ReadDir("/etc"); err != nil {
return err
} else {
b := make([][2]string, 0, len(d))
for _, ent := range d {
name := ent.Name()
switch name {
case "passwd":
case "group":
case "mtab":
b = append(b, [2]string{
"/proc/mounts",
"/etc/" + name,
})
default:
b = append(b, [2]string{
"/dev/fortify/etc/" + name,
"/etc/" + name,
})
}
}
conf.Link = append(conf.Link, b...)
}
config.Confinement.Sandbox = conf config.Confinement.Sandbox = conf
} }
seal.sys.bwrap = config.Confinement.Sandbox.Bwrap(a.os.Geteuid()) if b, err := config.Confinement.Sandbox.Bwrap(a.os); err != nil {
return err
} else {
seal.sys.bwrap = b
}
seal.sys.override = config.Confinement.Sandbox.Override seal.sys.override = config.Confinement.Sandbox.Override
if seal.sys.bwrap.SetEnv == nil { if seal.sys.bwrap.SetEnv == nil {
seal.sys.bwrap.SetEnv = make(map[string]string) seal.sys.bwrap.SetEnv = make(map[string]string)
@ -251,15 +223,10 @@ func (a *app) Seal(config *Config) error {
// open process state store // open process state store
// the simple store only starts holding an open file after first action // the simple store only starts holding an open file after first action
// store activity begins after Start is called and must end before Wait // store activity begins after Start is called and must end before Wait
seal.store = state.NewSimple(seal.RunDirPath, seal.sys.user.Uid) seal.store = state.NewSimple(seal.RunDirPath, seal.sys.user.as)
// parse string UID // initialise system interface with full uid
if u, err := strconv.Atoi(seal.sys.user.Uid); err != nil { seal.sys.I = system.New(seal.sys.user.uid)
// unreachable unless kernel bug
panic("uid parse")
} else {
seal.sys.I = system.New(u)
}
// pass through enablements // pass through enablements
seal.et = config.Confinement.Enablements seal.et = config.Confinement.Enablements
@ -270,11 +237,8 @@ func (a *app) Seal(config *Config) error {
} }
// verbose log seal information // verbose log seal information
fmsg.VPrintln("created application seal as user", fmsg.VPrintf("created application seal for uid %s (%s) groups: %v, command: %s",
seal.sys.user.Username, "("+seal.sys.user.Uid+"),", seal.sys.user.us, seal.sys.user.username, config.Confinement.Groups, config.Command)
"method:", config.Method+",",
"launcher:", seal.toolPath+",",
"command:", config.Command)
// seal app and release lock // seal app and release lock
a.seal = seal a.seal = seal

View File

@ -58,7 +58,7 @@ func (seal *appSeal) shareDisplay(os linux.System) error {
return fmsg.WrapError(ErrXDisplay, return fmsg.WrapError(ErrXDisplay,
"DISPLAY is not set") "DISPLAY is not set")
} else { } else {
seal.sys.ChangeHosts(seal.sys.user.Username) seal.sys.ChangeHosts("#" + seal.sys.user.us)
seal.sys.bwrap.SetEnv[display] = d seal.sys.bwrap.SetEnv[display] = d
seal.sys.bwrap.Bind("/tmp/.X11-unix", "/tmp/.X11-unix") seal.sys.bwrap.Bind("/tmp/.X11-unix", "/tmp/.X11-unix")
} }

View File

@ -16,12 +16,12 @@ const (
func (seal *appSeal) shareSystem() { func (seal *appSeal) shareSystem() {
// ensure Share (e.g. `/tmp/fortify.%d`) // ensure Share (e.g. `/tmp/fortify.%d`)
// acl is unnecessary as this directory is world executable // acl is unnecessary as this directory is world executable
seal.sys.Ensure(seal.SharePath, 0701) seal.sys.Ensure(seal.SharePath, 0711)
// ensure process-specific share (e.g. `/tmp/fortify.%d/%s`) // ensure process-specific share (e.g. `/tmp/fortify.%d/%s`)
// acl is unnecessary as this directory is world executable // acl is unnecessary as this directory is world executable
seal.share = path.Join(seal.SharePath, seal.id) seal.share = path.Join(seal.SharePath, seal.id)
seal.sys.Ephemeral(system.Process, seal.share, 0701) seal.sys.Ephemeral(system.Process, seal.share, 0711)
// ensure child tmpdir parent directory (e.g. `/tmp/fortify.%d/tmpdir`) // ensure child tmpdir parent directory (e.g. `/tmp/fortify.%d/tmpdir`)
targetTmpdirParent := path.Join(seal.SharePath, "tmpdir") targetTmpdirParent := path.Join(seal.SharePath, "tmpdir")
@ -29,7 +29,7 @@ func (seal *appSeal) shareSystem() {
seal.sys.UpdatePermType(system.User, targetTmpdirParent, acl.Execute) seal.sys.UpdatePermType(system.User, targetTmpdirParent, acl.Execute)
// ensure child tmpdir (e.g. `/tmp/fortify.%d/tmpdir/%d`) // ensure child tmpdir (e.g. `/tmp/fortify.%d/tmpdir/%d`)
targetTmpdir := path.Join(targetTmpdirParent, seal.sys.user.Uid) targetTmpdir := path.Join(targetTmpdirParent, seal.sys.user.as)
seal.sys.Ensure(targetTmpdir, 01700) seal.sys.Ensure(targetTmpdir, 01700)
seal.sys.UpdatePermType(system.User, targetTmpdir, acl.Read, acl.Write, acl.Execute) seal.sys.UpdatePermType(system.User, targetTmpdir, acl.Read, acl.Write, acl.Execute)
seal.sys.bwrap.Bind(targetTmpdir, "/tmp", false, true) seal.sys.bwrap.Bind(targetTmpdir, "/tmp", false, true)
@ -49,15 +49,21 @@ func (seal *appSeal) sharePasswd(os linux.System) {
// generate /etc/passwd // generate /etc/passwd
passwdPath := path.Join(seal.share, "passwd") passwdPath := path.Join(seal.share, "passwd")
username := "chronos" username := "chronos"
if seal.sys.user.Username != "" { if seal.sys.user.username != "" {
username = seal.sys.user.Username username = seal.sys.user.username
seal.sys.bwrap.SetEnv["USER"] = seal.sys.user.Username
} }
homeDir := "/var/empty" homeDir := "/var/empty"
if seal.sys.user.HomeDir != "" { if seal.sys.user.home != "" {
homeDir = seal.sys.user.HomeDir homeDir = seal.sys.user.home
seal.sys.bwrap.SetEnv["HOME"] = seal.sys.user.HomeDir
} }
// bind home directory
seal.sys.bwrap.Bind(seal.sys.user.data, homeDir, false, true)
seal.sys.bwrap.Chdir = homeDir
seal.sys.bwrap.SetEnv["USER"] = username
seal.sys.bwrap.SetEnv["HOME"] = homeDir
passwd := username + ":x:" + seal.sys.mappedIDString + ":" + seal.sys.mappedIDString + ":Fortify:" + homeDir + ":" + sh + "\n" passwd := username + ":x:" + seal.sys.mappedIDString + ":" + seal.sys.mappedIDString + ":Fortify:" + homeDir + ":" + sh + "\n"
seal.sys.Write(passwdPath, passwd) seal.sys.Write(passwdPath, passwd)

View File

@ -41,19 +41,13 @@ func (a *app) Start() error {
} }
} }
// select command builder
var commandBuilder shim.CommandBuilder
switch a.seal.launchOption {
case LaunchMethodSudo:
commandBuilder = a.commandBuilderSudo
case LaunchMethodMachineCtl:
commandBuilder = a.commandBuilderMachineCtl
default:
panic("unreachable")
}
// construct shim manager // construct shim manager
a.shim = shim.New(a.seal.toolPath, uint32(a.seal.sys.UID()), path.Join(a.seal.share, "shim"), a.seal.wl, a.shim = shim.New(
uint32(a.seal.sys.UID()),
a.seal.sys.user.as,
a.seal.sys.user.supp,
path.Join(a.seal.share, "shim"),
a.seal.wl,
&shim0.Payload{ &shim0.Payload{
Argv: a.seal.command, Argv: a.seal.command,
Exec: shimExec, Exec: shimExec,
@ -62,9 +56,6 @@ func (a *app) Start() error {
Verbose: fmsg.Verbose(), Verbose: fmsg.Verbose(),
}, },
// checkPid is impossible at the moment since there is no reliable way to obtain shim's pid
// this feature is disabled here until sudo is replaced by fortify suid wrapper
false,
) )
// startup will go ahead, commit system setup // startup will go ahead, commit system setup
@ -73,7 +64,7 @@ func (a *app) Start() error {
} }
a.seal.sys.needRevert = true a.seal.sys.needRevert = true
if startTime, err := a.shim.Start(commandBuilder); err != nil { if startTime, err := a.shim.Start(); err != nil {
return err return err
} else { } else {
// shim start and setup success, create process state // shim start and setup success, create process state
@ -81,7 +72,6 @@ func (a *app) Start() error {
PID: a.shim.Unwrap().Process.Pid, PID: a.shim.Unwrap().Process.Pid,
Command: a.seal.command, Command: a.seal.command,
Capability: a.seal.et, Capability: a.seal.et,
Method: method[a.seal.launchOption],
Argv: a.shim.Unwrap().Args, Argv: a.shim.Unwrap().Args,
Time: *startTime, Time: *startTime,
} }
@ -166,8 +156,13 @@ func (a *app) Wait() (int, error) {
// failure prior to process start // failure prior to process start
r = 255 r = 255
} else { } else {
wait := make(chan error, 1)
go func() { wait <- cmd.Wait() }()
select {
// wait for process and resolve exit code // wait for process and resolve exit code
if err := cmd.Wait(); err != nil { case err := <-wait:
if err != nil {
var exitError *exec.ExitError var exitError *exec.ExitError
if !errors.As(err, &exitError) { if !errors.As(err, &exitError) {
// should be unreachable // should be unreachable
@ -180,6 +175,16 @@ func (a *app) Wait() (int, error) {
r = cmd.ProcessState.ExitCode() r = cmd.ProcessState.ExitCode()
} }
fmsg.VPrintf("process %d exited with exit code %d", cmd.Process.Pid, r) fmsg.VPrintf("process %d exited with exit code %d", cmd.Process.Pid, r)
// alternative exit path when kill was unsuccessful
case err := <-a.shim.WaitFallback():
r = 255
if err != nil {
fmsg.Printf("cannot terminate shim on faulted setup: %v", err)
} else {
fmsg.VPrintln("alternative exit path selected")
}
}
} }
// child process exited, resume output // child process exited, resume output
@ -250,10 +255,17 @@ func (a *app) Wait() (int, error) {
} }
} }
if a.shim.Unwrap() == nil {
fmsg.VPrintln("fault before shim start")
} else {
a.shim.AbortWait(errors.New("shim exited")) a.shim.AbortWait(errors.New("shim exited"))
}
if a.seal.sys.needRevert {
if err := a.seal.sys.Revert(ec); err != nil { if err := a.seal.sys.Revert(ec); err != nil {
return err.(RevertCompoundError) return err.(RevertCompoundError)
} }
}
return nil return nil
}() }()

View File

@ -1,8 +1,6 @@
package app package app
import ( import (
"os/user"
"git.ophivana.moe/security/fortify/dbus" "git.ophivana.moe/security/fortify/dbus"
"git.ophivana.moe/security/fortify/helper/bwrap" "git.ophivana.moe/security/fortify/helper/bwrap"
"git.ophivana.moe/security/fortify/internal/linux" "git.ophivana.moe/security/fortify/internal/linux"
@ -18,7 +16,7 @@ type appSealSys struct {
// default formatted XDG_RUNTIME_DIR of User // default formatted XDG_RUNTIME_DIR of User
runtime string runtime string
// target user sealed from config // target user sealed from config
user *user.User user appUser
// mapped uid and gid in user namespace // mapped uid and gid in user namespace
mappedID int mappedID int
@ -32,6 +30,28 @@ type appSealSys struct {
// protected by upstream mutex // protected by upstream mutex
} }
type appUser struct {
// full uid resolved by fsu
uid int
// string representation of uid
us string
// supplementary group ids
supp []string
// application id
aid int
// string representation of aid
as string
// home directory host path
data string
// app user home directory
home string
// passwd database username
username string
}
// shareAll calls all share methods in sequence // shareAll calls all share methods in sequence
func (seal *appSeal) shareAll(bus [2]*dbus.Config, os linux.System) error { func (seal *appSeal) shareAll(bus [2]*dbus.Config, os linux.System) error {
if seal.shared { if seal.shared {

View File

@ -33,16 +33,17 @@ func dequeue() {
// queue submits ops to msgbuf but drops messages // queue submits ops to msgbuf but drops messages
// when the buffer is full and dequeue is withholding // when the buffer is full and dequeue is withholding
func queue(op dOp) { func queue(op dOp) {
queueSync.Add(1)
select { select {
case msgbuf <- op: case msgbuf <- op:
queueSync.Add(1)
default: default:
// send the op anyway if not withholding // send the op anyway if not withholding
// as dequeue will get to it eventually // as dequeue will get to it eventually
if !wstate.Load() { if !wstate.Load() {
queueSync.Add(1)
msgbuf <- op msgbuf <- op
} else { } else {
queueSync.Done()
// increment dropped message count // increment dropped message count
dropped.Add(1) dropped.Add(1)
} }
@ -56,9 +57,10 @@ func Exit(code int) {
os.Exit(code) os.Exit(code)
} }
func Withhold() { func Suspend() {
dequeueOnce.Do(dequeue) dequeueOnce.Do(dequeue)
if wstate.CompareAndSwap(false, true) { if wstate.CompareAndSwap(false, true) {
queueSync.Wait()
withhold <- struct{}{} withhold <- struct{}{}
} }
} }

View File

@ -1,6 +1,7 @@
package linux package linux
import ( import (
"io"
"io/fs" "io/fs"
"os/user" "os/user"
"path" "path"
@ -21,8 +22,8 @@ type System interface {
LookPath(file string) (string, error) LookPath(file string) (string, error)
// Executable provides [os.Executable]. // Executable provides [os.Executable].
Executable() (string, error) Executable() (string, error)
// Lookup provides [user.Lookup]. // LookupGroup provides [user.LookupGroup].
Lookup(username string) (*user.User, error) LookupGroup(name string) (*user.Group, error)
// ReadDir provides [os.ReadDir]. // ReadDir provides [os.ReadDir].
ReadDir(name string) ([]fs.DirEntry, error) ReadDir(name string) ([]fs.DirEntry, error)
// Stat provides [os.Stat]. // Stat provides [os.Stat].
@ -31,11 +32,13 @@ type System interface {
Open(name string) (fs.File, error) Open(name string) (fs.File, error)
// Exit provides [os.Exit]. // Exit provides [os.Exit].
Exit(code int) Exit(code int)
// Stdout provides [os.Stdout].
Stdout() io.Writer
// FshimPath returns an absolute path to the fshim binary.
FshimPath() string
// Paths returns a populated [Paths] struct. // Paths returns a populated [Paths] struct.
Paths() Paths Paths() Paths
// Uid invokes fsu and returns target uid.
Uid(aid int) (int, error)
// SdBooted implements https://www.freedesktop.org/software/systemd/man/sd_booted.html // SdBooted implements https://www.freedesktop.org/software/systemd/man/sd_booted.html
SdBooted() bool SdBooted() bool
} }

View File

@ -2,10 +2,12 @@ package linux
import ( import (
"errors" "errors"
"io"
"io/fs" "io/fs"
"os" "os"
"os/exec" "os/exec"
"os/user" "os/user"
"strconv"
"sync" "sync"
"git.ophivana.moe/security/fortify/internal" "git.ophivana.moe/security/fortify/internal"
@ -20,8 +22,12 @@ type Std struct {
sdBooted bool sdBooted bool
sdBootedOnce sync.Once sdBootedOnce sync.Once
fshim string uidOnce sync.Once
fshimOnce sync.Once uidCopy map[int]struct {
uid int
err error
}
uidMu sync.RWMutex
} }
func (s *Std) Geteuid() int { return os.Geteuid() } func (s *Std) Geteuid() int { return os.Geteuid() }
@ -29,31 +35,62 @@ func (s *Std) LookupEnv(key string) (string, bool) { return os.LookupEnv(
func (s *Std) TempDir() string { return os.TempDir() } func (s *Std) TempDir() string { return os.TempDir() }
func (s *Std) LookPath(file string) (string, error) { return exec.LookPath(file) } func (s *Std) LookPath(file string) (string, error) { return exec.LookPath(file) }
func (s *Std) Executable() (string, error) { return os.Executable() } func (s *Std) Executable() (string, error) { return os.Executable() }
func (s *Std) Lookup(username string) (*user.User, error) { return user.Lookup(username) } func (s *Std) LookupGroup(name string) (*user.Group, error) { return user.LookupGroup(name) }
func (s *Std) ReadDir(name string) ([]os.DirEntry, error) { return os.ReadDir(name) } func (s *Std) ReadDir(name string) ([]os.DirEntry, error) { return os.ReadDir(name) }
func (s *Std) Stat(name string) (fs.FileInfo, error) { return os.Stat(name) } func (s *Std) Stat(name string) (fs.FileInfo, error) { return os.Stat(name) }
func (s *Std) Open(name string) (fs.File, error) { return os.Open(name) } func (s *Std) Open(name string) (fs.File, error) { return os.Open(name) }
func (s *Std) Exit(code int) { fmsg.Exit(code) } func (s *Std) Exit(code int) { fmsg.Exit(code) }
func (s *Std) Stdout() io.Writer { return os.Stdout }
const xdgRuntimeDir = "XDG_RUNTIME_DIR" const xdgRuntimeDir = "XDG_RUNTIME_DIR"
func (s *Std) FshimPath() string {
s.fshimOnce.Do(func() {
p, ok := internal.Path(internal.Fshim)
if !ok {
fmsg.Fatal("invalid fshim path, this copy of fortify is not compiled correctly")
}
s.fshim = p
})
return s.fshim
}
func (s *Std) Paths() Paths { func (s *Std) Paths() Paths {
s.pathsOnce.Do(func() { CopyPaths(s, &s.paths) }) s.pathsOnce.Do(func() { CopyPaths(s, &s.paths) })
return s.paths return s.paths
} }
func (s *Std) Uid(aid int) (int, error) {
s.uidOnce.Do(func() {
s.uidCopy = make(map[int]struct {
uid int
err error
})
})
s.uidMu.RLock()
if u, ok := s.uidCopy[aid]; ok {
s.uidMu.RUnlock()
return u.uid, u.err
}
s.uidMu.RUnlock()
s.uidMu.Lock()
defer s.uidMu.Unlock()
u := struct {
uid int
err error
}{}
defer func() { s.uidCopy[aid] = u }()
u.uid = -1
if fsu, ok := internal.Check(internal.Fsu); !ok {
fmsg.Fatal("invalid fsu path, this copy of fshim is not compiled correctly")
panic("unreachable")
} else {
cmd := exec.Command(fsu)
cmd.Path = fsu
cmd.Stderr = os.Stderr // pass through fatal messages
cmd.Env = []string{"FORTIFY_APP_ID=" + strconv.Itoa(aid)}
cmd.Dir = "/"
var p []byte
if p, u.err = cmd.Output(); u.err == nil {
u.uid, u.err = strconv.Atoi(string(p))
}
return u.uid, u.err
}
}
func (s *Std) SdBooted() bool { func (s *Std) SdBooted() bool {
s.sdBootedOnce.Do(func() { s.sdBooted = copySdBooted() }) s.sdBootedOnce.Do(func() { s.sdBooted = copySdBooted() })
return s.sdBooted return s.sdBooted

View File

@ -4,7 +4,6 @@ import "path"
var ( var (
Fsu = compPoison Fsu = compPoison
Fshim = compPoison
Finit = compPoison Finit = compPoison
) )

View File

@ -67,10 +67,10 @@ func (s *simpleStore) mustPrintLauncherState(w **tabwriter.Writer, now time.Time
// write header when initialising // write header when initialising
if !fmsg.Verbose() { if !fmsg.Verbose() {
_, _ = fmt.Fprintln(*w, "\tUID\tPID\tUptime\tEnablements\tMethod\tCommand") _, _ = fmt.Fprintln(*w, "\tPID\tApp\tUptime\tEnablements\tCommand")
} else { } else {
// argv is emitted in body when verbose // argv is emitted in body when verbose
_, _ = fmt.Fprintln(*w, "\tUID\tPID\tArgv") _, _ = fmt.Fprintln(*w, "\tPID\tApp\tArgv")
} }
} }
@ -96,13 +96,13 @@ func (s *simpleStore) mustPrintLauncherState(w **tabwriter.Writer, now time.Time
} }
if !fmsg.Verbose() { if !fmsg.Verbose() {
_, _ = fmt.Fprintf(*w, "\t%s\t%d\t%s\t%s\t%s\t%s\n", _, _ = fmt.Fprintf(*w, "\t%d\t%s\t%s\t%s\t%s\n",
s.path[len(s.path)-1], state.PID, now.Sub(state.Time).Round(time.Second).String(), strings.TrimPrefix(ets.String(), ", "), state.Method, state.PID, s.path[len(s.path)-1], now.Sub(state.Time).Round(time.Second).String(), strings.TrimPrefix(ets.String(), ", "),
state.Command) state.Command)
} else { } else {
// emit argv instead when verbose // emit argv instead when verbose
_, _ = fmt.Fprintf(*w, "\t%s\t%d\t%s\n", _, _ = fmt.Fprintf(*w, "\t%d\t%s\t%s\n",
s.path[len(s.path)-1], state.PID, state.Argv) state.PID, s.path[len(s.path)-1], state.Argv)
} }
} }

View File

@ -33,8 +33,6 @@ type State struct {
// capability enablements applied to child // capability enablements applied to child
Capability system.Enablements Capability system.Enablements
// user switch method
Method string
// full argv whe launching // full argv whe launching
Argv []string Argv []string
// process start time // process start time

View File

@ -1,25 +0,0 @@
package main
import (
_ "embed"
"flag"
"fmt"
)
var (
//go:embed LICENSE
license string
printLicense bool
)
func init() {
flag.BoolVar(&printLicense, "license", false, "Print license")
}
func tryLicense() {
if printLicense {
fmt.Println(license)
os.Exit(0)
}
}

264
main.go
View File

@ -1,16 +1,30 @@
package main package main
import ( import (
_ "embed"
"encoding/json"
"flag" "flag"
"fmt"
"os/user"
"strconv"
"strings"
"sync"
"text/tabwriter"
"git.ophivana.moe/security/fortify/dbus"
"git.ophivana.moe/security/fortify/internal" "git.ophivana.moe/security/fortify/internal"
"git.ophivana.moe/security/fortify/internal/app" "git.ophivana.moe/security/fortify/internal/app"
"git.ophivana.moe/security/fortify/internal/fmsg" "git.ophivana.moe/security/fortify/internal/fmsg"
"git.ophivana.moe/security/fortify/internal/linux" "git.ophivana.moe/security/fortify/internal/linux"
"git.ophivana.moe/security/fortify/internal/state"
"git.ophivana.moe/security/fortify/internal/system"
) )
var ( var (
flagVerbose bool flagVerbose bool
//go:embed LICENSE
license string
) )
func init() { func init() {
@ -19,38 +33,257 @@ func init() {
var os = new(linux.Std) var os = new(linux.Std)
type gl []string
func (g *gl) String() string {
if g == nil {
return "<nil>"
}
return strings.Join(*g, " ")
}
func (g *gl) Set(v string) error {
*g = append(*g, v)
return nil
}
func main() { func main() {
if err := internal.PR_SET_DUMPABLE__SUID_DUMP_DISABLE(); err != nil { if err := internal.PR_SET_DUMPABLE__SUID_DUMP_DISABLE(); err != nil {
fmsg.Printf("cannot set SUID_DUMP_DISABLE: %s", err) fmsg.Printf("cannot set SUID_DUMP_DISABLE: %s", err)
// not fatal: this program runs as the privileged user // not fatal: this program runs as the privileged user
} }
flag.Parse()
fmsg.SetVerbose(flagVerbose)
if os.SdBooted() {
fmsg.VPrintln("system booted with systemd as init system")
}
// root check
if os.Geteuid() == 0 { if os.Geteuid() == 0 {
fmsg.Fatal("this program must not run as root") fmsg.Fatal("this program must not run as root")
panic("unreachable") panic("unreachable")
} }
// version/license/template command early exit flag.CommandLine.Usage = func() {
tryVersion() fmt.Println()
tryLicense() fmt.Println("Usage:\tfortify [-v] COMMAND [OPTIONS]")
tryTemplate() fmt.Println()
fmt.Println("Commands:")
w := tabwriter.NewWriter(os.Stdout(), 0, 1, 4, ' ', 0)
commands := [][2]string{
{"app", "Launch app defined by the specified config file"},
{"run", "Configure and start a permissive default sandbox"},
{"ps", "List active apps and their state"},
{"version", "Show fortify version"},
{"license", "Show full license text"},
{"template", "Produce a config template"},
{"help", "Show this help message"},
}
for _, c := range commands {
_, _ = fmt.Fprintf(w, "\t%s\t%s\n", c[0], c[1])
}
if err := w.Flush(); err != nil {
fmt.Printf("fortify: cannot write command list: %v\n", err)
}
fmt.Println()
}
flag.Parse()
fmsg.SetVerbose(flagVerbose)
// state query command early exit args := flag.Args()
tryState() if len(args) == 0 {
flag.CommandLine.Usage()
fmsg.Exit(0)
}
switch args[0] {
case "version": // print version string
if v, ok := internal.Check(internal.Version); ok {
fmt.Println(v)
} else {
fmt.Println("impure")
}
fmsg.Exit(0)
case "license": // print embedded license
fmt.Println(license)
fmsg.Exit(0)
case "template": // print full template configuration
if s, err := json.MarshalIndent(app.Template(), "", " "); err != nil {
fmsg.Fatalf("cannot generate template: %v", err)
panic("unreachable")
} else {
fmt.Println(string(s))
}
fmsg.Exit(0)
case "help": // print help message
flag.CommandLine.Usage()
fmsg.Exit(0)
case "ps": // print all state info
var w *tabwriter.Writer
state.MustPrintLauncherStateSimpleGlobal(&w, os.Paths().RunDirPath)
if w != nil {
if err := w.Flush(); err != nil {
fmsg.Println("cannot format output:", err)
}
} else {
fmt.Println("No information available")
}
fmsg.Exit(0)
case "app": // launch app from configuration file
if len(args) < 2 {
fmsg.Fatal("app requires at least 1 argument")
}
config := new(app.Config)
if f, err := os.Open(args[1]); err != nil {
fmsg.Fatalf("cannot access config file %q: %s", args[1], err)
panic("unreachable")
} else if err = json.NewDecoder(f).Decode(&config); err != nil {
fmsg.Fatalf("cannot parse config file %q: %s", args[1], err)
panic("unreachable")
}
// append extra args
config.Command = append(config.Command, args[2:]...)
// invoke app // invoke app
runApp(config)
case "run": // run app in permissive defaults usage pattern
set := flag.NewFlagSet("run", flag.ExitOnError)
var (
dbusConfigSession string
dbusConfigSystem string
dbusID string
mpris bool
dbusVerbose bool
aid int
groups gl
homeDir string
userName string
enablements [system.ELen]bool
)
set.StringVar(&dbusConfigSession, "dbus-config", "builtin", "Path to D-Bus proxy config file, or \"builtin\" for defaults")
set.StringVar(&dbusConfigSystem, "dbus-system", "nil", "Path to system D-Bus proxy config file, or \"nil\" to disable")
set.StringVar(&dbusID, "dbus-id", "", "D-Bus ID of application, leave empty to disable own paths, has no effect if custom config is available")
set.BoolVar(&mpris, "mpris", false, "Allow owning MPRIS D-Bus path, has no effect if custom config is available")
set.BoolVar(&dbusVerbose, "dbus-log", false, "Force logging in the D-Bus proxy")
set.IntVar(&aid, "a", 0, "Fortify application ID")
set.Var(&groups, "g", "Groups inherited by the app process")
set.StringVar(&homeDir, "d", "os", "Application home directory")
set.StringVar(&userName, "u", "chronos", "Passwd name within sandbox")
set.BoolVar(&enablements[system.EWayland], "wayland", false, "Share Wayland socket")
set.BoolVar(&enablements[system.EX11], "X", false, "Share X11 socket and allow connection")
set.BoolVar(&enablements[system.EDBus], "dbus", false, "Proxy D-Bus connection")
set.BoolVar(&enablements[system.EPulse], "pulse", false, "Share PulseAudio socket and cookie")
// Ignore errors; set is set for ExitOnError.
_ = set.Parse(args[1:])
// initialise config from flags
config := &app.Config{
ID: dbusID,
Command: set.Args(),
}
if aid < 0 || aid > 9999 {
fmsg.Fatalf("aid %d out of range", aid)
panic("unreachable")
}
// resolve home/username from os when flag is unset
var (
passwd *user.User
passwdOnce sync.Once
passwdFunc = func() {
var us string
if uid, err := os.Uid(aid); err != nil {
fmsg.Fatalf("cannot obtain uid from fsu: %v", err)
} else {
us = strconv.Itoa(uid)
}
if u, err := user.LookupId(us); err != nil {
fmsg.VPrintf("cannot look up uid %s", us)
passwd = &user.User{
Uid: us,
Gid: us,
Username: "chronos",
Name: "Fortify",
HomeDir: "/var/empty",
}
} else {
passwd = u
}
}
)
if homeDir == "os" {
passwdOnce.Do(passwdFunc)
homeDir = passwd.HomeDir
}
if userName == "chronos" {
passwdOnce.Do(passwdFunc)
userName = passwd.Username
}
config.Confinement.AppID = aid
config.Confinement.Groups = groups
config.Confinement.Outer = homeDir
config.Confinement.Username = userName
// enablements from flags
for i := system.Enablement(0); i < system.Enablement(system.ELen); i++ {
if enablements[i] {
config.Confinement.Enablements.Set(i)
}
}
// parse D-Bus config file from flags if applicable
if enablements[system.EDBus] {
if dbusConfigSession == "builtin" {
config.Confinement.SessionBus = dbus.NewConfig(dbusID, true, mpris)
} else {
if c, err := dbus.NewConfigFromFile(dbusConfigSession); err != nil {
fmsg.Fatalf("cannot load session bus proxy config from %q: %s", dbusConfigSession, err)
} else {
config.Confinement.SessionBus = c
}
}
// system bus proxy is optional
if dbusConfigSystem != "nil" {
if c, err := dbus.NewConfigFromFile(dbusConfigSystem); err != nil {
fmsg.Fatalf("cannot load system bus proxy config from %q: %s", dbusConfigSystem, err)
} else {
config.Confinement.SystemBus = c
}
}
// override log from configuration
if dbusVerbose {
config.Confinement.SessionBus.Log = true
config.Confinement.SystemBus.Log = true
}
}
// invoke app
runApp(config)
default:
fmsg.Fatalf("%q is not a valid command", args[0])
}
panic("unreachable")
}
func runApp(config *app.Config) {
if os.SdBooted() {
fmsg.VPrintln("system booted with systemd as init system")
}
a, err := app.New(os) a, err := app.New(os)
if err != nil { if err != nil {
fmsg.Fatalf("cannot create app: %s\n", err) fmsg.Fatalf("cannot create app: %s\n", err)
} else if err = a.Seal(loadConfig()); err != nil { } else if err = a.Seal(config); err != nil {
logBaseError(err, "cannot seal app:") logBaseError(err, "cannot seal app:")
fmsg.Exit(1) fmsg.Exit(1)
} else if err = a.Start(); err != nil { } else if err = a.Start(); err != nil {
@ -69,4 +302,5 @@ func main() {
fmsg.Println("inner wait failed:", err) fmsg.Println("inner wait failed:", err)
} }
fmsg.Exit(r) fmsg.Exit(r)
panic("unreachable")
} }

458
nixos.nix
View File

@ -7,290 +7,212 @@
let let
inherit (lib) inherit (lib)
types
mkOption
mkEnableOption
mkIf mkIf
mkDefault
mapAttrs mapAttrs
mapAttrsToList mapAttrsToList
mergeAttrsList
imap1
foldr
foldlAttrs foldlAttrs
optional optional
optionals
; ;
cfg = config.environment.fortify; cfg = config.environment.fortify;
in in
{ {
options = { imports = [ ./options.nix ];
environment.fortify = {
enable = mkEnableOption "fortify";
target = mkOption {
default = { };
type =
let
inherit (types)
str
enum
bool
package
anything
submodule
listOf
attrsOf
nullOr
;
in
attrsOf (submodule {
options = {
packages = mkOption {
type = listOf package;
default = [ ];
description = ''
List of extra packages to install via home-manager.
'';
};
launchers = mkOption {
type = attrsOf (submodule {
options = {
command = mkOption {
type = nullOr str;
default = null;
description = ''
Command to run as the target user.
Setting this to null will default command to wrapper name.
'';
};
dbus = {
config = mkOption {
type = nullOr anything;
default = null;
description = ''
D-Bus custom configuration.
Setting this to null will enable built-in defaults.
'';
};
configSystem = mkOption {
type = nullOr anything;
default = null;
description = ''
D-Bus system bus custom configuration.
Setting this to null will disable the system bus proxy.
'';
};
id = mkOption {
type = nullOr str;
default = null;
description = ''
D-Bus application id.
Setting this to null will disable own path in defaults.
Has no effect if custom configuration is set.
'';
};
mpris = mkOption {
type = bool;
default = false;
description = ''
Whether to enable MPRIS in D-Bus defaults.
'';
};
};
capability = {
wayland = mkOption {
type = bool;
default = true;
description = ''
Whether to share the Wayland socket.
'';
};
x11 = mkOption {
type = bool;
default = false;
description = ''
Whether to share the X11 socket and allow connection.
'';
};
dbus = mkOption {
type = bool;
default = true;
description = ''
Whether to proxy D-Bus.
'';
};
pulse = mkOption {
type = bool;
default = true;
description = ''
Whether to share the PulseAudio socket and cookie.
'';
};
};
share = mkOption {
type = nullOr package;
default = null;
description = ''
Package containing share files.
Setting this to null will default package name to wrapper name.
'';
};
method = mkOption {
type = enum [
"simple"
"sudo"
"systemd"
];
default = "systemd";
description = ''
Launch method for the sandboxed program.
'';
};
};
});
default = { };
};
persistence = mkOption {
type = submodule {
options = {
directories = mkOption {
type = listOf anything;
default = [ ];
};
files = mkOption {
type = listOf anything;
default = [ ];
};
};
};
description = ''
Per-user state passed to github:nix-community/impermanence.
'';
};
extraConfig = mkOption {
type = anything;
default = { };
description = "Extra home-manager configuration.";
};
};
});
};
package = mkOption {
type = types.package;
default = pkgs.callPackage ./package.nix { };
description = "Package providing fortify.";
};
user = mkOption {
type = types.str;
description = "Privileged user account.";
};
stateDir = mkOption {
type = types.str;
description = ''
The path to persistent storage where per-user state should be stored.
'';
};
};
};
config = mkIf cfg.enable { config = mkIf cfg.enable {
environment.persistence.${cfg.stateDir}.users = mapAttrs (_: target: target.persistence) cfg.target; security.wrappers.fsu = {
source = "${cfg.package}/libexec/fsu";
home-manager.users = setuid = true;
mapAttrs (_: target: target.extraConfig // { home.packages = target.packages; }) cfg.target owner = "root";
// { setgid = true;
${cfg.user}.home.packages = group = "root";
let
wrap =
user: launchers:
mapAttrsToList (
name: launcher:
with launcher.capability;
let
command = if launcher.command == null then name else launcher.command;
dbusConfig =
if launcher.dbus.config != null then
pkgs.writeText "${name}-dbus.json" (builtins.toJSON launcher.dbus.config)
else
null;
dbusSystem =
if launcher.dbus.configSystem != null then
pkgs.writeText "${name}-dbus-system.json" (builtins.toJSON launcher.dbus.configSystem)
else
null;
capArgs =
(if wayland then " --wayland" else "")
+ (if x11 then " -X" else "")
+ (if dbus then " --dbus" else "")
+ (if pulse then " --pulse" else "")
+ (if launcher.dbus.mpris then " --mpris" else "")
+ (if launcher.dbus.id != null then " --dbus-id ${launcher.dbus.id}" else "")
+ (if dbusConfig != null then " --dbus-config ${dbusConfig}" else "")
+ (if dbusSystem != null then " --dbus-system ${dbusSystem}" else "");
in
pkgs.writeShellScriptBin name (
if launcher.method == "simple" then
''
exec sudo -u ${user} -i ${command} $@
''
else
''
exec fortify${capArgs} --method ${launcher.method} -u ${user} $SHELL -c "exec ${command} $@"
''
)
) launchers;
in
foldlAttrs (
acc: user: target:
acc
++ (foldlAttrs (
shares: name: launcher:
let
pkg = if launcher.share != null then launcher.share else pkgs.${name};
link = source: "[ -d '${source}' ] && ln -sv '${source}' $out/share || true";
in
shares
++
optional (launcher.method != "simple" && (launcher.capability.wayland || launcher.capability.x11))
(
pkgs.runCommand "${name}-share" { } ''
mkdir -p $out/share
${link "${pkg}/share/applications"}
${link "${pkg}/share/icons"}
${link "${pkg}/share/man"}
''
)
) (wrap user target.launchers) target.launchers)
) [ cfg.package ] cfg.target;
}; };
security.polkit.extraConfig = environment.etc = {
let fsurc = {
allowList = builtins.toJSON (mapAttrsToList (name: _: name) cfg.target); mode = "0400";
in text = foldlAttrs (
'' acc: username: fid:
polkit.addRule(function(action, subject) { "${toString config.users.users.${username}.uid} ${toString fid}\n" + acc
if (action.id == "org.freedesktop.machine1.host-shell" && ) "" cfg.users;
${allowList}.indexOf(action.lookup("user")) > -1 && };
subject.user == "${cfg.user}") {
return polkit.Result.YES; userdb.source = pkgs.runCommand "fortify-userdb" { } ''
${cfg.package}/libexec/fuserdb -o $out ${
foldlAttrs (
acc: username: fid:
acc + " ${username}:${toString fid}"
) "-s /run/current-system/sw/bin/nologin -d ${cfg.stateDir}" cfg.users
} }
});
''; '';
}; };
services.userdbd.enable = mkDefault true;
home-manager =
let
privPackages = mapAttrs (username: fid: {
home.packages =
let
# aid 0 is reserved
wrappers = imap1 (
aid: app:
let
extendDBusDefault = id: ext: {
filter = true;
talk = [ "org.freedesktop.Notifications" ] ++ ext.talk;
own =
(optionals (app.id != null) [
"${id}.*"
"org.mpris.MediaPlayer2.${id}.*"
])
++ ext.own;
inherit (ext) call broadcast;
};
dbusConfig =
let
default = {
talk = [ ];
own = [ ];
call = { };
broadcast = { };
};
in
{
session_bus =
if app.dbus.session != null then
(app.dbus.session (extendDBusDefault app.id))
else
(extendDBusDefault app.id default);
system_bus = app.dbus.system;
};
command = if app.command == null then app.name else app.command;
script = if app.script == null then ("exec " + command + " $@") else app.script;
enablements =
with app.capability;
(if wayland then 1 else 0)
+ (if x11 then 2 else 0)
+ (if dbus then 4 else 0)
+ (if pulse then 8 else 0);
conf = {
inherit (app) id;
command = [
(pkgs.writeScript "${app.name}-start" ''
#!${pkgs.zsh}${pkgs.zsh.shellPath}
${script}
'')
];
confinement = {
app_id = aid;
inherit (app) groups;
username = "u${toString fid}_a${toString aid}";
home = "${cfg.stateDir}/${toString fid}/${toString aid}";
sandbox = {
inherit (app)
userns
net
dev
env
;
map_real_uid = app.mapRealUid;
filesystem =
[
{ src = "/bin"; }
{ src = "/usr/bin"; }
{ src = "/nix/store"; }
{ src = "/run/current-system"; }
{
src = "/sys/block";
require = false;
}
{
src = "/sys/bus";
require = false;
}
{
src = "/sys/class";
require = false;
}
{
src = "/sys/dev";
require = false;
}
{
src = "/sys/devices";
require = false;
}
]
++ optionals app.nix [
{ src = "/nix/var"; }
{ src = "/var/db/nix-channels"; }
]
++ optionals (if app.gpu != null then app.gpu else app.capability.wayland || app.capability.x11) [
{ src = "/run/opengl-driver"; }
{
src = "/dev/dri";
dev = true;
}
]
++ app.extraPaths;
auto_etc = true;
override = [ "/var/run/nscd" ];
};
inherit enablements;
inherit (dbusConfig) session_bus system_bus;
};
};
in
pkgs.writeShellScriptBin app.name ''
exec fortify app ${pkgs.writeText "fortify-${app.name}.json" (builtins.toJSON conf)} $@
''
) cfg.apps;
in
foldr (
app: acc:
let
pkg = if app.share != null then app.share else pkgs.${app.name};
copy = source: "[ -d '${source}' ] && cp -Lrv '${source}' $out/share || true";
in
optional (app.capability.wayland || app.capability.x11) (
pkgs.runCommand "${app.name}-share" { } ''
mkdir -p $out/share
${copy "${pkg}/share/applications"}
${copy "${pkg}/share/pixmaps"}
${copy "${pkg}/share/icons"}
${copy "${pkg}/share/man"}
substituteInPlace $out/share/applications/* \
--replace-warn '${pkg}/bin/' "" \
--replace-warn '${pkg}/libexec/' ""
''
)
++ acc
) (wrappers ++ [ cfg.package ]) cfg.apps;
}) cfg.users;
in
{
useUserPackages = false; # prevent users.users entries from being added
users = foldlAttrs (
acc: _: fid:
mergeAttrsList (
# aid 0 is reserved
imap1 (aid: app: {
"u${toString fid}_a${toString aid}" = app.extraConfig // {
home.packages = app.packages;
};
}) cfg.apps
)
// acc
) privPackages cfg.users;
};
};
} }

531
options.md Normal file
View File

@ -0,0 +1,531 @@
## environment\.fortify\.enable
Whether to enable fortify\.
*Type:*
boolean
*Default:*
` false `
*Example:*
` true `
## environment\.fortify\.package
The fortify package to use\.
*Type:*
package
*Default:*
` <derivation fortify-0.1.0> `
## environment\.fortify\.apps
Declarative fortify apps\.
*Type:*
list of (submodule)
*Default:*
` [ ] `
## environment\.fortify\.apps\.\*\.packages
List of extra packages to install via home-manager\.
*Type:*
list of package
*Default:*
` [ ] `
## environment\.fortify\.apps\.\*\.capability\.dbus
Whether to proxy D-Bus\.
*Type:*
boolean
*Default:*
` true `
## environment\.fortify\.apps\.\*\.capability\.pulse
Whether to share the PulseAudio socket and cookie\.
*Type:*
boolean
*Default:*
` true `
## environment\.fortify\.apps\.\*\.capability\.wayland
Whether to share the Wayland socket\.
*Type:*
boolean
*Default:*
` true `
## environment\.fortify\.apps\.\*\.capability\.x11
Whether to share the X11 socket and allow connection\.
*Type:*
boolean
*Default:*
` false `
## environment\.fortify\.apps\.\*\.command
Command to run as the target user\.
Setting this to null will default command to launcher name\.
Has no effect when script is set\.
*Type:*
null or string
*Default:*
` null `
## environment\.fortify\.apps\.\*\.dbus\.session
D-Bus session bus custom configuration\.
Setting this to null will enable built-in defaults\.
*Type:*
null or (function that evaluates to a(n) anything)
*Default:*
` null `
## environment\.fortify\.apps\.\*\.dbus\.system
D-Bus system bus custom configuration\.
Setting this to null will disable the system bus proxy\.
*Type:*
null or anything
*Default:*
` null `
## environment\.fortify\.apps\.\*\.dev
Whether to enable access to all devices within the sandbox\.
*Type:*
boolean
*Default:*
` false `
*Example:*
` true `
## environment\.fortify\.apps\.\*\.env
Environment variables to set for the initial process in the sandbox\.
*Type:*
null or (attribute set of string)
*Default:*
` null `
## environment\.fortify\.apps\.\*\.extraConfig
Extra home-manager configuration\.
*Type:*
anything
*Default:*
` { } `
## environment\.fortify\.apps\.\*\.extraPaths
Extra paths to make available to the sandbox\.
*Type:*
list of anything
*Default:*
` [ ] `
## environment\.fortify\.apps\.\*\.gpu
Target process GPU and driver access\.
Setting this to null will enable GPU whenever X or Wayland is enabled\.
*Type:*
null or boolean
*Default:*
` null `
## environment\.fortify\.apps\.\*\.groups
List of groups to inherit from the privileged user\.
*Type:*
list of string
*Default:*
` [ ] `
## environment\.fortify\.apps\.\*\.id
Freedesktop application ID\.
*Type:*
null or string
*Default:*
` null `
## environment\.fortify\.apps\.\*\.mapRealUid
Whether to enable mapping to fortifys real UID within the sandbox\.
*Type:*
boolean
*Default:*
` false `
*Example:*
` true `
## environment\.fortify\.apps\.\*\.name
Name of the apps launcher script\.
*Type:*
string
## environment\.fortify\.apps\.\*\.net
Whether to enable network access within the sandbox\.
*Type:*
boolean
*Default:*
` true `
*Example:*
` true `
## environment\.fortify\.apps\.\*\.nix
Whether to enable nix daemon access within the sandbox\.
*Type:*
boolean
*Default:*
` false `
*Example:*
` true `
## environment\.fortify\.apps\.\*\.script
Application launch script\.
*Type:*
null or string
*Default:*
` null `
## environment\.fortify\.apps\.\*\.share
Package containing share files\.
Setting this to null will default package name to wrapper name\.
*Type:*
null or package
*Default:*
` null `
## environment\.fortify\.apps\.\*\.userns
Whether to enable userns within the sandbox\.
*Type:*
boolean
*Default:*
` false `
*Example:*
` true `
## environment\.fortify\.stateDir
The state directory where app home directories are stored\.
*Type:*
string
## environment\.fortify\.users
Users allowed to spawn fortify apps and their corresponding fortify fid\.
*Type:*
attribute set of integer between 0 and 99 (both inclusive)

214
options.nix Normal file
View File

@ -0,0 +1,214 @@
{ lib, pkgs, ... }:
let
inherit (lib) types mkOption mkEnableOption;
in
{
options = {
environment.fortify = {
enable = mkEnableOption "fortify";
package = mkOption {
type = types.package;
default = pkgs.callPackage ./package.nix { };
description = "The fortify package to use.";
};
users = mkOption {
type =
let
inherit (types) attrsOf ints;
in
attrsOf (ints.between 0 99);
description = ''
Users allowed to spawn fortify apps and their corresponding fortify fid.
'';
};
apps = mkOption {
type =
let
inherit (types)
str
enum
bool
package
anything
submodule
listOf
attrsOf
nullOr
functionTo
;
in
listOf (submodule {
options = {
name = mkOption {
type = str;
description = ''
Name of the app's launcher script.
'';
};
id = mkOption {
type = nullOr str;
default = null;
description = ''
Freedesktop application ID.
'';
};
packages = mkOption {
type = listOf package;
default = [ ];
description = ''
List of extra packages to install via home-manager.
'';
};
extraConfig = mkOption {
type = anything;
default = { };
description = ''
Extra home-manager configuration.
'';
};
script = mkOption {
type = nullOr str;
default = null;
description = ''
Application launch script.
'';
};
command = mkOption {
type = nullOr str;
default = null;
description = ''
Command to run as the target user.
Setting this to null will default command to launcher name.
Has no effect when script is set.
'';
};
groups = mkOption {
type = listOf str;
default = [ ];
description = ''
List of groups to inherit from the privileged user.
'';
};
dbus = {
session = mkOption {
type = nullOr (functionTo anything);
default = null;
description = ''
D-Bus session bus custom configuration.
Setting this to null will enable built-in defaults.
'';
};
system = mkOption {
type = nullOr anything;
default = null;
description = ''
D-Bus system bus custom configuration.
Setting this to null will disable the system bus proxy.
'';
};
};
env = mkOption {
type = nullOr (attrsOf str);
default = null;
description = ''
Environment variables to set for the initial process in the sandbox.
'';
};
nix = mkEnableOption "nix daemon access within the sandbox";
userns = mkEnableOption "userns within the sandbox";
mapRealUid = mkEnableOption "mapping to fortify's real UID within the sandbox";
dev = mkEnableOption "access to all devices within the sandbox";
net = mkEnableOption "network access within the sandbox" // {
default = true;
};
gpu = mkOption {
type = nullOr bool;
default = null;
description = ''
Target process GPU and driver access.
Setting this to null will enable GPU whenever X or Wayland is enabled.
'';
};
extraPaths = mkOption {
type = listOf anything;
default = [ ];
description = ''
Extra paths to make available to the sandbox.
'';
};
capability = {
wayland = mkOption {
type = bool;
default = true;
description = ''
Whether to share the Wayland socket.
'';
};
x11 = mkOption {
type = bool;
default = false;
description = ''
Whether to share the X11 socket and allow connection.
'';
};
dbus = mkOption {
type = bool;
default = true;
description = ''
Whether to proxy D-Bus.
'';
};
pulse = mkOption {
type = bool;
default = true;
description = ''
Whether to share the PulseAudio socket and cookie.
'';
};
};
share = mkOption {
type = nullOr package;
default = null;
description = ''
Package containing share files.
Setting this to null will default package name to wrapper name.
'';
};
};
});
default = [ ];
description = "Declarative fortify apps.";
};
stateDir = mkOption {
type = types.str;
description = ''
The state directory where app home directories are stored.
'';
};
};
};
}

View File

@ -10,7 +10,7 @@
buildGoModule rec { buildGoModule rec {
pname = "fortify"; pname = "fortify";
version = "0.0.11"; version = "0.2.1";
src = ./.; src = ./.;
vendorHash = null; vendorHash = null;
@ -29,13 +29,14 @@ buildGoModule rec {
"-s" "-s"
"-w" "-w"
"-X" "-X"
"main.Fmain=${placeholder "out"}/bin/.fortify-wrapped" "main.Fmain=${placeholder "out"}/libexec/fortify"
"-X"
"main.Fshim=${placeholder "out"}/libexec/fshim"
] ]
{ {
Version = "v${version}"; Version = "v${version}";
Fsu = "/run/wrappers/bin/fsu"; Fsu = "/run/wrappers/bin/fsu";
Fshim = "${placeholder "out"}/bin/.fshim"; Finit = "${placeholder "out"}/libexec/finit";
Finit = "${placeholder "out"}/bin/.finit";
}; };
buildInputs = [ buildInputs = [
@ -46,15 +47,15 @@ buildGoModule rec {
nativeBuildInputs = [ makeBinaryWrapper ]; nativeBuildInputs = [ makeBinaryWrapper ];
postInstall = '' postInstall = ''
wrapProgram $out/bin/${pname} --prefix PATH : ${ mkdir "$out/libexec"
mv "$out"/bin/* "$out/libexec/"
makeBinaryWrapper "$out/libexec/fortify" "$out/bin/fortify" \
--inherit-argv0 --prefix PATH : ${
lib.makeBinPath [ lib.makeBinPath [
bubblewrap bubblewrap
xdg-dbus-proxy xdg-dbus-proxy
] ]
} }
mv $out/bin/fsu $out/bin/.fsu
mv $out/bin/fshim $out/bin/.fshim
mv $out/bin/finit $out/bin/.finit
''; '';
} }

View File

@ -1,35 +0,0 @@
package main
import (
"flag"
"fmt"
"text/tabwriter"
"git.ophivana.moe/security/fortify/internal/fmsg"
"git.ophivana.moe/security/fortify/internal/state"
)
var (
stateActionEarly bool
)
func init() {
flag.BoolVar(&stateActionEarly, "state", false, "print state information of active launchers")
}
// tryState is called after app initialisation
func tryState() {
if stateActionEarly {
var w *tabwriter.Writer
state.MustPrintLauncherStateSimpleGlobal(&w, os.Paths().RunDirPath)
if w != nil {
if err := w.Flush(); err != nil {
fmsg.Println("cannot format output:", err)
}
} else {
fmt.Println("No information available")
}
fmsg.Exit(0)
}
}

View File

@ -1,27 +0,0 @@
package main
import (
"flag"
"fmt"
"git.ophivana.moe/security/fortify/internal"
)
var (
printVersion bool
)
func init() {
flag.BoolVar(&printVersion, "V", false, "Print version")
}
func tryVersion() {
if printVersion {
if v, ok := internal.Check(internal.Version); ok {
fmt.Println(v)
} else {
fmt.Println("impure")
}
os.Exit(0)
}
}