Compare commits
6 Commits
c6d813b737
...
13cbd0b600
Author | SHA1 | Date | |
---|---|---|---|
13cbd0b600 | |||
7b99bfb636 | |||
88dcec1a39 | |||
c70f0612ad | |||
85e5b097fd | |||
0107620d8c |
57
cmd/fpkg/activate.go
Normal file
57
cmd/fpkg/activate.go
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"git.gensokyo.uk/security/fortify/fst"
|
||||||
|
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||||
|
)
|
||||||
|
|
||||||
|
func actionActivate(args []string) {
|
||||||
|
// simple check so activate is not attempted outside fortify
|
||||||
|
if s, err := os.Stat(path.Join(fst.Tmp, "sbin")); err != nil {
|
||||||
|
fmsg.Fatalf("cannot stat fortify sbin: %v", err)
|
||||||
|
} else if !s.IsDir() {
|
||||||
|
fmsg.Fatal("fortify sbin path is not a directory")
|
||||||
|
}
|
||||||
|
|
||||||
|
home := os.Getenv("HOME")
|
||||||
|
if !path.IsAbs(home) {
|
||||||
|
fmsg.Fatalf("path %q is not aboslute", home)
|
||||||
|
}
|
||||||
|
marker := path.Join(home, ".hm-activation")
|
||||||
|
|
||||||
|
if len(args) != 1 {
|
||||||
|
fmsg.Fatalf("invalid argument")
|
||||||
|
}
|
||||||
|
activate := path.Join(args[0], "activate")
|
||||||
|
|
||||||
|
var cmd *exec.Cmd
|
||||||
|
if l, err := os.Readlink(marker); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||||
|
fmsg.Fatalf("cannot read activation marker %q: %v", marker, err)
|
||||||
|
} else if err != nil || l != activate {
|
||||||
|
cmd = exec.Command(activate)
|
||||||
|
}
|
||||||
|
|
||||||
|
// marker present and equals to current activation package
|
||||||
|
if cmd == nil {
|
||||||
|
fmsg.Exit(0)
|
||||||
|
panic("unreachable")
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||||
|
cmd.Env = os.Environ()
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
fmsg.Fatalf("cannot activate home-manager configuration: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Remove(marker); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||||
|
fmsg.Fatalf("cannot remove existing marker: %v", err)
|
||||||
|
}
|
||||||
|
if err := os.Symlink(activate, marker); err != nil {
|
||||||
|
fmsg.Fatalf("cannot create activation marker: %v", err)
|
||||||
|
}
|
||||||
|
}
|
38
cmd/fpkg/main.go
Normal file
38
cmd/fpkg/main.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
|
||||||
|
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
flagVerbose bool
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
flag.BoolVar(&flagVerbose, "v", false, "Verbose output")
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
fmsg.SetPrefix("fpkg")
|
||||||
|
|
||||||
|
flag.Parse()
|
||||||
|
fmsg.SetVerbose(flagVerbose)
|
||||||
|
|
||||||
|
args := flag.Args()
|
||||||
|
if len(args) < 1 {
|
||||||
|
fmsg.Fatal("invalid argument")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch args[0] {
|
||||||
|
case "activate":
|
||||||
|
actionActivate(args[1:])
|
||||||
|
case "start":
|
||||||
|
actionStart(args[1:])
|
||||||
|
default:
|
||||||
|
fmsg.Fatal("invalid argument")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmsg.Exit(0)
|
||||||
|
}
|
205
cmd/fpkg/start.go
Normal file
205
cmd/fpkg/start.go
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"flag"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"git.gensokyo.uk/security/fortify/dbus"
|
||||||
|
"git.gensokyo.uk/security/fortify/fst"
|
||||||
|
"git.gensokyo.uk/security/fortify/internal"
|
||||||
|
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||||
|
"git.gensokyo.uk/security/fortify/internal/system"
|
||||||
|
)
|
||||||
|
|
||||||
|
const shell = "/run/current-system/sw/bin/bash"
|
||||||
|
|
||||||
|
type bundleInfo struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
|
||||||
|
// passed through to [fst.Config]
|
||||||
|
ID string `json:"id"`
|
||||||
|
// passed through to [fst.Config]
|
||||||
|
AppID int `json:"app_id"`
|
||||||
|
// passed through to [fst.Config]
|
||||||
|
Groups []string `json:"groups,omitempty"`
|
||||||
|
// passed through to [fst.Config]
|
||||||
|
UserNS bool `json:"userns,omitempty"`
|
||||||
|
// passed through to [fst.Config]
|
||||||
|
Net bool `json:"net,omitempty"`
|
||||||
|
// passed through to [fst.Config]
|
||||||
|
Dev bool `json:"dev,omitempty"`
|
||||||
|
// passed through to [fst.Config]
|
||||||
|
NoNewSession bool `json:"no_new_session,omitempty"`
|
||||||
|
// passed through to [fst.Config]
|
||||||
|
MapRealUID bool `json:"map_real_uid,omitempty"`
|
||||||
|
// passed through to [fst.Config]
|
||||||
|
DirectWayland bool `json:"direct_wayland,omitempty"`
|
||||||
|
// passed through to [fst.Config]
|
||||||
|
SystemBus *dbus.Config `json:"system_bus,omitempty"`
|
||||||
|
// passed through to [fst.Config]
|
||||||
|
SessionBus *dbus.Config `json:"session_bus,omitempty"`
|
||||||
|
// passed through to [fst.Config]
|
||||||
|
Enablements system.Enablements `json:"enablements"`
|
||||||
|
|
||||||
|
// allow gpu access within sandbox
|
||||||
|
GPU bool `json:"gpu"`
|
||||||
|
// inner nix store path to activate-and-exec script
|
||||||
|
Launcher string `json:"launcher"`
|
||||||
|
// store path to /run/current-system
|
||||||
|
CurrentSystem string `json:"current_system"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func actionStart(args []string) {
|
||||||
|
set := flag.NewFlagSet("start", flag.ExitOnError)
|
||||||
|
var dropShell bool
|
||||||
|
set.BoolVar(&dropShell, "s", false, "Drop to a shell on activation")
|
||||||
|
|
||||||
|
// Ignore errors; set is set for ExitOnError.
|
||||||
|
_ = set.Parse(args)
|
||||||
|
|
||||||
|
args = set.Args()
|
||||||
|
|
||||||
|
if len(args) < 1 {
|
||||||
|
fmsg.Fatal("invalid argument")
|
||||||
|
}
|
||||||
|
name := args[0]
|
||||||
|
if !path.IsAbs(name) {
|
||||||
|
if dir, err := os.Getwd(); err != nil {
|
||||||
|
fmsg.Fatalf("cannot get current directory: %v", err)
|
||||||
|
} else {
|
||||||
|
name = path.Join(dir, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bundle := new(bundleInfo)
|
||||||
|
if f, err := os.Open(path.Join(name, "bundle.json")); err != nil {
|
||||||
|
fmsg.Fatalf("cannot open bundle: %v", err)
|
||||||
|
} else if err = json.NewDecoder(f).Decode(&bundle); err != nil {
|
||||||
|
fmsg.Fatalf("cannot parse bundle metadata: %v", err)
|
||||||
|
} else if err = f.Close(); err != nil {
|
||||||
|
fmsg.Printf("cannot close bundle metadata: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Setenv("SHELL", shell); err != nil {
|
||||||
|
fmsg.Fatalf("cannot set $SHELL: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
command := make([]string, 1, len(args))
|
||||||
|
if !dropShell {
|
||||||
|
command[0] = bundle.Launcher
|
||||||
|
} else {
|
||||||
|
command[0] = shell
|
||||||
|
}
|
||||||
|
command = append(command, args[1:]...)
|
||||||
|
|
||||||
|
currentSystem := path.Join(name, bundle.CurrentSystem)
|
||||||
|
config := &fst.Config{
|
||||||
|
ID: bundle.ID,
|
||||||
|
Command: command,
|
||||||
|
Confinement: fst.ConfinementConfig{
|
||||||
|
AppID: bundle.AppID,
|
||||||
|
Groups: bundle.Groups,
|
||||||
|
Username: "fortify",
|
||||||
|
Inner: path.Join("/data/data", bundle.ID),
|
||||||
|
Outer: formatDataPath(bundle.ID),
|
||||||
|
Sandbox: &fst.SandboxConfig{
|
||||||
|
Hostname: formatHostname(bundle.Name),
|
||||||
|
UserNS: bundle.UserNS,
|
||||||
|
Net: bundle.Net,
|
||||||
|
Dev: bundle.Dev,
|
||||||
|
NoNewSession: bundle.NoNewSession || dropShell,
|
||||||
|
MapRealUID: bundle.MapRealUID,
|
||||||
|
DirectWayland: bundle.DirectWayland,
|
||||||
|
Filesystem: []*fst.FilesystemConfig{
|
||||||
|
{Src: path.Join(name, "nix"), Dst: "/nix", Must: true},
|
||||||
|
{Src: currentSystem, Dst: "/run/current-system", Must: true},
|
||||||
|
{Src: "/etc/resolv.conf"},
|
||||||
|
{Src: "/sys/block"},
|
||||||
|
{Src: "/sys/bus"},
|
||||||
|
{Src: "/sys/class"},
|
||||||
|
{Src: "/sys/dev"},
|
||||||
|
{Src: "/sys/devices"},
|
||||||
|
},
|
||||||
|
Link: [][2]string{
|
||||||
|
{"/run/current-system/sw/bin", "/bin"},
|
||||||
|
{"/run/current-system/sw/bin", "/usr/bin"},
|
||||||
|
},
|
||||||
|
Etc: path.Join(name, "etc"),
|
||||||
|
AutoEtc: true,
|
||||||
|
},
|
||||||
|
SystemBus: bundle.SystemBus,
|
||||||
|
SessionBus: bundle.SessionBus,
|
||||||
|
Enablements: bundle.Enablements,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if bundle.GPU {
|
||||||
|
config.Confinement.Sandbox.Filesystem = append(config.Confinement.Sandbox.Filesystem,
|
||||||
|
&fst.FilesystemConfig{Src: "/dev/dri", Device: true})
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
cmd *exec.Cmd
|
||||||
|
st io.WriteCloser
|
||||||
|
)
|
||||||
|
if p, ok := internal.Check(internal.Fortify); !ok {
|
||||||
|
fmsg.Fatal("invalid fortify path, this copy of fpkg is not compiled correctly")
|
||||||
|
panic("unreachable")
|
||||||
|
} else if r, w, err := os.Pipe(); err != nil {
|
||||||
|
fmsg.Fatalf("cannot pipe: %v", err)
|
||||||
|
panic("unreachable")
|
||||||
|
} else {
|
||||||
|
if fmsg.Verbose() {
|
||||||
|
cmd = exec.Command(p, "-v", "app", "3")
|
||||||
|
} else {
|
||||||
|
cmd = exec.Command(p, "app", "3")
|
||||||
|
}
|
||||||
|
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||||
|
cmd.ExtraFiles = []*os.File{r}
|
||||||
|
st = w
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := json.NewEncoder(st).Encode(config); err != nil {
|
||||||
|
fmsg.Fatalf("cannot send configuration: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
fmsg.Fatalf("cannot start fortify: %v", err)
|
||||||
|
}
|
||||||
|
if err := cmd.Wait(); err != nil {
|
||||||
|
var exitError *exec.ExitError
|
||||||
|
if errors.As(err, &exitError) {
|
||||||
|
fmsg.Exit(exitError.ExitCode())
|
||||||
|
} else {
|
||||||
|
fmsg.Fatalf("cannot wait: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmsg.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatHostname(name string) string {
|
||||||
|
if h, err := os.Hostname(); err != nil {
|
||||||
|
fmsg.Printf("cannot get hostname: %v", err)
|
||||||
|
return "fortify-" + name
|
||||||
|
} else {
|
||||||
|
return h + "-" + name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatDataPath(id string) string {
|
||||||
|
if p, ok := os.LookupEnv("FORTIFY_DATA_HOME"); ok {
|
||||||
|
return path.Join(p, id)
|
||||||
|
} else if p, ok = os.LookupEnv("HOME"); ok {
|
||||||
|
return path.Join(p, ".app", id)
|
||||||
|
} else {
|
||||||
|
return path.Join("/var/lib/fortify/app", id)
|
||||||
|
}
|
||||||
|
}
|
1
dist/install.sh
vendored
1
dist/install.sh
vendored
@ -4,6 +4,7 @@ cd "$(dirname -- "$0")" || exit 1
|
|||||||
install -vDm0755 "bin/fortify" "${FORTIFY_INSTALL_PREFIX}/usr/bin/fortify"
|
install -vDm0755 "bin/fortify" "${FORTIFY_INSTALL_PREFIX}/usr/bin/fortify"
|
||||||
install -vDm0755 "bin/fshim" "${FORTIFY_INSTALL_PREFIX}/usr/libexec/fortify/fshim"
|
install -vDm0755 "bin/fshim" "${FORTIFY_INSTALL_PREFIX}/usr/libexec/fortify/fshim"
|
||||||
install -vDm0755 "bin/finit" "${FORTIFY_INSTALL_PREFIX}/usr/libexec/fortify/finit"
|
install -vDm0755 "bin/finit" "${FORTIFY_INSTALL_PREFIX}/usr/libexec/fortify/finit"
|
||||||
|
install -vDm0755 "bin/fpkg" "${FORTIFY_INSTALL_PREFIX}/usr/libexec/fortify/fpkg"
|
||||||
install -vDm0755 "bin/fuserdb" "${FORTIFY_INSTALL_PREFIX}/usr/libexec/fortify/fuserdb"
|
install -vDm0755 "bin/fuserdb" "${FORTIFY_INSTALL_PREFIX}/usr/libexec/fortify/fuserdb"
|
||||||
|
|
||||||
install -vDm6511 "bin/fsu" "${FORTIFY_INSTALL_PREFIX}/usr/bin/fsu"
|
install -vDm6511 "bin/fsu" "${FORTIFY_INSTALL_PREFIX}/usr/bin/fsu"
|
||||||
|
1
dist/release.sh
vendored
1
dist/release.sh
vendored
@ -10,6 +10,7 @@ cp -rv "comp" "${out}"
|
|||||||
|
|
||||||
go build -v -o "${out}/bin/" -ldflags "-s -w
|
go build -v -o "${out}/bin/" -ldflags "-s -w
|
||||||
-X git.gensokyo.uk/security/fortify/internal.Version=${VERSION}
|
-X git.gensokyo.uk/security/fortify/internal.Version=${VERSION}
|
||||||
|
-X git.gensokyo.uk/security/fortify/internal.Fortify=/usr/bin/fortify
|
||||||
-X git.gensokyo.uk/security/fortify/internal.Fsu=/usr/bin/fsu
|
-X git.gensokyo.uk/security/fortify/internal.Fsu=/usr/bin/fsu
|
||||||
-X git.gensokyo.uk/security/fortify/internal.Finit=/usr/libexec/fortify/finit
|
-X git.gensokyo.uk/security/fortify/internal.Finit=/usr/libexec/fortify/finit
|
||||||
-X main.Fmain=/usr/bin/fortify
|
-X main.Fmain=/usr/bin/fortify
|
||||||
|
@ -35,6 +35,8 @@ type ConfinementConfig struct {
|
|||||||
Outer string `json:"home"`
|
Outer string `json:"home"`
|
||||||
// bwrap sandbox confinement configuration
|
// bwrap sandbox confinement configuration
|
||||||
Sandbox *SandboxConfig `json:"sandbox"`
|
Sandbox *SandboxConfig `json:"sandbox"`
|
||||||
|
// extra acl entries to append
|
||||||
|
ExtraPerms []*ExtraPermConfig `json:"extra_perms,omitempty"`
|
||||||
|
|
||||||
// reference to a system D-Bus proxy configuration,
|
// reference to a system D-Bus proxy configuration,
|
||||||
// nil value disables system bus proxy
|
// nil value disables system bus proxy
|
||||||
@ -78,6 +80,29 @@ type SandboxConfig struct {
|
|||||||
Override []string `json:"override"`
|
Override []string `json:"override"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ExtraPermConfig struct {
|
||||||
|
Path string `json:"path"`
|
||||||
|
Read bool `json:"r,omitempty"`
|
||||||
|
Write bool `json:"w,omitempty"`
|
||||||
|
Execute bool `json:"x,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ExtraPermConfig) String() string {
|
||||||
|
buf := make([]byte, 0, 4+len(e.Path))
|
||||||
|
buf = append(buf, '-', '-', '-', ':')
|
||||||
|
buf = append(buf, []byte(e.Path)...)
|
||||||
|
if e.Read {
|
||||||
|
buf[0] = 'r'
|
||||||
|
}
|
||||||
|
if e.Write {
|
||||||
|
buf[1] = 'w'
|
||||||
|
}
|
||||||
|
if e.Execute {
|
||||||
|
buf[2] = 'x'
|
||||||
|
}
|
||||||
|
return string(buf)
|
||||||
|
}
|
||||||
|
|
||||||
type FilesystemConfig struct {
|
type FilesystemConfig struct {
|
||||||
// mount point in sandbox, same as src if empty
|
// mount point in sandbox, same as src if empty
|
||||||
Dst string `json:"dst,omitempty"`
|
Dst string `json:"dst,omitempty"`
|
||||||
@ -222,6 +247,7 @@ func Template() *Config {
|
|||||||
{Src: "/dev/dri", Device: true},
|
{Src: "/dev/dri", Device: true},
|
||||||
},
|
},
|
||||||
Link: [][2]string{{"/run/user/65534", "/run/user/150"}},
|
Link: [][2]string{{"/run/user/65534", "/run/user/150"}},
|
||||||
|
Etc: "/etc",
|
||||||
AutoEtc: true,
|
AutoEtc: true,
|
||||||
Override: []string{"/var/run/nscd"},
|
Override: []string{"/var/run/nscd"},
|
||||||
},
|
},
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"git.gensokyo.uk/security/fortify/acl"
|
||||||
"git.gensokyo.uk/security/fortify/dbus"
|
"git.gensokyo.uk/security/fortify/dbus"
|
||||||
"git.gensokyo.uk/security/fortify/fst"
|
"git.gensokyo.uk/security/fortify/fst"
|
||||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||||
@ -48,6 +49,8 @@ type appSeal struct {
|
|||||||
et system.Enablements
|
et system.Enablements
|
||||||
// wayland socket direct access
|
// wayland socket direct access
|
||||||
directWayland bool
|
directWayland bool
|
||||||
|
// extra UpdatePerm ops
|
||||||
|
extraPerms []*sealedExtraPerm
|
||||||
|
|
||||||
// prevents sharing from happening twice
|
// prevents sharing from happening twice
|
||||||
shared bool
|
shared bool
|
||||||
@ -59,6 +62,11 @@ type appSeal struct {
|
|||||||
// protected by upstream mutex
|
// protected by upstream mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type sealedExtraPerm struct {
|
||||||
|
name string
|
||||||
|
perms acl.Perms
|
||||||
|
}
|
||||||
|
|
||||||
// Seal seals the app launch context
|
// Seal seals the app launch context
|
||||||
func (a *app) Seal(config *fst.Config) error {
|
func (a *app) Seal(config *fst.Config) error {
|
||||||
a.lock.Lock()
|
a.lock.Lock()
|
||||||
@ -100,7 +108,7 @@ func (a *app) Seal(config *fst.Config) error {
|
|||||||
if config.Confinement.AppID < 0 || config.Confinement.AppID > 9999 {
|
if config.Confinement.AppID < 0 || config.Confinement.AppID > 9999 {
|
||||||
return fmsg.WrapError(ErrUser,
|
return fmsg.WrapError(ErrUser,
|
||||||
fmt.Sprintf("aid %d out of range", config.Confinement.AppID))
|
fmt.Sprintf("aid %d out of range", config.Confinement.AppID))
|
||||||
} else {
|
}
|
||||||
seal.sys.user = appUser{
|
seal.sys.user = appUser{
|
||||||
aid: config.Confinement.AppID,
|
aid: config.Confinement.AppID,
|
||||||
as: strconv.Itoa(config.Confinement.AppID),
|
as: strconv.Itoa(config.Confinement.AppID),
|
||||||
@ -141,6 +149,25 @@ func (a *app) Seal(config *fst.Config) error {
|
|||||||
seal.sys.user.supp[i] = g.Gid
|
seal.sys.user.supp[i] = g.Gid
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// build extra perms
|
||||||
|
seal.extraPerms = make([]*sealedExtraPerm, len(config.Confinement.ExtraPerms))
|
||||||
|
for i, p := range config.Confinement.ExtraPerms {
|
||||||
|
if p == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
seal.extraPerms[i].name = p.Path
|
||||||
|
seal.extraPerms[i].perms = make(acl.Perms, 0, 3)
|
||||||
|
if p.Read {
|
||||||
|
seal.extraPerms[i].perms = append(seal.extraPerms[i].perms, acl.Read)
|
||||||
|
}
|
||||||
|
if p.Write {
|
||||||
|
seal.extraPerms[i].perms = append(seal.extraPerms[i].perms, acl.Write)
|
||||||
|
}
|
||||||
|
if p.Execute {
|
||||||
|
seal.extraPerms[i].perms = append(seal.extraPerms[i].perms, acl.Execute)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// map sandbox config to bwrap
|
// map sandbox config to bwrap
|
||||||
@ -229,7 +256,7 @@ func (a *app) Seal(config *fst.Config) error {
|
|||||||
seal.et = config.Confinement.Enablements
|
seal.et = config.Confinement.Enablements
|
||||||
|
|
||||||
// this method calls all share methods in sequence
|
// this method calls all share methods in sequence
|
||||||
if err := seal.shareAll([2]*dbus.Config{config.Confinement.SessionBus, config.Confinement.SystemBus}, a.os); err != nil {
|
if err := seal.setupShares([2]*dbus.Config{config.Confinement.SessionBus, config.Confinement.SystemBus}, a.os); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,44 +0,0 @@
|
|||||||
package app
|
|
||||||
|
|
||||||
import (
|
|
||||||
"path"
|
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/acl"
|
|
||||||
"git.gensokyo.uk/security/fortify/dbus"
|
|
||||||
"git.gensokyo.uk/security/fortify/internal/system"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
dbusSessionBusAddress = "DBUS_SESSION_BUS_ADDRESS"
|
|
||||||
dbusSystemBusAddress = "DBUS_SYSTEM_BUS_ADDRESS"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (seal *appSeal) shareDBus(config [2]*dbus.Config) error {
|
|
||||||
if !seal.et.Has(system.EDBus) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// downstream socket paths
|
|
||||||
sessionPath, systemPath := path.Join(seal.share, "bus"), path.Join(seal.share, "system_bus_socket")
|
|
||||||
|
|
||||||
// configure dbus proxy
|
|
||||||
if f, err := seal.sys.ProxyDBus(config[0], config[1], sessionPath, systemPath); err != nil {
|
|
||||||
return err
|
|
||||||
} else {
|
|
||||||
seal.dbusMsg = f
|
|
||||||
}
|
|
||||||
|
|
||||||
// share proxy sockets
|
|
||||||
sessionInner := path.Join(seal.sys.runtime, "bus")
|
|
||||||
seal.sys.bwrap.SetEnv[dbusSessionBusAddress] = "unix:path=" + sessionInner
|
|
||||||
seal.sys.bwrap.Bind(sessionPath, sessionInner)
|
|
||||||
seal.sys.UpdatePerm(sessionPath, acl.Read, acl.Write)
|
|
||||||
if config[1] != nil {
|
|
||||||
systemInner := "/run/dbus/system_bus_socket"
|
|
||||||
seal.sys.bwrap.SetEnv[dbusSystemBusAddress] = "unix:path=" + systemInner
|
|
||||||
seal.sys.bwrap.Bind(systemPath, systemInner)
|
|
||||||
seal.sys.UpdatePerm(systemPath, acl.Read, acl.Write)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,81 +0,0 @@
|
|||||||
package app
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"path"
|
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/acl"
|
|
||||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
|
||||||
"git.gensokyo.uk/security/fortify/internal/linux"
|
|
||||||
"git.gensokyo.uk/security/fortify/internal/system"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
term = "TERM"
|
|
||||||
display = "DISPLAY"
|
|
||||||
|
|
||||||
// https://manpages.debian.org/experimental/libwayland-doc/wl_display_connect.3.en.html
|
|
||||||
waylandDisplay = "WAYLAND_DISPLAY"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrWayland = errors.New(waylandDisplay + " unset")
|
|
||||||
ErrXDisplay = errors.New(display + " unset")
|
|
||||||
)
|
|
||||||
|
|
||||||
func (seal *appSeal) shareDisplay(os linux.System) error {
|
|
||||||
// pass $TERM to launcher
|
|
||||||
if t, ok := os.LookupEnv(term); ok {
|
|
||||||
seal.sys.bwrap.SetEnv[term] = t
|
|
||||||
}
|
|
||||||
|
|
||||||
// set up wayland
|
|
||||||
if seal.et.Has(system.EWayland) {
|
|
||||||
var wp string
|
|
||||||
if wd, ok := os.LookupEnv(waylandDisplay); !ok {
|
|
||||||
return fmsg.WrapError(ErrWayland,
|
|
||||||
"WAYLAND_DISPLAY is not set")
|
|
||||||
} else {
|
|
||||||
wp = path.Join(seal.RuntimePath, wd)
|
|
||||||
}
|
|
||||||
|
|
||||||
w := path.Join(seal.sys.runtime, "wayland-0")
|
|
||||||
seal.sys.bwrap.SetEnv[waylandDisplay] = w
|
|
||||||
|
|
||||||
if seal.directWayland {
|
|
||||||
// hardlink wayland socket
|
|
||||||
wpi := path.Join(seal.shareLocal, "wayland")
|
|
||||||
seal.sys.Link(wp, wpi)
|
|
||||||
seal.sys.bwrap.Bind(wpi, w)
|
|
||||||
|
|
||||||
// ensure Wayland socket ACL (e.g. `/run/user/%d/wayland-%d`)
|
|
||||||
seal.sys.UpdatePermType(system.EWayland, wp, acl.Read, acl.Write, acl.Execute)
|
|
||||||
} else {
|
|
||||||
wc := path.Join(seal.SharePath, "wayland")
|
|
||||||
wt := path.Join(wc, seal.id)
|
|
||||||
seal.sys.Ensure(wc, 0711)
|
|
||||||
appID := seal.fid
|
|
||||||
if appID == "" {
|
|
||||||
// use instance ID in case app id is not set
|
|
||||||
appID = "moe.ophivana.fortify." + seal.id
|
|
||||||
}
|
|
||||||
seal.sys.Wayland(wt, wp, appID, seal.id)
|
|
||||||
seal.sys.bwrap.Bind(wt, w)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// set up X11
|
|
||||||
if seal.et.Has(system.EX11) {
|
|
||||||
// discover X11 and grant user permission via the `ChangeHosts` command
|
|
||||||
if d, ok := os.LookupEnv(display); !ok {
|
|
||||||
return fmsg.WrapError(ErrXDisplay,
|
|
||||||
"DISPLAY is not set")
|
|
||||||
} else {
|
|
||||||
seal.sys.ChangeHosts("#" + seal.sys.user.us)
|
|
||||||
seal.sys.bwrap.SetEnv[display] = d
|
|
||||||
seal.sys.bwrap.Bind("/tmp/.X11-unix", "/tmp/.X11-unix")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
343
internal/app/share.go
Normal file
343
internal/app/share.go
Normal file
@ -0,0 +1,343 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"git.gensokyo.uk/security/fortify/acl"
|
||||||
|
"git.gensokyo.uk/security/fortify/dbus"
|
||||||
|
"git.gensokyo.uk/security/fortify/fst"
|
||||||
|
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||||
|
"git.gensokyo.uk/security/fortify/internal/linux"
|
||||||
|
"git.gensokyo.uk/security/fortify/internal/system"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
home = "HOME"
|
||||||
|
shell = "SHELL"
|
||||||
|
|
||||||
|
xdgConfigHome = "XDG_CONFIG_HOME"
|
||||||
|
xdgRuntimeDir = "XDG_RUNTIME_DIR"
|
||||||
|
xdgSessionClass = "XDG_SESSION_CLASS"
|
||||||
|
xdgSessionType = "XDG_SESSION_TYPE"
|
||||||
|
|
||||||
|
term = "TERM"
|
||||||
|
display = "DISPLAY"
|
||||||
|
|
||||||
|
// https://manpages.debian.org/experimental/libwayland-doc/wl_display_connect.3.en.html
|
||||||
|
waylandDisplay = "WAYLAND_DISPLAY"
|
||||||
|
|
||||||
|
pulseServer = "PULSE_SERVER"
|
||||||
|
pulseCookie = "PULSE_COOKIE"
|
||||||
|
|
||||||
|
dbusSessionBusAddress = "DBUS_SESSION_BUS_ADDRESS"
|
||||||
|
dbusSystemBusAddress = "DBUS_SYSTEM_BUS_ADDRESS"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrWayland = errors.New(waylandDisplay + " unset")
|
||||||
|
ErrXDisplay = errors.New(display + " unset")
|
||||||
|
|
||||||
|
ErrPulseCookie = errors.New("pulse cookie not present")
|
||||||
|
ErrPulseSocket = errors.New("pulse socket not present")
|
||||||
|
ErrPulseMode = errors.New("unexpected pulse socket mode")
|
||||||
|
)
|
||||||
|
|
||||||
|
func (seal *appSeal) setupShares(bus [2]*dbus.Config, os linux.System) error {
|
||||||
|
if seal.shared {
|
||||||
|
panic("seal shared twice")
|
||||||
|
}
|
||||||
|
seal.shared = true
|
||||||
|
|
||||||
|
/*
|
||||||
|
Tmpdir-based share directory
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ensure Share (e.g. `/tmp/fortify.%d`)
|
||||||
|
// acl is unnecessary as this directory is world executable
|
||||||
|
seal.sys.Ensure(seal.SharePath, 0711)
|
||||||
|
|
||||||
|
// ensure process-specific share (e.g. `/tmp/fortify.%d/%s`)
|
||||||
|
// acl is unnecessary as this directory is world executable
|
||||||
|
seal.share = path.Join(seal.SharePath, seal.id)
|
||||||
|
seal.sys.Ephemeral(system.Process, seal.share, 0711)
|
||||||
|
|
||||||
|
// ensure child tmpdir parent directory (e.g. `/tmp/fortify.%d/tmpdir`)
|
||||||
|
targetTmpdirParent := path.Join(seal.SharePath, "tmpdir")
|
||||||
|
seal.sys.Ensure(targetTmpdirParent, 0700)
|
||||||
|
seal.sys.UpdatePermType(system.User, targetTmpdirParent, acl.Execute)
|
||||||
|
|
||||||
|
// ensure child tmpdir (e.g. `/tmp/fortify.%d/tmpdir/%d`)
|
||||||
|
targetTmpdir := path.Join(targetTmpdirParent, seal.sys.user.as)
|
||||||
|
seal.sys.Ensure(targetTmpdir, 01700)
|
||||||
|
seal.sys.UpdatePermType(system.User, targetTmpdir, acl.Read, acl.Write, acl.Execute)
|
||||||
|
seal.sys.bwrap.Bind(targetTmpdir, "/tmp", false, true)
|
||||||
|
|
||||||
|
/*
|
||||||
|
XDG runtime directory
|
||||||
|
*/
|
||||||
|
|
||||||
|
// mount tmpfs on inner runtime (e.g. `/run/user/%d`)
|
||||||
|
seal.sys.bwrap.Tmpfs("/run/user", 1*1024*1024)
|
||||||
|
seal.sys.bwrap.Tmpfs(seal.sys.runtime, 8*1024*1024)
|
||||||
|
|
||||||
|
// point to inner runtime path `/run/user/%d`
|
||||||
|
seal.sys.bwrap.SetEnv[xdgRuntimeDir] = seal.sys.runtime
|
||||||
|
seal.sys.bwrap.SetEnv[xdgSessionClass] = "user"
|
||||||
|
seal.sys.bwrap.SetEnv[xdgSessionType] = "tty"
|
||||||
|
|
||||||
|
// ensure RunDir (e.g. `/run/user/%d/fortify`)
|
||||||
|
seal.sys.Ensure(seal.RunDirPath, 0700)
|
||||||
|
seal.sys.UpdatePermType(system.User, seal.RunDirPath, acl.Execute)
|
||||||
|
|
||||||
|
// ensure runtime directory ACL (e.g. `/run/user/%d`)
|
||||||
|
seal.sys.Ensure(seal.RuntimePath, 0700) // ensure this dir in case XDG_RUNTIME_DIR is unset
|
||||||
|
seal.sys.UpdatePermType(system.User, seal.RuntimePath, acl.Execute)
|
||||||
|
|
||||||
|
// ensure process-specific share local to XDG_RUNTIME_DIR (e.g. `/run/user/%d/fortify/%s`)
|
||||||
|
seal.shareLocal = path.Join(seal.RunDirPath, seal.id)
|
||||||
|
seal.sys.Ephemeral(system.Process, seal.shareLocal, 0700)
|
||||||
|
seal.sys.UpdatePerm(seal.shareLocal, acl.Execute)
|
||||||
|
|
||||||
|
/*
|
||||||
|
Inner passwd database
|
||||||
|
*/
|
||||||
|
|
||||||
|
// look up shell
|
||||||
|
sh := "/bin/sh"
|
||||||
|
if s, ok := os.LookupEnv(shell); ok {
|
||||||
|
seal.sys.bwrap.SetEnv[shell] = s
|
||||||
|
sh = s
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate /etc/passwd
|
||||||
|
passwdPath := path.Join(seal.share, "passwd")
|
||||||
|
username := "chronos"
|
||||||
|
if seal.sys.user.username != "" {
|
||||||
|
username = seal.sys.user.username
|
||||||
|
}
|
||||||
|
homeDir := "/var/empty"
|
||||||
|
if seal.sys.user.home != "" {
|
||||||
|
homeDir = seal.sys.user.home
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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"
|
||||||
|
seal.sys.Write(passwdPath, passwd)
|
||||||
|
|
||||||
|
// write /etc/group
|
||||||
|
groupPath := path.Join(seal.share, "group")
|
||||||
|
seal.sys.Write(groupPath, "fortify:x:"+seal.sys.mappedIDString+":\n")
|
||||||
|
|
||||||
|
// bind /etc/passwd and /etc/group
|
||||||
|
seal.sys.bwrap.Bind(passwdPath, "/etc/passwd")
|
||||||
|
seal.sys.bwrap.Bind(groupPath, "/etc/group")
|
||||||
|
|
||||||
|
/*
|
||||||
|
Display servers
|
||||||
|
*/
|
||||||
|
|
||||||
|
// pass $TERM to launcher
|
||||||
|
if t, ok := os.LookupEnv(term); ok {
|
||||||
|
seal.sys.bwrap.SetEnv[term] = t
|
||||||
|
}
|
||||||
|
|
||||||
|
// set up wayland
|
||||||
|
if seal.et.Has(system.EWayland) {
|
||||||
|
var wp string
|
||||||
|
if wd, ok := os.LookupEnv(waylandDisplay); !ok {
|
||||||
|
return fmsg.WrapError(ErrWayland,
|
||||||
|
"WAYLAND_DISPLAY is not set")
|
||||||
|
} else {
|
||||||
|
wp = path.Join(seal.RuntimePath, wd)
|
||||||
|
}
|
||||||
|
|
||||||
|
w := path.Join(seal.sys.runtime, "wayland-0")
|
||||||
|
seal.sys.bwrap.SetEnv[waylandDisplay] = w
|
||||||
|
|
||||||
|
if !seal.directWayland { // set up security-context-v1
|
||||||
|
wc := path.Join(seal.SharePath, "wayland")
|
||||||
|
wt := path.Join(wc, seal.id)
|
||||||
|
seal.sys.Ensure(wc, 0711)
|
||||||
|
appID := seal.fid
|
||||||
|
if appID == "" {
|
||||||
|
// use instance ID in case app id is not set
|
||||||
|
appID = "moe.ophivana.fortify." + seal.id
|
||||||
|
}
|
||||||
|
seal.sys.Wayland(wt, wp, appID, seal.id)
|
||||||
|
seal.sys.bwrap.Bind(wt, w)
|
||||||
|
} else { // bind mount wayland socket (insecure)
|
||||||
|
// hardlink wayland socket
|
||||||
|
wpi := path.Join(seal.shareLocal, "wayland")
|
||||||
|
seal.sys.Link(wp, wpi)
|
||||||
|
seal.sys.bwrap.Bind(wpi, w)
|
||||||
|
|
||||||
|
// ensure Wayland socket ACL (e.g. `/run/user/%d/wayland-%d`)
|
||||||
|
seal.sys.UpdatePermType(system.EWayland, wp, acl.Read, acl.Write, acl.Execute)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// set up X11
|
||||||
|
if seal.et.Has(system.EX11) {
|
||||||
|
// discover X11 and grant user permission via the `ChangeHosts` command
|
||||||
|
if d, ok := os.LookupEnv(display); !ok {
|
||||||
|
return fmsg.WrapError(ErrXDisplay,
|
||||||
|
"DISPLAY is not set")
|
||||||
|
} else {
|
||||||
|
seal.sys.ChangeHosts("#" + seal.sys.user.us)
|
||||||
|
seal.sys.bwrap.SetEnv[display] = d
|
||||||
|
seal.sys.bwrap.Bind("/tmp/.X11-unix", "/tmp/.X11-unix")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
PulseAudio server and authentication
|
||||||
|
*/
|
||||||
|
|
||||||
|
if seal.et.Has(system.EPulse) {
|
||||||
|
// check PulseAudio directory presence (e.g. `/run/user/%d/pulse`)
|
||||||
|
pd := path.Join(seal.RuntimePath, "pulse")
|
||||||
|
ps := path.Join(pd, "native")
|
||||||
|
if _, err := os.Stat(pd); err != nil {
|
||||||
|
if !errors.Is(err, fs.ErrNotExist) {
|
||||||
|
return fmsg.WrapErrorSuffix(err,
|
||||||
|
fmt.Sprintf("cannot access PulseAudio directory %q:", pd))
|
||||||
|
}
|
||||||
|
return fmsg.WrapError(ErrPulseSocket,
|
||||||
|
fmt.Sprintf("PulseAudio directory %q not found", pd))
|
||||||
|
}
|
||||||
|
|
||||||
|
// check PulseAudio socket permission (e.g. `/run/user/%d/pulse/native`)
|
||||||
|
if s, err := os.Stat(ps); err != nil {
|
||||||
|
if !errors.Is(err, fs.ErrNotExist) {
|
||||||
|
return fmsg.WrapErrorSuffix(err,
|
||||||
|
fmt.Sprintf("cannot access PulseAudio socket %q:", ps))
|
||||||
|
}
|
||||||
|
return fmsg.WrapError(ErrPulseSocket,
|
||||||
|
fmt.Sprintf("PulseAudio directory %q found but socket does not exist", pd))
|
||||||
|
} else {
|
||||||
|
if m := s.Mode(); m&0o006 != 0o006 {
|
||||||
|
return fmsg.WrapError(ErrPulseMode,
|
||||||
|
fmt.Sprintf("unexpected permissions on %q:", ps), m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// hard link pulse socket into target-executable share
|
||||||
|
psi := path.Join(seal.shareLocal, "pulse")
|
||||||
|
p := path.Join(seal.sys.runtime, "pulse", "native")
|
||||||
|
seal.sys.Link(ps, psi)
|
||||||
|
seal.sys.bwrap.Bind(psi, p)
|
||||||
|
seal.sys.bwrap.SetEnv[pulseServer] = "unix:" + p
|
||||||
|
|
||||||
|
// publish current user's pulse cookie for target user
|
||||||
|
if src, err := discoverPulseCookie(os); err != nil {
|
||||||
|
// not fatal
|
||||||
|
fmsg.VPrintln(err.(*fmsg.BaseError).Message())
|
||||||
|
} else {
|
||||||
|
dst := path.Join(seal.share, "pulse-cookie")
|
||||||
|
innerDst := fst.Tmp + "/pulse-cookie"
|
||||||
|
seal.sys.bwrap.SetEnv[pulseCookie] = innerDst
|
||||||
|
seal.sys.CopyFile(dst, src)
|
||||||
|
seal.sys.bwrap.Bind(dst, innerDst)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
D-Bus proxy
|
||||||
|
*/
|
||||||
|
|
||||||
|
if seal.et.Has(system.EDBus) {
|
||||||
|
// ensure dbus session bus defaults
|
||||||
|
if bus[0] == nil {
|
||||||
|
bus[0] = dbus.NewConfig(seal.fid, true, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// downstream socket paths
|
||||||
|
sessionPath, systemPath := path.Join(seal.share, "bus"), path.Join(seal.share, "system_bus_socket")
|
||||||
|
|
||||||
|
// configure dbus proxy
|
||||||
|
if f, err := seal.sys.ProxyDBus(bus[0], bus[1], sessionPath, systemPath); err != nil {
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
seal.dbusMsg = f
|
||||||
|
}
|
||||||
|
|
||||||
|
// share proxy sockets
|
||||||
|
sessionInner := path.Join(seal.sys.runtime, "bus")
|
||||||
|
seal.sys.bwrap.SetEnv[dbusSessionBusAddress] = "unix:path=" + sessionInner
|
||||||
|
seal.sys.bwrap.Bind(sessionPath, sessionInner)
|
||||||
|
seal.sys.UpdatePerm(sessionPath, acl.Read, acl.Write)
|
||||||
|
if bus[1] != nil {
|
||||||
|
systemInner := "/run/dbus/system_bus_socket"
|
||||||
|
seal.sys.bwrap.SetEnv[dbusSystemBusAddress] = "unix:path=" + systemInner
|
||||||
|
seal.sys.bwrap.Bind(systemPath, systemInner)
|
||||||
|
seal.sys.UpdatePerm(systemPath, acl.Read, acl.Write)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Miscellaneous
|
||||||
|
*/
|
||||||
|
|
||||||
|
// queue overriding tmpfs at the end of seal.sys.bwrap.Filesystem
|
||||||
|
for _, dest := range seal.sys.override {
|
||||||
|
seal.sys.bwrap.Tmpfs(dest, 8*1024)
|
||||||
|
}
|
||||||
|
|
||||||
|
// append extra perms
|
||||||
|
for _, p := range seal.extraPerms {
|
||||||
|
if p == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seal.sys.UpdatePermType(system.User, p.name, p.perms...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// discoverPulseCookie attempts various standard methods to discover the current user's PulseAudio authentication cookie
|
||||||
|
func discoverPulseCookie(os linux.System) (string, error) {
|
||||||
|
if p, ok := os.LookupEnv(pulseCookie); ok {
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// dotfile $HOME/.pulse-cookie
|
||||||
|
if p, ok := os.LookupEnv(home); ok {
|
||||||
|
p = path.Join(p, ".pulse-cookie")
|
||||||
|
if s, err := os.Stat(p); err != nil {
|
||||||
|
if !errors.Is(err, fs.ErrNotExist) {
|
||||||
|
return p, fmsg.WrapErrorSuffix(err,
|
||||||
|
fmt.Sprintf("cannot access PulseAudio cookie %q:", p))
|
||||||
|
}
|
||||||
|
// not found, try next method
|
||||||
|
} else if !s.IsDir() {
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// $XDG_CONFIG_HOME/pulse/cookie
|
||||||
|
if p, ok := os.LookupEnv(xdgConfigHome); ok {
|
||||||
|
p = path.Join(p, "pulse", "cookie")
|
||||||
|
if s, err := os.Stat(p); err != nil {
|
||||||
|
if !errors.Is(err, fs.ErrNotExist) {
|
||||||
|
return p, fmsg.WrapErrorSuffix(err,
|
||||||
|
fmt.Sprintf("cannot access PulseAudio cookie %q:", p))
|
||||||
|
}
|
||||||
|
// not found, try next method
|
||||||
|
} else if !s.IsDir() {
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmsg.WrapError(ErrPulseCookie,
|
||||||
|
fmt.Sprintf("cannot locate PulseAudio cookie (tried $%s, $%s/pulse/cookie, $%s/.pulse-cookie)",
|
||||||
|
pulseCookie, xdgConfigHome, home))
|
||||||
|
}
|
@ -1,119 +0,0 @@
|
|||||||
package app
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io/fs"
|
|
||||||
"path"
|
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/fst"
|
|
||||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
|
||||||
"git.gensokyo.uk/security/fortify/internal/linux"
|
|
||||||
"git.gensokyo.uk/security/fortify/internal/system"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
pulseServer = "PULSE_SERVER"
|
|
||||||
pulseCookie = "PULSE_COOKIE"
|
|
||||||
|
|
||||||
home = "HOME"
|
|
||||||
xdgConfigHome = "XDG_CONFIG_HOME"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrPulseCookie = errors.New("pulse cookie not present")
|
|
||||||
ErrPulseSocket = errors.New("pulse socket not present")
|
|
||||||
ErrPulseMode = errors.New("unexpected pulse socket mode")
|
|
||||||
)
|
|
||||||
|
|
||||||
func (seal *appSeal) sharePulse(os linux.System) error {
|
|
||||||
if !seal.et.Has(system.EPulse) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// check PulseAudio directory presence (e.g. `/run/user/%d/pulse`)
|
|
||||||
pd := path.Join(seal.RuntimePath, "pulse")
|
|
||||||
ps := path.Join(pd, "native")
|
|
||||||
if _, err := os.Stat(pd); err != nil {
|
|
||||||
if !errors.Is(err, fs.ErrNotExist) {
|
|
||||||
return fmsg.WrapErrorSuffix(err,
|
|
||||||
fmt.Sprintf("cannot access PulseAudio directory %q:", pd))
|
|
||||||
}
|
|
||||||
return fmsg.WrapError(ErrPulseSocket,
|
|
||||||
fmt.Sprintf("PulseAudio directory %q not found", pd))
|
|
||||||
}
|
|
||||||
|
|
||||||
// check PulseAudio socket permission (e.g. `/run/user/%d/pulse/native`)
|
|
||||||
if s, err := os.Stat(ps); err != nil {
|
|
||||||
if !errors.Is(err, fs.ErrNotExist) {
|
|
||||||
return fmsg.WrapErrorSuffix(err,
|
|
||||||
fmt.Sprintf("cannot access PulseAudio socket %q:", ps))
|
|
||||||
}
|
|
||||||
return fmsg.WrapError(ErrPulseSocket,
|
|
||||||
fmt.Sprintf("PulseAudio directory %q found but socket does not exist", pd))
|
|
||||||
} else {
|
|
||||||
if m := s.Mode(); m&0o006 != 0o006 {
|
|
||||||
return fmsg.WrapError(ErrPulseMode,
|
|
||||||
fmt.Sprintf("unexpected permissions on %q:", ps), m)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// hard link pulse socket into target-executable share
|
|
||||||
psi := path.Join(seal.shareLocal, "pulse")
|
|
||||||
p := path.Join(seal.sys.runtime, "pulse", "native")
|
|
||||||
seal.sys.Link(ps, psi)
|
|
||||||
seal.sys.bwrap.Bind(psi, p)
|
|
||||||
seal.sys.bwrap.SetEnv[pulseServer] = "unix:" + p
|
|
||||||
|
|
||||||
// publish current user's pulse cookie for target user
|
|
||||||
if src, err := discoverPulseCookie(os); err != nil {
|
|
||||||
fmsg.VPrintln(err.(*fmsg.BaseError).Message())
|
|
||||||
} else {
|
|
||||||
dst := path.Join(seal.share, "pulse-cookie")
|
|
||||||
innerDst := fst.Tmp + "/pulse-cookie"
|
|
||||||
seal.sys.bwrap.SetEnv[pulseCookie] = innerDst
|
|
||||||
seal.sys.CopyFile(dst, src)
|
|
||||||
seal.sys.bwrap.Bind(dst, innerDst)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// discoverPulseCookie attempts various standard methods to discover the current user's PulseAudio authentication cookie
|
|
||||||
func discoverPulseCookie(os linux.System) (string, error) {
|
|
||||||
if p, ok := os.LookupEnv(pulseCookie); ok {
|
|
||||||
return p, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// dotfile $HOME/.pulse-cookie
|
|
||||||
if p, ok := os.LookupEnv(home); ok {
|
|
||||||
p = path.Join(p, ".pulse-cookie")
|
|
||||||
if s, err := os.Stat(p); err != nil {
|
|
||||||
if !errors.Is(err, fs.ErrNotExist) {
|
|
||||||
return p, fmsg.WrapErrorSuffix(err,
|
|
||||||
fmt.Sprintf("cannot access PulseAudio cookie %q:", p))
|
|
||||||
}
|
|
||||||
// not found, try next method
|
|
||||||
} else if !s.IsDir() {
|
|
||||||
return p, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// $XDG_CONFIG_HOME/pulse/cookie
|
|
||||||
if p, ok := os.LookupEnv(xdgConfigHome); ok {
|
|
||||||
p = path.Join(p, "pulse", "cookie")
|
|
||||||
if s, err := os.Stat(p); err != nil {
|
|
||||||
if !errors.Is(err, fs.ErrNotExist) {
|
|
||||||
return p, fmsg.WrapErrorSuffix(err,
|
|
||||||
fmt.Sprintf("cannot access PulseAudio cookie %q:", p))
|
|
||||||
}
|
|
||||||
// not found, try next method
|
|
||||||
} else if !s.IsDir() {
|
|
||||||
return p, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", fmsg.WrapError(ErrPulseCookie,
|
|
||||||
fmt.Sprintf("cannot locate PulseAudio cookie (tried $%s, $%s/pulse/cookie, $%s/.pulse-cookie)",
|
|
||||||
pulseCookie, xdgConfigHome, home))
|
|
||||||
}
|
|
@ -1,39 +0,0 @@
|
|||||||
package app
|
|
||||||
|
|
||||||
import (
|
|
||||||
"path"
|
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/acl"
|
|
||||||
"git.gensokyo.uk/security/fortify/internal/system"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
xdgRuntimeDir = "XDG_RUNTIME_DIR"
|
|
||||||
xdgSessionClass = "XDG_SESSION_CLASS"
|
|
||||||
xdgSessionType = "XDG_SESSION_TYPE"
|
|
||||||
)
|
|
||||||
|
|
||||||
// shareRuntime queues actions for sharing/ensuring the runtime and share directories
|
|
||||||
func (seal *appSeal) shareRuntime() {
|
|
||||||
// mount tmpfs on inner runtime (e.g. `/run/user/%d`)
|
|
||||||
seal.sys.bwrap.Tmpfs("/run/user", 1*1024*1024)
|
|
||||||
seal.sys.bwrap.Tmpfs(seal.sys.runtime, 8*1024*1024)
|
|
||||||
|
|
||||||
// point to inner runtime path `/run/user/%d`
|
|
||||||
seal.sys.bwrap.SetEnv[xdgRuntimeDir] = seal.sys.runtime
|
|
||||||
seal.sys.bwrap.SetEnv[xdgSessionClass] = "user"
|
|
||||||
seal.sys.bwrap.SetEnv[xdgSessionType] = "tty"
|
|
||||||
|
|
||||||
// ensure RunDir (e.g. `/run/user/%d/fortify`)
|
|
||||||
seal.sys.Ensure(seal.RunDirPath, 0700)
|
|
||||||
seal.sys.UpdatePermType(system.User, seal.RunDirPath, acl.Execute)
|
|
||||||
|
|
||||||
// ensure runtime directory ACL (e.g. `/run/user/%d`)
|
|
||||||
seal.sys.Ensure(seal.RuntimePath, 0700) // ensure this dir in case XDG_RUNTIME_DIR is unset
|
|
||||||
seal.sys.UpdatePermType(system.User, seal.RuntimePath, acl.Execute)
|
|
||||||
|
|
||||||
// ensure process-specific share local to XDG_RUNTIME_DIR (e.g. `/run/user/%d/fortify/%s`)
|
|
||||||
seal.shareLocal = path.Join(seal.RunDirPath, seal.id)
|
|
||||||
seal.sys.Ephemeral(system.Process, seal.shareLocal, 0700)
|
|
||||||
seal.sys.UpdatePerm(seal.shareLocal, acl.Execute)
|
|
||||||
}
|
|
@ -1,74 +0,0 @@
|
|||||||
package app
|
|
||||||
|
|
||||||
import (
|
|
||||||
"path"
|
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/acl"
|
|
||||||
"git.gensokyo.uk/security/fortify/internal/linux"
|
|
||||||
"git.gensokyo.uk/security/fortify/internal/system"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
shell = "SHELL"
|
|
||||||
)
|
|
||||||
|
|
||||||
// shareSystem queues various system-related actions
|
|
||||||
func (seal *appSeal) shareSystem() {
|
|
||||||
// ensure Share (e.g. `/tmp/fortify.%d`)
|
|
||||||
// acl is unnecessary as this directory is world executable
|
|
||||||
seal.sys.Ensure(seal.SharePath, 0711)
|
|
||||||
|
|
||||||
// ensure process-specific share (e.g. `/tmp/fortify.%d/%s`)
|
|
||||||
// acl is unnecessary as this directory is world executable
|
|
||||||
seal.share = path.Join(seal.SharePath, seal.id)
|
|
||||||
seal.sys.Ephemeral(system.Process, seal.share, 0711)
|
|
||||||
|
|
||||||
// ensure child tmpdir parent directory (e.g. `/tmp/fortify.%d/tmpdir`)
|
|
||||||
targetTmpdirParent := path.Join(seal.SharePath, "tmpdir")
|
|
||||||
seal.sys.Ensure(targetTmpdirParent, 0700)
|
|
||||||
seal.sys.UpdatePermType(system.User, targetTmpdirParent, acl.Execute)
|
|
||||||
|
|
||||||
// ensure child tmpdir (e.g. `/tmp/fortify.%d/tmpdir/%d`)
|
|
||||||
targetTmpdir := path.Join(targetTmpdirParent, seal.sys.user.as)
|
|
||||||
seal.sys.Ensure(targetTmpdir, 01700)
|
|
||||||
seal.sys.UpdatePermType(system.User, targetTmpdir, acl.Read, acl.Write, acl.Execute)
|
|
||||||
seal.sys.bwrap.Bind(targetTmpdir, "/tmp", false, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (seal *appSeal) sharePasswd(os linux.System) {
|
|
||||||
// look up shell
|
|
||||||
sh := "/bin/sh"
|
|
||||||
if s, ok := os.LookupEnv(shell); ok {
|
|
||||||
seal.sys.bwrap.SetEnv[shell] = s
|
|
||||||
sh = s
|
|
||||||
}
|
|
||||||
|
|
||||||
// generate /etc/passwd
|
|
||||||
passwdPath := path.Join(seal.share, "passwd")
|
|
||||||
username := "chronos"
|
|
||||||
if seal.sys.user.username != "" {
|
|
||||||
username = seal.sys.user.username
|
|
||||||
}
|
|
||||||
homeDir := "/var/empty"
|
|
||||||
if seal.sys.user.home != "" {
|
|
||||||
homeDir = seal.sys.user.home
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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"
|
|
||||||
seal.sys.Write(passwdPath, passwd)
|
|
||||||
|
|
||||||
// write /etc/group
|
|
||||||
groupPath := path.Join(seal.share, "group")
|
|
||||||
seal.sys.Write(groupPath, "fortify:x:"+seal.sys.mappedIDString+":\n")
|
|
||||||
|
|
||||||
// bind /etc/passwd and /etc/group
|
|
||||||
seal.sys.bwrap.Bind(passwdPath, "/etc/passwd")
|
|
||||||
seal.sys.bwrap.Bind(groupPath, "/etc/group")
|
|
||||||
}
|
|
@ -1,9 +1,7 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"git.gensokyo.uk/security/fortify/dbus"
|
|
||||||
"git.gensokyo.uk/security/fortify/helper/bwrap"
|
"git.gensokyo.uk/security/fortify/helper/bwrap"
|
||||||
"git.gensokyo.uk/security/fortify/internal/linux"
|
|
||||||
"git.gensokyo.uk/security/fortify/internal/system"
|
"git.gensokyo.uk/security/fortify/internal/system"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -51,37 +49,3 @@ type appUser struct {
|
|||||||
// passwd database username
|
// passwd database username
|
||||||
username string
|
username string
|
||||||
}
|
}
|
||||||
|
|
||||||
// shareAll calls all share methods in sequence
|
|
||||||
func (seal *appSeal) shareAll(bus [2]*dbus.Config, os linux.System) error {
|
|
||||||
if seal.shared {
|
|
||||||
panic("seal shared twice")
|
|
||||||
}
|
|
||||||
seal.shared = true
|
|
||||||
|
|
||||||
seal.shareSystem()
|
|
||||||
seal.shareRuntime()
|
|
||||||
seal.sharePasswd(os)
|
|
||||||
if err := seal.shareDisplay(os); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := seal.sharePulse(os); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensure dbus session bus defaults
|
|
||||||
if bus[0] == nil {
|
|
||||||
bus[0] = dbus.NewConfig(seal.fid, true, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := seal.shareDBus(bus); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// queue overriding tmpfs at the end of seal.sys.bwrap.Filesystem
|
|
||||||
for _, dest := range seal.sys.override {
|
|
||||||
seal.sys.bwrap.Tmpfs(dest, 8*1024)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
@ -3,6 +3,7 @@ package internal
|
|||||||
import "path"
|
import "path"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
Fortify = compPoison
|
||||||
Fsu = compPoison
|
Fsu = compPoison
|
||||||
Finit = compPoison
|
Finit = compPoison
|
||||||
)
|
)
|
||||||
|
@ -45,6 +45,7 @@ buildGoModule rec {
|
|||||||
Version = "v${version}";
|
Version = "v${version}";
|
||||||
Fsu = "/run/wrappers/bin/fsu";
|
Fsu = "/run/wrappers/bin/fsu";
|
||||||
Finit = "${placeholder "out"}/libexec/finit";
|
Finit = "${placeholder "out"}/libexec/finit";
|
||||||
|
Fortify = "${placeholder "out"}/bin/fortify";
|
||||||
};
|
};
|
||||||
|
|
||||||
# nix build environment does not allow acls
|
# nix build environment does not allow acls
|
||||||
|
22
print.go
22
print.go
@ -90,10 +90,17 @@ func printShow(instance *state.State, config *fst.Config, short bool) {
|
|||||||
fmt.Fprintf(w, " Command:\t%s\n", strings.Join(config.Command, " "))
|
fmt.Fprintf(w, " Command:\t%s\n", strings.Join(config.Command, " "))
|
||||||
fmt.Fprintf(w, "\n")
|
fmt.Fprintf(w, "\n")
|
||||||
|
|
||||||
if !short && config.Confinement.Sandbox != nil && len(config.Confinement.Sandbox.Filesystem) > 0 {
|
if !short {
|
||||||
fmt.Fprintf(w, "Filesystem:\n")
|
if config.Confinement.Sandbox != nil && len(config.Confinement.Sandbox.Filesystem) > 0 {
|
||||||
|
fmt.Fprintf(w, "Filesystem\n")
|
||||||
for _, f := range config.Confinement.Sandbox.Filesystem {
|
for _, f := range config.Confinement.Sandbox.Filesystem {
|
||||||
|
if f == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
expr := new(strings.Builder)
|
expr := new(strings.Builder)
|
||||||
|
expr.Grow(3 + len(f.Src) + 1 + len(f.Dst))
|
||||||
|
|
||||||
if f.Device {
|
if f.Device {
|
||||||
expr.WriteString(" d")
|
expr.WriteString(" d")
|
||||||
} else if f.Write {
|
} else if f.Write {
|
||||||
@ -114,6 +121,17 @@ func printShow(instance *state.State, config *fst.Config, short bool) {
|
|||||||
}
|
}
|
||||||
fmt.Fprintf(w, "\n")
|
fmt.Fprintf(w, "\n")
|
||||||
}
|
}
|
||||||
|
if len(config.Confinement.ExtraPerms) > 0 {
|
||||||
|
fmt.Fprintf(w, "Extra ACL\n")
|
||||||
|
for _, p := range config.Confinement.ExtraPerms {
|
||||||
|
if p == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fmt.Fprintf(w, " %s\n", p.String())
|
||||||
|
}
|
||||||
|
fmt.Fprintf(w, "\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
printDBus := func(c *dbus.Config) {
|
printDBus := func(c *dbus.Config) {
|
||||||
fmt.Fprintf(w, " Filter:\t%v\n", c.Filter)
|
fmt.Fprintf(w, " Filter:\t%v\n", c.Filter)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user