ghcr.io/herobrauni/braunicore:stable is a signed derivative of
ucore-minimal:stable. It adds a small set
of host runtime tools, one secret-free Beszel Quadlet, and the trust
scope for this repository. It deliberately inherits uCore's kernel, boot
setup, container runtimes, signing policy, services, and automatic update
configuration.
The repository started from the current
ublue-os/image-template; it does
not copy or fork the uCore build system.
Renovate watches the digest-pinned uCore base, the version-and-digest-pinned
Beszel agent, GitHub Actions, and Cosign. CI verifies the pinned uCore digest
with the upstream public key in ucore.pub before either native
build starts. A dependency pull request must then pass native amd64 and arm64
builds, bootc container lint, package assertions, containers/image policy
checks, and Quadlet/systemd validation.
The pinned uCore key SHA-256 is
af78ecfda6eb21c35195af3739341715e9cfc3f2f5911dd9c10b0670547bf6e8.
An upstream key rotation requires explicit review of the replacement trust root.
On main, each architecture is rechunked, pushed to a commit-specific staging
tag, signed, and verified. CI then creates and signs the multi-architecture
commit tag. Only after containers/image successfully enforces that signature
does CI create the immutable UTC YYYYMMDD-<commit> tag and finally move
stable to the same digest, verifying both results. A failed build or signature
therefore cannot replace the working stable image. Commit and dated tags
retain previous releases for diagnosis and rollback.
Edit build_files/packages.txt, one Fedora package
per line. Adding or removing an RPM is a one-line change. The build uses dnf5;
hosts do not use runtime rpm-ostree install layering.
Fedora repositories are rolling rather than immutable snapshots, so package names alone do not make historical rebuilds bit-for-bit reproducible. Each native CI build therefore reports the exact architecture-specific NEVRA delta from the signature-verified uCore base in its GitHub Actions summary. Review that delta for dependency additions, upgrades, removals, and replacements before merging image changes.
The current additions are Wget2's wget compatibility command, Incus (from
the stock Fedora repository, which tracks the COPRs), and Fedora's nix and
nix-daemon packages. Braunix owns Fish, htop, btop, ripgrep, fd, tree, ncdu,
Atuin, uv, and chezmoi. The image build fails if any of those user tools is
also installed as an RPM.
Braunicore provides multi-user Nix; it does not use the upstream curl
installer. The Fedora packages provide the Nix CLI, daemon, build users,
tmpfiles rules, and shell environment. nix.mount bind-mounts the persistent
/var/lib/nix directory at the conventional /nix path before socket
activation. The direct daemon service is disabled; nix-daemon.socket starts
it on demand.
The image builds and installs the source-reviewed braunicore_nix SELinux
file-context module. Store objects and profiles use usr_t, while the daemon
socket uses var_run_t. nix-prepare.service runs restorecon before the
bind mount. New store objects inherit the store's usr_t context, so Nix does
not require permissive mode or an unconfined installer transition.
/usr/bin/fish is a small compatibility launcher, not a second Fish
installation. It executes $HOME/.nix-profile/bin/fish after Braunix is
activated and falls back to /bin/bash beforehand. This keeps existing passwd
entries valid while allowing Braunix to be Fish's only package owner.
Incus ships without its own SELinux module since Fedora's incus-6.23;
enforcement comes from container-selinux contexts already present in uCore,
which the build asserts. The incus.service/incus.socket units stay
disabled by default. Grant API access by adding users to the incus-admin
group (created by sysusers) and enable the daemon with
systemctl enable --now incus.service.
The current uCore base was inspected before this image was created. It already contains tmux, Tailscale, Podman, Moby/Docker, Docker Buildx/Compose, bootc, and rpm-ostree, so none is reinstalled. CI continues to assert those critical packages are present.
The system Quadlet runs a reviewed mirror of the official
henrygd/beszel-agent image
with host networking, as recommended for host network statistics. Upstream
publishes BuildKit provenance and an SBOM but no verifiable image signature, so
Beszel updates never automerge. After a reviewed update reaches main, release
CI copies the exact multi-architecture digest to ghcr.io/herobrauni, signs it
with the Braunicore key, and verifies containers/image enforcement before the
OS release can advance. The mirror uses a dedicated beszel-* tag in the
already-public Braunicore repository, and hosts reject it when unsigned. The
tag and digest are pinned in
beszel-agent.container
and updated by Renovate. Data persists at /var/lib/beszel-agent.
The unit has ConditionPathExists=/etc/beszel-agent.env; an unconfigured host
boots normally and does not enter a restart loop. Create the file with exactly
these per-host values from the Beszel Hub:
KEY=ssh-ed25519 AAAA...
TOKEN=...
HUB_URL=https://beszel.example.comKEY, TOKEN, and HUB_URL are the current required agent variables. Do
not quote the values: the Quadlet passes this file to Podman's --env-file,
which keeps quote characters in the value, breaking the agent's key parser. The
image supplies LISTEN=45876 and DATA_DIR=/var/lib/beszel-agent. If the Hub
only uses the outbound WebSocket connection, add DISABLE_SSH=true to avoid an
incoming SSH listener. Install the file as root:root mode 0600, then run:
sudo systemctl daemon-reload
sudo systemctl start beszel-agent.service
sudo systemctl status beszel-agent.serviceuCore's rootful Podman socket is present at /run/podman/podman.sock; the
Quadlet activates it and mounts it at /var/run/docker.sock, where Beszel's
Docker-compatible monitor expects it. Podman's API is Docker-compatible, but
socket access is effectively root-equivalent even with a read-only bind mount.
The Quadlet follows Podman's required SELinux handling for socket access. Use a
filtering socket proxy in Ansible if that risk is unacceptable.
The system D-Bus socket is mounted read-only at the same path inside the container, enabling Beszel's systemd service monitoring (status, CPU/memory usage, restart counts, and failed-service alerts). This requires systemd 243+ on the host, which uCore satisfies.
Podman automatic container updates are intentionally not enabled. A digest pin cannot float, and the controlled update path is Renovate PR → native CI → signed braunicore OS update. This also avoids an agent changing independently of the host image.
The image guarantees /usr/bin/fish but does not create users or edit a
machine's passwd database. A current Fedora CoreOS Butane snippet is:
variant: fcos
version: 1.7.0
passwd:
users:
- name: example
shell: /usr/bin/fishFor an existing host, verify the binary before changing the account:
- name: Check that Fish is in the deployed image
ansible.builtin.stat:
path: /usr/bin/fish
register: fish_binary
- name: Use Fish as the login shell
ansible.builtin.user:
name: "{{ fleet_user }}"
shell: /usr/bin/fish
when: fish_binary.stat.exists and fish_binary.stat.executablePodman and just follow the upstream template workflow:
just build braunicore dev
sudo just ostree-rechunk braunicore dev
sudo podman run --rm --entrypoint /bin/bash braunicore:dev -lc \
'bootc container lint && rpm -q wget2-wget incus incus-agent nix nix-daemon tmux tailscale podman moby-engine'Docker BuildKit can perform a quick non-rechunked development build:
docker build --pull -f Containerfile -t braunicore:dev .
docker run --rm --entrypoint /bin/bash braunicore:dev -lc \
'bootc container lint && command -v fish wget incus nix nix-daemon'CI additionally runs the Podman system generator and systemd-analyze verify
against the generated Beszel service.
Every architecture manifest and multi-architecture index is signed with the
dedicated public key in cosign.pub. Its SHA-256 is:
873b12b4337d06414daeeab6032b088ee7769280fbd641f2c966647b9797c7da
The encrypted private key is supplied to Actions as SIGNING_SECRET; its
separate passphrase is SIGNING_PASSWORD. Neither belongs in Git, Ignition,
Ansible inventory, logs, or the image. Keep an offline encrypted recovery copy
of cosign.key and store its passphrase separately (for example, an encrypted
removable backup in a safe plus a password-manager record). Test recovery by
signing a disposable registry image, and record key rotation as a reviewed
policy transition. Losing both GitHub secrets and the recovery copy requires a
fleet trust-policy migration.
Cosign 3 is instructed to emit the legacy attachment format currently consumed
by the containers/image sigstoreSigned policy used by bootc/rpm-ostree. The
build merges only the exact ghcr.io/herobrauni/braunicore scope into uCore's
existing /etc/containers/policy.json and preserves all upstream scopes.
See docs/switching.md for exact preflight, initial trust
bootstrap, bootc switch --enforce-container-sigpolicy, verification, reboot,
post-boot, automatic-update, and rollback commands. Do not run the switch fleet
wide; validate one disposable VPS and retain console access first.
The installation architecture remains the official Fedora CoreOS metal raw
image → Ignition → OCI transition. No per-release braunicore disk image is
built. docs/reinstall.md records the already-available
opt-in --ucore-image setting in the local herobrauni/reinstall fork.
- Image: identical fleet-wide RPMs, immutable vendor Quadlets, public trust roots, and secret-free defaults.
- Ignition: first-boot users, SSH keys, storage/networking, hostname, and the initial FCOS-to-braunicore transition.
- Ansible: Beszel environment files, Tailscale enrollment, account changes, firewalls, host-specific mounts, and other per-host agents.
Tailscale activation/authentication stays in Ansible. PatchMon and similar agents remain future Ansible provisioning until they have a clean, secret-free, bootc-compatible vendor unit.
Open the dependency PR's Build and publish checks and identify whether amd64
or arm64 failed. Reproduce with just build, inspect the dnf5, bootc lint, or
Quadlet error, and leave the PR unmerged until both native jobs pass. Closing a
bad PR leaves stable untouched; Renovate will propose a later update. Never
resolve a failure by removing the digest pin or signature checks.
See MAINTENANCE.md for the short maintenance policy.