Files
privatae-bastion/client-go/freeagent_drop_unix.go
T
Privatae 8822d5df85
ci / personal-data (push) Successful in 5s
ci / test (push) Successful in 3m51s
ci / fuzz (push) Successful in 2m55s
bastion 0.55.3
2026-10-25 23:16:45 +00:00

193 lines
6.6 KiB
Go

//go:build !windows
package main
import (
"context"
"errors"
"fmt"
"os"
"os/exec"
"os/user"
"path/filepath"
"strconv"
"syscall"
)
func faDropSupported() bool { return true }
// faRootStateBase: root-owned 0755, so every dropped child can traverse to its
// own 0700 leaf.
func faRootStateBase() string { return "/var/lib/privatae/freeagent" }
// faHandsOverByChown: on unix every file root writes for the child is handed
// over by changing its owner.
const faHandsOverByChown = true
// faResolveDropOwner is the folder's owner on a root install, refused for root /
// nobody / unknown; Stat, not Lstat, so a symlinked root resolves to its
// target's owner. HOME is the owner's home when that is a directory, else the
// (owner-owned) store state dir — never `/`, which is wrong for a Python child
// that writes ~/.cache.
func faResolveDropOwner(root, stateDir string) (*faOwner, error) {
uid, gid, err := faPathOwner(root)
if err != nil {
return nil, fmt.Errorf("cannot read the folder's owner: %v", err)
}
if uid == 0 {
return nil, errors.New("refusing to run the agent as root: the granted folder is owned by root — grant a folder owned by the user who should read it")
}
if uid == 65534 {
return nil, errors.New("refusing to run the agent as nobody: the granted folder reports owner 65534 (nobody, or an NFS export with root_squash) — a shared account would hold this store's vault")
}
u, err := user.LookupId(strconv.FormatUint(uint64(uid), 10))
if err != nil {
return nil, fmt.Errorf("refusing to run the agent: the granted folder's owner (uid %d) has no account on this machine", uid)
}
if u.Username == "nobody" {
return nil, errors.New("refusing to run the agent as nobody: a shared account would hold this store's vault")
}
if g, err := strconv.ParseUint(u.Gid, 10, 32); err == nil {
gid = uint32(g) // the account's primary group, not the folder's group
}
// The owner must be able to REACH the folder: a user-owned leaf under a
// root-only parent (/srv/data/alice with /srv/data 0700) would otherwise
// start two children that exit-loop forever with the card reading
// "starting". Checked as the dropped credential will see it: owner, or
// group with g+x, or o+x, on every ancestor.
if resolved, err := filepath.EvalSymlinks(root); err == nil {
for d := filepath.Dir(resolved); d != "/" && d != "."; d = filepath.Dir(d) {
if why := faTraversable(d, uid, gid); why != "" {
return nil, fmt.Errorf("refusing to run the agent as %s: it cannot reach the granted folder — %s", u.Username, why)
}
}
}
home := u.HomeDir
if st, err := os.Stat(home); home == "" || err != nil || !st.IsDir() {
home = stateDir
}
return &faOwner{drop: true, uid: uid, gid: gid, name: u.Username, home: home}, nil
}
// faTraversable: can uid/gid pass through directory d? "" = yes.
func faTraversable(d string, uid, gid uint32) string {
st, err := os.Stat(d)
if err != nil {
return d + ": " + err.Error()
}
duid, dgid, err := faPathOwner(d)
if err != nil {
return d + ": " + err.Error()
}
m := st.Mode().Perm()
switch {
case duid == uid && m&0o100 != 0, dgid == gid && m&0o010 != 0, m&0o001 != 0:
return ""
}
return fmt.Sprintf("%s is %04o, owner uid %d", d, m, duid)
}
// faHandPath hands the store's state leaf to the owner. Lchown: a link is
// re-owned as a link, never followed.
func faHandPath(o *faOwner, path string) error { return faChown(path, o.uid, o.gid) }
// faCheckOwnedRoot: the directory root is about to write into belongs to the
// owner. A directory the child swapped for one of root's would fail this.
func faCheckOwnedRoot(r *os.Root, dir string, o *faOwner) error {
st, err := r.Stat(".")
if err != nil {
return err
}
uid, _, ok := faInfoOwner(st)
if !ok || uid != o.uid {
return fmt.Errorf("%s is not owned by %s (uid %d) — refusing to write into it", dir, o.name, uid)
}
return nil
}
// faSharePub / faShareRuntime: on unix the public cert directory and the
// runtime are made readable by their modes (faStoreDirs, faEnsurePyz).
func faSharePub(string, *faOwner) error { return nil }
func faShareRuntime(string) error { return nil }
// faDropEnv is what a dropped child is told about who it is, and which of the
// daemon's variables it may keep. It must not inherit root's HOME (/root —
// wrong and unwritable after the drop) or root's XDG config dir.
func faDropEnv(o *faOwner, stateDir string) (set []string, passthrough []string) {
return []string{"HOME=" + o.home, "USER=" + o.name, "LOGNAME=" + o.name},
[]string{"PATH", "LANG", "LC_ALL", "TMPDIR", "TEMP", "TMP"}
}
// faStartChild starts the child. On unix the drop is the credential in
// cmd.SysProcAttr, so this is an ordinary start.
func faStartChild(_ context.Context, cmd *exec.Cmd, _ *faOwner) (faChild, error) {
if err := cmd.Start(); err != nil {
return nil, err
}
return execChild{cmd}, nil
}
// faPathOwner: uid/gid of the file at path, following symlinks.
func faPathOwner(path string) (uint32, uint32, error) {
st, err := os.Stat(path)
if err != nil {
return 0, 0, err
}
sys, ok := st.Sys().(*syscall.Stat_t)
if !ok {
return 0, 0, errors.New("no ownership information for this path")
}
return sys.Uid, sys.Gid, nil
}
func faChown(path string, uid, gid uint32) error {
return os.Lchown(path, int(uid), int(gid))
}
// sysProcAttr is the credential the child is started with. Groups nil +
// NoSetGroups false = setgroups() with an EMPTY list — the same as
// session_unix.go:71, and for the same measured reason: skipping setgroups
// leaves the child in root's supplementary groups.
func (o *faOwner) sysProcAttr() *syscall.SysProcAttr {
if o == nil || !o.drop {
return nil
}
return &syscall.SysProcAttr{Credential: &syscall.Credential{
Uid: o.uid, Gid: o.gid, Groups: nil, NoSetGroups: false}}
}
// faFdSane: the open log is a plain file with one link. A hard link to a file
// root owns would pass the os.Root walk (it is inside the directory) and still
// hand the child root's append on the target.
func faFdSane(f *os.File) error {
st, err := f.Stat()
if err != nil {
return err
}
if !st.Mode().IsRegular() {
return errors.New("not a regular file")
}
if sys, ok := st.Sys().(*syscall.Stat_t); ok && sys.Nlink != 1 {
return fmt.Errorf("has %d links", sys.Nlink)
}
return nil
}
// faInfoOwner: uid/gid from a FileInfo obtained through an os.Root handle.
func faInfoOwner(st os.FileInfo) (uint32, uint32, bool) {
sys, ok := st.Sys().(*syscall.Stat_t)
if !ok {
return 0, 0, false
}
return sys.Uid, sys.Gid, true
}
// faInfoLinks: the link count from a FileInfo.
func faInfoLinks(st os.FileInfo) (uint64, bool) {
sys, ok := st.Sys().(*syscall.Stat_t)
if !ok {
return 0, false
}
return uint64(sys.Nlink), true
}