Compare commits

..

No commits in common. "master" and "v0.0.11" have entirely different histories.

36 changed files with 1172 additions and 2137 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)

167
README.md
View File

@ -2,7 +2,6 @@ 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.
@ -32,9 +31,7 @@ nix run git+https://git.ophivana.moe/security/fortify -- -h
## Module usage ## Module usage
The NixOS module currently requires home-manager to function correctly. The NixOS module currently requires home-manager and impermanence 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
@ -71,102 +68,112 @@ This adds the `environment.fortify` option:
{ {
environment.fortify = { environment.fortify = {
enable = true; enable = true;
stateDir = "/var/lib/persist/module/fortify"; user = "nixos";
users = { stateDir = "/var/lib/persist/module";
alice = 0; target = {
nixos = 10; chronos = {
}; launchers = {
weechat.method = "sudo";
claws-mail.capability.pulse = false;
apps = [ discord = {
{ command = "vesktop --ozone-platform-hint=wayland";
name = "chromium"; share = pkgs.vesktop;
id = "org.chromium.Chromium";
packages = [ pkgs.chromium ];
userns = true;
mapRealUid = true;
dbus = {
system = {
filter = true;
talk = [
"org.bluez"
"org.freedesktop.Avahi"
"org.freedesktop.UPower"
];
}; };
session =
f: chromium.dbus = {
f { configSystem = {
filter = true;
talk = [
"org.bluez"
"org.freedesktop.Avahi"
"org.freedesktop.UPower"
];
};
config = {
filter = true;
talk = [ talk = [
"org.freedesktop.DBus" "org.freedesktop.DBus"
"org.freedesktop.FileManager1" "org.freedesktop.FileManager1"
"org.freedesktop.Notifications" "org.freedesktop.Notifications"
"org.freedesktop.ScreenSaver" "org.freedesktop.ScreenSaver"
"org.freedesktop.secrets" "org.freedesktop.secrets"
"org.kde.kwalletd5" "org.kde.kwalletd5"
"org.kde.kwalletd6" "org.kde.kwalletd6"
]; ];
own = [ own = [
"org.chromium.Chromium.*" "org.chromium.Chromium.*"
"org.mpris.MediaPlayer2.org.chromium.Chromium.*" "org.mpris.MediaPlayer2.org.chromium.Chromium.*"
"org.mpris.MediaPlayer2.chromium.*" "org.mpris.MediaPlayer2.chromium.*"
]; ];
call = { }; call = {
broadcast = { }; "org.freedesktop.portal.*" = "*";
};
broadcast = {
"org.freedesktop.portal.*" = "@/org/freedesktop/portal/*";
};
}; };
};
}; };
} packages = with pkgs; [
{ weechat
name = "claws-mail"; claws-mail
id = "org.claws_mail.Claws-Mail"; vesktop
packages = [ pkgs.claws-mail ]; chromium
gpu = false; ];
capability.pulse = false; persistence.directories = [
} ".config/weechat"
{ ".claws-mail"
name = "weechat"; ".config/vesktop"
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 = { };
};
system.filter = true;
};
}
{
name = "looking-glass-client";
groups = [ "plugdev" ];
extraPaths = [
{
src = "/dev/shm/looking-glass";
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.Suspend() fmsg.Withhold()
// 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,8 +5,6 @@ import (
"net" "net"
"os" "os"
"os/exec" "os/exec"
"os/signal"
"strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
"syscall" "syscall"
@ -14,7 +12,6 @@ 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"
) )
@ -27,26 +24,24 @@ type Shim struct {
cmd *exec.Cmd cmd *exec.Cmd
// uid of shim target user // uid of shim target user
uid uint32 uid uint32
// string representation of application id // whether to check shim pid
aid string checkPid bool
// string representation of supplementary group ids // user switcher executable path
supp []string executable 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(uid uint32, aid string, supp []string, socket string, wl *shim0.Wayland, payload *shim0.Payload) *Shim { func New(executable string, uid uint32, socket string, wl *shim0.Wayland, payload *shim0.Payload, checkPid bool) *Shim {
return &Shim{uid: uid, aid: aid, supp: supp, socket: socket, wl: wl, payload: payload} return &Shim{uid: uid, executable: executable, socket: socket, wl: wl, payload: payload, checkPid: checkPid}
} }
func (s *Shim) String() string { func (s *Shim) String() string {
@ -73,11 +68,9 @@ func (s *Shim) AbortWait(err error) {
<-s.abort <-s.abort
} }
func (s *Shim) WaitFallback() chan error { type CommandBuilder func(shimEnv string) (args []string)
return s.killFallback
}
func (s *Shim) Start() (*time.Time, error) { func (s *Shim) Start(f CommandBuilder) (*time.Time, error) {
var ( var (
cf chan *net.UnixConn cf chan *net.UnixConn
accept func() accept func()
@ -94,51 +87,26 @@ func (s *Shim) Start() (*time.Time, error) {
} }
// start user switcher process and save time // start user switcher process and save time
var fsu string s.cmd = exec.Command(s.executable, f(shim0.Env+"="+s.socket)...)
if p, ok := internal.Check(internal.Fsu); !ok { s.cmd.Env = []string{}
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 fsu:", s.cmd) fmsg.VPrintln("starting shim via user switcher:", s.cmd)
fmsg.Suspend() // withhold messages to stderr fmsg.Withhold() // 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 fsu:") "cannot start user switcher:")
} }
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 {
s.killFallback <- err fmsg.Println("cannot terminate shim on faulted setup:", 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 {
@ -164,7 +132,7 @@ func (s *Shim) Start() (*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 cred.Pid != int32(s.cmd.Process.Pid) { } else if s.checkPid && 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(err.Error()) fmsg.Fatal("cannot dial setup socket:", err)
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.Fatalf("cannot decode shim payload: %v", err) fmsg.Fatal("cannot decode shim payload:", 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.Fatalf("cannot receive wayland fd: %v", err) fmsg.Fatal("cannot receive wayland fd:", err)
} else { } else {
wfd = fd wfd = fd
} }
@ -102,10 +102,7 @@ func main() {
} else { } else {
// no argv, look up shell instead // no argv, look up shell instead
var ok bool var ok bool
if payload.Bwrap.SetEnv == nil { if ic.Argv0, ok = os.LookupEnv("SHELL"); !ok {
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")
} }
@ -128,7 +125,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.Fatalf("cannot pipe: %v", err) fmsg.Fatal("cannot pipe:", 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)
@ -137,7 +134,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.Fatalf("cannot transmit init config: %v", err) fmsg.Fatal("cannot transmit init config:", err)
} }
}() }()
} }
@ -145,7 +142,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.Fatalf("malformed sandbox config: %v", err) fmsg.Fatal("malformed sandbox config:", 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
@ -157,7 +154,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.Fatalf("cannot start target process: %v", err) fmsg.Fatal("cannot start target process:", 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,12 +1,10 @@
package main package main
import ( import (
"bytes" "bufio"
"fmt"
"log" "log"
"os" "os"
"path" "path"
"slices"
"strconv" "strconv"
"strings" "strings"
"syscall" "syscall"
@ -17,15 +15,9 @@ 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 ( var Fmain = compPoison
Fmain = compPoison
Fshim = compPoison
)
func main() { func main() {
log.SetFlags(0) log.SetFlags(0)
@ -41,17 +33,12 @@ func main() {
log.Fatal("this program must not be started by root") log.Fatal("this program must not be started by root")
} }
var fmain, fshim string var fmain 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 {
@ -74,76 +61,84 @@ 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 {
// fortify requests target uid log.Fatal("FORTIFY_SHIM not set")
// 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
} }
// supplementary groups // allowed aid range 0 to 9999
var suppGroups, suppCurrent []int if as, ok := os.LookupEnv(envAID); !ok {
log.Fatal("FORTIFY_APP_ID not set")
if gs, ok := os.LookupEnv(envGroups); ok { } else if aid, err := strconv.Atoi(as); err != nil || aid < 0 || aid > 9999 {
if cur, err := os.Getgroups(); err != nil { log.Fatal("invalid aid")
log.Fatalf("cannot get groups: %v", err)
} else {
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 { } else {
suppGroups = []int{uid} uid += aid
} }
// 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 _, _, errno := syscall.AllThreadsSyscall(syscall.SYS_PRCTL, PR_SET_NO_NEW_PRIVS, 1, 0); errno != 0 { if err := syscall.Exec(fmain, []string{"fortify", "shim"}, []string{envShim + "=" + shimSetupPath}); err != nil {
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)
} }

View File

@ -1,77 +0,0 @@
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
}
}

View File

@ -1,69 +0,0 @@
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)
}

View File

@ -1,64 +0,0 @@
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 Normal file
View File

@ -0,0 +1,135 @@
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,33 +42,6 @@
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.us + ")" return "(sealed fortified app as uid " + a.seal.sys.user.Uid + ")"
} }
return "(unsealed fortified app)" return "(unsealed fortified app)"

View File

@ -2,7 +2,6 @@ package app_test
import ( import (
"fmt" "fmt"
"io"
"io/fs" "io/fs"
"os/user" "os/user"
"strconv" "strconv"
@ -19,12 +18,9 @@ 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),
Confinement: app.ConfinementConfig{ Method: "sudo",
AppID: 0,
Username: "chronos",
Outer: "/home/chronos",
},
}, },
app.ID{ app.ID{
0x4a, 0x45, 0x0b, 0x65, 0x4a, 0x45, 0x0b, 0x65,
@ -32,11 +28,11 @@ var testCasesNixos = []sealTestCase{
0xbd, 0x01, 0x78, 0x0e, 0xbd, 0x01, 0x78, 0x0e,
0xb9, 0xa6, 0x07, 0xac, 0xb9, 0xa6, 0x07, 0xac,
}, },
system.New(1000000). system.New(150).
Ensure("/tmp/fortify.1971", 0711). Ensure("/tmp/fortify.1971", 0701).
Ephemeral(system.Process, "/tmp/fortify.1971/4a450b6596d7bc15bd01780eb9a607ac", 0711). Ephemeral(system.Process, "/tmp/fortify.1971/4a450b6596d7bc15bd01780eb9a607ac", 0701).
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/0", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/0", acl.Read, acl.Write, acl.Execute). Ensure("/tmp/fortify.1971/tmpdir/150", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/150", 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).
@ -46,7 +42,6 @@ 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",
@ -59,11 +54,11 @@ var testCasesNixos = []sealTestCase{
DieWithParent: true, DieWithParent: true,
AsInit: true, AsInit: true,
}).SetUID(65534).SetGID(65534). }).SetUID(65534).SetGID(65534).
Procfs("/proc"). Procfs("/proc").DevTmpfs("/dev").Mqueue("/dev/mqueue").
Tmpfs("/fortify", 4096). Tmpfs("/dev/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).
@ -107,90 +102,88 @@ 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).
Bind("/etc", "/fortify/etc"). Symlink("/dev/fortify/etc/alsa", "/etc/alsa").
Symlink("/fortify/etc/alsa", "/etc/alsa"). Symlink("/dev/fortify/etc/bashrc", "/etc/bashrc").
Symlink("/fortify/etc/bashrc", "/etc/bashrc"). Symlink("/dev/fortify/etc/binfmt.d", "/etc/binfmt.d").
Symlink("/fortify/etc/binfmt.d", "/etc/binfmt.d"). Symlink("/dev/fortify/etc/dbus-1", "/etc/dbus-1").
Symlink("/fortify/etc/dbus-1", "/etc/dbus-1"). Symlink("/dev/fortify/etc/default", "/etc/default").
Symlink("/fortify/etc/default", "/etc/default"). Symlink("/dev/fortify/etc/ethertypes", "/etc/ethertypes").
Symlink("/fortify/etc/ethertypes", "/etc/ethertypes"). Symlink("/dev/fortify/etc/fonts", "/etc/fonts").
Symlink("/fortify/etc/fonts", "/etc/fonts"). Symlink("/dev/fortify/etc/fstab", "/etc/fstab").
Symlink("/fortify/etc/fstab", "/etc/fstab"). Symlink("/dev/fortify/etc/fuse.conf", "/etc/fuse.conf").
Symlink("/fortify/etc/fuse.conf", "/etc/fuse.conf"). Symlink("/dev/fortify/etc/host.conf", "/etc/host.conf").
Symlink("/fortify/etc/host.conf", "/etc/host.conf"). Symlink("/dev/fortify/etc/hostid", "/etc/hostid").
Symlink("/fortify/etc/hostid", "/etc/hostid"). Symlink("/dev/fortify/etc/hostname", "/etc/hostname").
Symlink("/fortify/etc/hostname", "/etc/hostname"). Symlink("/dev/fortify/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM").
Symlink("/fortify/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM"). Symlink("/dev/fortify/etc/hosts", "/etc/hosts").
Symlink("/fortify/etc/hosts", "/etc/hosts"). Symlink("/dev/fortify/etc/inputrc", "/etc/inputrc").
Symlink("/fortify/etc/inputrc", "/etc/inputrc"). Symlink("/dev/fortify/etc/ipsec.d", "/etc/ipsec.d").
Symlink("/fortify/etc/ipsec.d", "/etc/ipsec.d"). Symlink("/dev/fortify/etc/issue", "/etc/issue").
Symlink("/fortify/etc/issue", "/etc/issue"). Symlink("/dev/fortify/etc/kbd", "/etc/kbd").
Symlink("/fortify/etc/kbd", "/etc/kbd"). Symlink("/dev/fortify/etc/libblockdev", "/etc/libblockdev").
Symlink("/fortify/etc/libblockdev", "/etc/libblockdev"). Symlink("/dev/fortify/etc/locale.conf", "/etc/locale.conf").
Symlink("/fortify/etc/locale.conf", "/etc/locale.conf"). Symlink("/dev/fortify/etc/localtime", "/etc/localtime").
Symlink("/fortify/etc/localtime", "/etc/localtime"). Symlink("/dev/fortify/etc/login.defs", "/etc/login.defs").
Symlink("/fortify/etc/login.defs", "/etc/login.defs"). Symlink("/dev/fortify/etc/lsb-release", "/etc/lsb-release").
Symlink("/fortify/etc/lsb-release", "/etc/lsb-release"). Symlink("/dev/fortify/etc/lvm", "/etc/lvm").
Symlink("/fortify/etc/lvm", "/etc/lvm"). Symlink("/dev/fortify/etc/machine-id", "/etc/machine-id").
Symlink("/fortify/etc/machine-id", "/etc/machine-id"). Symlink("/dev/fortify/etc/man_db.conf", "/etc/man_db.conf").
Symlink("/fortify/etc/man_db.conf", "/etc/man_db.conf"). Symlink("/dev/fortify/etc/modprobe.d", "/etc/modprobe.d").
Symlink("/fortify/etc/modprobe.d", "/etc/modprobe.d"). Symlink("/dev/fortify/etc/modules-load.d", "/etc/modules-load.d").
Symlink("/fortify/etc/modules-load.d", "/etc/modules-load.d").
Symlink("/proc/mounts", "/etc/mtab"). Symlink("/proc/mounts", "/etc/mtab").
Symlink("/fortify/etc/nanorc", "/etc/nanorc"). Symlink("/dev/fortify/etc/nanorc", "/etc/nanorc").
Symlink("/fortify/etc/netgroup", "/etc/netgroup"). Symlink("/dev/fortify/etc/netgroup", "/etc/netgroup").
Symlink("/fortify/etc/NetworkManager", "/etc/NetworkManager"). Symlink("/dev/fortify/etc/NetworkManager", "/etc/NetworkManager").
Symlink("/fortify/etc/nix", "/etc/nix"). Symlink("/dev/fortify/etc/nix", "/etc/nix").
Symlink("/fortify/etc/nixos", "/etc/nixos"). Symlink("/dev/fortify/etc/nixos", "/etc/nixos").
Symlink("/fortify/etc/NIXOS", "/etc/NIXOS"). Symlink("/dev/fortify/etc/NIXOS", "/etc/NIXOS").
Symlink("/fortify/etc/nscd.conf", "/etc/nscd.conf"). Symlink("/dev/fortify/etc/nscd.conf", "/etc/nscd.conf").
Symlink("/fortify/etc/nsswitch.conf", "/etc/nsswitch.conf"). Symlink("/dev/fortify/etc/nsswitch.conf", "/etc/nsswitch.conf").
Symlink("/fortify/etc/opensnitchd", "/etc/opensnitchd"). Symlink("/dev/fortify/etc/opensnitchd", "/etc/opensnitchd").
Symlink("/fortify/etc/os-release", "/etc/os-release"). Symlink("/dev/fortify/etc/os-release", "/etc/os-release").
Symlink("/fortify/etc/pam", "/etc/pam"). Symlink("/dev/fortify/etc/pam", "/etc/pam").
Symlink("/fortify/etc/pam.d", "/etc/pam.d"). Symlink("/dev/fortify/etc/pam.d", "/etc/pam.d").
Symlink("/fortify/etc/pipewire", "/etc/pipewire"). Symlink("/dev/fortify/etc/pipewire", "/etc/pipewire").
Symlink("/fortify/etc/pki", "/etc/pki"). Symlink("/dev/fortify/etc/pki", "/etc/pki").
Symlink("/fortify/etc/polkit-1", "/etc/polkit-1"). Symlink("/dev/fortify/etc/polkit-1", "/etc/polkit-1").
Symlink("/fortify/etc/profile", "/etc/profile"). Symlink("/dev/fortify/etc/profile", "/etc/profile").
Symlink("/fortify/etc/protocols", "/etc/protocols"). Symlink("/dev/fortify/etc/protocols", "/etc/protocols").
Symlink("/fortify/etc/qemu", "/etc/qemu"). Symlink("/dev/fortify/etc/qemu", "/etc/qemu").
Symlink("/fortify/etc/resolv.conf", "/etc/resolv.conf"). Symlink("/dev/fortify/etc/resolv.conf", "/etc/resolv.conf").
Symlink("/fortify/etc/resolvconf.conf", "/etc/resolvconf.conf"). Symlink("/dev/fortify/etc/resolvconf.conf", "/etc/resolvconf.conf").
Symlink("/fortify/etc/rpc", "/etc/rpc"). Symlink("/dev/fortify/etc/rpc", "/etc/rpc").
Symlink("/fortify/etc/samba", "/etc/samba"). Symlink("/dev/fortify/etc/samba", "/etc/samba").
Symlink("/fortify/etc/sddm.conf", "/etc/sddm.conf"). Symlink("/dev/fortify/etc/sddm.conf", "/etc/sddm.conf").
Symlink("/fortify/etc/secureboot", "/etc/secureboot"). Symlink("/dev/fortify/etc/secureboot", "/etc/secureboot").
Symlink("/fortify/etc/services", "/etc/services"). Symlink("/dev/fortify/etc/services", "/etc/services").
Symlink("/fortify/etc/set-environment", "/etc/set-environment"). Symlink("/dev/fortify/etc/set-environment", "/etc/set-environment").
Symlink("/fortify/etc/shadow", "/etc/shadow"). Symlink("/dev/fortify/etc/shadow", "/etc/shadow").
Symlink("/fortify/etc/shells", "/etc/shells"). Symlink("/dev/fortify/etc/shells", "/etc/shells").
Symlink("/fortify/etc/ssh", "/etc/ssh"). Symlink("/dev/fortify/etc/ssh", "/etc/ssh").
Symlink("/fortify/etc/ssl", "/etc/ssl"). Symlink("/dev/fortify/etc/ssl", "/etc/ssl").
Symlink("/fortify/etc/static", "/etc/static"). Symlink("/dev/fortify/etc/static", "/etc/static").
Symlink("/fortify/etc/subgid", "/etc/subgid"). Symlink("/dev/fortify/etc/subgid", "/etc/subgid").
Symlink("/fortify/etc/subuid", "/etc/subuid"). Symlink("/dev/fortify/etc/subuid", "/etc/subuid").
Symlink("/fortify/etc/sudoers", "/etc/sudoers"). Symlink("/dev/fortify/etc/sudoers", "/etc/sudoers").
Symlink("/fortify/etc/sysctl.d", "/etc/sysctl.d"). Symlink("/dev/fortify/etc/sysctl.d", "/etc/sysctl.d").
Symlink("/fortify/etc/systemd", "/etc/systemd"). Symlink("/dev/fortify/etc/systemd", "/etc/systemd").
Symlink("/fortify/etc/terminfo", "/etc/terminfo"). Symlink("/dev/fortify/etc/terminfo", "/etc/terminfo").
Symlink("/fortify/etc/tmpfiles.d", "/etc/tmpfiles.d"). Symlink("/dev/fortify/etc/tmpfiles.d", "/etc/tmpfiles.d").
Symlink("/fortify/etc/udev", "/etc/udev"). Symlink("/dev/fortify/etc/udev", "/etc/udev").
Symlink("/fortify/etc/udisks2", "/etc/udisks2"). Symlink("/dev/fortify/etc/udisks2", "/etc/udisks2").
Symlink("/fortify/etc/UPower", "/etc/UPower"). Symlink("/dev/fortify/etc/UPower", "/etc/UPower").
Symlink("/fortify/etc/vconsole.conf", "/etc/vconsole.conf"). Symlink("/dev/fortify/etc/vconsole.conf", "/etc/vconsole.conf").
Symlink("/fortify/etc/X11", "/etc/X11"). Symlink("/dev/fortify/etc/X11", "/etc/X11").
Symlink("/fortify/etc/zfs", "/etc/zfs"). Symlink("/dev/fortify/etc/zfs", "/etc/zfs").
Symlink("/fortify/etc/zinputrc", "/etc/zinputrc"). Symlink("/dev/fortify/etc/zinputrc", "/etc/zinputrc").
Symlink("/fortify/etc/zoneinfo", "/etc/zoneinfo"). Symlink("/dev/fortify/etc/zoneinfo", "/etc/zoneinfo").
Symlink("/fortify/etc/zprofile", "/etc/zprofile"). Symlink("/dev/fortify/etc/zprofile", "/etc/zprofile").
Symlink("/fortify/etc/zshenv", "/etc/zshenv"). Symlink("/dev/fortify/etc/zshenv", "/etc/zshenv").
Symlink("/fortify/etc/zshrc", "/etc/zshrc"). Symlink("/dev/fortify/etc/zshrc", "/etc/zshrc").
Bind("/tmp/fortify.1971/tmpdir/0", "/tmp", false, true). Bind("/tmp/fortify.1971/tmpdir/150", "/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),
@ -199,12 +192,9 @@ 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",
@ -238,6 +228,7 @@ 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,
@ -245,11 +236,11 @@ var testCasesNixos = []sealTestCase{
0x82, 0xd4, 0x13, 0x36, 0x82, 0xd4, 0x13, 0x36,
0x9b, 0x64, 0xce, 0x7c, 0x9b, 0x64, 0xce, 0x7c,
}, },
system.New(1000009). system.New(150).
Ensure("/tmp/fortify.1971", 0711). Ensure("/tmp/fortify.1971", 0701).
Ephemeral(system.Process, "/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c", 0711). Ephemeral(system.Process, "/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c", 0701).
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/9", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/9", acl.Read, acl.Write, acl.Execute). Ensure("/tmp/fortify.1971/tmpdir/150", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/150", 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).
@ -294,7 +285,6 @@ 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",
@ -314,11 +304,11 @@ var testCasesNixos = []sealTestCase{
DieWithParent: true, DieWithParent: true,
AsInit: true, AsInit: true,
}).SetUID(65534).SetGID(65534). }).SetUID(65534).SetGID(65534).
Procfs("/proc"). Procfs("/proc").DevTmpfs("/dev").Mqueue("/dev/mqueue").
Tmpfs("/fortify", 4096). Tmpfs("/dev/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).
@ -363,90 +353,88 @@ 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).
Bind("/etc", "/fortify/etc"). Symlink("/dev/fortify/etc/alsa", "/etc/alsa").
Symlink("/fortify/etc/alsa", "/etc/alsa"). Symlink("/dev/fortify/etc/bashrc", "/etc/bashrc").
Symlink("/fortify/etc/bashrc", "/etc/bashrc"). Symlink("/dev/fortify/etc/binfmt.d", "/etc/binfmt.d").
Symlink("/fortify/etc/binfmt.d", "/etc/binfmt.d"). Symlink("/dev/fortify/etc/dbus-1", "/etc/dbus-1").
Symlink("/fortify/etc/dbus-1", "/etc/dbus-1"). Symlink("/dev/fortify/etc/default", "/etc/default").
Symlink("/fortify/etc/default", "/etc/default"). Symlink("/dev/fortify/etc/ethertypes", "/etc/ethertypes").
Symlink("/fortify/etc/ethertypes", "/etc/ethertypes"). Symlink("/dev/fortify/etc/fonts", "/etc/fonts").
Symlink("/fortify/etc/fonts", "/etc/fonts"). Symlink("/dev/fortify/etc/fstab", "/etc/fstab").
Symlink("/fortify/etc/fstab", "/etc/fstab"). Symlink("/dev/fortify/etc/fuse.conf", "/etc/fuse.conf").
Symlink("/fortify/etc/fuse.conf", "/etc/fuse.conf"). Symlink("/dev/fortify/etc/host.conf", "/etc/host.conf").
Symlink("/fortify/etc/host.conf", "/etc/host.conf"). Symlink("/dev/fortify/etc/hostid", "/etc/hostid").
Symlink("/fortify/etc/hostid", "/etc/hostid"). Symlink("/dev/fortify/etc/hostname", "/etc/hostname").
Symlink("/fortify/etc/hostname", "/etc/hostname"). Symlink("/dev/fortify/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM").
Symlink("/fortify/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM"). Symlink("/dev/fortify/etc/hosts", "/etc/hosts").
Symlink("/fortify/etc/hosts", "/etc/hosts"). Symlink("/dev/fortify/etc/inputrc", "/etc/inputrc").
Symlink("/fortify/etc/inputrc", "/etc/inputrc"). Symlink("/dev/fortify/etc/ipsec.d", "/etc/ipsec.d").
Symlink("/fortify/etc/ipsec.d", "/etc/ipsec.d"). Symlink("/dev/fortify/etc/issue", "/etc/issue").
Symlink("/fortify/etc/issue", "/etc/issue"). Symlink("/dev/fortify/etc/kbd", "/etc/kbd").
Symlink("/fortify/etc/kbd", "/etc/kbd"). Symlink("/dev/fortify/etc/libblockdev", "/etc/libblockdev").
Symlink("/fortify/etc/libblockdev", "/etc/libblockdev"). Symlink("/dev/fortify/etc/locale.conf", "/etc/locale.conf").
Symlink("/fortify/etc/locale.conf", "/etc/locale.conf"). Symlink("/dev/fortify/etc/localtime", "/etc/localtime").
Symlink("/fortify/etc/localtime", "/etc/localtime"). Symlink("/dev/fortify/etc/login.defs", "/etc/login.defs").
Symlink("/fortify/etc/login.defs", "/etc/login.defs"). Symlink("/dev/fortify/etc/lsb-release", "/etc/lsb-release").
Symlink("/fortify/etc/lsb-release", "/etc/lsb-release"). Symlink("/dev/fortify/etc/lvm", "/etc/lvm").
Symlink("/fortify/etc/lvm", "/etc/lvm"). Symlink("/dev/fortify/etc/machine-id", "/etc/machine-id").
Symlink("/fortify/etc/machine-id", "/etc/machine-id"). Symlink("/dev/fortify/etc/man_db.conf", "/etc/man_db.conf").
Symlink("/fortify/etc/man_db.conf", "/etc/man_db.conf"). Symlink("/dev/fortify/etc/modprobe.d", "/etc/modprobe.d").
Symlink("/fortify/etc/modprobe.d", "/etc/modprobe.d"). Symlink("/dev/fortify/etc/modules-load.d", "/etc/modules-load.d").
Symlink("/fortify/etc/modules-load.d", "/etc/modules-load.d").
Symlink("/proc/mounts", "/etc/mtab"). Symlink("/proc/mounts", "/etc/mtab").
Symlink("/fortify/etc/nanorc", "/etc/nanorc"). Symlink("/dev/fortify/etc/nanorc", "/etc/nanorc").
Symlink("/fortify/etc/netgroup", "/etc/netgroup"). Symlink("/dev/fortify/etc/netgroup", "/etc/netgroup").
Symlink("/fortify/etc/NetworkManager", "/etc/NetworkManager"). Symlink("/dev/fortify/etc/NetworkManager", "/etc/NetworkManager").
Symlink("/fortify/etc/nix", "/etc/nix"). Symlink("/dev/fortify/etc/nix", "/etc/nix").
Symlink("/fortify/etc/nixos", "/etc/nixos"). Symlink("/dev/fortify/etc/nixos", "/etc/nixos").
Symlink("/fortify/etc/NIXOS", "/etc/NIXOS"). Symlink("/dev/fortify/etc/NIXOS", "/etc/NIXOS").
Symlink("/fortify/etc/nscd.conf", "/etc/nscd.conf"). Symlink("/dev/fortify/etc/nscd.conf", "/etc/nscd.conf").
Symlink("/fortify/etc/nsswitch.conf", "/etc/nsswitch.conf"). Symlink("/dev/fortify/etc/nsswitch.conf", "/etc/nsswitch.conf").
Symlink("/fortify/etc/opensnitchd", "/etc/opensnitchd"). Symlink("/dev/fortify/etc/opensnitchd", "/etc/opensnitchd").
Symlink("/fortify/etc/os-release", "/etc/os-release"). Symlink("/dev/fortify/etc/os-release", "/etc/os-release").
Symlink("/fortify/etc/pam", "/etc/pam"). Symlink("/dev/fortify/etc/pam", "/etc/pam").
Symlink("/fortify/etc/pam.d", "/etc/pam.d"). Symlink("/dev/fortify/etc/pam.d", "/etc/pam.d").
Symlink("/fortify/etc/pipewire", "/etc/pipewire"). Symlink("/dev/fortify/etc/pipewire", "/etc/pipewire").
Symlink("/fortify/etc/pki", "/etc/pki"). Symlink("/dev/fortify/etc/pki", "/etc/pki").
Symlink("/fortify/etc/polkit-1", "/etc/polkit-1"). Symlink("/dev/fortify/etc/polkit-1", "/etc/polkit-1").
Symlink("/fortify/etc/profile", "/etc/profile"). Symlink("/dev/fortify/etc/profile", "/etc/profile").
Symlink("/fortify/etc/protocols", "/etc/protocols"). Symlink("/dev/fortify/etc/protocols", "/etc/protocols").
Symlink("/fortify/etc/qemu", "/etc/qemu"). Symlink("/dev/fortify/etc/qemu", "/etc/qemu").
Symlink("/fortify/etc/resolv.conf", "/etc/resolv.conf"). Symlink("/dev/fortify/etc/resolv.conf", "/etc/resolv.conf").
Symlink("/fortify/etc/resolvconf.conf", "/etc/resolvconf.conf"). Symlink("/dev/fortify/etc/resolvconf.conf", "/etc/resolvconf.conf").
Symlink("/fortify/etc/rpc", "/etc/rpc"). Symlink("/dev/fortify/etc/rpc", "/etc/rpc").
Symlink("/fortify/etc/samba", "/etc/samba"). Symlink("/dev/fortify/etc/samba", "/etc/samba").
Symlink("/fortify/etc/sddm.conf", "/etc/sddm.conf"). Symlink("/dev/fortify/etc/sddm.conf", "/etc/sddm.conf").
Symlink("/fortify/etc/secureboot", "/etc/secureboot"). Symlink("/dev/fortify/etc/secureboot", "/etc/secureboot").
Symlink("/fortify/etc/services", "/etc/services"). Symlink("/dev/fortify/etc/services", "/etc/services").
Symlink("/fortify/etc/set-environment", "/etc/set-environment"). Symlink("/dev/fortify/etc/set-environment", "/etc/set-environment").
Symlink("/fortify/etc/shadow", "/etc/shadow"). Symlink("/dev/fortify/etc/shadow", "/etc/shadow").
Symlink("/fortify/etc/shells", "/etc/shells"). Symlink("/dev/fortify/etc/shells", "/etc/shells").
Symlink("/fortify/etc/ssh", "/etc/ssh"). Symlink("/dev/fortify/etc/ssh", "/etc/ssh").
Symlink("/fortify/etc/ssl", "/etc/ssl"). Symlink("/dev/fortify/etc/ssl", "/etc/ssl").
Symlink("/fortify/etc/static", "/etc/static"). Symlink("/dev/fortify/etc/static", "/etc/static").
Symlink("/fortify/etc/subgid", "/etc/subgid"). Symlink("/dev/fortify/etc/subgid", "/etc/subgid").
Symlink("/fortify/etc/subuid", "/etc/subuid"). Symlink("/dev/fortify/etc/subuid", "/etc/subuid").
Symlink("/fortify/etc/sudoers", "/etc/sudoers"). Symlink("/dev/fortify/etc/sudoers", "/etc/sudoers").
Symlink("/fortify/etc/sysctl.d", "/etc/sysctl.d"). Symlink("/dev/fortify/etc/sysctl.d", "/etc/sysctl.d").
Symlink("/fortify/etc/systemd", "/etc/systemd"). Symlink("/dev/fortify/etc/systemd", "/etc/systemd").
Symlink("/fortify/etc/terminfo", "/etc/terminfo"). Symlink("/dev/fortify/etc/terminfo", "/etc/terminfo").
Symlink("/fortify/etc/tmpfiles.d", "/etc/tmpfiles.d"). Symlink("/dev/fortify/etc/tmpfiles.d", "/etc/tmpfiles.d").
Symlink("/fortify/etc/udev", "/etc/udev"). Symlink("/dev/fortify/etc/udev", "/etc/udev").
Symlink("/fortify/etc/udisks2", "/etc/udisks2"). Symlink("/dev/fortify/etc/udisks2", "/etc/udisks2").
Symlink("/fortify/etc/UPower", "/etc/UPower"). Symlink("/dev/fortify/etc/UPower", "/etc/UPower").
Symlink("/fortify/etc/vconsole.conf", "/etc/vconsole.conf"). Symlink("/dev/fortify/etc/vconsole.conf", "/etc/vconsole.conf").
Symlink("/fortify/etc/X11", "/etc/X11"). Symlink("/dev/fortify/etc/X11", "/etc/X11").
Symlink("/fortify/etc/zfs", "/etc/zfs"). Symlink("/dev/fortify/etc/zfs", "/etc/zfs").
Symlink("/fortify/etc/zinputrc", "/etc/zinputrc"). Symlink("/dev/fortify/etc/zinputrc", "/etc/zinputrc").
Symlink("/fortify/etc/zoneinfo", "/etc/zoneinfo"). Symlink("/dev/fortify/etc/zoneinfo", "/etc/zoneinfo").
Symlink("/fortify/etc/zprofile", "/etc/zprofile"). Symlink("/dev/fortify/etc/zprofile", "/etc/zprofile").
Symlink("/fortify/etc/zshenv", "/etc/zshenv"). Symlink("/dev/fortify/etc/zshenv", "/etc/zshenv").
Symlink("/fortify/etc/zshrc", "/etc/zshrc"). Symlink("/dev/fortify/etc/zshrc", "/etc/zshrc").
Bind("/tmp/fortify.1971/tmpdir/9", "/tmp", false, true). Bind("/tmp/fortify.1971/tmpdir/150", "/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").
@ -513,12 +501,23 @@ 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) LookupGroup(name string) (*user.Group, error) { func (s *stubNixOS) Lookup(username string) (*user.User, error) {
switch name { if s.usernameErr != nil {
case "video": if err, ok := s.usernameErr[username]; ok {
return &user.Group{Gid: "26", Name: "video"}, nil return nil, err
}
}
switch username {
case "chronos":
return &user.User{
Uid: "150",
Gid: "101",
Username: "chronos",
HomeDir: "/home/chronos",
}, nil
default: default:
return nil, user.UnknownGroupError(name) return nil, user.UnknownUserError(username)
} }
} }
@ -580,8 +579,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) Stdout() io.Writer { func (s *stubNixOS) FshimPath() string {
panic("requested stdout") return "/nix/store/00000000000000000000000000000000-fortify-0.0.10/bin/.fshim"
} }
func (s *stubNixOS) Paths() linux.Paths { func (s *stubNixOS) Paths() linux.Paths {
@ -592,10 +591,6 @@ 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,22 +1,23 @@
package app package app
import ( import (
"errors" "os"
"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"`
@ -24,16 +25,6 @@ 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"`
@ -56,12 +47,10 @@ 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
MapRealUID bool `json:"map_real_uid"` UseRealUID bool `json:"use_real_uid"`
// mediated access to wayland socket // mediated access to wayland socket
Wayland bool `json:"wayland,omitempty"` Wayland bool `json:"wayland,omitempty"`
@ -71,8 +60,6 @@ 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"`
} }
@ -92,16 +79,13 @@ 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(os linux.System) (*bwrap.Config, error) { func (s *SandboxConfig) Bwrap(uid int) *bwrap.Config {
if s == nil { if s == nil {
return nil, errors.New("nil sandbox config") return nil
} }
var uid int if !s.UseRealUID {
if !s.MapRealUID {
uid = 65534 uid = 65534
} else {
uid = os.Geteuid()
} }
conf := (&bwrap.Config{ conf := (&bwrap.Config{
@ -115,21 +99,11 @@ func (s *SandboxConfig) Bwrap(os linux.System) (*bwrap.Config, error) {
AsInit: true, AsInit: true,
// initialise map // initialise map
Chmod: make(bwrap.ChmodConfig), Chmod: make(map[string]os.FileMode),
}). }).
SetUID(uid).SetGID(uid). SetUID(uid).SetGID(uid).
Procfs("/proc"). Procfs("/proc").DevTmpfs("/dev").Mqueue("/dev/mqueue").
Tmpfs(fTmp, 4*1024) Tmpfs("/dev/fortify", 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 {
@ -147,35 +121,14 @@ func (s *SandboxConfig) Bwrap(os linux.System) (*bwrap.Config, error) {
conf.Symlink(l[0], l[1]) conf.Symlink(l[0], l[1])
} }
if s.AutoEtc { return conf
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",
@ -183,19 +136,13 @@ 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
@ -205,15 +152,12 @@ func Template() *Config {
"GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT", "GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT",
}, },
Filesystem: []*FilesystemConfig{ Filesystem: []*FilesystemConfig{
{Src: "/nix/store"}, {Src: "/nix"},
{Src: "/run/current-system"}, {Src: "/storage/emulated/0", Write: true, Must: true},
{Src: "/run/opengl-driver"}, {Src: "/data/user/0", Dst: "/data/data", Write: true, Must: true},
{Src: "/var/db/nix-channels"}, {Src: "/var/tmp", Write: true},
{Src: "/home/chronos", Write: true, Must: true},
{Src: "/dev/dri", Device: true},
}, },
Link: [][2]string{{"/run/user/65534", "/run/user/150"}}, Link: [][2]string{{"/dev/fortify/etc", "/etc"}},
AutoEtc: true,
Override: []string{"/var/run/nscd"}, Override: []string{"/var/run/nscd"},
}, },
SystemBus: &dbus.Config{ SystemBus: &dbus.Config{

View File

@ -0,0 +1,57 @@
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

@ -0,0 +1,30 @@
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,10 +2,9 @@ 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"
@ -16,14 +15,25 @@ import (
"git.ophivana.moe/security/fortify/internal/system" "git.ophivana.moe/security/fortify/internal/system"
) )
var ( const (
ErrConfig = errors.New("no configuration to seal") LaunchMethodSudo uint8 = iota
ErrUser = errors.New("invalid aid") LaunchMethodMachineCtl
ErrHome = errors.New("invalid home directory")
ErrName = errors.New("invalid username")
) )
var posixUsername = regexp.MustCompilePOSIX("^[a-z_]([A-Za-z0-9_-]{0,31}|[A-Za-z0-9_-]{0,30}\\$)$") var method = [...]string{
LaunchMethodSudo: "sudo",
LaunchMethodMachineCtl: "systemd",
}
var (
ErrConfig = errors.New("no configuration to seal")
ErrUser = errors.New("unknown user")
ErrLaunch = errors.New("invalid launch method")
ErrSudo = errors.New("sudo not available")
ErrSystemd = errors.New("systemd not available")
ErrMachineCtl = errors.New("machinectl not available")
)
// appSeal seals the application with child-related information // appSeal seals the application with child-related information
type appSeal struct { type appSeal struct {
@ -41,11 +51,15 @@ 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
@ -84,11 +98,39 @@ 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.MapRealUID { if config.Confinement.Sandbox != nil && config.Confinement.Sandbox.UseRealUID {
seal.sys.mappedID = a.os.Geteuid() seal.sys.mappedID = a.os.Geteuid()
} else { } else {
seal.sys.mappedID = 65534 seal.sys.mappedID = 65534
@ -96,51 +138,16 @@ 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)
// validate uid and set user info // look up user from system
if config.Confinement.AppID < 0 || config.Confinement.AppID > 9999 { if u, err := a.os.Lookup(config.User); err != nil {
return fmsg.WrapError(ErrUser, if errors.As(err, new(user.UnknownUserError)) {
fmt.Sprintf("aid %d out of range", config.Confinement.AppID)) return fmsg.WrapError(ErrUser, "unknown user", config.User)
} else {
seal.sys.user = appUser{
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.uid = u // unreachable
seal.sys.user.us = strconv.Itoa(u) panic(err)
}
// 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
}
} }
} else {
seal.sys.user = u
} }
// map sandbox config to bwrap // map sandbox config to bwrap
@ -152,7 +159,6 @@ 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 {
@ -167,8 +173,9 @@ 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})
} }
@ -201,14 +208,35 @@ 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
} }
if b, err := config.Confinement.Sandbox.Bwrap(a.os); err != nil { seal.sys.bwrap = config.Confinement.Sandbox.Bwrap(a.os.Geteuid())
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)
@ -223,10 +251,15 @@ 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.as) seal.store = state.NewSimple(seal.RunDirPath, seal.sys.user.Uid)
// initialise system interface with full uid // parse string UID
seal.sys.I = system.New(seal.sys.user.uid) if u, err := strconv.Atoi(seal.sys.user.Uid); err != nil {
// 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
@ -237,8 +270,11 @@ func (a *app) Seal(config *Config) error {
} }
// verbose log seal information // verbose log seal information
fmsg.VPrintf("created application seal for uid %s (%s) groups: %v, command: %s", fmsg.VPrintln("created application seal as user",
seal.sys.user.us, seal.sys.user.username, config.Confinement.Groups, config.Command) seal.sys.user.Username, "("+seal.sys.user.Uid+"),",
"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.us) seal.sys.ChangeHosts(seal.sys.user.Username)
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, 0711) seal.sys.Ensure(seal.SharePath, 0701)
// 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, 0711) seal.sys.Ephemeral(system.Process, seal.share, 0701)
// 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.as) targetTmpdir := path.Join(targetTmpdirParent, seal.sys.user.Uid)
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,21 +49,15 @@ 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.home != "" { if seal.sys.user.HomeDir != "" {
homeDir = seal.sys.user.home homeDir = seal.sys.user.HomeDir
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,13 +41,19 @@ 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.shim = shim.New(a.seal.toolPath, uint32(a.seal.sys.UID()), path.Join(a.seal.share, "shim"), a.seal.wl,
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,
@ -56,6 +62,9 @@ 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
@ -64,7 +73,7 @@ func (a *app) Start() error {
} }
a.seal.sys.needRevert = true a.seal.sys.needRevert = true
if startTime, err := a.shim.Start(); err != nil { if startTime, err := a.shim.Start(commandBuilder); err != nil {
return err return err
} else { } else {
// shim start and setup success, create process state // shim start and setup success, create process state
@ -72,6 +81,7 @@ 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,
} }
@ -156,35 +166,20 @@ 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
case err := <-wait: if err := cmd.Wait(); err != nil {
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 a.waitErr = err
a.waitErr = err
}
// store non-zero return code
r = exitError.ExitCode()
} else {
r = cmd.ProcessState.ExitCode()
} }
fmsg.VPrintf("process %d exited with exit code %d", cmd.Process.Pid, r)
// alternative exit path when kill was unsuccessful // store non-zero return code
case err := <-a.shim.WaitFallback(): r = exitError.ExitCode()
r = 255 } else {
if err != nil { r = cmd.ProcessState.ExitCode()
fmsg.Printf("cannot terminate shim on faulted setup: %v", err)
} else {
fmsg.VPrintln("alternative exit path selected")
}
} }
fmsg.VPrintf("process %d exited with exit code %d", cmd.Process.Pid, r)
} }
// child process exited, resume output // child process exited, resume output
@ -255,16 +250,9 @@ func (a *app) Wait() (int, error) {
} }
} }
if a.shim.Unwrap() == nil { a.shim.AbortWait(errors.New("shim exited"))
fmsg.VPrintln("fault before shim start") if err := a.seal.sys.Revert(ec); err != nil {
} else { return err.(RevertCompoundError)
a.shim.AbortWait(errors.New("shim exited"))
}
if a.seal.sys.needRevert {
if err := a.seal.sys.Revert(ec); err != nil {
return err.(RevertCompoundError)
}
} }
return nil return nil

