Compare commits

..

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

52 changed files with 1358 additions and 2563 deletions

View File

@ -30,14 +30,8 @@ jobs:
- name: Build for Linux - name: Build for Linux
run: >- run: >-
go build -v -ldflags '-s -w sh -c "go build -v -ldflags '-s -w -X main.Version=${{ github.ref_name }}' -o bin/fortify &&
-X git.ophivana.moe/security/fortify/internal.Version=${{ github.ref_name }} sha256sum --tag -b bin/fortify > bin/fortify.sha256"
-X git.ophivana.moe/security/fortify/internal.Fsu=/usr/bin/fsu
-X git.ophivana.moe/security/fortify/internal.Finit=/usr/libexec/fortify/finit
-X main.Fmain=/usr/bin/fortify
-X main.Fshim=/usr/libexec/fortify/fshim'
-o bin/ ./... &&
(cd bin && sha512sum --tag -b * > sha512sums)
- name: Release - name: Release
id: use-go-action id: use-go-action

View File

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

145
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,21 +68,21 @@ 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;
discord = {
command = "vesktop --ozone-platform-hint=wayland";
share = pkgs.vesktop;
}; };
apps = [ chromium.dbus = {
{ configSystem = {
name = "chromium";
id = "org.chromium.Chromium";
packages = [ pkgs.chromium ];
userns = true;
mapRealUid = true;
dbus = {
system = {
filter = true; filter = true;
talk = [ talk = [
"org.bluez" "org.bluez"
@ -93,9 +90,8 @@ This adds the `environment.fortify` option:
"org.freedesktop.UPower" "org.freedesktop.UPower"
]; ];
}; };
session = config = {
f: filter = true;
f {
talk = [ talk = [
"org.freedesktop.DBus" "org.freedesktop.DBus"
"org.freedesktop.FileManager1" "org.freedesktop.FileManager1"
@ -110,63 +106,74 @@ This adds the `environment.fortify` option:
"org.mpris.MediaPlayer2.org.chromium.Chromium.*" "org.mpris.MediaPlayer2.org.chromium.Chromium.*"
"org.mpris.MediaPlayer2.chromium.*" "org.mpris.MediaPlayer2.chromium.*"
]; ];
call = { }; call = {
broadcast = { }; "org.freedesktop.portal.*" = "*";
};
broadcast = {
"org.freedesktop.portal.*" = "@/org/freedesktop/portal/*";
}; };
}; };
}
{
name = "claws-mail";
id = "org.claws_mail.Claws-Mail";
packages = [ pkgs.claws-mail ];
gpu = false;
capability.pulse = false;
}
{
name = "weechat";
packages = [ pkgs.weechat ];
capability = {
wayland = false;
x11 = false;
dbus = true;
pulse = false;
}; };
}
{
name = "discord";
id = "dev.vencord.Vesktop";
packages = [ pkgs.vesktop ];
share = pkgs.vesktop;
command = "vesktop --ozone-platform-hint=wayland";
userns = true;
mapRealUid = true;
capability.x11 = true;
dbus = {
session =
f:
f {
talk = [ "org.kde.StatusNotifierWatcher" ];
own = [ ];
call = { };
broadcast = { };
}; };
system.filter = true; packages = with pkgs; [
}; weechat
} claws-mail
{ vesktop
name = "looking-glass-client"; chromium
groups = [ "plugdev" ]; ];
extraPaths = [ persistence.directories = [
{ ".config/weechat"
src = "/dev/shm/looking-glass"; ".claws-mail"
write = true; ".config/vesktop"
}
]; ];
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

@ -1,4 +1,3 @@
// Package acl implements simple ACL manipulation via libacl.
package acl package acl
import "unsafe" import "unsafe"

View File

@ -1,31 +1,25 @@
package main package main
import ( import (
"bytes" "bufio"
"fmt"
"log" "log"
"os" "os"
"path" "path"
"slices"
"strconv" "strconv"
"strings" "strings"
"syscall" "syscall"
) )
const ( const (
compPoison = "INVALIDINVALIDINVALIDINVALIDINVALID"
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 fpPoison = "INVALIDINVALIDINVALIDINVALIDINVALID"
) )
var ( // FortifyPath is the path to fortify, set at compile time.
Fmain = compPoison var FortifyPath = fpPoison
Fshim = compPoison
)
func main() { func main() {
log.SetFlags(0) log.SetFlags(0)
@ -41,16 +35,9 @@ 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 // validate compiled in fortify path
if p, ok := checkPath(Fmain); !ok { if FortifyPath == fpPoison || !path.IsAbs(FortifyPath) {
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 {
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")
@ -58,7 +45,7 @@ func main() {
log.Fatalf("cannot read parent executable path: %v", err) log.Fatalf("cannot read parent executable path: %v", err)
} else if strings.HasSuffix(p, " (deleted)") { } else if strings.HasSuffix(p, " (deleted)") {
log.Fatal("fortify executable has been deleted") log.Fatal("fortify executable has been deleted")
} else if p != fmain { } else if p != FortifyPath {
log.Fatal("this program must be started by fortify") log.Fatal("this program must be started by fortify")
} }
@ -74,76 +61,80 @@ 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 { } else {
suppCurrent = cur uid += aid
} }
// parse space-separated list of group ids
gss := bytes.Split([]byte(gs), []byte{' '})
suppGroups = make([]int, len(gss)+1)
for i, s := range gss {
if gid, err := strconv.Atoi(string(s)); err != nil {
log.Fatalf("cannot parse %q: %v", string(s), err)
} else if gid > 0 && gid != uid && gid != os.Getgid() && slices.Contains(suppCurrent, gid) {
suppGroups[i] = gid
} else {
log.Fatalf("invalid gid %d", gid)
}
}
suppGroups[len(suppGroups)-1] = uid
} else {
suppGroups = []int{uid}
}
// careful! users in the allowlist is effectively allowed to drop groups via fsu
if err := syscall.Setresgid(uid, uid, uid); err != nil { if err := syscall.Setresgid(uid, uid, uid); err != nil {
log.Fatalf("cannot set gid: %v", err) log.Fatalf("cannot set gid: %v", err)
} }
if err := syscall.Setgroups(suppGroups); err != nil {
log.Fatalf("cannot set supplementary groups: %v", err)
}
if err := syscall.Setresuid(uid, uid, uid); err != nil { if err := syscall.Setresuid(uid, uid, uid); err != nil {
log.Fatalf("cannot set uid: %v", err) log.Fatalf("cannot set uid: %v", err)
} }
if _, _, errno := syscall.AllThreadsSyscall(syscall.SYS_PRCTL, PR_SET_NO_NEW_PRIVS, 1, 0); errno != 0 { if err := syscall.Exec(FortifyPath, []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 checkPath(p string) (string, bool) { func parseConfig(p string, puid int) (fid int, ok bool) {
return p, p != compPoison && p != "" && path.IsAbs(p) // 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
}
} }

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

@ -1,4 +1,3 @@
// Package dbus wraps xdg-dbus-proxy and implements configuration and sandboxing of the underlying helper process.
package dbus package dbus
import ( import (

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

@ -1,4 +1,6 @@
// Package helper runs external helpers with optional sandboxing and manages their status/args pipes. /*
Package helper runs external helpers and manages their status and args FDs.
*/
package helper package helper
import ( import (

View File

@ -3,8 +3,8 @@ package app
import ( import (
"sync" "sync"
"git.ophivana.moe/security/fortify/cmd/fshim/ipc/shim" "git.ophivana.moe/security/fortify/internal"
"git.ophivana.moe/security/fortify/internal/linux" "git.ophivana.moe/security/fortify/internal/shim"
) )
type App interface { type App interface {
@ -25,7 +25,7 @@ type app struct {
// application unique identifier // application unique identifier
id *ID id *ID
// operating system interface // operating system interface
os linux.System os internal.System
// shim process manager // shim process manager
shim *shim.Shim shim *shim.Shim
// child process related information // child process related information
@ -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)"
@ -63,7 +63,7 @@ func (a *app) WaitErr() error {
return a.waitErr return a.waitErr
} }
func New(os linux.System) (App, error) { func New(os internal.System) (App, error) {
a := new(app) a := new(app)
a.id = new(ID) a.id = new(ID)
a.os = os a.os = os

View File

@ -2,7 +2,6 @@ package app_test
import ( import (
"fmt" "fmt"
"io"
"io/fs" "io/fs"
"os/user" "os/user"
"strconv" "strconv"
@ -10,8 +9,8 @@ import (
"git.ophivana.moe/security/fortify/acl" "git.ophivana.moe/security/fortify/acl"
"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"
"git.ophivana.moe/security/fortify/internal/app" "git.ophivana.moe/security/fortify/internal/app"
"git.ophivana.moe/security/fortify/internal/linux"
"git.ophivana.moe/security/fortify/internal/system" "git.ophivana.moe/security/fortify/internal/system"
) )
@ -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,24 +42,23 @@ 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",
"TERM": "xterm-256color", "TERM": "xterm-256color",
"USER": "chronos", "USER": "chronos",
"XDG_RUNTIME_DIR": "/run/user/65534", "XDG_RUNTIME_DIR": "/run/user/150",
"XDG_SESSION_CLASS": "user", "XDG_SESSION_CLASS": "user",
"XDG_SESSION_TYPE": "tty"}, "XDG_SESSION_TYPE": "tty"},
Chmod: make(bwrap.ChmodConfig), Chmod: make(bwrap.ChmodConfig),
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/150", 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,19 +285,18 @@ 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/150/bus",
"DBUS_SYSTEM_BUS_ADDRESS": "unix:path=/run/dbus/system_bus_socket", "DBUS_SYSTEM_BUS_ADDRESS": "unix:path=/run/dbus/system_bus_socket",
"HOME": "/home/chronos", "HOME": "/home/chronos",
"PULSE_COOKIE": "/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/pulse-cookie", "PULSE_COOKIE": "/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/pulse-cookie",
"PULSE_SERVER": "unix:/run/user/65534/pulse/native", "PULSE_SERVER": "unix:/run/user/150/pulse/native",
"SHELL": "/run/current-system/sw/bin/zsh", "SHELL": "/run/current-system/sw/bin/zsh",
"TERM": "xterm-256color", "TERM": "xterm-256color",
"USER": "chronos", "USER": "chronos",
"WAYLAND_DISPLAY": "/run/user/65534/wayland-0", "WAYLAND_DISPLAY": "/run/user/150/wayland-0",
"XDG_RUNTIME_DIR": "/run/user/65534", "XDG_RUNTIME_DIR": "/run/user/150",
"XDG_SESSION_CLASS": "user", "XDG_SESSION_CLASS": "user",
"XDG_SESSION_TYPE": "tty", "XDG_SESSION_TYPE": "tty",
}, },
@ -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,96 +353,94 @@ 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/150", 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/150/wayland-0").
Bind("/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c/pulse", "/run/user/65534/pulse/native"). Bind("/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c/pulse", "/run/user/150/pulse/native").
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/pulse-cookie", "/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/pulse-cookie"). Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/pulse-cookie", "/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/pulse-cookie").
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", "/run/user/65534/bus"). Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", "/run/user/150/bus").
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", "/run/dbus/system_bus_socket"). Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", "/run/dbus/system_bus_socket").
Tmpfs("/var/run/nscd", 8192), Tmpfs("/var/run/nscd", 8192),
}, },
@ -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,22 +579,14 @@ 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) Paths() internal.Paths {
panic("requested stdout") return internal.Paths{
}
func (s *stubNixOS) Paths() linux.Paths {
return linux.Paths{
SharePath: "/tmp/fortify.1971", SharePath: "/tmp/fortify.1971",
RuntimePath: "/run/user/1971", RuntimePath: "/run/user/1971",
RunDirPath: "/run/user/1971/fortify", RunDirPath: "/run/user/1971/fortify",
} }
} }
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

