Compare commits
6 Commits
c4d6651cae
...
e2489059c1
Author | SHA1 | Date | |
---|---|---|---|
e2489059c1 | |||
2e3f6a4c51 | |||
2162029f46 | |||
a1148edd00 | |||
6acd0d4e88 | |||
35b7142317 |
@ -136,11 +136,17 @@ func (s *SandboxConfig) Bwrap(os linux.System) (*bwrap.Config, error) {
|
||||
}
|
||||
|
||||
conf := (&bwrap.Config{
|
||||
Net: s.Net,
|
||||
UserNS: s.UserNS,
|
||||
Hostname: s.Hostname,
|
||||
Clearenv: true,
|
||||
SetEnv: s.Env,
|
||||
Net: s.Net,
|
||||
UserNS: s.UserNS,
|
||||
Hostname: s.Hostname,
|
||||
Clearenv: true,
|
||||
SetEnv: s.Env,
|
||||
|
||||
/* this is only 4 KiB of memory on a 64-bit system,
|
||||
permissive defaults on NixOS results in around 100 entries
|
||||
so this capacity should eliminate copies for most setups */
|
||||
Filesystem: make([]bwrap.FSBuilder, 0, 256),
|
||||
|
||||
NewSession: !s.NoNewSession,
|
||||
DieWithParent: true,
|
||||
AsInit: true,
|
||||
|
5
fst/info.go
Normal file
5
fst/info.go
Normal file
@ -0,0 +1,5 @@
|
||||
package fst
|
||||
|
||||
type Info struct {
|
||||
User int `json:"user"`
|
||||
}
|
@ -4,10 +4,20 @@ const (
|
||||
Tmpfs = iota
|
||||
Dir
|
||||
Symlink
|
||||
|
||||
OverlaySrc
|
||||
Overlay
|
||||
TmpOverlay
|
||||
ROOverlay
|
||||
)
|
||||
|
||||
var awkwardArgs = [...]string{
|
||||
Tmpfs: "--tmpfs",
|
||||
Dir: "--dir",
|
||||
Symlink: "--symlink",
|
||||
|
||||
OverlaySrc: "--overlay-src",
|
||||
Overlay: "--overlay",
|
||||
TmpOverlay: "--tmp-overlay",
|
||||
ROOverlay: "--ro-overlay",
|
||||
}
|
||||
|
@ -52,7 +52,7 @@ type Config struct {
|
||||
LockFile []string `json:"lock_file,omitempty"`
|
||||
|
||||
// ordered filesystem args
|
||||
Filesystem []FSBuilder
|
||||
Filesystem []FSBuilder `json:"filesystem,omitempty"`
|
||||
|
||||
// change permissions (must already exist)
|
||||
// (--chmod OCTAL PATH)
|
||||
@ -78,6 +78,8 @@ type Config struct {
|
||||
--userns FD Use this user namespace (cannot combine with --unshare-user)
|
||||
--userns2 FD After setup switch to this user namespace, only useful with --userns
|
||||
--pidns FD Use this pid namespace (as parent namespace if using --unshare-pid)
|
||||
--bind-fd FD DEST Bind open directory or path fd on DEST
|
||||
--ro-bind-fd FD DEST Bind open directory or path fd read-only on DEST
|
||||
--exec-label LABEL Exec label for the sandbox
|
||||
--file-label LABEL File label for temporary sandbox content
|
||||
--file FD DEST Copy from FD to destination DEST
|
||||
@ -178,6 +180,74 @@ func (t *TmpfsConfig) Append(args *[]string) {
|
||||
*args = append(*args, awkwardArgs[Tmpfs], t.Dir)
|
||||
}
|
||||
|
||||
type OverlayConfig struct {
|
||||
/*
|
||||
read files from SRC in the following overlay
|
||||
(--overlay-src SRC)
|
||||
*/
|
||||
Src []string `json:"src,omitempty"`
|
||||
|
||||
/*
|
||||
mount overlayfs on DEST, with RWSRC as the host path for writes and
|
||||
WORKDIR an empty directory on the same filesystem as RWSRC
|
||||
(--overlay RWSRC WORKDIR DEST)
|
||||
|
||||
if nil, mount overlayfs on DEST, with writes going to an invisible tmpfs
|
||||
(--tmp-overlay DEST)
|
||||
|
||||
if either strings are empty, mount overlayfs read-only on DEST
|
||||
(--ro-overlay DEST)
|
||||
*/
|
||||
Persist *[2]string `json:"persist,omitempty"`
|
||||
|
||||
/*
|
||||
--overlay RWSRC WORKDIR DEST
|
||||
|
||||
--tmp-overlay DEST
|
||||
|
||||
--ro-overlay DEST
|
||||
*/
|
||||
Dest string `json:"dest"`
|
||||
}
|
||||
|
||||
func (o *OverlayConfig) Path() string {
|
||||
return o.Dest
|
||||
}
|
||||
|
||||
func (o *OverlayConfig) Len() int {
|
||||
// (--tmp-overlay DEST) or (--ro-overlay DEST)
|
||||
p := 2
|
||||
// (--overlay RWSRC WORKDIR DEST)
|
||||
if o.Persist != nil && o.Persist[0] != "" && o.Persist[1] != "" {
|
||||
p = 4
|
||||
}
|
||||
|
||||
return p + len(o.Src)*2
|
||||
}
|
||||
|
||||
func (o *OverlayConfig) Append(args *[]string) {
|
||||
// --overlay-src SRC
|
||||
for _, src := range o.Src {
|
||||
*args = append(*args, awkwardArgs[OverlaySrc], src)
|
||||
}
|
||||
|
||||
if o.Persist != nil {
|
||||
if o.Persist[0] != "" && o.Persist[1] != "" {
|
||||
// --overlay RWSRC WORKDIR
|
||||
*args = append(*args, awkwardArgs[Overlay], o.Persist[0], o.Persist[1])
|
||||
} else {
|
||||
// --ro-overlay
|
||||
*args = append(*args, awkwardArgs[ROOverlay])
|
||||
}
|
||||
} else {
|
||||
// --tmp-overlay
|
||||
*args = append(*args, awkwardArgs[TmpOverlay])
|
||||
}
|
||||
|
||||
// DEST
|
||||
*args = append(*args, o.Dest)
|
||||
}
|
||||
|
||||
type SymlinkConfig [2]string
|
||||
|
||||
func (s SymlinkConfig) Path() string {
|
||||
|
@ -96,6 +96,31 @@ func (c *Config) Tmpfs(dest string, size int, perm ...os.FileMode) *Config {
|
||||
return c
|
||||
}
|
||||
|
||||
// Overlay mount overlayfs on DEST, with writes going to an invisible tmpfs
|
||||
// (--tmp-overlay DEST)
|
||||
func (c *Config) Overlay(dest string, src ...string) *Config {
|
||||
c.Filesystem = append(c.Filesystem, &OverlayConfig{Src: src, Dest: dest})
|
||||
return c
|
||||
}
|
||||
|
||||
// Join mount overlayfs read-only on DEST
|
||||
// (--ro-overlay DEST)
|
||||
func (c *Config) Join(dest string, src ...string) *Config {
|
||||
c.Filesystem = append(c.Filesystem, &OverlayConfig{Src: src, Dest: dest, Persist: new([2]string)})
|
||||
return c
|
||||
}
|
||||
|
||||
// Persist mount overlayfs on DEST, with RWSRC as the host path for writes and
|
||||
// WORKDIR an empty directory on the same filesystem as RWSRC
|
||||
// (--overlay RWSRC WORKDIR DEST)
|
||||
func (c *Config) Persist(dest, rwsrc, workdir string, src ...string) *Config {
|
||||
if rwsrc == "" || workdir == "" {
|
||||
panic("persist called without required paths")
|
||||
}
|
||||
c.Filesystem = append(c.Filesystem, &OverlayConfig{Src: src, Dest: dest, Persist: &[2]string{rwsrc, workdir}})
|
||||
return c
|
||||
}
|
||||
|
||||
// Mqueue mount new mqueue in sandbox
|
||||
// (--mqueue DEST)
|
||||
func (c *Config) Mqueue(dest string) *Config {
|
||||
|
@ -1,19 +1,40 @@
|
||||
package bwrap
|
||||
package bwrap_test
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/helper/bwrap"
|
||||
)
|
||||
|
||||
func TestConfig_Args(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
conf *Config
|
||||
conf *bwrap.Config
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "overlayfs",
|
||||
conf: (new(bwrap.Config)).
|
||||
Overlay("/etc", "/etc").
|
||||
Join("/.fortify/bin", "/bin", "/usr/bin", "/usr/local/bin").
|
||||
Persist("/nix", "/data/data/org.chromium.Chromium/overlay/rwsrc", "/data/data/org.chromium.Chromium/workdir", "/data/app/org.chromium.Chromium/nix"),
|
||||
want: []string{
|
||||
"--unshare-all", "--unshare-user",
|
||||
"--disable-userns", "--assert-userns-disabled",
|
||||
// Overlay("/etc", "/etc")
|
||||
"--overlay-src", "/etc", "--tmp-overlay", "/etc",
|
||||
// Join("/.fortify/bin", "/bin", "/usr/bin", "/usr/local/bin")
|
||||
"--overlay-src", "/bin", "--overlay-src", "/usr/bin",
|
||||
"--overlay-src", "/usr/local/bin", "--ro-overlay", "/.fortify/bin",
|
||||
// Persist("/nix", "/data/data/org.chromium.Chromium/overlay/rwsrc", "/data/data/org.chromium.Chromium/workdir", "/data/app/org.chromium.Chromium/nix")
|
||||
"--overlay-src", "/data/app/org.chromium.Chromium/nix",
|
||||
"--overlay", "/data/data/org.chromium.Chromium/overlay/rwsrc", "/data/data/org.chromium.Chromium/workdir", "/nix",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "xdg-dbus-proxy constraint sample",
|
||||
conf: (&Config{
|
||||
conf: (&bwrap.Config{
|
||||
Unshare: nil,
|
||||
UserNS: false,
|
||||
Clearenv: true,
|
||||
@ -71,7 +92,7 @@ func TestConfig_Args(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "fortify permissive default nixos",
|
||||
conf: (&Config{
|
||||
conf: (&bwrap.Config{
|
||||
Unshare: nil,
|
||||
Net: true,
|
||||
UserNS: true,
|
||||
|
@ -1,6 +1,7 @@
|
||||
package linux
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
@ -8,6 +9,7 @@ import (
|
||||
"os/user"
|
||||
"strconv"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/internal"
|
||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||
@ -79,9 +81,15 @@ func (s *Std) Uid(aid int) (int, error) {
|
||||
cmd.Stderr = os.Stderr // pass through fatal messages
|
||||
cmd.Env = []string{"FORTIFY_APP_ID=" + strconv.Itoa(aid)}
|
||||
cmd.Dir = "/"
|
||||
var p []byte
|
||||
var (
|
||||
p []byte
|
||||
exitError *exec.ExitError
|
||||
)
|
||||
|
||||
if p, u.err = cmd.Output(); u.err == nil {
|
||||
u.uid, u.err = strconv.Atoi(string(p))
|
||||
} else if errors.As(u.err, &exitError) && exitError != nil && exitError.ExitCode() == 1 {
|
||||
u.err = syscall.EACCES
|
||||
}
|
||||
return u.uid, u.err
|
||||
}
|
||||
|
36
main.go
36
main.go
@ -4,6 +4,7 @@ import (
|
||||
_ "embed"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/user"
|
||||
"strconv"
|
||||
"strings"
|
||||
@ -16,7 +17,6 @@ import (
|
||||
"git.gensokyo.uk/security/fortify/internal/app"
|
||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||
"git.gensokyo.uk/security/fortify/internal/linux"
|
||||
"git.gensokyo.uk/security/fortify/internal/state"
|
||||
"git.gensokyo.uk/security/fortify/internal/system"
|
||||
)
|
||||
|
||||
@ -33,7 +33,7 @@ func init() {
|
||||
flag.BoolVar(&flagJSON, "json", false, "Format output in JSON when applicable")
|
||||
}
|
||||
|
||||
var os = new(linux.Std)
|
||||
var sys linux.System = new(linux.Std)
|
||||
|
||||
type gl []string
|
||||
|
||||
@ -65,7 +65,7 @@ func main() {
|
||||
fmt.Println("Usage:\tfortify [-v] [--json] COMMAND [OPTIONS]")
|
||||
fmt.Println()
|
||||
fmt.Println("Commands:")
|
||||
w := tabwriter.NewWriter(os.Stdout(), 0, 1, 4, ' ', 0)
|
||||
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"},
|
||||
@ -128,24 +128,20 @@ func main() {
|
||||
// Ignore errors; set is set for ExitOnError.
|
||||
_ = set.Parse(args[1:])
|
||||
|
||||
var (
|
||||
config *fst.Config
|
||||
instance *state.State
|
||||
name string
|
||||
)
|
||||
|
||||
if len(set.Args()) != 1 {
|
||||
switch len(set.Args()) {
|
||||
case 0: // system
|
||||
printShowSystem(short)
|
||||
case 1: // instance
|
||||
name := set.Args()[0]
|
||||
config, instance := tryShort(name)
|
||||
if config == nil {
|
||||
config = tryPath(name)
|
||||
}
|
||||
printShowInstance(instance, config, short)
|
||||
default:
|
||||
fmsg.Fatal("show requires 1 argument")
|
||||
} else {
|
||||
name = set.Args()[0]
|
||||
config, instance = tryShort(name)
|
||||
}
|
||||
|
||||
if config == nil {
|
||||
config = tryPath(name)
|
||||
}
|
||||
|
||||
printShow(instance, config, short)
|
||||
fmsg.Exit(0)
|
||||
case "app": // launch app from configuration file
|
||||
if len(args) < 2 {
|
||||
@ -211,7 +207,7 @@ func main() {
|
||||
passwdOnce sync.Once
|
||||
passwdFunc = func() {
|
||||
var us string
|
||||
if uid, err := os.Uid(aid); err != nil {
|
||||
if uid, err := sys.Uid(aid); err != nil {
|
||||
fmsg.Fatalf("cannot obtain uid from fsu: %v", err)
|
||||
} else {
|
||||
us = strconv.Itoa(uid)
|
||||
@ -292,7 +288,7 @@ func main() {
|
||||
}
|
||||
|
||||
func runApp(config *fst.Config) {
|
||||
a, err := app.New(os)
|
||||
a, err := app.New(sys)
|
||||
if err != nil {
|
||||
fmsg.Fatalf("cannot create app: %s\n", err)
|
||||
} else if err = a.Seal(config); err != nil {
|
||||
|
8
parse.go
8
parse.go
@ -4,7 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
direct "os"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
@ -38,7 +38,7 @@ func tryPath(name string) (config *fst.Config) {
|
||||
}()
|
||||
}
|
||||
} else {
|
||||
r = direct.Stdin
|
||||
r = os.Stdin
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r).Decode(&config); err != nil {
|
||||
@ -61,7 +61,7 @@ func tryFd(name string) io.ReadCloser {
|
||||
}
|
||||
fmsg.Fatalf("cannot get fd %d: %v", fd, errno)
|
||||
}
|
||||
return direct.NewFile(fd, strconv.Itoa(v))
|
||||
return os.NewFile(fd, strconv.Itoa(v))
|
||||
}
|
||||
}
|
||||
|
||||
@ -85,7 +85,7 @@ func tryShort(name string) (config *fst.Config, instance *state.State) {
|
||||
if likePrefix && len(name) >= 8 {
|
||||
fmsg.VPrintln("argument looks like prefix")
|
||||
|
||||
s := state.NewMulti(os.Paths().RunDirPath)
|
||||
s := state.NewMulti(sys.Paths().RunDirPath)
|
||||
if entries, err := state.Join(s); err != nil {
|
||||
fmsg.Printf("cannot join store: %v", err)
|
||||
// drop to fetch from file
|
||||
|
39
print.go
39
print.go
@ -16,14 +16,37 @@ import (
|
||||
"git.gensokyo.uk/security/fortify/internal/state"
|
||||
)
|
||||
|
||||
func printShow(instance *state.State, config *fst.Config, short bool) {
|
||||
if flagJSON {
|
||||
v := any(config)
|
||||
if instance != nil {
|
||||
v = instance
|
||||
}
|
||||
func printShowSystem(short bool) {
|
||||
info := new(fst.Info)
|
||||
|
||||
printJSON(v)
|
||||
// get fid by querying uid of aid 0
|
||||
if uid, err := sys.Uid(0); err != nil {
|
||||
fmsg.Fatalf("cannot obtain uid from fsu: %v", err)
|
||||
} else {
|
||||
info.User = (uid / 10000) - 100
|
||||
}
|
||||
|
||||
if flagJSON {
|
||||
printJSON(info)
|
||||
return
|
||||
}
|
||||
|
||||
w := tabwriter.NewWriter(direct.Stdout, 0, 1, 4, ' ', 0)
|
||||
|
||||
fmt.Fprintf(w, "User:\t%d\n", info.User)
|
||||
|
||||
if err := w.Flush(); err != nil {
|
||||
fmsg.Fatalf("cannot flush tabwriter: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func printShowInstance(instance *state.State, config *fst.Config, short bool) {
|
||||
if flagJSON {
|
||||
if instance != nil {
|
||||
printJSON(instance)
|
||||
} else {
|
||||
printJSON(config)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@ -171,7 +194,7 @@ func printPs(short bool) {
|
||||
now := time.Now().UTC()
|
||||
|
||||
var entries state.Entries
|
||||
s := state.NewMulti(os.Paths().RunDirPath)
|
||||
s := state.NewMulti(sys.Paths().RunDirPath)
|
||||
if e, err := state.Join(s); err != nil {
|
||||
fmsg.Fatalf("cannot join store: %v", err)
|
||||
} else {
|
||||
|
21
test.nix
21
test.nix
@ -19,11 +19,19 @@ nixosTest {
|
||||
nodes.machine =
|
||||
{ lib, pkgs, ... }:
|
||||
{
|
||||
users.users.alice = {
|
||||
isNormalUser = true;
|
||||
description = "Alice Foobar";
|
||||
password = "foobar";
|
||||
uid = 1000;
|
||||
users.users = {
|
||||
alice = {
|
||||
isNormalUser = true;
|
||||
description = "Alice Foobar";
|
||||
password = "foobar";
|
||||
uid = 1000;
|
||||
};
|
||||
untrusted = {
|
||||
isNormalUser = true;
|
||||
description = "Untrusted user";
|
||||
password = "foobar";
|
||||
uid = 1001;
|
||||
};
|
||||
};
|
||||
|
||||
home-manager.users.alice.home.stateVersion = "24.11";
|
||||
@ -198,6 +206,9 @@ nixosTest {
|
||||
machine.wait_for_file("/run/user/1000/wayland-1")
|
||||
machine.wait_for_file("/tmp/sway-ipc.sock")
|
||||
|
||||
# Deny unmapped uid:
|
||||
print(machine.fail("sudo -u untrusted -i ${self.packages.${system}.fortify}/bin/fortify -v run"))
|
||||
|
||||
# Create fortify uid 0 state directory:
|
||||
machine.succeed("install -dm 0755 -o u0_a0 -g users /var/lib/fortify/u0")
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user