View File

@ -1,6 +1,8 @@
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"
@ -16,7 +18,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 appUser user *user.User
// mapped uid and gid in user namespace // mapped uid and gid in user namespace
mappedID int mappedID int
@ -30,28 +32,6 @@ 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,17 +33,16 @@ 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)
} }
@ -57,10 +56,9 @@ func Exit(code int) {
os.Exit(code) os.Exit(code)
} }
func Suspend() { func Withhold() {
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,7 +1,6 @@
package linux package linux
import ( import (
"io"
"io/fs" "io/fs"
"os/user" "os/user"
"path" "path"
@ -22,8 +21,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)
// LookupGroup provides [user.LookupGroup]. // Lookup provides [user.Lookup].
LookupGroup(name string) (*user.Group, error) Lookup(username string) (*user.User, 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].
@ -32,13 +31,11 @@ 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,12 +2,10 @@ 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"
@ -22,75 +20,40 @@ type Std struct {
sdBooted bool sdBooted bool
sdBootedOnce sync.Once sdBootedOnce sync.Once
uidOnce sync.Once fshim string
uidCopy map[int]struct { fshimOnce sync.Once
uid int
err error
}
uidMu sync.RWMutex
} }
func (s *Std) Geteuid() int { return os.Geteuid() } func (s *Std) Geteuid() int { return os.Geteuid() }
func (s *Std) LookupEnv(key string) (string, bool) { return os.LookupEnv(key) } func (s *Std) LookupEnv(key string) (string, bool) { return os.LookupEnv(key) }
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) LookupGroup(name string) (*user.Group, error) { return user.LookupGroup(name) } func (s *Std) Lookup(username string) (*user.User, error) { return user.Lookup(username) }
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,6 +4,7 @@ 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, "\tPID\tApp\tUptime\tEnablements\tCommand") _, _ = fmt.Fprintln(*w, "\tUID\tPID\tUptime\tEnablements\tMethod\tCommand")
} else { } else {
// argv is emitted in body when verbose // argv is emitted in body when verbose
_, _ = fmt.Fprintln(*w, "\tPID\tApp\tArgv") _, _ = fmt.Fprintln(*w, "\tUID\tPID\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%d\t%s\t%s\t%s\t%s\n", _, _ = fmt.Fprintf(*w, "\t%s\t%d\t%s\t%s\t%s\t%s\n",
state.PID, s.path[len(s.path)-1], now.Sub(state.Time).Round(time.Second).String(), strings.TrimPrefix(ets.String(), ", "), s.path[len(s.path)-1], state.PID, now.Sub(state.Time).Round(time.Second).String(), strings.TrimPrefix(ets.String(), ", "), state.Method,
state.Command) state.Command)
} else { } else {
// emit argv instead when verbose // emit argv instead when verbose
_, _ = fmt.Fprintf(*w, "\t%d\t%s\t%s\n", _, _ = fmt.Fprintf(*w, "\t%s\t%d\t%s\n",
state.PID, s.path[len(s.path)-1], state.Argv) s.path[len(s.path)-1], state.PID, state.Argv)
} }
} }

