Compare commits

..

38 commits

Author SHA1 Message Date
s8n
5c961eba88 Update docs/DOCS-DOCS.md
Some checks failed
Lint / Kickstart syntax (push) Failing after 1s
Lint / Shell scripts (push) Failing after 6s
Lint / No personal/onyx leaks (push) Failing after 3s
2026-05-06 18:03:37 +01:00
obsidian-ai
8c70030d80 docs(ROADMAP): pivot — v0.6 cancelled, v0.7 BlueBuild OCI is mainline
Strategy pivot 2026-05-06: v0.5.32 produced a green ISO on Forgejo
runner. That's the kickstart-path proof point. Continuing v0.6
kickstart polish is sunk-cost work on tooling retired at v1.0.

Pivot:
- v0.5.0 is the FINAL kickstart-path release. Tag, freeze, ship.
- v0.6 cancelled as a milestone. Original plan kept inline as
  HISTORICAL reference.
- v0.7 promoted to primary active milestone. Absorbs the v0.6
  ergonomic CLI tools (veilor-postinstall / veilor-doctor /
  veilor-update) with bootc upgrade replacing dnf upgrade.
- Active branch: v0.7-bluebuild-spike. All future feature work lands
  there, not on main.
2026-05-06 15:55:08 +01:00
obsidian-ai
89c7df0ecc docs: add PROOF-OF-WORK.md — receipts of work, tooling, and decisions
Single document that surfaces the depth of work behind veilor-os:
metrics, distros studied, every tool traversed in the build chain,
all 35+ failure classes hit and beaten, key engineering decisions and
why, what's in the repo beyond the kickstart, and the self-hosted
nullstone CI infrastructure built to support it.

Receipts not narrative — every claim links back to a file path,
commit, error, or config. Useful as portfolio anchor and as a single
read-this-first for anyone returning to the project after a gap.
2026-05-06 15:51:40 +01:00
obsidian-ai
c2b4df8ef9 ci: gate cosign/sbom/attest steps to github only
cosign keyless sign uses Sigstore Fulcio which requires a
Fulcio-trusted OIDC issuer. Forgejo runs don't have one, so cosign
falls back to the interactive device flow and times out
(error obtaining token: expired_token). Same applies to
attest-build-provenance and the SBOM action's signed attestation.

Skip all three on Forgejo for now; ISO + sha256 are sufficient for
v0.5.x test releases. Re-add when we self-host a Sigstore stack or
sign with a key-pair instead of keyless.
2026-05-06 15:41:00 +01:00
obsidian-ai
b9df392fbc docs(README): tone down secureblue credit (no code lifted yet)
We layer on their OCI image as v0.7 base; we don't redistribute their
source. Drop the AGPLv3-attribution prose — that becomes relevant only
if/when we ship a verbatim chunk of their config/policy in our repo.
2026-05-06 15:38:35 +01:00
obsidian-ai
84fa325e46 docs(README): add secureblue column + upstream credit section
secureblue (AGPLv3) is the upstream hardened atomic Fedora that the
v0.7 BlueBuild spike layers on top of. Comparison table now includes
secureblue alongside Kicksecure + stock Fedora KDE. New "Credit &
relationship to secureblue" section spells out where their work
already solves problems we don't need to reinvent (Trivalent,
SELinux policy, kernel cmdline, signed OCI), how veilor-os differs
(kickstart install path + branding + Forgejo CI), and the AGPLv3
attribution rule for any code we lift verbatim.
2026-05-06 15:32:22 +01:00
obsidian-ai
1e4ca2b56b ci: symlink /work -> GITHUB_WORKSPACE for ks %post SRC probe 2026-05-06 14:58:24 +01:00
obsidian-ai
446c602683 ks: drop apparmor-* packages — not in Fedora 43 repos
Build error: 'Failed to find package apparmor-parser : No match for
argument'. Fedora 43 base and updates do not ship AppArmor packages;
the prior comment was incorrect. Defer AppArmor to v0.7 secureblue OCI
hybrid (which has its own LSM stack), or land via COPR overlay later.
2026-05-06 14:48:06 +01:00
obsidian-ai
ac5c29df42 ci: run build directly in Fedora job container, drop addnab nest
forgejo-runner labels nullstone -> fedora:43 image. Switching
runs-on: ubuntu-24.04 -> nullstone makes the job container itself
the build environment, eliminating the docker-in-docker workspace
bind-mount problem (host path != act-container path).

Build now runs as root in fedora:43, installs livecd-tools directly
via dnf, and writes outputs to $GITHUB_WORKSPACE which is the natural
runner workdir on host. No nested docker, no userns juggling, no
explicit -v workspace bind needed.
2026-05-06 14:35:44 +01:00
obsidian-ai
6f4842a75c ci: add /work diagnostic before sed-redirect to surface bind/perm issue 2026-05-06 14:19:13 +01:00
obsidian-ai
4b90e7e00b ci: repin fedora:43 build container to amd64 digest
Prior pin was the arm64 manifest digest (linux/arm64/v8); on x86_64
host it failed with `exec /usr/bin/sh: exec format error`. Pinned to
the amd64 manifest entry from the same fat-manifest.
2026-05-06 14:10:25 +01:00
obsidian-ai
7a0c665cf0 ci: add --userns=host to nested Fedora build container
Forgejo runner on nullstone runs against a daemon with
userns-remap=default. addnab/docker-run-action launches the Fedora 43
build container with --privileged, which is incompatible with
userns-remap unless --userns=host is also set.
2026-05-06 14:07:22 +01:00
obsidian-ai
d38fce4cb8 ci: pin sbom/cosign/attest actions to node20-safe versions
forgejo-runner v6.4.0 ships node20; floating tags @v0/@v3/@v2 now
resolve to actions whose runs.using=node24, which the runner cannot
exec. Pin to last node20-shipping release of each:

