Compare commits
22 commits
fe3239d5ed
...
f4ea27271d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4ea27271d | ||
| 8b1b49b5fc | |||
| a125e46c5e | |||
| 3d35196a2d | |||
| 1db860c557 | |||
| d042552752 | |||
| 8221228824 | |||
| 2f0403adfd | |||
| ec52fd4994 | |||
|
|
dfda66ac7e | ||
|
|
e4b6516f1c | ||
|
|
73c2c68e32 | ||
|
|
0b1a33326a | ||
|
|
b74ef5005d | ||
|
|
84275e2515 | ||
|
|
0a1b81a9e0 | ||
|
|
6db4b759fa | ||
|
|
b64ee19e53 | ||
|
|
138a04702c | ||
|
|
767fe71a3a | ||
|
|
4c1895dcc1 | ||
|
|
6d86e00284 |
12 changed files with 498 additions and 54 deletions
52
.github/workflows/build-iso.yml
vendored
52
.github/workflows/build-iso.yml
vendored
|
|
@ -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
|
name: Build veilor-os ISO
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
|
@ -21,6 +23,8 @@ on:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write # needed for action-gh-release to create+update ci-latest
|
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:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|
@ -30,7 +34,9 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- 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
|
- name: Free up disk
|
||||||
run: |
|
run: |
|
||||||
|
|
@ -39,9 +45,14 @@ jobs:
|
||||||
df -h
|
df -h
|
||||||
|
|
||||||
- name: Run build inside Fedora 43 container
|
- 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
|
uses: addnab/docker-run-action@v3
|
||||||
with:
|
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: |
|
options: |
|
||||||
--privileged
|
--privileged
|
||||||
-v ${{ github.workspace }}:/work
|
-v ${{ github.workspace }}:/work
|
||||||
|
|
@ -197,13 +208,42 @@ jobs:
|
||||||
echo "[OK] split into:"
|
echo "[OK] split into:"
|
||||||
ls "${ISO}".part-*
|
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
|
# GitHub-only: softprops/action-gh-release uses the GitHub REST API
|
||||||
# which Forgejo doesn't expose at the same endpoints. When this
|
# which Forgejo doesn't expose at the same endpoints. When this
|
||||||
# workflow runs on git.s8n.ru the step below (Forgejo) handles
|
# workflow runs on git.s8n.ru the step below (Forgejo) handles
|
||||||
# publishing instead.
|
# publishing instead.
|
||||||
- name: Publish to ci-latest rolling prerelease (GitHub)
|
- name: Publish to ci-latest rolling prerelease (GitHub)
|
||||||
if: success() && github.ref == 'refs/heads/main' && github.server_url == 'https://github.com'
|
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:
|
with:
|
||||||
tag_name: ci-latest
|
tag_name: ci-latest
|
||||||
name: "ci-latest (auto)"
|
name: "ci-latest (auto)"
|
||||||
|
|
@ -224,6 +264,9 @@ jobs:
|
||||||
files: |
|
files: |
|
||||||
build/out/*.iso.part-*
|
build/out/*.iso.part-*
|
||||||
build/out/*.sha256
|
build/out/*.sha256
|
||||||
|
build/out/*.sig
|
||||||
|
build/out/*.pem
|
||||||
|
build/out/*.spdx.json
|
||||||
|
|
||||||
# Forgejo equivalent: drop+recreate ci-latest release via the
|
# Forgejo equivalent: drop+recreate ci-latest release via the
|
||||||
# Forgejo REST API, then upload chunks. Only runs when not on GitHub.
|
# 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.
|
# GitHub-only: same restriction as ci-latest publish.
|
||||||
- name: Attach to release on tag (GitHub)
|
- name: Attach to release on tag (GitHub)
|
||||||
if: github.event_name == 'release' && github.server_url == 'https://github.com'
|
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:
|
with:
|
||||||
files: |
|
files: |
|
||||||
build/out/*.iso
|
build/out/*.iso
|
||||||
|
|
|
||||||
9
.github/workflows/lint.yml
vendored
9
.github/workflows/lint.yml
vendored
|
|
@ -12,7 +12,8 @@ jobs:
|
||||||
container:
|
container:
|
||||||
image: registry.fedoraproject.org/fedora:43
|
image: registry.fedoraproject.org/fedora:43
|
||||||
steps:
|
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: dnf -y install pykickstart
|
||||||
- run: ksvalidator kickstart/veilor-os.ks
|
- run: ksvalidator kickstart/veilor-os.ks
|
||||||
|
|
||||||
|
|
@ -20,7 +21,8 @@ jobs:
|
||||||
name: Shell scripts
|
name: Shell scripts
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
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
|
- uses: ludeeus/action-shellcheck@master
|
||||||
with:
|
with:
|
||||||
severity: warning
|
severity: warning
|
||||||
|
|
@ -30,7 +32,8 @@ jobs:
|
||||||
name: No personal/onyx leaks
|
name: No personal/onyx leaks
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
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
|
- name: Grep for leaks
|
||||||
run: |
|
run: |
|
||||||
set -e
|
set -e
|
||||||
|
|
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -13,4 +13,6 @@ secrets/
|
||||||
*.pem
|
*.pem
|
||||||
test/veilor-vm.qcow2
|
test/veilor-vm.qcow2
|
||||||
test/veilor-vm.nvram*
|
test/veilor-vm.nvram*
|
||||||
|
test/auto-install-vm.qcow2
|
||||||
|
test/auto-install-vm.nvram*
|
||||||
.claude/worktrees/
|
.claude/worktrees/
|
||||||
|
|
|
||||||
11
README.md
11
README.md
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
> **Hardened minimal Fedora KDE spin. Black-on-black. Locked down by default.**
|
> **Hardened minimal Fedora KDE spin. Black-on-black. Locked down by default.**
|
||||||
|
|
||||||
[](https://github.com/veilor-org/veilor-os/actions/workflows/build-iso.yml)
|
[](https://git.s8n.ru/veilor-org/veilor-os/actions?workflow=build-iso.yml)
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
|
|
||||||
veilor-os is a Fedora 43 KDE Plasma remix for operators who want a clean,
|
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
|
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).
|
[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,
|
What is **shipping**: hardening (SELinux, sysctl, USBGuard, fail2ban,
|
||||||
firewalld), KDE black theme, Fira Code system font, 3-mode power
|
firewalld), KDE black theme, Fira Code system font, 3-mode power
|
||||||
management, single-prompt LUKS install, first-boot admin password flow,
|
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
|
## Quick install
|
||||||
|
|
||||||
```bash
|
```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
|
sha256sum -c veilor-os-43-*.iso.sha256
|
||||||
|
|
||||||
# 2. Flash to USB. Replace /dev/sdX with your USB device — triple-check.
|
# 2. Flash to USB. Replace /dev/sdX with your USB device — triple-check.
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Threat Model
|
# 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
|
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
|
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
|
## 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 |
|
| 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. |
|
| 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 + `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. |
|
| 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, default-block, empty allowlist on first boot. New device = explicit operator allow. |
|
| 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 | sshd password-auth off, root login off, MaxAuthTries=3, fail2ban with sshd + pam-generic jails wired to firewalld rich-rule. |
|
| 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 covering passwd/shadow/sudoers/ssh/cron/sysctl/kernel modules and all privileged binaries. Logs survive reboot. |
|
| 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 | Fedora's signed shim → GRUB → kernel chain (Secure Boot enforced). v0.4 adds GPG-signed ISO + sha256 + own MOK. |
|
| 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 -S root` → `L`), single sudo user with pwquality minlen=14 / 4 classes, kernel module loading frozen 30 s after graphical boot. |
|
| 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/avahi/bluetooth/ModemManager/kdeconnectd/PackageKit are masked. |
|
| 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) | NTS-authenticated chrony, DNS-over-TLS via systemd-resolved, LLMNR off. |
|
| 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
|
## Out of scope — what veilor-os does NOT defend against
|
||||||
|
|
||||||
We are honest about this list because pretending otherwise is how people get
|
These adversaries are unambiguously outside our scope. Pretending otherwise
|
||||||
hurt. **If your adversary is here, pick a different tool.**
|
gets people hurt. **If your adversary is on this list, pick a different tool.**
|
||||||
|
|
||||||
| Adversary / scenario | Why veilor-os doesn't help | Use instead |
|
| 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. |
|
| 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 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. |
|
| 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 / hardware mod between keyboard and machine | We're software. Software cannot detect a passive hardware tap. | Physical custody of the device. Tamper-evident seals. |
|
| 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, 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). |
|
| 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 analysis) | We use stock kernel crypto. No constant-time guarantees beyond what the kernel/CPU provide. | Threat-specific HSM. |
|
| 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 (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. |
|
| 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. We don't onion-route. | Tails, Whonix, Tor. |
|
| 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 (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. |
|
| 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 enough physical time and tools, any laptop falls. | Operational security, not OS choice. |
|
| 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,
|
Scoring legend: `✓` shipped & on by default, `~` partial / opt-in,
|
||||||
`✗` not provided, `n/a` not applicable to that distro's model.
|
`✗` 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 |
|
| Axis | veilor-os | Stock Fedora KDE | Kicksecure | Tails | Qubes OS | secureblue | Athena OS |
|
||||||
|---|:---:|:---:|:---:|:---:|:---:|:---:|
|
|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|
||||||
| **Encrypted at rest by default** | ✓ (LUKS2 argon2id) | ~ (optional) | ✓ | n/a (amnesic) | ✓ | ✓ |
|
| **Encrypted at rest by default** | ✓ (LUKS2 argon2id, mem=1 GiB) | ~ (optional in Anaconda) | ✓ | n/a (amnesic, session-only) | ✓ | ✓ | ~ (optional) |
|
||||||
| **MAC enforcing OOTB** | ✓ (SELinux + AppArmor v0.5) | ✓ (SELinux) | ✓ (AppArmor) | ✓ (AppArmor) | ✓ (per-VM) | ✓ (SELinux) |
|
| **MAC enforcing OOTB** | ✓ (SELinux + opt-in AppArmor) | ✓ (SELinux) | ✓ (AppArmor) | ✓ (AppArmor) | ✓ (per-VM) | ✓ (SELinux) | ✓ (AppArmor) |
|
||||||
| **Default-deny firewall** | ✓ | ✗ | ✓ | ✓ (Tor-only) | ✓ | ✓ |
|
| **Default-deny firewall** | ✓ (firewalld zone=drop) | ✗ | ✓ | ✓ (Tor-only) | ✓ | ✓ | ✓ |
|
||||||
| **USB default-block** | ✓ (USBGuard) | ✗ | ✓ | ✓ | ✓ (sys-usb) | ✓ |
|
| **USB default-block** | ✓ (USBGuard, id-rules) | ✗ | ✓ | ✓ | ✓ (sys-usb) | ✓ (USBGuard) | ✗ |
|
||||||
| **Per-app isolation (VM/sandbox)** | ✗ | ✗ | ✗ | ~ (AppArmor) | ✓ (Xen VMs) | ~ (Flatpak/bwrap) |
|
| **Per-app isolation (VM/sandbox)** | ✗ | ✗ | ✗ | ~ (AppArmor) | ✓ (Xen VMs) | ~ (Flatpak/bwrap) | ✗ |
|
||||||
| **Anonymity / Tor by default** | ✗ | ✗ | ✗ | ✓ | ~ (Whonix VMs) | ✗ |
|
| **Anonymity / Tor by default** | ✗ | ✗ | ✗ | ✓ | ~ (Whonix VMs) | ✗ | ✗ |
|
||||||
| **Daily driver target (persistent)** | ✓ | ✓ | ✓ | ✗ | ✓ (heavy) | ✓ |
|
| **Daily driver target (persistent)** | ✓ | ✓ | ✓ | ✗ (amnesic) | ✓ (heavy, hardware-partitioning) | ✓ | ✓ |
|
||||||
| **Signed releases (publisher key)** | ✓ (v0.4) | ✓ | ✓ | ✓ | ✓ | ✓ |
|
| **Signed releases (cosign + GPG)** | ✓ (v0.7) | ✓ | ✓ | ✓ | ✓ | ✓ (cosign on OCI) | ~ (sha256 only) |
|
||||||
| **Threat model published** | ✓ (this doc) | ✗ | ✓ | ✓ | ✓ | ✓ |
|
| **Threat model published** | ✓ (this doc) | ✗ | ✓ | ✓ | ✓ | ✗ | ✓ |
|
||||||
| **Hardware compatibility (laptops)** | ✓ (Fedora kernel) | ✓ | ~ | ~ (live USB) | ~ (Xen-pinned) | ✓ |
|
| **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 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -119,6 +119,12 @@ chrony
|
||||||
firewalld
|
firewalld
|
||||||
plymouth
|
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
|
# admin essentials
|
||||||
git
|
git
|
||||||
vim-enhanced
|
vim-enhanced
|
||||||
|
|
|
||||||
11
overlay/etc/apparmor.d/veilor.d/firefox
Normal file
11
overlay/etc/apparmor.d/veilor.d/firefox
Normal 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>
|
||||||
|
}
|
||||||
11
overlay/etc/apparmor.d/veilor.d/thunderbird
Normal file
11
overlay/etc/apparmor.d/veilor.d/thunderbird
Normal 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>
|
||||||
|
}
|
||||||
|
|
@ -69,6 +69,21 @@ banner() {
|
||||||
local vline="veilor-os ${ver} · ${d} · live"
|
local vline="veilor-os ${ver} · ${d} · live"
|
||||||
|
|
||||||
if [[ -r $BANNER_FILE ]]; then
|
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
|
if [[ $TUI == gum ]]; then
|
||||||
# gum style: rounded border, banner + blank line + version line.
|
# gum style: rounded border, banner + blank line + version line.
|
||||||
gum style --border rounded --margin "0 2" --padding "1 3" \
|
gum style --border rounded --margin "0 2" --padding "1 3" \
|
||||||
|
|
@ -150,10 +165,30 @@ prompt_input() {
|
||||||
}
|
}
|
||||||
|
|
||||||
# prompt_password <header>
|
# 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() {
|
prompt_password() {
|
||||||
local header=$1
|
local header=$1
|
||||||
if [[ $TUI == gum ]]; then
|
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
|
else
|
||||||
whiptail --title "veilor-os" --passwordbox "$header" 10 60 \
|
whiptail --title "veilor-os" --passwordbox "$header" 10 60 \
|
||||||
3>&1 1>&2 2>&3
|
3>&1 1>&2 2>&3
|
||||||
|
|
@ -253,12 +288,36 @@ collect_answers() {
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── LUKS passphrase ──
|
# ── 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
|
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 ──
|
# ── 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
|
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 ──
|
# ── Locale ──
|
||||||
# Hardcoded en_US.UTF-8 for branded consistency. The picker that
|
# Hardcoded en_US.UTF-8 for branded consistency. The picker that
|
||||||
|
|
@ -488,6 +547,12 @@ chrony
|
||||||
firewalld
|
firewalld
|
||||||
plymouth
|
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
|
# admin essentials
|
||||||
git
|
git
|
||||||
vim-enhanced
|
vim-enhanced
|
||||||
|
|
@ -978,12 +1043,39 @@ run_install() {
|
||||||
--show-output \
|
--show-output \
|
||||||
-- bash -c 'anaconda --cmdline --kickstart=/run/install/veilor-generated.ks 2>&1 | tee /tmp/anaconda-cmdline.log' || rc=$?
|
-- bash -c 'anaconda --cmdline --kickstart=/run/install/veilor-generated.ks 2>&1 | tee /tmp/anaconda-cmdline.log' || rc=$?
|
||||||
if [[ $rc -eq 0 ]]; then
|
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" \
|
gum style --foreground 2 --border rounded --margin "1 2" --padding "1 3" \
|
||||||
"✓ Install complete" \
|
"✓ Install complete" \
|
||||||
"" \
|
"" \
|
||||||
"System will reboot in 5 seconds." \
|
"Rebooting in ${secs}s..."
|
||||||
"Remove the install media."
|
gum style --foreground 3 --border thick --margin "0 2" --padding "1 3" \
|
||||||
sleep 5
|
--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
|
systemctl reboot
|
||||||
else
|
else
|
||||||
prompt_error "Anaconda exited non-zero (status $rc).
|
prompt_error "Anaconda exited non-zero (status $rc).
|
||||||
|
|
|
||||||
77
scripts/40-apparmor.sh
Normal file
77
scripts/40-apparmor.sh
Normal 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
|
||||||
|
|
@ -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
|
## 2026-05-05 · v0.5.27 · TESTING.md created
|
||||||
|
|
||||||
**Change:** First version of the canonical procedure document.
|
**Change:** First version of the canonical procedure document.
|
||||||
|
|
|
||||||
142
test/test-runs/2026-05-06-v0.5.32-build.md
Normal file
142
test/test-runs/2026-05-06-v0.5.32-build.md
Normal 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_
|
||||||
Loading…
Reference in a new issue