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:
2026-08-30 03:22:38 +02:00
parent bc5b6e2143
commit c57087d75d
13 changed files with 1006 additions and 13 deletions
+37
View File
@@ -131,3 +131,40 @@ func errText(err error) string {
}
return err.Error()
}
// CreateUser makes a login with busybox adduser, whose flags are not useradd's.
//
// `-D` is "do not ask for a password", which is what makes it usable without a terminal. A login
// created this way has no password and cannot be logged into over the network with one, which is
// correct: what the mesh manages is what a login owns, never a way to become it.
func (alpine) CreateUser(ctx context.Context, run Runner, name, home, shell string) error {
args := []string{"-D"}
if home != "" {
args = append(args, "-h", home)
}
if shell != "" {
args = append(args, "-s", shell)
}
if _, err := run(ctx, "adduser", append(args, name)...); err != nil {
return fmt.Errorf("cannot create the user %q: %w", name, err)
}
return nil
}
func (alpine) SetUserShell(ctx context.Context, run Runner, name, shell string) error {
// busybox has no usermod. `sed`-ing /etc/passwd is what the distribution's own tooling does,
// and chsh is the one command that exists for it everywhere.
if _, err := run(ctx, "chsh", "-s", shell, name); err != nil {
return fmt.Errorf("cannot set %q's shell to %q: %w", name, shell, err)
}
return nil
}
// AddUserToGroup uses addgroup, which on busybox takes the user and the group and is additive by
// construction — there is no form of it that replaces the set.
func (alpine) AddUserToGroup(ctx context.Context, run Runner, name, group string) error {
if _, err := run(ctx, "addgroup", name, group); err != nil {
return fmt.Errorf("cannot put %q in the group %q: %w", name, group, err)
}
return nil
}