Files
mesh-host/internal/system/identities.go
T
jschoubben c7551162d6 Give no account a home, keep old data's sub-paths for the warrant, and never hand out setgid (hq ADR 0328 review)
usermod --gid regroups every file under an account's home, and accounts were made
with / as home: they now have /nonexistent, and an older account's home is moved
there (without its files) before its group changes. A sub-path asked for later that
holds data not in the group waits for the operator as the root would. The take act
gives the group to hardlinked files an account owns (the *arr apps' downloads) and
leaves root's; a file's setuid or setgid bit the regroup takes is never put back, so
no setgid file hands out the library's group; each is counted. An apply no longer
waits for a running take walk.
2026-10-12 22:18:16 +02:00

158 lines
6.4 KiB
Go

package system
import (
"context"
"fmt"
"strconv"
"strings"
)
// A module's identity as a system account (novox/hq ADR 0328 §3, §4): an account and a group with exactly the numbers
// the controller allocated, no login shell, no home and no password. Optional beside System: a system that has
// none (android) makes no module identity, and the engine says so.
// CreateSystemGroup makes a system group with gid.
func (arch) CreateSystemGroup(ctx context.Context, run Runner, name string, gid int) error {
if _, err := run(ctx, "groupadd", "--system", "--gid", strconv.Itoa(gid), name); err != nil {
return fmt.Errorf("cannot create the group %q: %w", name, err)
}
return nil
}
// NoHome is the home field of every module account: a path that is not there, so nothing that follows an account's
// home — usermod regrouping every file under it, for one — ever walks a real tree. Never `/`: `usermod --gid` on
// an account whose home is `/` regroups every file it owns on the machine.
const NoHome = "/nonexistent"
// CreateSystemAccount makes a system account with uid and the group gid as its own: no home made, NoHome as the
// home field, nologin as its shell, and the password useradd leaves locked.
func (arch) CreateSystemAccount(ctx context.Context, run Runner, name string, uid, gid int, groups []string) error {
args := []string{"--system", "--uid", strconv.Itoa(uid), "--gid", strconv.Itoa(gid)}
if len(groups) > 0 {
args = append(args, "--groups", strings.Join(groups, ","))
}
args = append(args, "--no-create-home", "--home-dir", NoHome, "--shell", "/usr/bin/nologin", name)
if _, err := run(ctx, "useradd", args...); err != nil {
return fmt.Errorf("cannot create the account %q: %w", name, err)
}
return nil
}
// ModifyAccount gives an account the mesh made its primary group gid and exactly groups as its added groups.
//
// **Its home is moved to NoHome first, never its files.** `usermod --gid` regroups every file under the account's
// home that the account owns; an account made with `/` as its home (an earlier engine's) would have the whole
// machine walked as root. So an account whose home is not NoHome is given NoHome first, without -m: no file moves.
func (arch) ModifyAccount(ctx context.Context, run Runner, name string, gid int, groups []string) error {
passwd, err := run(ctx, "getent", "passwd", name)
if err != nil {
return fmt.Errorf("cannot read the account %q: %w", name, err)
}
fields := strings.Split(strings.TrimSpace(passwd), ":")
if len(fields) < 6 {
return fmt.Errorf("the user database gave %q for %q, which is not a passwd entry", strings.TrimSpace(passwd),
name)
}
if fields[5] != NoHome {
if _, err := run(ctx, "usermod", "--home", NoHome, name); err != nil {
return fmt.Errorf("cannot give the account %q no home before its groups: %w", name, err)
}
}
if _, err := run(ctx, "usermod", "--gid", strconv.Itoa(gid), "--groups", strings.Join(groups, ","),
name); err != nil {
return fmt.Errorf("cannot give the account %q its groups: %w", name, err)
}
return nil
}
// RemoveAccount removes an account, leaving every file it owns where it is.
func (arch) RemoveAccount(ctx context.Context, run Runner, name string) error {
if _, err := run(ctx, "userdel", name); err != nil {
return fmt.Errorf("cannot remove the account %q: %w", name, err)
}
return nil
}
// RemoveGroup removes a group.
func (arch) RemoveGroup(ctx context.Context, run Runner, name string) error {
if _, err := run(ctx, "groupdel", name); err != nil {
return fmt.Errorf("cannot remove the group %q: %w", name, err)
}
return nil
}
// CreateSystemGroup makes a system group with gid, with busybox's addgroup.
func (alpine) CreateSystemGroup(ctx context.Context, run Runner, name string, gid int) error {
if _, err := run(ctx, "addgroup", "-S", "-g", strconv.Itoa(gid), name); err != nil {
return fmt.Errorf("cannot create the group %q: %w", name, err)
}
return nil
}
// CreateSystemAccount makes a system account with busybox's adduser: -S system, -D no password, -H no home made.
func (alpine) CreateSystemAccount(ctx context.Context, run Runner, name string, uid, gid int, groups []string) error {
group, err := run(ctx, "getent", "group", strconv.Itoa(gid))
if err != nil {
return fmt.Errorf("cannot create the account %q: its group gid %d is not here: %w", name, gid, err)
}
groupName := name
if n, _, ok := strings.Cut(group, ":"); ok {
groupName = n
}
if _, err := run(ctx, "adduser", "-S", "-D", "-H", "-h", NoHome, "-s", "/sbin/nologin", "-u", strconv.Itoa(uid),
"-G", groupName, name); err != nil {
return fmt.Errorf("cannot create the account %q: %w", name, err)
}
for _, g := range groups {
if _, err := run(ctx, "addgroup", name, g); err != nil {
return fmt.Errorf("cannot put the account %q in %q: %w", name, g, err)
}
}
return nil
}
// ModifyAccount gives an account the mesh made exactly groups as its added groups, with busybox's addgroup and
// delgroup. Its primary group busybox cannot change (it has no usermod, and the mesh edits no passwd by hand):
// an account whose primary group must change is refused, saying so, and left as it is.
func (alpine) ModifyAccount(ctx context.Context, run Runner, name string, gid int, groups []string) error {
passwd, err := run(ctx, "getent", "passwd", name)
if err != nil {
return fmt.Errorf("cannot read the account %q: %w", name, err)
}
fields := strings.Split(strings.TrimSpace(passwd), ":")
if len(fields) < 4 || fields[3] != strconv.Itoa(gid) {
return fmt.Errorf("this system cannot change the account %q's primary group to %d: busybox has no usermod, "+
"and the mesh edits no passwd by hand: %w", name, gid, ErrUnsupported)
}
primary := ""
if g, err := run(ctx, "getent", "group", fields[3]); err == nil {
primary, _, _ = strings.Cut(strings.TrimSpace(g), ":")
}
out, err := run(ctx, "id", "-nG", name)
if err != nil {
return fmt.Errorf("cannot read the account %q's groups: %w", name, err)
}
have, want := map[string]bool{}, map[string]bool{}
for _, g := range strings.Fields(out) {
if g != primary && g != name {
have[g] = true
}
}
for _, g := range groups {
want[g] = true
if !have[g] {
if _, err := run(ctx, "addgroup", name, g); err != nil {
return fmt.Errorf("cannot put the account %q in %q: %w", name, g, err)
}
}
}
for g := range have {
if !want[g] {
if _, err := run(ctx, "delgroup", name, g); err != nil {
return fmt.Errorf("cannot take the account %q out of %q: %w", name, g, err)
}
}
}
return nil
}