- anchore/sbom-action@v0.17.2
- sigstore/cosign-installer@v3.7.0
- actions/attest-build-provenance@v2.2.3
2026-05-06 13:57:49 +01:00
s8n
bc738c1c7b Merge pull request 'ci: gate softprops release steps + add Forgejo API equivalents' (#5) from feat/a1-forgejo-ci-adapt into main 2026-05-06 13:53:02 +01:00
s8n
a3f6c1a1a6 ci: gate softprops release steps + add Forgejo API equivalents
The build-iso workflow used softprops/action-gh-release@v2 unconditionally,
which only speaks the GitHub Releases REST API. When the workflow runs on
the Forgejo runner registered on nullstone, those steps would fail.

Add a server_url check so the GH-only path runs only on github.com, and
mirror it with a curl-based step that hits the Forgejo /api/v1/releases
endpoints. Behaviour:
  - github.com: identical to before (action-gh-release@v2).
  - git.s8n.ru: drop+recreate ci-latest release, upload chunked assets
                via the Forgejo attachments API.

Tag-driven "Attach to release" path mirrored the same way.

Refs: A1 build-eng task — Forgejo runner adaptation.
2026-05-06 13:52:43 +01:00
s8n
356013e1ca Merge pull request 'feat(installer): v0.6 ergonomics + polish — 5 quick wins' (#3) from feat/ux-installer-v06-polish into main 2026-05-06 13:47:35 +01:00
s8n
417acb5585 Merge pull request 'sec: AppArmor v0.6 stub — load profiles in complain mode' (#11) from feat/sec-apparmor-v06-stubs into main 2026-05-06 13:47:31 +01:00
s8n
df574e00f5 Merge pull request 'ci: cosign keyless sigs, SBOM, provenance + fedora digest pin' (#7) from feat/sre-cosign-sbom-attestation into main 2026-05-06 13:47:27 +01:00
s8n
3e660534a1 Merge pull request 'ci: pin actions to node20-safe tags + runner sock pass-through' (#8) from feat/runner-fix-docker-sock-and-node20 into main 2026-05-06 13:47:20 +01:00
s8n
749bcef5b4 Merge pull request 'sec: polish THREAT-MODEL.md for v0.7 public launch' (#10) from feat/sec-threat-model-polish into main 2026-05-06 13:46:58 +01:00
s8n
77ed91ed8e Merge pull request 'docs: test run report skeleton for v0.5.32 (Forgejo build)' (#4) from feat/docs-test-run-v0.5.32 into main 2026-05-06 13:46:12 +01:00
s8n
ad059ec73e docs: README de-GH + Forgejo build status 2026-05-06 13:45:33 +01:00
s8n
05d37f6419 docs: METHOD-CHANGELOG 2026-05-06 forgejo entry 2026-05-06 13:45:29 +01:00
s8n
eafb8b7aa1 sec: AppArmor v0.6 stub — load profiles in complain mode
Per docs/research/2026-05-05-agent-wave/04-hardening-tier-2.md (v0.6
scope item 1).

Adds:
  - apparmor-parser apparmor-utils apparmor-profiles to %packages in
    BOTH kickstart/veilor-os.ks (live ks) and overlay/usr/local/bin/
    veilor-installer (generated install ks heredoc).
  - scripts/40-apparmor.sh — wires aa-complain on every veilor-shipped
    profile. Idempotent. "loaded, present, nothing breaks".
  - overlay/etc/apparmor.d/veilor.d/firefox — 1-liner stub (binary
    confinement marker only; full policy post-v0.6).
  - overlay/etc/apparmor.d/veilor.d/thunderbird — same pattern.
  - Wired 40-apparmor.sh into install %post chain after
    30-apply-v03-theme.sh.

Complain mode means: profiles loaded, kernel logs syscall denials but
does NOT enforce. Operator can review audit.log post-install to
inform v0.7 policy authoring.
2026-05-06 11:15:30 +01:00
s8n
d0738970e0 sec: polish THREAT-MODEL.md for v0.7 public launch
Status flipped Draft → Final.

In-scope rows now cite specific config files / settings (auditable
from clean checkout):
  - LUKS2 params from kickstart/veilor-os.ks
  - sysctl knobs file path
  - USBGuard policy mode + rule type
  - sshd_config drop-in path + every directive
  - auditd rule path + watched paths
  - chrony NTS endpoints
  - systemd-resolved DoT settings
  - bootloader kernel args (lockdown, slab_nomerge, init_on_alloc/free, etc.)

Out-of-scope rows un-hedged. 'May not always' phrasings removed; each
adversary states unambiguously what veilor-os does NOT do.
2026-05-06 11:14:34 +01:00
obsidian-ai
7974ed7a6e ci: pin actions to node20-safe tags + runner sock pass-through
forgejo-runner v6.4.0 ships a node20 javascript engine. v4.2+ of
actions/checkout and v2.0.5+ of softprops/action-gh-release moved to
node24, which the runner refuses to exec. Pin both to last node20
release.

Pairs with a runner-side config change (separately deployed on
nullstone /home/docker/forgejo-runner/conf/config.yaml) that adds
`-v /var/run/docker.sock:/var/run/docker.sock` to per-job container
options + whitelists the socket via valid_volumes — without that
addnab/docker-run-action@v3 inside the catthehacker/ubuntu job
container can't reach the docker engine.

- actions/checkout v4 -> v4.1.7
- softprops/action-gh-release v2 -> v2.0.4
- addnab/docker-run-action v3 unchanged (composite/docker, no node)
- ludeeus/action-shellcheck@master unchanged (docker-based)
2026-05-06 10:50:15 +01:00
veilor-org
f06ee5cc1c chore: gitignore auto-install-vm test artifacts
Test rig was leaking test/auto-install-vm.{nvram,qcow2} into untracked
state. Pattern matches existing veilor-vm.* exclusion.
2026-05-06 10:50:04 +01:00
veilor-org
130f0432dd ci: TODO marker for SHA-pinning third-party actions
Note that all `uses:` directives still resolve to mutable major-
version tags. SHA-pinning is the Agent 8 audit recommendation but
requires per-action web lookups that stalled the previous SRE
attempt; tracked separately so this PR can land first.
2026-05-06 10:41:19 +01:00
veilor-org
08f16bb2ee ci: pin fedora:43 base image to digest
Pin registry.fedoraproject.org/fedora:43 to its current manifest
digest so a malicious or accidental tag-rewrite upstream cannot
silently change the base layer of every CI build. Digest was
captured via `skopeo inspect --raw` on 2026-05-06. Refresh
procedure documented inline.
2026-05-06 10:41:10 +01:00
veilor-org
25b8d30f35 ci: add cosign keyless sigs, SBOM, and provenance attestation
Sign each ISO chunk with cosign keyless OIDC, generate an SPDX SBOM
of the build output, and attach an in-toto build-provenance
attestation. Sigs/certs/SBOM are uploaded alongside the ISO parts in
the ci-latest rolling prerelease so the test/auto-install.sh path
can verify before reassembling.

Action versions are major-version tags (@v3, @v0, @v2). SHA-pinning
is tracked separately to keep this PR small and avoid the long web
lookups that stalled the previous attempt.
2026-05-06 10:40:56 +01:00
veilor-org
aa731f9daa docs: test run report skeleton for v0.5.32 (Forgejo build)
First test-runs/ report off the new template. Records the build host
(forgejo-runner on nullstone, ubuntu-24.04 / catthehacker:act-24.04),
notes that v0.5.32 is the first ISO produced after the GH Actions
mirror was disabled, and pre-populates the Findings section with the
7 v0.5.32 blocker fixes from the 2026-05-05 9-agent wave as expected
behaviours the tester must verify.

Result is left as "pending A1 build" — the operator + A5 fill in
per-step pass/fail and hardening output once the actual VM walkthrough
runs against the produced ISO. This is intentional: the report is the
scaffold; the test is a separate step.
2026-05-06 10:34:06 +01:00
veilor-org
441f7d057f feat(installer): promote eject-media reminder to its own box
In v0.5 the "Remove the install media" reminder was a single line
inside the green success box, and operators on both onyx and the
friend's RTX 4080 rig missed it — rebooted into the live ISO and
re-ran the installer thinking the install had silently failed.

Promote the reminder to its own loud yellow thick-bordered gum-style
box stacked directly below the success/countdown box, with three
lines of explanation. Renders for the full 10s of the countdown so
it stays in the operator's face the entire window.
2026-05-06 10:32:30 +01:00
veilor-org
816fc0ee68 feat(installer): 10s reboot countdown with per-tick redraw
v0.5 used `sleep 5` after a static "System will reboot in 5 seconds."
box, which left the operator guessing how much time was left to grab
the USB stick. The new loop runs 10 → 1, clearing + redrawing the
gum-style success box each tick with the remaining-seconds figure,
giving the operator a visible window to act.

10 seconds (vs 5) because real hardware operators were missing the
window — laptops with the USB on the far side of the dock take
4-5 seconds to physically reach. 10 is comfortable, not annoying.
2026-05-06 10:32:11 +01:00
veilor-org
44f0c787a7 feat(installer): confirm-twice for LUKS passphrase + admin password
A typo in the LUKS passphrase is unrecoverable — the disk is
unmountable without it and we don't escrow the key. Re-prompting
until the two reads match catches keyboard-layout surprises (the
US/UK quote-key position is the most common one) before they brick
the install.

Admin password gets the same treatment for consistency. Less
catastrophic (resettable from a recovery shell) but a mismatch
still locks the user out of their fresh install on first boot.

Loop bails on cancel/ESC and re-prompts on validate_pw failure.
2026-05-06 10:31:21 +01:00
veilor-org
900f5465b3 feat(installer): staged banner reveal at 40ms/line
Read banner.txt line by line with a 40ms sleep between each, then
clear and redraw the bordered gum-style version. 5-line banner ×
40ms = 200ms total reveal — slow enough to land an aesthetic on the
first frame, fast enough that the operator never feels it as lag.

Pure cosmetic; no functional change to the install flow.
2026-05-06 10:31:02 +01:00
veilor-org
63c5e199d9 fix(installer): swap gum input --password for bash read -srp
`gum input --password` corrupts the linux fbcon since v0.5.27 — the
bubbletea screen-restore writes back the previous menu buffer because
the framebuffer terminfo entry lacks `civis/cnorm` cursor-hide
sequences, leaving a duplicate "Install" plus a stray "T" rendered on
top of the password field. The fix is a single termios echo-off via
`read -srp`: no redraw, no glitch, no dependency on gum's TUI layer
for the one screen where it broke.

Header still rendered through `gum style` so visual parity with the
disk picker / confirm box is preserved. Whiptail fallback path
unchanged (passwordbox there has always rendered cleanly).
2026-05-06 10:30:06 +01:00
veilor-org
abb67841f1 docs: STRATEGY.md — primary git host moved to git.s8n.ru (Forgejo)
Self-hosted Forgejo + forgejo-runner on nullstone now primary.
GitHub becomes public mirror (Forgejo push-mirrors every commit
+ every 8h). 0 GH Actions minutes consumed.

Runner labels:
  ubuntu-24.04 — drop-in for existing build-iso.yml workflow
  nullstone    — privileged Fedora 43 (opt-in via runs-on: nullstone)

Deploy artifacts: ~/ai-lab/nullstone-server/forgejo/.

External TODO (parent operator owns):
  - router port-forward 222 → nullstone:222 for public SSH push
  - no-guest@file allowlist update for external web UI access
2026-05-06 02:01:06 +01:00
veilor-org
b86b4f9ec3 v0.5.32: ship 7 blockers from 9-agent wave
Per docs/research/2026-05-05-agent-wave/README.md priority list.
All 7 land together to keep iteration cycles useful — partial fixes
bury the lookahead findings agents already mapped.

## 1. CRITICAL — suspend/resume wifi death (Agent 9, B2)

`veilor-modules-lock.service` runs `kernel.modules_disabled=1` 30s
after graphical.target. iwlwifi/iwlmvm/cfg80211 reload on resume
from S3/S0ix → with modules locked, resume breaks wifi until
reboot. Same architectural class as the LUKS bug — security feature
breaks legitimate kernel state transitions.

The unit already has `ConditionKernelCommandLine=!module.sig_enforce=1`
(self-skip when signed-modules enforcement is on cmdline). Adding
`module.sig_enforce=1` to the kernel cmdline retains the security
property (no unsigned modules) without runtime lock-down → resume
works.

Files: kickstart/veilor-os.ks line 61 + overlay/usr/local/bin/veilor-installer
generated bootloader directive both gain `module.sig_enforce=1`.

## 2. veilor-firstboot.service WantedBy=graphical.target (Agent 2)

Was `WantedBy=multi-user.target` only. Real installs default to
graphical.target so the unit never ran on installed systems — admin
pw stayed at install-time + chage -d 0 expired, SDDM PAM bounced
to chauthtok screen (recoverable but ugly UX).

Now `WantedBy=graphical.target multi-user.target`. Live ISO +
multi-user installs both resolve via this list.

## 3. USBGuard hash → id-based baseline (Agent 9, A3)

Mirrors memory feedback_usbguard_dock.md — onyx had hash+parent-hash
rules that broke on dock replug; we shipped no rules.conf so first
boot blocks the USB keyboard.

Adds overlay/etc/usbguard/rules.conf with HID-class allow rule
(`allow with-interface match-all { 03:*:* }`) — covers every USB
keyboard, mouse, gamepad, fingerprint reader, NFC. Survives dock
replug + kernel-bump vendor renumeration. Mass-storage stays
implicit-block; user explicitly allows post-firstboot via
`ujust veilor-usbguard-enroll` (planned v0.6).

## 4. firewalld trusted zone with tailscale0 pre-bound (Agent 9, D1)

User uses Tailscale daily (memory: project_tailscale_mesh.md).
Default firewalld zone = drop, blocks tailnet traffic on tailscale0.

Adds overlay/etc/firewalld/zones/trusted.xml with
`<interface name="tailscale0"/>`. After `tailscale up` brings the
interface up, NetworkManager dispatcher associates it with the
trusted zone automatically — no user intervention.

Default zone stays drop. Only the tailscale0 interface gets ACCEPT.

## 5. /etc/skel branding (Agent 7)

Was completely empty. Result: per-user KDE config (~/.config/kdeglobals
etc.) pre-empty, so the moment user opened System Settings, KDE wrote
fresh ~/.config/* and silently shadowed our /etc/xdg/kdedefaults/*.
Visual brand evaporated on first click.

Seeds:
  /etc/skel/.config/kdeglobals    (copy of assets/kde/veilor-default.kdeglobals)
  /etc/skel/.config/breezerc      (copy of assets/kde/breezerc)
  /etc/skel/.config/kwinrc        (Plasma 6 wayland defaults: opengl, animspeed=0,
                                    blur off, click-to-focus)
  /etc/skel/.config/konsolerc     (default profile = Veilor)
  /etc/skel/.local/share/konsole/Veilor.profile + .colorscheme

User who opens System Settings now writes against branded baseline,
not against vanilla Breeze.

## 6. KMS modeset args + initramfs keymap (Agents 1 + 9)

Real laptop boot has a 5-15s blank between vt switch and SDDM start
because simpledrm releases before i915/nvidia-drm/amdgpu claim. Plus
non-US users get locked out at LUKS prompt because initramfs ships
en-US keymap by default (RHBZ 1405539, RHBZ 1890085).

Adds to bootloader cmdline (live + installed):
  i915.modeset=1 amdgpu.modeset=1 nvidia-drm.modeset=1
  rd.vconsole.keymap=us

`rd.vconsole.keymap=us` is a placeholder; the v0.6 firstboot keymap
picker will rewrite it from /etc/vconsole.conf. Until then, en-US
users get correct LUKS keyboard; non-US users still need the v0.6
fix (per Agent 1).

## 7. virtio-9p log capture (Agent 6)

The v0.5.30 virtio-serial wiring depends on rsyslog inside the live
ISO (anaconda's setupVirtio writes a rsyslog forward rule), which
the live ks doesn't install — files were 0-byte across three
install runs.

test/run-vm.sh now adds a `-virtfs local,...,mount_tag=hostlogs`
share pointing at `test/test-runs/<timestamp>/`. veilor-installer
runs `_dump_logs_to_host` via EXIT trap that mounts the share at
/mnt/hostlogs and rsyncs /tmp/{anaconda,program,storage,packaging,dnf}.log
+ /var/log/veilor-installer.log + dmesg + journalctl + the generated
ks. Runs on success AND failure AND ^C.

No-op on real hardware (9p tag absent) — VM-only debug.

## Validate

  bash -n overlay/usr/local/bin/veilor-installer  # OK
  ksvalidator kickstart/veilor-os.ks               # clean

## Out-of-scope for v0.5.32 (deferred to v0.6)

Per Agent 1 follow-ups: argon2id retune for slow CPUs, recovery key
generation in firstboot, TPM2/FIDO2 unlock helpers. Per Agent 9
follow-ups: Plasma Wayland fallback X11 install, lid-close handling,
SELinux relabel progress UX. Per Agent 4: AppArmor stack +
nftables preset + audit log shipping CLI.

Per Agent 8 (CI hardening): SHA-pin actions + dependabot + SBOM +
SLSA L3 attestation — separate workflow-only commit.
2026-05-05 15:36:24 +01:00
22 changed files with 85 additions and 1539 deletions

View file

@ -1,265 +0,0 @@
name: Build veilor-os OCI (BlueBuild)
# v0.7 spike — builds the bootable OCI image used by the bootstrap
# kickstart's `ostreecontainer` directive. Runs on the Forgejo
# self-hosted runner (label `nullstone`); GitHub-side cosign/SBOM/
# attest steps are gated off because Forgejo has no Sigstore Fulcio-
# trusted OIDC issuer (see docs/PROOF-OF-WORK.md, build-iso.yml fix).
#
# Reference: https://blue-build.org/how-to/setup-build-action/
on:
push:
branches: [v0.7-bluebuild-spike]
paths:
- 'bluebuild/**'
- 'overlay/**'
- 'assets/**'
- 'scripts/**'
- '.github/workflows/build-bluebuild.yml'
pull_request:
branches: [main, v0.7-bluebuild-spike]
schedule:
# Rebuild weekly so we pick up upstream secureblue + Fedora updates.
- cron: '0 6 * * 1'
workflow_dispatch:
permissions:
contents: read
jobs:
build:
name: Build + push OCI
# nullstone label resolves to veilor-build:43 (fedora43 + nodejs)
# via runner config. Privileged + userns=host + sock pass-through
# already wired in the runner config (see infra/forgejo/).
runs-on: nullstone
timeout-minutes: 60
permissions:
contents: read
packages: write
id-token: write # for GH-only cosign keyless (skipped on Forgejo)
attestations: write
env:
# Forgejo container registry path. PAT in FORGEJO_REGISTRY_TOKEN
# secret has package:write on veilor-org.
FORGEJO_REGISTRY: git.s8n.ru
FORGEJO_IMAGE: git.s8n.ru/veilor-org/veilor-os
OCI_TAG: "43"
# GH parallel target — only used when run on github.com.
GHCR_IMAGE: ghcr.io/${{ github.repository_owner }}/veilor-os
steps:
- name: Checkout
# Pinned to last v4 tag confirmed to ship on node20.
uses: actions/checkout@v4.1.7
- name: Fix sudo perms (userns=host artefact)
run: |
# Daemon has userns-remap=default; the act job container is
# launched with --userns=host. The image was pulled under
# remap so /etc/sudo.conf + /etc/sudoers ship as uid 100000.
# sudo refuses to read either unless owned by uid 0. Restore.
chown -R 0:0 /etc/sudo.conf /etc/sudoers /etc/sudoers.d 2>/dev/null || true
ls -la /etc/sudo.conf /etc/sudoers 2>&1 | head -5
- name: Install build tooling (Fedora)
run: |
set -euxo pipefail
dnf -y upgrade --refresh
# veilor-build:43 already ships git, curl, tar, sudo, nodejs.
# cosign is not packaged in Fedora 43; we install it from the
# upstream release tarball below in a separate step.
dnf -y install --skip-unavailable \
podman \
buildah \
skopeo \
jq
# blue-build/github-action shells out to `docker`; Fedora ships
# podman. Symlink so the action finds the CLI.
if ! command -v docker >/dev/null; then
ln -sf "$(command -v podman)" /usr/local/bin/docker
docker --version
fi
- name: Install cosign binary (upstream release)
run: |
set -euxo pipefail
# Fedora 43 has no cosign rpm. Pull static x86_64 binary
# from sigstore/cosign GitHub releases. Pinned to v2.4.1.
COSIGN_VERSION="2.4.1"
curl -fsSL \
"https://github.com/sigstore/cosign/releases/download/v${COSIGN_VERSION}/cosign-linux-amd64" \
-o /usr/local/bin/cosign
chmod +x /usr/local/bin/cosign
cosign version
- name: Pre-pull secureblue base image
env:
GHCR_PULL_TOKEN: ${{ secrets.GHCR_PULL_TOKEN }}
run: |
set -euxo pipefail
# GHCR rate-limits anonymous CI pulls (403 on bearer-token).
# Login with a read-only PAT (forgejo secret GHCR_PULL_TOKEN)
# so bluebuild's buildah inside the CLI container also sees a
# valid auth.json via shared storage bind-mount below.
if [ -n "${GHCR_PULL_TOKEN:-}" ]; then
echo "$GHCR_PULL_TOKEN" | podman login \
--username s8n-ru \
--password-stdin ghcr.io
else
echo "[WARN] GHCR_PULL_TOKEN secret empty; trying anonymous pull"
fi
podman pull ghcr.io/secureblue/kinoite-main-hardened:latest
- name: Stage cosign private key for signing module
env:
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
run: |
set -euo pipefail
if [ -z "${COSIGN_PRIVATE_KEY:-}" ]; then
echo "[ERR] COSIGN_PRIVATE_KEY secret missing"
exit 1
fi
# bluebuild signing module reads from this env var when
# building the cosign.key bind stage. Also write to bluebuild/
# so it sits next to cosign.pub for local reproducible runs.
mkdir -p bluebuild
printf '%s' "$COSIGN_PRIVATE_KEY" > bluebuild/cosign.key
chmod 600 bluebuild/cosign.key
# bluebuild's generated Containerfile uses `FROM scratch as
# stage-keys; COPY cosign.pub /keys/`. Buildah's build context
# is the cwd ($PWD) — symlink the keys to repo root so COPY
# finds them there too.
ln -sf bluebuild/cosign.pub cosign.pub
ln -sf bluebuild/cosign.key cosign.key
ls -la cosign.pub cosign.key 2>&1 | head -4
- name: Build OCI image with BlueBuild CLI container
id: bluebuild
# blue-build/github-action requires docker buildx which podman
# doesn't ship. Run the official BlueBuild CLI container with
# buildah driver instead — works against rootless or rootful
# podman, no docker dependency.
run: |
set -euxo pipefail
# Pull cli image; pinned to v0.9.x at action time.
podman pull ghcr.io/blue-build/cli:latest
# Mount the repo + podman socket; build with buildah driver.
# Bind host /var/lib/containers/storage into the bluebuild
# CLI container so buildah inside it can see the pre-pulled
# secureblue base layer (avoids GHCR auth round-trip during
# templating).
# podman login writes to $XDG_RUNTIME_DIR/containers/auth.json
# by default, which is volatile. Find it + copy to a stable
# path that we then bind into the bluebuild container.
AUTH_SRC=""
for cand in \
"${XDG_RUNTIME_DIR:-/run/user/0}/containers/auth.json" \
"/run/containers/0/auth.json" \
"/root/.config/containers/auth.json" \
"/root/.docker/config.json"; do
if [ -f "$cand" ]; then AUTH_SRC="$cand"; break; fi
done
if [ -z "$AUTH_SRC" ]; then
echo "[ERR] no podman/docker auth.json found post-login"
find / -name auth.json -o -name 'config.json' 2>/dev/null | head -10
exit 1
fi
mkdir -p /root/.config/containers
cp "$AUTH_SRC" /root/.config/containers/auth.json
ls -la /root/.config/containers/auth.json
# Diagnostic: confirm the keypair landed where bluebuild expects.
ls -la bluebuild/
head -1 bluebuild/cosign.pub
head -1 bluebuild/cosign.key | cut -c1-30
podman run --rm \
--privileged \
--security-opt label=disable \
--security-opt seccomp=unconfined \
--entrypoint /usr/bin/bluebuild \
-v "$PWD:/work" \
-v /var/lib/containers/storage:/var/lib/containers/storage \
-v /root/.config/containers/auth.json:/root/.config/containers/auth.json:ro \
-w /work \
-e BB_BUILD_DRIVER=buildah \
ghcr.io/blue-build/cli:latest \
build \
--build-driver buildah \
-vv \
bluebuild/recipe.yml
# bluebuild CLI tags as <recipe-name>:<tag> in local podman
# storage. List + verify, then re-tag for the registries.
podman images
podman tag localhost/veilor-os:latest "${FORGEJO_IMAGE}:${OCI_TAG}" || true
podman tag localhost/veilor-os:latest "${FORGEJO_IMAGE}:latest" || true
- name: Push to Forgejo registry (primary)
if: success() && github.event_name != 'pull_request' && github.server_url != 'https://github.com'
env:
FORGEJO_REGISTRY_TOKEN: ${{ secrets.FORGEJO_REGISTRY_TOKEN }}
FORGEJO_REGISTRY_USER: ${{ secrets.FORGEJO_REGISTRY_USER }}
run: |
set -euo pipefail
if [ -z "${FORGEJO_REGISTRY_TOKEN:-}" ]; then
echo "[WARN] FORGEJO_REGISTRY_TOKEN secret is empty; skipping push"
exit 0
fi
echo "$FORGEJO_REGISTRY_TOKEN" | podman login \
--username "${FORGEJO_REGISTRY_USER:-veilor-org}" \
--password-stdin "$FORGEJO_REGISTRY"
podman push "${FORGEJO_IMAGE}:${OCI_TAG}"
podman push "${FORGEJO_IMAGE}:latest"
echo "[OK] pushed ${FORGEJO_IMAGE}:{${OCI_TAG},latest}"
- name: Push to GHCR (mirror, GitHub-only)
if: success() && github.event_name != 'pull_request' && github.server_url == 'https://github.com'
run: |
set -euo pipefail
podman tag localhost/veilor-os:latest "${GHCR_IMAGE}:${OCI_TAG}"
podman tag localhost/veilor-os:latest "${GHCR_IMAGE}:latest"
echo "${{ secrets.GITHUB_TOKEN }}" | podman login \
--username "${{ github.repository_owner }}" \
--password-stdin ghcr.io
podman push "${GHCR_IMAGE}:${OCI_TAG}"
podman push "${GHCR_IMAGE}:latest"
- name: Smoke-test OCI image
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
run: |
set -euxo pipefail
podman run --rm "localhost/veilor-os:latest" /bin/bash -c '
set -e
echo "-- os-release"
head -5 /etc/os-release
echo "-- sudo present"; which sudo
echo "-- mullvad-browser path"; rpm -q mullvad-browser || echo "not installed"
echo "-- yggdrasil"; rpm -q yggdrasil || echo "not installed"
echo "-- tailscale"; rpm -q tailscale || echo "not installed"
echo "-- veilor-firstboot unit"; ls -la /etc/systemd/system/veilor-firstboot.service 2>&1 || true
'
# ── GitHub-only signing/SBOM/attest ────────────────────────────
# cosign keyless needs Sigstore Fulcio-trusted OIDC. Forgejo
# has none, so these are GH-only. v0.7+ TODO: cosign key-pair
# signing for Forgejo using a stored secret.
- name: SBOM (SPDX, GitHub-only)
if: github.event_name == 'push' && github.server_url == 'https://github.com'
# Pinned to last v0.17 release that ships node20.
uses: anchore/sbom-action@v0.17.2
with:
image: ${{ env.GHCR_IMAGE }}:${{ env.OCI_TAG }}
format: spdx-json
output-file: veilor-os-oci.spdx.json
- name: Build provenance attestation (GitHub-only)
if: github.event_name == 'push' && github.server_url == 'https://github.com'
# Pinned to last v2.2 release that ships node20.
uses: actions/attest-build-provenance@v2.2.3
with:
subject-name: ${{ env.GHCR_IMAGE }}
subject-digest: ${{ steps.bluebuild.outputs.digest }}

View file

@ -1,167 +0,0 @@
name: Build veilor-os Installer ISO
# v0.7+ — produces a small Anaconda installer ISO that consumes
# kickstart/install-ostreecontainer-installer.ks. The ISO boots
# Anaconda, asks for LUKS pw + admin pw interactively, then
# `ostreecontainer` populates / from the v0.7 OCI image at
# ghcr.io/veilor-org/veilor-os:43.
on:
push:
branches: [v0.7-bluebuild-spike]
paths:
- 'kickstart/install-ostreecontainer.ks'
- 'kickstart/install-ostreecontainer-installer.ks'
- 'bluebuild/recipe.yml'
- '.github/workflows/build-installer-iso.yml'
workflow_dispatch:
inputs:
releasever:
description: 'Fedora release version'
required: false
default: '43'
permissions:
contents: write # needed to create+update installer-latest release
jobs:
build:
name: Build installer ISO
runs-on: nullstone
timeout-minutes: 120
env:
RELEASEVER: ${{ github.event.inputs.releasever || '43' }}
steps:
- name: Checkout
uses: actions/checkout@v4.1.7
- name: Install build tooling (Fedora)
run: |
set -euxo pipefail
dnf -y upgrade --refresh
dnf -y install --skip-unavailable \
lorax \
pykickstart \
anaconda-tui \
syslinux \
xorriso \
grub2-efi-x64 \
grub2-efi-x64-modules \
grub2-pc \
grub2-pc-modules \
shim-x64 \
efibootmgr
- name: Validate installer kickstart
run: |
set -euxo pipefail
ksvalidator kickstart/install-ostreecontainer-installer.ks
- name: Build installer ISO with livemedia-creator
run: |
set -euxo pipefail
# livemedia-creator refuses an existing non-empty resultdir.
rm -rf build/out
mkdir -p /var/lmc
ln -sfn "$GITHUB_WORKSPACE" /work
# livemedia-creator does NOT support --title (that's livecd-creator).
# --volid replaces it for the ISO volume label.
livemedia-creator \
--make-iso \
--no-virt \
--ks kickstart/install-ostreecontainer-installer.ks \
--resultdir build/out \
--tmp /var/lmc \
--volid "veilor-os-installer-${RELEASEVER}" \
--project "veilor-os" \
--releasever "$RELEASEVER" \
--logfile build/out/build.log \
2>&1 | tee -a build/out/build.log
- name: Rename ISO + sha256
run: |
set -euxo pipefail
ISO_FILE=$(ls build/out/*.iso 2>/dev/null | head -1)
[ -n "$ISO_FILE" ] || { echo "[ERR] no ISO produced"; exit 1; }
ISO_NAME="veilor-os-installer-${RELEASEVER}-$(date +%Y%m%d-%H%M%S).iso"
mv "$ISO_FILE" "build/out/$ISO_NAME"
cd build/out
sha256sum "$ISO_NAME" > "$ISO_NAME.sha256"
ls -lh "$ISO_NAME"
- name: Split ISO into 1900M chunks
if: success() && github.ref == 'refs/heads/v0.7-bluebuild-spike'
run: |
set -euo pipefail
cd build/out
ISO=$(ls *.iso | head -1)
[ -n "$ISO" ] || { echo "[ERR] no ISO"; exit 1; }
split -b 1900M -d --suffix-length=2 "$ISO" "${ISO}.part-"
rm -f "$ISO"
sha256sum *.part-* > "${ISO}.parts.sha256"
ls "${ISO}".part-*
- name: Publish to installer-latest rolling prerelease (Forgejo)
if: success() && github.ref == 'refs/heads/v0.7-bluebuild-spike' && github.server_url != 'https://github.com'
env:
FORGEJO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
FORGEJO_API: ${{ github.server_url }}/api/v1
REPO: ${{ github.repository }}
GIT_SHA: ${{ github.sha }}
run: |
set -euo pipefail
TAG="installer-latest"
REL_JSON=$(curl -fsSL -H "Authorization: token ${FORGEJO_TOKEN}" \
"${FORGEJO_API}/repos/${REPO}/releases/tags/${TAG}" 2>/dev/null || echo "")
if [ -n "$REL_JSON" ]; then
REL_ID=$(echo "$REL_JSON" | grep -oE '"id":\s*[0-9]+' | head -1 | grep -oE '[0-9]+')
if [ -n "$REL_ID" ]; then
curl -fsSL -X DELETE -H "Authorization: token ${FORGEJO_TOKEN}" \
"${FORGEJO_API}/repos/${REPO}/releases/${REL_ID}" || true
curl -fsSL -X DELETE -H "Authorization: token ${FORGEJO_TOKEN}" \
"${FORGEJO_API}/repos/${REPO}/git/refs/tags/${TAG}" || true
fi
fi
BODY="Rolling auto-build from v0.7-bluebuild-spike. Latest commit: ${GIT_SHA}.
Installer ISO — boots Anaconda, prompts for LUKS pw + admin pw,
then ostreecontainer-pulls / from ghcr.io/veilor-org/veilor-os:43.
Reassemble:
cat veilor-os-installer-*.iso.part-* > veilor-os-installer.iso
sha256sum -c veilor-os-installer-*.iso.parts.sha256
Not a stable release — for testing only."
PAYLOAD=$(BODY="$BODY" TAG="$TAG" python3 -c "
import json,os
print(json.dumps({
'tag_name': os.environ['TAG'],
'target_commitish': 'v0.7-bluebuild-spike',
'name': 'installer-latest (auto)',
'body': os.environ['BODY'],
'prerelease': True,
'draft': False,
}))")
REL_ID=$(curl -fsSL -X POST -H "Authorization: token ${FORGEJO_TOKEN}" \
-H "Content-Type: application/json" \
-d "$PAYLOAD" \
"${FORGEJO_API}/repos/${REPO}/releases" | \
grep -oE '"id":\s*[0-9]+' | head -1 | grep -oE '[0-9]+')
[ -n "$REL_ID" ] || { echo "[ERR] failed to create release"; exit 1; }
cd build/out
for f in *.iso.part-* *.sha256; do
[ -f "$f" ] || continue
curl -fsSL -X POST -H "Authorization: token ${FORGEJO_TOKEN}" \
-F "attachment=@${f}" \
"${FORGEJO_API}/repos/${REPO}/releases/${REL_ID}/assets?name=${f}"
done
- name: Print build log on failure
if: failure()
run: |
echo "─── build/out/build.log ───"
tail -200 build/out/build.log 2>/dev/null || echo "(no build.log)"
find build/out -name 'program.log' -exec tail -100 {} \; 2>/dev/null || true
find /var/lmc -name '*.log' -exec tail -50 {} \; 2>/dev/null || true

View file

@ -1,122 +0,0 @@
name: Smoke-test veilor-os OCI
# Pulls git.s8n.ru/veilor-org/veilor-os:43 and asserts that the image
# contains the veilor brand + the v0.5.x hardening overlay + the v0.7
# CLI tools, and that cosign verifies it against bluebuild/cosign.pub.
on:
workflow_run:
workflows: ["Build veilor-os OCI (BlueBuild)"]
types: [completed]
workflow_dispatch:
permissions:
contents: read
jobs:
smoke:
name: OCI smoke test
runs-on: nullstone
if: >-
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'workflow_run' &&
github.event.workflow_run.conclusion == 'success')
timeout-minutes: 20
env:
IMAGE: git.s8n.ru/veilor-org/veilor-os:43
steps:
- name: Checkout
uses: actions/checkout@v4.1.7
- name: Fix sudo perms
run: chown -R 0:0 /etc/sudo.conf /etc/sudoers /etc/sudoers.d 2>/dev/null || true
- name: Install podman + cosign
run: |
set -euxo pipefail
command -v podman >/dev/null || dnf -y install --skip-unavailable podman
if ! command -v cosign >/dev/null 2>&1; then
curl -fsSL "https://github.com/sigstore/cosign/releases/download/v2.4.1/cosign-linux-amd64" \
-o /usr/local/bin/cosign
chmod +x /usr/local/bin/cosign
fi
podman --version
cosign version
- name: Login + pull OCI image
env:
FORGEJO_REGISTRY_TOKEN: ${{ secrets.FORGEJO_REGISTRY_TOKEN }}
FORGEJO_REGISTRY_USER: ${{ secrets.FORGEJO_REGISTRY_USER }}
run: |
set -euxo pipefail
if [ -n "${FORGEJO_REGISTRY_TOKEN:-}" ]; then
echo "$FORGEJO_REGISTRY_TOKEN" | podman login \
--username "${FORGEJO_REGISTRY_USER:-veilor-org}" \
--password-stdin git.s8n.ru
fi
podman pull "${IMAGE}"
- name: Verify cosign signature
run: |
set -euo pipefail
[ -f bluebuild/cosign.pub ] || { echo "[ERR] bluebuild/cosign.pub missing"; exit 1; }
cosign verify --key bluebuild/cosign.pub "${IMAGE}" 2>&1 | tail -10
- name: Run OCI assertions
run: |
set -uo pipefail
PASS=0; FAIL=0; ERRORS=""
pass() { echo "[PASS] $1"; PASS=$((PASS+1)); }
fail() { echo "[FAIL] $1"; FAIL=$((FAIL+1)); ERRORS="${ERRORS} - $1\n"; }
img() { podman run --rm "${IMAGE}" /bin/bash -c "$1" 2>/dev/null; }
OS=$(img 'cat /etc/os-release 2>/dev/null')
echo "$OS" | grep -q 'ID=veilor' && pass "ID=veilor" || fail "ID=veilor missing"
echo "$OS" | grep -q 'NAME="veilor-os"' && pass 'NAME="veilor-os"' || fail 'NAME="veilor-os" missing'
img 'which sudo' >/dev/null && pass "sudo present" || fail "sudo missing"
img 'rpm -q mullvad-browser' >/dev/null && pass "mullvad-browser present" || fail "mullvad-browser missing"
img 'rpm -q tailscale' >/dev/null && pass "tailscale present" || fail "tailscale missing"
img 'rpm -q yggdrasil' >/dev/null && pass "yggdrasil present" || fail "yggdrasil missing"
if img 'grep -qi "^SELINUX=enforcing" /etc/selinux/config'; then
pass "SELinux config = enforcing"
else
fail "SELinux not enforcing"
fi
img 'test -e /etc/systemd/system/multi-user.target.wants/veilor-firstboot.service \
-o -e /etc/systemd/system/graphical.target.wants/veilor-firstboot.service' >/dev/null \
&& pass "veilor-firstboot enabled" || fail "veilor-firstboot not enabled"
img 'test -e /etc/systemd/system/multi-user.target.wants/veilor-postinstall.service \
-o -e /etc/systemd/system/graphical.target.wants/veilor-postinstall.service' >/dev/null \
&& pass "veilor-postinstall enabled" || fail "veilor-postinstall not enabled"
for b in veilor-power veilor-update veilor-doctor veilor-postinstall; do
img "test -x /usr/local/bin/${b}" >/dev/null \
&& pass "${b} executable" || fail "${b} missing"
done
if img 'ls /usr/share/veilor-os/scripts/ 2>/dev/null' | grep -qE '(10-harden|20-harden|30-apply)'; then
pass "/usr/share/veilor-os/scripts populated"
else
fail "/usr/share/veilor-os/scripts missing"
fi
LEAKS=$(img "grep -rIni 'onyx\|192\.168\.0\.\|fedora\.local\|xynki\.dev' /etc/veilor* /usr/share/veilor-os 2>/dev/null")
if [ -z "$LEAKS" ]; then
pass "no brand leaks"
else
fail "brand leaks found"
echo "$LEAKS"
fi
echo
echo "═══ ${PASS} passed, ${FAIL} failed ═══"
if [ "$FAIL" -gt 0 ]; then
printf "%b" "$ERRORS"
exit 1
fi
echo "✓ veilor-os:43 smoke test passed"

1
.gitignore vendored
View file

@ -16,4 +16,3 @@ test/veilor-vm.nvram*
test/auto-install-vm.qcow2 test/auto-install-vm.qcow2
test/auto-install-vm.nvram* test/auto-install-vm.nvram*
.claude/worktrees/ .claude/worktrees/
**/cosign.key

View file

@ -11,18 +11,6 @@ future maintainers can see why a change exists, not just what it changes.
## [Unreleased] ## [Unreleased]
### v0.7 BlueBuild OCI spike (active)
- Promote `v0.7-bluebuild-spike` to active mainline; v0.6 cancelled.
- Port `build-bluebuild.yml` to the Forgejo runner (`runs-on: nullstone`):
install BlueBuild CLI in-job, push to `git.s8n.ru/veilor-org/veilor-os`,
gate cosign keyless / SBOM / attest steps to GitHub-only.
- Atomic CLI tools: `veilor-update` rewritten on `bootc upgrade`,
new `veilor-postinstall` first-login TUI, `veilor-doctor` learns
`bootc status --json` while keeping the legacy dnf path.
- Docs: `docs/INSTALL-V07.md`, `docs/STRATEGY.md` PIVOT EXECUTION
section, README quick-install rewritten for v0.7.
### Planned ### Planned
- v0.3 polish — Plymouth black theme, SDDM theme, Konsole profile, - v0.3 polish — Plymouth black theme, SDDM theme, Konsole profile,

View file

@ -48,46 +48,26 @@ spike at v0.7**, **bootc-only at v1.0**.
--- ---
## Quick install — v0.7+ (recommended, atomic / OCI) ## Quick install
```bash ```bash
# 1. Download the bootstrap installer ISO from Forgejo. # 1. Download the ISO from the latest Forgejo release.
# https://git.s8n.ru/veilor-org/veilor-os/releases/tag/ci-latest # https://git.s8n.ru/veilor-org/veilor-os/releases/tag/ci-latest
# (rolling tag; replaced on each successful build-iso.yml run)
sha256sum -c veilor-os-43-*.iso.sha256 sha256sum -c veilor-os-43-*.iso.sha256
# 2. Flash to USB. Replace /dev/sdX — triple-check. # 2. Flash to USB. Replace /dev/sdX with your USB device — triple-check.
sudo dd if=veilor-os-43-*.iso of=/dev/sdX bs=4M status=progress conv=fsync sudo dd if=veilor-os-43-*.iso of=/dev/sdX bs=4M status=progress conv=fsync
sync sync
# 3. Boot from USB. Anaconda asks for LUKS passphrase + admin password. # 3. Boot from USB, pick "Install veilor-os" from the menu.
# Anaconda then runs `ostreecontainer --url=git.s8n.ru/veilor-org/veilor-os:43` # 4. Set a strong LUKS passphrase — the only prompt during install.
# which populates / from the signed BlueBuild OCI image. # 5. Reboot, remove USB.
# 6. On first boot: TTY prompts for an admin password (≥14 chars, mixed case,
# 4. Reboot. Log in as `admin`. The first-login TUI (veilor-postinstall) # digit, symbol). Once accepted, SDDM starts. Log in as `admin`.
# asks for the small set of decisions we defer from install:
# keyboard, locale, hostname, GPU drivers, package presets,
# bluetooth, USBGuard policy snapshot. Each step skippable.
# 5. Day-to-day: `sudo veilor-update` (atomic, A/B, instant rollback).
``` ```
Full v0.7 walkthrough: [docs/INSTALL-V07.md](docs/INSTALL-V07.md). Full install + first-boot walkthrough: [docs/INSTALL.md](docs/INSTALL.md).
---
### Legacy v0.5.0 install (kickstart-flat path)
The kickstart-installed v0.5.0 ISO ships as a frozen proof-of-work
release. Same hardening, no bootc/rpm-ostree atomic layer. Updates
go through `dnf upgrade` instead of `bootc upgrade`.
```bash
# Same flash + boot, then pick "Install veilor-os".
# Single LUKS passphrase prompt during install; admin password set
# on first boot via TTY.
```
Walkthrough: [docs/INSTALL.md](docs/INSTALL.md).
--- ---
@ -167,7 +147,7 @@ clean, locked down, with no manual post-install hardening required.
[secureblue](https://github.com/secureblue/secureblue) is an upstream [secureblue](https://github.com/secureblue/secureblue) is an upstream
hardened atomic Fedora project we benchmark against and plan to **build hardened atomic Fedora project we benchmark against and plan to **build
on top of** at v0.7. The v0.7 BlueBuild spike uses their on top of** at v0.7. The v0.7 BlueBuild spike uses their
`kinoite-main-hardened` OCI image as its base — we don't `securecore-kinoite-hardened-userns` OCI image as its base — we don't
ship their source code in this repo, we layer veilor branding, ship their source code in this repo, we layer veilor branding,
theming, the gum installer, and the kickstart bootstrap on top of theming, the gum installer, and the kickstart bootstrap on top of
their already-signed image. their already-signed image.

View file

@ -1,96 +0,0 @@
# bluebuild/ — v0.7 spike
This directory contains the BlueBuild recipe + supporting config that
builds the veilor-os bootable OCI image. **Active on the
`v0.7-bluebuild-spike` branch only.** Does NOT land in v0.5.x main
until the spike passes its success criteria (see
`docs/STRATEGY.md`).
## What's here
```
bluebuild/
├── recipe.yml # primary BlueBuild recipe
├── config/
│ └── just/
│ └── 60-veilor.just # ujust recipes for opt-in components
└── README.md # this file
```
The recipe extends
`ghcr.io/secureblue/kinoite-main-hardened:latest`. We
inherit secureblue's hardening (sysctl + kargs + custom SELinux
policy + USBGuard + hardened-malloc + Unbound DoT + chronyd NTS +
Trivalent browser + cosign-signed image chain). On top, we layer:
- veilor branding (overlay/, theme, plymouth, sddm, os-release)
- mullvad-browser (anti-fingerprint companion to Trivalent)
- xorg-x11-server-Xwayland (re-enable; secureblue disables it)
- sudo (re-enable; secureblue replaces with run0)
- tailscale + yggdrasil (mesh stack layer 1 + 2)
- ujust recipes for Reticulum (mesh layer 3) + Thorium (opt-in browser)
Trivalent stays as the default browser (correcting an earlier draft).
## Build locally
```bash
# Requires bluebuild CLI:
# curl -fsSL https://raw.githubusercontent.com/blue-build/cli/main/install.sh | sh
cd bluebuild
bluebuild build recipe.yml
```
Output: `localhost/veilor-os:43` in podman storage. Push to GHCR
via the workflow.
## Test the OCI image
```bash
# Smoke-test (boots into the rootfs; no kernel, no init):
podman run --rm -it ghcr.io/veilor-org/veilor-os:43 /bin/bash
# Inside, sanity:
cat /etc/os-release # PRETTY_NAME=veilor-os
which sudo # /usr/bin/sudo (re-enabled)
which trivalent # secureblue's COPR (default browser)
which mullvad-browser # /usr/bin/mullvad-browser
systemctl is-enabled yggdrasil # enabled (idle)
systemctl is-enabled tailscaled # disabled (awaits ujust veilor-mesh-join)
```
## Test the installer ISO
The installer ISO is built separately by livecd-creator (current path)
or bootc-image-builder (v1.0+). Its kickstart's `%packages` block is
replaced with:
```
ostreecontainer --url=ghcr.io/veilor-org/veilor-os:43 --transport=registry
```
That populates the target's `/` directly from this OCI image during
the install pass. No first-boot rebase. No transition window.
## Spike success criteria (1 day)
- [ ] `bluebuild build recipe.yml` exits 0
- [ ] `bootc container lint` exits 0 on the resulting image
- [ ] `podman run` smoke-test (commands above) all pass
- [ ] `.github/workflows/build-bluebuild.yml` builds + cosign-signs +
pushes to `ghcr.io/veilor-org/veilor-os:43`
- [ ] An installer ISO using `ostreecontainer` against this OCI
reaches SDDM with admin login on first boot
If all five land, merge `v0.7-bluebuild-spike``main` as v0.7.0.
If any fail in ways that aren't trivially fixable, file each as a GH
issue + return to v0.5.x kickstart path.
## See also
- `docs/STRATEGY.md` — the strategic decision + override list
- `docs/ROADMAP.md` v0.7 — full schedule
- `docs/THREAT-MODEL.md` — what we publish before launch
- secureblue: <https://github.com/secureblue/secureblue>
- BlueBuild: <https://blue-build.org>
- bootc / ostreecontainer: <https://docs.fedoraproject.org/en-US/bootc/>

View file

@ -1,73 +0,0 @@
# veilor-os ujust recipes — opt-in components
# Loaded into /usr/share/ublue-os/just/ at image build time;
# `ujust install-X` discovers + dispatches.
# install Reticulum / RetiNet AGPL fork + Sideband (mesh layer 3)
install-reticulum:
#!/usr/bin/env bash
echo "═══ Reticulum (RetiNet AGPL fork) install ═══"
echo
echo "Installs RetiNet (AGPL fork — NOT upstream RNS due to anti-AI"
echo "license) plus Sideband messenger. Default config: AutoInterface"
echo "(LAN multicast) + 1-2 TCP backbone peers. RNode hardware (LoRa"
echo "transceiver) is a separate install."
echo
read -p "Proceed? [y/N]: " confirm
if [[ "$confirm" != "y" ]]; then echo "Cancelled."; exit 0; fi
rpm-ostree install python3-pip
pip install --user retinet sideband-cli
echo
echo "Done. To attach an RNode (LoRa transceiver), run:"
echo " ujust install-reticulum-rnode"
# install Reticulum RNode hardware support (LoRa transceiver)
install-reticulum-rnode:
#!/usr/bin/env bash
echo "═══ RNode (LoRa transceiver) hardware install ═══"
echo
echo "Adds RNode firmware-update tooling + udev rules for the LoRa"
echo "USB hardware. Required only if you have an RNode device."
echo
read -p "Proceed? [y/N]: " confirm
if [[ "$confirm" != "y" ]]; then echo "Cancelled."; exit 0; fi
pip install --user rnodeconf
echo "Done. Plug in your RNode via USB; it will appear as a serial device."
# install Thorium browser (OPT-IN, with explicit CVE-lag warning)
install-thorium:
#!/usr/bin/env bash
echo "═══ Thorium browser install ═══"
echo
echo "WARNING: Thorium is a perf/media-focused fork of Chromium that"
echo "uses LTS Chromium as its base. As of 2026-05 it lags upstream"
echo "stable by ~9 milestones (months of CVE backlog)."
echo
echo "veilor-os ships Trivalent (secureblue's hardened Chromium fork,"
echo "tracking upstream M147+ within hours) as the default browser."
echo "Thorium is provided as an OPT-IN profile for users who"
echo "explicitly need its perf characteristics (e.g. WebGL games,"
echo "media decode profiles)."
echo
echo "DO NOT use Thorium as your daily-driver browser. Use Trivalent"
echo "or Mullvad Browser for that."
echo
read -p "Acknowledge CVE-lag risk and continue? [y/N]: " confirm
if [[ "$confirm" != "y" ]]; then echo "Cancelled."; exit 0; fi
flatpak install --user -y org.thorium.Thorium 2>/dev/null || \
rpm-ostree install thorium-browser
echo "Done. Launch via Plasma menu or `flatpak run org.thorium.Thorium`."
# join the veilor mesh (Tailscale via Headscale)
veilor-mesh-join:
#!/usr/bin/env bash
echo "═══ Join veilor mesh (Tailscale via Headscale) ═══"
echo
echo "Pre-auth keys are minted by the Misskey signup page at"
echo "x.veilor (TTL 24h, single-use). You can paste the hex key"
echo "directly OR scan the QR code shown after signup."
echo
read -p "Hex key (paste): " preauth
if [[ -z "$preauth" ]]; then echo "Empty key. Cancelled."; exit 0; fi
sudo systemctl enable --now tailscaled
sudo tailscale up --login-server=https://hs.s8n.ru --auth-key="$preauth"
echo "Done. Status: $(sudo tailscale status | head -1)"

View file

@ -1,4 +0,0 @@
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE5xQcyP7FHNSiG7+VLsN2ViWlvvIB
FYmu2XmPah7/VBlmuQ88H0ZbqCqqnS2u9x5+P1OMaMK+//k89V0Blrx65Q==
-----END PUBLIC KEY-----

View file

@ -1,155 +0,0 @@
# veilor-os — BlueBuild recipe (v0.7 spike, 1-day target)
#
# Extends secureblue's hardened Kinoite OCI image with veilor branding,
# threat-model-driven UX choices, and the three-layer mesh stack
# (Tailscale + Yggdrasil + opt-in Reticulum). This is the OCI image
# that the v0.7+ kickstart's `ostreecontainer` directive pulls into
# the target root during the install pass.
#
# Build: bluebuild build recipe.yml
# Test: podman run --rm -it ghcr.io/veilor-org/veilor-os:43 /bin/bash
# CI: .github/workflows/build-bluebuild.yml signs + pushes to GHCR.
#
# Reference: https://blue-build.org/reference/recipe/
---
name: veilor-os
description: Hardened security-branded Fedora KDE on top of secureblue.
# Base image: secureblue's hardened Kinoite variant with userns sandboxing.
# That brings in: sysctl + kargs + custom SELinux policy + USBGuard +
# hardened-malloc + Unbound DoT + chronyd NTS + Trivalent browser.
base-image: ghcr.io/secureblue/kinoite-main-hardened
image-version: latest
modules:
# ── 1. veilor branding overlay ──────────────────────────────────
# `type: copy` is a low-level direct COPY (no chmod, no script).
# `type: files` was failing with `chmod: Operation not permitted` on
# the BlueBuild-shipped /tmp/modules/files/files.sh under buildah +
# podman privileged in our runner — the script tries to make itself
# executable inside its own bind-mounted layer.
- type: copy
source: ../overlay
destination: /
- type: copy
source: ../assets
destination: /usr/share/veilor-os/assets
- type: copy
source: ../scripts
destination: /usr/share/veilor-os/scripts
# ── 2. Branding overrides at build time ─────────────────────────
- type: script
snippets:
- |
# os-release brand
sed -i \
-e 's|^GRUB_DISTRIBUTOR=.*|GRUB_DISTRIBUTOR="veilor-os"|' \
/etc/default/grub 2>/dev/null || true
# Apply our kde-theme + plymouth in build
bash /usr/share/veilor-os/scripts/kde-theme-apply.sh || true
bash /usr/share/veilor-os/scripts/30-apply-v03-theme.sh 2>/dev/null || true
plymouth-set-default-theme details 2>/dev/null || true
# Mark all our shipped scripts + CLIs executable. cp -a from the
# repo preserves perms but BlueBuild's `type: files` sometimes
# drops the +x bit on the way through; belt-and-braces here.
chmod +x /usr/share/veilor-os/scripts/*.sh \
/usr/share/veilor-os/scripts/selinux/*.sh \
/usr/local/bin/veilor-* 2>/dev/null || true
# Refresh fontconfig cache so Fira Code is picked up by KDE
fc-cache -f 2>/dev/null || true
# os-release brand override (atomic /etc is r/w; safe to overwrite)
if [ -f /etc/os-release ]; then
sed -i \
-e 's|^NAME=.*|NAME="veilor-os"|' \
-e 's|^PRETTY_NAME=.*|PRETTY_NAME="veilor-os 0.7 (atomic)"|' \
-e 's|^ID=.*|ID=veilor|' \
-e 's|^ID_LIKE=.*|ID_LIKE="fedora kinoite"|' \
/etc/os-release || true
fi
# Sanity: brand-leak check, fail build if any onyx/personal data slipped in
if grep -rqi 'onyx\|192\.168\.0\.\|fedora\.local\|xynki\.dev' \
/etc/veilor* /etc/tuned/profiles/veilor-* /usr/share/veilor-os 2>/dev/null; then
echo "[ERR] brand leak detected in shipped state"
exit 1
fi
# ── 3. Override secureblue's run0-only — restore sudo ───────────
# secureblue removes sudo + replaces with run0. Too disruptive for
# daily-driver workflows. Restore sudo, keep run0 available.
- type: rpm-ostree
install:
- sudo
# ── 4. Re-enable Xwayland ───────────────────────────────────────
# secureblue disables Xwayland for attack-surface reduction. Some
# apps (Element, Slack-likes, older Qt5 tools) still need it.
# User who wants it removed back can `rpm-ostree override remove`.
- type: rpm-ostree
install:
- xorg-x11-server-Xwayland
# ── 5. Mullvad Browser as anti-fingerprint companion ────────────
# Layered alongside Trivalent (kept as default per STRATEGY.md).
# Trivalent for daily browsing, Mullvad for pseudonymous browsing.
# Thorium remains opt-in only via `ujust install-thorium` — see
# config/thorium.just for the warning + install logic.
- type: rpm-ostree
install:
- mullvad-browser
# ── 6. Mesh stack packages ──────────────────────────────────────
# Layer 1 (Day 1 daily driver, service pre-disabled): Tailscale
# Layer 2 (Day 1 idle warm-fallback): Yggdrasil-go
# Layer 3 (opt-in via ujust): Reticulum / RetiNet — handled in just/
- type: rpm-ostree
install:
- tailscale
- yggdrasil
# ── 6b. Memory hygiene + ergonomic deps ─────────────────────────
# zram-generator gives us zram swap (no disk swap, no cold-boot
# leak). gum is the TUI primitive used by veilor-postinstall +
# veilor-update + veilor-doctor — vendor binary at build time so
# post-install layering doesn't need it.
- type: rpm-ostree
install:
- zram-generator
- jq
- vim-enhanced
- tmux
- htop
# ── 7. ujust recipes for opt-in components ──────────────────────
- type: copy
source: config/just
destination: /usr/share/ublue-os/just
# ── 8. Service tuning: tailscale pre-disabled, yggdrasil idle ───
- type: systemd
system:
enabled:
- yggdrasil.service # idle warm-fallback (config = empty Listen[])
disabled:
- tailscaled.service # awaits first-boot prompt for join
# secureblue parents already enable: sshd, fail2ban, usbguard,
# auditd, firewalld, chronyd, sddm — no re-enable needed.
# ── 9. veilor-os specific systemd units ─────────────────────────
# All veilor-* units come in via overlay/etc/systemd/system/ —
# explicit enable here since they aren't part of secureblue's set.
- type: systemd
system:
enabled:
- veilor-firstboot.service
- veilor-modules-lock.service
- veilor-postinstall.service
- veilor-doctor.timer
# ── 10. signing config ──────────────────────────────────────────
# cosign.pub committed alongside this recipe; cosign.key kept off
# repo and provided to CI as Forgejo secret COSIGN_PRIVATE_KEY.
# The action exports it to /tmp at build time.
- type: signing

View file

@ -30,7 +30,7 @@
| Project | Role in veilor-os | | Project | Role in veilor-os |
|---|---| |---|---|
| Fedora 43 KDE | Base OS for v0.5.x kickstart-installed flat builds | | Fedora 43 KDE | Base OS for v0.5.x kickstart-installed flat builds |
| [secureblue](https://github.com/secureblue/secureblue) | Upstream hardened atomic Fedora; v0.7 BlueBuild spike layers our overlay on top of `kinoite-main-hardened` | | [secureblue](https://github.com/secureblue/secureblue) | Upstream hardened atomic Fedora; v0.7 BlueBuild spike layers our overlay on top of `securecore-kinoite-hardened-userns` |
| Kicksecure / Whonix | Reference for AppArmor + apt-transport-tor model (we don't ship Tor; we did read their docs) | | Kicksecure / Whonix | Reference for AppArmor + apt-transport-tor model (we don't ship Tor; we did read their docs) |
| Bluefin / Bazzite (uBlue) | Reference for BlueBuild recipe shape and OCI publishing pattern | | Bluefin / Bazzite (uBlue) | Reference for BlueBuild recipe shape and OCI publishing pattern |
| Tails | Reference for live-only install model — explicitly **not** veilor's path | | Tails | Reference for live-only install model — explicitly **not** veilor's path |
@ -194,7 +194,7 @@ The repo carries more than just an ISO recipe:
| `scripts/selinux/veilor-systemd.te` | Custom SELinux module (targeted policy gap fixes) | | `scripts/selinux/veilor-systemd.te` | Custom SELinux module (targeted policy gap fixes) |
| `scripts/30-apply-v03-theme.sh` | Plymouth + SDDM + Konsole + wallpaper apply | | `scripts/30-apply-v03-theme.sh` | Plymouth + SDDM + Konsole + wallpaper apply |
| `scripts/40-apparmor.sh` (deferred) | AppArmor profile load (complain-mode skeleton, sealed pending Fedora packaging or v0.7 secureblue) | | `scripts/40-apparmor.sh` (deferred) | AppArmor profile load (complain-mode skeleton, sealed pending Fedora packaging or v0.7 secureblue) |
| `bluebuild/recipe.yml` | v0.7 OCI recipe (base = secureblue kinoite-main-hardened) | | `bluebuild/recipe.yml` | v0.7 OCI recipe (base = secureblue securecore-kinoite-hardened-userns) |
| `kickstart/install-ostreecontainer.ks` | v0.7 install ks: 10 lines, just `ostreecontainer --url=ghcr.io/veilor-org/veilor-os:43 --transport=registry` | | `kickstart/install-ostreecontainer.ks` | v0.7 install ks: 10 lines, just `ostreecontainer --url=ghcr.io/veilor-org/veilor-os:43 --transport=registry` |
| `assets/installer/{banner.txt,colors.gum}` | Pure-block VEILOR OS wordmark + branded gum colour palette | | `assets/installer/{banner.txt,colors.gum}` | Pure-block VEILOR OS wordmark + branded gum colour palette |
| `assets/branding/` | Logo, wallpapers, plymouth theme assets | | `assets/branding/` | Logo, wallpapers, plymouth theme assets |

View file

@ -1,138 +0,0 @@
# Installing veilor-os (v0.7+)
> v0.7 is the first OCI / atomic release. The kickstart-installed
> v0.5.x path still ships as legacy — if you want that flow, see
> [INSTALL.md](INSTALL.md). Both paths produce a hardened veilor-os
> system; the v0.7 path is what we recommend going forward.
## What's different from v0.5
| Topic | v0.5.x (kickstart) | v0.7+ (BlueBuild OCI) |
|---|---|---|
| Root filesystem | mutable, `/usr` writable | atomic / immutable, layered via `rpm-ostree` |
| Updates | `sudo dnf upgrade` | `sudo bootc upgrade` (atomic A/B, instant rollback) |
| Adding a package | `sudo dnf install foo` | `sudo rpm-ostree install foo` (layered into next deployment) |
| Base hardening | re-derived in our `%post` scripts | inherited from secureblue OCI image |
| Build artefact | `~2.7 GB` live ISO | small bootstrap ISO + signed OCI image at registry |
## Step-by-step
### 1. Download the bootstrap installer ISO
The bootstrap ISO is a tiny Anaconda-driven installer. It does
nothing more than collect a LUKS passphrase + admin password and
then call `ostreecontainer --url=...:43 --transport=registry` to
populate `/` from the pre-built signed OCI image.
Download from the Forgejo release:
<https://git.s8n.ru/veilor-org/veilor-os/releases/tag/ci-latest>
Reassemble the chunked ISO if needed (legacy artefact format):
```sh
cat veilor-os-*.iso.part-* > veilor-os.iso
sha256sum -c veilor-os-*.iso.parts.sha256
```
### 2. Verify the OCI image signature (optional, recommended)
The OCI image is cosign-signed at build time. If you have `cosign`
installed:
```sh
cosign verify --key cosign.pub git.s8n.ru/veilor-org/veilor-os:43
```
The public key `cosign.pub` ships with the bootstrap ISO and is also
on the Forgejo release page.
### 3. Flash to USB
Replace `/dev/sdX` with your USB device — triple-check the path.
```sh
sudo dd if=veilor-os.iso of=/dev/sdX bs=4M status=progress conv=fsync
sync
```
### 4. Boot from USB
Pick **Install veilor-os** from the boot menu. Anaconda starts and
asks two things, no more:
- **LUKS passphrase** for the encrypted root
- **admin password** (≥14 chars, mixed case, digit, symbol)
Anaconda then runs the `ostreecontainer` directive — pulls the
signed OCI image, writes it to disk, configures bootloader.
### 5. Reboot, remove USB
The first boot lands on SDDM with `admin` pre-filled. Log in.
### 6. First-login TUI
`veilor-postinstall` runs once, asks for the small set of things we
defer from install time:
- Keyboard / locale (defaults are fine for most operators)
- Hostname (default `veilor`)
- GPU drivers (NVIDIA layered via `rpm-ostree install`; mesa = no-op)
- Package presets (`dev` / `media` / `homelab`, all opt-in)
- Bluetooth (opt-in)
- USBGuard snapshot (plug in trusted devices first)
- `veilor-doctor` first run
Each step is skippable. The TUI writes a marker file and disables
itself; it never runs again.
If you need to re-run it: `sudo veilor-postinstall --force`.
### 7. Day-to-day
```sh
# update (atomic, A/B, instant rollback)
sudo veilor-update
# layer a package (takes effect after reboot)
sudo rpm-ostree install foo
# remove a layered package
sudo rpm-ostree uninstall foo
# health check + drift report
veilor-doctor
# rollback to previous deployment
sudo bootc rollback
# inspect current and staged deployments
bootc status
```
### Troubleshooting
| Symptom | Try |
|---|---|
| `veilor-update` says "no rollback target" | First boot — bootc only has rollback after the first successful upgrade. Normal. |
| Network down inside Anaconda | Bootstrap ISO uses NetworkManager defaults; plug in ethernet for the first install. WiFi support post-first-boot. |
| `rpm-ostree install foo` fails | Run `bootc status` — if a staged deployment exists, reboot first, then re-try. rpm-ostree won't layer onto a staged tree. |
| First-login TUI didn't appear | Marker check: `ls /var/lib/veilor/postinstall-complete`. If present, run `sudo veilor-postinstall --force`. |
| GPU is black after NVIDIA layer + reboot | `bootc rollback` and try mesa first; check `journalctl -b -1 -u sddm` from the previous boot. |
### Where the OCI image comes from
The image is built by `.github/workflows/build-bluebuild.yml` on the
self-hosted Forgejo runner (label `nullstone`). Build inputs:
- Base: `ghcr.io/secureblue/kinoite-main-hardened`
- Recipe: [`bluebuild/recipe.yml`](../bluebuild/recipe.yml)
- Veilor overlay: stamped via BlueBuild `type: files` modules
- Layered RPMs: `sudo`, `xorg-x11-server-Xwayland`, `mullvad-browser`,
`tailscale`, `yggdrasil`
- Output: `git.s8n.ru/veilor-org/veilor-os:{43,latest}`
The build is cosign-signed (key-pair on Forgejo, keyless on GitHub
parallel mirror). See [`bluebuild/README.md`](../bluebuild/README.md)
for the recipe walk-through.

View file

@ -252,7 +252,7 @@ ergonomic work and becomes the next ship target.
Scope: Scope:
- BlueBuild recipe (`bluebuild/recipe.yml`) layering on - BlueBuild recipe (`bluebuild/recipe.yml`) layering on
`ghcr.io/secureblue/kinoite-main-hardened` `ghcr.io/secureblue/securecore-kinoite-hardened-userns`
- `kickstart/install-ostreecontainer.ks` — 10-line kickstart that calls - `kickstart/install-ostreecontainer.ks` — 10-line kickstart that calls
`ostreecontainer --url=ghcr.io/veilor-org/veilor-os:43 --transport=registry` `ostreecontainer --url=ghcr.io/veilor-org/veilor-os:43 --transport=registry`
and lets Anaconda's LUKS UX drive the install and lets Anaconda's LUKS UX drive the install
@ -264,21 +264,6 @@ Scope:
- `veilor-update` rewritten on `bootc upgrade` (was `dnf upgrade`) - `veilor-update` rewritten on `bootc upgrade` (was `dnf upgrade`)
- Forgejo registry as primary OCI publish target; GHCR mirror optional - Forgejo registry as primary OCI publish target; GHCR mirror optional
- cosign key-pair signing of OCI image (replaces broken keyless flow) - cosign key-pair signing of OCI image (replaces broken keyless flow)
- **Installer logs persisted to USB stick by default** (debug mode):
the bootstrap ISO writes `/var/log/anaconda/*` + the resolved
kickstart + ostreecontainer pull log + dmesg back onto the USB
install medium (mounted rw at `/run/install/repo` during install)
into a `veilor-install-logs/<timestamp>/` folder. Toggleable via
kernel cmdline `inst.veilor.savelogs=0` for opt-out, or
`inst.veilor.savelogs=1` (default). Stays **ON by default through
v0.7+v0.8+v0.9; flips OFF for v1.0 final release**. Why: any failed
install, the operator boots back to a working OS, plugs the USB,
reads the logs offline — no need to take screenshots of dracut on a
bricked machine. Implementation: `%post --nochroot` block in
`kickstart/install-ostreecontainer.ks` that detects the install
medium via `/run/install/repo` rw remount, copies the log set,
syncs, then unmounts. If the medium is read-only (DVD), skip
silently with a `journalctl` warning.
Public-flex items kept from original v0.7 entry: Public-flex items kept from original v0.7 entry:
@ -307,7 +292,7 @@ spike on `quay.io/fedora/fedora-bootc:43`. Research on 2026-05-05
`docs/research/2026-05-05-agent-wave/`), then a parent-operator `docs/research/2026-05-05-agent-wave/`), then a parent-operator
refinement same day, locked the path: **layer veilor's branding + refinement same day, locked the path: **layer veilor's branding +
threat model + UX on top of secureblue's already-shipping threat model + UX on top of secureblue's already-shipping
`kinoite-main-hardened` OCI image** via a BlueBuild `securecore-kinoite-hardened-userns` OCI image** via a BlueBuild
recipe, and install it directly during the Anaconda pass via the recipe, and install it directly during the Anaconda pass via the
`ostreecontainer` kickstart directive (no first-boot rebase). `ostreecontainer` kickstart directive (no first-boot rebase).

View file

@ -12,7 +12,7 @@ Locked at: **v0.5.31 → v0.7 spike → v1.0**
works). works).
- Anaconda's `ostreecontainer` directive populates the root filesystem - Anaconda's `ostreecontainer` directive populates the root filesystem
directly from a **veilor-os OCI image** (built via BlueBuild on top directly from a **veilor-os OCI image** (built via BlueBuild on top
of secureblue's `kinoite-main-hardened`) **during the of secureblue's `securecore-kinoite-hardened-userns`) **during the
install pass — no first-boot rebase, no mutable→atomic transition**. install pass — no first-boot rebase, no mutable→atomic transition**.
- All future updates flow through `bootc upgrade` — atomic A/B, - All future updates flow through `bootc upgrade` — atomic A/B,
instant rollback, cosign-signed. instant rollback, cosign-signed.
@ -236,7 +236,7 @@ distro: **honest, scoped, public threat model**.
The Containerfile-from-scratch spike plan (Agent 3 of 2026-05-05 The Containerfile-from-scratch spike plan (Agent 3 of 2026-05-05
wave) is **superseded** by this hybrid: don't build a Containerfile wave) is **superseded** by this hybrid: don't build a Containerfile
from scratch on `fedora-bootc:43`. Instead, write a BlueBuild recipe from scratch on `fedora-bootc:43`. Instead, write a BlueBuild recipe
on `kinoite-main-hardened`. With `ostreecontainer` on `securecore-kinoite-hardened-userns`. With `ostreecontainer`
swap, spike compresses 1 week → 1 day. swap, spike compresses 1 week → 1 day.
## Next concrete steps ## Next concrete steps
@ -254,7 +254,7 @@ in the v0.7 spike branch only.
### v0.7-spike (1 day, separate branch) ### v0.7-spike (1 day, separate branch)
1. New repo dir: `bluebuild/recipe.yml`. 1. New repo dir: `bluebuild/recipe.yml`.
2. `from`: `ghcr.io/secureblue/kinoite-main-hardened:latest`. 2. `from`: `ghcr.io/secureblue/securecore-kinoite-hardened-userns:latest`.
3. Override modules: 3. Override modules:
- `type: files` — stamp our `overlay/*` tree (branding, themes, - `type: files` — stamp our `overlay/*` tree (branding, themes,
veilor scripts, sddm theme, plymouth theme). veilor scripts, sddm theme, plymouth theme).
@ -334,29 +334,3 @@ dir.
- Yggdrasil: <https://github.com/yggdrasil-network/yggdrasil-go> - Yggdrasil: <https://github.com/yggdrasil-network/yggdrasil-go>
- Reticulum manual: <https://reticulum.network/manual/> - Reticulum manual: <https://reticulum.network/manual/>
- Iroh blobs design: <https://github.com/n0-computer/iroh-blobs/blob/main/DESIGN.md> - Iroh blobs design: <https://github.com/n0-computer/iroh-blobs/blob/main/DESIGN.md>
---
## PIVOT EXECUTION — 2026-05-06
The hybrid strategy locked at v0.5 is now in execution.
- **v0.5.0 shipped** as the proof-of-work / portfolio release of the
kickstart-flat path. Self-hosted Forgejo CI green-built a 2.7 GB
ISO; tag pushed; download lives at the ci-latest release.
- **v0.6 milestone cancelled.** Continuing to debug
`livecd-creator + anaconda` quirks for v0.6 polish would be sunk-
cost work on tooling we retire at v1.0. Original v0.6 plan kept in
ROADMAP.md as historical reference.
- **v0.7 BlueBuild OCI is the active mainline.** The
`v0.7-bluebuild-spike` branch carries the BlueBuild recipe layered
on `ghcr.io/secureblue/kinoite-main-hardened`, the
`ostreecontainer` kickstart bootstrap, and the new `bootc upgrade`-
driven update channel.
- **v0.6 ergonomic CLIs ported, not rewritten.** `veilor-update`
rewrites onto `bootc upgrade`; `veilor-postinstall` becomes the
first-login TUI on the atomic system; `veilor-doctor` learns
`bootc status --json` while keeping the legacy dnf path for v0.5.x.
- **v1.0 retires the kickstart entirely.** Only `kickstart/install-
ostreecontainer.ks` (10 lines) ships forward — bootstrap installer
for ostreecontainer pulls.

View file

@ -1,47 +0,0 @@
# veilor-os installer kickstart — v0.7 CI build variant
#
# Derived from kickstart/install-ostreecontainer.ks by stripping all
# __PLACEHOLDER__ tokens that the runtime gum TUI substitutes at install
# time. Anaconda's interactive TUI handles disk selection, LUKS passphrase,
# and user account creation in their place.
#
# Consumed by livemedia-creator --make-iso to produce
# veilor-os-installer-43-*.iso. Do NOT add __PLACEHOLDER__ tokens here —
# they cannot be filled at build time. See install-ostreecontainer.ks
# for the runtime template the gum TUI fills in.
# ── Locale / keyboard / time ──
keyboard --xlayouts='us'
lang en_US.UTF-8
timezone Europe/London --utc
# ── Install mode ──
text
firstboot --disable
eula --agreed
selinux --enforcing
# ── Network ──
network --bootproto=dhcp --device=link --activate --hostname=veilor-install
firewall --enabled --service=ssh
# ── Identity ──
# rootpw --lock only. No user directive — Anaconda's user spoke handles
# admin account creation interactively. Runtime ks substitutes
# --password=__ADMIN_PW__ for unattended installs.
rootpw --lock
# ── Disk / partitioning ──
# Intentionally absent. Anaconda's disk spoke presents interactive
# disk + LUKS + btrfs selection. Runtime ks (gum TUI) provides the
# full partition spec at real-install time.
# ── ostreecontainer: populate / from veilor-os OCI ──
ostreecontainer --url=ghcr.io/veilor-org/veilor-os:43 --transport=registry
# ── %post (chroot) ──
%post
set -uo pipefail
echo veilor-install > /etc/hostname
chage -d 0 admin 2>/dev/null || true
%end

View file

@ -1,80 +0,0 @@
# veilor-os install kickstart — v0.7 spike (ostreecontainer path)
#
# This is the install-time kickstart for the v0.7 hybrid path. The live
# ISO boots; the gum TUI collects user answers (disk, LUKS pw, admin pw);
# this template gets the answers substituted in and is fed to anaconda.
#
# Anaconda partitions the disk + creates LUKS + btrfs subvols + mounts
# /boot/efi + /boot, then `ostreecontainer` populates `/` directly from
# the cosign-signed veilor-os OCI image at `ghcr.io/veilor-org/veilor-os:43`.
#
# No `%packages` block. No first-boot rebase. No
# `veilor-firstboot-rebase.service`. The ostreecontainer install pass is
# the entire transition from "Fedora live ISO" to "veilor-os on disk".
#
# Reference: pykickstart docs ostreecontainer command;
# https://docs.fedoraproject.org/en-US/bootc/anaconda-install/
# ── Locale / keyboard / time ──
keyboard --xlayouts='us'
lang en_US.UTF-8
timezone Europe/London --utc
# ── Install mode / behaviour ──
firstboot --disable
eula --agreed
# SELinux state inherited from the OCI image; --enforcing is implicit
# since secureblue's image ships /etc/selinux/config = enforcing.
selinux --enforcing
# ── Network / hostname ──
network --bootproto=dhcp --device=link --activate --hostname=__HOSTNAME__
firewall --enabled --service=ssh
# ── Identity (single LUKS prompt asked at install via gum TUI) ──
rootpw --lock
user --name=admin --groups=wheel --gecos="veilor admin" --password=__ADMIN_PW__ --plaintext
# ── Bootloader ──
# fbcon=nodefer for laptop KMS handoff (real-hardware audit, agent 9 of
# 2026-05-05 wave). rd.luks.options=tries=5,timeout=0 for UX.
# rd.luks.uuid is auto-injected by anaconda based on the encrypted
# part directive below.
#
# All other hardening kargs (lockdown=integrity, slab_nomerge, etc.)
# come from /usr/lib/bootc/kargs.d/ inside the OCI image — bootc
# applies them at install time. We only add what the OCI image can't
# know (laptop-specific KMS flag).
bootloader --append="fbcon=nodefer"
# ── Disk: LUKS2 (argon2id) + btrfs subvols ──
zerombr
clearpart --all --initlabel --drives=__DISK_BASENAME__
part /boot/efi --fstype=efi --size=600
part /boot --fstype=ext4 --size=1024
part btrfs.veilor --grow --encrypted --luks-version=luks2 --pbkdf=argon2id --passphrase=__LUKS_PW__
btrfs none --label=veilor btrfs.veilor
btrfs / --subvol --name=root LABEL=veilor
btrfs /home --subvol --name=home LABEL=veilor
# ── ostreecontainer: populate / from the veilor-os OCI image ──
# `--transport=registry` pulls from ghcr.io directly. Authentication
# token can be supplied via /etc/ostree/auth.json baked into the live
# rootfs OR via a kickstart `--remote-token` if the registry is private.
# At v0.7 spike the OCI image is public, so no auth needed.
#
# DO NOT migrate to the new `bootc` kickstart command until v1.0 — it
# blocks multi-disk and authenticated registries (per parent-operator
# handoff 2026-05-05).
ostreecontainer --url=ghcr.io/veilor-org/veilor-os:43 --transport=registry
# ── %post (chroot) — minimal; OCI image already has everything ──
# What we keep:
# - chage -d 0 admin so first SDDM login forces password change
# - hostname write (anaconda's --hostname doesn't always survive)
# - veilor-firstboot.service is enabled in the OCI image already
%post
set -uo pipefail
echo veilor > /etc/hostname
chage -d 0 admin || true
%end

View file

@ -1,7 +0,0 @@
[Unit]
Description=veilor-doctor — system health + drift check
After=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/veilor-doctor --quiet

View file

@ -1,10 +0,0 @@
[Unit]
Description=veilor-doctor weekly drift check
[Timer]
OnCalendar=weekly
Persistent=true
RandomizedDelaySec=30m
[Install]
WantedBy=timers.target

View file

@ -1,17 +0,0 @@
[Unit]
Description=veilor-os one-time post-install TUI (first login)
After=graphical.target
ConditionPathExists=!/var/lib/veilor/postinstall-complete
[Service]
Type=oneshot
ExecStart=/usr/local/bin/veilor-postinstall
StandardInput=tty
StandardOutput=tty
StandardError=journal
TTYPath=/dev/tty1
TTYReset=yes
TTYVHangup=yes
[Install]
WantedBy=graphical.target multi-user.target

View file

@ -147,33 +147,15 @@ PUBLIC_IP=$(curl -s --max-time 3 ifconfig.me 2>/dev/null || echo "")
|| check Network public_ip fail "lookup timed out" || check Network public_ip fail "lookup timed out"
# ── 5. Updates ────────────────────────────────────────────────────── # ── 5. Updates ──────────────────────────────────────────────────────
# v0.7+ atomic — bootc replaces dnf as the update channel. Parse LAST_DNF=$(sudo -n dnf history list 2>/dev/null \
# `bootc status --json` for the booted deployment + staged/cached image
# age. Fall back to dnf history if bootc not present (legacy v0.5.x).
if have bootc; then
BOOTC_JSON=$(sudo -n bootc status --json 2>/dev/null || echo "")
if [[ -n $BOOTC_JSON ]] && have jq; then
BOOTED_IMG=$(jq -r '.status.booted.image.image.image // "unknown"' <<<"$BOOTC_JSON")
BOOTED_DIGEST=$(jq -r '.status.booted.image.imageDigest // ""' <<<"$BOOTC_JSON")
check Updates booted_image pass "${BOOTED_IMG}@${BOOTED_DIGEST:0:12}"
STAGED=$(jq -r '.status.staged.image.image.image // ""' <<<"$BOOTC_JSON")
if [[ -n $STAGED ]]; then
check Updates staged_image fail "staged: $STAGED — reboot to apply"
else
check Updates staged_image pass "no staged update"
fi
else
check Updates bootc_state pass "bootc present (jq missing — install for richer detail)"
fi
elif have dnf; then
# Legacy v0.5.x kickstart-installed system.
LAST_DNF=$(sudo -n dnf history list 2>/dev/null \
| awk 'NR==4 {for(i=4;i<NF;i++)printf "%s ", $i; print $NF; exit}') | awk 'NR==4 {for(i=4;i<NF;i++)printf "%s ", $i; print $NF; exit}')
[[ -n $LAST_DNF ]] && check Updates last_dnf pass "$LAST_DNF" \ [[ -n $LAST_DNF ]] && check Updates last_dnf pass "$LAST_DNF" \
|| check Updates last_dnf pass "(unknown — try \`sudo dnf history\`)" || check Updates last_dnf pass "(unknown — try \`sudo dnf history\`)"
sudo -n dnf check-update -q >/dev/null 2>&1
RC=$? # `dnf check-update` exits 100 if updates available, 0 if not.
case $RC in sudo -n dnf check-update -q >/dev/null 2>&1
RC=$?
case $RC in
0) check Updates pending pass "system up-to-date" ;; 0) check Updates pending pass "system up-to-date" ;;
100) 100)
AVAIL=$(sudo -n dnf check-update -q 2>/dev/null \ AVAIL=$(sudo -n dnf check-update -q 2>/dev/null \
@ -181,10 +163,7 @@ elif have dnf; then
check Updates pending fail "${AVAIL} update(s) available — run \`veilor-update\`" check Updates pending fail "${AVAIL} update(s) available — run \`veilor-update\`"
;; ;;
*) check Updates pending fail "dnf check-update returned $RC (need sudo?)" ;; *) check Updates pending fail "dnf check-update returned $RC (need sudo?)" ;;
esac esac
else
check Updates channel fail "neither bootc nor dnf available"
fi
# ── 6. veilor services ────────────────────────────────────────────── # ── 6. veilor services ──────────────────────────────────────────────
for unit in veilor-firstboot.service veilor-modules-lock.service; do for unit in veilor-firstboot.service veilor-modules-lock.service; do

View file

@ -1,178 +0,0 @@
#!/usr/bin/bash
# veilor-postinstall — first-login TUI on v0.7+ atomic systems.
#
# Runs ONCE on first SDDM login via the user-mode systemd unit
# `veilor-postinstall.service`. Asks the operator for the small set
# of decisions we deliberately defer from install time:
# - keyboard / locale
# - hostname override
# - GPU drivers (NVIDIA layered via rpm-ostree, mesa = no-op)
# - package preset (dev / media / homelab — additive, opt-out)
# - bluetooth opt-in
# - USBGuard policy snapshot
# - veilor-doctor first run
# Writes /var/lib/veilor/postinstall-complete on success and disables
# its own autostart unit. Idempotent: safe to re-run.
#
# Style: gum if present, plain bash read fallback. No decorative ASCII.
set -uo pipefail
export TERM="${TERM:-linux}"
STATE_DIR=/var/lib/veilor
DONE_MARKER="$STATE_DIR/postinstall-complete"
LOG=/var/log/veilor-postinstall.log
have() { command -v "$1" >/dev/null 2>&1; }
GUM=$(have gum && echo gum || echo "")
# Always log + tee to stdout for live progress.
mkdir -p "$STATE_DIR" 2>/dev/null || true
exec > >(tee -a "$LOG") 2>&1
if [[ -e $DONE_MARKER && ${1:-} != "--force" ]]; then
echo "veilor-postinstall already ran (marker: $DONE_MARKER). Pass --force to re-run."
exit 0
fi
# ── Wrappers ────────────────────────────────────────────────────────
choose() {
local header=$1; shift
if [[ -n $GUM ]]; then
gum choose --header "$header" "$@"
else
echo
echo "$header"
local i=1
for opt in "$@"; do printf ' %d) %s\n' "$i" "$opt"; ((i++)); done
local n
read -rp " choice (1-$#): " n
[[ $n -ge 1 && $n -le $# ]] || return 1
eval "echo \${$n}"
fi
}
ask() {
local prompt=$1 default=${2:-}
if [[ -n $GUM ]]; then
gum input --header "$prompt" --value "$default"
else
local v
read -rp "$prompt [$default] " v
echo "${v:-$default}"
fi
}
confirm() {
local prompt=$1
if [[ -n $GUM ]]; then
gum confirm "$prompt" && return 0 || return 1
else
read -rp "$prompt [y/N] " y
[[ ${y,,} == y* ]]
fi
}
say() {
if [[ -n $GUM ]]; then
gum style --foreground 212 --bold "$1"
else
printf '\n=== %s ===\n' "$1"
fi
}
# Need root for several actions; re-exec under sudo if not root.
if [[ $EUID -ne 0 ]]; then
say "veilor-postinstall: sudo required"
exec sudo -E bash "$0" "$@"
fi
say "veilor-postinstall — one-time setup"
echo " This runs once. Each step is skippable. Defaults are sane."
echo
# ── 1. Keyboard layout ──────────────────────────────────────────────
KB=$(choose "Keyboard layout" us gb de fr es ru "skip") || KB=skip
if [[ $KB != skip ]]; then
localectl set-keymap "$KB" 2>/dev/null || true
echo " [OK] keymap = $KB"
fi
# ── 2. Locale ───────────────────────────────────────────────────────
LOC=$(choose "Locale" en_US.UTF-8 en_GB.UTF-8 de_DE.UTF-8 fr_FR.UTF-8 "skip") || LOC=skip
if [[ $LOC != skip ]]; then
localectl set-locale LANG="$LOC" 2>/dev/null || true
echo " [OK] locale = $LOC"
fi
# ── 3. Hostname ─────────────────────────────────────────────────────
HN=$(ask "Hostname" "veilor")
if [[ -n $HN && $HN != $(hostnamectl --static 2>/dev/null) ]]; then
hostnamectl set-hostname "$HN"
echo " [OK] hostname = $HN"
fi
# ── 4. GPU drivers ──────────────────────────────────────────────────
GPU=$(choose "GPU drivers" "Skip (use mesa defaults)" "NVIDIA proprietary (akmod-nvidia)" "Intel/AMD mesa (no-op)") || GPU=skip
case "$GPU" in
*NVIDIA*)
say "Layering NVIDIA driver — this takes a few minutes"
rpm-ostree install --idempotent akmod-nvidia xorg-x11-drv-nvidia-cuda \
&& echo " [OK] NVIDIA driver layered (reboot to use)" \
|| echo " [WARN] NVIDIA layer failed; check rpm-ostree status"
;;
*) echo " (skipped GPU layering)" ;;
esac
# ── 5. Package presets (multi-select) ───────────────────────────────
say "Package presets — pick any combination (skip = none)"
PRESET_DEV="git tmux vim-enhanced htop podman skopeo"
PRESET_MEDIA="vlc obs-studio"
PRESET_HOMELAB="wireguard-tools jq yq tmux"
PICKED=()
confirm "Install dev preset? ($PRESET_DEV)" && PICKED+=($PRESET_DEV) || true
confirm "Install media preset? ($PRESET_MEDIA)" && PICKED+=($PRESET_MEDIA) || true
confirm "Install homelab preset? ($PRESET_HOMELAB)" && PICKED+=($PRESET_HOMELAB) || true
if (( ${#PICKED[@]} > 0 )); then
# de-dupe
UNIQ=$(printf '%s\n' "${PICKED[@]}" | sort -u | tr '\n' ' ')
say "Layering: $UNIQ"
rpm-ostree install --idempotent $UNIQ \
&& echo " [OK] preset packages layered (reboot to use)" \
|| echo " [WARN] preset layer failed; check rpm-ostree status"
fi
# ── 6. Bluetooth ────────────────────────────────────────────────────
if confirm "Enable Bluetooth?"; then
systemctl enable --now bluetooth.service 2>/dev/null || true
echo " [OK] bluetooth enabled"
else
echo " (skipped bluetooth)"
fi
# ── 7. USBGuard snapshot ────────────────────────────────────────────
say "USBGuard policy snapshot"
echo " Plug in EVERY USB device you trust right now (keyboard,"
echo " mouse, dock, yubikey, etc.) before continuing."
if confirm "Snapshot current USB devices into the allowlist?"; then
usbguard generate-policy > /etc/usbguard/rules.conf \
&& echo " [OK] policy written to /etc/usbguard/rules.conf" \
|| echo " [WARN] generate-policy failed"
systemctl restart usbguard 2>/dev/null || true
fi
# ── 8. veilor-doctor ────────────────────────────────────────────────
if confirm "Run veilor-doctor now?"; then
veilor-doctor || true
fi
# ── Done ────────────────────────────────────────────────────────────
date -u +"%Y-%m-%dT%H:%M:%SZ" > "$DONE_MARKER"
say "veilor-postinstall complete"
echo " Marker written: $DONE_MARKER"
echo " Disabling autostart unit so this never runs again."
systemctl --user --global disable veilor-postinstall.service 2>/dev/null || true
systemctl disable veilor-postinstall.service 2>/dev/null || true
echo
echo " If you layered any packages or drivers, reboot to activate."

View file

@ -1,22 +1,25 @@
#!/usr/bin/bash #!/usr/bin/bash
# veilor-update — atomic update wrapper for v0.7+ (bootc + rpm-ostree). # veilor-update — system update wrapper.
# # Wraps `dnf upgrade --refresh` + `flatpak update` behind a single command.
# Wraps `bootc upgrade` + flatpak update behind a single command. # User-facing CLI shipped in /usr/local/bin/. v0.6 ergonomic tooling.
# Pre-checks rollback availability, pauses auditd while staging the
# new image, prints a clear post-state summary, and offers reboot.
# #
# Exit codes: # Exit codes:
# 0 success (with or without pending reboot) # 0 success
# 1 bootc upgrade failed # 1 dnf failed
# 2 flatpak failed (bootc still ran successfully) # 2 flatpak failed (dnf still ran successfully)
# 3 no network # 3 no network
#
# Uses `gum` for spinner output if present, falls back to plain stdout.
set -uo pipefail set -uo pipefail
# ── Helpers ─────────────────────────────────────────────────────────
have() { command -v "$1" >/dev/null 2>&1; } have() { command -v "$1" >/dev/null 2>&1; }
GUM=$(have gum && echo gum || echo "") GUM=$(have gum && echo gum || echo "")
say() { say() {
# Print a status line. Coloured if gum present, else plain.
if [[ -n $GUM ]]; then if [[ -n $GUM ]]; then
gum style --foreground 212 --bold "$1" gum style --foreground 212 --bold "$1"
else else
@ -24,50 +27,46 @@ say() {
fi fi
} }
confirm() { run_with_spinner() {
local prompt=$1 local title=$1; shift
if [[ -n $GUM ]]; then if [[ -n $GUM ]]; then
gum confirm "$prompt" gum spin --spinner dot --title "$title" -- "$@"
else else
read -r -p "$prompt [y/N] " yn echo "[+] $title"
[[ ${yn,,} == y* ]] "$@"
fi fi
} }
# ── Pre-flight: network ───────────────────────────────────────────── # ── Pre-flight: network check ───────────────────────────────────────
say "veilor-update: checking network" say "veilor-update: checking network"
if ! ping -c 1 -W 2 1.1.1.1 >/dev/null 2>&1; then if ! ping -c 1 -W 2 mirrors.fedoraproject.org >/dev/null 2>&1; then
echo " No network. Connect and re-run \`veilor-update\`." echo
echo " No route to mirrors.fedoraproject.org."
echo " Connect to a network and re-run \`veilor-update\`."
exit 3 exit 3
fi fi
# ── Pre-flight: rollback target available ─────────────────────────── # ── Snapshot kernel before upgrade so we can warn about reboot need ─
# bootc has two deployments by design (booted + rollback). If KERNEL_BEFORE=$(uname -r)
# something's wrong we want the user to see it before staging more.
if have bootc; then # ── DNF upgrade ─────────────────────────────────────────────────────
say "veilor-update: bootc status" say "veilor-update: refreshing DNF metadata + applying updates"
bootc status || true # Capture upgrade output so we can count packages afterwards. Tee to
else # stdout for live progress; swallow into a tempfile for the count.
echo " bootc not present — this CLI targets v0.7+ atomic systems." LOG=$(mktemp -t veilor-update.XXXXXX)
trap 'rm -f "$LOG"' EXIT
if ! sudo dnf upgrade --refresh -y 2>&1 | tee "$LOG"; then
echo
echo " dnf upgrade failed. See output above."
exit 1 exit 1
fi fi
# ── Pause auditd while staging ────────────────────────────────────── # ── Count packages updated ──────────────────────────────────────────
# Reduces audit log noise during the heavy fs writes; resume after. # DNF prints "Upgraded: N", "Installed: N", "Removed: N" at end.
AUDIT_PAUSED=0 # Sum the upgrade/install lines for the user-visible total.
if systemctl is-active auditd >/dev/null 2>&1; then UPDATED=$(grep -E '^(Upgraded|Installed)\b' "$LOG" 2>/dev/null \
if sudo systemctl stop auditd 2>/dev/null; then | awk -F: '{ gsub(/[^0-9]/,"",$2); s+=$2 } END { print s+0 }')
AUDIT_PAUSED=1
fi
fi
trap '[[ $AUDIT_PAUSED == 1 ]] && sudo systemctl start auditd 2>/dev/null || true' EXIT
# ── bootc upgrade ───────────────────────────────────────────────────
say "veilor-update: bootc upgrade"
if ! sudo bootc upgrade; then
echo " bootc upgrade failed. See output above."
exit 1
fi
# ── Flatpak (best-effort) ─────────────────────────────────────────── # ── Flatpak (best-effort) ───────────────────────────────────────────
FLATPAK_RC=0 FLATPAK_RC=0
@ -75,20 +74,21 @@ if have flatpak; then
say "veilor-update: updating flatpaks" say "veilor-update: updating flatpaks"
if ! flatpak update -y; then if ! flatpak update -y; then
FLATPAK_RC=2 FLATPAK_RC=2
echo " flatpak update failed; continuing." echo " flatpak update failed; continuing anyway."
fi fi
else
echo " (flatpak not installed — skipping)"
fi fi
# ── Post-update summary ───────────────────────────────────────────── # ── Post-update: reboot hint if kernel changed ──────────────────────
say "veilor-update: complete" KERNEL_AFTER_LATEST=$(rpm -q kernel --last 2>/dev/null \
bootc status 2>/dev/null | head -20 || true | awk 'NR==1 { sub(/^kernel-/,"",$1); print $1 }')
# ── Reboot prompt ─────────────────────────────────────────────────── say "veilor-update: complete"
# bootc always writes the new image into the staged deployment; reboot printf ' Packages updated : %s\n' "${UPDATED:-0}"
# is required for it to become the running root. printf ' Running kernel : %s\n' "$KERNEL_BEFORE"
if confirm " Reboot now to activate the new image?"; then if [[ -n ${KERNEL_AFTER_LATEST:-} && $KERNEL_AFTER_LATEST != "$KERNEL_BEFORE" ]]; then
say "veilor-update: rebooting" printf ' Newest kernel : %s (reboot suggested)\n' "$KERNEL_AFTER_LATEST"
sudo systemctl reboot
fi fi
exit $FLATPAK_RC exit $FLATPAK_RC