@ -7,14 +7,14 @@ import (
"time" "time"
"git.ophivana.moe/security/fortify/helper/bwrap" "git.ophivana.moe/security/fortify/helper/bwrap"
"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/linux"
"git.ophivana.moe/security/fortify/internal/system" "git.ophivana.moe/security/fortify/internal/system"
) )
type sealTestCase struct { type sealTestCase struct {
name string name string
os linux.System os internal.System
config *app.Config config *app.Config
id app.ID id app.ID
wantSys *system.I wantSys *system.I

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,8 @@ 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
MapRealUID bool `json:"map_real_uid"`
// mediated access to wayland socket // mediated access to wayland socket
Wayland bool `json:"wayland,omitempty"` Wayland bool `json:"wayland,omitempty"`
@ -71,8 +58,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 +77,9 @@ 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() *bwrap.Config {
if s == nil { if s == nil {
return nil, errors.New("nil sandbox config") return nil
}
var uid int
if !s.MapRealUID {
uid = 65534
} else {
uid = os.Geteuid()
} }
conf := (&bwrap.Config{ conf := (&bwrap.Config{
@ -115,21 +93,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(65534).SetGID(65534).
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 +115,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 +130,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 +146,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

@ -2,11 +2,11 @@ package app
import ( import (
"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"
"git.ophivana.moe/security/fortify/internal/system" "git.ophivana.moe/security/fortify/internal/system"
) )
func NewWithID(id ID, os linux.System) App { func NewWithID(id ID, os internal.System) App {
a := new(app) a := new(app)
a.id = &id a.id = &id
a.os = os a.os = os

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 as shim
innerCommand.WriteString("exec " + a.seal.sys.executable + " shim")
// 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.seal.sys.executable, "shim")
return
}

View File

@ -2,28 +2,38 @@ 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"
"git.ophivana.moe/security/fortify/dbus" "git.ophivana.moe/security/fortify/dbus"
"git.ophivana.moe/security/fortify/internal"
"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/shim"
"git.ophivana.moe/security/fortify/internal/state" "git.ophivana.moe/security/fortify/internal/state"
"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 {
@ -31,8 +41,6 @@ type appSeal struct {
id string id string
// wayland mediation, disabled if nil // wayland mediation, disabled if nil
wl *shim.Wayland wl *shim.Wayland
// dbus proxy message buffer retriever
dbusMsg func(f func(msgbuf []string))
// freedesktop application ID // freedesktop application ID
fid string fid string
@ -41,11 +49,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
@ -54,7 +66,7 @@ type appSeal struct {
// seal system-level component // seal system-level component
sys *appSealSys sys *appSealSys
linux.Paths internal.Paths
// protected by upstream mutex // protected by upstream mutex
} }
@ -84,63 +96,55 @@ 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 // look up fortify executable path
if config.Confinement.Sandbox != nil && config.Confinement.Sandbox.MapRealUID { if p, err := a.os.Executable(); err != nil {
seal.sys.mappedID = a.os.Geteuid() return fmsg.WrapErrorSuffix(err, "cannot look up fortify executable path:")
} else { } else {
seal.sys.mappedID = 65534 seal.sys.executable = p
}
seal.sys.mappedIDString = strconv.Itoa(seal.sys.mappedID)
seal.sys.runtime = path.Join("/run/user", seal.sys.mappedIDString)
// validate uid and set user info
if config.Confinement.AppID < 0 || config.Confinement.AppID > 9999 {
return fmsg.WrapError(ErrUser,
fmt.Sprintf("aid %d out of range", config.Confinement.AppID))
} 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 // look up user from system
if u, err := a.os.Uid(seal.sys.user.aid); err != nil { if u, err := a.os.Lookup(config.User); err != nil {
return fmsg.WrapErrorSuffix(err, if errors.As(err, new(user.UnknownUserError)) {
"cannot obtain uid from fsu:") return fmsg.WrapError(ErrUser, "unknown user", config.User)
} 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 { } else {
seal.sys.user.supp[i] = g.Gid seal.sys.user = u
} seal.sys.runtime = path.Join("/run/user", u.Uid)
}
} }
// map sandbox config to bwrap // map sandbox config to bwrap
@ -152,7 +156,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 +170,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 +205,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()
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 +248,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 +267,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

@ -22,10 +22,8 @@ func (seal *appSeal) shareDBus(config [2]*dbus.Config) error {
sessionPath, systemPath := path.Join(seal.share, "bus"), path.Join(seal.share, "system_bus_socket") sessionPath, systemPath := path.Join(seal.share, "bus"), path.Join(seal.share, "system_bus_socket")
// configure dbus proxy // configure dbus proxy
if f, err := seal.sys.ProxyDBus(config[0], config[1], sessionPath, systemPath); err != nil { if err := seal.sys.ProxyDBus(config[0], config[1], sessionPath, systemPath); err != nil {
return err return err
} else {
seal.dbusMsg = f
} }
// share proxy sockets // share proxy sockets

View File

@ -5,8 +5,8 @@ import (
"path" "path"
"git.ophivana.moe/security/fortify/acl" "git.ophivana.moe/security/fortify/acl"
"git.ophivana.moe/security/fortify/internal"
"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/system" "git.ophivana.moe/security/fortify/internal/system"
) )
@ -23,7 +23,7 @@ var (
ErrXDisplay = errors.New(display + " unset") ErrXDisplay = errors.New(display + " unset")
) )
func (seal *appSeal) shareDisplay(os linux.System) error { func (seal *appSeal) shareDisplay(os internal.System) error {
// pass $TERM to launcher // pass $TERM to launcher
if t, ok := os.LookupEnv(term); ok { if t, ok := os.LookupEnv(term); ok {
seal.sys.bwrap.SetEnv[term] = t seal.sys.bwrap.SetEnv[term] = t
@ -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

@ -6,8 +6,8 @@ import (
"io/fs" "io/fs"
"path" "path"
"git.ophivana.moe/security/fortify/internal"
"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/system" "git.ophivana.moe/security/fortify/internal/system"
) )
@ -25,7 +25,7 @@ var (
ErrPulseMode = errors.New("unexpected pulse socket mode") ErrPulseMode = errors.New("unexpected pulse socket mode")
) )
func (seal *appSeal) sharePulse(os linux.System) error { func (seal *appSeal) sharePulse(os internal.System) error {
if !seal.et.Has(system.EPulse) { if !seal.et.Has(system.EPulse) {
return nil return nil
} }
@ -78,7 +78,7 @@ func (seal *appSeal) sharePulse(os linux.System) error {
} }
// discoverPulseCookie attempts various standard methods to discover the current user's PulseAudio authentication cookie // discoverPulseCookie attempts various standard methods to discover the current user's PulseAudio authentication cookie
func discoverPulseCookie(os linux.System) (string, error) { func discoverPulseCookie(os internal.System) (string, error) {
if p, ok := os.LookupEnv(pulseCookie); ok { if p, ok := os.LookupEnv(pulseCookie); ok {
return p, nil return p, nil
} }

View File

@ -4,7 +4,7 @@ import (
"path" "path"
"git.ophivana.moe/security/fortify/acl" "git.ophivana.moe/security/fortify/acl"
"git.ophivana.moe/security/fortify/internal/linux" "git.ophivana.moe/security/fortify/internal"
"git.ophivana.moe/security/fortify/internal/system" "git.ophivana.moe/security/fortify/internal/system"
) )
@ -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)
@ -38,7 +38,7 @@ func (seal *appSeal) shareSystem() {
seal.sys.bwrap.Tmpfs(seal.SharePath, 1*1024*1024) seal.sys.bwrap.Tmpfs(seal.SharePath, 1*1024*1024)
} }
func (seal *appSeal) sharePasswd(os linux.System) { func (seal *appSeal) sharePasswd(os internal.System) {
// look up shell // look up shell
sh := "/bin/sh" sh := "/bin/sh"
if s, ok := os.LookupEnv(shell); ok { if s, ok := os.LookupEnv(shell); ok {
@ -49,27 +49,21 @@ func (seal *appSeal) sharePasswd(os linux.System) {
// generate /etc/passwd // generate /etc/passwd
passwdPath := path.Join(seal.share, "passwd") passwdPath := path.Join(seal.share, "passwd")
username := "chronos" username := "chronos"
if seal.sys.user.username != "" { if seal.sys.user.Username != "" {
username = seal.sys.user.username username = seal.sys.user.Username
seal.sys.bwrap.SetEnv["USER"] = seal.sys.user.Username
} }
homeDir := "/var/empty" homeDir := "/var/empty"
if seal.sys.user.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
} }
passwd := username + ":x:65534:65534:Fortify:" + homeDir + ":" + sh + "\n"
// 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) seal.sys.Write(passwdPath, passwd)
// write /etc/group // write /etc/group
groupPath := path.Join(seal.share, "group") groupPath := path.Join(seal.share, "group")
seal.sys.Write(groupPath, "fortify:x:"+seal.sys.mappedIDString+":\n") seal.sys.Write(groupPath, "fortify:x:65534:\n")
// bind /etc/passwd and /etc/group // bind /etc/passwd and /etc/group
seal.sys.bwrap.Bind(passwdPath, "/etc/passwd") seal.sys.bwrap.Bind(passwdPath, "/etc/passwd")

View File

@ -8,10 +8,9 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
shim0 "git.ophivana.moe/security/fortify/cmd/fshim/ipc"
"git.ophivana.moe/security/fortify/cmd/fshim/ipc/shim"
"git.ophivana.moe/security/fortify/helper" "git.ophivana.moe/security/fortify/helper"
"git.ophivana.moe/security/fortify/internal/fmsg" "git.ophivana.moe/security/fortify/internal/fmsg"
"git.ophivana.moe/security/fortify/internal/shim"
"git.ophivana.moe/security/fortify/internal/state" "git.ophivana.moe/security/fortify/internal/state"
"git.ophivana.moe/security/fortify/internal/system" "git.ophivana.moe/security/fortify/internal/system"
) )
@ -23,9 +22,9 @@ func (a *app) Start() error {
defer a.lock.Unlock() defer a.lock.Unlock()
// resolve exec paths // resolve exec paths
shimExec := [2]string{helper.BubblewrapName} shimExec := [3]string{a.seal.sys.executable, helper.BubblewrapName}
if len(a.seal.command) > 0 { if len(a.seal.command) > 0 {
shimExec[1] = a.seal.command[0] shimExec[2] = a.seal.command[0]
} }
for i, n := range shimExec { for i, n := range shimExec {
if len(n) == 0 { if len(n) == 0 {
@ -41,14 +40,20 @@ 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()), &shim.Payload{
a.seal.sys.user.as,
a.seal.sys.user.supp,
path.Join(a.seal.share, "shim"),
a.seal.wl,
&shim0.Payload{
Argv: a.seal.command, Argv: a.seal.command,
Exec: shimExec, Exec: shimExec,
Bwrap: a.seal.sys.bwrap, Bwrap: a.seal.sys.bwrap,
@ -56,6 +61,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 +72,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 +80,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,13 +165,8 @@ 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
@ -175,30 +179,11 @@ func (a *app) Wait() (int, error) {
r = cmd.ProcessState.ExitCode() r = cmd.ProcessState.ExitCode()
} }
fmsg.VPrintf("process %d exited with exit code %d", cmd.Process.Pid, r) fmsg.VPrintf("process %d exited with exit code %d", cmd.Process.Pid, r)
// alternative exit path when kill was unsuccessful
case err := <-a.shim.WaitFallback():
r = 255
if err != nil {
fmsg.Printf("cannot terminate shim on faulted setup: %v", err)
} else {
fmsg.VPrintln("alternative exit path selected")
}
}
} }
// child process exited, resume output // child process exited, resume output
fmsg.Resume() fmsg.Resume()
// print queued up dbus messages
if a.seal.dbusMsg != nil {
a.seal.dbusMsg(func(msgbuf []string) {
for _, msg := range msgbuf {
fmsg.Println(msg)
}
})
}
// close wayland connection // close wayland connection
if a.seal.wl != nil { if a.seal.wl != nil {
if err := a.seal.wl.Close(); err != nil { if err := a.seal.wl.Close(); err != nil {
@ -255,17 +240,10 @@ func (a *app) Wait() (int, error) {
} }
} }
if a.shim.Unwrap() == nil {
fmsg.VPrintln("fault before shim start")
} else {
a.shim.AbortWait(errors.New("shim exited")) a.shim.AbortWait(errors.New("shim exited"))
}
if a.seal.sys.needRevert {
if err := a.seal.sys.Revert(ec); err != nil { if err := a.seal.sys.Revert(ec); err != nil {
return err.(RevertCompoundError) return err.(RevertCompoundError)
} }
}
return nil return nil
}() }()

View File

@ -1,9 +1,11 @@
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"
"git.ophivana.moe/security/fortify/internal/system" "git.ophivana.moe/security/fortify/internal/system"
) )
@ -15,13 +17,10 @@ type appSealSys struct {
// default formatted XDG_RUNTIME_DIR of User // default formatted XDG_RUNTIME_DIR of User
runtime string runtime string
// sealed path to fortify executable, used by shim
executable string
// target user sealed from config // target user sealed from config
user appUser user *user.User
// mapped uid and gid in user namespace
mappedID int
// string representation of mappedID
mappedIDString string
needRevert bool needRevert bool
saveState bool saveState bool
@ -30,30 +29,8 @@ 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 internal.System) error {
if seal.shared { if seal.shared {
panic("seal shared twice") panic("seal shared twice")
} }

View File

@ -1,12 +0,0 @@
package internal
const compPoison = "INVALIDINVALIDINVALIDINVALIDINVALID"
var (
Version = compPoison
)
// Check validates string value set at compile time.
func Check(s string) (string, bool) {
return s, s != compPoison && s != ""
}

View File

@ -8,7 +8,6 @@ import (
var ( var (
wstate atomic.Bool wstate atomic.Bool
dropped atomic.Uint64
withhold = make(chan struct{}, 1) withhold = make(chan struct{}, 1)
msgbuf = make(chan dOp, 64) // these ops are tiny so a large buffer is allocated for withholding output msgbuf = make(chan dOp, 64) // these ops are tiny so a large buffer is allocated for withholding output
@ -30,26 +29,6 @@ func dequeue() {
}() }()
} }
// queue submits ops to msgbuf but drops messages
// when the buffer is full and dequeue is withholding
func queue(op dOp) {
queueSync.Add(1)
select {
case msgbuf <- op:
default:
// send the op anyway if not withholding
// as dequeue will get to it eventually
if !wstate.Load() {
msgbuf <- op
} else {
queueSync.Done()
// increment dropped message count
dropped.Add(1)
}
}
}
type dOp interface{ Do() } type dOp interface{ Do() }
func Exit(code int) { func Exit(code int) {
@ -57,10 +36,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{}{}
} }
} }
@ -69,9 +47,6 @@ func Resume() {
dequeueOnce.Do(dequeue) dequeueOnce.Do(dequeue)
if wstate.CompareAndSwap(true, false) { if wstate.CompareAndSwap(true, false) {
withhold <- struct{}{} withhold <- struct{}{}
if d := dropped.Swap(0); d != 0 {
Printf("dropped %d messages during withhold", d)
}
} }
} }

View File

@ -16,17 +16,20 @@ func SetPrefix(prefix string) {
func Print(v ...any) { func Print(v ...any) {
dequeueOnce.Do(dequeue) dequeueOnce.Do(dequeue)
queue(dPrint(v)) queueSync.Add(1)
msgbuf <- dPrint(v)
} }
func Printf(format string, v ...any) { func Printf(format string, v ...any) {
dequeueOnce.Do(dequeue) dequeueOnce.Do(dequeue)
queue(&dPrintf{format, v}) queueSync.Add(1)
msgbuf <- &dPrintf{format, v}
} }
func Println(v ...any) { func Println(v ...any) {
dequeueOnce.Do(dequeue) dequeueOnce.Do(dequeue)
queue(dPrintln(v)) queueSync.Add(1)
msgbuf <- dPrintln(v)
} }
func Fatal(v ...any) { func Fatal(v ...any) {

View File

@ -1,8 +1,9 @@
package main package init0
import ( import (
"encoding/gob" "encoding/gob"
"errors" "errors"
"flag"
"os" "os"
"os/exec" "os/exec"
"os/signal" "os/signal"
@ -11,80 +12,58 @@ import (
"syscall" "syscall"
"time" "time"
init0 "git.ophivana.moe/security/fortify/cmd/finit/ipc"
"git.ophivana.moe/security/fortify/internal"
"git.ophivana.moe/security/fortify/internal/fmsg" "git.ophivana.moe/security/fortify/internal/fmsg"
) )
const ( const (
// time to wait for linger processes after death of initial process // time to wait for linger processes after death initial process
residualProcessTimeout = 5 * time.Second residualProcessTimeout = 5 * time.Second
) )
// everything beyond this point runs within pid namespace // everything beyond this point runs within pid namespace
// proceed with caution! // proceed with caution!
func main() { func doInit(fd uintptr) {
// sharing stdout with shim
// USE WITH CAUTION
fmsg.SetPrefix("init") fmsg.SetPrefix("init")
// setting this prevents ptrace
if err := internal.PR_SET_DUMPABLE__SUID_DUMP_DISABLE(); err != nil {
fmsg.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err)
panic("unreachable")
}
if os.Getpid() != 1 {
fmsg.Fatal("this process must run as pid 1")
panic("unreachable")
}
// re-exec // re-exec
if len(os.Args) > 0 && (os.Args[0] != "finit" || len(os.Args) != 1) && path.IsAbs(os.Args[0]) { if len(os.Args) > 0 && os.Args[0] != "fortify" && path.IsAbs(os.Args[0]) {
if err := syscall.Exec(os.Args[0], []string{"finit"}, os.Environ()); err != nil { if err := syscall.Exec(os.Args[0], []string{"fortify", "init"}, os.Environ()); err != nil {
fmsg.Println("cannot re-exec self:", err) fmsg.Println("cannot re-exec self:", err)
// continue anyway // continue anyway
} }
} }
// setup pipe fd from environment var payload Payload
var setup *os.File p := os.NewFile(fd, "config-stream")
if s, ok := os.LookupEnv(init0.Env); !ok { if p == nil {
fmsg.Fatal("FORTIFY_INIT not set")
panic("unreachable")
} else {
if fd, err := strconv.Atoi(s); err != nil {
fmsg.Fatalf("cannot parse %q: %v", s, err)
panic("unreachable")
} else {
setup = os.NewFile(uintptr(fd), "setup")
if setup == nil {
fmsg.Fatal("invalid config descriptor") fmsg.Fatal("invalid config descriptor")
panic("unreachable")
} }
} if err := gob.NewDecoder(p).Decode(&payload); err != nil {
} fmsg.Fatal("cannot decode init payload:", err)
var payload init0.Payload
if err := gob.NewDecoder(setup).Decode(&payload); err != nil {
fmsg.Fatal("cannot decode init setup payload:", err)
panic("unreachable")
} else { } else {
// sharing stdout with parent
// USE WITH CAUTION
fmsg.SetVerbose(payload.Verbose) fmsg.SetVerbose(payload.Verbose)
// child does not need to see this // child does not need to see this
if err = os.Unsetenv(init0.Env); err != nil { if err = os.Unsetenv(EnvInit); err != nil {
fmsg.Printf("cannot unset %s: %v", init0.Env, err) fmsg.Println("cannot unset", EnvInit+":", err)
// not fatal // not fatal
} else { } else {
fmsg.VPrintln("received configuration") fmsg.VPrintln("received configuration")
} }
} }
// close config fd
if err := p.Close(); err != nil {
fmsg.Println("cannot close config fd:", err)
// not fatal
}
// die with parent // die with parent
if err := internal.PR_SET_PDEATHSIG__SIGKILL(); err != nil { if _, _, errno := syscall.RawSyscall(syscall.SYS_PRCTL, syscall.PR_SET_PDEATHSIG, uintptr(syscall.SIGKILL), 0); errno != 0 {
fmsg.Fatalf("prctl(PR_SET_PDEATHSIG, SIGKILL): %v", err) fmsg.Fatal("prctl(PR_SET_PDEATHSIG, SIGKILL):", errno.Error())
} }
cmd := exec.Command(payload.Argv0) cmd := exec.Command(payload.Argv0)
@ -103,13 +82,6 @@ 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()
// close setup pipe as setup is now complete
if err := setup.Close(); err != nil {
fmsg.Println("cannot close setup pipe:", err)
// not fatal
}
sig := make(chan os.Signal, 2) sig := make(chan os.Signal, 2)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
@ -150,7 +122,6 @@ func main() {
close(done) close(done)
}() }()
// closed after residualProcessTimeout has elapsed after initial process death
timeout := make(chan struct{}) timeout := make(chan struct{})
r := 2 r := 2
@ -158,13 +129,9 @@ func main() {
select { select {
case s := <-sig: case s := <-sig:
fmsg.VPrintln("received", s.String()) fmsg.VPrintln("received", s.String())
fmsg.Resume() // output could still be withheld at this point, so resume is called
fmsg.Exit(0) fmsg.Exit(0)
case w := <-info: case w := <-info:
if w.wpid == cmd.Process.Pid { if w.wpid == cmd.Process.Pid {
// initial process exited, output is most likely available again
fmsg.Resume()
switch { switch {
case w.wstatus.Exited(): case w.wstatus.Exited():
r = w.wstatus.ExitStatus() r = w.wstatus.ExitStatus()
@ -187,3 +154,21 @@ func main() {
} }
} }
} }
// Try runs init and stops execution if FORTIFY_INIT is set.
func Try() {
if os.Getpid() != 1 {
return
}
if args := flag.Args(); len(args) == 1 && args[0] == "init" {
if s, ok := os.LookupEnv(EnvInit); ok {
if fd, err := strconv.Atoi(s); err != nil {
fmsg.Fatalf("cannot parse %q: %v", s, err)
} else {
doInit(uintptr(fd))
}
panic("unreachable")
}
}
}

View File

@ -1,6 +1,6 @@
package init0 package init0
const Env = "FORTIFY_INIT" const EnvInit = "FORTIFY_INIT"
type Payload struct { type Payload struct {
// target full exec path // target full exec path

View File

@ -1,120 +0,0 @@
package linux
import (
"errors"
"io"
"io/fs"
"os"
"os/exec"
"os/user"
"strconv"
"sync"
"git.ophivana.moe/security/fortify/internal"
"git.ophivana.moe/security/fortify/internal/fmsg"
)
// Std implements System using the standard library.
type Std struct {
paths Paths
pathsOnce sync.Once
sdBooted bool
sdBootedOnce sync.Once
uidOnce sync.Once
uidCopy map[int]struct {
uid int
err error
}
uidMu sync.RWMutex
}
func (s *Std) Geteuid() int { return os.Geteuid() }
func (s *Std) LookupEnv(key string) (string, bool) { return os.LookupEnv(key) }
func (s *Std) TempDir() string { return os.TempDir() }
func (s *Std) LookPath(file string) (string, error) { return exec.LookPath(file) }
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) 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) Open(name string) (fs.File, error) { return os.Open(name) }
func (s *Std) Exit(code int) { fmsg.Exit(code) }
func (s *Std) Stdout() io.Writer { return os.Stdout }
const xdgRuntimeDir = "XDG_RUNTIME_DIR"
func (s *Std) Paths() Paths {
s.pathsOnce.Do(func() { CopyPaths(s, &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 {
s.sdBootedOnce.Do(func() { s.sdBooted = copySdBooted() })
return s.sdBooted
}
const systemdCheckPath = "/run/systemd/system"
func copySdBooted() bool {
if v, err := sdBooted(); err != nil {
fmsg.Println("cannot read systemd marker:", err)
return false
} else {
return v
}
}
func sdBooted() (bool, error) {
_, err := os.Stat(systemdCheckPath)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
err = nil
}
return false, err
}
return true, nil
}

View File

@ -1,12 +0,0 @@
package internal
import "path"
var (
Fsu = compPoison
Finit = compPoison
)
func Path(p string) (string, bool) {
return p, p != compPoison && p != "" && path.IsAbs(p)
}

View File

@ -1,20 +0,0 @@
package internal
import "syscall"
func PR_SET_DUMPABLE__SUID_DUMP_DISABLE() error {
// linux/sched/coredump.h
if _, _, errno := syscall.RawSyscall(syscall.SYS_PRCTL, syscall.PR_SET_DUMPABLE, 0, 0); errno != 0 {
return errno
}
return nil
}
func PR_SET_PDEATHSIG__SIGKILL() error {
if _, _, errno := syscall.AllThreadsSyscall(syscall.SYS_PRCTL, syscall.PR_SET_PDEATHSIG, uintptr(syscall.SIGKILL), 0); errno != 0 {
return errno
}
return nil
}

View File

@ -1,74 +1,50 @@
package main package shim
import ( import (
"encoding/gob" "encoding/gob"
"errors" "errors"
"flag"
"net" "net"
"os" "os"
"path" "path"
"strconv" "strconv"
"syscall" "syscall"
init0 "git.ophivana.moe/security/fortify/cmd/finit/ipc"
shim "git.ophivana.moe/security/fortify/cmd/fshim/ipc"
"git.ophivana.moe/security/fortify/helper" "git.ophivana.moe/security/fortify/helper"
"git.ophivana.moe/security/fortify/internal"
"git.ophivana.moe/security/fortify/internal/fmsg" "git.ophivana.moe/security/fortify/internal/fmsg"
init0 "git.ophivana.moe/security/fortify/internal/init"
) )
// everything beyond this point runs as unconstrained target user // everything beyond this point runs as target user
// proceed with caution! // proceed with caution!
func main() { func doShim(socket string) {
// sharing stdout with fortify
// USE WITH CAUTION
fmsg.SetPrefix("shim") fmsg.SetPrefix("shim")
// setting this prevents ptrace
if err := internal.PR_SET_DUMPABLE__SUID_DUMP_DISABLE(); err != nil {
fmsg.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err)
panic("unreachable")
}
// re-exec // re-exec
if len(os.Args) > 0 && (os.Args[0] != "fshim" || len(os.Args) != 1) && path.IsAbs(os.Args[0]) { if len(os.Args) > 0 && os.Args[0] != "fortify" && path.IsAbs(os.Args[0]) {
if err := syscall.Exec(os.Args[0], []string{"fshim"}, os.Environ()); err != nil { if err := syscall.Exec(os.Args[0], []string{"fortify", "shim"}, os.Environ()); err != nil {
fmsg.Println("cannot re-exec self:", err) fmsg.Println("cannot re-exec self:", err)
// continue anyway // continue anyway
} }
} }
// lookup socket path from environment
var socketPath string
if s, ok := os.LookupEnv(shim.Env); !ok {
fmsg.Fatal("FORTIFY_SHIM not set")
panic("unreachable")
} else {
socketPath = s
}
// check path to finit
var finitPath string
if p, ok := internal.Path(internal.Finit); !ok {
fmsg.Fatal("invalid finit path, this copy of fshim is not compiled correctly")
} else {
finitPath = p
}
// 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: socket, 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
} }
// decode payload gob stream // decode payload gob stream
var payload shim.Payload var payload 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 {
// sharing stdout with parent
// USE WITH CAUTION
fmsg.SetVerbose(payload.Verbose) fmsg.SetVerbose(payload.Verbose)
} }
@ -80,7 +56,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
} }
@ -98,14 +74,11 @@ func main() {
ic.Argv = payload.Argv ic.Argv = payload.Argv
if len(ic.Argv) > 0 { if len(ic.Argv) > 0 {
// looked up from $PATH by parent // looked up from $PATH by parent
ic.Argv0 = payload.Exec[1] ic.Argv0 = payload.Exec[2]
} 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,24 +101,23 @@ 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.EnvInit] = strconv.Itoa(3 + len(extraFiles))
extraFiles = append(extraFiles, r) extraFiles = append(extraFiles, r)
fmsg.VPrintln("transmitting config to init") fmsg.VPrintln("transmitting config to init")
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)
} }
}() }()
} }
helper.BubblewrapName = payload.Exec[0] // resolved bwrap path by parent helper.BubblewrapName = payload.Exec[1] // resolved bwrap path by parent
if b, err := helper.NewBwrap(conf, nil, finitPath, if b, err := helper.NewBwrap(conf, nil, payload.Exec[0], func(int, int) []string { return []string{"init"} }); err != nil {
func(int, int) []string { return make([]string, 0) }); err != nil { fmsg.Fatal("malformed sandbox config:", err)
fmsg.Fatalf("malformed sandbox config: %v", err)
} else { } else {
cmd := b.Unwrap() cmd := b.Unwrap()
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
@ -157,7 +129,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)
} }
@ -195,3 +167,13 @@ func receiveWLfd(conn *net.UnixConn) (int, error) {
return fds[0], nil return fds[0], nil
} }
} }
// Try runs shim and stops execution if FORTIFY_SHIM is set.
func Try() {
if args := flag.Args(); len(args) == 1 && args[0] == "shim" {
if s, ok := os.LookupEnv(EnvShim); ok {
doShim(s)
panic("unreachable")
}
}
}

View File

@ -5,21 +5,15 @@ import (
"net" "net"
"os" "os"
"os/exec" "os/exec"
"os/signal"
"strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
"syscall" "syscall"
"time" "time"
"git.ophivana.moe/security/fortify/acl" "git.ophivana.moe/security/fortify/acl"
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"
) )
const shimSetupTimeout = 5 * time.Second
// used by the parent process // used by the parent process
type Shim struct { type Shim struct {
@ -27,26 +21,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 *Wayland
// shim setup payload // shim setup payload
payload *shim0.Payload payload *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 *Wayland, payload *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 +65,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,65 +84,30 @@ 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(EnvShim+"="+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 conn := <-cf
select { if conn == nil {
case c := <-cf:
if c == nil {
return &startTime, fmsg.WrapErrorSuffix(*s.abortErr.Load(), "cannot accept call on setup socket:") return &startTime, fmsg.WrapErrorSuffix(*s.abortErr.Load(), "cannot accept call on setup socket:")
} else {
conn = c
}
case <-time.After(shimSetupTimeout):
err := fmsg.WrapError(errors.New("timed out waiting for shim"),
"timed out waiting for shim to connect")
s.AbortWait(err)
return &startTime, err
} }
// authenticate against called provided uid and shim pid // authenticate against called provided uid and shim pid
@ -164,7 +119,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")
@ -174,7 +129,7 @@ func (s *Shim) Start() (*time.Time, error) {
// serve payload and wayland fd if enabled // serve payload and wayland fd if enabled
// this also closes the connection // this also closes the connection
err := s.payload.Serve(conn, s.wl) err := s.payload.serve(conn, s.wl)
if err == nil { if err == nil {
killShim = func() {} killShim = func() {}
} }
@ -203,7 +158,6 @@ func (s *Shim) serve() (chan *net.UnixConn, func(), error) {
} }
go func() { go func() {
cfWg := new(sync.WaitGroup)
for { for {
select { select {
case err = <-s.abort: case err = <-s.abort:
@ -214,24 +168,15 @@ func (s *Shim) serve() (chan *net.UnixConn, func(), error) {
fmsg.Println("cannot close setup socket:", err) fmsg.Println("cannot close setup socket:", err)
} }
close(s.abort) close(s.abort)
go func() {
cfWg.Wait()
close(cf) close(cf)
}()
return return
case <-accept: case <-accept:
cfWg.Add(1)
go func() {
defer cfWg.Done()
if conn, err0 := l.AcceptUnix(); err0 != nil { if conn, err0 := l.AcceptUnix(); err0 != nil {
// breaks loop s.Abort(err0) // does not block, breaks loop
s.Abort(err0) cf <- nil // receiver sees nil value and loads err0 stored during abort
// receiver sees nil value and loads err0 stored during abort
cf <- nil
} else { } else {
cf <- conn cf <- conn
} }
}()
} }
} }
}() }()

View File

@ -1,4 +1,4 @@
package shim0 package shim
import ( import (
"encoding/gob" "encoding/gob"
@ -9,13 +9,13 @@ import (
"git.ophivana.moe/security/fortify/internal/fmsg" "git.ophivana.moe/security/fortify/internal/fmsg"
) )
const Env = "FORTIFY_SHIM" const EnvShim = "FORTIFY_SHIM"
type Payload struct { type Payload struct {
// child full argv // child full argv
Argv []string Argv []string
// bwrap, target full exec path // fortify, bwrap, target full exec path
Exec [2]string Exec [3]string
// bwrap config // bwrap config
Bwrap *bwrap.Config Bwrap *bwrap.Config
// whether to pass wayland fd // whether to pass wayland fd
@ -25,7 +25,7 @@ type Payload struct {
Verbose bool Verbose bool
} }
func (p *Payload) Serve(conn *net.UnixConn, wl *Wayland) error { func (p *Payload) serve(conn *net.UnixConn, wl *Wayland) error {
if err := gob.NewEncoder(conn).Encode(*p); err != nil { if err := gob.NewEncoder(conn).Encode(*p); err != nil {
return fmsg.WrapErrorSuffix(err, return fmsg.WrapErrorSuffix(err,
"cannot stream shim payload:") "cannot stream shim payload:")

View File

@ -1,4 +1,4 @@
package shim0 package shim
import ( import (
"fmt" "fmt"

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

View File

@ -1,11 +1,14 @@
package linux package internal
import ( import (
"io" "errors"
"io/fs" "io/fs"
"os"
"os/exec"
"os/user" "os/user"
"path" "path"
"strconv" "strconv"
"sync"
"git.ophivana.moe/security/fortify/internal/fmsg" "git.ophivana.moe/security/fortify/internal/fmsg"
) )
@ -22,8 +25,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 +35,9 @@ 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
// 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
} }
@ -70,3 +69,58 @@ func CopyPaths(os System, v *Paths) {
fmsg.VPrintf("runtime directory at %q", v.RunDirPath) fmsg.VPrintf("runtime directory at %q", v.RunDirPath)
} }
// Std implements System using the standard library.
type Std struct {
paths Paths
pathsOnce sync.Once
sdBooted bool
sdBootedOnce sync.Once
}
func (s *Std) Geteuid() int { return os.Geteuid() }
func (s *Std) LookupEnv(key string) (string, bool) { return os.LookupEnv(key) }
func (s *Std) TempDir() string { return os.TempDir() }
func (s *Std) LookPath(file string) (string, error) { return exec.LookPath(file) }
func (s *Std) Executable() (string, error) { return os.Executable() }
func (s *Std) Lookup(username string) (*user.User, error) { return user.Lookup(username) }
func (s *Std) 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) Open(name string) (fs.File, error) { return os.Open(name) }
func (s *Std) Exit(code int) { fmsg.Exit(code) }
const xdgRuntimeDir = "XDG_RUNTIME_DIR"
func (s *Std) Paths() Paths {
s.pathsOnce.Do(func() { CopyPaths(s, &s.paths) })
return s.paths
}
func (s *Std) SdBooted() bool {
s.sdBootedOnce.Do(func() { s.sdBooted = copySdBooted() })
return s.sdBooted
}
const systemdCheckPath = "/run/systemd/system"
func copySdBooted() bool {
if v, err := sdBooted(); err != nil {
fmsg.Println("cannot read systemd marker:", err)
return false
} else {
return v
}
}
func sdBooted() (bool, error) {
_, err := os.Stat(systemdCheckPath)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
err = nil
}
return false, err
}
return true, nil
}

View File

@ -1,11 +1,8 @@
package system package system
import ( import (
"bytes"
"errors" "errors"
"os" "os"
"strings"
"sync"
"git.ophivana.moe/security/fortify/dbus" "git.ophivana.moe/security/fortify/dbus"
"git.ophivana.moe/security/fortify/internal/fmsg" "git.ophivana.moe/security/fortify/internal/fmsg"
@ -16,14 +13,14 @@ var (
) )
func (sys *I) MustProxyDBus(sessionPath string, session *dbus.Config, systemPath string, system *dbus.Config) *I { func (sys *I) MustProxyDBus(sessionPath string, session *dbus.Config, systemPath string, system *dbus.Config) *I {
if _, err := sys.ProxyDBus(session, system, sessionPath, systemPath); err != nil { if err := sys.ProxyDBus(session, system, sessionPath, systemPath); err != nil {
panic(err.Error()) panic(err.Error())
} else { } else {
return sys return sys
} }
} }
func (sys *I) ProxyDBus(session, system *dbus.Config, sessionPath, systemPath string) (func(f func(msgbuf []string)), error) { func (sys *I) ProxyDBus(session, system *dbus.Config, sessionPath, systemPath string) error {
d := new(DBus) d := new(DBus)
// used by waiting goroutine to notify process exit // used by waiting goroutine to notify process exit
@ -31,7 +28,7 @@ func (sys *I) ProxyDBus(session, system *dbus.Config, sessionPath, systemPath st
// session bus is mandatory // session bus is mandatory
if session == nil { if session == nil {
return nil, fmsg.WrapError(ErrDBusConfig, return fmsg.WrapError(ErrDBusConfig,
"attempted to seal message bus proxy without session bus config") "attempted to seal message bus proxy without session bus config")
} }
@ -64,15 +61,13 @@ func (sys *I) ProxyDBus(session, system *dbus.Config, sessionPath, systemPath st
sys.ops = append(sys.ops, d) sys.ops = append(sys.ops, d)
// seal dbus proxy // seal dbus proxy
d.out = &scanToFmsg{msg: new(strings.Builder)} return fmsg.WrapErrorSuffix(d.proxy.Seal(session, system),
return d.out.F, fmsg.WrapErrorSuffix(d.proxy.Seal(session, system),
"cannot seal message bus proxy:") "cannot seal message bus proxy:")
} }
type DBus struct { type DBus struct {
proxy *dbus.Proxy proxy *dbus.Proxy
out *scanToFmsg
// whether system bus proxy is enabled // whether system bus proxy is enabled
system bool system bool
// notification from goroutine waiting for dbus.Proxy // notification from goroutine waiting for dbus.Proxy
@ -93,7 +88,7 @@ func (d *DBus) apply(_ *I) error {
ready := make(chan error, 1) ready := make(chan error, 1)
// background dbus proxy start // background dbus proxy start
if err := d.proxy.Start(ready, d.out, true); err != nil { if err := d.proxy.Start(ready, os.Stderr, true); err != nil {
return fmsg.WrapErrorSuffix(err, return fmsg.WrapErrorSuffix(err,
"cannot start message bus proxy:") "cannot start message bus proxy:")
} }
@ -169,34 +164,3 @@ func (d *DBus) Path() string {
func (d *DBus) String() string { func (d *DBus) String() string {
return d.proxy.String() return d.proxy.String()
} }
type scanToFmsg struct {
msg *strings.Builder
msgbuf []string
mu sync.RWMutex
}
func (s *scanToFmsg) Write(p []byte) (n int, err error) {
s.mu.Lock()
defer s.mu.Unlock()
return s.write(p, 0)
}
func (s *scanToFmsg) write(p []byte, a int) (int, error) {
if i := bytes.IndexByte(p, '\n'); i == -1 {
n, _ := s.msg.Write(p)
return a + n, nil
} else {
n, _ := s.msg.Write(p[:i])
s.msgbuf = append(s.msgbuf, s.msg.String())
s.msg.Reset()
return s.write(p[i+1:], a+n+1)
}
}
func (s *scanToFmsg) F(f func(msgbuf []string)) {
s.mu.RLock()
f(s.msgbuf)
s.mu.RUnlock()
}

View File

@ -1,4 +1,3 @@
// Package ldd retrieves linker information by invoking ldd from glibc or musl and parsing its output.
package ldd package ldd
import ( import (

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)
}
}

280
main.go
View File

@ -1,289 +1,62 @@
package main package main
import ( import (
_ "embed"
"encoding/json"
"flag" "flag"
"fmt" "syscall"
"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" init0 "git.ophivana.moe/security/fortify/internal/init"
"git.ophivana.moe/security/fortify/internal/state" "git.ophivana.moe/security/fortify/internal/shim"
"git.ophivana.moe/security/fortify/internal/system"
) )
var ( var (
flagVerbose bool flagVerbose bool
//go:embed LICENSE
license string
) )
func init() { func init() {
flag.BoolVar(&flagVerbose, "v", false, "Verbose output") flag.BoolVar(&flagVerbose, "v", false, "Verbose output")
} }
var os = new(linux.Std) var os = new(internal.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 { // linux/sched/coredump.h
fmsg.Printf("cannot set SUID_DUMP_DISABLE: %s", err) if _, _, errno := syscall.RawSyscall(syscall.SYS_PRCTL, syscall.PR_SET_DUMPABLE, 0, 0); errno != 0 {
// not fatal: this program runs as the privileged user fmsg.Printf("fortify: cannot set SUID_DUMP_DISABLE: %s", errno.Error())
} }
flag.Parse()
fmsg.SetVerbose(flagVerbose)
if os.SdBooted() {
fmsg.VPrintln("system booted with systemd as init system")
}
// shim/init early exit
init0.Try()
shim.Try()
// 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 // invoke app
runApp(config)
case "run": // run app in permissive defaults usage pattern
set := flag.NewFlagSet("run", flag.ExitOnError)
var (
dbusConfigSession string
dbusConfigSystem string
dbusID string
mpris bool
dbusVerbose bool
aid int
groups gl
homeDir string
userName string
enablements [system.ELen]bool
)
set.StringVar(&dbusConfigSession, "dbus-config", "builtin", "Path to D-Bus proxy config file, or \"builtin\" for defaults")
set.StringVar(&dbusConfigSystem, "dbus-system", "nil", "Path to system D-Bus proxy config file, or \"nil\" to disable")
set.StringVar(&dbusID, "dbus-id", "", "D-Bus ID of application, leave empty to disable own paths, has no effect if custom config is available")
set.BoolVar(&mpris, "mpris", false, "Allow owning MPRIS D-Bus path, has no effect if custom config is available")
set.BoolVar(&dbusVerbose, "dbus-log", false, "Force logging in the D-Bus proxy")
set.IntVar(&aid, "a", 0, "Fortify application ID")
set.Var(&groups, "g", "Groups inherited by the app process")
set.StringVar(&homeDir, "d", "os", "Application home directory")
set.StringVar(&userName, "u", "chronos", "Passwd name within sandbox")
set.BoolVar(&enablements[system.EWayland], "wayland", false, "Share Wayland socket")
set.BoolVar(&enablements[system.EX11], "X", false, "Share X11 socket and allow connection")
set.BoolVar(&enablements[system.EDBus], "dbus", false, "Proxy D-Bus connection")
set.BoolVar(&enablements[system.EPulse], "pulse", false, "Share PulseAudio socket and cookie")
// Ignore errors; set is set for ExitOnError.
_ = set.Parse(args[1:])
// initialise config from flags
config := &app.Config{
ID: dbusID,
Command: set.Args(),
}
if aid < 0 || aid > 9999 {
fmsg.Fatalf("aid %d out of range", aid)
panic("unreachable")
}
// resolve home/username from os when flag is unset
var (
passwd *user.User
passwdOnce sync.Once
passwdFunc = func() {
var us string
if uid, err := os.Uid(aid); err != nil {
fmsg.Fatalf("cannot obtain uid from fsu: %v", err)
} else {
us = strconv.Itoa(uid)
}
if u, err := user.LookupId(us); err != nil {
fmsg.VPrintf("cannot look up uid %s", us)
passwd = &user.User{
Uid: us,
Gid: us,
Username: "chronos",
Name: "Fortify",
HomeDir: "/var/empty",
}
} else {
passwd = u
}
}
)
if homeDir == "os" {
passwdOnce.Do(passwdFunc)
homeDir = passwd.HomeDir
}
if userName == "chronos" {
passwdOnce.Do(passwdFunc)
userName = passwd.Username
}
config.Confinement.AppID = aid
config.Confinement.Groups = groups
config.Confinement.Outer = homeDir
config.Confinement.Username = userName
// enablements from flags
for i := system.Enablement(0); i < system.Enablement(system.ELen); i++ {
if enablements[i] {
config.Confinement.Enablements.Set(i)
}
}
// parse D-Bus config file from flags if applicable
if enablements[system.EDBus] {
if dbusConfigSession == "builtin" {
config.Confinement.SessionBus = dbus.NewConfig(dbusID, true, mpris)
} else {
if c, err := dbus.NewConfigFromFile(dbusConfigSession); err != nil {
fmsg.Fatalf("cannot load session bus proxy config from %q: %s", dbusConfigSession, err)
} else {
config.Confinement.SessionBus = c
}
}
// system bus proxy is optional
if dbusConfigSystem != "nil" {
if c, err := dbus.NewConfigFromFile(dbusConfigSystem); err != nil {
fmsg.Fatalf("cannot load system bus proxy config from %q: %s", dbusConfigSystem, err)
} else {
config.Confinement.SystemBus = c
}
}
// override log from configuration
if dbusVerbose {
config.Confinement.SessionBus.Log = true
config.Confinement.SystemBus.Log = true
}
}
// invoke app
runApp(config)
default:
fmsg.Fatalf("%q is not a valid command", args[0])
}
panic("unreachable")
}
func runApp(config *app.Config) {
if os.SdBooted() {
fmsg.VPrintln("system booted with systemd as init system")
}
a, err := app.New(os) a, err := app.New(os)
if err != nil { if err != nil {
fmsg.Fatalf("cannot create app: %s\n", err) fmsg.Fatalf("cannot create app: %s\n", err)
} else if err = a.Seal(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 +75,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")
} }

436
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";
config = mkIf cfg.enable { target = mkOption {
security.wrappers.fsu = { default = { };
source = "${cfg.package}/libexec/fsu"; type =
setuid = true; let
owner = "root"; inherit (types)
setgid = true; str
group = "root"; enum
}; bool
package
environment.etc = { anything
fsurc = { submodule
mode = "0400"; listOf
text = foldlAttrs ( attrsOf
acc: username: fid: nullOr
"${toString config.users.users.${username}.uid} ${toString fid}\n" + acc ;
) "" cfg.users; in
}; attrsOf (submodule {
options = {
userdb.source = pkgs.runCommand "fortify-userdb" { } '' packages = mkOption {
${cfg.package}/libexec/fuserdb -o $out ${ type = listOf package;
foldlAttrs ( default = [ ];
acc: username: fid: description = ''
acc + " ${username}:${toString fid}" List of extra packages to install via home-manager.
) "-s /run/current-system/sw/bin/nologin -d ${cfg.stateDir}" cfg.users
}
''; '';
}; };
services.userdbd.enable = mkDefault true; launchers = mkOption {
type = attrsOf (submodule {
home-manager = options = {
let command = mkOption {
privPackages = mapAttrs (username: fid: { type = nullOr str;
home.packages = default = null;
let description = ''
# aid 0 is reserved Command to run as the target user.
wrappers = imap1 ( Setting this to null will default command to wrapper name.
aid: app: '';
let
extendDBusDefault = id: ext: {
filter = true;
talk = [ "org.freedesktop.Notifications" ] ++ ext.talk;
own =
(optionals (app.id != null) [
"${id}.*"
"org.mpris.MediaPlayer2.${id}.*"
])
++ ext.own;
inherit (ext) call broadcast;
}; };
dbusConfig =
let dbus = {
default = { config = mkOption {
talk = [ ]; type = nullOr anything;
own = [ ]; default = null;
call = { }; description = ''
broadcast = { }; D-Bus custom configuration.
Setting this to null will enable built-in defaults.
'';
}; };
in
{ configSystem = mkOption {
session_bus = type = nullOr anything;
if app.dbus.session != null then default = null;
(app.dbus.session (extendDBusDefault app.id)) description = ''
else D-Bus system bus custom configuration.
(extendDBusDefault app.id default); Setting this to null will disable the system bus proxy.
system_bus = app.dbus.system; '';
}; };
command = if app.command == null then app.name else app.command;
script = if app.script == null then ("exec " + command + " $@") else app.script; id = mkOption {
enablements = type = nullOr str;
with app.capability; default = null;
(if wayland then 1 else 0) description = ''
+ (if x11 then 2 else 0) D-Bus application id.
+ (if dbus then 4 else 0) Setting this to null will disable own path in defaults.
+ (if pulse then 8 else 0); Has no effect if custom configuration is set.
conf = { '';
inherit (app) id; };
command = [
(pkgs.writeScript "${app.name}-start" '' mpris = mkOption {
#!${pkgs.zsh}${pkgs.zsh.shellPath} type = bool;
${script} 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"
]; ];
confinement = { default = "systemd";
app_id = aid; description = ''
inherit (app) groups; Launch method for the sandboxed program.
username = "u${toString fid}_a${toString aid}"; '';
home = "${cfg.stateDir}/${toString fid}/${toString aid}";
sandbox = {
inherit (app)
userns
net
dev
env
;
map_real_uid = app.mapRealUid;
filesystem =
[
{ src = "/bin"; }
{ src = "/usr/bin"; }
{ src = "/nix/store"; }
{ src = "/run/current-system"; }
{
src = "/sys/block";
require = false;
}
{
src = "/sys/bus";
require = false;
}
{
src = "/sys/class";
require = false;
}
{
src = "/sys/dev";
require = false;
}
{
src = "/sys/devices";
require = false;
}
]
++ optionals app.nix [
{ src = "/nix/var"; }
{ src = "/var/db/nix-channels"; }
]
++ optionals (if app.gpu != null then app.gpu else app.capability.wayland || app.capability.x11) [
{ src = "/run/opengl-driver"; }
{
src = "/dev/dri";
dev = true;
}
]
++ app.extraPaths;
auto_etc = true;
override = [ "/var/run/nscd" ];
};
inherit enablements;
inherit (dbusConfig) session_bus system_bus;
}; };
}; };
in });
pkgs.writeShellScriptBin app.name '' default = { };
exec fortify app ${pkgs.writeText "fortify-${app.name}.json" (builtins.toJSON conf)} $@ };
''
) cfg.apps; persistence = mkOption {
in type = submodule {
foldr ( options = {
app: acc: 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 {
environment.persistence.${cfg.stateDir}.users = mapAttrs (_: target: target.persistence) cfg.target;
home-manager.users =
mapAttrs (_: target: target.extraConfig // { home.packages = target.packages; }) cfg.target
// {
${cfg.user}.home.packages =
let let
pkg = if app.share != null then app.share else pkgs.${app.name}; wrap =
copy = source: "[ -d '${source}' ] && cp -Lrv '${source}' $out/share || true"; user: launchers:
mapAttrsToList (
name: launcher:
with launcher.capability;
let
command = if launcher.command == null then name else launcher.command;
dbusConfig =
if launcher.dbus.config != null then
pkgs.writeText "${name}-dbus.json" (builtins.toJSON launcher.dbus.config)
else
null;
dbusSystem =
if launcher.dbus.configSystem != null then
pkgs.writeText "${name}-dbus-system.json" (builtins.toJSON launcher.dbus.configSystem)
else
null;
capArgs =
(if wayland then " --wayland" else "")
+ (if x11 then " -X" else "")
+ (if dbus then " --dbus" else "")
+ (if pulse then " --pulse" else "")
+ (if launcher.dbus.mpris then " --mpris" else "")
+ (if launcher.dbus.id != null then " --dbus-id ${launcher.dbus.id}" else "")
+ (if dbusConfig != null then " --dbus-config ${dbusConfig}" else "")
+ (if dbusSystem != null then " --dbus-system ${dbusSystem}" else "");
in in
optional (app.capability.wayland || app.capability.x11) ( pkgs.writeShellScriptBin name (
pkgs.runCommand "${app.name}-share" { } '' if launcher.method == "simple" then
mkdir -p $out/share ''
${copy "${pkg}/share/applications"} exec sudo -u ${user} -i ${command} $@
${copy "${pkg}/share/pixmaps"} ''
${copy "${pkg}/share/icons"} else
${copy "${pkg}/share/man"} ''
exec fortify${capArgs} --method ${launcher.method} -u ${user} $SHELL -c "exec ${command} $@"
substituteInPlace $out/share/applications/* \
--replace-warn '${pkg}/bin/' "" \
--replace-warn '${pkg}/libexec/' ""
'' ''
) )
++ acc ) launchers;
) (wrappers ++ [ cfg.package ]) cfg.apps;
}) cfg.users;
in in
{ foldlAttrs (
useUserPackages = false; # prevent users.users entries from being added acc: user: target:
acc
users = foldlAttrs ( ++ (foldlAttrs (
acc: _: fid: shares: name: launcher:
mergeAttrsList ( let
# aid 0 is reserved pkg = if launcher.share != null then launcher.share else pkgs.${name};
imap1 (aid: app: { link = source: "[ -d '${source}' ] && ln -sv '${source}' $out/share || true";
"u${toString fid}_a${toString aid}" = app.extraConfig // { in
home.packages = app.packages; shares
}; ++
}) cfg.apps optional (launcher.method != "simple" && (launcher.capability.wayland || launcher.capability.x11))
(
pkgs.runCommand "${name}-share" { } ''
mkdir -p $out/share
${link "${pkg}/share/applications"}
${link "${pkg}/share/icons"}
${link "${pkg}/share/man"}
''
) )
// acc ) (wrap user target.launchers) target.launchers)
) privPackages cfg.users; ) [ cfg.package ] cfg.target;
}; };
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,34 +10,19 @@
buildGoModule rec { buildGoModule rec {
pname = "fortify"; pname = "fortify";
version = "0.2.1"; version = "0.0.10";
src = ./.; src = ./.;
vendorHash = null; vendorHash = null;
ldflags = ldflags = [
lib.attrsets.foldlAttrs
(
ldflags: name: value:
ldflags
++ [
"-X"
"git.ophivana.moe/security/fortify/internal.${name}=${value}"
]
)
[
"-s" "-s"
"-w" "-w"
"-X" "-X"
"main.Fmain=${placeholder "out"}/libexec/fortify" "main.Version=v${version}"
"-X" "-X"
"main.Fshim=${placeholder "out"}/libexec/fshim" "main.FortifyPath=${placeholder "out"}/bin/.fortify-wrapped"
] ];
{
Version = "v${version}";
Fsu = "/run/wrappers/bin/fsu";
Finit = "${placeholder "out"}/libexec/finit";
};
buildInputs = [ buildInputs = [
acl acl
@ -47,15 +32,13 @@ buildGoModule rec {
nativeBuildInputs = [ makeBinaryWrapper ]; nativeBuildInputs = [ makeBinaryWrapper ];
postInstall = '' postInstall = ''
mkdir "$out/libexec" wrapProgram $out/bin/${pname} --prefix PATH : ${
mv "$out"/bin/* "$out/libexec/"
makeBinaryWrapper "$out/libexec/fortify" "$out/bin/fortify" \
--inherit-argv0 --prefix PATH : ${
lib.makeBinPath [ lib.makeBinPath [
bubblewrap bubblewrap
xdg-dbus-proxy xdg-dbus-proxy
] ]
} }
mv $out/bin/fsu $out/bin/.fsu
''; '';
} }

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)
}
}

23
version.go Normal file
View File

@ -0,0 +1,23 @@
package main
import (
"flag"
"fmt"
)
var (
Version = "impure"
printVersion bool
)
func init() {
flag.BoolVar(&printVersion, "V", false, "Print version")
}
func tryVersion() {
if printVersion {
fmt.Println(Version)
os.Exit(0)
}
}

View File

@ -1,4 +1,3 @@
// Package xcb implements X11 ChangeHosts via libxcb.
package xcb package xcb
//#include <stdlib.h> //#include <stdlib.h>