Compare commits
1 commit
f4ea27271d
...
fe3239d5ed
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe3239d5ed |
12 changed files with 54 additions and 498 deletions
54
.github/workflows/build-iso.yml
vendored
54
.github/workflows/build-iso.yml
vendored
|
|
@ -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
|
||||
|
||||
on:
|
||||
|
|
@ -22,9 +20,7 @@ on:
|
|||
types: [published]
|
||||
|
||||
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
|
||||
contents: write # needed for action-gh-release to create+update ci-latest
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
|
@ -34,9 +30,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout
|
||||
# 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
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Free up disk
|
||||
run: |
|
||||
|
|
@ -45,14 +39,9 @@ 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:
|
||||
# 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
|
||||
image: registry.fedoraproject.org/fedora:43
|
||||
options: |
|
||||
--privileged
|
||||
-v ${{ github.workspace }}:/work
|
||||
|
|
@ -208,42 +197,13 @@ 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'
|
||||
# Pinned to last v2 tag confirmed to ship on node20.
|
||||
uses: softprops/action-gh-release@v2.0.4
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ci-latest
|
||||
name: "ci-latest (auto)"
|
||||
|
|
@ -264,9 +224,6 @@ 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.
|
||||
|
|
@ -342,8 +299,7 @@ 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'
|
||||
# Pinned to last v2 tag confirmed to ship on node20.
|
||||
uses: softprops/action-gh-release@v2.0.4
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: |
|
||||
build/out/*.iso
|
||||
|
|
|
|||
9
.github/workflows/lint.yml
vendored
9
.github/workflows/lint.yml
vendored
|
|
@ -12,8 +12,7 @@ jobs:
|
|||
container:
|
||||
image: registry.fedoraproject.org/fedora:43
|
||||
steps:
|
||||
# Pinned to last v4 tag confirmed to ship on node20.
|
||||
- uses: actions/checkout@v4.1.7
|
||||
- uses: actions/checkout@v4
|
||||
- run: dnf -y install pykickstart
|
||||
- run: ksvalidator kickstart/veilor-os.ks
|
||||
|
||||
|
|
@ -21,8 +20,7 @@ jobs:
|
|||
name: Shell scripts
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
# Pinned to last v4 tag confirmed to ship on node20.
|
||||
- uses: actions/checkout@v4.1.7
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ludeeus/action-shellcheck@master
|
||||
with:
|
||||
severity: warning
|
||||
|
|
@ -32,8 +30,7 @@ jobs:
|
|||
name: No personal/onyx leaks
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
# Pinned to last v4 tag confirmed to ship on node20.
|
||||
- uses: actions/checkout@v4.1.7
|
||||
- uses: actions/checkout@v4
|
||||
- name: Grep for leaks
|
||||
run: |
|
||||
set -e
|
||||
|
|
|
|||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -13,6 +13,4 @@ secrets/
|
|||
*.pem
|
||||
test/veilor-vm.qcow2
|
||||
test/veilor-vm.nvram*
|
||||
test/auto-install-vm.qcow2
|
||||
test/auto-install-vm.nvram*
|
||||
.claude/worktrees/
|
||||
|
|
|
|||
11
README.md
11
README.md
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
> **Hardened minimal Fedora KDE spin. Black-on-black. Locked down by default.**
|
||||
|
||||
[](https://git.s8n.ru/veilor-org/veilor-os/actions?workflow=build-iso.yml)
|
||||
[](https://github.com/veilor-org/veilor-os/actions/workflows/build-iso.yml)
|
||||
[](LICENSE)
|
||||
|
||||
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
|
||||
[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,
|
||||
|
|
@ -51,9 +46,7 @@ spike at v0.7**, **bootc-only at v1.0**.
|
|||
## Quick install
|
||||
|
||||
```bash
|
||||
# 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)
|
||||
# 1. Download the ISO (after public release; CI artifact for now)
|
||||
sha256sum -c veilor-os-43-*.iso.sha256
|
||||
|
||||
# 2. Flash to USB. Replace /dev/sdX with your USB device — triple-check.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# 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
|
||||
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
|
||||
|
||||
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 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. |
|
||||
| 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. |
|
||||
|
||||
---
|
||||
|
||||
## Out of scope — what veilor-os does NOT defend against
|
||||
|
||||
These adversaries are unambiguously outside our scope. Pretending otherwise
|
||||
gets people hurt. **If your adversary is on this list, pick a different tool.**
|
||||
We are honest about this list because pretending otherwise is how people get
|
||||
hurt. **If your adversary is here, pick a different tool.**
|
||||
|
||||
| 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. |
|
||||
| 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. |
|
||||
| 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. |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -95,21 +92,19 @@ 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 | 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 |
|
||||
| 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) | ✓ |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -119,12 +119,6 @@ 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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -69,21 +69,6 @@ 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" \
|
||||
|
|
@ -165,30 +150,10 @@ 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
|
||||
# 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"
|
||||
gum input --password --header "$header"
|
||||
else
|
||||
whiptail --title "veilor-os" --passwordbox "$header" 10 60 \
|
||||
3>&1 1>&2 2>&3
|
||||
|
|
@ -288,36 +253,12 @@ 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" || 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
|
||||
luks_pw=$(prompt_password "[2/3] Encryption · LUKS2 passphrase (min 8)") || return 1
|
||||
validate_pw "$luks_pw" "passphrase" || return 1
|
||||
|
||||
# ── 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" || 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
|
||||
admin_pw=$(prompt_password "[3/3] Admin user · password for 'admin'") || return 1
|
||||
validate_pw "$admin_pw" "password" || return 1
|
||||
|
||||
# ── Locale ──
|
||||
# Hardcoded en_US.UTF-8 for branded consistency. The picker that
|
||||
|
|
@ -547,12 +488,6 @@ 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
|
||||
|
|
@ -1043,39 +978,12 @@ 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" \
|
||||
"" \
|
||||
"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
|
||||
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
|
||||
systemctl reboot
|
||||
else
|
||||
prompt_error "Anaconda exited non-zero (status $rc).
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
||||
**Change:** First version of the canonical procedure document.
|
||||
|
|
|
|||
|
|
@ -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_
|
||||
Loading…
Reference in a new issue