Skip to content

bug: configure an OS whose /etc is assembled at boot without breaking it - #771

Draft
Paco Huelsz (frhuelsz) wants to merge 2 commits into
user/frhuelsz/inline-verityfrom
user/frhuelsz/acl-etc-overlay
Draft

bug: configure an OS whose /etc is assembled at boot without breaking it#771
Paco Huelsz (frhuelsz) wants to merge 2 commits into
user/frhuelsz/inline-verityfrom
user/frhuelsz/acl-etc-overlay

Conversation

@frhuelsz

@frhuelsz Paco Huelsz (frhuelsz) commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

🔍 Description

bug. Two fixes for configuring an OS whose /etc is assembled at boot rather than shipped in the image. Azure Container Linux does this; configuring a single user was enough to make an installed system unbootable.

Stacked on #754, which is this PR's base, so the diff shows only the two commits below. It is stacked because ACL cannot be installed at all without #754, so these fixes are untestable without it. Merge #754 first; this will then retarget to main cleanly.

🤔 Rationale

ACL ships no /etc on its root filesystem. Its initrd assembles one at boot as an overlay: the factory contents on the sealed /usr as the lower layer, the root filesystem's own /etc as the upper.

Servicing runs before any of that. A chroot into the new root sees an empty /etc, so anything that rewrites a file the image provides writes a replacement rather than an edit. At boot that replacement becomes the upper layer and hides the factory file.

Configuring one user left the installed system with a 43-byte /etc/passwd containing only that user — no root, no core:

matrix:!:1000:1000::/home/matrix:/bin/bash

and the boot ended in an emergency shell:

systemd-tmpfiles: Failed to resolve user 'core': No such process
[FAILED] Failed to start initrd-setup-root.service - Root filesystem setup.
[DEPEND] Dependency failed for initrd.target

1. Refuse when there is no user database

useradd creates /etc/passwd from scratch when none exists. Without an existing database there is nothing meaningful to add users to, so refuse rather than produce one that cannot boot. This does not make the feature work; it stops it destroying the system.

Worth keeping even with the overlay fix below: it is distro-agnostic, and it turns any future recurrence into a clear message rather than an unbootable machine.

2. Give servicing the same /etc the system boots with

Mount the same overlay before entering the chroot, so servicing sees what the booted system will.

Why this is lower-risk than it looks: the overlay's upper layer is the root filesystem's /etc — exactly where Trident already writes. So the on-disk result is unchanged for everything that already worked. hostname, netplan, modules, services and additionalFiles only add files, and overlayfs merges directories. What changes is that modifying a file the image provides now copies it up from the factory version first, so it stays complete.

It also fixes more than users: any configuration rewriting a factory /etc file was subject to the same hazard, /etc/selinux/config among them.

Applied to both paths that chroot — clean install and A/B update. Images not using this layout are unaffected: the overlay is mounted only for ACL, and only when the factory directory is present.

📝 Checks

Verified on a real ACL image via make run-netlaunch, installing and booting in QEMU/KVM.

Before — install fails, or bricks:

usermod: group 'wheel' does not exist          # factory /etc/group not visible
/etc/passwd: 1 line, 43 bytes                  # factory passwd replaced
boot -> emergency shell

After — install succeeds and the system boots:

/etc/passwd: 24 lines
root     root:x:0:0:root:/root:/bin/bash
core     core:x:500:500:Inert Identity:/home/core:/bin/bash
matrix   matrix:x:1000:1000::/home/matrix:/bin/bash
wheel:x:28:matrix
id matrix -> uid=1000(matrix) groups=1000(matrix),28(wheel)

Confirmed the overlay is unmounted and its work directory removed; the /.etc-work present at runtime is ACL's own, recreated by the initrd (findmnt shows the live /etc overlay using it).

Automated: 22 test suites pass, cargo fmt --check clean, cargo clippy introduces no new warnings.

📌 Follow-ups

TODO:

  • A user created with no password is locked (matrix:! in shadow), so key-based SSH is refused by PAM. That is deliberate — Trident locks accounts with no password configured — but it means sshPublicKeys alone does not yield a usable login. Worth documenting rather than changing.

🗒️ Notes

Opened as a draft pending #754.

Found while surveying which os features work on ACL. The same survey shows hostname, netplan, modules, services and additionalFiles already work, and kernelCommandLine fails cleanly because it is implemented via /etc/default/grub, which a systemd-boot/UKI image does not have.

`useradd` creates /etc/passwd from scratch when none is present, holding
only the users it was asked to add. On an image that assembles /etc at boot
rather than shipping it, Trident runs inside a chroot where that file is not
there yet, so the database it creates replaces the real one: the installed
system loses every account it had, root included.

Azure Container Linux does exactly this. Configuring a single user left the
system with a 43-byte /etc/passwd, and it failed to boot -- systemd-tmpfiles
could not resolve the `core` user, `initrd-setup-root.service` failed, and
the boot ended in an emergency shell.

Refuse instead. Without an existing database there is nothing meaningful to
add users to, so say so rather than producing one that cannot boot. This does
not make user configuration work on such an image; it stops it destroying the
system while a proper fix is made.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Azure Container Linux ships no /etc on its root filesystem. Its initrd
assembles one at boot as an overlay: the factory contents on the sealed /usr
as the lower layer, the root filesystem's own /etc as the upper.

Servicing runs before any of that. A chroot into the new root therefore sees
an empty /etc, so anything rewriting a file the image provides writes a
replacement rather than an edit, and at boot that replacement becomes the
upper layer and hides the factory file. Configuring a single user was enough
to leave the system with a /etc/passwd holding only that user, and no way to
boot.

Mount the same overlay before entering the chroot, so servicing sees what the
booted system will. Writes still land in the root filesystem's /etc exactly
as before, so nothing changes for configuration that only adds files --
hostname, netplan, kernel modules, systemd units, arbitrary additional files
all behaved correctly already, because overlayfs merges directories. What
changes is that modifying a file the image provides now copies it up from the
factory version first, so it stays complete.

That covers more than the user database: any configuration that rewrites a
file from the factory /etc, such as the SELinux configuration, was subject to
the same hazard.

Applied to both servicing paths that chroot, clean install and A/B update.
Images that do not use this layout are unaffected: the overlay is mounted
only for ACL, and only when the factory directory is present.

The layout is described in sysdefs, alongside the other ACL definitions.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).
There may be pipelines that require an authorized user to comment /azp run to run.

@frhuelsz
Paco Huelsz (frhuelsz) changed the base branch from main to user/frhuelsz/inline-verity September 3, 2026 01:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant