A user, bytes, and an archive — because most of what people install is
not a service A shell, a terminal, a chat client, a desktop are a package plus configuration in somebody's home. A mesh with no notion of a user can own /etc and nothing anybody looks at, which is most of the reason to manage a machine at all. Three shapes, and the vocabulary test asserts the count precisely because widening it widens what a compromised control plane can express: user a login, its shell and its groups archive a set of files, fetched by digest and unpacked (file) gains `bytes` for what is not text, and `owner` `user` also makes "zsh is my login shell" declared state. chsh is a command, the link may not carry one, and a shell settable only by hand is a shell the mesh cannot manage. Groups are additive and never pruned — usermod without --append REPLACES them, which would silently remove every group that makes a login able to use the machine. A machine's own groups are not the mesh's to know about. The archive is the one place this host reaches out on its own; everywhere else it holds one outbound connection and fetches nothing. So it carries the discipline the bootstrap already uses for images: pinned by digest, and the digest checked before a single file is written. Two decisions in the unpacker worth naming: - an entry naming a path outside the archive is REFUSED, not sanitised. Rewriting it to land inside would put a file somewhere nobody asked for and report success. Found by the test: the first version quietly relocated it. - symlinks and device nodes are refused rather than skipped, or an archive that needed one arrives silently incomplete. A partial host does archives and refuses users: an archive needs a filesystem and a way to fetch; a user needs a user database it is allowed to write.
This commit is contained in:
@@ -142,3 +142,38 @@ func (arch) SetServiceBoot(ctx context.Context, run Runner, unit, boot string) e
|
||||
_, err := run(ctx, "systemctl", verb, unit)
|
||||
return err
|
||||
}
|
||||
|
||||
// CreateUser makes a login with useradd.
|
||||
//
|
||||
// `--create-home` because a user whose home does not exist is a user nothing can be delivered
|
||||
// to, and delivering a shell's configuration is most of why the mesh knows about users at all.
|
||||
func (arch) CreateUser(ctx context.Context, run Runner, name, home, shell string) error {
|
||||
args := []string{"--create-home"}
|
||||
if home != "" {
|
||||
args = append(args, "--home-dir", home)
|
||||
}
|
||||
if shell != "" {
|
||||
args = append(args, "--shell", shell)
|
||||
}
|
||||
if _, err := run(ctx, "useradd", append(args, name)...); err != nil {
|
||||
return fmt.Errorf("cannot create the user %q: %w", name, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (arch) SetUserShell(ctx context.Context, run Runner, name, shell string) error {
|
||||
if _, err := run(ctx, "usermod", "--shell", shell, name); err != nil {
|
||||
return fmt.Errorf("cannot set %q's shell to %q: %w", name, shell, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddUserToGroup appends, and `--append` is the whole point: without it usermod REPLACES the
|
||||
// user's supplementary groups, so a declaration naming one group would silently remove every
|
||||
// other — including the ones that make a login able to use a machine at all.
|
||||
func (arch) AddUserToGroup(ctx context.Context, run Runner, name, group string) error {
|
||||
if _, err := run(ctx, "usermod", "--append", "--groups", group, name); err != nil {
|
||||
return fmt.Errorf("cannot put %q in the group %q: %w", name, group, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user