View File

@ -33,6 +33,8 @@ 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

25
license.go Normal file
View File

@ -0,0 +1,25 @@
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)
}
}

266
main.go
View File

@ -1,30 +1,16 @@
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() {
@ -33,257 +19,38 @@ 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")
} }
flag.CommandLine.Usage = func() { // version/license/template command early exit
fmt.Println() tryVersion()
fmt.Println("Usage:\tfortify [-v] COMMAND [OPTIONS]") tryLicense()
fmt.Println() tryTemplate()
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)
args := flag.Args() // state query command early exit
if len(args) == 0 { tryState()
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
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")
}
// invoke app
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(config); err != nil { } else if err = a.Seal(loadConfig()); 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 {
@ -302,5 +69,4 @@ func runApp(config *app.Config) {
fmsg.Println("inner wait failed:", err) fmsg.Println("inner wait failed:", err)
} }
fmsg.Exit(r) fmsg.Exit(r)
panic("unreachable")
} }

452
nixos.nix
View File

@ -7,212 +7,290 @@
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
{ {
imports = [ ./options.nix ]; options = {
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 {
security.wrappers.fsu = { environment.persistence.${cfg.stateDir}.users = mapAttrs (_: target: target.persistence) cfg.target;
source = "${cfg.package}/libexec/fsu";
setuid = true;
owner = "root";
setgid = true;
group = "root";
};
environment.etc = { home-manager.users =
fsurc = { mapAttrs (_: target: target.extraConfig // { home.packages = target.packages; }) cfg.target
mode = "0400"; // {
text = foldlAttrs ( ${cfg.user}.home.packages =
acc: username: fid: let
"${toString config.users.users.${username}.uid} ${toString fid}\n" + acc wrap =
) "" cfg.users; user: launchers:
}; mapAttrsToList (
name: launcher:
userdb.source = pkgs.runCommand "fortify-userdb" { } '' with launcher.capability;
${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 let
extendDBusDefault = id: ext: { command = if launcher.command == null then name else launcher.command;
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 = dbusConfig =
let if launcher.dbus.config != null then
default = { pkgs.writeText "${name}-dbus.json" (builtins.toJSON launcher.dbus.config)
talk = [ ]; else
own = [ ]; null;
call = { }; dbusSystem =
broadcast = { }; if launcher.dbus.configSystem != null then
}; pkgs.writeText "${name}-dbus-system.json" (builtins.toJSON launcher.dbus.configSystem)
in else
{ null;
session_bus = capArgs =
if app.dbus.session != null then (if wayland then " --wayland" else "")
(app.dbus.session (extendDBusDefault app.id)) + (if x11 then " -X" else "")
else + (if dbus then " --dbus" else "")
(extendDBusDefault app.id default); + (if pulse then " --pulse" else "")
system_bus = app.dbus.system; + (if launcher.dbus.mpris then " --mpris" else "")
}; + (if launcher.dbus.id != null then " --dbus-id ${launcher.dbus.id}" else "")
command = if app.command == null then app.name else app.command; + (if dbusConfig != null then " --dbus-config ${dbusConfig}" else "")
script = if app.script == null then ("exec " + command + " $@") else app.script; + (if dbusSystem != null then " --dbus-system ${dbusSystem}" else "");
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 in
pkgs.writeShellScriptBin app.name '' pkgs.writeShellScriptBin name (
exec fortify app ${pkgs.writeText "fortify-${app.name}.json" (builtins.toJSON conf)} $@ if launcher.method == "simple" then
'' ''
) cfg.apps; exec sudo -u ${user} -i ${command} $@
in ''
foldr ( else
app: acc: ''
exec fortify${capArgs} --method ${launcher.method} -u ${user} $SHELL -c "exec ${command} $@"
''
)
) launchers;
in
foldlAttrs (
acc: user: target:
acc
++ (foldlAttrs (
shares: name: launcher:
let let
pkg = if app.share != null then app.share else pkgs.${app.name}; pkg = if launcher.share != null then launcher.share else pkgs.${name};
copy = source: "[ -d '${source}' ] && cp -Lrv '${source}' $out/share || true"; link = source: "[ -d '${source}' ] && ln -sv '${source}' $out/share || true";
in in
optional (app.capability.wayland || app.capability.x11) ( shares
pkgs.runCommand "${app.name}-share" { } '' ++
mkdir -p $out/share optional (launcher.method != "simple" && (launcher.capability.wayland || launcher.capability.x11))
${copy "${pkg}/share/applications"} (
${copy "${pkg}/share/pixmaps"} pkgs.runCommand "${name}-share" { } ''
${copy "${pkg}/share/icons"} mkdir -p $out/share
${copy "${pkg}/share/man"} ${link "${pkg}/share/applications"}
${link "${pkg}/share/icons"}
substituteInPlace $out/share/applications/* \ ${link "${pkg}/share/man"}
--replace-warn '${pkg}/bin/' "" \ ''
--replace-warn '${pkg}/libexec/' "" )
'' ) (wrap user target.launchers) target.launchers)
) ) [ cfg.package ] cfg.target;
++ 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;
}; };
security.polkit.extraConfig =
let
allowList = builtins.toJSON (mapAttrsToList (name: _: name) cfg.target);
in
''
polkit.addRule(function(action, subject) {
if (action.id == "org.freedesktop.machine1.host-shell" &&
${allowList}.indexOf(action.lookup("user")) > -1 &&
subject.user == "${cfg.user}") {
return polkit.Result.YES;
}
});
'';
}; };
} }

View File

@ -1,531 +0,0 @@
## 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)

View File

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

35
state.go Normal file
View File

@ -0,0 +1,35 @@
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)
}
}

27
version.go Normal file
View File

@ -0,0 +1,27 @@
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)
}
}