Compare commits

..

22 commits

Author SHA1 Message Date
s8n-ru
f4ea27271d ci: gate softprops release steps + add Forgejo API equivalents
Some checks failed
Lint / Kickstart syntax (pull_request) Failing after 0s
Lint / Shell scripts (pull_request) Failing after 0s
Lint / No personal/onyx leaks (pull_request) Failing after 0s
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
8b1b49b5fc Merge pull request 'feat(installer): v0.6 ergonomics + polish — 5 quick wins' (#3) from feat/ux-installer-v06-polish into main
Some checks failed
Build veilor-os ISO / Build live ISO (push) Failing after 0s
Lint / Kickstart syntax (push) Failing after 0s
Lint / Shell scripts (push) Failing after 0s
Lint / No personal/onyx leaks (push) Failing after 0s
2026-05-06 13:47:35 +01:00
a125e46c5e Merge pull request 'sec: AppArmor v0.6 stub — load profiles in complain mode' (#11) from feat/sec-apparmor-v06-stubs into main
Some checks failed
Lint / Kickstart syntax (push) Waiting to run
Lint / Shell scripts (push) Waiting to run
Lint / No personal/onyx leaks (push) Waiting to run
Build veilor-os ISO / Build live ISO (push) Failing after 0s
2026-05-06 13:47:31 +01:00
3d35196a2d Merge pull request 'ci: cosign keyless sigs, SBOM, provenance + fedora digest pin' (#7) from feat/sre-cosign-sbom-attestation into main
Some checks failed
Build veilor-os ISO / Build live ISO (push) Failing after 0s
Lint / Kickstart syntax (push) Failing after 0s
Lint / Shell scripts (push) Has been cancelled
Lint / No personal/onyx leaks (push) Has been cancelled
2026-05-06 13:47:27 +01:00
1db860c557 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
Some checks failed
Build veilor-os ISO / Build live ISO (push) Failing after 0s
Lint / Kickstart syntax (push) Failing after 0s
Lint / Shell scripts (push) Failing after 0s
Lint / No personal/onyx leaks (push) Has been cancelled
2026-05-06 13:47:20 +01:00
d042552752 Merge pull request 'sec: polish THREAT-MODEL.md for v0.7 public launch' (#10) from feat/sec-threat-model-polish into main
Some checks failed
Lint / Kickstart syntax (push) Failing after 0s
Lint / Shell scripts (push) Failing after 0s
Lint / No personal/onyx leaks (push) Failing after 0s
2026-05-06 13:46:58 +01:00
8221228824 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
Some checks failed
Lint / Kickstart syntax (push) Failing after 0s
Lint / Shell scripts (push) Failing after 0s
Lint / No personal/onyx leaks (push) Failing after 0s
2026-05-06 13:46:12 +01:00
2f0403adfd docs: README de-GH + Forgejo build status
Some checks failed
Lint / Kickstart syntax (push) Failing after 0s
Lint / Shell scripts (push) Failing after 0s
Lint / No personal/onyx leaks (push) Failing after 0s
2026-05-06 13:45:33 +01:00
ec52fd4994 docs: METHOD-CHANGELOG 2026-05-06 forgejo entry
Some checks failed
Lint / Kickstart syntax (push) Failing after 0s
Lint / Shell scripts (push) Failing after 0s
Lint / No personal/onyx leaks (push) Failing after 0s
2026-05-06 13:45:29 +01:00
s8n-ru
dfda66ac7e sec: AppArmor v0.6 stub — load profiles in complain mode
Some checks failed
Lint / Kickstart syntax (pull_request) Failing after 0s
Lint / Shell scripts (pull_request) Failing after 0s
Lint / No personal/onyx leaks (pull_request) Failing after 0s
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-ru
e4b6516f1c sec: polish THREAT-MODEL.md for v0.7 public launch
Some checks failed
Lint / Kickstart syntax (pull_request) Failing after 0s
Lint / Shell scripts (pull_request) Failing after 0s
Lint / No personal/onyx leaks (pull_request) Failing after 0s
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
claude-veilor-bot
73c2c68e32 ci: pin actions to node20-safe tags + runner sock pass-through
Some checks failed
Lint / Kickstart syntax (pull_request) Failing after 0s
Lint / Shell scripts (pull_request) Failing after 0s
Lint / No personal/onyx leaks (pull_request) Failing after 0s
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)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-06 10:50:15 +01:00
veilor-org
0b1a33326a chore: gitignore auto-install-vm test artifacts
Some checks failed
Lint / Kickstart syntax (push) Failing after 0s
Lint / Shell scripts (push) Failing after 0s
Lint / No personal/onyx leaks (push) Failing after 0s
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
b74ef5005d ci: TODO marker for SHA-pinning third-party actions
Some checks failed
Lint / Kickstart syntax (pull_request) Failing after 3s
Lint / Shell scripts (pull_request) Failing after 38s
Lint / No personal/onyx leaks (pull_request) Failing after 11m14s
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
84275e2515 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
0a1b81a9e0 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
6db4b759fa docs: test run report skeleton for v0.5.32 (Forgejo build)
Some checks failed
Lint / Kickstart syntax (pull_request) Failing after 3s
Lint / Shell scripts (pull_request) Failing after 41s
Lint / No personal/onyx leaks (pull_request) Failing after 35s
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
b64ee19e53 feat(installer): promote eject-media reminder to its own box
Some checks failed
Lint / Kickstart syntax (pull_request) Failing after 3s
Lint / Shell scripts (pull_request) Failing after 39s
Lint / No personal/onyx leaks (pull_request) Failing after 29s
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
138a04702c 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
767fe71a3a 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
4c1895dcc1 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
6d86e00284 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
12 changed files with 498 additions and 54 deletions

View file

@ -1,3 +1,5 @@
# TODO: SHA-pin all uses: tags to commit SHAs (Agent 8 audit recommendation).
# Tracked separately so this PR can land without long web lookups.
name: Build veilor-os ISO
on:
@ -21,6 +23,8 @@ on:
permissions:
contents: write # needed for action-gh-release to create+update ci-latest
id-token: write # cosign keyless OIDC + attest-build-provenance
attestations: write # attest-build-provenance writes the attestation
jobs:
build:
@ -30,7 +34,9 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
# Pinned to last v4 tag confirmed to ship on node20. v4.2+ ships
# node24 which forgejo-runner v6.4.0 (node20) cannot exec.
uses: actions/checkout@v4.1.7
- name: Free up disk
run: |
@ -39,9 +45,14 @@ jobs:
df -h
- name: Run build inside Fedora 43 container
# v3 is composite/docker-based — no node runtime in the action
# itself. Safe under node20 forgejo-runner. TODO(infra): consider
# SHA pinning in a follow-up sweep.
uses: addnab/docker-run-action@v3
with:
image: registry.fedoraproject.org/fedora:43
# Pinned to digest from `skopeo inspect --raw` on 2026-05-06.
# Refresh by re-running skopeo against fedora:43 and bumping.
image: registry.fedoraproject.org/fedora:43@sha256:72e874e771b953c6357c7a5823c6fc1e3e3253b90121e795febe01380e32269b
options: |
--privileged
-v ${{ github.workspace }}:/work
@ -197,13 +208,42 @@ jobs:
echo "[OK] split into:"
ls "${ISO}".part-*
- name: Install cosign
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
uses: sigstore/cosign-installer@v3
- name: Sign ISO parts (keyless)
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
run: |
cd build/out
for f in *.part-*; do
cosign sign-blob --yes "$f" \
--output-signature "$f.sig" \
--output-certificate "$f.pem"
done
- name: Generate SBOM (SPDX)
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
uses: anchore/sbom-action@v0
with:
path: build/out
format: spdx-json
output-file: build/out/veilor-os.spdx.json
- name: Build provenance attestation
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
uses: actions/attest-build-provenance@v2
with:
subject-path: 'build/out/*.iso.part-*'
# GitHub-only: softprops/action-gh-release uses the GitHub REST API
# which Forgejo doesn't expose at the same endpoints. When this
# workflow runs on git.s8n.ru the step below (Forgejo) handles
# publishing instead.
- name: Publish to ci-latest rolling prerelease (GitHub)
if: success() && github.ref == 'refs/heads/main' && github.server_url == 'https://github.com'
uses: softprops/action-gh-release@v2
# Pinned to last v2 tag confirmed to ship on node20.
uses: softprops/action-gh-release@v2.0.4
with:
tag_name: ci-latest
name: "ci-latest (auto)"
@ -224,6 +264,9 @@ jobs:
files: |
build/out/*.iso.part-*
build/out/*.sha256
build/out/*.sig
build/out/*.pem
build/out/*.spdx.json
# Forgejo equivalent: drop+recreate ci-latest release via the
# Forgejo REST API, then upload chunks. Only runs when not on GitHub.
@ -299,7 +342,8 @@ jobs:
# GitHub-only: same restriction as ci-latest publish.
- name: Attach to release on tag (GitHub)
if: github.event_name == 'release' && github.server_url == 'https://github.com'
uses: softprops/action-gh-release@v2
# Pinned to last v2 tag confirmed to ship on node20.
uses: softprops/action-gh-release@v2.0.4
with:
files: |
build/out/*.iso

View file

@ -12,7 +12,8 @@ jobs:
container:
image: registry.fedoraproject.org/fedora:43
steps:
- uses: actions/checkout@v4
# Pinned to last v4 tag confirmed to ship on node20.
- uses: actions/checkout@v4.1.7
- run: dnf -y install pykickstart
- run: ksvalidator kickstart/veilor-os.ks
@ -20,7 +21,8 @@ jobs:
name: Shell scripts
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
# Pinned to last v4 tag confirmed to ship on node20.
- uses: actions/checkout@v4.1.7
- uses: ludeeus/action-shellcheck@master
with:
severity: warning
@ -30,7 +32,8 @@ jobs:
name: No personal/onyx leaks
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
# Pinned to last v4 tag confirmed to ship on node20.
- uses: actions/checkout@v4.1.7
- name: Grep for leaks
run: |
set -e

2
.gitignore vendored
View file

@ -13,4 +13,6 @@ secrets/
*.pem
test/veilor-vm.qcow2
test/veilor-vm.nvram*
test/auto-install-vm.qcow2
test/auto-install-vm.nvram*
.claude/worktrees/

View file

@ -2,7 +2,7 @@
> **Hardened minimal Fedora KDE spin. Black-on-black. Locked down by default.**
[![Build veilor-os ISO](https://github.com/veilor-org/veilor-os/actions/workflows/build-iso.yml/badge.svg)](https://github.com/veilor-org/veilor-os/actions/workflows/build-iso.yml)
[![Build veilor-os ISO](https://git.s8n.ru/veilor-org/veilor-os/badges/workflows/build-iso.yml/badge.svg)](https://git.s8n.ru/veilor-org/veilor-os/actions?workflow=build-iso.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
veilor-os is a Fedora 43 KDE Plasma remix for operators who want a clean,
@ -30,6 +30,11 @@ brittleness, bootloader install via `gen_grub_cfgstub`); current focus
is the v0.5.32 blocker list from the
[2026-05-05 9-agent research wave](docs/research/2026-05-05-agent-wave/README.md).
Primary git host: <https://git.s8n.ru/veilor-org/veilor-os>. The GitHub
mirror was disabled 2026-05-06; this repo is private-by-default on
Forgejo. ISO builds and CI artifacts are produced by the Forgejo runner
on nullstone — no GitHub Actions involvement.
What is **shipping**: hardening (SELinux, sysctl, USBGuard, fail2ban,
firewalld), KDE black theme, Fira Code system font, 3-mode power
management, single-prompt LUKS install, first-boot admin password flow,
@ -46,7 +51,9 @@ spike at v0.7**, **bootc-only at v1.0**.
## Quick install
```bash
# 1. Download the ISO (after public release; CI artifact for now)
# 1. Download the ISO from the latest Forgejo release.
# 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
# 2. Flash to USB. Replace /dev/sdX with your USB device — triple-check.

View file

@ -1,6 +1,6 @@
# Threat Model
> **Status:** Draft for v0.7 public flex. Honest scope.
> **Status:** Final for v0.7 public launch. Honest scope.
veilor-os is a hardened daily-driver desktop. Not a paranoia OS, not an
anonymity OS, not an isolation OS. This document exists so that
@ -14,36 +14,39 @@ tool**. veilor-os will not save you, and we will not pretend otherwise.
## In scope — what veilor-os defends against
Every row cites the file or setting that implements the mitigation, so the
claim is auditable from a clean checkout.
| Adversary / scenario | veilor-os mitigation |
|---|---|
| Lost or stolen laptop, powered off | LUKS2 (aes-xts-plain64, argon2id, mem=1 GB) on root + swap-as-zram. Disk yields ciphertext. |
| Generic browser / email malware (drive-by RCE, malicious attachment) | SELinux enforcing + `veilor-systemd` policy + sysctl hardening (kptr_restrict, ptrace=2, perf=3, BPF JIT harden, full ASLR, no SUID core dumps). AppArmor stack lands in v0.5. |
| Console-side USB attack (BadUSB, rubber ducky, juice-jack) | USBGuard daemon, default-block, empty allowlist on first boot. New device = explicit operator allow. |
| SSH brute-force / credential-stuffing | sshd password-auth off, root login off, MaxAuthTries=3, fail2ban with sshd + pam-generic jails wired to firewalld rich-rule. |
| Post-incident forensics ("what happened?") | auditd rules covering passwd/shadow/sudoers/ssh/cron/sysctl/kernel modules and all privileged binaries. Logs survive reboot. |
| Supply-chain on the OS image itself | Fedora's signed shim → GRUB → kernel chain (Secure Boot enforced). v0.4 adds GPG-signed ISO + sha256 + own MOK. |
| Unprivileged local user attempting LPE | root account locked (`passwd -S root` → `L`), single sudo user with pwquality minlen=14 / 4 classes, kernel module loading frozen 30 s after graphical boot. |
| Network-listening services as attack surface | firewalld default zone = `drop`; only sshd answers. abrt/cups/avahi/bluetooth/ModemManager/kdeconnectd/PackageKit are masked. |
| Time-based MITM (back-dated certs, replay) | NTS-authenticated chrony, DNS-over-TLS via systemd-resolved, LLMNR off. |
| Lost or stolen laptop, powered off | LUKS2 `aes-xts-plain64` + `argon2id` (`mem=1 GiB`, `time=9`) on root LV; swap is `zram` only — no persistent key material on disk. Defined in `kickstart/veilor-os.ks` `part pv.veilor` block. |
| Generic browser / email malware (drive-by RCE, malicious attachment) | SELinux `enforcing` + targeted policy + custom `veilor-systemd.te` module (`scripts/selinux/`); sysctl knobs in `/etc/sysctl.d/99-veilor-hardening.conf`: `kernel.kptr_restrict=2`, `kernel.yama.ptrace_scope=2`, `kernel.perf_event_paranoid=3`, `net.core.bpf_jit_harden=2`, `kernel.randomize_va_space=2`, `fs.suid_dumpable=0`, `dev.tty.ldisc_autoload=0`. AppArmor profile skeletons in `scripts/apparmor/` for Trivalent/Thorium/lm-studio (opt-in, complain mode, hardens to enforce per profile). |
| Console-side USB attack (BadUSB, rubber ducky, juice-jack) | USBGuard daemon, `ImplicitPolicyTarget=block`, **id-based** rules in `/etc/usbguard/rules.conf` (vendor:product, not hash — survives dock replug). Empty allowlist on first boot; operator runs `usbguard generate-policy` after plugging trusted devices. |
| SSH brute-force / credential-stuffing | `/etc/ssh/sshd_config.d/10-veilor-hardening.conf`: `PasswordAuthentication no`, `PermitRootLogin no`, `AllowUsers admin`, `MaxAuthTries 3`, `X11Forwarding no`, `LogLevel VERBOSE`. `fail2ban` `sshd` + `pam-generic` jails (journald backend) ban via firewalld `rich-rule` action. |
| Post-incident forensics ("what happened?") | `auditd` rules in `/etc/audit/rules.d/99-veilor-hardening.rules` watch `/etc/{passwd,shadow,group,sudoers,sudoers.d,ssh/sshd_config*,selinux,firewalld,cron.*,sysctl.*,systemd/system}`, every privileged binary (`sudo`, `su`, `passwd`, `mount`, `pkexec`, …), `init_module`/`finit_module`/`delete_module` syscalls, and uid≥1000 perm/owner changes. Logs persist across reboot. |
| Supply-chain on the OS image itself | Secure Boot enforced (Fedora signed shim → GRUB → kernel). v0.7 adds cosign-signed OCI image at `ghcr.io/veilor/veilor-os:43`, GPG-signed ISO + sha256 + .asc, plus our own MOK for out-of-tree module signing. |
| Unprivileged local user attempting LPE | Root account locked (`passwd -l root`; `passwd -S root``L`); single `admin` user in `wheel`; `pwquality.conf` `minlen=14`, `minclass=4`, dictcheck on. Kernel `lockdown=integrity`, `slab_nomerge`, `init_on_alloc=1`, `init_on_free=1`, `randomize_kstack_offset=on`, `vsyscall=none` set in bootloader args. Module loading frozen 30 s after graphical boot via `veilor-modules-lock.service`. |
| Network-listening services as attack surface | `firewalld` default zone = `drop`; only `sshd` answers. `abrt*`, `cups`, `cups-browsed`, `geoclue`, `avahi-daemon`, `bluetooth`, `ModemManager`, `gssproxy`, `atd`, `pcscd.{socket,service}` are masked; `kdeconnectd` and `PackageKit` are removed at the package level. |
| Time-based MITM (back-dated certs, replay) | `chrony` with NTS authentication against `time.cloudflare.com` and `nts.sth1/2.ntp.se` (pool fallback only). `systemd-resolved` with DNS-over-TLS opportunistic, DNSSEC `allow-downgrade`, LLMNR off; resolvers Cloudflare 1.1.1.1 / 1.0.0.1, fallback Quad9 9.9.9.9 / 149.112.112.112. |
---
## Out of scope — what veilor-os does NOT defend against
We are honest about this list because pretending otherwise is how people get
hurt. **If your adversary is here, pick a different tool.**
These adversaries are unambiguously outside our scope. Pretending otherwise
gets people hurt. **If your adversary is on this list, pick a different tool.**
| Adversary / scenario | Why veilor-os doesn't help | Use instead |
|---|---|---|
| Nation-state firmware-level implant (UEFI, ME, BMC) | Secure Boot validates the OS, not the firmware below it. We do not flash custom firmware. | Heads / coreboot on supported hardware. |
| Evil-maid attack on a running, unlocked system | LUKS keys live in RAM while the system is up. A physically present attacker can dump RAM (cold boot, DMA via Thunderbolt, debug header). | Power off when unattended. Disable Thunderbolt DMA in firmware. Qubes-in-a-Faraday-bag if you're that target. |
| Hardware keylogger / hardware mod between keyboard and machine | We're software. Software cannot detect a passive hardware tap. | Physical custody of the device. Tamper-evident seals. |
| Targeted RCE on the user session (browser 0-day, signal-app exploit) | KDE Plasma is not sandboxed. A logged-in compromise has the user's full data and tokens. SELinux confines daemons, not the desktop. | Qubes (per-app VM isolation). |
| Side-channel attacks on AES (timing, cache, power analysis) | We use stock kernel crypto. No constant-time guarantees beyond what the kernel/CPU provide. | Threat-specific HSM. |
| Physical attack on a TPM2 chip (probe, glitch, decap) | We don't ship TPM2 binding yet. Even when v1.0 lands, TPM2 is not anti-tamper hardware. | Off-device key custody. |
| Network-level traffic correlation / traffic analysis | All packets leave the box on the local IP. We don't onion-route. | Tails, Whonix, Tor. |
| Trust-on-first-use attacks (user clicks "accept bad cert") | We can't override the user's decisions. Bad SSL/SSH key acceptance by the operator is out of scope. | Enrolment policy, MDM. |
| Adversary with sustained physical access and time | Given enough physical time and tools, any laptop falls. | Operational security, not OS choice. |
| Firmware-level implant (UEFI, Intel ME, BMC, EC) | veilor-os does not protect against firmware implants. Secure Boot validates the OS chain only; we do not flash, audit, or sign firmware below GRUB. | Heads / coreboot on supported hardware. |
| Evil-maid attack on a running, unlocked system | LUKS master keys live in RAM while the system is up. A physically present attacker can dump RAM (cold-boot, Thunderbolt DMA, debug header) and recover them. | Power off when unattended. Disable Thunderbolt DMA in firmware. Qubes-in-a-Faraday-bag if you are that target. |
| Hardware keylogger / interposer between keyboard and machine | veilor-os is software. Software cannot detect a passive hardware tap. | Physical custody of the device. Tamper-evident seals. |
| Targeted RCE on the user session (browser 0-day, messenger exploit) | KDE Plasma is not sandboxed. A logged-in compromise owns the user's data and tokens. SELinux confines daemons; it does not confine the desktop session. | Qubes OS (per-app Xen VM isolation). |
| Side-channel attacks on AES (timing, cache, power, EM) | veilor-os ships stock kernel crypto. We provide no constant-time or power-analysis guarantees beyond what the kernel and CPU deliver. | Threat-specific HSM, air-gap. |
| Physical attack on a TPM2 chip (bus probe, glitch, decap) | veilor-os does not bind keys to TPM2 in v0.7. Even when binding lands post-v1.0, TPM2 is not anti-tamper hardware. | Off-device key custody (smartcard / YubiKey / OnlyKey). |
| Network-level traffic correlation / traffic analysis | All packets leave the box on the local IP. veilor-os does not onion-route. | Tails, Whonix, Tor. |
| Trust-on-first-use attacks (operator accepts a bad cert) | veilor-os cannot override the operator's explicit decisions. Bad SSL or SSH host-key acceptance is out of scope. | Enrolment policy, MDM, certificate pinning. |
| Adversary with sustained physical access and time | Given unlimited physical time and tools, any laptop falls. | Operational security, not OS choice. |
---
@ -92,19 +95,21 @@ Hardening that breaks ordinary work gets called out, not hidden.
Scoring legend: `✓` shipped & on by default, `~` partial / opt-in,
`✗` not provided, `n/a` not applicable to that distro's model.
Project metrics are GitHub / Codeberg figures as of 2026-05.
| Axis | veilor-os | Stock Fedora KDE | Kicksecure | Tails | Qubes OS | secureblue |
|---|:---:|:---:|:---:|:---:|:---:|:---:|
| **Encrypted at rest by default** | ✓ (LUKS2 argon2id) | ~ (optional) | ✓ | n/a (amnesic) | ✓ | ✓ |
| **MAC enforcing OOTB** | ✓ (SELinux + AppArmor v0.5) | ✓ (SELinux) | ✓ (AppArmor) | ✓ (AppArmor) | ✓ (per-VM) | ✓ (SELinux) |
| **Default-deny firewall** | ✓ | ✗ | ✓ | ✓ (Tor-only) | ✓ | ✓ |
| **USB default-block** | ✓ (USBGuard) | ✗ | ✓ | ✓ | ✓ (sys-usb) | ✓ |
| **Per-app isolation (VM/sandbox)** | ✗ | ✗ | ✗ | ~ (AppArmor) | ✓ (Xen VMs) | ~ (Flatpak/bwrap) |
| **Anonymity / Tor by default** | ✗ | ✗ | ✗ | ✓ | ~ (Whonix VMs) | ✗ |
| **Daily driver target (persistent)** | ✓ | ✓ | ✓ | ✗ | ✓ (heavy) | ✓ |
| **Signed releases (publisher key)** | ✓ (v0.4) | ✓ | ✓ | ✓ | ✓ | ✓ |
| **Threat model published** | ✓ (this doc) | ✗ | ✓ | ✓ | ✓ | ✓ |
| **Hardware compatibility (laptops)** | ✓ (Fedora kernel) | ✓ | ~ | ~ (live USB) | ~ (Xen-pinned) | ✓ |
| Axis | veilor-os | Stock Fedora KDE | Kicksecure | Tails | Qubes OS | secureblue | Athena OS |
|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
| **Encrypted at rest by default** | ✓ (LUKS2 argon2id, mem=1 GiB) | ~ (optional in Anaconda) | ✓ | n/a (amnesic, session-only) | ✓ | ✓ | ~ (optional) |
| **MAC enforcing OOTB** | ✓ (SELinux + opt-in AppArmor) | ✓ (SELinux) | ✓ (AppArmor) | ✓ (AppArmor) | ✓ (per-VM) | ✓ (SELinux) | ✓ (AppArmor) |
| **Default-deny firewall** | ✓ (firewalld zone=drop) | ✗ | ✓ | ✓ (Tor-only) | ✓ | ✓ | ✓ |
| **USB default-block** | ✓ (USBGuard, id-rules) | ✗ | ✓ | ✓ | ✓ (sys-usb) | ✓ (USBGuard) | ✗ |
| **Per-app isolation (VM/sandbox)** | ✗ | ✗ | ✗ | ~ (AppArmor) | ✓ (Xen VMs) | ~ (Flatpak/bwrap) | ✗ |
| **Anonymity / Tor by default** | ✗ | ✗ | ✗ | ✓ | ~ (Whonix VMs) | ✗ | ✗ |
| **Daily driver target (persistent)** | ✓ | ✓ | ✓ | ✗ (amnesic) | ✓ (heavy, hardware-partitioning) | ✓ | ✓ |
| **Signed releases (cosign + GPG)** | ✓ (v0.7) | ✓ | ✓ | ✓ | ✓ | ✓ (cosign on OCI) | ~ (sha256 only) |
| **Threat model published** | ✓ (this doc) | ✗ | ✓ | ✓ | ✓ | ✗ | ✓ |
| **Hardware compatibility (laptops)** | ✓ (Fedora kernel) | ✓ | ~ | ~ (live USB) | ~ (Xen-pinned HCL) | ✓ | ✓ (Arch kernel) |
| **Project size (contributors / stars, 2026-05)** | solo / pre-public | n/a (Fedora-wide) | small team / ~600 | ~30 / ~3k | large / ~5k | ~30 / ~940, active monthly cadence | ~8 / ~1.4k |
---

View file

@ -119,6 +119,12 @@ chrony
firewalld
plymouth
# AppArmor stack — Fedora 43 ships parser/utils/profiles. v0.6 ships
# loaded-but-complain only (see scripts/40-apparmor.sh + tier-2 plan).
apparmor-parser
apparmor-utils
apparmor-profiles
# admin essentials
git
vim-enhanced

View file

@ -0,0 +1,11 @@
# veilor-os AppArmor profile stub — firefox
#
# v0.6 scope: marker only. Loads in complain mode via scripts/40-apparmor.sh
# so AppArmor can log the syscall surface for v0.7 policy authoring. No
# actual confinement rules yet — full policy is post-v0.6.
#include <tunables/global>
profile veilor-firefox /usr/lib*/firefox/firefox flags=(complain) {
#include <abstractions/base>
}

View file

@ -0,0 +1,11 @@
# veilor-os AppArmor profile stub — thunderbird
#
# v0.6 scope: marker only. Loads in complain mode via scripts/40-apparmor.sh
# so AppArmor can log the syscall surface for v0.7 policy authoring. No
# actual confinement rules yet — full policy is post-v0.6.
#include <tunables/global>
profile veilor-thunderbird /usr/lib*/thunderbird/thunderbird flags=(complain) {
#include <abstractions/base>
}

View file

@ -69,6 +69,21 @@ banner() {
local vline="veilor-os ${ver} · ${d} · live"
if [[ -r $BANNER_FILE ]]; then
# v0.6: staged line-by-line reveal of the banner before the
# gum-style border draws around it. 40ms/line gives a subtle
# "typewriter" feel — 5 lines × 40ms = 200ms total, fast enough
# not to feel laggy but slow enough to land an aesthetic on the
# very first frame the user sees. Once the reveal finishes we
# clear and re-draw with the bordered gum-style version so the
# operator never sees both stacked on top of each other.
local line
while IFS= read -r line; do
printf ' %s\n' "$line"
sleep 0.04
done < "$BANNER_FILE"
sleep 0.08
clear
if [[ $TUI == gum ]]; then
# gum style: rounded border, banner + blank line + version line.
gum style --border rounded --margin "0 2" --padding "1 3" \
@ -150,10 +165,30 @@ prompt_input() {
}
# prompt_password <header>
#
# v0.6: gum-path replaced with bash `read -srp` because `gum input
# --password` rendered as a duplicate-"Install" + stray-T artefact on
# the linux fbcon since v0.5.27 (Agent 7 of the v0.6 polish research
# wave traced this to gum's bubbletea screen-restore writing back the
# previous menu buffer when the framebuffer terminfo lacked
# `civis/cnorm` cursor-hide sequences). bash `read -srp` is a single
# write to stdout + termios echo-off — no redraw, no glitch. Header
# rendered separately via gum style for visual parity with the rest
# of the installer.
prompt_password() {
local header=$1
if [[ $TUI == gum ]]; then
gum input --password --header "$header"
# Render the prompt header as a styled box so it looks at home
# next to the other gum prompts, then collect the password via
# plain bash read on the next line. `read -s` disables echo,
# `read -p` writes the prompt to stderr (so command-substitution
# callers still get the password on stdout cleanly).
gum style --foreground "${VEILOR_FG:-15}" --border rounded \
--border-foreground "${VEILOR_DIM:-240}" --padding "0 2" -- "$header"
local pw
read -srp " password: " pw
echo >&2 # newline after silent read so next prompt isn't on same line
printf '%s' "$pw"
else
whiptail --title "veilor-os" --passwordbox "$header" 10 60 \
3>&1 1>&2 2>&3
@ -253,12 +288,36 @@ collect_answers() {
}
# ── LUKS passphrase ──
# v0.6: prompt twice + string-compare. 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 (US vs UK quote position is
# the most common one) before they brick the install.
local luks_pw_confirm
while true; do
luks_pw=$(prompt_password "[2/3] Encryption · LUKS2 passphrase (min 8)") || return 1
validate_pw "$luks_pw" "passphrase" || return 1
validate_pw "$luks_pw" "passphrase" || continue
luks_pw_confirm=$(prompt_password "[2/3] Confirm LUKS2 passphrase") || return 1
if [[ $luks_pw == "$luks_pw_confirm" ]]; then
break
fi
prompt_error "Passphrases do not match — try again."
done
# ── Admin password ──
# Same confirm-twice pattern. Less catastrophic than LUKS (admin
# password can be reset from a recovery shell) but a mismatch here
# still locks the user out of their fresh install on first boot.
local admin_pw_confirm
while true; do
admin_pw=$(prompt_password "[3/3] Admin user · password for 'admin'") || return 1
validate_pw "$admin_pw" "password" || return 1
validate_pw "$admin_pw" "password" || continue
admin_pw_confirm=$(prompt_password "[3/3] Confirm admin password") || return 1
if [[ $admin_pw == "$admin_pw_confirm" ]]; then
break
fi
prompt_error "Passwords do not match — try again."
done
# ── Locale ──
# Hardcoded en_US.UTF-8 for branded consistency. The picker that
@ -488,6 +547,12 @@ chrony
firewalld
plymouth
# AppArmor stack — Fedora 43 ships parser/utils/profiles. v0.6 ships
# loaded-but-complain only (see scripts/40-apparmor.sh + tier-2 plan).
apparmor-parser
apparmor-utils
apparmor-profiles
# admin essentials
git
vim-enhanced
@ -978,12 +1043,39 @@ run_install() {
--show-output \
-- bash -c 'anaconda --cmdline --kickstart=/run/install/veilor-generated.ks 2>&1 | tee /tmp/anaconda-cmdline.log' || rc=$?
if [[ $rc -eq 0 ]]; then
# v0.6: split the success screen into THREE stacked boxes.
#
# 1. Green success box — quiet confirmation.
# 2. Yellow eject box — promoted out of the buried
# one-liner the v0.5 success box used. Operators on
# both onyx and the friend's RTX 4080 rig missed the
# reminder and rebooted into the live ISO instead of
# the install. Now it's its own loud thick-bordered
# box that sits BELOW the success box and is
# impossible to miss.
# 3. Reboot countdown — embedded inside the green
# success box so the operator can see "complete +
# Xs to reboot" at a glance.
#
# Each tick clears + redraws all three, so the eject-media
# box stays in front of the operator for the full 10-second
# window and isn't scrolled off by a banner refresh.
local secs
for secs in 10 9 8 7 6 5 4 3 2 1; do
clear
gum style --foreground 2 --border rounded --margin "1 2" --padding "1 3" \
"✓ Install complete" \
"" \
"System will reboot in 5 seconds." \
"Remove the install media."
sleep 5
"Rebooting in ${secs}s..."
gum style --foreground 3 --border thick --margin "0 2" --padding "1 3" \
--border-foreground 3 \
" Remove the install media NOW " \
"" \
" Unplug the USB stick / eject the DVD before " \
" reboot, otherwise the system will boot back " \
" into the live ISO instead of your fresh install. "
sleep 1
done
systemctl reboot
else
prompt_error "Anaconda exited non-zero (status $rc).

77
scripts/40-apparmor.sh Normal file
View file

@ -0,0 +1,77 @@
#!/usr/bin/env bash
# veilor-os — 40-apparmor: load veilor-shipped AppArmor profiles in
# COMPLAIN mode. v0.6 scope: "loaded, present, nothing breaks".
#
# Per docs/research/2026-05-05-agent-wave/04-hardening-tier-2.md, v0.6
# ships AppArmor stacked alongside SELinux, but every veilor-shipped
# profile stays in complain mode (logs only, no enforce). Real policy
# authoring is post-v0.6.
#
# Idempotent: profiles already in complain mode are skipped. Run as
# root during kickstart %post or post-install.
set -uo pipefail
GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; NC='\033[0m'
ok() { echo -e "${GREEN}[OK]${NC} $*"; }
info() { echo -e "${YELLOW}[INFO]${NC} $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
err() { echo -e "${RED}[ERR]${NC} $*"; }
[[ $EUID -eq 0 ]] || { err "Must run as root"; exit 1; }
echo "════════════════════════════════════════════════════════"
echo " veilor-os :: 40-apparmor (complain mode only)"
echo "════════════════════════════════════════════════════════"
PROFILE_DIR=/etc/apparmor.d/veilor.d
# ── Sanity: tools present? ──
if ! command -v apparmor_parser >/dev/null 2>&1; then
warn "apparmor_parser not installed — skipping (package step missed?)"
exit 0
fi
if ! command -v aa-complain >/dev/null 2>&1; then
warn "aa-complain not installed (apparmor-utils missing) — skipping"
exit 0
fi
if [[ ! -d $PROFILE_DIR ]]; then
info "$PROFILE_DIR not present — no veilor profiles to load"
exit 0
fi
# ── Walk every profile we ship and force complain mode ──
shopt -s nullglob
loaded=0
skipped=0
failed=0
for profile in "$PROFILE_DIR"/*; do
[[ -f $profile ]] || continue
name=$(basename "$profile")
# Already in complain mode? aa-status reports loaded profiles by
# internal profile name, not file path — best-effort match against
# the file basename to avoid re-parsing on repeat runs.
if command -v aa-status >/dev/null 2>&1 \
&& aa-status --complaining 2>/dev/null | grep -qE "(^|/)veilor-${name}([[:space:]]|$)"; then
info "$name already in complain mode — skipping"
skipped=$((skipped + 1))
continue
fi
info "loading $name (complain mode)"
if aa-complain "$profile" >/dev/null 2>&1; then
ok "$name → complain"
loaded=$((loaded + 1))
else
warn "$name failed to load (parser may reject stub on this kernel)"
failed=$((failed + 1))
fi
done
echo "────────────────────────────────────────────────────────"
info "summary: loaded=$loaded skipped=$skipped failed=$failed"
ok "v0.6 AppArmor stub: complain-mode only — no enforcement, log-only"
exit 0

View file

@ -9,6 +9,50 @@ Entries are newest-first.
---
## 2026-05-06 · v0.5.32 · ISO build path moved to Forgejo
**Change:** Build host for the test ISO has moved off GitHub Actions
onto the Forgejo runner on nullstone. The hybrid VM test procedure in
`TESTING.md` is **unchanged** — the gum installer still drives every
step it can, the operator still types the LUKS + admin passwords
directly into the QEMU window. The only thing different is where the
ISO comes from and how the host log is captured.
**Practical deltas for testers:**
- ISO download: from the Forgejo `ci-latest` rolling release at
<https://git.s8n.ru/veilor-org/veilor-os/releases/tag/ci-latest>.
The tag is force-replaced on each successful `build-iso.yml` run, so
always re-download — don't rely on a cached copy.
- Re-flash to USB / virtio-blk image via Etcher / `dd`**unchanged**.
Same `sha256sum -c` step; same image format.
- virtio-9p host log capture is now **active by default** in
`test/run-vm.sh`. This replaces the broken virtio-serial path
flagged by Agent 6 in the 2026-05-05 wave; Anaconda logs land in the
host-side mount automatically once the VM boots, no manual `tail -f`
on a broken serial console.
- Build host for the record: forgejo-runner on nullstone, runner label
`ubuntu-24.04`, image `catthehacker/ubuntu:act-24.04`. Reproducibility
is unchanged from the GH Actions ubuntu-24.04 base — the act image
matches GHA's runner image to within package versions.
**Why:** GitHub mirror was disabled 2026-05-06 (repo is now
private-by-default on Forgejo); GH Actions builds would just stop
producing artifacts. Moving CI in-house onto nullstone keeps the
test/release loop intact and removes the external dependency for
private-build cycles. Documenting the change here so a future tester
reading TESTING.md doesn't waste time hunting an artifact in a
GitHub run that never happened.
**Files touched in this entry:**
- `test/METHOD-CHANGELOG.md` — this entry.
`test/TESTING.md` itself is **not** edited — the procedure prose still
applies verbatim. Only the build host and the URL where the ISO lives
changed.
---
## 2026-05-05 · v0.5.27 · TESTING.md created
**Change:** First version of the canonical procedure document.

View file

@ -0,0 +1,142 @@
# Test run — v0.5.32
- **Date:** 2026-05-06
- **ISO:** `veilor-os-43-20260506-HHMMSS.iso` (sha256: `TBD — fill in once A1 reports the build artifact`)
- **Tester:** A1 (build) + operator (P) + A5 (report scribe)
- **Build host:** forgejo-runner on nullstone (runner label `ubuntu-24.04`,
image `catthehacker/ubuntu:act-24.04`); first ISO produced off the
Forgejo build pipeline after the GH Actions mirror was disabled
2026-05-06.
- **Environment:** VM (qemu/q35/ovmf, 4 vCPU, 4 GiB RAM, virtio-vga,
virtio-9p host log mount). Real-hardware run is a separate report —
this file is the VM run only.
---
## Result
**Pending A1 build.** Operator + A5 fill in pass/fail per-step once
the actual VM test is walked through against the v0.5.32 ISO. Until
the ISO sha256 lands here, treat every row in the per-step table as
unverified.
One-line summary (write here once known): _TBD_.
---
## Regressions vs previous run
(v0.5.31 was the last tagged release; compare against any pass-with-issues
notes from that test run if a report exists. Empty otherwise — fill in
during the actual test walkthrough.)
- _TBD_
---
## Per-step results
Walk `test/TESTING.md` step-by-step. Mark each pass/fail with a brief
note when failed. Until the test runs, every row is `⏳ pending`.
| # | Step | Result | Notes |
|----|-----------------------------------|--------|-------|
| 1 | Live boot to installer banner | ⏳ pending | |
| 2 | Installer menu render | ⏳ pending | |
| 3 | Disk picker | ⏳ pending | |
| 4 | LUKS + admin passwords | ⏳ pending | Operator types directly into QEMU window — plymouth ignores synthesised keys. |
| 5 | Locale | ⏳ pending | |
| 6 | Confirm | ⏳ pending | |
| 7 | Anaconda transaction | ⏳ pending | |
| 8 | Reboot | ⏳ pending | |
| 9 | GRUB single veilor-os entry | ⏳ pending | |
| 10 | LUKS unlock prompt | ⏳ pending | |
| 11 | First boot → SDDM → KDE | ⏳ pending | |
| 12 | Hardening checks | ⏳ pending | |
---
## Hardening verification
```text
$ getenforce
TBD
$ systemctl is-active fail2ban usbguard tuned auditd firewalld
TBD
$ cat /proc/cmdline
TBD — must include rd.luks.uuid=luks-... and the v0.5.32 cmdline set.
$ lsblk -f
TBD
$ systemctl is-enabled veilor-firstboot.service
TBD — must report enabled with WantedBy=graphical.target (blocker #2).
$ nft list ruleset | grep -i tailscale
TBD — tailscale0 must be in the trusted zone (blocker #5).
$ cat /etc/skel/.config/kdeglobals 2>/dev/null | head
TBD — branding must be present (blocker #6).
$ ls /var/log/anaconda/host-9p-mount/
TBD — virtio-9p Anaconda log capture (blocker #7).
```
Paste real output. If any service is inactive, any cmdline arg is
missing, or any blocker artifact is absent, raise as a Regression
above.
---
## Findings
The 7 v0.5.32 blocker fixes from the
[2026-05-05 9-agent research wave](../../docs/research/2026-05-05-agent-wave/README.md)
land in this build. Each is listed here as an **expected behaviour**
the tester must observe — if any of these regress, log it under
Regressions above.
1. **Suspend/resume wifi survives lid-close.** `kernel.modules_disabled=1`
no longer fires before the wifi module reloads on resume. Test:
suspend the VM (or lid-close on real HW), wake, reconnect to the
same network without manual `modprobe`.
2. **`veilor-firstboot.service` is `WantedBy=graphical.target`.** The
first-boot admin password flow must run on real installs, not just
on multi-user.target boots. Test: fresh install boots straight to
the TTY password prompt before SDDM lights up.
3. **Kernel-upgrade does not drift GRUB.** First `dnf upgrade kernel`
must leave the system bootable — `grub2-mkconfig` is wired into the
kernel-install hook. Test: install, run `sudo dnf upgrade kernel`,
reboot, system comes up.
4. **USBGuard rules are id-based, not hash + parent-hash.** Mirrors the
onyx dock-replug fix in `feedback_usbguard_dock.md`. Test:
unplug/replug a known device — it stays allowed. The hash variant
re-blocks on every replug; the id variant must not.
5. **firewalld trusts `tailscale0`.** The interface is in the trusted
zone out-of-the-box. Test: bring tailscale up, ping a peer in the
mesh — no firewall mods required.
6. **`/etc/skel/` carries veilor branding.** New users get the black
colour scheme, Konsole profile, and Plasma layout on first login.
Test: `useradd test`; log in as `test`; KDE comes up branded, no
white flash, Fira Code system font.
7. **virtio-9p Anaconda log capture is active by default.**
`test/run-vm.sh` mounts a host directory into the VM; Anaconda logs
land there during install. Replaces the broken virtio-serial path
from earlier runs. Test: run install in VM; host-side mount has
`program.log`, `storage.log`, `packaging.log` populated.
Free-form notes from the actual walkthrough — cosmetic glitches, slow
paths, surprising behaviour — append below.
- _TBD — fill in during the operator-driven VM run._
---
## Action items for next release
(Empty until the test exposes something. PRs / commits opened during
the run go here.)
- [ ] _TBD_