Compare commits

..

1 commit

Author SHA1 Message Date
s8n-ru
fe3239d5ed ci: gate softprops release steps + add Forgejo API equivalents
Some checks failed
Lint / Kickstart syntax (pull_request) Failing after 3s
Lint / Shell scripts (pull_request) Failing after 29s
Lint / No personal/onyx leaks (pull_request) Failing after 34s
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 10:31:39 +01:00
12 changed files with 54 additions and 498 deletions

View file

@ -1,5 +1,3 @@
# 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:
@ -23,8 +21,6 @@ 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:
@ -34,9 +30,7 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
# Pinned to last v4 tag confirmed to ship on node20. v4.2+ ships uses: actions/checkout@v4
# 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: |
@ -45,14 +39,9 @@ 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:
# Pinned to digest from `skopeo inspect --raw` on 2026-05-06. image: registry.fedoraproject.org/fedora:43
# 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
@ -208,42 +197,13 @@ 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'
# Pinned to last v2 tag confirmed to ship on node20. uses: softprops/action-gh-release@v2
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)"
@ -264,9 +224,6 @@ 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.
@ -342,8 +299,7 @@ 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'
# Pinned to last v2 tag confirmed to ship on node20. uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v2.0.4
with: with:
files: | files: |
build/out/*.iso build/out/*.iso

View file

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

2
.gitignore vendored
View file

@ -13,6 +13,4 @@ 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/

View file

@ -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.**
[![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) [![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)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) [![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, veilor-os is a Fedora 43 KDE Plasma remix for operators who want a clean,
@ -30,11 +30,6 @@ 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,
@ -51,9 +46,7 @@ spike at v0.7**, **bootc-only at v1.0**.
## Quick install ## Quick install
```bash ```bash
# 1. Download the ISO from the latest Forgejo release. # 1. Download the ISO (after public release; CI artifact for now)
# 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.

View file

@ -1,6 +1,6 @@
# Threat Model # Threat Model
> **Status:** Final for v0.7 public launch. Honest scope. > **Status:** Draft for v0.7 public flex. 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,39 +14,36 @@ 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 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. | | 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` + 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). | | 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, `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. | | 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 | `/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. | | 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 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. | | 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 | 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. | | 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 -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`. | | 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`, `cups-browsed`, `geoclue`, `avahi-daemon`, `bluetooth`, `ModemManager`, `gssproxy`, `atd`, `pcscd.{socket,service}` are masked; `kdeconnectd` and `PackageKit` are removed at the package level. | | 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) | `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. | | Time-based MITM (back-dated certs, replay) | NTS-authenticated chrony, DNS-over-TLS via systemd-resolved, LLMNR off. |
--- ---
## Out of scope — what veilor-os does NOT defend against ## Out of scope — what veilor-os does NOT defend against
These adversaries are unambiguously outside our scope. Pretending otherwise We are honest about this list because pretending otherwise is how people get
gets people hurt. **If your adversary is on this list, pick a different tool.** hurt. **If your adversary is here, pick a different tool.**
| Adversary / scenario | Why veilor-os doesn't help | Use instead | | Adversary / scenario | Why veilor-os doesn't help | Use instead |
|---|---|---| |---|---|---|
| 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. | | 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 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. | | 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 / interposer between keyboard and machine | veilor-os is software. Software cannot detect a passive hardware tap. | Physical custody of the device. Tamper-evident seals. | | 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, 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). | | 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, 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. | | 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 (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). | | 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. veilor-os does not onion-route. | Tails, Whonix, Tor. | | 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 (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. | | 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 unlimited physical time and tools, any laptop falls. | Operational security, not OS choice. | | Adversary with sustained physical access and time | Given enough physical time and tools, any laptop falls. | Operational security, not OS choice. |
--- ---
@ -95,21 +92,19 @@ 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 | Athena OS | | Axis | veilor-os | Stock Fedora KDE | Kicksecure | Tails | Qubes OS | secureblue |
|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:| |---|:---:|:---:|:---:|:---:|:---:|:---:|
| **Encrypted at rest by default** | ✓ (LUKS2 argon2id, mem=1 GiB) | ~ (optional in Anaconda) | ✓ | n/a (amnesic, session-only) | ✓ | ✓ | ~ (optional) | | **Encrypted at rest by default** | ✓ (LUKS2 argon2id) | ~ (optional) | ✓ | n/a (amnesic) | ✓ | ✓ |
| **MAC enforcing OOTB** | ✓ (SELinux + opt-in AppArmor) | ✓ (SELinux) | ✓ (AppArmor) | ✓ (AppArmor) | ✓ (per-VM) | ✓ (SELinux) | ✓ (AppArmor) | | **MAC enforcing OOTB** | ✓ (SELinux + AppArmor v0.5) | ✓ (SELinux) | ✓ (AppArmor) | ✓ (AppArmor) | ✓ (per-VM) | ✓ (SELinux) |
| **Default-deny firewall** | ✓ (firewalld zone=drop) | ✗ | ✓ | ✓ (Tor-only) | ✓ | ✓ | ✓ | | **Default-deny firewall** | ✓ | ✗ | ✓ | ✓ (Tor-only) | ✓ | ✓ |
| **USB default-block** | ✓ (USBGuard, id-rules) | ✗ | ✓ | ✓ | ✓ (sys-usb) | ✓ (USBGuard) | ✗ | | **USB default-block** | ✓ (USBGuard) | ✗ | ✓ | ✓ | ✓ (sys-usb) | ✓ |
| **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)** | ✓ | ✓ | ✓ | ✗ (amnesic) | ✓ (heavy, hardware-partitioning) | ✓ | ✓ | | **Daily driver target (persistent)** | ✓ | ✓ | ✓ | ✗ | ✓ (heavy) | ✓ |
| **Signed releases (cosign + GPG)** | ✓ (v0.7) | ✓ | ✓ | ✓ | ✓ | ✓ (cosign on OCI) | ~ (sha256 only) | | **Signed releases (publisher key)** | ✓ (v0.4) | ✓ | ✓ | ✓ | ✓ | ✓ |
| **Threat model published** | ✓ (this doc) | ✗ | ✓ | ✓ | ✓ | ✗ | ✓ | | **Threat model published** | ✓ (this doc) | ✗ | ✓ | ✓ | ✓ | ✓ |
| **Hardware compatibility (laptops)** | ✓ (Fedora kernel) | ✓ | ~ | ~ (live USB) | ~ (Xen-pinned HCL) | ✓ | ✓ (Arch kernel) | | **Hardware compatibility (laptops)** | ✓ (Fedora kernel) | ✓ | ~ | ~ (live USB) | ~ (Xen-pinned) | ✓ |
| **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,12 +119,6 @@ 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

View file

@ -1,11 +0,0 @@
# 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

@ -1,11 +0,0 @@
# 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,21 +69,6 @@ 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" \
@ -165,30 +150,10 @@ 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
# Render the prompt header as a styled box so it looks at home gum input --password --header "$header"
# 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
@ -288,36 +253,12 @@ 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" || continue validate_pw "$luks_pw" "passphrase" || return 1
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" || continue validate_pw "$admin_pw" "password" || return 1
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
@ -547,12 +488,6 @@ 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
@ -1043,39 +978,12 @@ 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" \
"" \ "" \
"Rebooting in ${secs}s..." "System will reboot in 5 seconds." \
gum style --foreground 3 --border thick --margin "0 2" --padding "1 3" \ "Remove the install media."
--border-foreground 3 \ sleep 5
" 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).

View file

@ -1,77 +0,0 @@
#!/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,50 +9,6 @@ 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.

View file

@ -1,142 +0,0 @@
# 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_