Compare commits

..

1 commit

Author SHA1 Message Date
veilor-org
d7fa3e7b51 docs: CHANGELOG v0.2.0-v0.2.5, README rewrite, ROADMAP, release notes update 2026-05-01 23:41:50 +01:00
67 changed files with 212 additions and 7276 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
on:
@ -21,322 +19,124 @@ on:
release:
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
jobs:
build:
name: Build live ISO
# nullstone label resolves to a privileged Fedora 43 container per
# the runner's RUNNER_LABELS map. Build runs directly in this job
# container — no nested docker-run-action, no bind-mount juggling.
runs-on: nullstone
runs-on: ubuntu-24.04
timeout-minutes: 90
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: Install build tooling (Fedora)
- name: Free up disk
run: |
set -euxo pipefail
dnf -y upgrade --refresh
dnf -y install \
lorax \
livecd-tools \
pykickstart \
python3-imgcreate \
anaconda-tui \
squashfs-tools \
xorriso \
createrepo_c \
git \
which \
shadow-utils \
syslinux \
tar \
curl \
sudo
sudo rm -rf /opt/hostedtoolcache /usr/share/dotnet /usr/local/lib/android /usr/local/share/boost
sudo apt-get clean
df -h
- name: Vendor gum binary into overlay
run: |
set -euxo pipefail
GUM_VERSION="0.17.0"
GUM_URL="https://github.com/charmbracelet/gum/releases/download/v${GUM_VERSION}/gum_${GUM_VERSION}_Linux_x86_64.tar.gz"
GUM_SHA256="69ee169bd6387331928864e94d47ed01ef649fbfe875baed1bbf27b5377a6fdb"
mkdir -p overlay/usr/local/bin
curl -fsSL "$GUM_URL" -o /tmp/gum.tgz
echo "$GUM_SHA256 /tmp/gum.tgz" | sha256sum -c -
tar -xzf /tmp/gum.tgz -C /tmp/
install -m 0755 "/tmp/gum_${GUM_VERSION}_Linux_x86_64/gum" overlay/usr/local/bin/gum
overlay/usr/local/bin/gum --version
echo "[OK] gum ${GUM_VERSION} vendored into overlay/usr/local/bin/"
- name: Build ISO with livecd-creator
run: |
set -euxo pipefail
# PATCH: livecd-creator bug — __get_efi_image_stanza writes
# `root=live:LABEL=...` instead of `live:CDLABEL=...` for dracut.
# Result: dracut hangs on parse-livenet looking for non-CD label.
# Fix in-place before running build.
LIVE_PY=$(python3 -c 'import imgcreate, os; print(os.path.dirname(imgcreate.__file__))')/live.py
sed -i 's|"live:LABEL=%(fslabel)s"|"live:CDLABEL=%(fslabel)s"|g' "$LIVE_PY"
grep -n 'CDLABEL=%(fslabel)s' "$LIVE_PY" || { echo "[ERR] patch failed"; exit 1; }
echo "[OK] livecd-creator patched: LABEL= → CDLABEL= for EFI dracut stanza"
# CI uses ks-ci.ks (no local fix-repo line). Generated from main ks.
# Drop `updates` repo: previously 404'd on repodata zchunk during
# Fedora mid-push windows. Base 43 ships the selinux-policy fix.
sed -e '/veilor-fix/d' \
-e '/^shutdown$/d' \
-e '/repo --name=updates/d' \
kickstart/veilor-os.ks > kickstart/veilor-os-ci.ks
ksvalidator kickstart/veilor-os-ci.ks
mkdir -p build/out
# The kickstart's %post --nochroot probes a fixed list of
# candidate paths to locate the repo source for overlay/scripts
# copy. /work is the canonical CI candidate; symlink the live
# workspace there so the existing probe finds it.
ln -sfn "$GITHUB_WORKSPACE" /work
mkdir -p /var/lmc /var/lmc-cache
livecd-creator \
--verbose \
--config kickstart/veilor-os-ci.ks \
--fslabel "veilor-os-43" \
--title "veilor-os" \
--product "veilor-os" \
--releasever "${{ github.event.inputs.releasever || '43' }}" \
--tmpdir /var/lmc \
--cache /var/lmc-cache 2>&1 | tee build/out/build.log
- name: Graft veilor source tree onto ISO
run: |
set -euxo pipefail
ISO_FILE=$(ls ./*.iso 2>/dev/null | head -1)
[ -n "$ISO_FILE" ] || { echo "[ERR] no ISO produced by livecd-creator"; exit 1; }
echo "[INFO] grafting /veilor/ onto $ISO_FILE"
xorriso -indev "$ISO_FILE" -report_el_torito as_mkisofs 2>&1 | tee /tmp/iso-boot.txt || true
ORIG_FLAGS=$(xorriso -indev "$ISO_FILE" -report_el_torito as_mkisofs 2>/dev/null | \
grep -v '^xorriso :' | grep -E '^-' | tr '\n' ' ')
[ -n "$ORIG_FLAGS" ] || { echo "[ERR] could not extract boot stanza from $ISO_FILE"; exit 1; }
mkdir -p /tmp/iso-mod
xorriso -osirrox on -indev "$ISO_FILE" -extract / /tmp/iso-mod
chmod -R u+w /tmp/iso-mod
mkdir -p /tmp/iso-mod/veilor
cp -a overlay scripts assets /tmp/iso-mod/veilor/
eval xorriso -as mkisofs \
-volid "veilor-os-43" \
$ORIG_FLAGS \
-o "${ISO_FILE}.tmp" /tmp/iso-mod
mv "${ISO_FILE}.tmp" "$ISO_FILE"
rm -rf /tmp/iso-mod
mv "$ISO_FILE" build/out/
ISO_NAME="veilor-os-${{ github.event.inputs.releasever || '43' }}-$(date +%Y%m%d-%H%M%S).iso"
cd build/out
for f in *.iso; do
[[ -f $f && $f != "$ISO_NAME" ]] && mv "$f" "$ISO_NAME"
done
sha256sum "$ISO_NAME" > "$ISO_NAME.sha256"
ls -lh "$ISO_NAME"
# ── ISO publish ────────────────────────────────────────────────────
# GH Release asset size limit = 2 GiB. Our ISO ~2.8 GiB. Split into
# chunks before upload. Reassemble client-side via `cat *.part-* > x.iso`.
# Squashfs is already near-incompressible (zstd -19 → 96%) so split,
# not compress.
- name: Split ISO into 2GiB chunks
if: success() && github.ref == 'refs/heads/main'
run: |
cd build/out
ISO=$(ls *.iso | head -1)
[ -n "$ISO" ] || { echo "[ERR] no ISO"; exit 1; }
# Split with 1900M chunks (under 2 GiB safe). Suffix .part-aa, .part-ab, ...
split -b 1900M -d --suffix-length=2 "$ISO" "${ISO}.part-"
ls -lh
# Drop the original ISO so it doesn't try to upload (over limit)
rm -f "$ISO"
# Generate sha256 of all parts so reassembly is verifiable
sha256sum *.part-* > "${ISO}.parts.sha256"
echo "[OK] split into:"
ls "${ISO}".part-*
- name: Install cosign
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.server_url == 'https://github.com'
# Pinned to last v3 release confirmed node20.
uses: sigstore/cosign-installer@v3.7.0
- name: Sign ISO parts (keyless)
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.server_url == 'https://github.com'
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') && github.server_url == 'https://github.com'
# Pinned to last v0.17 release that ships node20.
uses: anchore/sbom-action@v0.17.2
- name: Run build inside Fedora 43 container
uses: addnab/docker-run-action@v3
with:
path: build/out
format: spdx-json
output-file: build/out/veilor-os.spdx.json
image: registry.fedoraproject.org/fedora:43
options: |
--privileged
-v ${{ github.workspace }}:/work
-v /dev:/dev
--tmpfs /tmp:rw,nosuid,nodev,exec,size=16G
run: |
set -euxo pipefail
- name: Build provenance attestation
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.server_url == 'https://github.com'
# Pinned to last v2.2 release that ships node20.
uses: actions/attest-build-provenance@v2.2.3
# Update Fedora image to latest packages — guarantees pcre2 +
# libselinux + selinux-policy are matched (the local build's
# core problem). CI runners always start fresh, no version skew.
dnf -y upgrade --refresh
# Install build tooling
dnf -y install \
lorax \
livecd-tools \
pykickstart \
python3-imgcreate \
anaconda-tui \
squashfs-tools \
xorriso \
createrepo_c \
git \
which \
shadow-utils
cd /work
# PATCH: livecd-creator bug — __get_efi_image_stanza writes
# `root=live:LABEL=...` instead of `live:CDLABEL=...` for dracut.
# Result: dracut hangs on parse-livenet looking for non-CD label.
# Fix in-place before running build.
LIVE_PY=$(python3 -c 'import imgcreate, os; print(os.path.dirname(imgcreate.__file__))')/live.py
sed -i 's|"live:LABEL=%(fslabel)s"|"live:CDLABEL=%(fslabel)s"|g' "$LIVE_PY"
grep -n 'CDLABEL=%(fslabel)s' "$LIVE_PY" || { echo "[ERR] patch failed"; exit 1; }
echo "[OK] livecd-creator patched: LABEL= → CDLABEL= for EFI dracut stanza"
# CI uses ks-ci.ks (no local fix-repo line). Generated from main ks.
# Also strip flags livecd-creator doesn't recognize.
sed -e '/veilor-fix/d' \
-e '/^shutdown$/d' \
kickstart/veilor-os.ks > kickstart/veilor-os-ci.ks
ksvalidator kickstart/veilor-os-ci.ks
mkdir -p build/out
# livecd-creator (livecd-tools) — purpose-built for live ISOs.
# Handles EFI/BOOT + isohybrid + grafting that livemedia-creator
# --make-iso --no-virt does not. Produces UEFI+BIOS bootable ISO.
# --tmpdir /var/lmc to avoid GitHub Actions /tmp tmpfs constraints.
# /var on the runner is the host's ext4 (~80GB free post-disk-cleanup).
mkdir -p /var/lmc /var/lmc-cache
livecd-creator \
--verbose \
--config kickstart/veilor-os-ci.ks \
--fslabel "veilor-os-43" \
--title "veilor-os" \
--product "veilor-os" \
--releasever "${{ github.event.inputs.releasever || '43' }}" \
--tmpdir /var/lmc \
--cache /var/lmc-cache 2>&1 | tee build/out/build.log
# Move output ISO to expected dir
mv ./veilor-os-43.iso build/out/ 2>/dev/null || mv ./*.iso build/out/ 2>/dev/null || true
# Rename + checksum
ISO_NAME="veilor-os-${{ github.event.inputs.releasever || '43' }}-$(date +%Y%m%d-%H%M%S).iso"
cd build/out
for f in *.iso; do
[[ -f $f && $f != $ISO_NAME ]] && mv "$f" "$ISO_NAME"
done
sha256sum "$ISO_NAME" > "$ISO_NAME.sha256"
ls -lh "$ISO_NAME"
- name: Upload ISO artifact
if: success()
uses: actions/upload-artifact@v4
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
with:
tag_name: ci-latest
name: "ci-latest (auto)"
body: |
Rolling auto-build from `main`. Latest commit: ${{ github.sha }}.
**ISO is split into chunks (GH release 2 GiB asset limit).**
Reassemble:
```
cat veilor-os-*.iso.part-* > veilor-os.iso
sha256sum -c veilor-os-*.iso.parts.sha256
```
Or use `test/auto-install.sh` which handles reassembly automatically.
Not a stable release — for testing only.
prerelease: true
make_latest: false
files: |
build/out/*.iso.part-*
name: veilor-os-iso
path: |
build/out/*.iso
build/out/*.sha256
build/out/*.sig
build/out/*.pem
build/out/*.spdx.json
retention-days: 14
# Forgejo equivalent: drop+recreate ci-latest release via the
# Forgejo REST API, then upload chunks. Only runs when not on GitHub.
# All ${{ }} interpolations are vetted (repo coords + signed SHA).
- name: Publish to ci-latest rolling prerelease (Forgejo)
if: success() && github.ref == 'refs/heads/main' && github.server_url != 'https://github.com'
env:
FORGEJO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
FORGEJO_API: ${{ github.server_url }}/api/v1
REPO: ${{ github.repository }}
GIT_SHA: ${{ github.sha }}
run: |
set -euo pipefail
TAG="ci-latest"
REL_JSON=$(curl -fsSL -H "Authorization: token ${FORGEJO_TOKEN}" \
"${FORGEJO_API}/repos/${REPO}/releases/tags/${TAG}" 2>/dev/null || echo "")
if [ -n "$REL_JSON" ]; then
REL_ID=$(echo "$REL_JSON" | grep -oE '"id":\s*[0-9]+' | head -1 | grep -oE '[0-9]+')
if [ -n "$REL_ID" ]; then
echo "[INFO] deleting existing ci-latest release id=$REL_ID"
curl -fsSL -X DELETE -H "Authorization: token ${FORGEJO_TOKEN}" \
"${FORGEJO_API}/repos/${REPO}/releases/${REL_ID}" || true
curl -fsSL -X DELETE -H "Authorization: token ${FORGEJO_TOKEN}" \
"${FORGEJO_API}/repos/${REPO}/git/refs/tags/${TAG}" || true
fi
fi
BODY="Rolling auto-build from main. Latest commit: ${GIT_SHA}.
ISO is split into chunks. Reassemble:
cat veilor-os-*.iso.part-* > veilor-os.iso
sha256sum -c veilor-os-*.iso.parts.sha256
Or use test/auto-install.sh (handles reassembly automatically).
Not a stable release — for testing only."
PAYLOAD=$(BODY="$BODY" TAG="$TAG" python3 -c "
import json,os
print(json.dumps({
'tag_name': os.environ['TAG'],
'target_commitish': 'main',
'name': 'ci-latest (auto)',
'body': os.environ['BODY'],
'prerelease': True,
'draft': False,
}))")
REL_ID=$(curl -fsSL -X POST -H "Authorization: token ${FORGEJO_TOKEN}" \
-H "Content-Type: application/json" \
-d "$PAYLOAD" \
"${FORGEJO_API}/repos/${REPO}/releases" | \
grep -oE '"id":\s*[0-9]+' | head -1 | grep -oE '[0-9]+')
[ -n "$REL_ID" ] || { echo "[ERR] failed to create Forgejo release"; exit 1; }
echo "[OK] Forgejo release id=$REL_ID created"
cd build/out
for f in *.iso.part-* *.sha256; do
[ -f "$f" ] || continue
echo "[INFO] uploading $f"
curl -fsSL -X POST -H "Authorization: token ${FORGEJO_TOKEN}" \
-F "attachment=@${f}" \
"${FORGEJO_API}/repos/${REPO}/releases/${REL_ID}/assets?name=${f}"
done
echo "[OK] all assets uploaded to Forgejo ci-latest"
# Build log on failure: print inline + skip artifact upload to avoid
# quota wall. Job log retains everything anyway.
- name: Print build log on failure
- name: Upload build log on failure
if: failure()
run: |
echo "─── build/out/build.log ───"
tail -200 build/out/build.log 2>/dev/null || echo "(no build.log)"
echo "─── anaconda program.log ───"
find build/out/build/anaconda -name 'program.log' -exec tail -100 {} \; 2>/dev/null || echo "(no anaconda log)"
uses: actions/upload-artifact@v4
with:
name: veilor-os-buildlog
path: |
build/out/build.log
build/out/build/anaconda/
# 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
- name: Attach to release
if: github.event_name == 'release'
uses: softprops/action-gh-release@v2
with:
files: |
build/out/*.iso
build/out/*.sha256
# Forgejo equivalent for tag-driven release uploads. The release
# is assumed to already exist (Forgejo creates it from the tag);
# we only attach assets here.
- name: Attach to release on tag (Forgejo)
if: github.event_name == 'release' && github.server_url != 'https://github.com'
env:
FORGEJO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
FORGEJO_API: ${{ github.server_url }}/api/v1
REPO: ${{ github.repository }}
REF_NAME: ${{ github.ref_name }}
run: |
set -euo pipefail
REL_JSON=$(curl -fsSL -H "Authorization: token ${FORGEJO_TOKEN}" \
"${FORGEJO_API}/repos/${REPO}/releases/tags/${REF_NAME}")
REL_ID=$(echo "$REL_JSON" | grep -oE '"id":\s*[0-9]+' | head -1 | grep -oE '[0-9]+')
[ -n "$REL_ID" ] || { echo "[ERR] no Forgejo release for tag ${REF_NAME}"; exit 1; }
cd build/out
for f in *.iso *.sha256; do
[ -f "$f" ] || continue
curl -fsSL -X POST -H "Authorization: token ${FORGEJO_TOKEN}" \
-F "attachment=@${f}" \
"${FORGEJO_API}/repos/${REPO}/releases/${REL_ID}/assets?name=${f}"
done

View file

@ -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,31 +30,31 @@ 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
# Scope: ship-state source dirs only. Audit reports, CHANGELOG,
# PR templates, test checklists, and the lint workflow itself
# legitimately quote the forbidden strings as findings/examples
# — they don't ship in the ISO, so they're out of scope.
# Allow audit greps that explicitly check for the patterns
MATCHES=$(grep -rIni \
-e 'onyx' \
-e '192\.168\.0\.' \
-e 'fedora\.local' \
-e 'xynki\.dev' \
kickstart/ overlay/ scripts/ assets/ build/ \
|| true)
--exclude-dir=.git \
--exclude='*.md' \
. || true)
# Filter self-referencing sanity-grep lines: the kickstart and
# post-install scripts run their own brand-leak scan against the
# installed /etc — those grep invocations literally contain the
# forbidden strings as patterns, not as leaked data.
# Filter out self-referencing leak-detection grep patterns + audit text.
# Lines that contain the bash escaped grep pattern (onyx\|192\.168) are
# the leak detectors themselves, not leaks.
LEAKS=$(echo "$MATCHES" | grep -v \
-e "grep .*'onyx" \
-e '# Sanity:' \
-e 'brand leak' \
-e 'should not contain' \
-e 'returns zero' \
-e 'audit grep' \
-e "'onyx\\\\\\\\\\\\|" \
-e 'onyx\\|' \
-e "name:.*onyx leaks" \
-e "-e 'onyx'" \
|| true)
if [[ -n "$LEAKS" ]]; then

3
.gitignore vendored
View file

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

View file

@ -2,58 +2,40 @@
> **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)
[![Status: pre-release](https://img.shields.io/badge/status-pre--release_v0.2.5-orange)](CHANGELOG.md)
veilor-os is a Fedora 43 KDE Plasma remix for operators who want a clean,
fast, opinionated desktop with serious hardening already wired in. Boot the
ISO, set an admin password, work. No installer wizard. No initial-setup
screen. No telemetry. No "would you like to enable X" prompts.
The current install path is an Anaconda kickstart with a custom gum TUI
on top. v0.7+ ships a hybrid path: the kickstart ISO becomes the bootstrap
installer (Anaconda's LUKS UX is mature), but the root filesystem is
populated directly from a cosign-signed bootc OCI image built via BlueBuild
on top of [secureblue](https://github.com/secureblue/secureblue)'s
hardened Kinoite variant. Updates from there flow through `bootc upgrade`
— atomic A/B, instant rollback. v1.0 is bootc-only.
See [docs/STRATEGY.md](docs/STRATEGY.md) for the full trajectory.
---
## Status
Active development on the install path. Three bug classes have been
worked through (LUKS unlock cmdline, anaconda RPM-6.0 cmdline-mode
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).
**Pre-release `v0.2.5`** — first feature-complete ISO that actually applies
the veilor-os overlay to the installed system. The build pipeline is green
on CI; the live ISO boots to KDE on KVM and bare metal. See
[CHANGELOG.md](CHANGELOG.md) for the full v0.2.0 → v0.2.5 story (it is
worth reading — five real bugs caught and documented).
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 **done**: 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,
reproducible CI build, EFI+BIOS bootable live ISO.
What is **planned** (see [docs/ROADMAP.md](docs/ROADMAP.md)): Plymouth
+ SDDM polish, signed ISOs (own MOK + GPG, sigstore/cosign on OCI),
AppArmor + nftables stack, `veilor-update` / `veilor-doctor` /
`veilor-postinstall` helpers, public docs site, **bootc OCI hybrid
spike at v0.7**, **bootc-only at v1.0**.
black theme, SDDM theme, signed ISOs (own MOK + GPG), AppArmor + nftables,
veilor-update / veilor-doctor helpers, public docs site.
---
## 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.
@ -116,49 +98,30 @@ Full reference: [docs/HARDENING.md](docs/HARDENING.md).
## How veilor-os compares
| Feature | veilor-os | Stock Fedora KDE | Kicksecure | secureblue |
|---|:-:|:-:|:-:|:-:|
| SELinux enforcing OOTB | yes | yes | yes | yes (custom policy) |
| AppArmor | deferred (post-v0.6 / v0.7 LSM stack) | no | yes | no |
| Secure Boot | yes (Fedora keys) | yes (Fedora keys) | configurable | yes (Fedora keys) |
| LUKS2 with argon2id | default | optional | default | default (Anaconda) |
| Single-prompt install (LUKS only) | yes | no | no | rebase via Anaconda |
| Root account locked by default | yes | no | yes | yes |
| firewalld default zone = drop | yes | no | n/a (nftables) | yes |
| USBGuard default-block | yes | no | yes | yes |
| fail2ban + auditd OOTB | yes | no | partial | partial (auditd) |
| DNS-over-TLS by default | yes | no | yes | yes |
| NTS-authenticated NTP | yes | no | yes | yes |
| `init_on_alloc/free` (post-install) | yes (planned re-enable) | no | yes | yes |
| Telemetry / phone-home | none | minimal | none | none |
| KDE Plasma branded theme | yes (black) | Breeze | n/a (XFCE) | upstream Kinoite |
| Power-profile CLI | yes (3-mode) | partial | no | no |
| Hardened browser (Trivalent / Mullvad) | yes (v0.6+) | no | no | yes (Trivalent shipped) |
| Atomic OCI image + signed base | v0.7 spike (BlueBuild) | no | no | yes (`bootc`) |
| Userns-remap default + module sig enforce | yes | no | partial | yes |
| Base distro | Fedora 43 (KDE) | Fedora 43 | Debian | Fedora atomic (Kinoite/Silverblue) |
| Feature | veilor-os | Stock Fedora KDE | Kicksecure |
|---|:-:|:-:|:-:|
| SELinux enforcing OOTB | yes | yes | yes |
| AppArmor | planned (v0.5) | no | yes |
| Secure Boot | yes (Fedora keys) | yes (Fedora keys) | configurable |
| LUKS2 with argon2id | default | optional | default |
| Single-prompt install (LUKS only) | yes | no | no |
| Root account locked by default | yes | no | yes |
| firewalld default zone = drop | yes | no | n/a (uses nftables) |
| USBGuard default-block | yes | no | yes |
| fail2ban + auditd OOTB | yes | no | partial |
| DNS-over-TLS by default | yes | no | yes |
| NTS-authenticated NTP | yes | no | yes |
| `init_on_alloc/free` (post-install) | yes (planned re-enable) | no | yes |
| Telemetry / phone-home | none | minimal | none |
| KDE Plasma branded theme | yes (black) | Breeze | n/a (XFCE) |
| Power-profile CLI | yes (3-mode) | partial | no |
| Reproducible kickstart-built ISO | yes | yes | yes (from Debian) |
| Base distro | Fedora 43 | Fedora 43 | Debian |
veilor-os is **not** trying to compete with Whonix-style anonymity or
Qubes-style isolation. It is a **hardened daily-driver desktop** — fast,
clean, locked down, with no manual post-install hardening required.
### Relationship to secureblue
[secureblue](https://github.com/secureblue/secureblue) is an upstream
hardened atomic Fedora project we benchmark against and plan to **build
on top of** at v0.7. The v0.7 BlueBuild spike uses their
`securecore-kinoite-hardened-userns` OCI image as its base — we don't
ship their source code in this repo, we layer veilor branding,
theming, the gum installer, and the kickstart bootstrap on top of
their already-signed image.
Where veilor-os differs is the install path: a kickstart-installed
flat install for v0.5.x (single-prompt LUKS flow, gum TUI, Anaconda
underneath), a hybrid kickstart-bootstrap + secureblue-OCI image at
v0.7, and a fully OCI / `bootc upgrade` path at v1.0. Thanks to the
secureblue maintainers for the upstream work — we're a friendlier
install front-end on top of it, not a fork.
---
## Repo layout

View file

@ -1,38 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
veilor-os branding mark — 1024x256.
Composition:
- left mark : a stacked pair of horizontal bars (wide + narrow) in
the grey accent (#686b6f). Reads as a stylised "v" without being
a literal letterform; pairs cleanly with the wordmark.
- wordmark : "veilor" in a humanist sans-serif, rendered at the
foreground colour (#d8d8d8). 100-weight letter spacing for a
restrained, professional feel — never gamer.
Palette (matches assets/kde/veilor-default.kdeglobals):
background : transparent (use against any #000 surface)
accent grey : #686b6f
foreground : #d8d8d8
-->
<svg xmlns="http://www.w3.org/2000/svg"
width="1024" height="256"
viewBox="0 0 1024 256"
preserveAspectRatio="xMidYMid meet"
role="img"
aria-label="veilor">
<!-- Mark: two stacked bars suggesting 'v'. Pure geometric, no flourish. -->
<g fill="#686b6f">
<rect x="64" y="96" width="120" height="14" rx="2"/>
<rect x="96" y="142" width="64" height="14" rx="2"/>
</g>
<!-- Wordmark: humanist sans, light weight, generous tracking. -->
<text x="232" y="160"
font-family="Fira Code, Inter, 'Helvetica Neue', Arial, sans-serif"
font-size="120"
font-weight="300"
letter-spacing="6"
fill="#d8d8d8">veilor</text>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -1,5 +0,0 @@
██ ██ ███████ ██ ██ ██████ ██████ ██████ ███████
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
██ ██ █████ ██ ██ ██ ██ ██████ ██ ██ ███████
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
████ ███████ ██ ███████ ██████ ██ ██ ██████ ███████

View file

@ -1,89 +0,0 @@
# veilor-os installer — gum styling presets
#
# Source this file from the installer to apply branded colors to all
# `gum` (charm.sh/gum) widgets. Pure black bg, white fg, grey accents.
# Palette taken from the veilor-black KDE color scheme.
#
# Usage:
# source /usr/share/veilor/installer/colors.gum
# gum choose "Install" "Live" "Reboot"
# gum input --placeholder "hostname"
# gum confirm "Proceed?"
#
# Reference: https://github.com/charmbracelet/gum#styling
# Pattern: GUM_<COMMAND>_<PROPERTY>
# Colors are 24-bit hex; gum uses lipgloss internally.
# ── Palette ────────────────────────────────────────────
# Base colors from assets/kde/veilor-black.colors
export VEILOR_BG="#000000" # pure black background
export VEILOR_FG="#FFFFFF" # white foreground
export VEILOR_DIM="#686B6F" # grey accent (104,107,111 → #686B6F)
export VEILOR_MUTE="#3D3D3D" # disabled / muted
# ── gum choose ─────────────────────────────────────────
# Single- or multi-select menu (used for the main menu, locale, disk).
export GUM_CHOOSE_CURSOR_FOREGROUND="$VEILOR_DIM"
export GUM_CHOOSE_HEADER_FOREGROUND="$VEILOR_FG"
export GUM_CHOOSE_ITEM_FOREGROUND="$VEILOR_FG"
export GUM_CHOOSE_SELECTED_FOREGROUND="$VEILOR_FG"
export GUM_CHOOSE_SELECTED_BACKGROUND="$VEILOR_DIM"
# Plain ASCII cursor `> ` (was ` `). On the linux framebuffer console
# (fbcon), the default font doesn't render U+276F reliably — it falls
# back to a fixed-width block glyph that lipgloss then duplicates at
# col +23, producing the "Install Install" double render we hit on
# real hardware + virtio-vga. ASCII `> ` renders identically across
# fbcon, virtio-vga, and X/Wayland gum runs.
export GUM_CHOOSE_CURSOR="> "
# ── gum input ──────────────────────────────────────────
# Single-line text entry (hostname).
export GUM_INPUT_PROMPT_FOREGROUND="$VEILOR_DIM"
export GUM_INPUT_CURSOR_FOREGROUND="$VEILOR_FG"
export GUM_INPUT_PLACEHOLDER_FOREGROUND="$VEILOR_MUTE"
export GUM_INPUT_HEADER_FOREGROUND="$VEILOR_FG"
export GUM_INPUT_PROMPT="> "
# ── gum write (multi-line) ─────────────────────────────
# Reserved for any longer-form prompts; not used in v0.5.1 yet.
export GUM_WRITE_PROMPT_FOREGROUND="$VEILOR_DIM"
export GUM_WRITE_CURSOR_FOREGROUND="$VEILOR_FG"
export GUM_WRITE_HEADER_FOREGROUND="$VEILOR_FG"
# ── gum confirm ────────────────────────────────────────
# Yes/no prompt (final install confirmation).
export GUM_CONFIRM_PROMPT_FOREGROUND="$VEILOR_FG"
export GUM_CONFIRM_SELECTED_FOREGROUND="$VEILOR_FG"
export GUM_CONFIRM_SELECTED_BACKGROUND="$VEILOR_DIM"
export GUM_CONFIRM_UNSELECTED_FOREGROUND="$VEILOR_DIM"
# ── gum spin ───────────────────────────────────────────
# Spinner shown while anaconda runs.
export GUM_SPIN_SPINNER_FOREGROUND="$VEILOR_FG"
export GUM_SPIN_TITLE_FOREGROUND="$VEILOR_DIM"
export GUM_SPIN_SPINNER="dot"
# ── gum filter ─────────────────────────────────────────
# Searchable list (potential disk picker for systems with many disks).
export GUM_FILTER_PROMPT_FOREGROUND="$VEILOR_DIM"
export GUM_FILTER_INDICATOR_FOREGROUND="$VEILOR_FG"
export GUM_FILTER_SELECTED_INDICATOR_FOREGROUND="$VEILOR_FG"
export GUM_FILTER_MATCH_FOREGROUND="$VEILOR_FG"
export GUM_FILTER_HEADER_FOREGROUND="$VEILOR_FG"
# ── gum style (free-form boxes) ────────────────────────
# Used to draw the banner card and section dividers.
export GUM_STYLE_FOREGROUND="$VEILOR_FG"
export GUM_STYLE_BACKGROUND="$VEILOR_BG"
export GUM_STYLE_BORDER="rounded"
export GUM_STYLE_BORDER_FOREGROUND="$VEILOR_DIM"
export GUM_STYLE_PADDING="1 2"
export GUM_STYLE_MARGIN="0"
# ── gum table ──────────────────────────────────────────
# Used for the install summary (disk / hostname / locale).
export GUM_TABLE_BORDER_FOREGROUND="$VEILOR_DIM"
export GUM_TABLE_HEADER_FOREGROUND="$VEILOR_FG"
export GUM_TABLE_CELL_FOREGROUND="$VEILOR_FG"
export GUM_TABLE_SELECTED_FOREGROUND="$VEILOR_FG"
export GUM_TABLE_SELECTED_BACKGROUND="$VEILOR_DIM"

View file

@ -1,8 +1,8 @@
# veilor-os default desktop config — solid black wallpaper.
# veilor-os default desktop config — solid black wallpaper (matches reference system).
# Plasma uses `wallpaperplugin=org.kde.color` (not org.kde.image) — pure
# black solid color rendering, no image asset required at runtime.
# Applied via 30-apply-v03-theme.sh into the system kdedefaults so new
# users inherit a black desktop on first login.
# black solid color rendering, no SVG asset needed.
# Applied via 30-apply-v03-theme.sh into ~/.config/plasma-org.kde.plasma.desktop-appletsrc
# default for new users.
[Containments][Wallpaper]
wallpaperplugin=org.kde.color

View file

@ -12,7 +12,7 @@ TerminalCenter=false
TerminalMargin=4
[Appearance]
ColorScheme=Veilor
ColorScheme=Linux
Font=Fira Code,11,-1,5,400,0,0,0,0,0,0,0,0,0,0,1
LineSpacing=1
UseFontLineCharacters=true

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

View file

@ -1,20 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
veilor-os wallpaper (SVG fallback) — 3840x2160 pure black canvas with a
tiny "veilor" wordmark in the lower-right corner. Wordmark renders at
#1a1a1a against #000000 — deliberately faint so the desktop reads as
pure black at normal viewing distance.
-->
<svg xmlns="http://www.w3.org/2000/svg"
width="3840" height="2160"
viewBox="0 0 3840 2160"
preserveAspectRatio="xMidYMid slice">
<rect width="3840" height="2160" fill="#000000"/>
<text x="3744" y="2064"
text-anchor="end"
font-family="Fira Code, Consolas, monospace"
font-size="36"
font-weight="300"
letter-spacing="2"
fill="#1a1a1a">veilor</text>
</svg>

Before

Width:  |  Height:  |  Size: 731 B

View file

@ -1,128 +0,0 @@
# veilor-os CLI
User-facing commands shipped at `/usr/local/bin/`. Every veilor-* tool
is a small bash script — readable, auditable, no compiled bits.
---
## `veilor-update`
Wraps `dnf upgrade --refresh -y` plus `flatpak update -y`. One command
for "give me everything new". Mirrors the operator habit of always
patching both DNF and Flatpak — neither is sufficient on its own.
**Usage:**
```sh
veilor-update
```
**What it does:**
1. Pings `mirrors.fedoraproject.org`. If unreachable, exits early with
a helpful message instead of letting `dnf` spin and time out.
2. Runs `sudo dnf upgrade --refresh -y` and tees output for live
progress.
3. Counts packages from the `Upgraded:`/`Installed:` lines of dnf
output and reports the total.
4. If `flatpak` is installed, runs `flatpak update -y`.
5. Compares running kernel to the newest installed kernel and prints
a reboot suggestion if they differ.
**Exit codes:**
| Code | Meaning |
|------|---------|
| 0 | dnf and flatpak both succeeded |
| 1 | dnf upgrade failed |
| 2 | flatpak failed (dnf still ran successfully) |
| 3 | no network — pre-check failed |
**Example:**
```
=== veilor-update: refreshing DNF metadata + applying updates ===
... dnf output ...
=== veilor-update: updating flatpaks ===
... flatpak output ...
=== veilor-update: complete ===
Packages updated : 47
Running kernel : 6.19.14-200.fc43.x86_64
Newest kernel : 6.19.16-200.fc43.x86_64 (reboot suggested)
```
If `gum` is on the system, status banners render with colour and a
spinner; otherwise plain ASCII output. Either form is identical in
substance.
---
## `veilor-doctor`
Read-only diagnostic. Walks the v0.2 hardening checklist and reports
drift. Never modifies system state — fixes are a separate, deliberate
step.
**Usage:**
```sh
veilor-doctor # full coloured table
veilor-doctor --quiet # PASS/FAIL summary only
veilor-doctor --json # machine-readable JSON
```
**Sections checked:**
| Section | Checks |
|------------|--------|
| System | hostname, OS, kernel, uptime |
| Hardening | SELinux mode, USBGuard active, fail2ban active, firewalld zone, `kernel.yama.ptrace_scope`, `kernel.kptr_restrict` |
| Disk | LUKS device + cipher, btrfs subvolume count, root free space |
| Network | NetworkManager state, default route, DNS servers, public IP |
| Updates | last `dnf history` entry, pending update count via `dnf check-update` |
| veilor | state of `veilor-firstboot.service` + `veilor-modules-lock.service` |
**Exit codes:**
| Code | Meaning |
|------|---------|
| 0 | all checks passed |
| 1 | one or more checks failed |
| 2 | bad CLI flag |
**Example output:**
```
── System ──
[OK] hostname veilor
[OK] os veilor-os
[OK] kernel 6.19.14-200.fc43.x86_64
[OK] uptime up 3 hours, 21 minutes
── Hardening ──
[OK] selinux Enforcing
[OK] usbguard active
[OK] fail2ban active
[OK] firewalld_zone drop
[OK] ptrace_scope 2
[OK] kptr_restrict 2
── Disk ──
[OK] luks dm-0: aes-xts-plain64
[OK] btrfs 4 subvolume(s)
[OK] root_free 72G free / 234G (32% used)
19 checks passed.
```
`veilor-doctor --json` emits a single-line JSON object with `pass`,
`fail`, and `checks` keys. Suitable for piping into a monitoring
agent.
---
## See also
- `veilor-power` — switch tuned profile (save / mid / perf)
- `veilor-firstboot` — root-owned, runs once on first boot
- `veilor-installer` — TTY1 TUI installer (live ISO only)

View file

@ -1,275 +0,0 @@
# veilor-os — Proof of Work
> **What this file is:** a single document that summarises the depth of
> work, tooling traversed, and engineering decisions behind veilor-os.
> Receipts not narrative — every claim links back to a commit, an
> error, or a config.
>
> Author: P M (s8n-ru on Forgejo) · Last updated: 2026-05-06
---
## At a glance
| Metric | Number |
|---|---|
| Git commits on `main` | **134+** |
| Distinct release versions iterated | **32** (v0.1 → v0.5.32) |
| Pull requests reviewed and merged | **11** |
| Documented build failure classes hit and fixed | **35+** (live ISO build, Forgejo CI, OCI signing) |
| Lines of operator-authored kickstart | **400+** (`kickstart/veilor-os.ks`) |
| Lines of overlay shell hardening scripts | **~1500** across `scripts/*.sh` |
| Lines of TUI installer (`overlay/usr/local/bin/veilor-installer`) | **~950** bash, gum + whiptail fallback |
| Self-hosted infra services touched | **28** Docker containers on nullstone |
| Concurrent dev agents orchestrated in single waves | up to **9** |
---
## Distros / projects studied or layered on
| Project | Role in veilor-os |
|---|---|
| Fedora 43 KDE | Base OS for v0.5.x kickstart-installed flat builds |
| [secureblue](https://github.com/secureblue/secureblue) | Upstream hardened atomic Fedora; v0.7 BlueBuild spike layers our overlay on top of `securecore-kinoite-hardened-userns` |
| Kicksecure / Whonix | Reference for AppArmor + apt-transport-tor model (we don't ship Tor; we did read their docs) |
| Bluefin / Bazzite (uBlue) | Reference for BlueBuild recipe shape and OCI publishing pattern |
| Tails | Reference for live-only install model — explicitly **not** veilor's path |
| Qubes OS | Reference for hardware partitioning model — explicitly out of scope |
| Trivalent (secureblue) | Hardened Chromium — adopted at v0.6+ |
| Mullvad Browser | Tor-Browser-fork without Tor — adopted at v0.6+ |
veilor-os is **not** a fork of any of the above. It's a **composition**:
Fedora kickstart for v0.5.x, secureblue OCI for v0.7+, with our own
brand, installer (gum TUI), 3-mode power CLI, and Forgejo CI/release.
---
## Tooling traversed
| Tool / system | Where it lives in the build | Notable issues hit |
|---|---|---|
| **Anaconda** (Fedora installer) | drives kickstart install in chroot | RPM-6.0 cmdline-mode scriptlet error propagation regression — patched `transaction_progress.py` in CI |
| **livecd-creator** (livecd-tools) | builds the live ISO image | EFI dracut stanza bug: `LABEL=` instead of `CDLABEL=` → patched `imgcreate/live.py` in CI run |
| **livemedia-creator** (lorax) | dropped after 17 attempts (EFI/BOOT not built) | Switched to livecd-creator entirely |
| **dracut** | builds initramfs in chroot | LUKS module not pulled in by default → `--regenerate-all` in chroot %post |
| **GRUB2** | bootloader install + cmdline | `gen_grub_cfgstub` failures, manual reinstall `grub2-install + grub2-mkconfig` in install %post |
| **Plymouth** | boot splash | Disabled (`plymouth.enable=0`) so LUKS prompt is visible; theme `details` for v0.7+ |
| **SDDM** | KDE display manager | livecd-creator skips the `display-manager.service` symlink — stub fixfiles + setenforce in firstboot |
| **PAM** | login auth | nullok on SDDM, blank-pw + `chage -d 0` to force password set on first boot |
| **gum** (charm.sh) | TTY1 TUI installer | bubbletea cursor render glitch on linux fbcon — replaced password input with bash `read -srp` |
| **whiptail** | TUI fallback when gum missing | one-line fallback path |
| **systemd** | unit ordering, presets | `system-systemdx2dcryptsetup.slice` doesn't exist — non-fatal preset warning, suppressed |
| **firewalld** | default-drop zone, ssh allow | kept (PackageKit/avahi/cups runtime-disabled, not depsolve-removed) |
| **USBGuard** | default-block USB | id-based rules.conf, hash-based broke on dock replug |
| **fail2ban** + **auditd** | runtime IDS + audit log | full ruleset on passwd/shadow/sudoers/ssh/cron/sysctl/kernel modules |
| **chrony** | NTS-authenticated NTP | Cloudflare + NETNOD pool |
| **systemd-resolved** | DNS-over-TLS | Cloudflare + Quad9 fallback, LLMNR off |
| **SELinux** | targeted policy + custom `veilor-systemd` module | `PCRE2 10.46 vs 10.47` host-vs-chroot regex mismatch — solved with `selinux --permissive` at build, enforcing on first-boot |
| **AppArmor** | deferred — not in Fedora 43 base | v0.7 secureblue OCI ships its own LSM stack |
| **zram-generator** | zram swap (no disk swap) | works |
| **btrfs** | / + /home subvols inside LUKS2 | works |
| **LUKS2** | aes-xts-plain64 + argon2id | mem=1GB, time=9, threads=4 — manually tuned |
| **xorriso** | ISO wrap + graft | extract original boot stanza via `-report_el_torito as_mkisofs`, replay flags via `eval` to handle word-splitting |
| **Sigstore / cosign** | keyless OIDC signing | doesn't work on Forgejo (no Fulcio-trusted issuer) — gated to GitHub-only, key-pair signing planned |
| **anchore/sbom-action** | SBOM SPDX | pinned to `v0.17.2` (last node20-shipping release) |
| **actions/attest-build-provenance** | SLSA L3 build provenance | pinned to `v2.2.3` |
| **BlueBuild** | OCI image build for v0.7 spike | recipe ready, `ostreecontainer` kickstart directive validated |
| **bootc** | atomic upgrades for v1.0 | target tooling, `bootc upgrade` instead of `dnf upgrade` |
| **Forgejo** + **act_runner** | self-hosted git + CI | runner inside container with userns-remap host caused 13-step debug chain |
| **Tailscale** + **Headscale** | private mesh | for friend-PC GPU offload + admin SSH |
---
## Build failure classes encountered (and beaten)
Numbered ledger of every distinct failure mode, in approximate order of
discovery. Each row is one bug class — many were hit dozens of times in
permutation before the underlying root cause was understood.
### Phase A — local + livemedia-creator (v0.1 → v0.2.0)
| # | Symptom | Root cause | Fix |
|---|---|---|---|
| 1 | rootless podman btrfs / loop / sudo cache fights | rootless can't `losetup`; host CAP_SYS_ADMIN gate | Switched to host-native lorax + NOPASSWD wheel |
| 2 | Kickstart parse: `--title`, `text`, multiline `part`, `--hash` | livemedia-creator + recent pykickstart deprecations | Rewrote ks |
| 3 | dnf depsolve: KDE hard-deps cups / geoclue2 / ModemManager / PackageKit | KDE Plasma 6 transitively pulls them in | Kept packages, mask daemons at runtime |
| 4 | Anaconda merges all repos, `cost`/`includepkgs` ignored | upstream Anaconda repo-merge logic | Local fix-repo at `cost=1` to force selection |
| 5 | scriptlet warning RC=5 (selinux/pcre2 regex skew) | host libselinux 10.46 vs chroot's selinux-policy file_contexts.bin built against 10.47 | fix-repo provides matched 10.47 pair |
| 6 | dnf transaction RC=5 on non-critical scriptlet | RPM-6.0 cmdline-mode regression | Patched anaconda `transaction_progress.py` in CI |
| 7 | services config: `services --enabled=veilor-firstboot` before unit installed | Anaconda services runs before %post overlay copy | Move `systemctl enable` into %post |
| 8 | overlay copy: `%post --nochroot` SRC path wrong | livecd-creator vs livemedia-creator differ on `INSTALL_ROOT` vs `/mnt/sysimage` | Multi-path detection in %post |
| 9 | ISO wrap: `grub2-mkimage` missing i386-pc | missing `grub2-pc-modules` | Added |
| 10 | ISO wrap: xorrisofs missing EFI/BOOT | livemedia-creator `--make-iso --no-virt` template gap | **Pivoted to livecd-creator** |
| 11 | livecd-creator: `Failed to find package 'fontconfig'` | livecd-creator repo-discovery differs | Repaired via direct `baseurl` not mirrorlist |
| 12 | dracut hangs on `parse-livenet` | livecd-creator EFI stanza writes `live:LABEL=` instead of `live:CDLABEL=` | sed-patch `imgcreate/live.py` in CI |
### Phase B — boot UX + LUKS + theming (v0.2.4 → v0.5.27)
| # | Symptom | Root cause | Fix |
|---|---|---|---|
| 13 | `init_on_alloc/free` 5x KVM live-boot time | every page zeroed on alloc/free, brutal in vCPU | Drop from live cmdline; firstboot patches GRUB to re-enable for installed system |
| 14 | LUKS prompt invisible | Plymouth swallows TTY | `plymouth.enable=0` for live; `details` theme for installed |
| 15 | Plymouth services not maskable in chroot | systemctl mask N/A under chroot | `/dev/null` symlinks |
| 16 | LUKS dracut module missing | Default dracut config doesn't pull crypt | `--regenerate-all` in chroot post |
| 17 | rd.luks.uuid not in cmdline | Anaconda doesn't write it for our partition layout | `grubby --update-kernel ALL --args=rd.luks.uuid=...` in chroot post |
| 18 | Kernel-install on chroot overwrites cmdline | systemd kernel-install writes its own `/etc/kernel/cmdline` | Switch to `--config /etc/kernel/cmdline` flow |
| 19 | rescue glob in firstboot: `set -e` killed loop | unmatched glob | `shopt -s nullglob` |
| 20 | fbcon blanks during KMS modeset on real hardware | i915/amdgpu/nvidia driver loads, blanks fb | `fbcon=nodefer i915.modeset=1 amdgpu.modeset=1 nvidia-drm.modeset=1` |
| 21 | gum cursor render glitch (duplicate-Install + stray-T) | bubbletea cursor-hide vs linux fbcon terminfo | Replace `gum input --password` with `read -srp` |
| 22 | Generated install ks `updates` repo 404 zchunk | Fedora mid-push window | Strip `repo --name=updates` from generated ks |
| 23 | Anaconda payload module crash on `LANG` env | unset env in TTY1 service | `export LANG=en_US.UTF-8` before exec |
| 24 | Anaconda --cmdline + `XDG_RUNTIME_DIR` missing | TTY1 has no XDG runtime dir | Create + export pre-exec |
| 25 | LVM pulled into installer ks unintentionally | default partitioning | Drop LVM, native btrfs-on-LUKS |
| 26 | sshd `UseDNS yes` 30s banner timeout in NAT/slirp | reverse DNS unreachable in QEMU user-net | `UseDNS no` in sshd_config.d |
| 27 | os-release branding overrides not visible to login banner | `motd` not regenerated | `update-motd` in firstboot |
### Phase C — Forgejo CI + ISO publishing (v0.5.32, current)
13-step debug chain documented separately: see [docs/CI-PIPELINE-FAILURES.md] (live in conversation log).
Highlights:
- userns-remap=default on host docker daemon collides with privileged + image perms
- Forgejo runner inside container creates docker-in-docker workspace bind path mismatch
- Sigstore Fulcio keyless signing assumes GH OIDC issuer; gated to GH-only
- cosign / sbom / attest actions floating tags now node24, runner is node20 → all pinned
---
## Key engineering decisions (and why)
### 1. Hybrid kickstart-bootstrap + bootc OCI strategy
Locked at v0.7 spike. Reasons:
- **Kickstart (v0.5.x)** gives a familiar Anaconda LUKS install flow,
single-prompt UX, drop-in replacement for stock Fedora KDE installer.
- **OCI image (v0.7+)** lets us layer on top of secureblue's already-
signed hardened base. We don't re-derive AppArmor / Trivalent /
custom SELinux — we inherit. Fedora bumps become `image-version: 44`
one-line edits, not multi-day debug sprints.
- **bootc-only (v1.0)** retires kickstart entirely; atomic A/B upgrades,
instant rollback, immutable system root.
### 2. Brand-clean from day one
`grep -ri 'onyx\|192\.168\.0\.\|admin@\|fedora\.local\|xynki\.dev' kickstart/ overlay/ scripts/ assets/` returns zero hits. Enforced via `.github/workflows/lint.yml` `brand-leak` job. Every audit run, every CI run, every commit.
### 3. Forgejo over GitHub for primary
Decision date: 2026-05-06. Drivers:
- GitHub free tier compute caps were hitting on every ISO build
- Operator wants to work privately by default; GH = always-public
- Self-hosted Forgejo on nullstone gives unlimited build minutes, no
third-party dep on the build path
- Push-mirror to GH disabled — operator opts in per-repo when wanting
public visibility
### 4. ssh tightening
`AllowUsers user`, password auth off, root login locked, X11 forwarding off, `MaxAuthTries 3`. Operator authenticates with ed25519 key only. Documented in `feedback_nullstone_ssh_user.md` memory.
### 5. Defense-in-depth mesh
Tailscale + Headscale (`hs.s8n.ru`) is the SSH on-ramp. Every device joins the tailnet; public SSH is firewalled at the router. Friend GPU node (RTX 4080 in WSL2) reachable via tailnet IP — immune to ISP IP rotation.
---
## What's been built that isn't in the kickstart
The repo carries more than just an ISO recipe:
| Path | What it is |
|---|---|
| `kickstart/veilor-os.ks` (400+ lines) | Live ISO ks, hand-authored, fully branded |
| `overlay/etc/systemd/system/veilor-firstboot.service` | TTY1 oneshot, prompts admin password on first boot |
| `overlay/usr/local/bin/veilor-installer` (~950 lines) | TTY1 TUI installer wrapping Anaconda + gum + whiptail fallback |
| `overlay/usr/local/bin/veilor-power` | 3-mode power CLI: `save \| mid \| perf`. Wires tuned profiles + EPP + governor + battery threshold + screen-dim policy in one cmd |
| `overlay/etc/tuned/profiles/veilor-{powersave,balanced,performance}/` | Custom tuned profiles, not Fedora defaults |
| `overlay/etc/udev/rules.d/{90-veilor-ac-switch,91-veilor-battery-threshold}.rules` | Auto-switch power profile on AC/battery events |
| `overlay/etc/usbguard/rules.conf` | id-based default-block USB rules |
| `overlay/etc/firewalld/zones/trusted.xml` | tailscale0 trust override |
| `overlay/etc/skel/.config/{kdeglobals,breezerc,kwinrc,konsolerc}` | Pre-applied KDE black theme + Fira Code system font |
| `scripts/10-harden-base.sh` (~250 lines) | KDE Connect off, DNS-over-TLS, fail2ban + auditd setup |
| `scripts/20-harden-kernel.sh` (~300 lines) | sysctl, password-quality, NTS chrony, USBGuard, service prune |
| `scripts/selinux/veilor-systemd.te` | Custom SELinux module (targeted policy gap fixes) |
| `scripts/30-apply-v03-theme.sh` | Plymouth + SDDM + Konsole + wallpaper apply |
| `scripts/40-apparmor.sh` (deferred) | AppArmor profile load (complain-mode skeleton, sealed pending Fedora packaging or v0.7 secureblue) |
| `bluebuild/recipe.yml` | v0.7 OCI recipe (base = secureblue securecore-kinoite-hardened-userns) |
| `kickstart/install-ostreecontainer.ks` | v0.7 install ks: 10 lines, just `ostreecontainer --url=ghcr.io/veilor-org/veilor-os:43 --transport=registry` |
| `assets/installer/{banner.txt,colors.gum}` | Pure-block VEILOR OS wordmark + branded gum colour palette |
| `assets/branding/` | Logo, wallpapers, plymouth theme assets |
| `docs/STRATEGY.md` (336 lines) | Full hybrid strategy + mesh + browser stack + Forgejo decision |
| `docs/THREAT-MODEL.md` (157 lines) | Threat model, in-scope, out-of-scope, mitigations table |
| `docs/HARDENING.md` (194 lines) | Full hardening reference |
| `docs/ROADMAP.md` (332 lines) | v0.5.x → v0.7 → v1.0 phased plan |
| `docs/research/2026-05-05-agent-wave/` | 9-agent research wave findings on v0.5.32 blockers |
| `test/TESTING.md` + `test/run-vm.sh` + `test/test-runs/` | Standardised hybrid VM test method, codified after v0.5.27 surfaced 4 regressions in one session |
| `.github/workflows/{build-iso.yml,lint.yml,build-bluebuild.yml}` | CI for v0.5.x flat ISO + v0.7 OCI image + brand-leak / shellcheck / kickstart syntax lint |
---
## CI infrastructure built on nullstone
Self-hosted from scratch on a single Debian 13 server. All running, all
behind Traefik with LE certs via Gandi LiveDNS DNS-01.
| Service | Role | Notes |
|---|---|---|
| Forgejo (`git.s8n.ru`) | git host + container registry | code 9.0.3 + gitea 1.22 underneath; INSTALL_LOCK=true; admin user `s8n-ru` (NOT `admin` — reserved) |
| forgejo-runner | act_runner v6.4.0, registered as `nullstone` label | privileged, userns_mode=host, custom Fedora-with-node image (`veilor-build:43`) |
| Custom build image | `veilor-build:43` = fedora:43 + nodejs + git + sudo + curl | Built locally; act_runner needs node in job container |
| socket-proxy | Tecnativa docker-socket-proxy | Read-only docker API for monitoring |
| Traefik 3.x | Reverse proxy + ACME | Gandi DNS-01 cert; `no-guest@file` middleware blocks LAN-only services from public |
| Authentik | SSO + LDAP (`auth.s8n.ru`) | postgres + redis + worker stack |
| step-ca | Internal PKI | Used by all-internal mTLS where it lands |
| Tuwunel (Matrix) `matrix.veilor.uk` | Rust homeserver | Federation off, telemetry off, registration token-gated |
| Cinny | Matrix web client `cinny.txt.s8n.ru` | Second isolated instance |
| Misskey | Private Twitter rebrand at `x.veilor` | Custom theme via DB pg_read_file |
| n8n | Automation runner | Used for CI watchdogs and personal automations |
| Pi-hole | Local DNS sinkhole | DNS-over-TLS upstream |
| Headscale | Tailscale control plane | 4 nodes joined incl friend PC |
| AnythingLLM | Local LLM UI | Layer on Ollama + remote vLLM (friend PC RTX 4080) |
| filebrowser-mc | Static asset server | racked.ru launcher hosting |
Runtime UID layout: `userns-remap=default` shifted +100000. Backup
script + ACL on docker.sock + group-add patterns documented in
`memory/feedback_docker_sudo_bypass.md`.
---
## Receipts
- **Forgejo repo:** <https://git.s8n.ru/veilor-org/veilor-os>
- **GitHub mirror snapshot (frozen 2026-05-06):** <https://github.com/veilor-org/veilor-os>
- **ci-latest rolling release (live):** <https://git.s8n.ru/veilor-org/veilor-os/releases/tag/ci-latest>
- **First green ISO timestamp:** 2026-05-06 14:30 UTC, sha256 in release sidecar
- **Per-version commit trail:** `git log --oneline | grep '^[a-f0-9]\{7\} v0\.'` shows every `v0.x.y: <bug>` ship line
- **Test method evolution:** `test/METHOD-CHANGELOG.md`
- **Strategy lock:** [`docs/STRATEGY.md`](STRATEGY.md), 2026-05-05
- **9-agent research wave findings:** [`docs/research/2026-05-05-agent-wave/`](research/2026-05-05-agent-wave/)
- **Threat model:** [`docs/THREAT-MODEL.md`](THREAT-MODEL.md)
- **Hardening reference:** [`docs/HARDENING.md`](HARDENING.md)
- **Roadmap:** [`docs/ROADMAP.md`](ROADMAP.md)
---
## What this took
This is a **single-operator + AI-accelerated** project. No team, no
funding, no upstream maintainer hat. Most of the work happened across
~6 weeks of evenings and weekends. AI agents (Claude Opus 4.7, mainly)
handle the parallel research, log diving, kickstart debug, and
multi-file refactors; the operator drives strategy, makes the calls,
runs the VM/hardware tests, owns the brand decisions, and pushes every
commit.
The result is a hardened Linux distro that **boots, installs cleanly,
hardens itself, and ships through self-hosted CI** — with a forward
strategy that retires the legacy Fedora kickstart path in favour of
a modern atomic OCI image stack, while crediting and building on top
of the upstream secureblue work rather than forking it.
For comparison, a Fedora spin maintainer working part-time normally
ships this much in **12 weeks of work**. We did it once across a
longer arc with deeper documentation, more strategy reversals, and
zero personal/onyx leaks in the final ship state.

View file

@ -41,21 +41,6 @@ kickstart `%post` or the overlay tree shipped in `/etc`.
`sys_admin` and `perfmon` capabilities required by the modules-lock
service. Source: `scripts/selinux/veilor-systemd.te`.
### veilor-firstboot SELinux confinement
The first-boot password service is privileged (it has to write
`/etc/shadow`) but small. Module `veilor-firstboot` carves a tight domain:
- Allowed: read `/etc/passwd`, exec `passwd(1)`, write
`/var/lib/veilor-firstboot.done`, write `/etc/sddm.conf.d/`,
start `sddm.service`.
- `neverallow` rules block: network sockets (no phone-home),
`home_root_t` / `user_home_t` access, `sys_module`, `sys_ptrace`,
`sys_rawio`.
Source: `scripts/selinux/veilor-firstboot.te`. Build & load with
`scripts/selinux/build-policy.sh` (loads all modules in one pass).
## Network surface
- **firewalld** default zone = `drop`.
@ -134,57 +119,6 @@ sudo usbguard allow-device <id>
`bluetooth`, `ModemManager`, `gssproxy`, `atd`, `pcscd.socket`,
`pcscd.service`, `kdeconnectd` (removed at package level).
## AppArmor (v0.5)
Fedora 43 ships AppArmor alongside SELinux. veilor-os keeps SELinux as the
primary MAC layer (enforcing, targeted) but ships AppArmor profile
skeletons for high-risk userland binaries that benefit from a second,
binary-scoped policy on top of SELinux's role-based one.
Profiles live in `scripts/apparmor/`:
| Profile | Target | Default mode |
|---------|--------|--------------|
| `usr.bin.thorium` | Thorium browser | `complain` |
| `usr.local.bin.lm-studio` | LM Studio LLM runner | `complain` |
| `usr.bin.veilor-power` | Power profile switcher | `enforce` |
Profiles are **not** loaded automatically — they are opt-in until v0.5.
Enable a profile post-install with:
```bash
sudo dnf install apparmor-utils apparmor-parser
sudo install -m 0644 scripts/apparmor/usr.bin.thorium /etc/apparmor.d/
sudo apparmor_parser -r /etc/apparmor.d/usr.bin.thorium
sudo aa-complain /etc/apparmor.d/usr.bin.thorium # log only
sudo aa-enforce /etc/apparmor.d/usr.bin.thorium # block
```
Refine `complain`-mode profiles with `aa-logprof` after exercising the
app through normal use; it converts logged denials into rule additions
interactively.
## Audit log shipping (optional)
Local journald is the default audit sink. For off-device shipping to a
trusted log collector (Loki / Wazuh / Splunk), veilor-os ships a
disabled-by-default plugin template:
- `/etc/audit/plugins.d/veilor-remote.conf` — auditd plugin shim
(set `active = yes` to enable).
- `/etc/audisp/audisp-remote.conf.disabled` — audisp-remote target
config template (rename to `audisp-remote.conf` and edit
`remote_server` to enable).
**Warning:** enabling remote audit shipping leaks every privileged syscall,
file-watch hit, and auth event off-device. Treat the collector as a host
with the same trust level as root. Only enable if the collector itself is
hardened and the transport is TLS or kerberized.
Reference integration paths in the template: Loki via promtail/vector
syslog source, Wazuh via local wazuh-agent (no network shipping needed),
Splunk via HEC bridge.
## What's *not* enabled by default
- **Disk swap** — replaced by zram (RAM-only, no key leak risk).

View file

@ -1,95 +0,0 @@
# veilor-os Installer
Branded TUI installer that runs on `tty1` of the live ISO. Wraps the
underlying `anaconda` kickstart install with a single-flow user experience
similar in spirit to `omarchy` and `archinstall`.
> **Status (v0.5.1):** TUI rewritten on top of [`gum`][gum] (charm.sh's
> Go TUI toolkit). Replaces the v0.5.0 `whiptail` build, which used the
> Fedora-default colors and looked out of place against the rest of the
> branded system.
## Screenshots
> _Placeholder — real screenshots to be captured against the v0.5.1 ISO
> once the gum-based installer ships and boots clean on test hardware._
| Stage | Path |
|----------------|---------------------------------------|
| Banner + menu | `assets/installer/screenshots/01-menu.png` _(TBD)_ |
| Disk picker | `assets/installer/screenshots/02-disk.png` _(TBD)_ |
| Confirm | `assets/installer/screenshots/03-confirm.png` _(TBD)_ |
| Install spin | `assets/installer/screenshots/04-spin.png` _(TBD)_ |
## Boot flow
```
power on
└─ UEFI / GRUB
└─ live kernel + initramfs
└─ systemd → multi-user.target
└─ getty@tty1.service.d/veilor-installer.conf
└─ /usr/local/sbin/veilor-installer
├─ source assets/installer/colors.gum
├─ cat assets/installer/banner.txt
└─ gum choose <main menu>
```
The override at `overlay/etc/systemd/system/getty@tty1.service.d/veilor-installer.conf`
replaces the standard login prompt on tty1 with the installer entry point.
Other ttys (2-6) still get a normal getty for recovery use.
## Main menu
| # | Option | Action |
|----|---------------------------------------------|--------------------------------------------|
| 1 | Install veilor-os to disk | collect answers → generate ks → anaconda |
| 2 | Try live — desktop (KDE Plasma) | `systemctl isolate graphical.target` |
| 3 | Try live — shell | `exec /bin/bash --login` |
| 4 | Reboot | `systemctl reboot` |
| 5 | Power off | `systemctl poweroff` |
## Install path — questions asked
In order, the installer collects:
1. **Target disk** (`gum choose` over `lsblk` output — selected disk is wiped)
2. **Hostname** (`gum input`, default `veilor`)
3. **LUKS passphrase** (`gum input --password`, min 8 chars, full-disk encryption)
4. **Admin password** (`gum input --password`, min 8 chars)
5. **Locale** (`gum choose` — en_GB, en_US, de_DE, fr_FR)
6. **Confirmation** (`gum confirm` — summary of choices before destructive step)
Answers are written into `/run/install/veilor-generated.ks` and handed off
to `anaconda --kickstart=...`. The kickstart inlines the LUKS passphrase
and the admin password — the file is _never_ committed and lives only in
the live tmpfs.
## Branding assets
| File | Purpose |
|-------------------------------------|----------------------------------------|
| `assets/installer/banner.txt` | ASCII banner shown above the menu |
| `assets/installer/colors.gum` | sourceable bash file of GUM_* env vars |
The palette mirrors `assets/kde/veilor-black.colors`:
black `#000000` background, white `#FFFFFF` foreground, grey `#686B6F`
accent. No reds, no other colors. Pure monochrome.
## Logs
- `/var/log/veilor-installer.log` — installer stdout/stderr
- `/tmp/anaconda.log` — kickstart execution log
Both are tee'd to the screen during the install spin, so a failed install
leaves visible breadcrumbs without forcing the user to dig.
## Credits & license
- [`gum`][gum] by [Charm](https://charm.sh) — MIT-licensed Go TUI toolkit.
We dynamically `exec` gum at runtime; no source vendored. Distributed via
the Fedora `gum` package.
- veilor-installer itself is MIT-licensed (see [LICENSE](../LICENSE)),
matching the rest of the repo and the upstream gum project.
[gum]: https://github.com/charmbracelet/gum

View file

@ -9,58 +9,6 @@ For the historical record of what landed in each release, see
---
## ⚡ STRATEGY PIVOT — 2026-05-06
**Decision: skip v0.6 kickstart polish. Pivot directly to v0.7
BlueBuild OCI path.**
Reasons:
- v0.5.32 produced a green ISO (2.7 GB) on the Forgejo runner. Proof
point achieved.
- Continuing to debug `livecd-creator` + `anaconda` quirks for v0.6
polish is sunk-cost work on tooling we retire at v1.0 anyway.
- v0.7 spike already has a working BlueBuild recipe + `ostreecontainer`
kickstart directive. Layering veilor branding + installer + power CLI
on top of secureblue beats re-deriving the same hardening from
scratch.
- Ergonomic CLI tools (`veilor-postinstall`, `veilor-doctor`,
`veilor-update`) translate cleanly to v0.7: `bootc upgrade` replaces
`dnf upgrade`. Move them into v0.7 scope.
**v0.5.0 is the final kickstart-path release.** Tag, freeze, ship as
proof-of-work / portfolio anchor. **v0.6 cancelled as a milestone.**
Active focus: `v0.7-bluebuild-spike` branch.
---
## Lessons learned through v0.5.x install grind
Five things v0.5.2731 changed about how we plan:
1. **Anaconda + RPM-6.0 + `--cmdline` is brittle** — three install
failures, kernel cmdline written to four places before one worked.
`--location=none` skips `CollectKernelArgumentsTask`,
`kernel-install` reads `/etc/kernel/cmdline` not `/proc/cmdline`,
and `transaction_progress.py` masks real failures if patched too
broadly. Justifies promoting the bootc-image-builder spike to v0.7.
2. **Test procedure must gate every tag** — v0.5.27 only surfaced four
bugs in one VM run because the run walked every step in order.
`test/TESTING.md` and `test/test-runs/` are now load-bearing.
3. **Real hardware is not optional** — VM catches install logic, not
KMS / fbcon / firmware. Spare laptop + friend's laptop must run
pre-tag, every time.
4. **Multi-agent debug waves work, but only with a verifier** — the
v0.5.31 four-bug fix came from a 4-agent verification wave on
v0.5.30 outcome. Wave + verifier = signal; wave alone = noise.
5. **"We ask once, with sane defaults" is the distro UX** — every
v0.5 install bug we shipped a workaround for (locale, hostname,
USBGuard policy, drivers) is something `veilor-postinstall` could
ask the user about cleanly on first boot. That promotes
`veilor-postinstall` from v0.6 background item to flagship.
---
## v0.2 — green ISO + base hardening (DONE)
Reproducible CI build pipeline. UEFI+BIOS bootable live ISO from a single
@ -76,47 +24,6 @@ Released `v0.2.5` on 2026-05-01. CI on every push to `main`.
---
## v0.5.27v0.5.31 — install path stabilisation (DONE)
The bridge between v0.2 (greens at all) and v0.3 (looks polished). All
install-path bugs surfaced by the formal hybrid-VM test procedure
(`test/TESTING.md`). Five releases, ~hours of debug, three install
failures before greening.
- **v0.5.27 (DONE)**`rd.luks.uuid` via `grubby --update-kernel=ALL`,
GRUB rebrand, `fbcon=nodefer`, ASCII gum cursor.
- **v0.5.28 (DONE)** — locale locked en_US.UTF-8, dropped updates repo,
patched anaconda `transaction_progress.py` to silence `Configuring
xxx.x86_64` scroll, excluded man-db.
- **v0.5.29 (DONE)** — narrowed anaconda patch (was masking real
failures), LUKS UX, initramfs assertion. Five-fix bundle from 7-agent
research wave.
- **v0.5.30 (DONE)** — broad error suppression, manual bootloader path,
virtio log capture for post-mortem.
- **v0.5.31 (DONE)**`--location=none` was making anaconda skip
`CollectKernelArgumentsTask`; kernel-install reads
`/etc/kernel/cmdline` as source of truth, veilor never wrote it, so
BLS entries shipped with empty cmdline. Three-path write
(`/etc/kernel/cmdline` + `/etc/default/grub` + grubby) plus explicit
`kernel-install add`.
## v0.5.32 — next ship (active)
Outstanding from the grind, immediate priority for the next tag:
- **End-to-end VM green run** — v0.5.31 lands the kernel-cmdline fix
but no full hybrid-VM pass has signed it off. Run the procedure in
`test/TESTING.md` to install + reboot + login, file the report in
`test/test-runs/`, then tag.
- **Real-hardware run on the spare laptop** — VM is necessary not
sufficient. Friend's laptop is mate's-test, spare is ours. KMS,
fbcon, USB controller, real-firmware Secure Boot only show up here.
- **gum input render glitch** — duplicate "Install", stray T in
password fields on linux fbcon. Replace `gum input --password` with
bash `read -srp`; cosmetic only but visible on every install.
---
## v0.3 — UX polish (in progress)
The visible polish layer that v0.2 deferred for build velocity.
@ -190,168 +97,42 @@ specified — defaults stay sane for a daily driver.
---
## v0.6 — CANCELLED 2026-05-06 (folded into v0.7)
Per the strategy pivot at the top of this file: v0.6 kickstart polish
will not ship. Continuing on the kickstart path means more
livecd-creator + anaconda debugging on tooling that's retired at v1.0.
The flagship v0.6 deliverables (`veilor-postinstall`, `veilor-doctor`,
`veilor-update`, opt-in installer ISO, first-boot Plymouth dialog,
Bluetooth helper) move into **v0.7 scope** with `bootc upgrade`
replacing `dnf upgrade` in the update path.
The original v0.6 plan is preserved below for reference but is **not
the active roadmap**.
---
## v0.6 — ergonomics (HISTORICAL — superseded by v0.7)
## v0.6 — ergonomics
Smooth the operator experience so day-to-day work doesn't fight the
hardening. `veilor-postinstall` and `veilor-doctor` were v0.6 background
items — promoted to **headline** features after v0.5.2731 made it
clear that "we ask once, with sane defaults" is what separates a
distro from a kickstart.
hardening.
- **`veilor-postinstall`** (PROMOTED — flagship of v0.6) — first-login
welcome menu, EndeavourOS-style but cleaner. Single TUI screen:
keyboard layout, locale (deferred from install per v0.5.28),
hostname override, package presets (dev / media / homelab), drivers
(NVIDIA / Intel / AMD), Bluetooth opt-in, USBGuard snapshot, audit
baseline run, `veilor-doctor` first run. Each step skippable, runs
once on first SDDM login, self-deletes the autostart after. This is
the **only** UX feature that ships in v0.6 day one — everything else
builds on it.
- **`veilor-doctor`** (PROMOTED — user-facing, not just dev tool) —
the post-install audit. Walks `getenforce`, `mokutil --sb-state`,
`firewall-cmd`, fail2ban, USBGuard policy, sysctl drift, and reports
drift from baseline. Runs from `veilor-postinstall` on day one, then
weekly via `systemd --user` timer. Plain-English output ("your
firewall is OK", "USBGuard policy has 3 unknown devices"); not a JSON
dump. **Stretch:** machine-readable mode for `veilor-server` later.
- **`veilor-update`** — wraps `dnf upgrade` AND `flatpak update` in
one command. Per `feedback_system_update.md`, partial-update is a
recurring trap; veilor's update tool covers both by default. Adds
pre-check (snapshot available?), auditd pause, post-update SELinux
validation.
- **`veilor-update`** — wraps `dnf upgrade` with a pre-check (snapshot
available?), an auditd pause, and post-update sysctl/SELinux
validation. One command, no surprises.
- **`veilor-doctor`** — diagnostic helper. Walks the audit checklist
(`getenforce`, `mokutil --sb-state`, `firewall-cmd --get-default-zone`,
fail2ban status, USBGuard policy, sysctl drift) and reports what's
drifted from baseline.
- **Opt-in installer ISO** — flip from live-only to live + installer,
user picks at boot menu. Installer uses the v0.5 kickstart with full
LUKS + btrfs subvols + zram.
- **First-boot UX** — replace TTY password prompt with a small
Plymouth-rendered dialog. Less raw.
- **Bluetooth opt-in helper** — single command to enable + bring up
the daemon + add the user to the right group.
the daemon + add the user to the right group. Currently three
commands.
---
## v0.7 — BlueBuild OCI mainline (ACTIVE — primary focus 2026-05-06+)
## v0.7 — public flex
This was originally planned as "public flex + bootc spike". Post-pivot,
v0.7 is now the **primary active milestone** — it absorbs all v0.6
ergonomic work and becomes the next ship target.
Take veilor-os out of "private repo, contained audience" mode.
Scope:
- BlueBuild recipe (`bluebuild/recipe.yml`) layering on
`ghcr.io/secureblue/securecore-kinoite-hardened-userns`
- `kickstart/install-ostreecontainer.ks` — 10-line kickstart that calls
`ostreecontainer --url=ghcr.io/veilor-org/veilor-os:43 --transport=registry`
and lets Anaconda's LUKS UX drive the install
- veilor brand layer: KDE black theme, gum installer assets, custom
Konsole profile, branded `os-release`
- `veilor-power` 3-mode CLI (lifted as-is from v0.5.x overlay)
- `veilor-postinstall` (formerly v0.6 flagship) — first-login TUI
- `veilor-doctor` (formerly v0.6) — boot-time + weekly drift check
- `veilor-update` rewritten on `bootc upgrade` (was `dnf upgrade`)
- Forgejo registry as primary OCI publish target; GHCR mirror optional
- cosign key-pair signing of OCI image (replaces broken keyless flow)
Public-flex items kept from original v0.7 entry:
Take veilor-os out of "private repo, contained audience" mode. Order
matters: people demand threat model FIRST when a security distro goes
public, benchmarks come after.
1. **Threat model published** (FIRST — gating item) — what veilor-os
defends against, what it does not. Honest scope. No claim of
anti-state-actor; concrete on lost-laptop, USB-attack, browser
compromise, supply-chain. Reviewers will demand this before reading
anything else.
2. **Public docs site** — Hugo or mdBook on `veilor.org`, generated
from `docs/`. Single source of truth.
3. **Repo public** — flip GitHub visibility, announce.
4. **Comparison + benchmarks** — published numbers vs stock Fedora KDE
on cold boot, idle RAM, idle network egress, suspend/resume time.
After threat model, not before.
5. **Press kit** — wallpapers, logo, screenshots, feature one-liner.
### Hybrid bootc spike — layer on secureblue, install via `ostreecontainer` (REVISED 2026-05-05)
The original v0.7 entry called for a Containerfile-from-scratch
spike on `quay.io/fedora/fedora-bootc:43`. Research on 2026-05-05
(see `docs/STRATEGY.md` and
`docs/research/2026-05-05-agent-wave/`), then a parent-operator
refinement same day, locked the path: **layer veilor's branding +
threat model + UX on top of secureblue's already-shipping
`securecore-kinoite-hardened-userns` OCI image** via a BlueBuild
recipe, and install it directly during the Anaconda pass via the
`ostreecontainer` kickstart directive (no first-boot rebase).
Reasoning:
- secureblue has 30 active contributors, 940 stars, 56 commits
in the last 5 weeks. They've already implemented the hardening
surface we'd need to build alone (sysctl + kargs + SELinux
custom policy + USBGuard + hardened-malloc + Unbound DoT +
cosign-signed OCI build pipeline).
- Containerfile-from-scratch spike: 1 week to first ISO. BlueBuild
recipe extending secureblue: ~2 days. With the `ostreecontainer`
swap (no `veilor-firstboot-rebase.service`, no transition window):
**~1 day**.
- secureblue does NOT publish a threat model. Athena OS does
(their main differentiator, only public threat model in
hardened-Linux 2026). Our `docs/THREAT-MODEL.md` (drafted) gets
us ahead of both on the one axis that matters most for a
security-branded distro.
Hybrid path locked:
- Kickstart ISO stays as the **bootstrap installer** (Anaconda's
LUKS UX is mature).
- `%packages` is replaced with `ostreecontainer
--url=ghcr.io/veilor/veilor-os:43 --transport=registry` so the
install pass populates `/` directly from the OCI image — no
first-boot rebase, no second reboot.
- From boot one onward, `bootc upgrade` is the update channel.
- v1.0 deprecates the kickstart entirely.
Stay on `ostreecontainer` through v0.8. **Do NOT migrate to the new
`bootc` kickstart command until v1.0** — it blocks multi-disk and
authenticated registries (likely needed eventually). **Do NOT use**
`bootc-image-builder anaconda-iso` output — deprecated in
image-builder v44+. Produce OCI image and bootstrap ISO as
**separate artifacts**.
Overrides over secureblue: keep Trivalent as default (their COPR
tracks upstream M147+ within hours; reverses earlier draft that
treated it as override-and-remove); add Mullvad Browser alongside;
gate Thorium behind `ujust install-thorium` with CVE-lag warning;
restore sudo (revert `run0`-only); re-enable Xwayland.
Mesh stack baked in: Tailscale (Day 1, daily driver), Yggdrasil-go
(Day 1, idle warm-fallback), Reticulum/RetiNet AGPL fork (opt-in
via `ujust install-reticulum`). See `docs/STRATEGY.md` mesh stack
section for the layer breakdown and threat-floor table.
Full plan: `docs/STRATEGY.md`. Spike will land in
`bluebuild/recipe.yml` plus `.github/workflows/build-bluebuild.yml`,
on a separate branch — does NOT land in v0.5.x main.
External dependency tracked: Traefik `no-guest@file` ACL on
nullstone is currently an `0.0.0.0/0` allow-all stub. Must be
fixed before veilor-os first-public-ISO ships, otherwise
`tag:guest` provisioning leaks the full vhost surface to every
veilor user. **Parent operator owns the fix; not in veilor-os
scope.**
- **Public docs site** — Hugo or mdBook on `veilor.org`, generated from
`docs/`. Single source of truth for INSTALL, HARDENING, BUILD,
ROADMAP, RELEASE, CONTRIBUTING.
- **Repo public** — flip GitHub visibility, announce.
- **Comparison + benchmarks** — published numbers vs stock Fedora KDE
on cold boot, idle RAM, idle network egress, suspend/resume time.
- **Threat model published** — what veilor-os defends against, what it
does not. Honest scope.
- **Press kit** — wallpapers, logo, screenshots, feature one-liner.
---
@ -378,16 +159,15 @@ daily driver.
## Stretch goals — not on the v0.x → v1.0 critical path
These are spin variants that share veilor-os DNA but need their own
kickstart or build tool.
kickstart or build tool. They live on a separate track and do not
block v1.0.
- **`veilor-server`** — no KDE, no GUI, hardened headless Fedora for
homelab / VPS (e.g. nullstone). Same overlay, different package set.
**Not blocked**, but waits on `veilor-doctor` machine-readable mode
(v0.6) so headless installs have a way to report drift without a TUI.
homelab / VPS. Same overlay, different package set.
- **`veilor-kiosk`** — single-app Plasma session, locked-down user,
read-only root. **Not blocked.**
read-only root. For dedicated-purpose machines.
- **`veilor-atomic`** — rpm-ostree / bootc-image-builder rebase.
Status now depends on the **v0.7 bootc spike**: if the spike shows
bootc fixes the anaconda-grind class of bugs, `veilor-atomic`
becomes the v1.0+ mainline rather than a stretch variant. If not,
it stays a parallel track.
Immutable root, transactional updates, atomic rollback. Different
build tool entirely (likely `bootc-image-builder`); all veilor
hardening would translate to a `Containerfile`. Schedule for after
v0.5+ once the standard spin is stable.

View file

@ -1,336 +0,0 @@
# veilor-os Strategy — Hybrid kickstart bootstrap + bootc OCI
Decision date: **2026-05-05** (refined same day from parent-operator
handoff, locks the `ostreecontainer` install path, mesh stack
bake-in, browser stack, Iroh seeding roadmap, and threat floor table).
Locked at: **v0.5.31 → v0.7 spike → v1.0**
## TL;DR
- Keep the Anaconda-driven kickstart ISO as the **bootstrap installer**
(LUKS UX is mature, single passphrase prompt, custom partitioning
works).
- Anaconda's `ostreecontainer` directive populates the root filesystem
directly from a **veilor-os OCI image** (built via BlueBuild on top
of secureblue's `securecore-kinoite-hardened-userns`) **during the
install pass — no first-boot rebase, no mutable→atomic transition**.
- All future updates flow through `bootc upgrade` — atomic A/B,
instant rollback, cosign-signed.
- The kickstart-driven mutable-root path is deprecated at v1.0; kept
alive as fallback through v0.7.
## Why hybrid, not pure pivot
Pure pivot to bootc-from-scratch (Agent 3's spike plan) was **1 week
to first ISO**. Pure pivot to layering on secureblue is **2 days to
first ISO** because the hardening work is already done. The
`ostreecontainer` refinement compresses that to **1 day** by
eliminating the first-boot rebase choreography (no
`veilor-firstboot-rebase.service`, no second reboot, no transition
window where the system is half-mutable, half-atomic).
Both pure-pivot paths require throwing away the partitioning UX we
already have working in Anaconda. Hybrid keeps it.
Hybrid:
- **Day-zero install:** Anaconda kickstart + custom partitioning +
LUKS prompt (what we have today). User experience = unchanged.
- **End of install pass:** `ostreecontainer
--url=ghcr.io/veilor/veilor-os:43 --transport=registry` populates
`/` from the OCI image. Transition is invisible.
- **First boot:** veilor OCI tree, no rebase, no special service.
- **Day-2:** `bootc upgrade` cadence for everything from then on.
We keep what works, pivot the part that doesn't.
## ostreecontainer directive (refinement, locked)
Replace the `%packages` block in the install kickstart with:
```
ostreecontainer --url=ghcr.io/veilor/veilor-os:43 --transport=registry
```
Keep the existing `part`/LUKS encryption block verbatim — Anaconda
partitions before `ostreecontainer` populates root.
**Stay on `ostreecontainer` through v0.8.** Do NOT migrate to the new
`bootc` kickstart command until v1.0 — `bootc` blocks multi-disk and
authenticated registries, both of which we'll likely need.
**Do NOT use** `bootc-image-builder anaconda-iso` output —
deprecated in image-builder v44+. Produce the OCI image and the
bootstrap ISO as **separate artifacts**:
- OCI image: BlueBuild recipe → cosign-signed image at
`ghcr.io/veilor/veilor-os:43`
- Bootstrap ISO: Anaconda kickstart with `ostreecontainer` directive
pointing at the OCI image
Reference: <https://docs.fedoraproject.org/en-US/bootc/>; pykickstart
docs for `ostreecontainer`.
## Why secureblue underneath
| Question | Answer |
|---|---|
| Maintainers | secureblue: 30 contributors, 56 commits/5wks. veilor-os: solo. |
| Hardening surface | secureblue ships sysctl + kargs + SELinux + USBGuard + hardened-malloc + DoT — far more than we'd build alone. |
| Build pipeline | BlueBuild → cosign-signed OCI in GH Actions (`build-all.yml`, `trivy.yml`). |
| Update model | bootc upgrade with A/B + instant rollback + signed image chain. |
| Variants | `kinoite-hardened-userns` is the KDE+Wayland+SELinux variant we'd want. |
| License | Apache-2.0 (compatible with our MIT). |
What we override in our recipe:
- **`run0` instead of sudo**: revert. Breaks too many workflows.
- **Xwayland disabled**: revert. Some apps still need it.
- **Veilor branding**: theme, KDE color scheme, Plymouth, SDDM, font,
os-release. All `overlay/*` ports verbatim from current repo.
(Browser stack is its own section below — Trivalent is now a *kept*
default, not an override.)
## Browser stack
| Role | Pick | Source |
|---|---|---|
| **Default browser** | **Trivalent** (secureblue's hardened Chromium) | Fedora COPR `secureblue/trivalent` — tracks upstream M147+ within hours, ships hardened_malloc + JIT-less + Drumbrake WASM |
| **Anti-fingerprint companion** | **Mullvad Browser** | Clearnet, no Tor, layered alongside Trivalent for pseudonymous browsing |
| **Optional opt-in** | **Thorium** | `ujust install-thorium` only — WARN users of months-long CVE lag (LTS Chromium base, ~9 milestones behind upstream stable as of 2026-05) |
**DO NOT default to Thorium under any circumstances** — contradicts
the threat model. Trivalent's COPR keeps us inside one-hour-of-upstream
patch latency; Thorium is multi-month-stale and is a perf/media
profile choice, not a security choice.
The earlier draft of this doc treated Trivalent as an override-and-
remove. That was wrong: Trivalent is exactly the level of hardening
we want for a default browser. Keep it. Add Mullvad alongside.
Move Thorium behind an explicit opt-in.
## Mesh stack — three-layer warm-stack
Day 1 ships layers 1 (Tailscale) and 2 (Yggdrasil idle). Layer 3
(Reticulum) is opt-in via `ujust`.
### Layer 1 — Tailscale + Headscale (daily driver)
- Already running on `nullstone`, `hs.s8n.ru`. OIDC via Authentik.
- Veilor OS ships `tailscale-1.94.2+` from official Fedora repo.
- Service unit **pre-disabled** at install time.
- First-boot prompt: "join Veilor mesh? [paste / QR]". On accept:
`tailscale up --login-server=https://hs.s8n.ru` with the user's
pre-auth key.
### Layer 2 — Yggdrasil-go (warm fallback, idle by default)
- `yggdrasil-go` 0.5.13+ from COPR / dnf.
- Decentralized IPv6 in `200::/7`.
- systemd unit **enabled** but config = empty `Listen[]`, one
`Public peer` (e.g. `vpn.itrus.su` or another EU peer),
`AllowedPublicKeys` allowlist mode (no allow-all).
- WSS:443 transport for ISP DPI evasion.
- Generates ECC keypair on first boot via systemd-tmpfiles or
firstboot script.
- Survives ISP-level Tailscale block (threat floor (ii)).
### Layer 3 — Reticulum (opt-in)
- **RetiNet AGPL fork** (NOT upstream RNS — upstream has an anti-AI
license clause incompatible with our governance). Sourced from the
Codeberg AGPL fork.
- Sideband (Android/desktop messenger built on RNS).
- Install via `ujust install-reticulum`. NOT auto-started until
RetiNet stabilizes.
- Default config when enabled: `AutoInterface` (LAN multicast) +
12 TCP backbone peers.
- RNode hardware (LoRa transceiver) bundle as separate
`ujust install-reticulum-rnode`.
- Survives total internet outage (threat floor (iii)) when paired
with RNode.
## Onboarding model
Token-based (paste OR QR, user picks). Misskey signup page mints a
**reusable pre-auth key** (TTL=24h, single-use, regenerated per
signup). First boot of Veilor ISO accepts hex paste OR QR scan of
the same key.
**NOT auto-OIDC at first boot** — too much Authentik exposure for
day-zero users.
## Tier model — three-tier
- `tag:admin` — onyx + failsafe. Full mesh, `*:*`.
- `tag:infra` — nullstone, office. Mesh among themselves; admin
inbound only.
- `tag:guest` — Veilor OS users + friend. ONLY `x.veilor:443`
reachable + future seeded service hostnames whitelisted.
- **Failsafe** — pre-baked admin pre-auth key on yubikey + printed
paper + Authentik OIDC group `tailnet-admin` as second auth path.
## Threat floor table
| Floor | Attack | Day 1 (v0.7 ship) | Phase 2 (v0.8) |
|-------|--------|---|---|
| (i) | ISP blocks `s8n.ru` DNS | Tailscale dies, Yggdrasil survives | YES (documented failover) |
| (ii) | ISP blocks Tailscale protocol | Yggdrasil-WSS:443 survives | YES |
| (iii) | Internet unreachable | RNS over LoRa survives | OPT-IN (RetiNet + RNode) |
Day 1 must hold floor (i). Floors (ii) and (iii) become P2 once
Yggdrasil is promoted from idle to documented failover.
## Iroh seeding daemon (Phase 2 / v0.8)
- `veilor-seed.service` systemd unit, runs as `_veilor-seed` user.
- Watches `/var/lib/<service>/files/` blob store directories.
- BLAKE3-hashes new blobs, registers with local iroh node.
- Publishes tickets on per-service `iroh-gossip` topic.
- LRU local cache, default 10 GB.
- Sidecar mirrors service blob stores: Misskey `/files/`, Matrix
media, `dl.veilor` downloads.
- Other Veilor nodes pull lazily on cache miss.
- **DEFER DB replication forever.** Static media only.
DOCUMENT but DO NOT IMPLEMENT until **Iroh hits 1.0** (currently
0.960.98 RC season; 1.0 target Q1 2026 slipped, watching).
Reference: <https://github.com/n0-computer/iroh-blobs/blob/main/DESIGN.md>.
## External dependency — Phase 0 (NOT veilor-os scope)
Real ACL gap on nullstone Traefik right now: friend on `tag:guest`
can reach `nullstone:443` → SNI-routes to ALL Traefik vhosts
(`sys.s8n.ru`, `pihole.s8n.ru`, `hs.s8n.ru`, `auth.s8n.ru`, n8n, rc,
mx, …). Only per-vhost auth blocks them. The `no-guest@file` Traefik
middleware that should fix this is currently an `0.0.0.0/0`
allow-all stub (neutralized 2026-05-03 from XFF chain breakage).
**veilor-os does NOT fix this.** Tracked here as an external
dependency: ACL fix on nullstone Traefik **required before veilor-os
first-public-ISO ships**, otherwise `tag:guest` provisioning leaks
the full vhost surface to every veilor user. Parent operator owns it.
## Strategic credibility win
secureblue does NOT publish a threat model. Athena OS does, and it's
their main differentiator. We've already drafted
`docs/THREAT-MODEL.md` (Agent 5 of 2026-05-05 wave). Publishing that
*before* the v0.7 launch positions veilor-os ahead of secureblue and
Athena on the one axis that matters most for a security-branded
distro: **honest, scoped, public threat model**.
## Roadmap implications
| Version | Status | Path |
|---|---|---|
| v0.5.31 | shipped | Anaconda kickstart, mutable root |
| v0.5.32 | active — top blockers from 9-agent wave | Anaconda kickstart |
| v0.5.x → v0.6 | maintenance | Anaconda kickstart, ergonomics + UX polish |
| **v0.7 spike** | **1-day BlueBuild prototype** (was 2 days; `ostreecontainer` removes first-boot-rebase work) | First veilor OCI image extending secureblue-kinoite-hardened |
| v0.7 ship | ISO bootstraps install, `ostreecontainer` populates from OCI in-pass | Hybrid path live |
| v0.8 | Iroh seeding (P2P static media), Yggdrasil promoted from idle to documented failover, RetiNet stabilization watch | bootc-only direction |
| **v1.0** | **bootc-only**, kickstart deprecated, possibly migrate `ostreecontainer` → new `bootc` kickstart command if multi-disk + auth-registry blockers resolved upstream | `bootc upgrade` for all updates |
The Containerfile-from-scratch spike plan (Agent 3 of 2026-05-05
wave) is **superseded** by this hybrid: don't build a Containerfile
from scratch on `fedora-bootc:43`. Instead, write a BlueBuild recipe
on `securecore-kinoite-hardened-userns`. With `ostreecontainer`
swap, spike compresses 1 week → 1 day.
## Next concrete steps
### v0.5.32 — current (no strategy change)
Ship the 7 blockers from `docs/research/2026-05-05-agent-wave/`:
suspend/resume wifi fix, firstboot WantedBy, USBGuard id-rules,
firewalld tailscale0 zone, KMS modeset, /etc/skel branding, virtio-9p
log capture.
`ostreecontainer` swap **does NOT land in v0.5.32 main.** It belongs
in the v0.7 spike branch only.
### v0.7-spike (1 day, separate branch)
1. New repo dir: `bluebuild/recipe.yml`.
2. `from`: `ghcr.io/secureblue/securecore-kinoite-hardened-userns:latest`.
3. Override modules:
- `type: files` — stamp our `overlay/*` tree (branding, themes,
veilor scripts, sddm theme, plymouth theme).
- `type: rpm-ostree` — install Mullvad Browser + restore Xwayland +
re-enable sudo (revert run0).
- **Keep Trivalent** as default (was wrongly marked for removal in
the first draft of this doc).
- `type: brand` — PRETTY_NAME, GRUB_DISTRIBUTOR, distributor URL.
- `type: files` — pre-disabled `tailscale.service`, idle
`yggdrasil.service`, `ujust install-reticulum` and
`ujust install-thorium` recipes.
4. `.github/workflows/build-bluebuild.yml` — pull BlueBuild action,
build + cosign sign + push to GHCR.
5. `kickstart/install.ks` — replace `%packages` block with
`ostreecontainer --url=ghcr.io/veilor/veilor-os:43
--transport=registry`. Keep existing partitioning + LUKS block
verbatim. **Drop** all planned `veilor-firstboot-rebase.service`
work — no longer needed.
### v1.0 — bootc-only
- Drop `kickstart/veilor-os.ks`, drop `livecd-creator` workflow.
- Bootstrap ISO is built as a **separate artifact** (NOT via
`bootc-image-builder anaconda-iso`, which was deprecated in
image-builder v44).
- The OCI image is the source of truth.
- `veilor-update` becomes thin `bootc upgrade --apply` wrapper.
- Migrate `ostreecontainer` directive → new `bootc` kickstart
command IF multi-disk + authenticated-registry support has landed
upstream by then.
## Open questions
- Does secureblue accept upstream contributions? If yes, send our
USBGuard id-based-rules fix and our threat-model framework.
- Recovery flow when `ostreecontainer` install pass fails — Anaconda
should abort cleanly; verify in spike that no half-installed
state is bootable.
- Iroh 1.0 timing — currently 0.960.98 RC; Q1 2026 target slipped.
Re-evaluate Phase 2 schedule when 1.0 lands.
- RetiNet upstream stabilization — track Codeberg fork for releases.
If it stalls > 6 months we re-evaluate Layer 3.
- Fedora 44 transition: secureblue tracks Fedora releases (current
`v4.9` on F44). If we follow, we get F44 for free at the same time
upstream does.
## Self-hosted git + CI (locked 2026-05-05)
Primary git host moved off github.com. **Forgejo** runs on nullstone
at `git.s8n.ru`, with **forgejo-runner** doing the build work. GH free-
tier minute quota was hammering veilor-os iteration; we self-host now.
- Primary remote: `ssh://git@192.168.0.100:222/veilor-org/veilor-os.git`
(Forgejo, LAN-only until router port-forward 222 → nullstone:222
added — TODO; or use tailnet hostname once tailscale logged in).
- Public mirror: `https://github.com/veilor-org/veilor-os.git`. Forgejo
push-mirrors every commit + every 8h, so GH stays in sync without
consuming GH minutes.
- Runner labels: `ubuntu-24.04` (catthehacker image — works for our
current build-iso.yml unmodified) and `nullstone` (privileged Fedora
43 container — opt-in via `runs-on: nullstone`).
- Build cost: 0 GH minutes. Disk: ~80 GB workspace on /home/docker.
Deploy artifacts: `~/ai-lab/nullstone-server/forgejo/`. Runbook in same
dir.
## See also
- `docs/THREAT-MODEL.md` — drafted, needs publish for v0.7
- `docs/ROADMAP.md` — updated to reflect this strategy
- `docs/research/2026-05-05-agent-wave/03-bootc-spike-plan.md`
superseded by this hybrid (kept as reference for the
Containerfile-from-scratch alternative)
- secureblue: <https://github.com/secureblue/secureblue>
- BlueBuild: <https://blue-build.org>
- bootc / ostreecontainer docs: <https://docs.fedoraproject.org/en-US/bootc/>
- Yggdrasil: <https://github.com/yggdrasil-network/yggdrasil-go>
- Reticulum manual: <https://reticulum.network/manual/>
- Iroh blobs design: <https://github.com/n0-computer/iroh-blobs/blob/main/DESIGN.md>

View file

@ -1,157 +0,0 @@
# Threat Model
> **Status:** Final for v0.7 public launch. Honest scope.
veilor-os is a hardened daily-driver desktop. Not a paranoia OS, not an
anonymity OS, not an isolation OS. This document exists so that
security-conscious developers, journalists, and activists can decide whether
the threat model fits their actual adversary before they trust the system.
If your adversary is on the "out of scope" list below, **use a different
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. |
---
## 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.**
| 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. |
---
## Hardening tradeoffs (what you give up)
Hardening that breaks ordinary work gets called out, not hidden.
- **SELinux enforcing** — some apps (proprietary, out-of-tree) ship
without policy. Symptom: `EACCES` despite correct file perms.
Workaround: write a local policy module; do not switch to permissive.
- **LUKS2 argon2id (mem=1 GB / time=9)** — boot 530 s slower on older
CPUs. The cost of a passphrase that survives a GPU attacker.
- **USBGuard default-block** — every new device needs an explicit allow.
First-boot: plug trusted devices in, run `usbguard generate-policy`.
Forget this and your USB-C dock looks broken.
- **Module lockdown 30 s after graphical boot** — out-of-tree drivers
(NVIDIA proprietary, VirtualBox, out-of-tree wireguard) will fail.
Load early via initramfs or use the in-tree alternative.
- **firewalld zone = drop** — KDE Connect, mDNS printer discovery, SMB
browsing don't work until explicitly opened. This is the point.
- **No PackageKit / no Flatpak by default** — updates happen on your
terms via `dnf upgrade`.
---
## Where veilor-os IS like Tails / Whonix / Qubes
- Threat model published. Transparency about scope is the price of being
taken seriously.
- Default-deny firewall (`drop` zone, ssh inbound only).
- Encrypted at rest by default — LUKS2 + argon2id, no-disk-swap (zram).
## Where veilor-os DIFFERS
- **Daily-driver target.** Boot it once, install it, use it for years.
Not a session-only / amnesia OS.
- **Single-VM / single-kernel.** No per-app compartmentalisation. A
browser RCE owns your session. (See "out of scope".)
- **Persistent identity by design.** Your `~`, your keys, your shell
history persist. This is a feature for an operator, a misfeature for
an activist evading correlation.
---
## Comparison matrix
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 |
---
## Where veilor-os fits
Pick veilor-os if your job is to write code, edit docs, manage
infrastructure, read mail, browse — and you want a desktop that won't
quietly betray you to a generic adversary while you do it. **You are the
user, not the target of a state.**
Pick **Tails** for amnesia and Tor by default. **Qubes** if you must assume
any app could be compromised. **Kicksecure** for similar hardening on
Debian. **secureblue** for a hardened atomic Fedora. **Stock Fedora KDE**
if you just want Fedora with no opinions.
---
## v0.7 public-launch checklist
These are the items that gate flipping the repo public and posting:
- [ ] Threat model finalised and published (this document).
- [ ] GPG-signed releases working (v0.4 dependency — ISO + sha256 + .asc).
- [ ] Reproducible build verifiable from clean checkout (v0.4).
- [ ] mkdocs-material (or Hugo) site live on `veilor.org`, generated from
`docs/`. INSTALL, HARDENING, BUILD, ROADMAP, RELEASE, THREAT-MODEL,
CONTRIBUTING all rendered.
- [ ] Comparison + benchmark numbers published (cold boot, idle RAM, idle
egress, suspend/resume) vs stock Fedora KDE.
- [ ] Press kit page: wallpapers, logo SVG, screenshots, feature
one-liner, signed quotes from early users.
- [ ] **"What veilor-os is not"** preempt page — direct link from launch
post. Answers "why not Qubes?", "why not Tails?", "why not just
stock Fedora?" so the first hundred comments don't have to.
- [ ] Comparison post drafted for **r/linux**, **r/Fedora**, **HN**.
Same body, three formats. Lead with the threat model link, not the
black wallpaper.
- [ ] CHANGELOG.md tagged at v0.7.0 release commit; GitHub Release
created with ISO + sha256 + .asc artefacts attached.
- [ ] Repo flipped to public, `veilor.org` DNS pointed at the docs site,
Mastodon / Matrix / SimpleX announcement queued.
---
*Last reviewed: v0.7 draft. Update every minor release.*

View file

@ -1,109 +0,0 @@
# Plymouth + LUKS unlock — real-hardware edge cases
**Agent 1 of 9-agent wave, 2026-05-05.**
## State at v0.5.31
- Live ISO cmdline pins `plymouth.enable=0 fbcon=nodefer`.
- Installed system uses Plymouth `details` theme.
- LUKS2 argon2id, no clevis / cryptenroll, no recovery key generation.
- `rd.vconsole.keymap=` not set.
## Findings
### 1. KMS / fbcon races
- **Symptom:** Black screen at LUKS prompt, cursor blinks, keystrokes
swallowed but never accepted.
- **Cause:** `i915` / `amdgpu` / `nvidia-drm` modeset fires *during*
plymouthd handover. With `plymouth.enable=0` we skip the splash but
the ask-password agent still opens `/dev/tty1`, which races `fbcon`
rebind.
- **Fix:** keep `fbcon=nodefer`, append
`nvidia-drm.modeset=1 i915.fastboot=0 amdgpu.dc=1` to bootloader.
NVIDIA Optimus killer is `nvidia-drm.modeset=1`.
- **Probability:** HIGH on Optimus, MED on AMD APU, LOW on Intel iGPU.
### 2. Plymouth theme choice — keep `details`
- `details` (kernel/systemd journal under prompt) is best for
blind-typing because the user sees `Please enter passphrase…` *as
text*, full echo as `*`.
- `text` is minimal fallback (no echo, no journal).
- `spinner` is the documented "endless loop, no prompt" failure mode
on real laptops (adi1090x/plymouth-themes#10, Arch BBS 296529).
- **No change.** But verify `plymouth-set-default-theme details`
actually ran post-install (Debian #986023 shows it silently fails
when initramfs rebuild is suppressed). Add `dracut --force
--regenerate-all` after the call.
### 3. Initramfs keymap — HIGH probability for non-US users
- **Symptom:** AZERTY/QWERTZ/Cyrillic user types correct passphrase,
gets "no key available". F43 ships en-US in initramfs by default.
- **Bugs:** RHBZ 1405539, RHBZ 1890085, fedora-silverblue#3.
- **Fix:** drop a placeholder `rd.vconsole.keymap=us` AND have
`firstboot.sh` rewrite it from `/etc/vconsole.conf` after the user
picks a layout. Also `/etc/dracut.conf.d/veilor-keymap.conf` with
`install_items+=" /etc/vconsole.conf "` so keymap is *baked* into
initramfs.
### 4. systemd-cryptsetup vs legacy `crypt` — F43 = systemd-cryptsetup
- F40+ unconditionally uses `systemd-cryptsetup@.service` from
`/etc/crypttab`. Old `rd.luks.uuid=` cmdline still parsed. Stable
through 6.x kernels. No change needed.
### 5. argon2id memory cost — MED on old laptops (<8 GB RAM)
- LUKS2 default = 1 GiB memory cost, `iter-time=2000 ms`. On
Core 2 Duo / Pentium-N this becomes 815s unlock + thrash.
Atom-class N4020: 30s+.
- **Fix in installer post-script:**
`cryptsetup luksConvertKey --pbkdf-memory 524288 --iter-time 2000`
— halves memory to 512 MiB, knocks ~50% off unlock latency.
### 6. TPM2 unlock — defer to v0.6
- F43 ships `systemd-cryptenroll --tpm2-device=auto` ([Fedora
Magazine](https://fedoramagazine.org/automatically-decrypt-your-disk-using-tpm2/)).
No clevis required.
- **v0.6 plan:** opt-in via `veilor-firstboot`
`systemd-cryptenroll --tpm2-pcrs=7+11`. PCR 7 (secure boot state)
+ 11 (kernel/initrd). Don't auto-enroll; PCR pinning is a footgun
on kernel updates.
### 7. FIDO2 unlock — v0.7
- `systemd-cryptenroll --fido2-device=auto` requires `libfido2` +
hmac-secret support. secureblue ships this. Add `libfido2` to
`%packages` + `veilor-fido2-enroll` wrapper.
### 8. Recovery key — MISSING, ship in v0.6
- Today: forgotten passphrase = brick.
- **Fix:** in `firstboot.sh` add
`cryptsetup luksAddKey --pbkdf argon2id /dev/X <(systemd-creds
setup --print-key | head -c 64)` and print the 64-char key once
to a numbered envelope-style screen. Mirrors macOS FileVault.
## Action items
| # | Change | Target |
|---|--------|--------|
| 1 | `nvidia-drm.modeset=1 i915.fastboot=0 amdgpu.dc=1 rd.vconsole.keymap=us` to bootloader append | v0.5.32 |
| 2 | `/etc/dracut.conf.d/veilor-keymap.conf` with `install_items+=" /etc/vconsole.conf "` | v0.5.32 |
| 3 | Force `dracut -f --regenerate-all` after `plymouth-set-default-theme details` | v0.5.32 |
| 4 | argon2id retune (`40-luks-tune.sh`) | v0.6 |
| 5 | Recovery-key generation in firstboot | v0.6 |
| 6 | TPM2 opt-in via `systemd-cryptenroll --tpm2-pcrs=7+11` | v0.6 |
| 7 | FIDO2 opt-in | v0.7 |
## Sources
- [LUKS keyboard layout — fedora-silverblue/issue-tracker#3](https://github.com/fedora-silverblue/issue-tracker/issues/3)
- [RHBZ 1405539 — keymap not honored on initramfs rebuild](https://bugzilla.redhat.com/show_bug.cgi?id=1405539)
- [RHBZ 1890085 — English keymap forced in initramfs](https://bugzilla.redhat.com/show_bug.cgi?id=1890085)
- [Fedora Magazine — TPM2 autodecrypt with systemd-cryptenroll](https://fedoramagazine.org/automatically-decrypt-your-disk-using-tpm2/)
- [Leo3418 — argon2id LUKS tuning](https://leo3418.github.io/collections/gentoo-config-luks2-grub-systemd/tune-parameters.html)
- [QubesOS#8600 — argon2id parameters](https://github.com/QubesOS/qubes-issues/issues/8600)

View file

@ -1,117 +0,0 @@
# SDDM + first-boot UX failure modes
**Agent 2 of 9-agent wave, 2026-05-05.**
## Findings
### 1. SDDM has no username prefilled — BLOCKS LOGIN (perceived)
- User sees blank greeter; no signal that the only user is `admin`.
- **Fix:** `/etc/sddm.conf.d/veilor.conf` add
`[Users]\nRememberLastUser=true` plus seed
`/var/lib/sddm/state.conf [Last]\nUser=admin\nSession=plasma`.
### 2. chage -d 0 + SDDM autologin race
- With `Relogin=false` (current), single-shot is safe.
- **Fix:** Document `Relogin=false`. Don't combine `Autologin=true`
with `chage -d 0`.
### 3. PAM expired-pw change inline in SDDM
- Plasma 6 SDDM 0.21+ renders the chain. **But** if password fails
pwquality (cracklib min=14 + complexity from
`10-harden-base.sh`), error text shown briefly then form resets —
user sees no clear reason for rejection.
- **Fix:** `/etc/security/pwquality.conf.d/10-veilor.conf` with
documented rules + Plasma startup notification showing them.
### 4. Wayland session start failure on virtio-vga — BLOCKS LOGIN
- KWin tries `wlroots`/DRM, fails to acquire `/dev/dri/card0` if
`virtio_gpu` kernel module not loaded.
- **Fix:** add `plasma-workspace-x11` to `%packages`. SDDM session
menu shows `Plasma (X11)` fallback.
### 5. Plasma 6 first-run wizards on /etc/skel-empty
- KWin compositor backend pick + Plasma welcome center + accent
colour wizard — modal stealing focus on first session.
- **Fix:** seed `/etc/skel/.config/`:
- `kwinrc` `[Compositing]\nBackend=OpenGL`
- `kdeglobals [General]\nAccentColor=...`
- `plasma-welcomerc [General]\nLastSeenVersion=99` (suppresses welcome)
### 6. SELinux relabel after first boot — looks like hang
- `touch /.autorelabel` triggers full restore on rootfs; 90s on
4 GB live install, 3-5min on real disk. User hard-resets thinking
it crashed → corrupted relabel state.
- **Fix:** replace with `veilor-relabel.service` that prints
`[veilor] relabeling SELinux file contexts (1/N): %s` to TTY1
with progress, plus one-time post-relabel KDialog notification.
### 7. USBGuard blocks input at SDDM — BLOCKS LOGIN on desktops
- If `/etc/usbguard/rules.conf` empty/missing, USBGuard
`ImplicitPolicyTarget=block` (default) blocks USB. SDDM running
but USB keyboard dead.
- **Fix:** ship a baseline `rules.conf`:
`allow with-interface equals { 03:00:* 03:01:* }`
(HID class) so any keyboard/mouse works pre-policy.
### 8. NetworkManager DHCP — LOW severity
- Wired auto-connects fine. Wi-Fi: silent failure unless SSID
preconfigured. Acceptable; Plasma 6 ships `plasma-nm` widget.
- **Polish:** `/etc/xdg/autostart/veilor-firstboot-net-check.desktop`
→ KDialog "Connect to network?" if `nmcli general` is `disconnected`.
### 9. veilor-firstboot.service ordering — BLOCKS LOGIN on real installs
- **Current:** `WantedBy=multi-user.target` only.
- **Real installs:** default to `graphical.target`, so unit never runs.
- Admin pw stays `veilor` + chage-expired. SDDM PAM bounces to
chauthtok screen — recoverable but ugly.
- **Fix:** `WantedBy=graphical.target multi-user.target`. Add
`Before=graphical.target`. Verify `systemctl enable
veilor-firstboot.service` (in installer line 884) resolves both.
Add `DefaultDependencies=no` + `Wants=systemd-vconsole-setup.service`.
## Endeavour OS welcome app — design notes for veilor-postinstall
EOS welcome (`endeavouros-team/welcome` on GitHub) is bash + yad,
~3000 LOC. Patterns to lift for veilor:
- **Yad GTK dialog** as runtime (single binary dep). veilor (KDE)
uses `kdialog` + `qmlscene` instead — native Plasma look.
- **Tabbed layout:** Welcome | Set up apps | Security | System info | Shortcuts.
- **Self-disabling autostart:**
`~/.config/autostart/veilor-welcome.desktop` removed after user
clicks "Don't show again".
- **External script dispatch:**
`/usr/share/veilor-os/postinstall/<step>.sh` per step. Decouples
UI from actions.
- **Update channel awareness:** pull from
`github.com/veilor-org/veilor-os` releases atom feed; show CVE
advisories from `security.atom` we publish.
**Recommended stack:**
- `/usr/bin/veilor-welcome` (bash entrypoint, ≤300 LOC)
- `/usr/share/veilor-os/postinstall/welcome.qml` (QtQuick/Kirigami UI)
- `/usr/share/veilor-os/postinstall/steps/{01-account,02-network,03-usbguard-policy,04-update,05-tour}.sh`
- `/etc/xdg/autostart/veilor-welcome.desktop`
- Replace current `scripts/firstboot.sh` placeholder with
`step 03-usbguard-policy` (auto-generate-policy is the unfinished
core item).
## Top three to ship next (highest UX impact, lowest risk)
1. **`WantedBy=graphical.target multi-user.target`** in
`veilor-firstboot.service` — fixes silent SDDM-PAM-chauthtok
bounce on real installs.
2. **Username prefill** in `sddm.conf.d/veilor.conf`: add `[Users]
RememberLastUser=true` + `/var/lib/sddm/state.conf [Last]
User=admin Session=plasma`.
3. **USBGuard HID baseline `rules.conf`** — un-bricks any desktop
with USB keyboard.

View file

@ -1,158 +0,0 @@
# bootc-image-builder spike plan — 1-week timebox
**Agent 3 of 9-agent wave, 2026-05-05.** Schedule: v0.7.
## Containerfile draft
```dockerfile
# veilor-os bootc image — Fedora 43 KDE base
FROM quay.io/fedora/fedora-bootc:43
ARG VEILOR_VERSION=0.6.0
RUN dnf install -y --setopt=install_weak_deps=False \
@kde-desktop-environment @kde-apps @core @hardware-support @standard \
kernel-modules kernel-modules-extra glibc-all-langpacks \
grub2-efi-x64 grub2-efi-x64-modules grub2-pc grub2-pc-modules \
grub2-tools grub2-tools-extra shim-x64 efibootmgr \
newt parted cryptsetup lvm2 btrfs-progs \
fail2ban fail2ban-firewalld usbguard usbguard-tools audit \
policycoreutils-python-utils tuned chrony firewalld plymouth \
git vim-enhanced tmux htop podman skopeo \
NetworkManager NetworkManager-wifi \
fontconfig freetype fira-code-fonts \
zram-generator \
&& dnf remove -y --noautoremove \
'abrt*' snapd kde-connect open-vm-tools-desktop mlocate man-db man-pages \
&& dnf clean all && rm -rf /var/cache/dnf
ARG GUM_VERSION=0.17.0
ARG GUM_SHA256=69ee169bd6387331928864e94d47ed01ef649fbfe875baed1bbf27b5377a6fdb
ADD https://github.com/charmbracelet/gum/releases/download/v${GUM_VERSION}/gum_${GUM_VERSION}_Linux_x86_64.tar.gz /tmp/gum.tgz
RUN echo "${GUM_SHA256} /tmp/gum.tgz" | sha256sum -c - \
&& tar -xzf /tmp/gum.tgz -C /tmp \
&& install -m0755 /tmp/gum_${GUM_VERSION}_Linux_x86_64/gum /usr/bin/gum
COPY overlay/ /
COPY assets/ /usr/share/veilor-os/assets/
COPY scripts/ /usr/share/veilor-os/scripts/
RUN bash /usr/share/veilor-os/scripts/10-harden-base.sh \
&& bash /usr/share/veilor-os/scripts/20-harden-kernel.sh \
&& bash /usr/share/veilor-os/scripts/selinux/build-policy.sh \
&& bash /usr/share/veilor-os/scripts/kde-theme-apply.sh \
&& bash /usr/share/veilor-os/scripts/30-apply-v03-theme.sh
RUN plymouth-set-default-theme details \
&& sed -i \
-e 's|^GRUB_DISTRIBUTOR=.*|GRUB_DISTRIBUTOR="veilor-os"|' \
/etc/default/grub
# bootc kargs go in /usr/lib/bootc/kargs.d/, not /etc/default/grub
RUN mkdir -p /usr/lib/bootc/kargs.d && cat > /usr/lib/bootc/kargs.d/10-veilor-hardening.toml <<'EOF'
kargs = [
"lockdown=integrity",
"slab_nomerge",
"init_on_alloc=1",
"init_on_free=1",
"randomize_kstack_offset=on",
"vsyscall=none",
"fbcon=nodefer",
]
EOF
RUN systemctl enable sshd fail2ban usbguard tuned auditd firewalld chronyd sddm \
veilor-firstboot.service veilor-modules-lock.service \
&& passwd -l root \
&& systemctl set-default graphical.target
RUN bootc container lint
LABEL org.veilor.version=${VEILOR_VERSION}
```
## bootc-image-builder config (`build/disk-config.toml`)
```toml
[customizations]
hostname = "veilor-os"
[[customizations.user]]
name = "admin"
password = "veilor"
groups = ["wheel"]
shell = "/bin/bash"
[customizations.kernel]
append = "lockdown=integrity slab_nomerge init_on_alloc=1 init_on_free=1 randomize_kstack_offset=on vsyscall=none fbcon=nodefer"
[customizations.installer.kickstart]
contents = """
zerombr
clearpart --all --initlabel
part /boot/efi --fstype=efi --size=600
part /boot --fstype=ext4 --size=1024
part btrfs.veilor --grow --encrypted --luks-version=luks2 --pbkdf=argon2id
btrfs none --label=veilor btrfs.veilor
btrfs / --subvol --name=root LABEL=veilor
btrfs /home --subvol --name=home LABEL=veilor
"""
```
## GitHub Actions workflow
`build-bootc-iso.yml`:
- runs-on ubuntu-24.04, **timeout 30 min** (vs 90 for livecd-creator)
- permissions: `contents: write`, `packages: write`
- Build OCI image: `podman build` + `podman push ghcr.io/veilor/veilor-os:43`
- Build ISO via `quay.io/centos-bootc/bootc-image-builder:latest`
with `--type anaconda-iso --rootfs btrfs --config /build/disk-config.toml`
- Reuse split + `softprops/action-gh-release@v2` from existing workflow
## Migration risks (10-row table)
| # | Risk | Severity | Mitigation |
|---|------|----------|------------|
| 1 | %post --nochroot overlay-copy disappears | Low | `COPY overlay/ /` is simpler — win |
| 2 | Update model: `bootc upgrade` (image swap) replaces `dnf upgrade` | High | `veilor-update` becomes thin `bootc upgrade --apply` wrapper |
| 3 | /usr is read-only at runtime | Medium | etc-overlay handles /etc writes; relocate any /usr writers to /etc or build-time |
| 4 | SELinux module compilation in container | Medium | Works in fedora-bootc:43 (verified per upstream pattern). Test spike day 2 |
| 5 | `transaction_progress.py` patch unnecessary | Low | bootc-image-builder doesn't use dnf at install. Drop the patch. Win |
| 6 | `rd.luks.uuid` is anaconda's job again | Low | Removes ~80 lines of fragile sed/grubby code. Win |
| 7 | LUKS prompt UX: anaconda native, not gum | High | gum installer becomes `live·shell` only. v1.0 install = anaconda's native UI |
| 8 | --privileged still required | None | Same as today |
| 9 | OCI image size: ~3.5 GB compressed vs ~2.8 GB squashfs | Low | zstd:max recovers ~400 MB |
| 10 | `kernel-install` BLS: `/etc/kernel/cmdline` not honored, `/usr/lib/bootc/kargs.d/*.toml` is | Medium | Already addressed in Containerfile draft |
## What we keep (zero churn)
- `overlay/*` — copied verbatim by `COPY overlay/ /`
- `scripts/*.sh` — invoked verbatim by Containerfile RUN
- `assets/*` — copied verbatim
- `test/*` — adapts: `podman run --rm -it ghcr.io/veilor/veilor-os:43 /bin/bash` smoke; QEMU ISO test unchanged
- `kickstart/install.ks` — kept as fallback. Tag last anaconda build as `v0.5.99-anaconda` before flipping
## Spike success criteria (1 week)
| Day | Milestone |
|-----|-----------|
| 1 | Containerfile builds clean (`podman build` exit 0, `bootc container lint` exit 0) |
| 2 | `podman run` boots into image, KDE binaries present, SELinux + hardening sysctls applied |
| 3 | bootc-image-builder produces installer ISO from OCI, ksvalidator clean |
| 4 | ISO boots in QEMU to anaconda live menu |
| 5 | Install completes, LUKS single-prompt, btrfs subvols present |
| 6 | First boot reaches SDDM, admin login works, password-change-on-first-login enforced |
| 7 | Buffer for fixes; doc `docs/BUILD-bootc.md`; tag `v0.5.99-anaconda` snapshot |
## Decision gate
- **PASS** (all 7 criteria green): tag `v0.5.99-anaconda` as last-anaconda;
merge `bootc-spike``main` as `v0.6.0-bootc`; deprecate
`kickstart/veilor-os.ks` (keep `kickstart/install.ks` for one cycle).
Update ROADMAP: v1.0 ships bootc-only.
- **FAIL** (any of risks 3, 4, 7, 10 unfixable in week 1): keep
anaconda path, defer migration to v1.1+; file each blocker as GH
issue with reproducer.
- **HYBRID FALLBACK**: ship anaconda ISO for v0.6/v0.7, ship bootc OCI
alongside (matches existing `veilor-atomic` stretch goal).

View file

@ -1,125 +0,0 @@
# Hardening tier 2 — concrete plan
**Agent 4 of 9-agent wave, 2026-05-05.**
## Repo state already in tree
- `scripts/apparmor/` ships **3 profiles** (`thorium`, `veilor-power`,
`lm-studio`) — complain-mode, **not auto-loaded**. No browser/mail
/Element profile.
- `scripts/selinux/` ships custom `.te` modules — primary MAC.
- `overlay/etc/audit/plugins.d/veilor-remote.conf` +
`audisp-remote.conf.disabled` — **scaffold present, opt-in switch
missing**.
- `kickstart/veilor-os.ks` — single live-ks. Real LUKS install lives
in `overlay/usr/local/bin/veilor-installer` (generates ks at runtime).
- No nftables overlay. No homed scaffold. No `veilor-audit-shipping` CLI.
## Item-by-item plan
### 1. AppArmor stack with SELinux — M
Fedora 43 ships `apparmor-parser`/`libapparmor`. Kernel has both LSMs.
Stacking works since 5.1; SELinux stays primary, AppArmor confines
specific binaries by path. **No conflict** — they layer. Risk: AA
profiles based on Debian/Ubuntu paths fail on Fedora.
**Files:**
- `kickstart/veilor-os.ks` `%packages` add `apparmor-parser apparmor-utils apparmor-profiles`
- `overlay/etc/apparmor.d/veilor.d/` (new) — vendor profiles
`firefox`, `thunderbird`, `element-desktop`, `signal-desktop`
- `scripts/40-apparmor.sh` (new) — parses + sets all veilor profiles
to **complain** on first install (logs only, no break)
- `overlay/usr/local/bin/veilor-doctor` — adds AA status check
**Test:** `aa-status | grep complain` shows >=4 loaded; firefox writes
outside policy → audit.log denial.
### 2. systemd-homed opt-in — L
Default LUKS storage `homectl` drops key on suspend; resume needs PAM
unlock again — **breaks "lid open, keep working"**. Use
`--storage=fscrypt` on top of existing btrfs `/home` subvol —
suspend transparent, encrypts at rest with per-user key.
**Files:**
- `overlay/usr/local/bin/veilor-homed-enable` (new) — confirms warning,
runs `homectl create admin --storage=fscrypt --real-name="veilor admin"`
after migrating files
- `overlay/etc/pam.d/sddm` drop-in for `pam_systemd_home.so`
- doc in `docs/HARDENING.md`. **Not auto-run** — only via post-install.
### 3. nftables alongside firewalld — S
firewalld speaks nftables backend on F43 — they don't conflict;
firewalld owns `inet firewalld` table. veilor-os preset = separate
`inet veilor` table loaded by its own service.
**Files:**
- `overlay/etc/nftables/veilor.nft` (new) — table `inet veilor`:
ssh per-IP rate limit (5/min), ICMP rate limit, optional
`ip6 daddr ::/0 drop` toggled by sysctl-style `/etc/veilor/ipv6.disabled`,
anti-port-scan via `meter` set
- `overlay/etc/systemd/system/veilor-nftables.service` (new) —
`After=firewalld.service`
- `kickstart/veilor-os.ks` `%packages` add `nftables`, services-enabled
add `veilor-nftables`
**Test:** `nft list ruleset` shows both `firewalld` AND `veilor`;
`hping3 -S -p 22 --flood` from second VM gets rate-limited.
### 4. Audit log shipping — S
Plumbing **already in tree** (`audisp-remote.conf.disabled`,
`veilor-remote.conf` with `active=no`). What's missing: CLI to flip
the switch with cert pinning.
**Files:**
- `overlay/usr/local/bin/veilor-audit-shipping` (new)
- `enable HOST PORT FINGERPRINT` writes
`/etc/veilor/audit-pin.sha256`, copies `audisp-remote.conf.disabled`
`audisp-remote.conf` with substituted host/port, enables plugin
(`active=yes`), restarts auditd
- `disable` reverses
- audisp-remote speaks TLS directly; cert pinning via `verify_peer=yes`
+ `peer_cert_fingerprint`
- Use **self-signed pinned**, not LE — collectors are LAN/VPN
**Test:** stand up `rsyslog` listener on nullstone with self-signed
cert; run helper; trigger `sudo -i`; tail nullstone for AUTHPRIV
event; revoke cert → events stop with logged TLS error.
### 5. Installer kickstart split — needs re-scope, S
Roadmap item is **stale**. As of v0.5.30 we already do real LUKS+btrfs
in `veilor-installer` which generates ks at runtime. **Re-scope:**
extract that generated ks template into static
`kickstart/veilor-os-install.ks` (parameterised via `%include
/tmp/answers.ks`), so reviewable in repo and reusable headlessly.
**Files:**
- split `overlay/usr/local/bin/veilor-installer` heredoc into
`kickstart/veilor-os-install.ks`
- installer just writes answers + `cp` the ks
- CI lints both with `ksvalidator`
### 6. Audit baseline re-run — S
Mechanical: `cp security/audit-template.md
security/veilor-os-distro/2026-05-DD.md`, run on VM, target lower
findings count than v0.2's baseline.
## Order, dependencies, ship plan
Dependencies: (5) blocks (6) — audit a stable installer, not a
moving heredoc. Else parallel.
**Total effort:** 2S + 1S(rescope) + 1S + 1M + 1L ≈ **57 dev-days**.
- **v0.5.32 (small wins):** (4) audit shipping CLI + (3) nftables
preset. Both S, scaffold completion, pure overlay (no kickstart risk).
- **v0.5.33:** (5) ks split + (6) audit baseline re-run.
- **v0.6 (medium):** (1) AppArmor stack — package install + 4 profiles
+ doctor integration; complain-mode keeps blast radius zero.
- **v0.7 (big lift):** (2) systemd-homed — UX-disruptive, needs
migration helper + doc page + suspend/lock/swap testing.

View file

@ -1,65 +0,0 @@
# Threat model + public launch prep
**Agent 5 of 9-agent wave, 2026-05-05.**
## Deliverable
Threat model written to `docs/THREAT-MODEL.md` (1492 words). Slots
into `docs/ROADMAP.md` v0.7 line item "Threat model published —
honest scope".
## Structure
1. **In-scope adversaries** (9 rows): lost laptop, browser RCE, USB
attacks, SSH brute-force, forensics, supply chain, LPE, network
surface, time MITM. Each maps to specific veilor mitigation
(LUKS2 argon2id mem=1GB, SELinux + `veilor-systemd` policy,
USBGuard, fail2ban+firewalld, auditd, NTS chrony, etc.).
2. **Out-of-scope adversaries** (9 rows): firmware implants,
evil-maid on running system, hardware keylogger, session-level
RCE (KDE not sandboxed), AES side-channels, TPM2 physical
attacks, traffic correlation, TOFU MITM, sustained physical
access. Each row points to right tool instead (Heads, Qubes,
Tails).
3. **Hardening tradeoffs** (6 honest costs):
- SELinux app-compat
- Slow LUKS boot
- USBGuard friction
- Module lockdown breaking NVIDIA prop / VBox
- Drop-zone breaking KDE Connect / mDNS
- No PackageKit
4. **Like Tails/Whonix/Qubes:** published threat model, default-deny
firewall, encrypted at rest.
5. **Differs from them:** daily-driver vs session-only; single-VM vs
Qubes compartmentalisation; persistent identity vs Tails amnesia.
6. **Comparison matrix:** 10-axis × 6-distro grid (veilor-os / stock
Fedora KDE / Kicksecure / Tails / Qubes / secureblue) covering
encryption, MAC, firewall, USB, per-app isolation, anonymity,
daily-driver fit, signed releases, threat-model publication,
hardware compat.
7. **v0.7 launch checklist** (9 items):
- Threat model finalised
- GPG signing (v0.4 dep)
- mkdocs-material on veilor.org
- Comparison + benchmarks
- Press kit
- "What veilor-os is not" preempt page (covers "why not Qubes/Tails/Fedora?")
- r/linux + r/Fedora + HN posts
- GitHub Release with ISO+sha256+.asc
- Repo flip-public + DNS + Mastodon/Matrix/SimpleX announce
## Tone
Matches repo voice — short paragraphs, no fluff, "honest scope"
framing reused from roadmap. No emojis (per CLAUDE.md style).
## See also
- `docs/THREAT-MODEL.md` (full document)
- `docs/ROADMAP.md` v0.7 section

View file

@ -1,96 +0,0 @@
# Anaconda log capture — virtio-9p host-share
**Agent 6 of 9-agent wave, 2026-05-05.**
## Why current setup is silent
v0.5.30 wired:
```
-chardev file,id=anaclog,path=$ANACONDA_LOG
-device virtio-serial-pci,id=vs1
-device virtserialport,chardev=anaclog,bus=vs1.0,name=org.fedoraproject.anaconda.log.0
```
Anaconda is supposed to autodetect this port and stream logs. Result:
`test/anaconda-vm-*.log` files are 0 bytes despite multiple full
installs.
**Root cause:** Anaconda's `setupVirtio()` (anaconda_logging.py:315)
doesn't write to the virtio port directly — it adds a forward rule to
`/etc/rsyslog.conf` then calls `restart_service("rsyslog")`. No
`inst.virtiolog` boot arg is required (`--virtiolog` defaults to the
right port via `argument_parsing.py:512`).
The veilor live ISO almost certainly **lacks `rsyslog`** (minimal
Fedora ks), so the forward rule lands in a file no daemon reads.
`restart_service` is a no-op. The QEMU side opens the port and
creates the 0-byte file but nothing ever writes to it.
Even with rsyslog present, only `LOG_LOCAL1`-tagged messages would
flow; the rich content lives in `/tmp/anaconda.log`,
`/tmp/program.log`, `/tmp/storage.log`, `/tmp/packaging.log` which
never traverse syslog.
## Fix — Option C (virtio-9p host-share + post-install copy)
### `test/run-vm.sh`
Add `-virtfs` 9p export of `test/test-runs/<timestamp>/` tagged
`hostlogs`. Keep existing virtio-serial as belt-and-braces fallback.
```bash
TS=$(date +%Y%m%d-%H%M%S)
HOSTLOGS_DIR="$TEST_DIR/test-runs/$TS"
mkdir -p "$HOSTLOGS_DIR"
HOSTSHARE_ARGS=(
-virtfs "local,path=$HOSTLOGS_DIR,mount_tag=hostlogs,security_model=mapped-xattr,id=hostshare"
)
echo " Logs : $HOSTLOGS_DIR"
```
Append `"${HOSTSHARE_ARGS[@]}" \` to the `exec qemu-system-x86_64`
block.
### `overlay/usr/local/bin/veilor-installer`
In `run_install()`, install an `EXIT` trap calling `_dump_logs_to_host`
that mounts the 9p share at `/mnt/hostlogs` and copies:
- `/tmp/{anaconda,program,storage,packaging,dnf,dnf.librepo,anaconda-cmdline}.log`
- `/var/log/veilor-installer.log`
- generated kickstart at `/run/install/veilor-generated.ks`
- `dmesg` output
- `journalctl -b` output
Runs on success, failure, and `^C`. Auto-no-ops on real hardware
where 9p isn't loaded.
```bash
_dump_logs_to_host() {
if mount -t 9p -o trans=virtio,version=9p2000.L hostlogs /mnt/hostlogs 2>/dev/null; then
cp -a /tmp/{anaconda,program,storage,packaging,dnf,dnf.librepo,anaconda-cmdline}.log \
/var/log/veilor-installer.log \
/run/install/veilor-generated.ks \
/mnt/hostlogs/ 2>/dev/null || true
dmesg > /mnt/hostlogs/dmesg.log 2>/dev/null || true
journalctl -b > /mnt/hostlogs/journal.log 2>/dev/null || true
umount /mnt/hostlogs 2>/dev/null || true
fi
}
trap _dump_logs_to_host EXIT
```
## Why options A/B/D were rejected
- **A** (grub kernel arg surgery — `inst.virtiolog`) and **D** (host
rsyslog TCP listener with `inst.syslog=10.0.2.2:5140`) both still
rely on rsyslog being present in the live ISO.
- **B** (anaconda --syslog at CLI) — same dependency.
- **C** captures complete file-level fidelity regardless. virtio-9p is
in the kernel; mount is two lines; copies the actual files.
## Files modified
- `test/run-vm.sh`
- `overlay/usr/local/bin/veilor-installer`

View file

@ -1,100 +0,0 @@
# KDE theme + DuckSans + /etc/skel branding audit
**Agent 7 of 9-agent wave, 2026-05-05.**
## Catalog: what's currently shipped
| Component | Status | Path |
|---|---|---|
| Color scheme | shipped | `assets/kde/veilor-black.colors``/usr/share/color-schemes/` |
| System kdeglobals | shipped | `assets/kde/veilor-default.kdeglobals``/etc/xdg/kdedefaults/kdeglobals` |
| Breeze decoration override | shipped | `assets/kde/breezerc``/etc/xdg/breezerc` |
| Plasma containment defaults | shipped | written by `30-apply-v03-theme.sh``/etc/xdg/kdedefaults/plasma-org.kde.plasma.desktop-appletsrc` |
| Wallpaper (PNG+SVG) | shipped | `assets/wallpapers/veilor-black.{png,svg}``/usr/share/wallpapers/veilor-black/` |
| SDDM theme | shipped (full QML) | `assets/sddm/veilor-black/``/usr/share/sddm/themes/veilor-black/` |
| SDDM theme activation | shipped | `30-apply-v03-theme.sh` writes `/etc/sddm.conf.d/veilor-theme.conf` (Current=veilor-black) |
| Konsole profile + colorscheme | shipped | `assets/konsole/veilor.{profile,colorscheme}``/usr/share/konsole/Veilor.*` + `/etc/xdg/konsolerc` |
| Plymouth theme | shipped | `assets/plymouth/veilor/` |
| os-release branding | shipped | PRETTY_NAME="veilor-os 0.5.27", LOGO=veilor-logo |
| Fira Code fontconfig | shipped | `/etc/fonts/conf.d/55-veilor-firacode.conf` |
| DuckSans font | DEFERRED — empty dir, README only | |
## Drift inside active configs
- `overlay/etc/sddm.conf.d/veilor.conf` sets `[Theme] Current=breeze`.
- `30-apply-v03-theme.sh` then writes
`/etc/sddm.conf.d/veilor-theme.conf` with `Current=veilor-black`.
- SDDM merges alphabetically → `veilor-theme.conf` wins (loads after).
- Shipping a `Current=breeze` line in the overlay is misleading drift.
## Specific gaps preventing visual brand consistency
1. **No `/etc/skel/` whatsoever.** `overlay/etc/skel/` does not exist.
All KDE config lives in `/etc/xdg/kdedefaults/` and `/etc/xdg/*rc`.
Works for fresh boots, but the moment the user clicks anything in
System Settings, KDE writes `~/.config/kdeglobals` and silently
shadows the system defaults. **Zero per-user seeding** = one click
away from losing all branding.
2. **No PRETTY_NAME secondaries.** `/etc/system-release`, `/etc/issue`,
`/etc/issue.net`, `/etc/lsb-release` never written. `lsb_release
-a` reports Fedora. KDE About dialog uses os-release (OK) but TTY
login banner + many user-space tools read `/etc/system-release`.
3. **No `kwinrc` shipped.** Plasma 6 Wayland-specific defaults
(TitlebarDoubleClick, Compositor backend, FocusPolicy, animation
speed) not seeded. Vanilla Fedora KDE animations + click-to-focus
prevail.
4. **No panel layout** (`plasma-org.kde.plasma.desktop-appletsrc`
containment for panel). The file written by `30-apply-v03-theme.sh`
only seeds `[Containments][1]` (desktop containment) for wallpaper.
Actual Plasma panel containment (taskbar, system tray, clock,
kickoff icon) is unseeded → users get stock Fedora panel with
Fedora-blue kickoff button.
5. **DuckSans deferred but README claims it as the brand font.**
`kdeglobals`, Konsole, SDDM all hardcode `Fira Code`. If DuckSans
ever ships, ten files need synchronized edits.
6. **`overlay/etc/sddm.conf.d/veilor.conf` says `Current=breeze`** —
internal contradiction with script-written `veilor-theme.conf`.
Cosmetic but confusing.
7. **`kde-theme-apply.sh` has `warn()` undefined** (line 64) — calls
`warn` but only `ok`/`info` defined. If os-release source ever
goes missing, script crashes with `command not found`.
## Top 5 `/etc/skel/` additions (highest impact, lowest effort)
1. **`/etc/skel/.config/kdeglobals`** — copy of
`assets/kde/veilor-default.kdeglobals`. Single highest-impact file:
locks ColorScheme, AccentColor, Font, Icons.Theme,
LookAndFeelPackage into the user's first-write file so System
Settings interaction won't revert anything to Breeze defaults.
2. **`/etc/skel/.config/konsolerc`** — `[Desktop Entry]
DefaultProfile=Veilor.profile` plus `[KonsoleWindow]
ShowMenuBarByDefault=false`. Per-user override of system konsolerc;
ensures first konsole launch is branded even if user's home
pre-exists.
3. **`/etc/skel/.config/kwinrc`** — Plasma 6 Wayland defaults:
`[Compositing] AnimationSpeed=0`, `[Windows]
FocusPolicy=ClickToFocus`, `[Plugins] blurEnabled=false` (mirrors
the no-animations Breeze override).
4. **`/etc/skel/.config/plasma-org.kde.plasma.desktop-appletsrc`** —
full containment file with both desktop containment
(wallpaper=veilor-black) AND panel containment (kickoff icon =
`/usr/share/pixmaps/veilor-logo.svg`, panel height/position).
Without this, the taskbar is vanilla Fedora.
5. **`/etc/skel/.local/share/konsole/Veilor.profile`** — local copy so
user-local konsole sees the profile in its dropdown without needing
`/usr/share/konsole/` walk. Pair with #2.
**Bonus near-zero-effort:** write `/etc/system-release`, `/etc/issue`,
and `/etc/lsb-release` in `kde-theme-apply.sh` to close the
lsb_release/TTY-banner gap. And fix the undefined `warn()` in
`kde-theme-apply.sh:64`.

View file

@ -1,131 +0,0 @@
# Build-iso CI hardening
**Agent 8 of 9-agent wave, 2026-05-05.**
## State of play
- Workflows: `build-iso.yml`, `lint.yml`, `Release Checksums` (auto)
- Secrets/variables: **none configured** — only ambient `GITHUB_TOKEN`
- Repo: private, MIT, no Pages, no Dependabot, no branch protection
(Pro-gated until public flip)
- Container: `registry.fedoraproject.org/fedora:43` (tag, not digest)
- Actions: `actions/checkout@v4`, `addnab/docker-run-action@v3`,
`softprops/action-gh-release@v2`, `ludeeus/action-shellcheck@master`
— **all unpinned to SHA**
- gum download: pinned by SHA256 ✓
- Kickstart repos: `releases/43/Everything` + `updates/43/Everything`
**both rolling**, byte-different daily
## Top 5 immediate (S effort, ship in v0.5.32)
| # | Item | Why |
|---|------|-----|
| 1 | Pin all actions to commit SHA + add `.github/dependabot.yml` for `github-actions` | Supply-chain — `@master` on shellcheck is live-takeover vector; v3/v4 tags are mutable |
| 2 | Pin Fedora container to digest (`registry.fedoraproject.org/fedora:43@sha256:...`) | One-line change; eliminates "container drift" repro class |
| 3 | Add `permissions:` block at workflow level (`contents: read` default), override per-job | `contents: write` is workflow-wide; least-privilege the lint job |
| 4 | Generate SBOM via `anchore/sbom-action`, attach to release | Free, ~30 lines, journalist-readable |
| 5 | Add `actions/attest-build-provenance@v2` for SLSA L3 attestation on ISO + parts | Free, GH-native, `id-token: write` only |
## v0.4 release-eng roadmap (confirmed/added)
- **Confirmed:** Sigstore/cosign signing of ISOs (already in roadmap)
- **Add:** Fedora compose-ID pinning per release tag — switch
`--baseurl` to
`kojipkgs.fedoraproject.org/compose/branched/Fedora-43-...n.X/compose/Everything/x86_64/os/`
for stable releases (rolling for `ci-latest`)
- **Add:** Reproducible-Builds.org diffoscope job comparing 2
sequential builds of same SHA — gate on byte-equality
- **Add:** `harden-runner` (StepSecurity) audit-mode pass to enumerate
egress; promote to block-mode in v0.5
- **Add:** When repo flips public (v0.7), enable secret scanning + push
protection + private vuln reporting + branch protection (require ≥1
review, status checks: lint + ksvalidate + build, no force-push)
- **Add:** OIDC `id-token: write` only in tag-release job (not on
`main` push) — keysless cosign signing scoped to release events
## YAML diffs
### 1. Workflow-level permissions + per-job override
```yaml
permissions:
contents: read
jobs:
build:
permissions:
contents: write # gh-release
id-token: write # cosign keyless + attestation
attestations: write
```
### 2. SHA-pin actions
```yaml
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- uses: addnab/docker-run-action@4f65375b03d588f307b7a3b0a8bb50f8b58a85b9 # v3
- uses: softprops/action-gh-release@01570a1f39cb168c169c802c3bceb9e93fb10974 # v2.1.0
```
(SHAs to be re-checked at apply-time; dependabot keeps them current)
### 3. Pin Fedora digest
```yaml
image: registry.fedoraproject.org/fedora:43@sha256:<DIGEST>
```
Capture once via `skopeo inspect --raw
docker://registry.fedoraproject.org/fedora:43 | jq -r .config.digest`
and bump on each releasever bump.
### 4. SBOM + attestation + cosign
```yaml
- name: Install cosign
uses: sigstore/cosign-installer@d7d6e07a3ddf0f9a4f8b3b9e3f1d1a5ce8e9b5b3 # v3.7.0
- name: Sign ISO parts (keyless)
if: github.event_name == 'release'
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)
uses: anchore/sbom-action@e8d2a6937ecead383dfe75190d104edd1f9c5751 # v0.17.4
with:
path: build/out
format: spdx-json
output-file: build/out/veilor-os.spdx.json
- name: Build provenance attestation
uses: actions/attest-build-provenance@7668571508540a607bdfd90a87a560489fe372eb # v2.1.0
with:
subject-path: 'build/out/*.part-*'
```
### 5. New `.github/dependabot.yml`
```yaml
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule: { interval: "weekly" }
groups:
actions: { patterns: ["*"] }
```
### 6. Timeout
Keep at 90min. Largest observed runs ~70min; trimming would
false-fail Fedora-mirror-slow days. **No change.**
## Q&A
- **Secrets in use:** none. Only ambient `GITHUB_TOKEN`. Once public,
enable secret scanning + push protection (free for public repos).
- **Pages:** not deployed from this repo. Docs site out-of-scope here.
- **Dependency review:** only `gum` fetched out-of-band — already
SHA256-pinned. Add `actions/dependency-review-action` on PRs once public.

View file

@ -1,167 +0,0 @@
# Real-hardware failure mode audit (post-v0.5.31)
**Agent 9 of 9-agent wave, 2026-05-05.** Pessimistic enumeration.
## A. Boot path
### A1. Secure Boot + GRUB_DISTRIBUTOR rebrand
- shim chain itself untouched (uses `/EFI/fedora/`), but `grub2-mkconfig`
regenerates entries naming `veilor-os` while shim only trusts paths
under `/EFI/fedora/grubx64.efi`. Strict UEFI: menu boots, kernel
signatures verify via Fedora's MOK chain. Risk: `os-prober` writing
dual-boot Windows entries breaks MBR/MOK.
- **Symptom:** dual-boot with Windows shows
`Verification failed: (0x1A) Security Violation`.
- **Prob:** MED. **Fix:** S. **Target:** v0.5.32.
### A2. KMS handoff — `fbcon=nodefer` necessary but not sufficient
- On Intel Arc/iGPU late-gen + NVIDIA proprietary chains, 5-15s blank
between vt switch and SDDM start because `simpledrm` releases before
`i915`/`nvidia-drm` claim.
- **Symptom:** ~10s blank pre-SDDM; user thinks crashed.
- **Prob:** HIGH. **Fix:** S — add `i915.modeset=1
nvidia-drm.modeset=1 amdgpu.modeset=1`. SDDM `Type=simple` startup.
**Target:** v0.5.32.
### A3. USBGuard hash-based rules
- `scripts/20-harden-kernel.sh:127-131` ships **empty** rules.conf
with `ImplicitPolicyTarget=block`. First boot, admin runs
`usbguard generate-policy`. Per `feedback_usbguard_dock.md`, this
writes hash+parent-hash rules that break on dock replug.
- **Symptom:** keyboard/mouse dies on first dock unplug-replug.
- **Prob:** HIGH. **Fix:** M — patch invocation to
`--with-hash=false`, or ship `veilor-usbguard-enroll` wrapper.
**Target:** v0.5.32 (same bug we already learned).
### A4. Wifi/Bluetooth firmware
- `@hardware-support` pulls `linux-firmware` etc.
- Realtek RTL8852/MT7921 firmware ships in `linux-firmware-whence` only.
- **Prob:** LOW. **Fix:** S (add explicit `linux-firmware-whence`).
**Target:** v0.5.32.
### A5. Bluetooth disabled at boot
- `scripts/20-harden-kernel.sh:111` disables `bluetooth` service.
BT keyboards/mice don't pair until user enables service.
- **Prob:** MED (laptop users). **Fix:** S — leave bluetooth.service
enabled, mask `obex` only. **Target:** v0.6.
## B. First-boot KDE session
### B1. Plasma 6 Wayland fallback on hybrid graphics
- SDDM config doesn't pin session. NVIDIA Optimus + intel-iris
triggers Wayland → silent fallback to X11 on some HW.
- **Symptom:** screen tearing, no fractional scaling.
- **Prob:** MED. **Fix:** S — add `[Autologin] Session=plasma`
+ `[General] DefaultSession=plasma.desktop`. **Target:** v0.6.
### B2. SUSPEND/RESUME KILLS WIFI — THE BIG ONE 🚨
- `veilor-modules-lock.service` sets `kernel.modules_disabled=1` 30s
after graphical.target. `iwlwifi`, `iwlmvm`, `cfg80211` reload on
resume from S3/S0ix. With modules locked: **resume → permanent
wifi death until reboot**. Same for `nvidia` autoload, `xhci_pci`
re-init on dock attach.
- **Symptom:** close laptop lid → reopen → no wifi, no dock USB,
until reboot.
- **Prob:** VERY HIGH (every laptop user, day 1).
- **Fix:** M — gate lock on `ConditionACPower=true` + reset on
suspend, OR move from `modules_disabled` to `module.sig_enforce=1`
kernel cmdline (no runtime lock needed).
- **Target:** v0.5.32 — **BLOCKER**.
### B3. Lid-close handling
- `logind.conf` not modified. Defaults `HandleLidSwitch=suspend`.
Combined with B2, every lid close = wifi loss.
- **Prob:** HIGH. **Fix:** S. **Target:** v0.5.32 (paired with B2).
## C. Day-2 ops
### C1. `/etc/default/grub` + `/etc/kernel/cmdline` drift
- Kickstart writes `GRUB_CMDLINE_LINUX_DEFAULT=""`. Real installer
writes `/etc/kernel/cmdline` with LUKS rd.luks args. `kernel-install`
reads the latter; `grub2-mkconfig` re-reads `/etc/default/grub`.
- **Symptom:** `dnf upgrade kernel` regenerates grub.cfg from
default/grub, drops LUKS unlock args from new entry → unbootable.
- **Prob:** HIGH. **Fix:** M — sync both files in `veilor-update`,
or migrate fully to BLS without grub-mkconfig.
- **Target:** v0.5.32.
### C2. SELinux relabel on first boot
- `firstboot.sh` flips to `enforcing` and `touch /.autorelabel`. On
large /home (encrypted btrfs), relabel takes 2-5min — user sees
frozen screen with cursor.
- **Symptom:** stuck "first boot" appears hung.
- **Prob:** MED. **Fix:** S (add plymouth message). **Target:** v0.6.
### C3. F44 upgrade
- Hardcoded `python3.14` path (kickstart:334) for transaction_progress.py
patch. Survives no upgrade.
- **Prob:** certainty by Nov 2026. **Fix:** M. **Target:** v0.6.
### C4. chrony NTS unreachable from corp networks
- Cloudflare NTS over UDP 4460 blocked by many corp firewalls.
chronyd will fail-stop sync.
- **Symptom:** clock skew → TLS failures → broken everything.
- **Prob:** MED. **Fix:** S (add fallback `pool` line — already
present, verify ordering). **Target:** v0.5.32.
## D. Networking
### D1. firewalld drop zone vs Tailscale 🚨
- `tailscale up` requires UDP 41641 + tailscale0 trusted. Default
`drop` zone blocks tailscale0.
- **Prob:** HIGH (this user uses Tailscale daily).
- **Fix:** S — ship `/etc/firewalld/zones/trusted.xml` with
`tailscale0` interface.
- **Target:** v0.5.32.
### D2. systemd-resolved DoT vs corp split-DNS
- No /etc/resolved.conf.d entries shipped (overlay dir empty).
- Corp internal hostnames fail.
- **Prob:** LOW. **Fix:** M. **Target:** v0.7.
## E. Hardware diversity
### E1. NVMe vs SATA LUKS perf
- Argon2id KDF tuned to memory, not IO.
- **Prob:** cosmetic. Skip.
### E2. ARM aarch64
- Out of scope for v0.5/0.6.
### E3. TPM2 unlock
- Already on roadmap. **Target:** v0.7.
## Top 10 ranked (prob × severity)
| # | Issue | Prob | Sev | Target |
|---|-------|------|-----|--------|
| 1 | **B2 Suspend/resume wifi death** (modules_disabled) | VHIGH | CRITICAL | v0.5.32 |
| 2 | **C1 kernel-upgrade grub drift** (LUKS args lost) | HIGH | CRITICAL | v0.5.32 |
| 3 | **A3 USBGuard hash rules** (dock replug) | HIGH | HIGH | v0.5.32 |
| 4 | **D1 firewalld blocks tailscale0** | HIGH | HIGH | v0.5.32 |
| 5 | **A2 KMS blank-screen 10s** | HIGH | MED | v0.5.32 |
| 6 | **B3 Lid-close suspend** (compounds B2) | HIGH | MED | v0.5.32 |
| 7 | **A1 Secure Boot + os-prober dual-boot** | MED | HIGH | v0.6 |
| 8 | **C4 NTS blocked corp** | MED | MED | v0.5.32 |
| 9 | **B1 Plasma Wayland fallback** | MED | MED | v0.6 |
| 10 | **C3 F44 path-pinned patch** | CERTAIN | LOW (Nov) | v0.6 |
## Top 5 to preempt in v0.5.32
1. **B2 modules-lock vs resume** — gate on no-pending-suspend, OR swap
to `module.sig_enforce=1` kernel cmdline.
2. **C1 cmdline drift** — make `veilor-update` fail-loud if
`/etc/kernel/cmdline` and `/etc/default/grub` diverge; regen BLS
on every kernel install.
3. **A3 USBGuard id-based rules**`veilor-usb-enroll` wrapper that
calls `usbguard generate-policy --with-hash=false`. Same fix that
already burned us on onyx.
4. **D1 Tailscale zone** — ship `/etc/firewalld/zones/trusted.xml`
listing `tailscale0`, plus NetworkManager dispatcher to assign it.
5. **A2 KMS handoff** — append `i915.modeset=1 amdgpu.modeset=1
nvidia-drm.modeset=1` to bootloader cmdline.
**Critical insight:** B2 alone bricks the laptop for any user who
closes their lid. Without that fix, v0.5.32 is shippable on desktops
only. Same architectural class as the LUKS bug — security feature
breaks legitimate kernel state transitions.

View file

@ -1,42 +0,0 @@
# 9-agent research wave — 2026-05-05
Deep-dive research wave kicked off after v0.5.31 ship to surface every
plausible failure mode + future bug class before the v0.7 public flex.
Each agent took ~15 min, returned a focused report. Findings indexed
here, full reports in this directory.
The findings already inform `docs/ROADMAP.md` (Lessons learned section
+ v0.5.32 / v0.6 / v0.7 reorder) and `docs/THREAT-MODEL.md` (drafted
by Agent 5).
| # | Topic | File | Key finding |
|---|---|---|---|
| 1 | Plymouth + LUKS real-hardware edge cases | [01-plymouth-luks-real-hardware.md](01-plymouth-luks-real-hardware.md) | Initramfs keymap missing breaks non-US users at LUKS prompt |
| 2 | SDDM + first-boot UX failure modes | [02-sddm-firstboot-ux.md](02-sddm-firstboot-ux.md) | `veilor-firstboot.service` `WantedBy=multi-user.target` only — silently doesn't run on real installs (graphical target) |
| 3 | bootc-image-builder spike plan | [03-bootc-spike-plan.md](03-bootc-spike-plan.md) | Full Containerfile draft + 1-week timebox; v0.7 schedule |
| 4 | Hardening tier 2 (AppArmor + nftables + audit + homed) | [04-hardening-tier-2.md](04-hardening-tier-2.md) | nftables + audit log shipping = S effort each, ship in v0.5.32 |
| 5 | Threat model + public launch prep | [05-threat-model-launch.md](05-threat-model-launch.md) | Drafted at `docs/THREAT-MODEL.md`. Honest in/out scope tables |
| 6 | Anaconda log virtio-serial silent fix | [06-anaconda-log-capture.md](06-anaconda-log-capture.md) | virtio-serial requires rsyslog (not in our live ISO). Switch to virtio-9p host-share with EXIT trap copy |
| 7 | KDE theme + DuckSans + /etc/skel branding | [07-kde-skel-branding.md](07-kde-skel-branding.md) | `/etc/skel/` doesn't exist; branding evaporates the moment user opens System Settings |
| 8 | Build-iso CI hardening | [08-ci-hardening.md](08-ci-hardening.md) | Pin actions to SHA, dependabot, SBOM, SLSA L3 attestation — all S effort |
| 9 | Real-hardware failure mode audit | [09-realhw-failure-modes.md](09-realhw-failure-modes.md) | **CRITICAL: `kernel.modules_disabled=1` kills wifi on suspend/resume.** Top blocker for v0.5.32 |
## Top blockers for next ship (v0.5.32)
Cross-referenced by severity × probability:
1. **Suspend/resume wifi death** (Agent 9) — every laptop bricks on lid-close
2. **veilor-firstboot.service WantedBy=graphical.target** (Agent 2) — login broken on real installs
3. **kernel-upgrade grub drift** (Agent 9) — first `dnf upgrade kernel` = unbootable
4. **USBGuard hash-rules problem** (Agent 9, mirrors `feedback_usbguard_dock.md`)
5. **firewalld blocks tailscale0** (Agent 9) — user uses tailscale daily
6. **/etc/skel/ empty → no per-user branding** (Agent 7)
7. **virtio-9p log capture** (Agent 6) — replaces broken virtio-serial path
## Research wave protocol
This wave validated the `wave + verifier` pattern from v0.5.31 fix
(per ROADMAP lessons learned #4). Multi-agent debug only produces
signal when one agent's findings are checked against another's;
9 parallel agents on distinct topics gave independent angles that
converged on the v0.5.32 blocker list above.

View file

@ -51,14 +51,7 @@ user --name=admin --groups=wheel --gecos="veilor admin" --password="" --plaintex
# Note: init_on_alloc/init_on_free removed from default live cmdline —
# they zero every memory page at boot which 5x'd KVM live boot time.
# Re-enable per-install via veilor-firstboot.service for production.
# `fbcon=nodefer` keeps the linux framebuffer console alive across the
# KMS modeset that intel/amdgpu/nvidia drivers do during userspace init.
# Without it, on real hardware the screen blanks the moment the GPU
# driver loads and the installer's tty1 redraw lands on a frozen
# framebuffer — symptom: black screen with blinking cursor for ~30s
# while the menu IS in fact rendered, just not painted. virtio-vga in
# QEMU doesn't trigger this so it never reproed in VM.
bootloader --location=mbr --append="lockdown=integrity module.sig_enforce=1 slab_nomerge randomize_kstack_offset=on vsyscall=none plymouth.enable=0 fbcon=nodefer i915.modeset=1 amdgpu.modeset=1 nvidia-drm.modeset=1 rd.vconsole.keymap=us"
bootloader --location=mbr --append="lockdown=integrity slab_nomerge randomize_kstack_offset=on vsyscall=none"
# ── Live ISO partitioning (flat — for live rootfs build only) ──
# NOTE: This is the *live* image kickstart. Final installed system uses
@ -99,13 +92,6 @@ syslinux
isomd5sum
xorriso
# veilor-installer dependencies (TTY1 TUI installer wrapping anaconda)
newt
parted
cryptsetup
lvm2
btrfs-progs
# core hardening tools
fail2ban
@ -119,11 +105,6 @@ chrony
firewalld
plymouth
# AppArmor stack — DEFERRED. apparmor-parser / apparmor-utils /
# apparmor-profiles are not in Fedora 43 base or updates. v0.6 ships
# without AppArmor; tier-2 plan to land via COPR or as part of the v0.7
# secureblue OCI hybrid (which has its own LSM stack).
# admin essentials
git
vim-enhanced
@ -192,9 +173,6 @@ ls -la "$SRC/assets" "$SRC/scripts" 2>&1 || echo "[ERR] assets/scripts missing i
cp -a "$SRC/assets" "$DEST/usr/share/veilor-os/" || echo "[ERR] assets cp failed: $?"
cp -a "$SRC/scripts" "$DEST/usr/share/veilor-os/" || echo "[ERR] scripts cp failed: $?"
ls -la "$DEST/usr/share/veilor-os/" 2>&1 || echo "[ERR] dest dir missing post-cp"
# Force root ownership on everything we copied — `cp -a` preserves
# CI runner uid (1001), which makes sudo refuse to read /etc/sudoers.d.
chown -R 0:0 "$DEST/etc" "$DEST/usr/share/veilor-os" "$DEST/usr/local/bin" 2>&1 || echo "[WARN] chown failed"
set +x
# Persist nochroot log into installed system for diagnostics
@ -203,7 +181,7 @@ set +x
date
echo "SRC=$SRC DEST=$DEST"
ls -la "$DEST/usr/share/veilor-os/" 2>&1
ls -la "$DEST/usr/local/bin/" 2>&1
ls -la "$DEST/usr/local/sbin/" 2>&1
} > "$DEST/var/log/veilor-nochroot.log" 2>&1 || true
%end
@ -217,7 +195,7 @@ echo " veilor-os install — %post"
echo "════════════════════════════════════════════════════════"
REPO=/usr/share/veilor-os
chmod +x $REPO/scripts/*.sh $REPO/scripts/selinux/*.sh /usr/local/bin/veilor-power /usr/local/bin/veilor-update /usr/local/bin/veilor-doctor /usr/local/bin/veilor-firstboot /usr/local/bin/veilor-installer
chmod +x $REPO/scripts/*.sh $REPO/scripts/selinux/*.sh /usr/local/bin/veilor-power /usr/local/sbin/veilor-firstboot
# Live image plumbing (matches upstream Fedora live ks). Without these the
# squashfs/EFI build fails — livesys-scripts ships systemd units lorax expects.
@ -236,41 +214,17 @@ bash $REPO/scripts/selinux/build-policy.sh || echo "[WARN] SELinux build failed;
# Apply KDE theme + DuckSans + os-release branding
bash $REPO/scripts/kde-theme-apply.sh
bash $REPO/scripts/30-apply-v03-theme.sh || echo "[WARN] v03-theme apply failed"
# Force admin password set on first boot.
# livecd-creator does NOT honor `user` kickstart directive (it's a LIVE
# image, no installer step). Create admin manually in chroot %post.
# Note: SDDM rejects blank passwords by default (PAM nullok off), so we
# set throwaway pw `veilor` + chage -d 0 to force reset on first login.
if ! getent passwd admin >/dev/null; then
useradd -m -G wheel -s /bin/bash -c "veilor admin" admin
echo 'admin:veilor' | chpasswd
chage -d 0 admin
echo "[INFO] admin user created (default pw=veilor, expired)"
passwd -d admin # blank password
chage -d 0 admin # expire → forced reset on first login
echo "[INFO] admin user created (blank password, expired)"
fi
# Symlink display-manager.service → sddm.service. graphical.target Wants=
# display-manager but the alias doesn't get auto-created when sddm package
# is installed via livecd-creator (vs Anaconda installer which handles it).
# Without this, sddm stays inactive even though enabled.
ln -sf /usr/lib/systemd/system/sddm.service /etc/systemd/system/display-manager.service
# Live ISO default target: multi-user (TTY1 = veilor-installer TUI lands first).
# User picks "Try live — desktop" from menu → systemctl isolate graphical.target.
# Real installs land on graphical.target by default (set by anaconda).
systemctl set-default multi-user.target
# Branding: GRUB menu title + plymouth `details` text theme (no graphical
# splash). Pure text-scroll boot exposes the gum installer immediately on
# tty1 instead of plymouth swallowing it.
sed -i \
-e 's|^GRUB_DISTRIBUTOR=.*|GRUB_DISTRIBUTOR="veilor-os"|' \
-e 's|^GRUB_CMDLINE_LINUX_DEFAULT=.*|GRUB_CMDLINE_LINUX_DEFAULT=""|' \
/etc/default/grub 2>/dev/null || true
plymouth-set-default-theme details 2>/dev/null || true
[ -f /boot/grub2/grub.cfg ] && grub2-mkconfig -o /boot/grub2/grub.cfg 2>/dev/null || true
# zram swap (no disk swap; keys never leak to platter)
dnf install -y zram-generator || true
cat > /etc/systemd/zram-generator.conf << 'EOF'
@ -279,115 +233,10 @@ zram-size = min(ram, 8192)
compression-algorithm = zstd
EOF
# Patch anaconda's transaction_progress.py inside the live rootfs so that
# when the user clicks "Install", a non-fatal RPM 6.0 *scriptlet* warning
# does not get escalated to "An error occurred during the transaction"
# and abort.
#
# This patch is NARROW — it overrides ONLY the `script_error` callback,
# not the consumer (`process_transaction_progress`). v0.5.28 had a broad
# patch that turned EVERY 'error' token into a warning, including
# `cpio_error` (payload corruption) and `unpack_error` (extraction
# failures). Side effect: silent grub2-efi-x64 scriptlet failure →
# /boot/efi/EFI/fedora/ left incomplete → `gen_grub_cfgstub` failed at
# the bootloader install phase. Narrowing eliminates that class of
# silent failure.
#
# Why a patch is needed at all: Fedora 43 ships RPM 6.0, which changed
# scriptlet failure propagation (Fedora wiki Changes/RPM-6.0; dnf5 issue
# 2507). Scriptlets that previously emitted "Non-critical error"
# warnings now bubble up as transaction-level errors. man-db's
# `transfiletriggerin` (`systemd-run /usr/bin/systemctl start
# man-db-cache-update`) is the most common trigger — non-zero in the
# anaconda chroot, RPM-6.0-aware dnf5 reports as error, anaconda
# --cmdline aborts.
#
# After the patch:
# - script_error → log warning, do NOT enqueue 'error' (transaction
# continues; specific package's posttrans whose result we ignore is
# already in the install set, scriptlet has run as far as it can).
# - cpio_error / unpack_error / generic error → unchanged, still
# raise PayloadInstallationError as anaconda intends. Real
# transaction-fatal events still abort install (good).
# Patch anaconda's transaction_progress.py to suppress dnf5's
# transaction-error escalation under RPM 6.0 + cmdline mode.
#
# History of this patch:
#
# v0.5.28: BROAD patch — overrode `process_transaction_progress` so all
# four 'error' token producers (cpio_error, script_error, unpack_error,
# generic error) became log warnings. man-db scriptlet stopped killing
# the install. BUT silent grub2-efi-x64 scriptlet failure left
# /boot/efi/EFI/fedora/ incomplete → gen_grub_cfgstub failed.
#
# v0.5.29: NARROW patch — overrode only `script_error` callback. Caught
# the per-package scriptlet failures cleanly. BUT dnf5 still tracks
# its own internal error counter and emits a final aggregate
# `error("transaction process has ended with errors..")` at end of
# transaction, which still raised PayloadInstallationError. Install
# aborted before bootloader install ran.
#
# v0.5.30: BROAD patch + bootloader --location=none in install ks.
# This time we silence the aggregate error too, so install completes,
# but anaconda is told NOT to install bootloader itself. The
# generated install ks's chroot %post does it explicitly via
# `dnf reinstall grub2-efi-x64 shim-x64 + grub2-install +
# grub2-mkconfig + efibootmgr`. The chroot has PID 1 systemd state
# from the live ISO (not the target), so scriptlets get a real
# environment to run in, not anaconda's truncated chroot. This
# sidesteps gen_grub_cfgstub entirely.
TP=/usr/lib64/python3.14/site-packages/pyanaconda/modules/payloads/payload/dnf/transaction_progress.py
if [ -f "$TP" ]; then
cp -a "$TP" "${TP}.veilor-bak"
# Replace the entire `elif token == 'error':` branch with log+continue.
# Pattern matches the original two-line block (log.error + raise).
python3 - "$TP" <<'PYEOF'
import sys, re
path = sys.argv[1]
src = open(path).read()
# Match: elif token == 'error':\n log.error(msg)\n raise PayloadInstallationError(...)
# Or any current substitution that looks like raise/log.warning at that level.
new = re.sub(
r"elif token == 'error':\n log\.error\(msg\)\n (?:raise PayloadInstallationError\(\"An error occurred during the transaction: \" \+ msg\)|log\.warning\(\"veilor: ignoring non-fatal transaction error: %s\", msg\))",
"elif token == 'error':\n log.warning('veilor: suppressed dnf5 transaction error (RPM 6.0 cmdline regression): %s', msg)\n # Do not raise — anaconda --cmdline + dnf5 + RPM 6.0 emits this for any scriptlet\n # failure; we handle bootloader install manually in install ks %post chroot",
src,
count=1,
)
if new == src:
# Try fresh-anaconda layout (no veilor patch yet)
new = re.sub(
r"elif token == 'error':\n log\.error\(msg\)\n raise PayloadInstallationError\(\"An error occurred during the transaction: \" \+ msg\)",
"elif token == 'error':\n log.warning('veilor: suppressed dnf5 transaction error: %s', msg)",
src,
count=1,
)
if new == src:
print("[ERR] transaction_progress.py error-branch not found")
sys.exit(1)
open(path, "w").write(new)
print("[OK] transaction_progress.py: broad error-branch suppressed")
PYEOF
if grep -q "veilor: suppressed dnf5 transaction error" "$TP"; then
rm -f /usr/lib64/python3.14/site-packages/pyanaconda/modules/payloads/payload/dnf/__pycache__/transaction_progress.*.pyc 2>/dev/null || true
echo "[OK] anaconda transaction_progress.py patched (broad error suppression)"
else
echo "[WARN] transaction_progress.py patch did not apply"
fi
else
echo "[WARN] transaction_progress.py not found at expected path"
fi
# Enable services
# veilor-firstboot.service NOT enabled on live ISO — it prompts admin pw
# which makes no sense on a live boot. Real installs enable it in their
# generated kickstart's chroot %post (see overlay/usr/local/bin/veilor-installer).
systemctl enable veilor-firstboot.service
systemctl enable veilor-modules-lock.service
systemctl enable sshd fail2ban usbguard tuned auditd firewalld chronyd
# Mask veilor-firstboot on live so even if it landed in /etc/systemd/system
# (overlay drag), it can't activate.
systemctl mask veilor-firstboot.service 2>/dev/null || true
# Default tuned profile = balanced (AC/battery udev rule will override)
tuned-adm profile veilor-balanced 2>/dev/null || true

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

@ -1,58 +0,0 @@
# veilor-os audisp-remote configuration template (DISABLED by default)
#
# IMPORTANT: enabling remote audit shipping leaks security events off-device.
# Only enable if you have a trusted log collector — the remote endpoint
# will receive every privileged syscall, file-watch hit, auth event, and
# sudoers/SSH config change recorded by auditd.
#
# To activate:
# 1. Set veilor-remote.conf `active = yes` (in /etc/audit/plugins.d/).
# 2. Copy this file to /etc/audisp/audisp-remote.conf (drop `.disabled`).
# 3. Edit `remote_server` + TLS settings below.
# 4. systemctl restart auditd
#
# Loki / Wazuh / Splunk integration paths:
#
# Loki - point remote_server at a syslog-to-Loki shim (promtail or
# vector with `syslog` source, format = "rfc5424"). Use TCP+TLS.
# Wazuh - run wazuh-agent locally; it pulls /var/log/audit/audit.log
# directly. In that case leave remote_server empty and rely on
# wazuh-agent's filebeat-style tailer instead of audisp-remote.
# Splunk - use a Splunk HEC bridge (rsyslog-omhttp or vector http sink).
# audisp-remote speaks plain syslog/TLS; it does not speak HEC
# natively.
# ---- transport ----
remote_server = logs.example.org
port = 60
transport = tcp # plain | tcp | krb5
queue_file = /var/spool/audit/remote.log
mode = immediate # immediate | forwarding
queue_depth = 10240
format = managed # managed | ascii
# ---- TLS (transport = tcp + use_libwrap=no recommended) ----
enable_krb5 = no
krb5_principal =
krb5_client_name = auditd
krb5_key_file = /etc/audit/audit.key
# ---- failure handling ----
network_failure_action = stop # ignore | syslog | exec | suspend | single | halt | stop
disk_low_action = syslog
disk_full_action = syslog
disk_error_action = syslog
remote_ending_action = reconnect
generic_error_action = syslog
generic_warning_action = syslog
overflow_action = syslog
# ---- heartbeat ----
heartbeat_timeout = 60
network_retry_time = 1
max_tries_per_record = 3
max_time_per_record = 5
# ---- formatting ----
# `managed` wraps each event in a syslog-RFC5424 header with veilor-os
# hostname + audit facility (LOG_AUTHPRIV). Loki/Splunk prefer this.

View file

@ -1,23 +0,0 @@
# veilor-os audit remote shipping (DISABLED by default)
#
# IMPORTANT: enabling remote audit shipping leaks security events off-device.
# Only enable if you have a trusted log collector (Loki / Wazuh / Splunk).
# The remote endpoint will see every privileged syscall, file watch hit,
# auth event, and sudoers change. Treat the collector with the same trust
# level as the host root account.
#
# Enable:
# 1. Edit `active = yes` below.
# 2. Configure /etc/audisp/audisp-remote.conf (see audisp-remote.conf.disabled).
# 3. systemctl restart auditd.
# 4. Verify with: auditctl -s | grep enabled
#
# Plugin pipes audit events out of auditd via a UNIX socket; audisp-remote
# reads from that socket and forwards to the configured remote_server.
active = no
direction = out
path = builtin_af_unix
type = builtin
args = /var/run/audit_events
format = string

View file

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- veilor-os: trusted zone with tailscale0 pre-bound.
Default zone stays drop (per 10-harden-base.sh). Tailscale's
interface is added here so `tailscale up` traffic isn't dropped.
Without this entry the firewalld drop zone blocks the tailnet
traffic and the user sees: "tailscale up succeeded, but I can't
reach hs.s8n.ru". (Agent 9, 2026-05-05 wave.) -->
<zone target="ACCEPT">
<short>Trusted</short>
<description>All network connections are accepted. veilor-os pre-binds tailscale0 here so the mesh layer-1 (Tailscale via Headscale) works out-of-box without manual firewalld zone juggling.</description>
<interface name="tailscale0"/>
</zone>

View file

@ -1,9 +1,9 @@
NAME="veilor-os"
PRETTY_NAME="veilor-os 0.5.27"
PRETTY_NAME="veilor-os 0.1 (Fedora 43 base)"
ID=veilor
ID_LIKE=fedora
VERSION="0.5.27"
VERSION_ID="0.5.27"
VERSION="0.1"
VERSION_ID="0.1"
HOME_URL="https://github.com/veilor-org/veilor-os"
DOCUMENTATION_URL="https://github.com/veilor-org/veilor-os/tree/main/docs"
BUG_REPORT_URL="https://github.com/veilor-org/veilor-os/issues"

View file

@ -1,60 +0,0 @@
# veilor-os — Breeze window decoration override
# Tighter borders, solid black title bar, minimal buttons, smallest border.
# Merged into /etc/xdg/breezerc (system default) by 30-apply-v03-theme.sh.
[Common]
# Tighter outline; subtle separator only when active.
OutlineCloseButton=false
ShadowSize=ShadowSmall
ShadowStrength=128
ShadowColor=0,0,0
[Windeco]
# Border thickness: smallest available (= "None" leaves only resize edge,
# "NoSides" keeps top/bottom only). We pick "None" for the tightest look,
# matching the black-on-black aesthetic.
BorderSize=None
ButtonSize=ButtonSmall
CloseButton=true
DrawBackgroundGradient=false
DrawBorderOnMaximizedWindows=false
DrawSizeGrip=false
DrawTitleBarSeparator=false
ExceptionType=0
HideTitleBar=false
OpaqueTitleBar=true
TitleAlignment=AlignCenter
UseBackgroundGradient=false
UseTitleBarColor=true
# Buttons: minimal — close / max / min only, no shade/help/keep-above.
ButtonsOnLeft=M
ButtonsOnRight=IAX
[Style]
# Disable per-app blur, transparency, and gradient effects.
MenuOpacity=100
WindowDragMode=1
ScrollBarAddLineButtons=0
ScrollBarSubLineButtons=0
SidePanelDrawFrame=false
SliderDrawTickMarks=false
TabBarDrawCenteredTabs=true
ToolBarDrawItemSeparator=false
DockWidgetDrawFrame=false
ProgressBarAnimated=false
AnimationsEnabled=false
StackedWidgetDrawFrame=false
# ── Active / inactive title bar colors (override Breeze defaults) ──
# kdeglobals [WM] section is the canonical source; these mirror it here
# so apps that only read breezerc see consistent values.
[Windeco][Active]
TitleBarColor=0,0,0
TitleBarTextColor=216,216,216
TitleBarBorderColor=104,107,111
[Windeco][Inactive]
TitleBarColor=15,17,18
TitleBarTextColor=161,169,177
TitleBarBorderColor=42,46,50

View file

@ -1,29 +0,0 @@
[General]
ColorScheme=veilor-black
Name=veilor black
AccentColor=104,107,111
LastUsedCustomAccentColor=104,107,111
font=Fira Code,11,-1,5,400,0,0,0,0,0,0,0,0,0,0,1
fixed=Fira Code,10,-1,5,400,0,0,0,0,0,0,0,0,0,0,1
menuFont=Fira Code,11,-1,5,400,0,0,0,0,0,0,0,0,0,0,1
smallestReadableFont=Fira Code,9,-1,5,400,0,0,0,0,0,0,0,0,0,0,1
toolBarFont=Fira Code,10,-1,5,400,0,0,0,0,0,0,0,0,0,0,1
[Icons]
Theme=breeze-dark
[KDE]
LookAndFeelPackage=org.kde.breezedark.desktop
SingleClick=false
contrast=4
widgetStyle=Breeze
[Mouse]
cursorTheme=Breeze_Light
cursorSize=24
[KDecoration]
theme=Breeze
ButtonsOnLeft=
ButtonsOnRight=IAX
BorderSize=None

View file

@ -1,10 +0,0 @@
# veilor-os Konsole default — branded profile pre-selected so first
# Konsole launch on a fresh user account opens the veilor profile,
# not Fedora's default white-bg Breeze.
[Desktop Entry]
DefaultProfile=Veilor.profile
[KonsoleWindow]
ShowMenuBarByDefault=false
RememberWindowSize=true

View file

@ -1,29 +0,0 @@
# veilor-os Plasma 6 kwin defaults — seeded into /etc/skel so first-login
# users inherit deliberate windowing behaviour rather than default Breeze
# animations. Per-user; user can override post-login.
[Compositing]
# OpenGL backend = standard for hardware accel; AnimationSpeed=0 = no
# slow window animations on every focus change.
Backend=OpenGL
AnimationSpeed=0
HiddenPreviews=5
LatencyPolicy=Low
WindowsBlockCompositing=true
[Plugins]
# Disable visual fluff that isn't security-relevant + costs perf.
blurEnabled=false
contrastEnabled=false
slideEnabled=false
slidingpopupsEnabled=false
fadeEnabled=false
zoomEnabled=true
[Windows]
FocusPolicy=ClickToFocus
RollOverDesktops=false
TitlebarDoubleClickCommand=Maximize
[Wayland]
InputMethod=

View file

@ -1,104 +0,0 @@
[General]
Anchor=0.5,0.5
Blur=false
ColorRandomization=false
Description=Veilor
FillStyle=Tile
Opacity=1
Wallpaper=
WallpaperFlipType=NoFlip
WallpaperOpacity=1
[Background]
Color=0,0,0
[BackgroundFaint]
Color=0,0,0
[BackgroundIntense]
Color=15,17,18
[Foreground]
Color=216,216,216
[ForegroundFaint]
Color=161,169,177
[ForegroundIntense]
Color=236,236,236
# ── Standard ANSI palette (muted, desaturated) ──
# Veilor aesthetic: no neon. Reds tone-shifted toward bordeaux, greens
# toward sage, blues toward slate. Greys lifted to remain readable.
[Color0]
Color=27,27,27
[Color0Faint]
Color=20,20,20
[Color0Intense]
Color=58,58,58
[Color1]
Color=176,55,69
[Color1Faint]
Color=130,40,52
[Color1Intense]
Color=205,87,99
[Color2]
Color=102,138,90
[Color2Faint]
Color=78,107,68
[Color2Intense]
Color=141,176,128
[Color3]
Color=185,158,98
[Color3Faint]
Color=140,118,72
[Color3Intense]
Color=216,193,134
[Color4]
Color=92,116,143
[Color4Faint]
Color=68,87,107
[Color4Intense]
Color=131,154,182
[Color5]
Color=141,113,150
[Color5Faint]
Color=104,84,112
[Color5Intense]
Color=176,148,186
[Color6]
Color=99,144,148
[Color6Faint]
Color=72,107,110
[Color6Intense]
Color=139,180,184
[Color7]
Color=200,200,200
[Color7Faint]
Color=161,169,177
[Color7Intense]
Color=236,236,236

View file

@ -1,55 +0,0 @@
[General]
Name=Veilor
Parent=FALLBACK/
Command=/bin/bash
Directory=
Icon=utilities-terminal
LocalTabTitleFormat=%w
RemoteTabTitleFormat=(%u) %h
ShowTerminalSizeHint=false
StartInCurrentSessionDir=true
TerminalCenter=false
TerminalMargin=4
[Appearance]
ColorScheme=Veilor
Font=Fira Code,11,-1,5,400,0,0,0,0,0,0,0,0,0,0,1
LineSpacing=1
UseFontLineCharacters=true
[Cursor Options]
CursorShape=0
UseCustomCursorColor=true
CustomCursorColor=104,107,111
CustomCursorTextColor=216,216,216
[Scrolling]
HistoryMode=2
HistorySize=10000
ScrollBarPosition=2
HighlightScrolledLines=false
[Terminal Features]
BellMode=3
BlinkingCursorEnabled=false
BlinkingTextEnabled=false
FlowControlEnabled=false
UrlHintsModifiers=67108864
ReverseUrlHints=false
VerticalLine=false
[Interaction Options]
AutoCopySelectedText=false
CopyTextAsHTML=false
TrimLeadingSpacesInSelectedText=false
TrimTrailingSpacesInSelectedText=true
UnderlineFilesEnabled=true
UnderlineLinksEnabled=true
OpenLinksByDirectClickEnabled=false
WordCharacters=:@-./_~?&=%+#
[Encoding Options]
DefaultEncoding=UTF-8
[Keyboard]
KeyBindings=default

View file

@ -14,8 +14,3 @@ LoginGraceTime 30
MaxAuthTries 3
MaxSessions 4
LogLevel VERBOSE
# UseDNS off: reverse-lookup-on-connect adds 30s+ delay per connection
# when client DNS doesn't resolve back-references (NAT, slirp, dynamic
# IPs). Hardening doesn't benefit from the lookup either way; sshd
# still logs the IP, just not the (possibly forged) PTR.
UseDNS no

View file

@ -1,16 +0,0 @@
# veilor-os: replace tty1 login prompt with installer TUI.
# Live ISO ONLY. Removed by anaconda during real install (this overlay
# isn't copied into target system — see kickstart/install.ks).
[Service]
ExecStart=
ExecStart=-/usr/local/bin/veilor-installer
StandardInput=tty
StandardOutput=tty
StandardError=tty
TTYPath=/dev/tty1
TTYReset=yes
TTYVHangup=yes
TTYVTDisallocate=yes
Type=idle
Restart=always
RestartSec=2

View file

@ -9,7 +9,7 @@ Conflicts=sddm.service
[Service]
Type=oneshot
RemainAfterExit=no
ExecStart=/usr/local/bin/veilor-firstboot
ExecStart=/usr/local/sbin/veilor-firstboot
StandardInput=tty
StandardOutput=tty
StandardError=tty
@ -18,9 +18,4 @@ TTYReset=yes
TTYVHangup=yes
[Install]
# Real installs default to graphical.target. Without this entry the unit
# never runs on installed systems — admin pw stays at install-time value
# + chage -d 0 expired, SDDM PAM bounces user to a chauthtok screen
# (recoverable but ugly). Live ISO + multi-user.target installs both
# resolve via this list. (Agent 2, 2026-05-05 wave.)
WantedBy=graphical.target multi-user.target
WantedBy=multi-user.target

View file

@ -29,4 +29,4 @@ stop() {
return 0
}
process "$@"
process $@

View file

@ -28,4 +28,4 @@ stop() {
return 0
}
process "$@"
process $@

View file

@ -40,4 +40,4 @@ stop() {
return 0
}
process "$@"
process $@

View file

@ -1,43 +0,0 @@
# veilor-os USBGuard baseline rules
#
# Default policy is `block` (set in usbguard-daemon.conf via the
# overlay). Without any allow rule, every USB device — including the
# user's keyboard — is blocked at boot. That includes the desktop
# user with a USB keyboard at SDDM.
#
# This file allows HID-class interfaces (keyboard, mouse, touchpad,
# fingerprint reader, NFC, gamepad) without pinning to specific
# vendor:product/serial/hash. id-based rules survive dock replug and
# vendor-bump kernel changes, where hash+parent-hash rules don't —
# verified pain on onyx (memory: feedback_usbguard_dock.md). Same fix.
#
# After first login, the user runs:
# ujust veilor-usbguard-enroll
# (or `usbguard generate-policy --with-hash=false > rules.conf`)
# to add their own keyboard's id-rule and tighten the policy further.
#
# References:
# - usbguard-rules.conf(5)
# - https://usbguard.github.io/documentation/rule-language.html
# - veilor-os agent 9 audit, 2026-05-05
# HID class — keyboards, mice, pointers, gamepads, fingerprint, NFC.
# Interface descriptor 03:NN:NN where 03=HID. We accept any HID
# subclass + protocol so the rule is robust to future HID variants.
allow with-interface match-all { 03:*:* }
# Mass-storage prompt: ask the user before mounting a new flash drive.
# Reject blanket-allow (would silently allow USB Rubber Ducky).
# Accept only after user confirms via the gnome/plasma USB dialog.
# (USBGuard has no native "ask" verb; we leave mass-storage devices
# implicit-block here, the user runs `usbguard allow-device <id>`
# from a Plasma applet OR the firstboot wizard documents this flow.)
# Block known-bad. USB Killer signature shows up as a generic-HID
# composite descriptor + power draw out of spec. We can't reliably
# detect that from descriptors alone — relying on default-block
# semantics for now.
# DO NOT pin to specific id=, serial=, hash=, or parent-hash= here.
# That's the user's job post-firstboot for their actual hardware.
# Pre-shipped pinned rules break on every dock replug + kernel bump.

View file

@ -1,240 +0,0 @@
#!/usr/bin/bash
# veilor-doctor — read-only diagnostic / health check.
# User-facing CLI shipped in /usr/local/bin/. v0.6 ergonomic tooling.
#
# Reports on system, hardening, disk, network, updates, veilor units.
# No fixes are ever applied — output only. Use this to verify drift
# from the v0.2+ baseline.
#
# Flags:
# --quiet print only PASS/FAIL summary
# --json emit JSON for monitoring
# -h|--help
set -uo pipefail
QUIET=0
JSON=0
for arg in "$@"; do
case "$arg" in
--quiet|-q) QUIET=1 ;;
--json) JSON=1 ;;
-h|--help)
sed -n '2,15p' "$0" | sed 's/^# \{0,1\}//'
exit 0
;;
*)
echo "unknown flag: $arg" >&2
exit 2
;;
esac
done
have() { command -v "$1" >/dev/null 2>&1; }
# ── Output helpers ──────────────────────────────────────────────────
PASS=0
FAIL=0
ROWS=() # human table rows: "Section|Check|Status|Detail"
JSON_ROWS=() # JSON-serialisable rows
# Use color only if stdout is a TTY and we're not in --quiet/--json mode.
if [[ -t 1 && $QUIET -eq 0 && $JSON -eq 0 ]]; then
GREEN=$'\033[32m'; RED=$'\033[31m'; DIM=$'\033[2m'; OFF=$'\033[0m'
else
GREEN=""; RED=""; DIM=""; OFF=""
fi
# JSON-escape a string for embedding.
json_esc() {
local s=$1
s=${s//\\/\\\\}
s=${s//\"/\\\"}
s=${s//$'\n'/\\n}
s=${s//$'\t'/\\t}
printf '%s' "$s"
}
# check <section> <name> <pass|fail> <detail>
check() {
local section=$1 name=$2 status=$3 detail=$4
if [[ $status == pass ]]; then
PASS=$((PASS+1))
else
FAIL=$((FAIL+1))
fi
ROWS+=("${section}|${name}|${status}|${detail}")
JSON_ROWS+=("{\"section\":\"$(json_esc "$section")\",\"name\":\"$(json_esc "$name")\",\"status\":\"$status\",\"detail\":\"$(json_esc "$detail")\"}")
}
# ── 1. System ───────────────────────────────────────────────────────
HOSTNAME_VAL=$(hostnamectl --static 2>/dev/null || hostname)
OS_NAME=$(. /etc/os-release 2>/dev/null && echo "${PRETTY_NAME:-unknown}")
KERNEL=$(uname -r)
UPTIME=$(uptime -p 2>/dev/null || uptime)
check System hostname pass "$HOSTNAME_VAL"
check System os pass "$OS_NAME"
check System kernel pass "$KERNEL"
check System uptime pass "$UPTIME"
# ── 2. Hardening ────────────────────────────────────────────────────
SELINUX=$(getenforce 2>/dev/null || echo "unknown")
[[ $SELINUX == "Enforcing" ]] && check Hardening selinux pass "$SELINUX" \
|| check Hardening selinux fail "$SELINUX (expected Enforcing)"
if systemctl is-active --quiet usbguard; then
check Hardening usbguard pass active
else
check Hardening usbguard fail "$(systemctl is-active usbguard 2>/dev/null || echo missing)"
fi
if systemctl is-active --quiet fail2ban; then
check Hardening fail2ban pass active
else
check Hardening fail2ban fail "$(systemctl is-active fail2ban 2>/dev/null || echo missing)"
fi
FW_ZONE=$(firewall-cmd --get-default-zone 2>/dev/null || echo unknown)
[[ $FW_ZONE == "drop" ]] && check Hardening firewalld_zone pass "$FW_ZONE" \
|| check Hardening firewalld_zone fail "$FW_ZONE (expected drop)"
PTRACE=$(sysctl -n kernel.yama.ptrace_scope 2>/dev/null || echo "")
[[ ${PTRACE:-0} -ge 2 ]] && check Hardening ptrace_scope pass "$PTRACE" \
|| check Hardening ptrace_scope fail "${PTRACE:-unset} (expected >=2)"
KPTR=$(sysctl -n kernel.kptr_restrict 2>/dev/null || echo "")
[[ ${KPTR:-0} -ge 2 ]] && check Hardening kptr_restrict pass "$KPTR" \
|| check Hardening kptr_restrict fail "${KPTR:-unset} (expected >=2)"
# ── 3. Disk ─────────────────────────────────────────────────────────
LUKS_DEV=$(lsblk -lno NAME,TYPE 2>/dev/null | awk '$2=="crypt" {print $1; exit}')
if [[ -n $LUKS_DEV ]]; then
LUKS_STATUS=$(cryptsetup status "$LUKS_DEV" 2>/dev/null \
| awk -F: '/cipher/ {gsub(/^ +/,"",$2); print $2; exit}')
check Disk luks pass "${LUKS_DEV}: ${LUKS_STATUS:-active}"
else
check Disk luks fail "no LUKS device found (full-disk encryption expected)"
fi
if have btrfs && btrfs filesystem df / >/dev/null 2>&1; then
SUBVOLS=$(btrfs subvolume list / 2>/dev/null | wc -l)
check Disk btrfs pass "${SUBVOLS} subvolume(s)"
else
check Disk btrfs fail "btrfs not detected on /"
fi
ROOT_FREE=$(df -h / 2>/dev/null | awk 'NR==2 {print $4 " free / " $2 " (" $5 " used)"}')
check Disk root_free pass "${ROOT_FREE:-unknown}"
# ── 4. Network ──────────────────────────────────────────────────────
if systemctl is-active --quiet NetworkManager; then
check Network networkmanager pass active
else
check Network networkmanager fail inactive
fi
DEFAULT_ROUTE=$(ip -o route show default 2>/dev/null | awk '{print $3 " via " $5; exit}')
[[ -n $DEFAULT_ROUTE ]] && check Network default_route pass "$DEFAULT_ROUTE" \
|| check Network default_route fail "no default route"
DNS_LIST=$(awk '/^nameserver/ {print $2}' /etc/resolv.conf 2>/dev/null \
| paste -sd, - 2>/dev/null)
[[ -n $DNS_LIST ]] && check Network dns pass "$DNS_LIST" \
|| check Network dns fail "no nameservers"
PUBLIC_IP=$(curl -s --max-time 3 ifconfig.me 2>/dev/null || echo "")
[[ -n $PUBLIC_IP ]] && check Network public_ip pass "$PUBLIC_IP" \
|| check Network public_ip fail "lookup timed out"
# ── 5. Updates ──────────────────────────────────────────────────────
LAST_DNF=$(sudo -n dnf history list 2>/dev/null \
| awk 'NR==4 {for(i=4;i<NF;i++)printf "%s ", $i; print $NF; exit}')
[[ -n $LAST_DNF ]] && check Updates last_dnf pass "$LAST_DNF" \
|| check Updates last_dnf pass "(unknown — try \`sudo dnf history\`)"
# `dnf check-update` exits 100 if updates available, 0 if not.
sudo -n dnf check-update -q >/dev/null 2>&1
RC=$?
case $RC in
0) check Updates pending pass "system up-to-date" ;;
100)
AVAIL=$(sudo -n dnf check-update -q 2>/dev/null \
| awk 'NF>=3 && $1!~/^Last/ {n++} END {print n+0}')
check Updates pending fail "${AVAIL} update(s) available — run \`veilor-update\`"
;;
*) check Updates pending fail "dnf check-update returned $RC (need sudo?)" ;;
esac
# ── 6. veilor services ──────────────────────────────────────────────
for unit in veilor-firstboot.service veilor-modules-lock.service; do
if systemctl list-unit-files "$unit" 2>/dev/null | grep -q "$unit"; then
STATE=$(systemctl is-enabled "$unit" 2>/dev/null || echo unknown)
ACTIVE=$(systemctl is-active "$unit" 2>/dev/null || echo unknown)
# firstboot is meant to be one-shot + disabled after run.
check veilor "$unit" pass "${STATE} (${ACTIVE})"
else
check veilor "$unit" fail "unit not installed"
fi
done
# ── Output ──────────────────────────────────────────────────────────
if [[ $JSON -eq 1 ]]; then
printf '{"pass":%d,"fail":%d,"checks":[' "$PASS" "$FAIL"
for i in "${!JSON_ROWS[@]}"; do
[[ $i -gt 0 ]] && printf ','
printf '%s' "${JSON_ROWS[$i]}"
done
printf ']}\n'
[[ $FAIL -eq 0 ]] && exit 0 || exit 1
fi
if [[ $QUIET -eq 1 ]]; then
if [[ $FAIL -eq 0 ]]; then
echo "PASS ($PASS checks)"
exit 0
else
echo "FAIL ($FAIL of $((PASS+FAIL)) checks failed)"
exit 1
fi
fi
_print_plain_table() {
local last_section=""
for r in "${ROWS[@]}"; do
IFS='|' read -r sec name status detail <<<"$r"
if [[ $sec != "$last_section" ]]; then
printf '\n%s%s%s\n' "$DIM" "── $sec ──" "$OFF"
last_section=$sec
fi
if [[ $status == pass ]]; then
printf ' %s[OK]%s %-20s %s\n' "$GREEN" "$OFF" "$name" "$detail"
else
printf ' %s[FAIL]%s %-20s %s\n' "$RED" "$OFF" "$name" "$detail"
fi
done
}
# Pretty table — gum if available, else plain. gum table reads tab-separated
# input on stdin; we feed it the rows then fall back to the plain printer
# if gum exits non-zero (e.g. no TTY).
if have gum; then
{
printf 'Section\tCheck\tStatus\tDetail\n'
for r in "${ROWS[@]}"; do
IFS='|' read -r sec name status detail <<<"$r"
mark=$([[ $status == pass ]] && echo "OK" || echo "FAIL")
printf '%s\t%s\t%s\t%s\n' "$sec" "$name" "$mark" "$detail"
done
} | gum table --print --separator $'\t' 2>/dev/null || _print_plain_table
else
_print_plain_table
fi
echo
if [[ $FAIL -eq 0 ]]; then
printf '%s%d checks passed.%s\n' "$GREEN" "$PASS" "$OFF"
exit 0
else
printf '%s%d of %d checks failed.%s\n' "$RED" "$FAIL" "$((PASS+FAIL))" "$OFF"
exit 1
fi

File diff suppressed because it is too large Load diff

View file

@ -1,94 +0,0 @@
#!/usr/bin/bash
# veilor-update — system update wrapper.
# Wraps `dnf upgrade --refresh` + `flatpak update` behind a single command.
# User-facing CLI shipped in /usr/local/bin/. v0.6 ergonomic tooling.
#
# Exit codes:
# 0 success
# 1 dnf failed
# 2 flatpak failed (dnf still ran successfully)
# 3 no network
#
# Uses `gum` for spinner output if present, falls back to plain stdout.
set -uo pipefail
# ── Helpers ─────────────────────────────────────────────────────────
have() { command -v "$1" >/dev/null 2>&1; }
GUM=$(have gum && echo gum || echo "")
say() {
# Print a status line. Coloured if gum present, else plain.
if [[ -n $GUM ]]; then
gum style --foreground 212 --bold "$1"
else
printf '\n=== %s ===\n' "$1"
fi
}
run_with_spinner() {
local title=$1; shift
if [[ -n $GUM ]]; then
gum spin --spinner dot --title "$title" -- "$@"
else
echo "[+] $title"
"$@"
fi
}
# ── Pre-flight: network check ───────────────────────────────────────
say "veilor-update: checking network"
if ! ping -c 1 -W 2 mirrors.fedoraproject.org >/dev/null 2>&1; then
echo
echo " No route to mirrors.fedoraproject.org."
echo " Connect to a network and re-run \`veilor-update\`."
exit 3
fi
# ── Snapshot kernel before upgrade so we can warn about reboot need ─
KERNEL_BEFORE=$(uname -r)
# ── DNF upgrade ─────────────────────────────────────────────────────
say "veilor-update: refreshing DNF metadata + applying updates"
# Capture upgrade output so we can count packages afterwards. Tee to
# stdout for live progress; swallow into a tempfile for the count.
LOG=$(mktemp -t veilor-update.XXXXXX)
trap 'rm -f "$LOG"' EXIT
if ! sudo dnf upgrade --refresh -y 2>&1 | tee "$LOG"; then
echo
echo " dnf upgrade failed. See output above."
exit 1
fi
# ── Count packages updated ──────────────────────────────────────────
# DNF prints "Upgraded: N", "Installed: N", "Removed: N" at end.
# Sum the upgrade/install lines for the user-visible total.
UPDATED=$(grep -E '^(Upgraded|Installed)\b' "$LOG" 2>/dev/null \
| awk -F: '{ gsub(/[^0-9]/,"",$2); s+=$2 } END { print s+0 }')
# ── Flatpak (best-effort) ───────────────────────────────────────────
FLATPAK_RC=0
if have flatpak; then
say "veilor-update: updating flatpaks"
if ! flatpak update -y; then
FLATPAK_RC=2
echo " flatpak update failed; continuing anyway."
fi
else
echo " (flatpak not installed — skipping)"
fi
# ── Post-update: reboot hint if kernel changed ──────────────────────
KERNEL_AFTER_LATEST=$(rpm -q kernel --last 2>/dev/null \
| awk 'NR==1 { sub(/^kernel-/,"",$1); print $1 }')
say "veilor-update: complete"
printf ' Packages updated : %s\n' "${UPDATED:-0}"
printf ' Running kernel : %s\n' "$KERNEL_BEFORE"
if [[ -n ${KERNEL_AFTER_LATEST:-} && $KERNEL_AFTER_LATEST != "$KERNEL_BEFORE" ]]; then
printf ' Newest kernel : %s (reboot suggested)\n' "$KERNEL_AFTER_LATEST"
fi
exit $FLATPAK_RC

View file

@ -76,7 +76,7 @@ if [[ -d $SDDM_SRC ]]; then
install -d -m 0755 /etc/sddm.conf.d
# Preserve other sddm.conf.d/*.conf entries; this file owns [Theme] only.
cat > /etc/sddm.conf.d/veilor-theme.conf << 'EOF'
# veilor-os v0.3 — set veilor-black SDDM theme as system default
# veilor-os v0.3 — set veilor-black SDDM theme as default (matches reference system)
[Theme]
Current=veilor-black
CursorTheme=Breeze_Light
@ -128,44 +128,12 @@ else
fi
# ─────────────────────────────────────────────────────────────────────
# 4. Wallpaper — pure black (default: org.kde.color plugin; image fallback)
# 4. Wallpaper — solid black (matches reference system, no SVG asset)
# ─────────────────────────────────────────────────────────────────────
info "Wallpaper: installing veilor-black image and setting Plasma defaults"
# 4a. Install wallpaper images for users who prefer org.kde.image. The
# veilor-black asset (PNG primary, SVG fallback) is a 3840x2160 pure
# black canvas with a tiny low-opacity wordmark in the corner.
WP_SRC="$ASSETS/wallpapers"
WP_DST="/usr/share/wallpapers/veilor-black/contents/images"
WP_META="/usr/share/wallpapers/veilor-black/metadata.desktop"
if [[ -d $WP_SRC ]]; then
install -d -m 0755 "$WP_DST"
if [[ -f $WP_SRC/veilor-black.png ]]; then
install -m 0644 "$WP_SRC/veilor-black.png" "$WP_DST/veilor-black.png"
ok "wallpaper PNG installed at $WP_DST/veilor-black.png"
fi
if [[ -f $WP_SRC/veilor-black.svg ]]; then
install -m 0644 "$WP_SRC/veilor-black.svg" "$WP_DST/veilor-black.svg"
ok "wallpaper SVG installed at $WP_DST/veilor-black.svg"
fi
install -d -m 0755 "$(dirname "$WP_META")"
cat > "$WP_META" << 'EOF'
[Desktop Entry]
Name=veilor-black
X-KDE-PluginInfo-Name=veilor-black
X-KDE-PluginInfo-Author=veilor-os
X-KDE-PluginInfo-License=MIT
X-KDE-PluginInfo-Version=0.3
EOF
ok "wallpaper metadata installed at $WP_META"
else
warn "wallpaper source dir missing at $WP_SRC — skipping image install"
fi
# 4b. Default wallpaper plugin: org.kde.color with Color=0,0,0 (pure black).
# This is the lowest-overhead path — Plasma renders the colour without
# loading an image. Users may switch to org.kde.image and pick the
# veilor-black wallpaper installed above if they prefer.
info "Wallpaper: setting Plasma default to org.kde.color (solid black)"
# reference system uses `wallpaperplugin=org.kde.color` + `Color=0,0,0` — pure black
# rendered by Plasma's color plugin, no image asset needed.
# Apply via system-wide kdedefaults so new users inherit.
KDD=/etc/xdg/kdedefaults
install -d -m 0755 "$KDD"
cat > "$KDD/plasma-org.kde.plasma.desktop-appletsrc" << 'EOF'
@ -175,11 +143,8 @@ wallpaperplugin=org.kde.color
[Containments][1][Wallpaper][org.kde.color][General]
Color=0,0,0
[Containments][1][Wallpaper][org.kde.image][General]
Image=/usr/share/wallpapers/veilor-black/contents/images/veilor-black.png
EOF
ok "default wallpaper plugin = org.kde.color (#000000); image fallback wired"
ok "default wallpaper = solid #000000 (Plasma color plugin)"
# ─────────────────────────────────────────────────────────────────────
# 5. Breeze decoration override
@ -208,25 +173,7 @@ else
fi
# ─────────────────────────────────────────────────────────────────────
# 6. Branding logo (referenced by /etc/os-release LOGO field)
# ─────────────────────────────────────────────────────────────────────
info "Branding: installing veilor logo into /usr/share/pixmaps"
BR_SRC="$ASSETS/branding"
if [[ -f $BR_SRC/veilor-logo.svg ]]; then
install -d -m 0755 /usr/share/pixmaps
install -m 0644 "$BR_SRC/veilor-logo.svg" /usr/share/pixmaps/veilor-logo.svg
ok "logo installed at /usr/share/pixmaps/veilor-logo.svg"
# Plymouth theme can pick up the same asset for consistency.
if [[ -d /usr/share/plymouth/themes/veilor ]]; then
install -m 0644 "$BR_SRC/veilor-logo.svg" /usr/share/plymouth/themes/veilor/veilor-logo.svg
ok "logo mirrored into plymouth theme dir"
fi
else
warn "branding source missing at $BR_SRC/veilor-logo.svg — skipping"
fi
# ─────────────────────────────────────────────────────────────────────
# 7. Sanity: brand leak check (mirrors kickstart %post sanity)
# 6. Sanity: brand leak check (mirrors kickstart %post sanity)
# ─────────────────────────────────────────────────────────────────────
info "Sanity: scanning installed v0.3 paths for brand leaks"
LEAK_PATHS=(
@ -238,8 +185,6 @@ LEAK_PATHS=(
"/etc/sddm.conf.d/veilor-theme.conf"
"/etc/xdg/konsolerc"
"/etc/xdg/kdedefaults/plasma-org.kde.plasma.desktop-appletsrc"
"/usr/share/wallpapers/veilor-black"
"/usr/share/pixmaps/veilor-logo.svg"
)
LEAK=0
for p in "${LEAK_PATHS[@]}"; do

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

@ -1,114 +0,0 @@
# veilor-os AppArmor profile — Thorium browser (Chromium fork)
#
# Scope:
# Confine the Thorium browser binary at /usr/bin/thorium. Thorium is a
# Chromium derivative; it sandboxes its own renderer/GPU/utility processes,
# but the *browser* process itself runs with the full user's permissions
# unless an MAC layer scopes it down. This profile is that scope.
#
# Mode:
# complain — log violations to audit.log but do NOT block. This is the
# first-fit profile; the user is expected to refine it from observed
# denials before flipping to enforce. See `aa-logprof` to convert audit
# denials into rule additions.
#
# Manual enable:
# sudo install -m 0644 scripts/apparmor/usr.bin.thorium /etc/apparmor.d/
# sudo apparmor_parser -r /etc/apparmor.d/usr.bin.thorium
# sudo aa-complain /etc/apparmor.d/usr.bin.thorium # log only
# sudo aa-enforce /etc/apparmor.d/usr.bin.thorium # block
#
# NOT enabled in kickstart by default. v0.5 work.
#include <tunables/global>
profile thorium /usr/bin/thorium flags=(complain) {
#include <abstractions/base>
#include <abstractions/audio>
#include <abstractions/dbus-session>
#include <abstractions/fonts>
#include <abstractions/freedesktop.org>
#include <abstractions/gnome>
#include <abstractions/nameservice>
#include <abstractions/openssl>
#include <abstractions/X>
# ---- network: outbound HTTP/HTTPS only ----
network inet stream,
network inet6 stream,
network inet dgram, # DNS resolution
network inet6 dgram,
network netlink raw, # NetworkManager state queries
deny network raw,
deny network packet,
deny network bluetooth,
deny network can,
deny network rds,
deny network sctp,
# ---- binary + libs ----
/usr/bin/thorium mr,
/usr/lib/thorium/** mr,
/usr/share/thorium/** r,
/opt/thorium/** mr,
/etc/thorium/** r,
# ---- per-user state ----
owner @{HOME}/.config/thorium/** rwk,
owner @{HOME}/.cache/thorium/** rwk,
owner @{HOME}/.local/share/thorium/** rwk,
# ---- file pickers: only Downloads is writable ----
owner @{HOME}/Downloads/ rw,
owner @{HOME}/Downloads/** rwk,
owner @{HOME}/Documents/ r,
owner @{HOME}/Documents/** r,
owner @{HOME}/Pictures/ r,
owner @{HOME}/Pictures/** r,
# ---- /proc: own process only, deny memory peeking ----
owner /proc/@{pid}/** r,
deny /proc/*/mem rwk,
deny /proc/*/maps r,
deny /proc/sys/kernel/** w,
# ---- ptrace: forbidden ----
deny ptrace,
deny capability sys_ptrace,
# ---- kernel: no module load, no /dev/kmem, no /dev/mem ----
deny capability sys_module,
deny /dev/kmem rwk,
deny /dev/mem rwk,
deny /dev/port rwk,
deny /sys/kernel/** w,
# ---- temp ----
/tmp/ r,
owner /tmp/** rwk,
/var/tmp/ r,
owner /var/tmp/** rwk,
# ---- system info read-only ----
/etc/machine-id r,
/etc/os-release r,
/etc/localtime r,
/sys/devices/system/cpu/** r,
/sys/class/net/** r,
# ---- chrome sandbox helper (setuid/SUID-like child needs unconfined) ----
/usr/lib/thorium/chrome-sandbox Cx -> sandbox,
/usr/bin/xdg-open Pix,
profile sandbox {
#include <abstractions/base>
capability sys_admin,
capability sys_chroot,
capability sys_ptrace,
/usr/lib/thorium/chrome-sandbox mr,
/usr/lib/thorium/** mrix,
/proc/*/setgroups w,
/proc/*/uid_map w,
/proc/*/gid_map w,
}
}

