package main // Who can become root here without a person (novox/hq ADR 0259 ยง8): the controller's self-check asks it of // every machine where the router or a channel proving its sender runs, for the accounts agents run as and // the account the machine's runtime runs commands as. // // Root without a person is any of: a sudo rule letting the account run every command as root with no // password; membership of a group that is root by another name (docker, which mounts the host; disk, which // reads it raw; wheel or sudo only where a rule grants them without a password, which the listing shows); or // the account being root itself. import ( "fmt" "regexp" "strings" ) // Escalation is one account's answer. type Escalation struct { Account string `json:"account"` Root bool `json:"root_without_a_person"` Why string `json:"why"` } // rootGroups are groups that are root by another name. var rootGroups = map[string]string{"docker": "the docker group, which can mount the machine's root file system", "disk": "the disk group, which reads and writes the drives raw", "root": "the root group"} var accountShape = regexp.MustCompile(`^[a-z_][a-z0-9_-]{0,31}$`) // Escalations answers, for each account, whether it can become root here without a person. func (m *Machine) Escalations(accounts []string) ([]Escalation, error) { if len(accounts) == 0 { return nil, fmt.Errorf("name the accounts to judge") } var out []Escalation for _, a := range accounts { if !accountShape.MatchString(a) { return nil, fmt.Errorf("%q is not an account name", a) } out = append(out, m.escalation(a)) } return out, nil } func (m *Machine) escalation(account string) Escalation { e := Escalation{Account: account} if account == "root" { e.Root, e.Why = true, "it is root" return e } id := m.Run(bg(), "id", "-nG", account) if id.Status != 0 || id.Err != "" { e.Why = "no such account here" return e } for _, g := range strings.Fields(id.Stdout) { if why, ok := rootGroups[g]; ok { e.Root, e.Why = true, "a member of "+why return e } } // sudo's own word, for that account: listing another account's rules needs root, which the runtime's // account escalates to as the mesh's acting tools do. r := m.Run(bg(), "sudo", "-n", "-l", "-U", account) if r.Status != 0 || r.Err != "" { if strings.Contains(r.Stdout+r.Stderr, "not allowed to run sudo") { e.Why = "no sudo rule names it" return e } e.Why = "sudo could not be asked: " + strings.TrimSpace(firstLine(r.Stderr+r.Err)) return e } if strings.Contains(r.Stdout, "is not allowed to run sudo") { e.Why = "no sudo rule names it" return e } rules := ParseList(r.Stdout, account) if rules.PasswordlessAll { for _, rule := range rules.Rules { if hasTag(rule.Tags, "NOPASSWD") && contains(rule.Commands, "ALL") && runsAsRoot(rule.RunAs) { e.Root, e.Why = true, "sudo lets it run every command as root without a password ("+rule.Line+")" return e } } } e.Why = "sudo asks it for a password, or lets it run named commands only" return e }