aboutsummaryrefslogtreecommitdiffhomepage
path: root/fst
diff options
context:
space:
mode:
Diffstat (limited to 'fst')
-rw-r--r--fst/app.go3
-rw-r--r--fst/config.go51
-rw-r--r--fst/sandbox.go263
3 files changed, 175 insertions, 142 deletions
diff --git a/fst/app.go b/fst/app.go
index 87a836ca..3e026391 100644
--- a/fst/app.go
+++ b/fst/app.go
@@ -2,7 +2,6 @@
package fst
import (
- "context"
"time"
)
@@ -19,7 +18,7 @@ type App interface {
type SealedApp interface {
// Run commits sealed system setup and starts the app process.
- Run(ctx context.Context, rs *RunState) error
+ Run(rs *RunState) error
}
// RunState stores the outcome of a call to [SealedApp.Run].
diff --git a/fst/config.go b/fst/config.go
index d96934d4..7fc3af6c 100644
--- a/fst/config.go
+++ b/fst/config.go
@@ -2,7 +2,7 @@ package fst
import (
"git.gensokyo.uk/security/fortify/dbus"
- "git.gensokyo.uk/security/fortify/helper/bwrap"
+ "git.gensokyo.uk/security/fortify/sandbox/seccomp"
"git.gensokyo.uk/security/fortify/system"
)
@@ -14,8 +14,11 @@ type Config struct {
// passed to wayland security-context-v1 as application ID
// and used as part of defaults in dbus session proxy
ID string `json:"id"`
- // final argv, passed to init
- Command []string `json:"command"`
+
+ // absolute path to executable file
+ Path string `json:"path,omitempty"`
+ // final args passed to container init
+ Args []string `json:"args"`
Confinement ConfinementConfig `json:"confinement"`
}
@@ -26,13 +29,13 @@ type ConfinementConfig struct {
AppID int `json:"app_id"`
// list of supplementary groups to inherit
Groups []string `json:"groups"`
- // passwd username in the sandbox, defaults to passwd name of target uid or chronos
+ // passwd username in container, defaults to passwd name of target uid or chronos
Username string `json:"username,omitempty"`
- // home directory in sandbox, empty for outer
+ // home directory in container, empty for outer
Inner string `json:"home_inner"`
// home directory in init namespace
Outer string `json:"home"`
- // bwrap sandbox confinement configuration
+ // abstract sandbox configuration
Sandbox *SandboxConfig `json:"sandbox"`
// extra acl ops, runs after everything else
ExtraPerms []*ExtraPermConfig `json:"extra_perms,omitempty"`
@@ -44,7 +47,7 @@ type ConfinementConfig struct {
// nil value makes session bus proxy assume built-in defaults
SessionBus *dbus.Config `json:"session_bus,omitempty"`
- // system resources to expose to the sandbox
+ // system resources to expose to the container
Enablements system.Enablements `json:"enablements"`
}
@@ -76,24 +79,12 @@ func (e *ExtraPermConfig) String() string {
return string(buf)
}
-type FilesystemConfig struct {
- // mount point in sandbox, same as src if empty
- Dst string `json:"dst,omitempty"`
- // host filesystem path to make available to sandbox
- Src string `json:"src"`
- // write access
- Write bool `json:"write,omitempty"`
- // device access
- Device bool `json:"dev,omitempty"`
- // fail if mount fails
- Must bool `json:"require,omitempty"`
-}
-
// Template returns a fully populated instance of Config.
func Template() *Config {
return &Config{
- ID: "org.chromium.Chromium",
- Command: []string{
+ ID: "org.chromium.Chromium",
+ Path: "/run/current-system/sw/bin/chromium",
+ Args: []string{
"chromium",
"--ignore-gpu-blocklist",
"--disable-smooth-scrolling",
@@ -108,11 +99,13 @@ func Template() *Config {
Inner: "/var/lib/fortify",
Sandbox: &SandboxConfig{
Hostname: "localhost",
- UserNS: true,
+ Devel: true,
+ Userns: true,
Net: true,
Dev: true,
- Syscall: &bwrap.SyscallPolicy{DenyDevel: true, Multiarch: true},
- NoNewSession: true,
+ Seccomp: seccomp.FlagMultiarch,
+ Tty: true,
+ Multiarch: true,
MapRealUID: true,
DirectWayland: false,
// example API credentials pulled from Google Chrome
@@ -131,10 +124,10 @@ func Template() *Config {
Dst: "/data/data/org.chromium.Chromium", Write: true, Must: true},
{Src: "/dev/dri", Device: true},
},
- Link: [][2]string{{"/run/user/65534", "/run/user/150"}},
- Etc: "/etc",
- AutoEtc: true,
- Override: []string{"/var/run/nscd"},
+ Link: [][2]string{{"/run/user/65534", "/run/user/150"}},
+ Etc: "/etc",
+ AutoEtc: true,
+ Cover: []string{"/var/run/nscd"},
},
ExtraPerms: []*ExtraPermConfig{
{Path: "/var/lib/fortify/u0", Ensure: true, Execute: true},
diff --git a/fst/sandbox.go b/fst/sandbox.go
index fc12da43..b1240eca 100644
--- a/fst/sandbox.go
+++ b/fst/sandbox.go
@@ -4,125 +4,149 @@ import (
"errors"
"fmt"
"io/fs"
+ "maps"
"path"
+ "slices"
+ "syscall"
"git.gensokyo.uk/security/fortify/dbus"
- "git.gensokyo.uk/security/fortify/helper/bwrap"
+ "git.gensokyo.uk/security/fortify/sandbox"
+ "git.gensokyo.uk/security/fortify/sandbox/seccomp"
)
// SandboxConfig describes resources made available to the sandbox.
-type SandboxConfig struct {
- // unix hostname within sandbox
- Hostname string `json:"hostname,omitempty"`
- // allow userns within sandbox
- UserNS bool `json:"userns,omitempty"`
- // share net namespace
- Net bool `json:"net,omitempty"`
- // share all devices
- Dev bool `json:"dev,omitempty"`
- // seccomp syscall filter policy
- Syscall *bwrap.SyscallPolicy `json:"syscall"`
- // do not run in new session
- NoNewSession bool `json:"no_new_session,omitempty"`
- // map target user uid to privileged user uid in the user namespace
- MapRealUID bool `json:"map_real_uid"`
- // direct access to wayland socket; when this gets set no attempt is made to attach security-context-v1
- // and the bare socket is mounted to the sandbox
- DirectWayland bool `json:"direct_wayland,omitempty"`
+type (
+ SandboxConfig struct {
+ // container hostname
+ Hostname string `json:"hostname,omitempty"`
- // final environment variables
- Env map[string]string `json:"env"`
- // sandbox host filesystem access
- Filesystem []*FilesystemConfig `json:"filesystem"`
- // symlinks created inside the sandbox
- Link [][2]string `json:"symlink"`
- // read-only /etc directory
- Etc string `json:"etc,omitempty"`
- // automatically set up /etc symlinks
- AutoEtc bool `json:"auto_etc"`
- // mount tmpfs over these paths,
- // runs right before [ConfinementConfig.ExtraPerms]
- Override []string `json:"override"`
-}
+ // extra seccomp flags
+ Seccomp seccomp.SyscallOpts `json:"seccomp"`
+ // allow ptrace and friends
+ Devel bool `json:"devel,omitempty"`
+ // allow userns creation in container
+ Userns bool `json:"userns,omitempty"`
+ // share host net namespace
+ Net bool `json:"net,omitempty"`
+ // expose main process tty
+ Tty bool `json:"tty,omitempty"`
+ // allow multiarch
+ Multiarch bool `json:"multiarch,omitempty"`
-// SandboxSys encapsulates system functions used during the creation of [bwrap.Config].
-type SandboxSys interface {
- Getuid() int
- Paths() Paths
- ReadDir(name string) ([]fs.DirEntry, error)
- EvalSymlinks(path string) (string, error)
+ // initial process environment variables
+ Env map[string]string `json:"env"`
+ // map target user uid to privileged user uid in the user namespace
+ MapRealUID bool `json:"map_real_uid"`
- Println(v ...any)
- Printf(format string, v ...any)
-}
+ // expose all devices
+ Dev bool `json:"dev,omitempty"`
+ // container host filesystem bind mounts
+ Filesystem []*FilesystemConfig `json:"filesystem"`
+ // create symlinks inside container filesystem
+ Link [][2]string `json:"symlink"`
-// 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.
-func (s *SandboxConfig) Bwrap(sys SandboxSys, uid *int) (*bwrap.Config, error) {
- if s == nil {
- return nil, errors.New("nil sandbox config")
+ // direct access to wayland socket; when this gets set no attempt is made to attach security-context-v1
+ // and the bare socket is mounted to the sandbox
+ DirectWayland bool `json:"direct_wayland,omitempty"`
+
+ // read-only /etc directory
+ Etc string `json:"etc,omitempty"`
+ // automatically set up /etc symlinks
+ AutoEtc bool `json:"auto_etc"`
+ // cover these paths or create them if they do not already exist
+ Cover []string `json:"cover"`
}
- if s.Syscall == nil {
- sys.Println("syscall filter not configured, PROCEED WITH CAUTION")
+ // SandboxSys encapsulates system functions used during [sandbox.Container] initialisation.
+ SandboxSys interface {
+ Getuid() int
+ Getgid() int
+ Paths() Paths
+ ReadDir(name string) ([]fs.DirEntry, error)
+ EvalSymlinks(path string) (string, error)
+
+ Println(v ...any)
+ Printf(format string, v ...any)
}
- if !s.MapRealUID {
- // mapped uid defaults to 65534 to work around file ownership checks due to a bwrap limitation
- *uid = 65534
- } else {
- // some programs fail to connect to dbus session running as a different uid, so a separate workaround
- // is introduced to map priv-side caller uid in namespace
- *uid = sys.Getuid()
+ // FilesystemConfig is a representation of [sandbox.BindMount].
+ FilesystemConfig struct {
+ // mount point in container, same as src if empty
+ Dst string `json:"dst,omitempty"`
+ // host filesystem path to make available to the container
+ Src string `json:"src"`
+ // do not mount filesystem read-only
+ Write bool `json:"write,omitempty"`
+ // do not disable device files
+ Device bool `json:"dev,omitempty"`
+ // fail if the bind mount cannot be established for any reason
+ Must bool `json:"require,omitempty"`
}
+)
- conf := (&bwrap.Config{
- Net: s.Net,
- UserNS: s.UserNS,
- UID: uid,
- GID: uid,
- Hostname: s.Hostname,
- Clearenv: true,
- SetEnv: s.Env,
+// ToContainer initialises [sandbox.Params] via [SandboxConfig].
+// Note that remaining container setup must be queued by the [App] implementation.
+func (s *SandboxConfig) ToContainer(sys SandboxSys, uid, gid *int) (*sandbox.Params, map[string]string, error) {
+ if s == nil {
+ return nil, nil, syscall.EBADE
+ }
- /* 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),
+ container := &sandbox.Params{
+ Hostname: s.Hostname,
+ Ops: new(sandbox.Ops),
+ Seccomp: s.Seccomp,
+ }
- Syscall: s.Syscall,
- NewSession: !s.NoNewSession,
- DieWithParent: true,
- AsInit: true,
+ /* 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 */
+ *container.Ops = slices.Grow(*container.Ops, 1<<8)
- // initialise unconditionally as Once cannot be justified
- // for saving such a miniscule amount of memory
- Chmod: make(bwrap.ChmodConfig),
- }).
- Procfs("/proc").
- Tmpfs(Tmp, 4*1024)
+ if s.Devel {
+ container.Flags |= sandbox.FAllowDevel
+ }
+ if s.Userns {
+ container.Flags |= sandbox.FAllowUserns
+ }
+ if s.Net {
+ container.Flags |= sandbox.FAllowNet
+ }
+ if s.Tty {
+ container.Flags |= sandbox.FAllowTTY
+ }
- if !s.Dev {
- conf.DevTmpfs("/dev").Mqueue("/dev/mqueue")
+ if s.MapRealUID {
+ /* some programs fail to connect to dbus session running as a different uid
+ so this workaround is introduced to map priv-side caller uid in container */
+ container.Uid = sys.Getuid()
+ *uid = container.Uid
+ container.Gid = sys.Getgid()
+ *gid = container.Gid
} else {
- conf.Bind("/dev", "/dev", false, true, true)
+ *uid = sandbox.OverflowUid()
+ *gid = sandbox.OverflowGid()
}
- if !s.AutoEtc {
- if s.Etc == "" {
- conf.Dir("/etc")
- } else {
- conf.Bind(s.Etc, "/etc")
- }
+ container.
+ Proc("/proc").
+ Tmpfs(Tmp, 1<<12, 0755)
+
+ if !s.Dev {
+ container.Dev("/dev").Mqueue("/dev/mqueue")
+ } else {
+ container.Bind("/dev", "/dev", sandbox.BindDevice)
}
- // retrieve paths and hide them if they're made available in the sandbox
+ /* retrieve paths and hide them if they're made available in the sandbox;
+ this feature tries to improve user experience of permissive defaults, and
+ to warn about issues in custom configuration; it is NOT a security feature
+ and should not be treated as such, ALWAYS be careful with what you bind */
var hidePaths []string
sc := sys.Paths()
hidePaths = append(hidePaths, sc.RuntimePath, sc.SharePath)
_, systemBusAddr := dbus.Address()
if entries, err := dbus.Parse([]byte(systemBusAddr)); err != nil {
- return nil, err
+ return nil, nil, err
} else {
// there is usually only one, do not preallocate
for _, entry := range entries {
@@ -148,7 +172,7 @@ func (s *SandboxConfig) Bwrap(sys SandboxSys, uid *int) (*bwrap.Config, error) {
hidePathMatch := make([]bool, len(hidePaths))
for i := range hidePaths {
if err := evalSymlinks(sys, &hidePaths[i]); err != nil {
- return nil, err
+ return nil, nil, err
}
}
@@ -158,19 +182,19 @@ func (s *SandboxConfig) Bwrap(sys SandboxSys, uid *int) (*bwrap.Config, error) {
}
if !path.IsAbs(c.Src) {
- return nil, fmt.Errorf("src path %q is not absolute", c.Src)
+ return nil, nil, fmt.Errorf("src path %q is not absolute", c.Src)
}
dest := c.Dst
if c.Dst == "" {
dest = c.Src
} else if !path.IsAbs(dest) {
- return nil, fmt.Errorf("dst path %q is not absolute", dest)
+ return nil, nil, fmt.Errorf("dst path %q is not absolute", dest)
}
srcH := c.Src
if err := evalSymlinks(sys, &srcH); err != nil {
- return nil, err
+ return nil, nil, err
}
for i := range hidePaths {
@@ -180,54 +204,71 @@ func (s *SandboxConfig) Bwrap(sys SandboxSys, uid *int) (*bwrap.Config, error) {
}
if ok, err := deepContainsH(srcH, hidePaths[i]); err != nil {
- return nil, err
+ return nil, nil, err
} else if ok {
hidePathMatch[i] = true
sys.Printf("hiding paths from %q", c.Src)
}
}
- conf.Bind(c.Src, dest, !c.Must, c.Write, c.Device)
+ var flags int
+ if c.Write {
+ flags |= sandbox.BindWritable
+ }
+ if c.Device {
+ flags |= sandbox.BindDevice | sandbox.BindWritable
+ }
+ if !c.Must {
+ flags |= sandbox.BindOptional
+ }
+ container.Bind(c.Src, dest, flags)
}
- // hide marked paths before setting up shares
+ // cover matched paths
for i, ok := range hidePathMatch {
if ok {
- conf.Tmpfs(hidePaths[i], 8192)
+ container.Tmpfs(hidePaths[i], 1<<13, 0755)
}
}
for _, l := range s.Link {
- conf.Symlink(l[0], l[1])
+ container.Link(l[0], l[1])
}
- if s.AutoEtc {
- etc := s.Etc
- if etc == "" {
- etc = "/etc"
+ // perf: this might work better if implemented as a setup op in container init
+ if !s.AutoEtc {
+ if s.Etc != "" {
+ container.Bind(s.Etc, "/etc", 0)
+ }
+ } else {
+ etcPath := s.Etc
+ if etcPath == "" {
+ etcPath = "/etc"
}
- conf.Bind(etc, Tmp+"/etc")
+ container.
+ Bind(etcPath, Tmp+"/etc", 0).
+ Mkdir("/etc", 0700)
- // link host /etc contents to prevent passwd/group from being overwritten
- if d, err := sys.ReadDir(etc); err != nil {
- return nil, err
+ // link host /etc contents to prevent dropping passwd/group bind mounts
+ if d, err := sys.ReadDir(etcPath); err != nil {
+ return nil, nil, err
} else {
for _, ent := range d {
- name := ent.Name()
- switch name {
+ n := ent.Name()
+ switch n {
case "passwd":
case "group":
case "mtab":
- conf.Symlink("/proc/mounts", "/etc/"+name)
+ container.Link("/proc/mounts", "/etc/"+n)
default:
- conf.Symlink(Tmp+"/etc/"+name, "/etc/"+name)
+ container.Link(Tmp+"/etc/"+n, "/etc/"+n)
}
}
}
}
- return conf, nil
+ return container, maps.Clone(s.Env), nil
}
func evalSymlinks(sys SandboxSys, v *string) error {