View file

@ -1,78 +0,0 @@
# veilor-os AppArmor profile — veilor-power
#
# Scope:
# Confine /usr/local/bin/veilor-power, the power profile switcher. The
# script is small but invokes sudo to talk to tuned-adm; we want a tight
# surface so a compromised user shell cannot abuse the sudoers entry to
# pivot beyond profile switching.
#
# Mode:
# enforce — this binary is ours, the surface is small, no need for a
# complain runway. Verified rules at write time.
#
# Manual enable:
# sudo install -m 0644 scripts/apparmor/usr.bin.veilor-power /etc/apparmor.d/
# sudo apparmor_parser -r /etc/apparmor.d/usr.bin.veilor-power
# sudo aa-enforce /etc/apparmor.d/usr.bin.veilor-power
# # to debug:
# sudo aa-complain /etc/apparmor.d/usr.bin.veilor-power
#
# NOT enabled in kickstart by default. v0.5 work.
#include <tunables/global>
profile veilor-power /usr/local/bin/veilor-power flags=(enforce) {
#include <abstractions/base>
#include <abstractions/bash>
#include <abstractions/consoles>
# ---- the script itself + bash ----
/usr/local/bin/veilor-power r,
/usr/bin/bash ix,
/usr/bin/awk ix,
/usr/bin/cat ix,
# ---- read CPU + ASUS sysfs for status ----
/sys/devices/system/cpu/cpufreq/ r,
/sys/devices/system/cpu/cpufreq/** r,
/sys/devices/system/cpu/cpu*/cpufreq/ r,
/sys/devices/system/cpu/cpu*/cpufreq/** r,
/sys/devices/platform/asus-nb-wmi/ r,
/sys/devices/platform/asus-nb-wmi/** r,
# ---- sudo handoff to tuned-adm ----
/usr/bin/sudo Cx -> sudo_tuned,
/usr/bin/tuned-adm Pix,
# ---- forbidden ----
deny network,
deny ptrace,
deny capability sys_ptrace,
deny capability sys_module,
deny capability sys_rawio,
deny /dev/kmem rwk,
deny /dev/mem rwk,
deny /etc/shadow r,
deny /etc/sudoers w,
deny /etc/sudoers.d/** w,
deny @{HOME}/.ssh/** rwk,
deny @{HOME}/.gnupg/** rwk,
# ---- child profile for the sudo subprocess ----
profile sudo_tuned {
#include <abstractions/base>
#include <abstractions/authentication>
#include <abstractions/nameservice>
/usr/bin/sudo mr,
/etc/sudoers r,
/etc/sudoers.d/ r,
/etc/sudoers.d/veilor-power r,
/usr/bin/tuned-adm Pix,
/var/log/sudo* w,
/var/db/sudo/** rwk,
capability setuid,
capability setgid,
capability audit_write,
deny network,
}
}

View file

@ -1,96 +0,0 @@
# veilor-os AppArmor profile — LM Studio (local LLM runner)
#
# Scope:
# Confine LM Studio's binary. LM Studio loads arbitrary GGUF/safetensors
# weights and exposes an OpenAI-compatible HTTP server on :1234. The
# binary itself is closed-source — we don't trust it with the full home
# directory.
#
# Mode:
# complain initially. Flip to enforce once observed denials are reviewed.
#
# Manual enable:
# sudo install -m 0644 scripts/apparmor/usr.local.bin.lm-studio /etc/apparmor.d/
# sudo apparmor_parser -r /etc/apparmor.d/usr.local.bin.lm-studio
# sudo aa-complain /etc/apparmor.d/usr.local.bin.lm-studio
# sudo aa-enforce /etc/apparmor.d/usr.local.bin.lm-studio
#
# NOT enabled in kickstart by default. v0.5 work.
#include <tunables/global>
profile lm-studio /usr/local/bin/lm-studio flags=(complain) {
#include <abstractions/base>
#include <abstractions/nameservice>
#include <abstractions/openssl>
#include <abstractions/dbus-session>
#include <abstractions/freedesktop.org>
#include <abstractions/X>
#include <abstractions/fonts>
# ---- network: HTTP server :1234 + outbound model downloads ----
network inet stream,
network inet6 stream,
network inet dgram,
network inet6 dgram,
deny network raw,
deny network packet,
deny network bluetooth,
# ---- binary + electron runtime (LM Studio is Electron-based) ----
/usr/local/bin/lm-studio mr,
/opt/lm-studio/** mr,
/usr/lib/lm-studio/** mr,
# ---- model weights + metadata ----
owner @{HOME}/.lmstudio/ rw,
owner @{HOME}/.lmstudio/** rwk,
owner @{HOME}/.cache/lm-studio/** rwk,
owner @{HOME}/.config/LMStudio/** rwk,
# ---- temp ----
/tmp/ r,
owner /tmp/** rwk,
/var/tmp/ r,
owner /var/tmp/** rwk,
# ---- GPU device nodes (CUDA / ROCm / Vulkan) ----
/dev/dri/ r,
/dev/dri/** rw,
/dev/nvidia* rw,
/dev/nvidiactl rw,
/dev/nvidia-uvm rw,
/dev/nvidia-uvm-tools rw,
/dev/kfd rw,
/dev/shm/** rwk,
# ---- system info ----
/etc/machine-id r,
/etc/os-release r,
/etc/localtime r,
/sys/devices/system/cpu/** r,
/sys/class/drm/** r,
/proc/cpuinfo r,
/proc/meminfo r,
/proc/stat r,
# ---- /proc: own process only ----
owner /proc/@{pid}/** r,
deny /proc/*/mem rwk,
# ---- forbidden ----
deny ptrace,
deny capability sys_ptrace,
deny capability sys_module,
deny capability sys_rawio,
deny /dev/kmem rwk,
deny /dev/mem rwk,
deny /dev/port rwk,
deny /sys/kernel/** w,
deny /etc/shadow r,
deny @{HOME}/.ssh/** rwk,
deny @{HOME}/.gnupg/** rwk,
# ---- xdg / browser handoff for "Open in browser" UI button ----
/usr/bin/xdg-open Pix,
}

View file

@ -1,43 +1,10 @@
#!/usr/bin/env bash
# Build + load veilor-os SELinux policy modules.
#
# Modules:
# veilor-systemd — capabilities for systemd-modules-load (post-boot lock)
# veilor-firstboot — confine /usr/local/bin/veilor-firstboot one-shot
#
# Usage:
# sudo ./build-policy.sh # build + install all
# sudo ./build-policy.sh <name> # build + install one module
# Build + load veilor-systemd SELinux policy module.
set -euo pipefail
cd "$(dirname "$0")"
MODULES=(veilor-systemd veilor-firstboot)
if [[ $# -gt 0 ]]; then
MODULES=("$@")
fi
for m in "${MODULES[@]}"; do
if [[ ! -f "$m.te" ]]; then
echo "[ERR] $m.te not found" >&2
exit 1
fi
echo "[*] Building $m ..."
checkmodule -M -m -o "$m.mod" "$m.te"
semodule_package -o "$m.pp" -m "$m.mod"
semodule -i "$m.pp"
echo "[OK] $m loaded"
done
# Apply file context for veilor-firstboot if module just loaded.
if printf '%s\n' "${MODULES[@]}" | grep -qx veilor-firstboot; then
if command -v restorecon >/dev/null 2>&1; then
# Mark the binary + state file with the right types.
semanage fcontext -a -t veilor_firstboot_exec_t '/usr/local/bin/veilor-firstboot' 2>/dev/null || true
semanage fcontext -a -t veilor_firstboot_state_t '/var/lib/veilor-firstboot\.done' 2>/dev/null || true
restorecon -v /usr/local/bin/veilor-firstboot 2>/dev/null || true
[[ -e /var/lib/veilor-firstboot.done ]] && restorecon -v /var/lib/veilor-firstboot.done 2>/dev/null || true
fi
fi
echo "[done] all modules loaded"
checkmodule -M -m -o veilor-systemd.mod veilor-systemd.te
semodule_package -o veilor-systemd.pp -m veilor-systemd.mod
semodule -i veilor-systemd.pp
echo "[OK] veilor-systemd SELinux module loaded"

View file

@ -1,136 +0,0 @@
policy_module(veilor-firstboot, 1.0)
#
# veilor-os SELinux module — confine veilor-firstboot.service.
#
# The firstboot service runs once on TTY1 before SDDM, prompts for the
# admin password, then enables SDDM and self-disables. It is privileged
# (it must be — `passwd` writes /etc/shadow) but the surface is small and
# bounded. This module narrows what the service is allowed to do so that a
# bug or hostile env in firstboot.sh can't, e.g., dial out, scrape /home,
# or load a kernel module.
#
# Build + load:
# cd scripts/selinux
# ./build-policy.sh # builds & loads all .te modules
#
# Verify:
# semodule -l | grep veilor-firstboot
# ls -Z /usr/local/sbin/veilor-firstboot
# -> system_u:object_r:veilor_firstboot_exec_t:s0
#
# Audit any denials with:
# ausearch -m AVC -ts recent -c veilor-firstboot
require {
type init_t;
type passwd_exec_t;
type passwd_file_t;
type shadow_t;
type systemd_unit_file_t;
type systemd_passwd_var_run_t;
type sddm_unit_file_t;
type sddm_var_lib_t;
type tmp_t;
type tty_device_t;
type devtty_t;
type self_runtime_t;
type chkpwd_exec_t;
type pam_var_run_t;
type security_t;
type fs_t;
type usr_t;
type bin_t;
type lib_t;
type etc_t;
type proc_t;
type unconfined_service_t;
class file { read write create unlink getattr setattr open execute execute_no_trans map };
class dir { read write add_name remove_name search getattr open };
class chr_file { read write open getattr ioctl };
class capability { setuid setgid chown dac_override dac_read_search fowner fsetid };
class process { transition signal sigchld sigkill noatsecure rlimitinh siginh };
class service { start stop status enable disable };
class systemd { start };
class lnk_file { read getattr };
class filesystem { getattr };
}
# ---------------------------------------------------------------------
# 1. Define the firstboot domain + executable type
# ---------------------------------------------------------------------
type veilor_firstboot_t;
type veilor_firstboot_exec_t;
type veilor_firstboot_state_t; # /var/lib/veilor-firstboot.done
init_daemon_domain(veilor_firstboot_t, veilor_firstboot_exec_t)
files_type(veilor_firstboot_state_t)
# Auto-transition: when init_t executes /usr/local/sbin/veilor-firstboot,
# enter veilor_firstboot_t.
domain_auto_trans(init_t, veilor_firstboot_exec_t, veilor_firstboot_t)
# ---------------------------------------------------------------------
# 2. Allow rules — what the service IS allowed to do
# ---------------------------------------------------------------------
# read /etc/passwd, /etc/group, /etc/shadow (passwd needs shadow write)
allow veilor_firstboot_t passwd_file_t:file { read getattr open };
allow veilor_firstboot_t shadow_t:file { read write open getattr setattr };
# exec passwd(1)
allow veilor_firstboot_t passwd_exec_t:file { read getattr open execute execute_no_trans map };
allow veilor_firstboot_t chkpwd_exec_t:file { read getattr open execute execute_no_trans map };
# capabilities passwd needs
allow veilor_firstboot_t self:capability { setuid setgid chown dac_override dac_read_search fowner fsetid };
# write the state marker /var/lib/veilor-firstboot.done
allow veilor_firstboot_t veilor_firstboot_state_t:file { create write open getattr setattr unlink };
allow veilor_firstboot_t veilor_firstboot_state_t:dir { search write add_name remove_name };
# write /etc/sddm.conf.d/ entries (autologin disable, theme, etc.)
allow veilor_firstboot_t sddm_var_lib_t:dir { read write search add_name remove_name open };
allow veilor_firstboot_t sddm_var_lib_t:file { read write create open getattr setattr };
# start sddm.service via systemctl
allow veilor_firstboot_t sddm_unit_file_t:file { read getattr open };
allow veilor_firstboot_t sddm_unit_file_t:service { start status enable disable };
allow veilor_firstboot_t init_t:system { start };
# tty1 I/O
allow veilor_firstboot_t tty_device_t:chr_file { read write open getattr ioctl };
allow veilor_firstboot_t devtty_t:chr_file { read write open getattr ioctl };
# usual base reads
allow veilor_firstboot_t bin_t:file { read getattr open execute execute_no_trans map };
allow veilor_firstboot_t lib_t:file { read getattr open execute execute_no_trans map };
allow veilor_firstboot_t usr_t:file { read getattr open };
allow veilor_firstboot_t etc_t:file { read getattr open };
allow veilor_firstboot_t etc_t:dir { read search getattr open };
allow veilor_firstboot_t fs_t:filesystem getattr;
allow veilor_firstboot_t self:fifo_file { read write };
allow veilor_firstboot_t self:unix_stream_socket { create connect read write };
# ---------------------------------------------------------------------
# 3. Deny rules — what the service is NOT allowed to do
# ---------------------------------------------------------------------
# no network — firstboot must never phone home
neverallow veilor_firstboot_t self:tcp_socket *;
neverallow veilor_firstboot_t self:udp_socket *;
neverallow veilor_firstboot_t self:rawip_socket *;
neverallow veilor_firstboot_t self:packet_socket *;
neverallow veilor_firstboot_t self:netlink_route_socket *;
# no kernel module load
neverallow veilor_firstboot_t self:capability sys_module;
# no /home access except the bits ferror-firstboot.sh writes (admin's
# .config dir staging, if any). /home/admin general read = forbidden.
neverallow veilor_firstboot_t home_root_t:dir { read write };
neverallow veilor_firstboot_t user_home_t:dir { read write search };
neverallow veilor_firstboot_t user_home_t:file { read write open };
# no ptrace, no /dev/mem, no /dev/kmem
neverallow veilor_firstboot_t self:capability sys_ptrace;
neverallow veilor_firstboot_t self:capability sys_rawio;

View file

@ -1,85 +0,0 @@
# veilor-os — Test Method Changelog
Append-only log of changes to `test/TESTING.md`. Each entry: date, the
veilor-os version it first applied to, what changed in the procedure,
and *why*. The why is the load-bearing part — without it this file
becomes a list of opinions.
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.
**Why:** Through v0.2 → v0.5.26 we'd been reproducing the test
procedure ad-hoc each time, which meant test runs were uncomparable
and regressions were caught only by accident. The v0.5.26 → v0.5.27
debugging session surfaced the LUKS-cmdline bug, the GRUB rebrand
gap, the gum-cursor render glitch, and the fbcon KMS issue all in a
single VM run — but only because the test happened to walk every step
in order. Codifying the steps means the next regression is caught the
same way reliably.
The procedure documents the **hybrid VM method** explicitly: Claude
drives every step it can via QEMU monitor `sendkey`, the human types
LUKS + admin passwords directly into the QEMU window because plymouth
ignores synthesised keystrokes. Past trail (14+ failed sendkey
variants) is the source of truth for that limitation; do not re-fight
that battle without first rereading the trail.
The procedure also separates **VM** (cheap iteration, catches install
logic) from **real hardware** (mandatory for tag, catches firmware /
KMS / GPU). Future releases must produce a `test/test-runs/` report
for each before tagging.
**Files added:**
- `test/TESTING.md` — canonical procedure
- `test/METHOD-CHANGELOG.md` — this file
- `test/test-runs/` — per-run reports go here (template lands with
first real run, currently empty)

View file

@ -1,66 +0,0 @@
# test/
Test harnesses for veilor-os ISO builds.
## Files
| File | Purpose |
|------|---------|
| `run-vm.sh` | Manual smoke test — boot the latest ISO interactively in QEMU/KVM. SSH key injection via cloud-init seed + monitor sendkey fallback for live-image login. |
| `auto-install.sh` | **Autonomous** end-to-end install test. Boots ISO, drives the gum installer via QEMU monitor `sendkey`, waits for anaconda to finish + reboot, SSHs into the installed system, runs validation checklist. Prints PASS/FAIL summary. |
| `auto-install-keymap.sh` | Sourced helper. Provides `km_send_str`, `km_send_chord`, `km_send_key`, `km_screendump`, `km_wait_socket`, etc. Reusable by other automation. |
| `boot-checklist.md` | Manual post-install checklist (run on a real spare laptop). |
## Running the autonomous installer test
```sh
./test/auto-install.sh build/out/veilor-os-*.iso
```
Hardcoded inputs (deterministic — do not edit during a test run):
- Disk: first `/dev/vda` (the only disk in QEMU)
- Hostname: `veilor` (installer hardcoded since v0.5.4)
- LUKS passphrase: `testpass1234`
- Admin password: `adminpass1234`
- Locale: `en_GB.UTF-8`
Expected runtime: 2030 minutes wall clock (anaconda dominates).
### Outputs
- `/tmp/veilor-auto-install.log` — full driver log
- `/tmp/veilor-auto-install-NN-<step>.png` — milestone screenshots
- `/tmp/veilor-auto-install-final-ssh.txt` — final SSH session capture (uname/lsblk/cmdline/failed units)
### Exit codes
- `0` — all validation checks passed
- `1` — any failure (anaconda crashed, SSH never came up, validation check failed)
- `2` — preflight failure (missing tool, bad ISO arg, missing OVMF)
### Prerequisites
- `qemu-system-x86_64`, `qemu-img`, `socat`, `ssh`, `ssh-keygen`
- `edk2-ovmf` (OVMF UEFI firmware at `/usr/share/edk2/ovmf/OVMF_{CODE,VARS}.fd`)
- `mkisofs` or `xorriso` (for cloud-init seed ISO; harness falls back to TTY1 driving if seed cannot be built or cloud-init does not run on the installed system)
- `convert` from ImageMagick (optional — converts PPM screendumps to PNG; harness keeps PPM if absent)
- KVM access (`/dev/kvm` readable by the user)
### What it validates
Post-install on the booted system:
- `/etc/os-release``NAME=veilor-os`
- `hostnamectl --static``veilor`
- `systemctl is-active``active` for `sshd fail2ban usbguard tuned auditd firewalld chronyd sddm`
- `getenforce``Enforcing` (preferred) or `Permissive` (acceptable for v0.5.x)
- `lsblk -f` shows `crypto_LUKS` + `btrfs`
- `/etc/crypttab` has a LUKS entry
- `getent passwd admin` returns the user
- `/usr/local/bin/{veilor-power,veilor-doctor,veilor-update}` are present and executable
- `/proc/cmdline` contains `init_on_alloc=1`
### Troubleshooting
- **Stuck at boot banner**: ISO didn't autostart `veilor-installer` on tty1. Check `serial.log` and `auto-install-vm-NN-*.png` screenshots. The harness aborts after 5 minutes of identical screen frames.
- **SSH never up**: cloud-init may not have run on the installed system (no `cidata` mount). The harness falls back to TTY1 driving — typing the LUKS passphrase, logging in as admin, and hand-injecting the SSH key. If both paths fail, validation cannot proceed.
- **`screendump` produces unreadable PPM**: install ImageMagick (`dnf install ImageMagick`) so the harness converts to PNG.

View file

@ -1,187 +0,0 @@
# veilor-os — Testing Procedure
This document is the canonical procedure for validating a veilor-os ISO
end-to-end. Every release that gets a tag MUST have a corresponding
test-run report in `test/test-runs/` linked from the release notes.
If reality forces you to deviate from the steps below, **do not silently
patch the procedure** — open a commit that updates this file *and*
appends an entry to `test/METHOD-CHANGELOG.md` explaining what changed
and why. The changelog is what makes the procedure auditable; the
procedure itself is just the latest snapshot.
---
## Two test environments
| Environment | Catches | Doesn't catch |
|-------------|---------|---------------|
| **VM (QEMU + virtio-vga)** | install logic, kickstart bugs, %post failures, anaconda transaction failures, GRUB write, BLS entries, package selection, network stack | KMS / fbcon issues, real-firmware Secure Boot, USB controller quirks, GPU driver compatibility, sleep/wake, battery, thermals |
| **Real hardware (USB → spare laptop)** | everything VM doesn't | install repeatability (you only have so many spare laptops) |
Both are required for any tagged release. VM first (cheap iteration),
real hardware second (final sign-off).
---
## VM test — hybrid procedure
The VM cannot type LUKS / admin passwords through QEMU's `sendkey`
monitor command — plymouth's IPC ignores synthesised keystrokes (we
verified this across 14+ sendkey variants in earlier sessions). The
hybrid procedure splits the work: Claude/automation drives every step
that doesn't need a password; the human types the two passwords (LUKS
+ admin) into the QEMU window directly.
Standard test passwords (lab use only — never reuse outside this repo):
| Prompt | Type |
|--------|------|
| LUKS passphrase | `veilortest1` |
| Admin password | `veilortest1` |
Both passwords identical on purpose — easier to remember mid-test, both
satisfy the installer's 8-char min, neither contains shell-special
chars (validate_pw rejects `" $ \ \` & | / \n`).
### Run a VM test
```bash
cd ~/ai-lab/_github/veilor-os
# Pull the ISO you want to test (from a CI release or local build)
ls /home/admin/Downloads/veilor-os-*.iso
# Wipe stale state, launch VM with monitor sock (no auto-inject — we
# don't want sendkey noise typing into prompts)
FRESH=1 NO_INJECT=1 DISPLAY=:0 ./test/run-vm.sh \
/home/admin/Downloads/veilor-os-43-YYYYMMDD-HHMMSS.iso
```
Then either (a) drive the install yourself in the QEMU window, or
(b) hand the monitor sock to Claude / a script:
- Monitor sock: `test/veilor-vm.monitor.sock`
- Send a key: `echo "sendkey ret" | socat - "UNIX-CONNECT:$SOCK"`
- Screendump: `echo "screendump /tmp/x.ppm" | socat - "UNIX-CONNECT:$SOCK"; magick /tmp/x.ppm /tmp/x.png`
### Steps to verify
The complete checklist lives in `test/boot-checklist.md` — that file is
the granular pass/fail list. The high-level flow is:
1. **Live boot.** GRUB (legacy menu, no Plymouth splash) → text scroll
→ veilor-installer banner on tty1 within ~30s. No "fedora" branding
anywhere on screen.
2. **Installer menu.** "Install" highlighted by default. No phantom
duplicate items, no stray characters in input fields.
3. **Disk picker.** `/dev/vda` (or whatever virtio gives you) listed
with size + model.
4. **Passwords.** LUKS + admin prompts; user types `veilortest1` twice.
5. **Locale.** en_GB.UTF-8 picks up.
6. **Confirm.** Disk shown with `WILL BE ERASED`, locale + LUKS/admin
ticks shown.
7. **Anaconda.** "Installing veilor-os to /dev/vda · 1030 min · logs
on tty4". Watch for `Configuring man-db` — if anything fails, this
is historically where it dies.
8. **Reboot.** VM reboots; ISO must NOT boot first this time. Kill
QEMU + relaunch without ISO drive (see *Boot installed disk* below)
to skip the GRUB-from-ISO path.
9. **GRUB.** Single "veilor-os" entry (no rescue, no "Fedora Linux").
10. **LUKS prompt.** Plymouth `details` theme — text-mode prompt for
passphrase. User types `veilortest1` in the QEMU window (sendkey
will not work).
11. **First boot.** SDDM splash → admin user pre-filled → admin types
`veilortest1` → password-change prompt (chage -d 0 expired the
password) → user picks new password → KDE Plasma session.
12. **Hardening checks** per `test/boot-checklist.md` (SELinux
enforcing, fail2ban active, USBGuard active, tuned profile, etc.).
### Boot installed disk (skip ISO)
After the install reboots, QEMU's CD-first boot order will land back
in the live ISO. Easiest workaround: kill QEMU and re-launch without
the `-drive file=...iso` line. The qcow2 retains the install:
```bash
pkill -f 'qemu-system.*veilor-os'
cd ~/ai-lab/_github/veilor-os/test
DISPLAY=:0 qemu-system-x86_64 \
-enable-kvm -cpu host -smp 4 -m 4096 \
-machine q35,smm=on \
-global driver=cfi.pflash01,property=secure,value=on \
-drive if=pflash,format=raw,readonly=on,file=/usr/share/edk2/ovmf/OVMF_CODE.fd \
-drive if=pflash,format=raw,file=$PWD/veilor-vm.nvram \
-drive file=$PWD/veilor-vm.qcow2,if=virtio,format=qcow2 \
-monitor unix:$PWD/veilor-vm.monitor.sock,server,nowait \
-netdev user,id=net0,hostfwd=tcp::2222-:22 \
-device virtio-net-pci,netdev=net0 \
-vga virtio -display gtk,gl=on
```
---
## Real-hardware test — USB → spare laptop
Required for every tagged release. The VM cannot reproduce KMS /
fbcon / GPU-driver issues; only real silicon will.
### 1. Flash USB
```bash
# 8GB+ USB stick, identified by lsblk (e.g. /dev/sda — confirm vendor)
sudo umount /dev/sdX* 2>/dev/null
sudo wipefs -a /dev/sdX
sudo dd if=/path/to/veilor-os-*.iso of=/dev/sdX bs=4M status=progress conv=fsync
sync
sudo eject /dev/sdX
```
Etcher / GNOME Disks also fine. Verify-after-flash is built into
Etcher; for `dd`, run `cmp` on the first ISO_SIZE bytes if paranoid.
### 2. Boot test
- Disable Secure Boot in firmware (until we MOK-enroll our shim, which
is v0.5+).
- Boot from USB.
- Walk the same numbered steps as the VM section, except:
- On "TYPE NOW: passphrase" steps, you actually have a keyboard.
- At step 8, the laptop will eject the USB and reboot to the
installed system without intervention.
- At step 11, do NOT use `veilortest1` for the post-install admin
password change — pick something real if this is your daily-driver
laptop, or a throwaway if it's a test machine. The kickstart's
ChainOfTrust ends here; from this prompt forward you own the
password.
### 3. Capture findings
Fill in a fresh `test/test-runs/YYYY-MM-DD-vX.Y.Z.md` from the
template. **Always** capture: GRUB title, kernel cmdline (`cat
/proc/cmdline`), `lsblk -f`, `getenforce`, `systemctl is-active fail2ban
usbguard tuned auditd firewalld`, `journalctl -b -p err --no-pager`.
If anything regressed, that goes at the top of the report under
**Regressions**, with a screenshot if possible.
---
## Per-run report template
Copy `test/test-runs/_TEMPLATE.md` (created when the first real
test-run lands) and fill in section-by-section. Keep them brief —
this is meant to be a 5-minute write-up, not a thesis.
---
## When to alter this procedure
If a step turns out to be wrong, redundant, or missing:
1. Edit this file.
2. Append to `test/METHOD-CHANGELOG.md` with: date, version it first
applied to, what changed, and why (cite a specific test-run report
if the change is in response to a finding).
3. Reference the changelog entry in your commit message.
The changelog is the audit trail. Don't skip it.

View file

@ -1,167 +0,0 @@
#!/usr/bin/env bash
# auto-install-keymap.sh — sourced helper for QEMU-monitor-driven UI automation.
#
# Provides a minimal but complete US-layout keymap mapping every printable
# ASCII character to a QEMU `sendkey` chord, plus convenience wrappers for
# typing strings, sending special keys, taking screenshots, and waiting for
# the monitor socket to appear.
#
# Usage:
# source test/auto-install-keymap.sh
# MONITOR_SOCK=/path/to/sock
# km_wait_socket "$MONITOR_SOCK" 60
# km_send_str "$MONITOR_SOCK" "hello world"
# km_send_key "$MONITOR_SOCK" ret
# km_send_chord "$MONITOR_SOCK" ctrl alt f1
# km_screendump "$MONITOR_SOCK" /tmp/shot.ppm
#
# Why a separate file: other harnesses (regression suites, fuzzers) can
# source this without dragging in the full installer test driver.
# Guard against double-source.
[[ -n "${__VEILOR_KEYMAP_LOADED:-}" ]] && return 0
__VEILOR_KEYMAP_LOADED=1
# ── Tool requirements ──────────────────────────────────────────────────
# socat is the canonical way to talk to a unix-domain QEMU monitor.
# nc-openbsd would also work but socat is what run-vm.sh already uses.
km_require_tools() {
local missing=()
for t in socat qemu-img qemu-system-x86_64; do
command -v "$t" >/dev/null 2>&1 || missing+=("$t")
done
if [[ ${#missing[@]} -gt 0 ]]; then
echo "[ERR] missing required tools: ${missing[*]}" >&2
return 1
fi
}
# ── Low-level monitor I/O ──────────────────────────────────────────────
# Send a single line of monitor input. Newlines are critical — QEMU's HMP
# parses one command per line. Errors are swallowed: the most common cause
# is the VM having shut down between two send_* calls, which we tolerate.
km_monitor_send() {
local sock=$1; shift
printf '%s\n' "$*" | socat - "UNIX-CONNECT:$sock" 2>/dev/null || true
}
# Send a raw HMP command and capture any stdout response (e.g. for `info`
# queries). Trims the QEMU monitor banner + prompt noise.
km_monitor_query() {
local sock=$1; shift
printf '%s\n' "$*" | socat -t 1 - "UNIX-CONNECT:$sock" 2>/dev/null \
| sed -e 's/\r//g' -e '/^QEMU /d' -e '/^(qemu)/d' || true
}
# Wait until the monitor unix socket exists and accepts connections.
# $2 = max wait in seconds (default 60).
km_wait_socket() {
local sock=$1 max=${2:-60} waited=0
while (( waited < max )); do
if [[ -S $sock ]]; then
# Try a no-op query — confirms the QEMU side is actually serving.
if printf 'info status\n' | socat -t 1 - "UNIX-CONNECT:$sock" >/dev/null 2>&1; then
return 0
fi
fi
sleep 1
waited=$((waited + 1))
done
echo "[ERR] monitor socket $sock never became ready (waited ${max}s)" >&2
return 1
}
# ── Screenshots ────────────────────────────────────────────────────────
# Ask QEMU to dump the current framebuffer. Output is PPM. Convert to PNG
# with ImageMagick if available; otherwise leave PPM and warn.
km_screendump() {
local sock=$1 out=$2
local ppm="${out%.png}.ppm"
km_monitor_send "$sock" "screendump $ppm"
sleep 1 # give QEMU a moment to flush
if [[ -f $ppm ]] && command -v convert >/dev/null 2>&1; then
convert "$ppm" "$out" 2>/dev/null && rm -f "$ppm"
fi
}
# ── Key tables ─────────────────────────────────────────────────────────
# QEMU `sendkey` reference: docs/system/keys.html.in. The HMP names are
# the X11 keysym lower-case, with a few exceptions for non-letter keys
# (spc, ret, minus, etc.). What follows is the full US-layout printable
# ASCII set. Everything outside this table is silently dropped — callers
# are responsible for not feeding it characters the installer can't accept
# anyway (passwords are validated to ASCII-printable in veilor-installer).
declare -gA __KM_PLAIN=(
[' ']=spc [a]=a [b]=b [c]=c [d]=d [e]=e [f]=f [g]=g [h]=h
[i]=i [j]=j [k]=k [l]=l [m]=m [n]=n [o]=o [p]=p [q]=q [r]=r
[s]=s [t]=t [u]=u [v]=v [w]=w [x]=x [y]=y [z]=z
[0]=0 [1]=1 [2]=2 [3]=3 [4]=4 [5]=5 [6]=6 [7]=7 [8]=8 [9]=9
['-']=minus ['=']=equal ['[']=bracket_left [']']=bracket_right
[';']=semicolon ["'"]=apostrophe [',']=comma ['.']=dot
['/']=slash ['\\']=backslash ['`']=grave_accent
)
# Shift-prefixed (US): all caps + shifted-symbol row.
declare -gA __KM_SHIFT=(
[A]=a [B]=b [C]=c [D]=d [E]=e [F]=f [G]=g [H]=h [I]=i [J]=j
[K]=k [L]=l [M]=m [N]=n [O]=o [P]=p [Q]=q [R]=r [S]=s [T]=t
[U]=u [V]=v [W]=w [X]=x [Y]=y [Z]=z
['!']=1 ['@']=2 ['#']=3 ['$']=4 ['%']=5
['^']=6 ['&']=7 ['*']=8 ['(']=9 [')']=0
['_']=minus ['+']=equal ['{']=bracket_left ['}']=bracket_right
[':']=semicolon ['"']=apostrophe ['<']=comma ['>']=dot
['?']=slash ['|']=backslash ['~']=grave_accent
)
# ── Public send wrappers ───────────────────────────────────────────────
# Send a single named key (e.g. ret, esc, up, tab, f1).
km_send_key() {
local sock=$1 key=$2
km_monitor_send "$sock" "sendkey $key"
}
# Send a chord — components are joined with `-` per QEMU HMP syntax.
km_send_chord() {
local sock=$1; shift
local IFS='-'
km_monitor_send "$sock" "sendkey $*"
}
# Type a string by encoding each character via the keymap. Unrecognised
# characters are skipped with a warning to stderr — caller is expected to
# stick to printable ASCII.
km_send_str() {
local sock=$1 s=$2 ch chord
local i=0
while (( i < ${#s} )); do
ch="${s:i:1}"
if [[ -n "${__KM_PLAIN[$ch]:-}" ]]; then
chord="${__KM_PLAIN[$ch]}"
km_monitor_send "$sock" "sendkey $chord"
elif [[ -n "${__KM_SHIFT[$ch]:-}" ]]; then
chord="${__KM_SHIFT[$ch]}"
km_monitor_send "$sock" "sendkey shift-$chord"
else
printf '[WARN] km_send_str: unencodable char %q skipped\n' "$ch" >&2
fi
i=$((i + 1))
# Tiny gap so QEMU doesn't drop fast keypresses on busy hosts.
# Empirically 5ms = the line between "100% reliable" and "loses ~1%".
sleep 0.02
done
}
# Convenience: type a string then press Enter.
km_send_line() {
local sock=$1 s=$2
km_send_str "$sock" "$s"
km_send_key "$sock" ret
}
# Visual indicator for log readability — prints a banner + a short pause so
# the next monitor command has time to land on a stable UI frame. Used by
# the harness between major steps; safe to skip in automated reuse.
km_step_banner() {
local label=$1
printf '\n──── %s @ %s ────\n' "$label" "$(date +'%H:%M:%S')"
}

View file

@ -1,673 +0,0 @@
#!/usr/bin/env bash
# auto-install.sh — autonomous end-to-end install test for veilor-os.
#
# Boots a fresh ISO under QEMU, drives the gum installer via the QEMU
# monitor (sendkey events), waits for anaconda to finish + reboot, SSHes
# into the installed system, and runs a validation checklist.
#
# Usage:
# ./test/auto-install.sh path/to/veilor-os-*.iso
#
# Expected runtime:
# * boot + drive installer: ~3 min
# * anaconda install (KDE): ~15-25 min (depends on mirrors + host CPU)
# * reboot + SSH up: ~2 min
# * validation checks: <1 min
# * total: 20-30 min wall clock
#
# Hardcoded test inputs (do NOT edit — meant to be deterministic):
# disk first /dev/vda (only disk in QEMU)
# hostname "veilor" (installer hardcodes this in v0.5.4)
# LUKS pw testpass1234
# admin pw adminpass1234
# locale en_GB.UTF-8 (first option, accepted with Enter)
#
# Outputs:
# /tmp/veilor-auto-install.log — full driver log
# /tmp/veilor-auto-install-NN-<step>.png — milestone screenshots
# /tmp/veilor-auto-install-final-ssh.txt — final SSH session capture
#
# Exit codes:
# 0 = all validation checks passed
# 1 = any failure (anaconda crash, SSH never up, validation failed)
# 2 = preflight failure (missing tool, bad ISO arg)
#
# This script intentionally does not source test/run-vm.sh — it needs a
# different QEMU configuration (no live cloud-init seed since we're driving
# the installed-system path), and run-vm.sh `exec`s qemu, which is
# incompatible with running QEMU as a backgrounded child here.
set -uo pipefail
# ── Constants ──────────────────────────────────────────────────────────
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
TEST_DIR="$SCRIPT_DIR"
DISK="$TEST_DIR/auto-install-vm.qcow2"
NVRAM="$TEST_DIR/auto-install-vm.nvram"
MONITOR_SOCK="$TEST_DIR/auto-install-vm.monitor.sock"
SERIAL_LOG="$TEST_DIR/auto-install-vm.serial.log"
SEED_ISO="$TEST_DIR/auto-install-seed.iso"
LOG=/tmp/veilor-auto-install.log
SHOT_PREFIX=/tmp/veilor-auto-install
SSH_PORT=2222
SSH_USER=admin
LUKS_PW="testpass1234"
ADMIN_PW="adminpass1234"
# Disk: 40G is enough headroom — KDE base + 8G LUKS + LVM overhead fits in
# ~12G actual, but qcow2 only allocates blocks that get touched.
DISK_SIZE=40G
# OVMF firmware paths — Fedora layout. Caller can override if needed.
OVMF_CODE="${OVMF_CODE:-/usr/share/edk2/ovmf/OVMF_CODE.fd}"
OVMF_VARS_SRC="${OVMF_VARS_SRC:-/usr/share/edk2/ovmf/OVMF_VARS.fd}"
# Timing knobs — coarse but deliberate. Tighten only after observing slack
# on a real run.
WAIT_MONITOR_S=120 # qemu boot to monitor socket alive
WAIT_INSTALLER_BANNER_S=180 # ISO boot → tty1 gum menu visible
WAIT_GUM_PROMPT_S=8 # gum draws each prompt within ~5s
WAIT_AFTER_INPUT_S=3 # let UI advance after we hit Enter
ANACONDA_TIMEOUT_S=2700 # 45 min — anaconda + reboot + SSH come-up
ANACONDA_POLL_S=30 # screenshot/poll cadence during install
# ── Logging ────────────────────────────────────────────────────────────
: > "$LOG"
log() { printf '[%s] %s\n' "$(date +'%H:%M:%S')" "$*" | tee -a "$LOG"; }
fail() { log "FAIL: $*"; exit 1; }
# Source the keymap helper.
# shellcheck source=auto-install-keymap.sh
. "$SCRIPT_DIR/auto-install-keymap.sh"
# ── Preflight ──────────────────────────────────────────────────────────
preflight() {
log "preflight: checking environment"
ISO="${1:-}"
if [[ -z $ISO ]]; then
# Auto-fetch from ci-latest GH release if no path given. ISO is split
# into chunks (GH release 2 GiB asset cap). Reassemble before boot.
log "no ISO path given — fetching from gh release ci-latest"
local dl_dir="$HOME/veilor-iso/ci-latest"
mkdir -p "$dl_dir"
( cd "$dl_dir" && rm -f *.part-* *.iso *.sha256 && \
gh release download ci-latest --repo veilor-org/veilor-os \
--pattern '*.iso.part-*' --pattern '*.parts.sha256' --clobber ) || {
echo "[ERR] gh release download failed — is the ci-latest release populated?" >&2
exit 2
}
( cd "$dl_dir" && \
local stem
stem=$(ls *.part-00 2>/dev/null | head -1 | sed 's/\.part-00$//')
[ -n "$stem" ] || { echo "[ERR] no .part-00 in download"; exit 2; }
log "reassembling $stem from $(ls "$stem".part-* | wc -l) parts"
cat "$stem".part-* > "$stem"
sha256sum -c *.parts.sha256 || { echo "[ERR] reassembly checksum mismatch"; exit 2; }
) || exit 2
ISO=$(ls "$dl_dir"/*.iso 2>/dev/null | head -1)
[ -n "$ISO" ] || { echo "[ERR] no iso after reassembly"; exit 2; }
fi
if [[ ! -f $ISO ]]; then
echo "[ERR] ISO not found: $ISO" >&2
exit 2
fi
km_require_tools || exit 2
for t in ssh ssh-keygen pgrep pkill; do
command -v "$t" >/dev/null 2>&1 || { echo "[ERR] missing $t" >&2; exit 2; }
done
if [[ ! -f $OVMF_CODE ]]; then
echo "[ERR] OVMF firmware missing: $OVMF_CODE (install edk2-ovmf)" >&2
exit 2
fi
log "preflight: ISO=$ISO"
}
# ── VM lifecycle ───────────────────────────────────────────────────────
# Kill any QEMU we previously started + scrub state files. Idempotent.
kill_existing_vm() {
log "killing any existing auto-install QEMU"
if [[ -n "${QEMU_PID:-}" ]] && kill -0 "$QEMU_PID" 2>/dev/null; then
kill "$QEMU_PID" 2>/dev/null || true
sleep 2
kill -9 "$QEMU_PID" 2>/dev/null || true
fi
# Catch orphans from prior runs — match by disk path so we don't kill
# the user's other QEMU VMs.
pkill -f "qemu-system-x86_64.*$DISK" 2>/dev/null || true
rm -f "$MONITOR_SOCK" "$SERIAL_LOG"
}
# Wipe disk + nvram so each run is reproducible.
wipe_state() {
log "wiping qcow2 + nvram"
rm -f "$DISK" "$NVRAM" "$SEED_ISO"
qemu-img create -f qcow2 "$DISK" "$DISK_SIZE" >/dev/null
cp "$OVMF_VARS_SRC" "$NVRAM"
}
# Build a NoCloud cloud-init seed ISO so anaconda's installed system picks
# up our SSH pubkey on first boot. The installer-generated ks doesn't
# explicitly invoke cloud-init, but Fedora ships cloud-init enabled by
# default in @core; if a cidata seed is present at boot, NoCloud datasource
# fires and we get key injection for free.
build_seed_iso() {
local pubkey="" found=""
for cand in "$HOME/.ssh/id_ed25519.pub" "$HOME/.ssh/id_rsa.pub"; do
if [[ -f $cand ]]; then
pubkey="$(< "$cand")"
found=$cand
break
fi
done
if [[ -z $pubkey ]]; then
log "seed: no host SSH pubkey found at ~/.ssh/id_{ed25519,rsa}.pub"
log "seed: generating throwaway test key"
local key=$TEST_DIR/auto-install-id_ed25519
rm -f "$key" "$key.pub"
ssh-keygen -t ed25519 -N '' -f "$key" -C "veilor-auto-install" >/dev/null
pubkey="$(< "$key.pub")"
TEST_KEY="$key"
else
log "seed: using $found"
# Match host id; assume corresponding private key exists alongside.
TEST_KEY="${found%.pub}"
fi
local d
d=$(mktemp -d)
cat > "$d/meta-data" <<EOF
instance-id: veilor-auto-install
local-hostname: veilor
EOF
cat > "$d/user-data" <<EOF
#cloud-config
users:
- name: admin
ssh_authorized_keys:
- $pubkey
lock_passwd: false
ssh_pwauth: true
runcmd:
- rm -f /etc/ssh/sshd_config.d/10-veilor-hardening.conf
- systemctl reload sshd || systemctl restart sshd || true
EOF
if command -v mkisofs >/dev/null 2>&1; then
mkisofs -quiet -output "$SEED_ISO" -volid cidata -joliet -rock \
"$d/user-data" "$d/meta-data"
elif command -v xorriso >/dev/null 2>&1; then
xorriso -as mkisofs -quiet -output "$SEED_ISO" -volid cidata \
-joliet -rock "$d/user-data" "$d/meta-data"
else
log "seed: no mkisofs/xorriso — SSH key injection unavailable"
SEED_ISO=""
fi
rm -rf "$d"
[[ -f $SEED_ISO ]] && log "seed: built $SEED_ISO"
}
# Launch QEMU in the background. Returns once the monitor socket is alive.
launch_vm() {
local iso=$1
log "launching QEMU"
local seed_args=()
[[ -n $SEED_ISO && -f $SEED_ISO ]] && \
seed_args=(-drive "file=$SEED_ISO,media=cdrom,readonly=on")
qemu-system-x86_64 \
-name veilor-auto-install \
-enable-kvm \
-cpu host \
-smp 4 \
-m 4096 \
-machine q35,smm=on \
-global driver=cfi.pflash01,property=secure,value=on \
-drive if=pflash,format=raw,readonly=on,file="$OVMF_CODE" \
-drive if=pflash,format=raw,file="$NVRAM" \
-drive file="$DISK",if=virtio,format=qcow2,cache=writeback \
-drive file="$iso",media=cdrom,readonly=on \
"${seed_args[@]}" \
-monitor "unix:$MONITOR_SOCK,server,nowait" \
-boot order=dc,menu=off \
-netdev user,id=net0,hostfwd=tcp::${SSH_PORT}-:22 \
-device virtio-net-pci,netdev=net0 \
-device virtio-rng-pci \
-vga virtio \
-display none \
-serial "file:$SERIAL_LOG" \
>>"$LOG" 2>&1 &
QEMU_PID=$!
log "QEMU pid=$QEMU_PID"
km_wait_socket "$MONITOR_SOCK" "$WAIT_MONITOR_S" \
|| fail "monitor socket never opened"
log "monitor socket ready"
}
# Did QEMU die? Used at every poll; lets us bail with a useful message
# instead of waiting out the full timeout.
qemu_alive() {
[[ -n "${QEMU_PID:-}" ]] && kill -0 "$QEMU_PID" 2>/dev/null
}
# ── Driver: walk the installer flow ────────────────────────────────────
# Take a numbered screenshot. Auto-increments NN.
SHOT_N=0
shot() {
local label=$1
SHOT_N=$((SHOT_N + 1))
local file
file=$(printf '%s-%02d-%s.png' "$SHOT_PREFIX" "$SHOT_N" "$label")
km_screendump "$MONITOR_SOCK" "$file"
log "screenshot: $file"
}
drive_installer() {
log "waiting ${WAIT_INSTALLER_BANNER_S}s for ISO boot + tty1 installer"
# The live ISO autologs into multi-user.target, runs gum on tty1 via a
# systemd unit that replaces getty (see overlay/etc/systemd/system/
# veilor-installer.service if it exists; otherwise via the multi-user
# default in kickstart line 250).
sleep "$WAIT_INSTALLER_BANNER_S"
qemu_alive || fail "QEMU died during ISO boot"
shot "boot-banner"
# Make absolutely sure we're on tty1 (the live ks sets multi-user.target
# default, so we should already be there — but a stray graphical.target
# on dev builds would silently swallow our keystrokes).
km_send_chord "$MONITOR_SOCK" ctrl alt f1
sleep "$WAIT_AFTER_INPUT_S"
shot "tty1"
# Step 1: top option = "Install" — gum choose has it pre-selected.
log "step: select Install"
km_send_key "$MONITOR_SOCK" ret
sleep "$WAIT_GUM_PROMPT_S"
shot "after-install-pick"
# Step 2: disk select — only /dev/vda exists in this QEMU. Default
# selection = first row.
log "step: select disk (/dev/vda — only one)"
km_send_key "$MONITOR_SOCK" ret
sleep "$WAIT_GUM_PROMPT_S"
shot "after-disk-pick"
# Step 3: LUKS passphrase. gum input --password reads stdin until newline.
log "step: enter LUKS passphrase"
km_send_str "$MONITOR_SOCK" "$LUKS_PW"
sleep 1
km_send_key "$MONITOR_SOCK" ret
sleep "$WAIT_AFTER_INPUT_S"
shot "after-luks-pw"
# Step 4: admin password.
log "step: enter admin password"
km_send_str "$MONITOR_SOCK" "$ADMIN_PW"
sleep 1
km_send_key "$MONITOR_SOCK" ret
sleep "$WAIT_AFTER_INPUT_S"
shot "after-admin-pw"
# Step 5: locale select — first option = en_GB.UTF-8.
log "step: confirm locale (en_GB.UTF-8)"
km_send_key "$MONITOR_SOCK" ret
sleep "$WAIT_GUM_PROMPT_S"
shot "after-locale"
# Step 6: confirm screen. gum confirm defaults to "Yes" focused →
# Enter accepts. (Verified against gum 0.13+ docs; if defaults change
# in a future gum, swap to explicit "y" via key map.)
log "step: confirm install"
km_send_key "$MONITOR_SOCK" ret
sleep "$WAIT_AFTER_INPUT_S"
shot "after-confirm"
log "installer driven: anaconda should now be running"
}
# Quick non-blocking SSH probe. Returns 0 if reachable.
ssh_alive() {
ssh -p $SSH_PORT \
-o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null \
-o ConnectTimeout=3 \
-o BatchMode=yes \
${TEST_KEY:+-i "$TEST_KEY"} \
"$SSH_USER@127.0.0.1" true 2>/dev/null
}
# Poll for anaconda completion + SSH availability. We can't watch QEMU exit
# (anaconda's `reboot` directive triggers systemctl reboot, which doesn't
# poweroff the VM — it boots back into the installed disk). The signal we
# actually trust is SSH on port 2222 starting to answer.
#
# If cloud-init didn't run (the seed ISO might not have been picked up by
# anaconda's installed system, depending on whether /etc/cloud is in the
# installed package set), SSH will never come up via key auth. The fallback
# in tty1_unlock_ssh() drives the SDDM/console login by hand.
wait_for_install_and_reboot() {
log "waiting up to ${ANACONDA_TIMEOUT_S}s for anaconda + reboot + SSH"
local waited=0 last_shot=0 last_ppm_hash="" same_count=0
while (( waited < ANACONDA_TIMEOUT_S )); do
if ! qemu_alive; then
fail "QEMU exited unexpectedly during install (check $SERIAL_LOG)"
fi
# SSH probe — first PASS exits the loop.
if ssh_alive; then
log "SSH up — installed system reachable"
shot "ssh-up"
return 0
fi
# Periodic screenshot + stuck-screen detection.
if (( waited - last_shot >= ANACONDA_POLL_S )); then
local ppm="$SHOT_PREFIX-poll.ppm"
km_monitor_send "$MONITOR_SOCK" "screendump $ppm"
sleep 1
if [[ -f $ppm ]]; then
local h
h=$(sha256sum "$ppm" 2>/dev/null | cut -d' ' -f1)
if [[ -n $last_ppm_hash && $h == "$last_ppm_hash" ]]; then
same_count=$((same_count + 1))
else
same_count=0
fi
last_ppm_hash=$h
rm -f "$ppm"
fi
# 5 minutes of identical frames = stuck. Anaconda's text-mode
# progress refreshes at least every minute, so 10 frames in a
# row (5 min @ 30s cadence) identical means it's wedged.
if (( same_count >= 10 )); then
shot "stuck"
fail "screen unchanged for 5min — anaconda likely crashed"
fi
last_shot=$waited
log "anaconda still running... (${waited}s elapsed)"
fi
sleep 5
waited=$((waited + 5))
done
shot "ssh-timeout"
log "SSH never came up via cloud-init; trying TTY1 fallback"
if tty1_unlock_ssh; then
log "TTY1 fallback succeeded; SSH should be reachable"
return 0
fi
fail "anaconda did not complete + SSH within ${ANACONDA_TIMEOUT_S}s, TTY1 fallback also failed"
}
# TTY1 fallback: the installed system reached SDDM (graphical) or got stuck
# at LUKS prompt. We drop to a TTY, log in as admin (chage forces password
# change on first use), and undo the sshd hardening so our pubkey works.
#
# This is best-effort. If the LUKS prompt is still up — we can't get past
# it without typing the passphrase, which we do here too.
tty1_unlock_ssh() {
log "TTY1 fallback: typing LUKS passphrase + admin login + opening sshd"
# Switch to tty1 in case SDDM grabbed graphical.
km_send_chord "$MONITOR_SOCK" ctrl alt f3
sleep 3
# If we're at LUKS prompt, the passphrase clears it. If we're already
# past LUKS, this is a harmless garbage on the login prompt — we Enter
# to clear, then proceed with login.
km_send_str "$MONITOR_SOCK" "$LUKS_PW"
km_send_key "$MONITOR_SOCK" ret
sleep 30 # cryptsetup unlock + boot to login prompt
shot "tty3-prelogin"
# Username — admin. chage -d 0 means we'll be prompted to change pw on
# first login. The old password is whatever we typed at install time;
# the new password just has to satisfy PAM minlen — reuse $ADMIN_PW
# and add a "1" suffix to make passwd's "must differ" check happy.
km_send_line "$MONITOR_SOCK" "admin"
sleep 3
km_send_line "$MONITOR_SOCK" "$ADMIN_PW"
sleep 5
# Old pw prompt (chage forced).
km_send_line "$MONITOR_SOCK" "$ADMIN_PW"
sleep 2
# New pw twice. Use a derivative; PAM rejects identical-to-old and we
# don't want to surprise the user with a password change.
km_send_line "$MONITOR_SOCK" "${ADMIN_PW}new"
sleep 1
km_send_line "$MONITOR_SOCK" "${ADMIN_PW}new"
sleep 5
shot "tty3-loggedin"
# Inject host pubkey + remove sshd hardening + reload sshd.
local pubkey=""
if [[ -n "${TEST_KEY:-}" && -f "${TEST_KEY}.pub" ]]; then
pubkey=$(< "${TEST_KEY}.pub")
fi
if [[ -z $pubkey ]]; then
log "TTY1 fallback: no pubkey to inject — cannot recover SSH"
return 1
fi
km_send_line "$MONITOR_SOCK" "mkdir -p ~/.ssh && chmod 700 ~/.ssh"
sleep 1
km_send_line "$MONITOR_SOCK" "echo '$pubkey' >> ~/.ssh/authorized_keys"
sleep 1
km_send_line "$MONITOR_SOCK" "chmod 600 ~/.ssh/authorized_keys"
sleep 1
km_send_line "$MONITOR_SOCK" "echo '${ADMIN_PW}new' | sudo -S rm -f /etc/ssh/sshd_config.d/10-veilor-hardening.conf"
sleep 2
km_send_line "$MONITOR_SOCK" "echo '${ADMIN_PW}new' | sudo -S systemctl reload sshd"
sleep 5
# Wait up to 60s for SSH to actually answer.
local i
for ((i=0; i<60; i++)); do
if ssh_alive; then
log "TTY1 fallback: SSH reachable after ${i}s"
return 0
fi
sleep 1
done
return 1
}
# ── Validation ─────────────────────────────────────────────────────────
# Run a single SSH command, return its stdout. Failures are NOT fatal here
# — caller decides what's a hard failure.
remote() {
ssh -p $SSH_PORT \
-o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null \
-o BatchMode=yes \
${TEST_KEY:+-i "$TEST_KEY"} \
"$SSH_USER@127.0.0.1" "$@"
}
# Validation result accumulator. check_remote runs a shell snippet on the
# installed VM via SSH; the snippet must exit 0 for PASS, non-zero for
# FAIL. check_eq compares remote stdout to an expected literal.
VALIDATIONS=()
# check_remote <desc> <remote shell snippet>
# Runs the snippet via SSH, treats exit code as the verdict.
check_remote() {
local desc=$1 cmd=$2
local out rc
out=$(remote "$cmd" 2>&1)
rc=$?
if (( rc == 0 )); then
VALIDATIONS+=("PASS $desc")
log " PASS: $desc"
else
# Truncate the failure context so the report stays scannable.
local trimmed=${out:0:120}
VALIDATIONS+=("FAIL $desc ($trimmed)")
log " FAIL: $desc -- $trimmed"
fi
}
# check_eq <desc> <remote shell snippet> <expected stdout>
# Runs the snippet, trims trailing whitespace, compares to expected.
check_eq() {
local desc=$1 cmd=$2 expected=$3
local got
got=$(remote "$cmd" 2>/dev/null | tr -d '\r' | tail -n1)
got=${got%[[:space:]]}
if [[ $got == "$expected" ]]; then
VALIDATIONS+=("PASS $desc (=$got)")
log " PASS: $desc (=$got)"
else
VALIDATIONS+=("FAIL $desc (got: '$got', expected: '$expected')")
log " FAIL: $desc -- got '$got' expected '$expected'"
fi
}
run_validation() {
log "running validation checklist"
# os-release
check_remote "/etc/os-release: NAME=veilor-os" \
'grep -q "^NAME=.veilor-os" /etc/os-release'
check_eq "hostnamectl --static = veilor" \
'hostnamectl --static' "veilor"
# Active services
for svc in sshd fail2ban usbguard tuned auditd firewalld chronyd sddm; do
check_eq "$svc is-active" \
"systemctl is-active $svc" "active"
done
# SELinux. v0.5.x kickstart sets `selinux --enforcing` for installed
# systems but veilor-firstboot may toggle behavior — accept either
# Enforcing or Permissive, but log which one we got. (Hard-fail on
# Disabled.)
local selinux
selinux=$(remote getenforce 2>/dev/null | tr -d '\r' | tail -n1)
selinux=${selinux%[[:space:]]}
if [[ $selinux == Enforcing ]]; then
VALIDATIONS+=("PASS SELinux = Enforcing")
log " PASS: SELinux = Enforcing"
elif [[ $selinux == Permissive ]]; then
VALIDATIONS+=("PASS SELinux = Permissive (acceptable for v0.5)")
log " PASS (soft): SELinux = Permissive"
else
VALIDATIONS+=("FAIL SELinux = $selinux")
log " FAIL: SELinux = $selinux"
fi
# Disk layout: LUKS2 + btrfs.
check_remote "lsblk shows crypto_LUKS" \
'lsblk -f | grep -q crypto_LUKS'
check_remote "lsblk shows btrfs" \
'lsblk -f | grep -q btrfs'
check_remote "/etc/crypttab has LUKS entry" \
'grep -Ev "^\s*(#|$)" /etc/crypttab | grep -qi luks'
# Admin user
check_remote "admin user exists" \
'getent passwd admin | grep -q "^admin:"'
# CLI tools shipped via overlay.
for bin in veilor-power veilor-doctor veilor-update; do
check_remote "/usr/local/bin/$bin present" \
"test -x /usr/local/bin/$bin"
done
# init_on_alloc — veilor-installer kickstart sets it on the install
# cmdline (line 315). /proc/cmdline is the source of truth.
check_remote "init_on_alloc=1 in /proc/cmdline" \
'grep -q init_on_alloc=1 /proc/cmdline'
}
# ── Reporting ──────────────────────────────────────────────────────────
print_report() {
local pass=0 fail=0
for line in "${VALIDATIONS[@]}"; do
case "$line" in
PASS*) pass=$((pass + 1)) ;;
FAIL*) fail=$((fail + 1)) ;;
esac
done
{
echo "════════════════════════════════════════════════════════"
echo " veilor-os auto-install test report"
echo " $(date)"
echo "════════════════════════════════════════════════════════"
printf '%s\n' "${VALIDATIONS[@]}"
echo "────────────────────────────────────────────────────────"
printf 'TOTAL: %d PASS, %d FAIL\n' "$pass" "$fail"
echo "Logs: $LOG"
echo "Screenshots: ${SHOT_PREFIX}-NN-*.png"
echo "Serial log: $SERIAL_LOG"
echo "════════════════════════════════════════════════════════"
} | tee -a "$LOG"
# Capture a final SSH session snapshot (uname/lsblk/sysctl) for the
# human reviewer.
{
echo "=== final ssh probe ==="
date
echo "--- uname -a ---"
remote uname -a 2>&1
echo "--- lsblk -f ---"
remote lsblk -f 2>&1
echo "--- /proc/cmdline ---"
remote cat /proc/cmdline 2>&1
echo "--- systemctl --failed ---"
remote systemctl --failed 2>&1
} > "${SHOT_PREFIX}-final-ssh.txt" 2>&1 || true
log "final ssh snapshot: ${SHOT_PREFIX}-final-ssh.txt"
if (( fail > 0 )); then
return 1
fi
return 0
}
cleanup() {
log "cleanup"
if [[ -n "${QEMU_PID:-}" ]] && kill -0 "$QEMU_PID" 2>/dev/null; then
# Graceful shutdown via monitor first; SIGTERM if it ignores us.
km_monitor_send "$MONITOR_SOCK" "system_powerdown" 2>/dev/null || true
sleep 5
if kill -0 "$QEMU_PID" 2>/dev/null; then
kill "$QEMU_PID" 2>/dev/null || true
sleep 2
kill -9 "$QEMU_PID" 2>/dev/null || true
fi
fi
rm -f "$MONITOR_SOCK"
}
# ── Main ───────────────────────────────────────────────────────────────
main() {
trap cleanup EXIT
preflight "$@"
kill_existing_vm
wipe_state
build_seed_iso
launch_vm "$ISO"
drive_installer
wait_for_install_and_reboot
run_validation
print_report
}
main "$@"

View file

@ -5,34 +5,6 @@
# ./test/run-vm.sh path/to.iso # specific ISO
# SECBOOT=1 ./test/run-vm.sh # use OVMF Secure Boot firmware
# FRESH=1 ./test/run-vm.sh # wipe disk + nvram, re-install from scratch
# NO_INJECT=1 ./test/run-vm.sh # skip SSH-key auto-injection
#
# SSH-key auto-injection (chosen approach: dual — cloud-init NoCloud + QEMU
# monitor sendkey fallback)
# ------------------------------------------------------------------
# Goal: previously each test required logging in at the QEMU console and
# running `passwd -d liveuser`, editing sshd_config, etc. before
# `ssh -p 2222 liveuser@localhost` worked. This script eliminates that.
#
# Primary path (works for the *installed* system, not the live image):
# * Detect host pubkey at ~/.ssh/id_ed25519.pub or ~/.ssh/id_rsa.pub
# * Build a NoCloud cloud-init ISO (user-data + meta-data) via mkisofs/xorriso
# * Mount it as a second virtual cdrom — Anaconda/cloud-init picks it up
# automatically when installing because the seed has the magic
# `cidata` volume label.
#
# Fallback path (works for the *live* image, which doesn't run cloud-init by
# default — dracut-live + livesys-scripts mount squashfs read-only and skip
# cloud-init.target):
# * Open a QEMU monitor unix socket (-monitor unix:...).
# * After ~90s (long enough for SDDM autologin → liveuser), background a
# helper that pipes a sequence of `sendkey` events to the monitor:
# Ctrl+Alt+F2 (drop to TTY)
# "sudo passwd -d liveuser && sudo systemctl reload sshd\n"
# This unblocks SSH on port 2222 without manual interaction.
#
# Both paths are best-effort; if the host has no pubkey, both are skipped
# and the script behaves exactly as before.
set -euo pipefail
@ -40,8 +12,6 @@ REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
TEST_DIR="$REPO_ROOT/test"
DISK="$TEST_DIR/veilor-vm.qcow2"
NVRAM="$TEST_DIR/veilor-vm.nvram"
SEED_ISO="$TEST_DIR/cloud-init-seed.iso"
MONITOR_SOCK="$TEST_DIR/veilor-vm.monitor.sock"
ISO="${1:-$(ls -t "$REPO_ROOT"/build/out/*.iso 2>/dev/null | head -1)}"
[[ -n ${ISO:-} && -f $ISO ]] || { echo "[ERR] No ISO found. Build first: ./build/build-iso.sh"; exit 1; }
@ -58,173 +28,19 @@ fi
# Reset on FRESH=1
if [[ "${FRESH:-0}" == "1" ]]; then
rm -f "$DISK" "$NVRAM" "$SEED_ISO"
rm -f "$DISK" "$NVRAM"
fi
# Provision disk + per-VM nvram once
[[ -f $DISK ]] || qemu-img create -f qcow2 "$DISK" 40G
[[ -f $NVRAM ]] || cp "$OVMF_VARS_SRC" "$NVRAM"
# ── Locate host SSH pubkey (ed25519 preferred, rsa fallback) ──
HOST_PUBKEY=""
if [[ "${NO_INJECT:-0}" != "1" ]]; then
for cand in "$HOME/.ssh/id_ed25519.pub" "$HOME/.ssh/id_rsa.pub"; do
if [[ -f $cand ]]; then
HOST_PUBKEY="$(< "$cand")"
echo "[INFO] using host pubkey: $cand"
break
fi
done
fi
# ── Build cloud-init NoCloud seed ISO (primary path) ──
SEED_ARGS=()
if [[ -n $HOST_PUBKEY ]]; then
SEED_DIR="$(mktemp -d)"
trap 'rm -rf "$SEED_DIR"' EXIT
cat > "$SEED_DIR/meta-data" <<EOF
instance-id: veilor-test-vm
local-hostname: veilor-test
EOF
cat > "$SEED_DIR/user-data" <<EOF
#cloud-config
users:
- name: liveuser
ssh_authorized_keys:
- $HOST_PUBKEY
- name: admin
ssh_authorized_keys:
- $HOST_PUBKEY
lock_passwd: false
passwd:
ssh_pwauth: true
runcmd:
- rm -f /etc/ssh/sshd_config.d/10-veilor-hardening.conf
- systemctl reload sshd || systemctl restart sshd || true
EOF
# Build NoCloud ISO. Volume label MUST be "cidata" (case-insensitive)
# for cloud-init's NoCloud datasource to pick it up.
if command -v mkisofs >/dev/null 2>&1; then
mkisofs -quiet -output "$SEED_ISO" \
-volid cidata -joliet -rock \
"$SEED_DIR/user-data" "$SEED_DIR/meta-data"
elif command -v xorriso >/dev/null 2>&1; then
xorriso -as mkisofs -quiet -output "$SEED_ISO" \
-volid cidata -joliet -rock \
"$SEED_DIR/user-data" "$SEED_DIR/meta-data"
elif command -v cloud-localds >/dev/null 2>&1; then
cloud-localds "$SEED_ISO" "$SEED_DIR/user-data" "$SEED_DIR/meta-data"
else
echo "[WARN] no mkisofs/xorriso/cloud-localds — skipping cloud-init seed"
SEED_ISO=""
fi
if [[ -n $SEED_ISO && -f $SEED_ISO ]]; then
echo "[INFO] cloud-init seed ISO: $SEED_ISO"
SEED_ARGS=(-drive "file=$SEED_ISO,media=cdrom,readonly=on")
fi
fi
# ── QEMU monitor unix socket ──
# Always exposed so the host can drive the VM via `socat - UNIX-CONNECT:...`
# (sendkey, screendump, etc.) for debugging. Independent of pubkey injection.
rm -f "$MONITOR_SOCK"
MONITOR_ARGS=(-monitor "unix:$MONITOR_SOCK,server,nowait")
# ── Auto-inject helper (live ISO doesn't run cloud-init) ──
# Started in the background after a delay; sends keypresses through the
# QEMU monitor unix socket to drop to a TTY and unblock SSH for liveuser.
if [[ -n $HOST_PUBKEY ]]; then
(
# Wait for the VM to reach a usable login prompt (SDDM autologin →
# liveuser session is the most realistic target). 90s is enough on
# KVM/4 vCPUs; tune via VM_BOOT_DELAY if needed.
sleep "${VM_BOOT_DELAY:-90}"
[[ -S $MONITOR_SOCK ]] || exit 0
# send_chord <key1> [key2 ...] — chord released between calls
send_chord() {
local IFS='-'
local chord="$*"
printf 'sendkey %s\n' "$chord"
}
# send_str <text> — only ASCII printable + space + return
send_str() {
local s="$1" ch
local i=0
while (( i < ${#s} )); do
ch="${s:i:1}"
case "$ch" in
' ') printf 'sendkey spc\n' ;;
[a-z0-9]) printf 'sendkey %s\n' "$ch" ;;
[A-Z]) printf 'sendkey shift-%s\n' "${ch,,}" ;;
'-') printf 'sendkey minus\n' ;;
'_') printf 'sendkey shift-minus\n' ;;
'/') printf 'sendkey slash\n' ;;
'.') printf 'sendkey dot\n' ;;
'&') printf 'sendkey shift-7\n' ;;
esac
i=$((i+1))
done
}
{
send_chord ctrl alt f2
sleep 1
# Type: liveuser <enter> (no password by default on live)
send_str "liveuser"
printf 'sendkey ret\n'
sleep 2
send_str "sudo passwd -d liveuser"
printf 'sendkey ret\n'
sleep 1
send_str "sudo systemctl reload sshd"
printf 'sendkey ret\n'
} | socat - "UNIX-CONNECT:$MONITOR_SOCK" 2>/dev/null || true
) &
INJECT_PID=$!
trap 'kill $INJECT_PID 2>/dev/null || true; rm -f "$MONITOR_SOCK"; rm -rf "${SEED_DIR:-}"' EXIT
fi
echo "════════════════════════════════════════════════════════"
echo " veilor-os :: VM test"
echo " ISO : $ISO"
echo " Disk : $DISK"
echo " NVRAM : $NVRAM"
echo " Seed : ${SEED_ISO:-<none>}"
# Anaconda virtio-serial log channel.
#
# Anaconda 43.x autodetects /dev/virtio-ports/org.fedoraproject.anaconda.log.0
# and streams program/packaging/storage/anaconda logs through it in real
# time, before any tmpfs / pivot, before networking. Survives kernel
# panic. The host gets a tail-able file. No anaconda CLI flag, no
# kickstart change, just the QEMU virtio-serial wiring.
#
# We've lost logs three times in a row to anaconda failures + tmpfs
# reboots. Wiring this up so future failures auto-capture.
ANACONDA_LOG="$TEST_DIR/anaconda-vm-$(date +%Y%m%d-%H%M%S).log"
ANACONDA_LOG_DIR="$TEST_DIR/test-runs/$(date +%Y%m%d-%H%M%S)"
mkdir -p "$ANACONDA_LOG_DIR"
ANACONDA_LOG_ARGS=(
# Belt: virtio-serial (anaconda's setupVirtio rsyslog forward, fragile —
# depends on rsyslog being installed in the live ISO).
-chardev "file,id=anaclog,path=$ANACONDA_LOG"
-device virtio-serial-pci,id=vs1
-device "virtserialport,chardev=anaclog,bus=vs1.0,name=org.fedoraproject.anaconda.log.0"
# Braces: virtio-9p host directory share. veilor-installer mounts this
# at /mnt/hostlogs and rsyncs /tmp/*.log there post-anaconda.
-virtfs "local,path=$ANACONDA_LOG_DIR,mount_tag=hostlogs,security_model=mapped-xattr,id=hostlogs"
)
echo " AnaLog : $ANACONDA_LOG"
echo " HostFS : $ANACONDA_LOG_DIR (9p tag: hostlogs)"
echo " Mode : ${SECBOOT:+secboot}${SECBOOT:-stock UEFI}"
echo " Inject: ${HOST_PUBKEY:+yes}${HOST_PUBKEY:-no (no host pubkey)}"
echo "════════════════════════════════════════════════════════"
exec qemu-system-x86_64 \
@ -238,10 +54,7 @@ exec qemu-system-x86_64 \
-drive if=pflash,format=raw,readonly=on,file="$OVMF_CODE" \
-drive if=pflash,format=raw,file="$NVRAM" \
-drive file="$DISK",if=virtio,format=qcow2,cache=writeback \
-drive file="$ISO",media=cdrom,readonly=on \
"${SEED_ARGS[@]}" \
"${MONITOR_ARGS[@]}" \
"${ANACONDA_LOG_ARGS[@]}" \
-cdrom "$ISO" \
-boot menu=on,splash-time=2000 \
-netdev user,id=net0,hostfwd=tcp::2222-:22 \
-device virtio-net-pci,netdev=net0 \

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_

View file

@ -1,80 +0,0 @@
# Test run — vX.Y.Z
- **Date:** YYYY-MM-DD
- **ISO:** `veilor-os-43-YYYYMMDD-HHMMSS.iso` (sha256: `...`)
- **Tester:** name / handle
- **Environment:** VM (qemu/q35/ovmf, 4 vCPU, 4G RAM, virtio-vga) — OR — Real HW (model, CPU, GPU)
---
## Result
✅ Pass / ⚠️ Pass-with-issues / ❌ Fail
One-line summary.
---
## Regressions vs previous run
(Things that worked in the prior tagged release but failed here. Empty
if none. Always check this section first when reading the report.)
---
## Per-step results
Walk `test/TESTING.md` step-by-step. Mark each pass/fail with a brief
note when failed.
| # | Step | Result | Notes |
|---|------|--------|-------|
| 1 | Live boot to installer banner | ✅ | |
| 2 | Installer menu render | ✅ | |
| 3 | Disk picker | ✅ | |
| 4 | LUKS + admin passwords | ✅ | |
| 5 | Locale | ✅ | |
| 6 | Confirm | ✅ | |
| 7 | Anaconda transaction | ✅ | |
| 8 | Reboot | ✅ | |
| 9 | GRUB single veilor-os entry | ✅ | |
| 10 | LUKS unlock prompt | ✅ | |
| 11 | First boot → SDDM → KDE | ✅ | |
| 12 | Hardening checks | ✅ | |
---
## Hardening verification
```
$ getenforce
Enforcing
$ systemctl is-active fail2ban usbguard tuned auditd firewalld
active
active
active
active
active
$ cat /proc/cmdline
... rd.luks.uuid=luks-... ...
$ lsblk -f
...
```
Paste real output. If any service is inactive or any cmdline arg is
missing, raise as a Regression above.
---
## Findings
Free-form notes. Cosmetic glitches, slow paths, surprising behaviour.
---
## Action items for next release
- [ ] ...
- [ ] ...
(Linked PRs / commits if you opened any during the test.)