postgres's data directory moved to /var/lib/mesh/postgres. On ace and novox that directory was there, root:root 0700, before the directory resource was applied, so the engine used it as found (issue 339's rule) and left it to root: the postgres provider, running as the operator's account, cannot reach its own secrets, consumers and grants there. /var/lib/mesh is the engine's tree (root, 0755): only the engine's account makes a directory in it. A directory below it owned by that account is now the mesh's and is given its declared owner and mode. Anywhere else, or owned by another account, issue 339's rule stands.
132 lines
6.0 KiB
Go
132 lines
6.0 KiB
Go
package apply
|
|
|
|
// A mesh directory outside a home is given its mode and owner only through a descriptor (novox/hq issue 358).
|
|
//
|
|
// Below a home every component is opened without following a link (ADR 0266, home_links.go), and a directory the
|
|
// operator handed to the mesh is changed through one descriptor opened from / with no link followed (issue 356).
|
|
// Every other directory the mesh owns — a module's state under /var/lib, a drop-in directory under /etc — was
|
|
// given its mode and owner by path, with chmod and chown, which follow a link anywhere in the path: whoever could
|
|
// turn a parent into a link between two applies directed root's chmod and chown at the link's target.
|
|
//
|
|
// The danger is a link another account can make or replace, not a link as such: /var/run is a link to /run on
|
|
// every node, and a disk is sometimes reached through one. So (the decision recorded on issue 358, 2026-10-11):
|
|
//
|
|
// - **the directory is opened component by component from /**, each with O_DIRECTORY|O_NOFOLLOW, and its mode
|
|
// and owner are changed with fchmod and fchown on that descriptor; a directory the walk makes is made in its
|
|
// parent's descriptor. A link put in place after any check is refused by the kernel at the moment of use;
|
|
// - **a link in a parent is followed only when root alone could have put it there**: the link is root's, and
|
|
// every directory from / down to the one holding it is root's and writable by no other account (no write for
|
|
// others, no write for a group other than root's). Its target is then walked the same way, from /;
|
|
// - **any other link refuses that directory**, naming the link and the account that could replace it; nothing
|
|
// is changed there, and the rest of the declaration applies;
|
|
// - **the directory itself is never a link** (issue 339, applyDirectory).
|
|
//
|
|
// A directory below a home keeps the walk of ADR 0266, which follows no link below the home at all.
|
|
|
|
import (
|
|
"fmt"
|
|
osuser "os/user"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/declaration"
|
|
)
|
|
|
|
// LinkInPathError is a mesh directory refused because a link in its path is one another account could replace.
|
|
type LinkInPathError struct {
|
|
// Path is the directory declared, Link the link in its path, Who the account that could replace it.
|
|
Path, Link, Who string
|
|
}
|
|
|
|
func (e *LinkInPathError) Error() string { return e.Path + " " + e.why() }
|
|
|
|
// why is the refusal without the path it refuses, as a PlacementRefusedError says it.
|
|
func (e *LinkInPathError) why() string {
|
|
return fmt.Sprintf("is reached through the link %s, which %s could replace: the node-engine runs as root and "+
|
|
"follows a link to a directory it gives a mode and owner only when root alone could have made it (novox/hq "+
|
|
"issue 358). Make %s a directory, or make it and every directory above it root's and writable by no other "+
|
|
"account", e.Link, e.Who, e.Link)
|
|
}
|
|
|
|
// couldReplace says who besides root could replace an entry of a directory with this owner, group and mode: ""
|
|
// when nobody can. A variable so a test, which runs below a directory every account may write (/tmp), can name
|
|
// which of its own directories count as root's alone.
|
|
var couldReplace = func(path string, uid, gid, mode uint32) string {
|
|
if uid != 0 {
|
|
return "the account " + accountNamed(uid) + " (it owns " + path + ")"
|
|
}
|
|
if mode&0o002 != 0 {
|
|
return "every account (" + path + " is writable by all)"
|
|
}
|
|
if mode&0o020 != 0 && gid != 0 {
|
|
return "the group " + groupNamed(gid) + " (it may write " + path + ")"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// linkReplacer says who besides root owns a link: "" when root does. A variable for the same reason.
|
|
var linkReplacer = func(link string, uid uint32) string {
|
|
if uid != 0 {
|
|
return "the account " + accountNamed(uid) + " (it owns the link)"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func accountNamed(uid uint32) string {
|
|
if u, err := osuser.LookupId(strconv.FormatUint(uint64(uid), 10)); err == nil {
|
|
return u.Username
|
|
}
|
|
return "with uid " + strconv.FormatUint(uint64(uid), 10)
|
|
}
|
|
|
|
func groupNamed(gid uint32) string {
|
|
if g, err := osuser.LookupGroupId(strconv.FormatUint(uint64(gid), 10)); err == nil {
|
|
return g.Name
|
|
}
|
|
return "with gid " + strconv.FormatUint(uint64(gid), 10)
|
|
}
|
|
|
|
// JudgeDirectories is the walk's judgement over every directory a declaration names, read-only (novox/hq issue
|
|
// 358): nothing is made, chmodded or chowned. One line per directory, in the declaration's order, after one line
|
|
// that counts them: placed, would be refused and why, refused already by the placement guard (issue 339), or
|
|
// below a home, where ADR 0266's walk judges it as before.
|
|
func JudgeDirectories(d *declaration.Declaration) string {
|
|
var lines []string
|
|
var placed, refused, guarded, homed int
|
|
declared := declaredHomes(d.Resources)
|
|
for _, r := range d.Resources {
|
|
dir, ok := r.(*declaration.Directory)
|
|
if !ok {
|
|
continue
|
|
}
|
|
path := filepath.Clean(dir.Path)
|
|
if err := refusePathWith(path, placingDirectory, moduleOfID(dir.ID), dir.Owner, declared); err != nil {
|
|
guarded++
|
|
lines = append(lines, path+": refused already by the placement guard: "+err.Error())
|
|
continue
|
|
}
|
|
if home := homeAbove(path); home != "" {
|
|
homed++
|
|
lines = append(lines, path+": below the home "+home+", walked without following any link as before (ADR 0266)")
|
|
continue
|
|
}
|
|
if err := judgeMeshDir(path); err != nil {
|
|
refused++
|
|
lines = append(lines, path+": would be refused: "+err.Error())
|
|
continue
|
|
}
|
|
placed++
|
|
lines = append(lines, path+": placed")
|
|
}
|
|
head := fmt.Sprintf("%d directories declared: %d placed, %d would be refused, %d refused already by the "+
|
|
"placement guard, %d below a home (novox/hq issue 358; judged read-only, nothing changed)",
|
|
placed+refused+guarded+homed, placed, refused, guarded, homed)
|
|
return strings.Join(append([]string{head}, lines...), "\n")
|
|
}
|
|
|
|
// meshDataRoot is the mesh's own tree, where a module placed "mesh" keeps its directory (<data root>/mesh/<module>):
|
|
// the engine's, 0755, so only the engine's account makes a directory in it (novox/hq issue 1873). A variable so a
|
|
// test can place it.
|
|
var meshDataRoot = "/var/lib/mesh"
|