Compare commits
90 commits
feat/sre-f
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 5c961eba88 | |||
|
|
8c70030d80 | ||
|
|
89c7df0ecc | ||
|
|
c2b4df8ef9 | ||
|
|
b9df392fbc | ||
|
|
84fa325e46 | ||
|
|
1e4ca2b56b | ||
|
|
446c602683 | ||
|
|
ac5c29df42 | ||
|
|
6f4842a75c | ||
|
|
4b90e7e00b | ||
|
|
7a0c665cf0 | ||
|
|
d38fce4cb8 | ||
| bc738c1c7b | |||
| a3f6c1a1a6 | |||
| 356013e1ca | |||
| 417acb5585 | |||
| df574e00f5 | |||
| 3e660534a1 | |||
| 749bcef5b4 | |||
| 77ed91ed8e | |||
| ad059ec73e | |||
| 05d37f6419 | |||
| eafb8b7aa1 | |||
| d0738970e0 | |||
|
|
7974ed7a6e | ||
|
|
f06ee5cc1c | ||
|
|
130f0432dd | ||
|
|
08f16bb2ee | ||
|
|
25b8d30f35 | ||
|
|
aa731f9daa | ||
|
|
441f7d057f | ||
|
|
816fc0ee68 | ||
|
|
44f0c787a7 | ||
|
|
900f5465b3 | ||
|
|
63c5e199d9 | ||
|
|
abb67841f1 | ||
|
|
b86b4f9ec3 | ||
|
|
7060d9aa6b | ||
|
|
50a241a603 | ||
|
|
4e9782a18a | ||
|
|
2788b95a12 | ||
|
|
e83483a077 | ||
|
|
613d35402e | ||
|
|
fae677fb68 | ||
|
|
931a19ec93 | ||
|
|
e848c7ffc3 | ||
|
|
1881c14ea7 | ||
|
|
c89c73ee84 | ||
|
|
b3509b4b06 | ||
|
|
4dabbd8fcf | ||
|
|
6197f7bf89 | ||
|
|
abfba24512 | ||
|
|
68ebe6fdbe | ||
|
|
26d6ff277b | ||
|
|
15311f56e9 | ||
|
|
2be5692c74 | ||
|
|
8ebe3a9713 | ||
|
|
77266faa4f | ||
|
|
d07adf3b14 | ||
|
|
8861e12485 | ||
|
|
1a0cf689a8 | ||
|
|
e90d6ef662 | ||
|
|
f588f15a6e | ||
|
|
38d702e14a | ||
|
|
2511df6327 | ||
|
|
2784fbd6e9 | ||
|
|
f8fc89e399 | ||
|
|
53949b0899 | ||
|
|
ac371bdc36 | ||
|
|
5e38412944 | ||
|
|
dce276586f | ||
|
|
9921745c9d | ||
|
|
deef914064 | ||
|
|
da08047172 | ||
|
|
73ac2cf96f | ||
|
|
75a68a1187 | ||
|
|
0f4647577b | ||
|
|
125e5f93af | ||
|
|
9fedb8592f | ||
|
|
ec4291293e | ||
|
|
3cbffaf714 | ||
|
|
8127f32868 | ||
|
|
4c8002cda7 | ||
|
|
70abf8c496 | ||
|
|
09f7c1f753 | ||
|
|
408a0e4862 | ||
|
|
d543e71f74 | ||
|
|
2d6f6b07f6 | ||
|
|
b4b5d7c007 |
68 changed files with 7235 additions and 508 deletions
304
.github/workflows/build-iso.yml
vendored
304
.github/workflows/build-iso.yml
vendored
|
|
@ -1,3 +1,5 @@
|
|||
# TODO: SHA-pin all uses: tags to commit SHAs (Agent 8 audit recommendation).
|
||||
# Tracked separately so this PR can land without long web lookups.
|
||||
name: Build veilor-os ISO
|
||||
|
||||
on:
|
||||
|
|
@ -19,40 +21,30 @@ 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
|
||||
runs-on: ubuntu-24.04
|
||||
# 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
|
||||
timeout-minutes: 90
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
# Pinned to last v4 tag confirmed to ship on node20. v4.2+ ships
|
||||
# node24 which forgejo-runner v6.4.0 (node20) cannot exec.
|
||||
uses: actions/checkout@v4.1.7
|
||||
|
||||
- name: Free up disk
|
||||
run: |
|
||||
sudo rm -rf /opt/hostedtoolcache /usr/share/dotnet /usr/local/lib/android /usr/local/share/boost
|
||||
sudo apt-get clean
|
||||
df -h
|
||||
|
||||
- name: Run build inside Fedora 43 container
|
||||
uses: addnab/docker-run-action@v3
|
||||
with:
|
||||
image: registry.fedoraproject.org/fedora:43
|
||||
options: |
|
||||
--privileged
|
||||
-v ${{ github.workspace }}:/work
|
||||
-v /dev:/dev
|
||||
--tmpfs /tmp:rw,nosuid,nodev,exec,size=16G
|
||||
- name: Install build tooling (Fedora)
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
|
||||
# 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 \
|
||||
|
|
@ -64,9 +56,29 @@ jobs:
|
|||
createrepo_c \
|
||||
git \
|
||||
which \
|
||||
shadow-utils
|
||||
shadow-utils \
|
||||
syslinux \
|
||||
tar \
|
||||
curl \
|
||||
sudo
|
||||
|
||||
cd /work
|
||||
- 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.
|
||||
|
|
@ -78,19 +90,22 @@ jobs:
|
|||
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.
|
||||
# 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
|
||||
|
||||
# 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).
|
||||
# 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 \
|
||||
|
|
@ -102,41 +117,226 @@ jobs:
|
|||
--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
|
||||
- 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/
|
||||
|
||||
# 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"
|
||||
[[ -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
|
||||
# ── 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
|
||||
with:
|
||||
name: veilor-os-iso
|
||||
path: |
|
||||
build/out/*.iso
|
||||
path: build/out
|
||||
format: spdx-json
|
||||
output-file: build/out/veilor-os.spdx.json
|
||||
|
||||
- name: Build provenance attestation
|
||||
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.server_url == 'https://github.com'
|
||||
# Pinned to last v2.2 release that ships node20.
|
||||
uses: actions/attest-build-provenance@v2.2.3
|
||||
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-*
|
||||
build/out/*.sha256
|
||||
retention-days: 14
|
||||
build/out/*.sig
|
||||
build/out/*.pem
|
||||
build/out/*.spdx.json
|
||||
|
||||
- name: Upload build log on failure
|
||||
# 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
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: veilor-os-buildlog
|
||||
path: |
|
||||
build/out/build.log
|
||||
build/out/build/anaconda/
|
||||
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)"
|
||||
|
||||
- name: Attach to release
|
||||
if: github.event_name == 'release'
|
||||
uses: softprops/action-gh-release@v2
|
||||
# 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
|
||||
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
|
||||
|
|
|
|||
36
.github/workflows/lint.yml
vendored
36
.github/workflows/lint.yml
vendored
|
|
@ -12,7 +12,8 @@ jobs:
|
|||
container:
|
||||
image: registry.fedoraproject.org/fedora:43
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
# Pinned to last v4 tag confirmed to ship on node20.
|
||||
- uses: actions/checkout@v4.1.7
|
||||
- run: dnf -y install pykickstart
|
||||
- run: ksvalidator kickstart/veilor-os.ks
|
||||
|
||||
|
|
@ -20,7 +21,8 @@ jobs:
|
|||
name: Shell scripts
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
# Pinned to last v4 tag confirmed to ship on node20.
|
||||
- uses: actions/checkout@v4.1.7
|
||||
- uses: ludeeus/action-shellcheck@master
|
||||
with:
|
||||
severity: warning
|
||||
|
|
@ -30,31 +32,31 @@ jobs:
|
|||
name: No personal/onyx leaks
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
# Pinned to last v4 tag confirmed to ship on node20.
|
||||
- uses: actions/checkout@v4.1.7
|
||||
- name: Grep for leaks
|
||||
run: |
|
||||
set -e
|
||||
# Allow audit greps that explicitly check for the patterns
|
||||
# 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.
|
||||
MATCHES=$(grep -rIni \
|
||||
-e 'onyx' \
|
||||
-e '192\.168\.0\.' \
|
||||
-e 'fedora\.local' \
|
||||
-e 'xynki\.dev' \
|
||||
--exclude-dir=.git \
|
||||
--exclude='*.md' \
|
||||
. || true)
|
||||
kickstart/ overlay/ scripts/ assets/ build/ \
|
||||
|| true)
|
||||
|
||||
# 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.
|
||||
# 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.
|
||||
LEAKS=$(echo "$MATCHES" | grep -v \
|
||||
-e 'should not contain' \
|
||||
-e 'returns zero' \
|
||||
-e 'audit grep' \
|
||||
-e "'onyx\\\\\\\\\\\\|" \
|
||||
-e 'onyx\\|' \
|
||||
-e "name:.*onyx leaks" \
|
||||
-e "-e 'onyx'" \
|
||||
-e "grep .*'onyx" \
|
||||
-e '# Sanity:' \
|
||||
-e 'brand leak' \
|
||||
|| true)
|
||||
|
||||
if [[ -n "$LEAKS" ]]; then
|
||||
|
|
|
|||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -13,4 +13,6 @@ secrets/
|
|||
*.pem
|
||||
test/veilor-vm.qcow2
|
||||
test/veilor-vm.nvram*
|
||||
test/auto-install-vm.qcow2
|
||||
test/auto-install-vm.nvram*
|
||||
.claude/worktrees/
|
||||
|
|
|
|||
97
README.md
97
README.md
|
|
@ -2,40 +2,58 @@
|
|||
|
||||
> **Hardened minimal Fedora KDE spin. Black-on-black. Locked down by default.**
|
||||
|
||||
[](https://github.com/veilor-org/veilor-os/actions/workflows/build-iso.yml)
|
||||
[](https://git.s8n.ru/veilor-org/veilor-os/actions?workflow=build-iso.yml)
|
||||
[](LICENSE)
|
||||
[](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
|
||||
|
||||
**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).
|
||||
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).
|
||||
|
||||
What is **done**: hardening (SELinux, sysctl, USBGuard, fail2ban,
|
||||
Primary git host: <https://git.s8n.ru/veilor-org/veilor-os>. The GitHub
|
||||
mirror was disabled 2026-05-06; this repo is private-by-default on
|
||||
Forgejo. ISO builds and CI artifacts are produced by the Forgejo runner
|
||||
on nullstone — no GitHub Actions involvement.
|
||||
|
||||
What is **shipping**: hardening (SELinux, sysctl, USBGuard, fail2ban,
|
||||
firewalld), KDE black theme, Fira Code system font, 3-mode power
|
||||
management, single-prompt LUKS install, first-boot admin password flow,
|
||||
reproducible CI build, EFI+BIOS bootable live ISO.
|
||||
|
||||
What is **planned** (see [docs/ROADMAP.md](docs/ROADMAP.md)): Plymouth
|
||||
black theme, SDDM theme, signed ISOs (own MOK + GPG), AppArmor + nftables,
|
||||
veilor-update / veilor-doctor helpers, public docs site.
|
||||
+ 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**.
|
||||
|
||||
---
|
||||
|
||||
## Quick install
|
||||
|
||||
```bash
|
||||
# 1. Download the ISO (after public release; CI artifact for now)
|
||||
# 1. Download the ISO from the latest Forgejo release.
|
||||
# https://git.s8n.ru/veilor-org/veilor-os/releases/tag/ci-latest
|
||||
# (rolling tag; replaced on each successful build-iso.yml run)
|
||||
sha256sum -c veilor-os-43-*.iso.sha256
|
||||
|
||||
# 2. Flash to USB. Replace /dev/sdX with your USB device — triple-check.
|
||||
|
|
@ -98,30 +116,49 @@ Full reference: [docs/HARDENING.md](docs/HARDENING.md).
|
|||
|
||||
## How veilor-os compares
|
||||
|
||||
| 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 |
|
||||
| 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) |
|
||||
|
||||
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
|
||||
|
|
|
|||
38
assets/branding/veilor-logo.svg
Normal file
38
assets/branding/veilor-logo.svg
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
<?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>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
5
assets/installer/banner.txt
Normal file
5
assets/installer/banner.txt
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
██ ██ ███████ ██ ██ ██████ ██████ ██████ ███████
|
||||
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
||||
██ ██ █████ ██ ██ ██ ██ ██████ ██ ██ ███████
|
||||
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
||||
████ ███████ ██ ███████ ██████ ██ ██ ██████ ███████
|
||||
89
assets/installer/colors.gum
Normal file
89
assets/installer/colors.gum
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
# 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"
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
# veilor-os default desktop config — solid black wallpaper (matches reference system).
|
||||
# veilor-os default desktop config — solid black wallpaper.
|
||||
# Plasma uses `wallpaperplugin=org.kde.color` (not org.kde.image) — pure
|
||||
# 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.
|
||||
# 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.
|
||||
|
||||
[Containments][Wallpaper]
|
||||
wallpaperplugin=org.kde.color
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ TerminalCenter=false
|
|||
TerminalMargin=4
|
||||
|
||||
[Appearance]
|
||||
ColorScheme=Linux
|
||||
ColorScheme=Veilor
|
||||
Font=Fira Code,11,-1,5,400,0,0,0,0,0,0,0,0,0,0,1
|
||||
LineSpacing=1
|
||||
UseFontLineCharacters=true
|
||||
|
|
|
|||
BIN
assets/wallpapers/veilor-black.png
Normal file
BIN
assets/wallpapers/veilor-black.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
20
assets/wallpapers/veilor-black.svg
Normal file
20
assets/wallpapers/veilor-black.svg
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<?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>
|
||||
|
After Width: | Height: | Size: 731 B |
128
docs/CLI.md
Normal file
128
docs/CLI.md
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
# 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)
|
||||
275
docs/DOCS-DOCS.md
Normal file
275
docs/DOCS-DOCS.md
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
# 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 **1–2 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.
|
||||
|
|
@ -41,6 +41,21 @@ 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`.
|
||||
|
|
@ -119,6 +134,57 @@ 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).
|
||||
|
|
|
|||
95
docs/INSTALLER.md
Normal file
95
docs/INSTALLER.md
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
# 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
|
||||
278
docs/ROADMAP.md
278
docs/ROADMAP.md
|
|
@ -9,6 +9,58 @@ 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.27–31 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
|
||||
|
|
@ -24,6 +76,47 @@ Released `v0.2.5` on 2026-05-01. CI on every push to `main`.
|
|||
|
||||
---
|
||||
|
||||
## v0.5.27–v0.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.
|
||||
|
|
@ -97,42 +190,168 @@ specified — defaults stay sane for a daily driver.
|
|||
|
||||
---
|
||||
|
||||
## v0.6 — ergonomics
|
||||
## 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)
|
||||
|
||||
Smooth the operator experience so day-to-day work doesn't fight the
|
||||
hardening.
|
||||
hardening. `veilor-postinstall` and `veilor-doctor` were v0.6 background
|
||||
items — promoted to **headline** features after v0.5.27–31 made it
|
||||
clear that "we ask once, with sane defaults" is what separates a
|
||||
distro from a kickstart.
|
||||
|
||||
- **`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.
|
||||
- **`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.
|
||||
- **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. Currently three
|
||||
commands.
|
||||
the daemon + add the user to the right group.
|
||||
|
||||
---
|
||||
|
||||
## v0.7 — public flex
|
||||
## v0.7 — BlueBuild OCI mainline (ACTIVE — primary focus 2026-05-06+)
|
||||
|
||||
Take veilor-os out of "private repo, contained audience" mode.
|
||||
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.
|
||||
|
||||
- **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
|
||||
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.
|
||||
- **Threat model published** — what veilor-os defends against, what it
|
||||
does not. Honest scope.
|
||||
- **Press kit** — wallpapers, logo, screenshots, feature one-liner.
|
||||
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.**
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -159,15 +378,16 @@ 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. They live on a separate track and do not
|
||||
block v1.0.
|
||||
kickstart or build tool.
|
||||
|
||||
- **`veilor-server`** — no KDE, no GUI, hardened headless Fedora for
|
||||
homelab / VPS. Same overlay, different package set.
|
||||
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.
|
||||
- **`veilor-kiosk`** — single-app Plasma session, locked-down user,
|
||||
read-only root. For dedicated-purpose machines.
|
||||
read-only root. **Not blocked.**
|
||||
- **`veilor-atomic`** — rpm-ostree / bootc-image-builder rebase.
|
||||
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.
|
||||
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.
|
||||
|
|
|
|||
336
docs/STRATEGY.md
Normal file
336
docs/STRATEGY.md
Normal file
|
|
@ -0,0 +1,336 @@
|
|||
# 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) +
|
||||
1–2 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.96–0.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.96–0.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>
|
||||
157
docs/THREAT-MODEL.md
Normal file
157
docs/THREAT-MODEL.md
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
# 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 5–30 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.*
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
# 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 8–15s 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)
|
||||
117
docs/research/2026-05-05-agent-wave/02-sddm-firstboot-ux.md
Normal file
117
docs/research/2026-05-05-agent-wave/02-sddm-firstboot-ux.md
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
# 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.
|
||||
158
docs/research/2026-05-05-agent-wave/03-bootc-spike-plan.md
Normal file
158
docs/research/2026-05-05-agent-wave/03-bootc-spike-plan.md
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
# 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).
|
||||
125
docs/research/2026-05-05-agent-wave/04-hardening-tier-2.md
Normal file
125
docs/research/2026-05-05-agent-wave/04-hardening-tier-2.md
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
# 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 ≈ **5–7 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.
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
# 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
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
# 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`
|
||||
100
docs/research/2026-05-05-agent-wave/07-kde-skel-branding.md
Normal file
100
docs/research/2026-05-05-agent-wave/07-kde-skel-branding.md
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
# 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`.
|
||||
131
docs/research/2026-05-05-agent-wave/08-ci-hardening.md
Normal file
131
docs/research/2026-05-05-agent-wave/08-ci-hardening.md
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
# 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.
|
||||
167
docs/research/2026-05-05-agent-wave/09-realhw-failure-modes.md
Normal file
167
docs/research/2026-05-05-agent-wave/09-realhw-failure-modes.md
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
# 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.
|
||||
42
docs/research/2026-05-05-agent-wave/README.md
Normal file
42
docs/research/2026-05-05-agent-wave/README.md
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# 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.
|
||||
|
|
@ -51,7 +51,14 @@ 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.
|
||||
bootloader --location=mbr --append="lockdown=integrity slab_nomerge randomize_kstack_offset=on vsyscall=none"
|
||||
# `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"
|
||||
|
||||
# ── Live ISO partitioning (flat — for live rootfs build only) ──
|
||||
# NOTE: This is the *live* image kickstart. Final installed system uses
|
||||
|
|
@ -112,6 +119,11 @@ 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
|
||||
|
|
@ -182,7 +194,7 @@ cp -a "$SRC/scripts" "$DEST/usr/share/veilor-os/" || echo "[ERR] scripts cp fail
|
|||
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" "$DEST/usr/local/sbin" 2>&1 || echo "[WARN] chown failed"
|
||||
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
|
||||
|
|
@ -191,7 +203,7 @@ set +x
|
|||
date
|
||||
echo "SRC=$SRC DEST=$DEST"
|
||||
ls -la "$DEST/usr/share/veilor-os/" 2>&1
|
||||
ls -la "$DEST/usr/local/sbin/" 2>&1
|
||||
ls -la "$DEST/usr/local/bin/" 2>&1
|
||||
} > "$DEST/var/log/veilor-nochroot.log" 2>&1 || true
|
||||
%end
|
||||
|
||||
|
|
@ -205,7 +217,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/sbin/veilor-firstboot /usr/local/sbin/veilor-installer
|
||||
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
|
||||
|
||||
# Live image plumbing (matches upstream Fedora live ks). Without these the
|
||||
# squashfs/EFI build fails — livesys-scripts ships systemd units lorax expects.
|
||||
|
|
@ -224,6 +236,7 @@ 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
|
||||
|
|
@ -248,6 +261,16 @@ ln -sf /usr/lib/systemd/system/sddm.service /etc/systemd/system/display-manager.
|
|||
# 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'
|
||||
|
|
@ -256,10 +279,115 @@ 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
|
||||
systemctl enable veilor-firstboot.service
|
||||
# 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-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
|
||||
|
|
|
|||
11
overlay/etc/apparmor.d/veilor.d/firefox
Normal file
11
overlay/etc/apparmor.d/veilor.d/firefox
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# veilor-os AppArmor profile stub — firefox
|
||||
#
|
||||
# v0.6 scope: marker only. Loads in complain mode via scripts/40-apparmor.sh
|
||||
# so AppArmor can log the syscall surface for v0.7 policy authoring. No
|
||||
# actual confinement rules yet — full policy is post-v0.6.
|
||||
|
||||
#include <tunables/global>
|
||||
|
||||
profile veilor-firefox /usr/lib*/firefox/firefox flags=(complain) {
|
||||
#include <abstractions/base>
|
||||
}
|
||||
11
overlay/etc/apparmor.d/veilor.d/thunderbird
Normal file
11
overlay/etc/apparmor.d/veilor.d/thunderbird
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# veilor-os AppArmor profile stub — thunderbird
|
||||
#
|
||||
# v0.6 scope: marker only. Loads in complain mode via scripts/40-apparmor.sh
|
||||
# so AppArmor can log the syscall surface for v0.7 policy authoring. No
|
||||
# actual confinement rules yet — full policy is post-v0.6.
|
||||
|
||||
#include <tunables/global>
|
||||
|
||||
profile veilor-thunderbird /usr/lib*/thunderbird/thunderbird flags=(complain) {
|
||||
#include <abstractions/base>
|
||||
}
|
||||
58
overlay/etc/audisp/audisp-remote.conf.disabled
Normal file
58
overlay/etc/audisp/audisp-remote.conf.disabled
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
# 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.
|
||||
23
overlay/etc/audit/plugins.d/veilor-remote.conf
Normal file
23
overlay/etc/audit/plugins.d/veilor-remote.conf
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
# 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
|
||||
12
overlay/etc/firewalld/zones/trusted.xml
Normal file
12
overlay/etc/firewalld/zones/trusted.xml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<?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>
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
NAME="veilor-os"
|
||||
PRETTY_NAME="veilor-os 0.1"
|
||||
PRETTY_NAME="veilor-os 0.5.27"
|
||||
ID=veilor
|
||||
ID_LIKE=fedora
|
||||
VERSION="0.1"
|
||||
VERSION_ID="0.1"
|
||||
VERSION="0.5.27"
|
||||
VERSION_ID="0.5.27"
|
||||
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"
|
||||
|
|
|
|||
60
overlay/etc/skel/.config/breezerc
Normal file
60
overlay/etc/skel/.config/breezerc
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
# 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
|
||||
29
overlay/etc/skel/.config/kdeglobals
Normal file
29
overlay/etc/skel/.config/kdeglobals
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
[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
|
||||
10
overlay/etc/skel/.config/konsolerc
Normal file
10
overlay/etc/skel/.config/konsolerc
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# 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
|
||||
29
overlay/etc/skel/.config/kwinrc
Normal file
29
overlay/etc/skel/.config/kwinrc
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# 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=
|
||||
104
overlay/etc/skel/.local/share/konsole/Veilor.colorscheme
Normal file
104
overlay/etc/skel/.local/share/konsole/Veilor.colorscheme
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
[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
|
||||
55
overlay/etc/skel/.local/share/konsole/Veilor.profile
Normal file
55
overlay/etc/skel/.local/share/konsole/Veilor.profile
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
[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
|
||||
|
|
@ -14,3 +14,8 @@ 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
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
# isn't copied into target system — see kickstart/install.ks).
|
||||
[Service]
|
||||
ExecStart=
|
||||
ExecStart=-/usr/local/sbin/veilor-installer
|
||||
ExecStart=-/usr/local/bin/veilor-installer
|
||||
StandardInput=tty
|
||||
StandardOutput=tty
|
||||
StandardError=tty
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ Conflicts=sddm.service
|
|||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=no
|
||||
ExecStart=/usr/local/sbin/veilor-firstboot
|
||||
ExecStart=/usr/local/bin/veilor-firstboot
|
||||
StandardInput=tty
|
||||
StandardOutput=tty
|
||||
StandardError=tty
|
||||
|
|
@ -18,4 +18,9 @@ TTYReset=yes
|
|||
TTYVHangup=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
# 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
|
||||
|
|
|
|||
|
|
@ -29,4 +29,4 @@ stop() {
|
|||
return 0
|
||||
}
|
||||
|
||||
process $@
|
||||
process "$@"
|
||||
|
|
|
|||
|
|
@ -28,4 +28,4 @@ stop() {
|
|||
return 0
|
||||
}
|
||||
|
||||
process $@
|
||||
process "$@"
|
||||
|
|
|
|||
|
|
@ -40,4 +40,4 @@ stop() {
|
|||
return 0
|
||||
}
|
||||
|
||||
process $@
|
||||
process "$@"
|
||||
|
|
|
|||
43
overlay/etc/usbguard/rules.conf
Normal file
43
overlay/etc/usbguard/rules.conf
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
# 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.
|
||||
240
overlay/usr/local/bin/veilor-doctor
Executable file
240
overlay/usr/local/bin/veilor-doctor
Executable file
|
|
@ -0,0 +1,240 @@
|
|||
#!/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
|
||||
1151
overlay/usr/local/bin/veilor-installer
Normal file
1151
overlay/usr/local/bin/veilor-installer
Normal file
File diff suppressed because it is too large
Load diff
94
overlay/usr/local/bin/veilor-update
Executable file
94
overlay/usr/local/bin/veilor-update
Executable file
|
|
@ -0,0 +1,94 @@
|
|||
#!/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
|
||||
|
|
@ -1,297 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# veilor-os installer — TUI wrapper around anaconda kickstart install.
|
||||
# Runs on tty1 in place of getty (live ISO boot path).
|
||||
#
|
||||
# Flow:
|
||||
# 1. ASCII banner
|
||||
# 2. Menu: Install / Live shell / Reboot / Power off
|
||||
# 3. If Install: collect answers via whiptail (disk, hostname, LUKS pw,
|
||||
# admin pw, locale)
|
||||
# 4. Generate /run/install/veilor-generated.ks from template + answers
|
||||
# 5. Exec anaconda --kickstart=/run/install/veilor-generated.ks
|
||||
# 6. On finish: reboot into installed system
|
||||
#
|
||||
# v0.5.0 — first cut. v0.5.1 swaps whiptail for gum (Go TUI, prettier).
|
||||
|
||||
set -uo pipefail
|
||||
export TERM="${TERM:-linux}"
|
||||
LOG=/var/log/veilor-installer.log
|
||||
exec > >(tee -a "$LOG") 2>&1
|
||||
|
||||
banner() {
|
||||
clear
|
||||
cat << 'EOF'
|
||||
|
||||
▌ ▌▙▀▖▌ ▐▌▛▀▖▌ ▖▙▀▖▖▖
|
||||
▙▖▌█ ▐▖▟▘▙▄▘▌ ▌▌ ▙▟
|
||||
▘
|
||||
veilor-os installer
|
||||
hardened. branded. yours.
|
||||
|
||||
EOF
|
||||
echo "──────────────────────────────────────────"
|
||||
echo
|
||||
}
|
||||
|
||||
require_tty() {
|
||||
if ! [[ -t 0 && -t 1 ]]; then
|
||||
echo "[ERR] veilor-installer must run on a real tty" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
main_menu() {
|
||||
local choice
|
||||
choice=$(whiptail --title "veilor-os" \
|
||||
--menu "Welcome. What would you like to do?" 16 60 5 \
|
||||
"1" "Install veilor-os to disk" \
|
||||
"2" "Try live — desktop (KDE Plasma)" \
|
||||
"3" "Try live — shell" \
|
||||
"4" "Reboot" \
|
||||
"5" "Power off" \
|
||||
3>&1 1>&2 2>&3)
|
||||
echo "$choice"
|
||||
}
|
||||
|
||||
collect_answers() {
|
||||
local disk hostname luks_pw admin_pw locale
|
||||
local disks_list
|
||||
|
||||
# ── Disk ──
|
||||
# Build "tag description" pairs for whiptail. Model strings have spaces
|
||||
# (e.g. "WD PC SN740"), so collapse model to underscores for menu.
|
||||
disks_list=$(lsblk -dpno NAME,SIZE,MODEL | grep -E '^/dev/(sd|nvme|vd|mmcblk)' | \
|
||||
awk '{name=$1; size=$2; $1=""; $2=""; sub(/^ +/,""); gsub(/ /,"_"); model=$0; if(model=="")model="unknown"; print name, size"_"model}')
|
||||
if [[ -z $disks_list ]]; then
|
||||
whiptail --title "veilor-os" --msgbox "No installable disks found." 8 50
|
||||
return 1
|
||||
fi
|
||||
disk=$(whiptail --title "Select install disk" \
|
||||
--menu "WARNING: selected disk will be ERASED." 18 70 8 \
|
||||
$disks_list 3>&1 1>&2 2>&3) || return 1
|
||||
|
||||
# ── Hostname ──
|
||||
hostname=$(whiptail --title "Hostname" \
|
||||
--inputbox "Set hostname:" 10 60 "veilor" \
|
||||
3>&1 1>&2 2>&3) || return 1
|
||||
|
||||
# Reject shell-special chars in passwords. Generated kickstart writes
|
||||
# them via heredoc + sed substitution; bare $, ", \, ` would corrupt
|
||||
# the ks line or partially expand. 8-char min for entropy.
|
||||
validate_pw() {
|
||||
local pw=$1 label=$2
|
||||
if [[ ${#pw} -lt 8 ]]; then
|
||||
whiptail --title "Weak $label" --msgbox "Min 8 chars." 8 40
|
||||
return 1
|
||||
fi
|
||||
if [[ $pw =~ [\"\$\\\`] ]]; then
|
||||
whiptail --title "Invalid $label" --msgbox \
|
||||
"Cannot contain: \" \$ \\ \`" 8 50
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# ── LUKS passphrase ──
|
||||
luks_pw=$(whiptail --title "Disk encryption" \
|
||||
--passwordbox "LUKS passphrase (full-disk encryption):" 10 60 \
|
||||
3>&1 1>&2 2>&3) || return 1
|
||||
validate_pw "$luks_pw" "passphrase" || return 1
|
||||
|
||||
# ── Admin password ──
|
||||
admin_pw=$(whiptail --title "Admin password" \
|
||||
--passwordbox "Admin user password (login after install):" 10 60 \
|
||||
3>&1 1>&2 2>&3) || return 1
|
||||
validate_pw "$admin_pw" "password" || return 1
|
||||
|
||||
# ── Locale ──
|
||||
locale=$(whiptail --title "Locale" \
|
||||
--menu "Choose locale:" 14 50 4 \
|
||||
"en_GB.UTF-8" "English (UK)" \
|
||||
"en_US.UTF-8" "English (US)" \
|
||||
"de_DE.UTF-8" "Deutsch" \
|
||||
"fr_FR.UTF-8" "Francais" \
|
||||
3>&1 1>&2 2>&3) || return 1
|
||||
|
||||
# ── Confirmation ──
|
||||
whiptail --title "Confirm install" --yesno \
|
||||
"About to install veilor-os:
|
||||
|
||||
Disk: $disk (will be ERASED)
|
||||
Hostname: $hostname
|
||||
Locale: $locale
|
||||
LUKS: set
|
||||
Admin pw: set
|
||||
|
||||
Proceed?" 16 60 || return 1
|
||||
|
||||
# Export to caller via globals
|
||||
SEL_DISK=$disk
|
||||
SEL_HOSTNAME=$hostname
|
||||
SEL_LUKS_PW=$luks_pw
|
||||
SEL_ADMIN_PW=$admin_pw
|
||||
SEL_LOCALE=$locale
|
||||
return 0
|
||||
}
|
||||
|
||||
generate_ks() {
|
||||
# Build kickstart for actual disk install.
|
||||
# NOTE: passwords go in via --plaintext to avoid storing crypted hash
|
||||
# collisions; anaconda hashes per /etc/login.defs at install time.
|
||||
local out=/run/install/veilor-generated.ks
|
||||
local disk_basename
|
||||
disk_basename=$(basename "$SEL_DISK")
|
||||
mkdir -p /run/install
|
||||
# Single-quoted heredoc → no shell expansion. Substitute placeholders
|
||||
# via sed afterwards. Bulletproof against $/`/" in passwords.
|
||||
cat > "$out" << 'KSEOF' || return 1
|
||||
# veilor-os installer-generated kickstart
|
||||
# DO NOT commit this file — secrets inline.
|
||||
url --mirrorlist="https://mirrors.fedoraproject.org/mirrorlist?repo=fedora-43&arch=x86_64"
|
||||
repo --name=fedora --baseurl="https://download.fedoraproject.org/pub/fedora/linux/releases/43/Everything/x86_64/os/" --install
|
||||
repo --name=updates --baseurl="https://download.fedoraproject.org/pub/fedora/linux/updates/43/Everything/x86_64/" --install
|
||||
|
||||
keyboard --xlayouts='us'
|
||||
lang __LOCALE__
|
||||
timezone Europe/London --utc
|
||||
|
||||
firstboot --disable
|
||||
eula --agreed
|
||||
selinux --enforcing
|
||||
services --enabled=sshd,fail2ban,usbguard,tuned,auditd,firewalld,chronyd,sddm
|
||||
|
||||
network --bootproto=dhcp --device=link --activate --hostname=__HOSTNAME__
|
||||
firewall --enabled --service=ssh
|
||||
|
||||
rootpw --lock
|
||||
user --name=admin --groups=wheel --gecos="veilor admin" --password=__ADMIN_PW__ --plaintext
|
||||
|
||||
# Full hardening cmdline (installed system, not live):
|
||||
# --location=none: anaconda auto-places bootloader (UEFI grub2-efi or BIOS).
|
||||
bootloader --location=none --append="lockdown=integrity slab_nomerge init_on_alloc=1 init_on_free=1 randomize_kstack_offset=on vsyscall=none"
|
||||
|
||||
# Disk: zero, LUKS2 (argon2id), btrfs subvolumes
|
||||
zerombr
|
||||
clearpart --all --initlabel --drives=__DISK_BASENAME__
|
||||
part /boot/efi --fstype=efi --size=600
|
||||
part /boot --fstype=ext4 --size=1024
|
||||
part pv.veilor --grow --encrypted --luks-version=luks2 --pbkdf=argon2id --passphrase=__LUKS_PW__
|
||||
volgroup veilor pv.veilor
|
||||
logvol / --vgname=veilor --name=root --fstype=btrfs --size=8192 --grow
|
||||
|
||||
%packages --excludedocs
|
||||
@^kde-desktop-environment
|
||||
@kde-apps
|
||||
@core
|
||||
@hardware-support
|
||||
@standard
|
||||
fail2ban
|
||||
fail2ban-firewalld
|
||||
usbguard
|
||||
usbguard-tools
|
||||
audit
|
||||
policycoreutils-python-utils
|
||||
tuned
|
||||
chrony
|
||||
firewalld
|
||||
plymouth
|
||||
git
|
||||
vim-enhanced
|
||||
tmux
|
||||
htop
|
||||
podman
|
||||
NetworkManager
|
||||
NetworkManager-wifi
|
||||
fontconfig
|
||||
fira-code-fonts
|
||||
zram-generator
|
||||
-abrt*
|
||||
-snapd
|
||||
-kde-connect
|
||||
-mlocate
|
||||
%end
|
||||
|
||||
# Reboot when done
|
||||
reboot
|
||||
KSEOF
|
||||
# Substitute placeholders. Use | as sed delimiter (passwords might
|
||||
# contain /). Forbidden chars in passwords (validated upstream): "$\`
|
||||
# — sed safe.
|
||||
sed -i \
|
||||
-e "s|__LOCALE__|$SEL_LOCALE|" \
|
||||
-e "s|__HOSTNAME__|$SEL_HOSTNAME|" \
|
||||
-e "s|__DISK_BASENAME__|$disk_basename|" \
|
||||
-e "s|__LUKS_PW__|$SEL_LUKS_PW|" \
|
||||
-e "s|__ADMIN_PW__|$SEL_ADMIN_PW|" \
|
||||
"$out"
|
||||
echo "[INFO] generated kickstart at $out"
|
||||
return 0
|
||||
}
|
||||
|
||||
run_install() {
|
||||
whiptail --title "Installing" --infobox \
|
||||
"Installing veilor-os to $SEL_DISK ...
|
||||
This will take 10-30 minutes.
|
||||
Logs: /var/log/veilor-installer.log + /tmp/anaconda.log" 10 60
|
||||
sleep 2
|
||||
# Hand off to anaconda. --kickstart runs unattended.
|
||||
if anaconda --kickstart=/run/install/veilor-generated.ks; then
|
||||
whiptail --title "Done" --msgbox \
|
||||
"Install complete. System will reboot.
|
||||
Remove the install media after shutdown." 10 50
|
||||
sleep 3
|
||||
systemctl reboot
|
||||
else
|
||||
whiptail --title "Install failed" --msgbox \
|
||||
"Anaconda exited non-zero.
|
||||
Logs at /tmp/anaconda.log + /var/log/veilor-installer.log.
|
||||
Press OK to drop to shell." 12 60
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
drop_to_shell() {
|
||||
clear
|
||||
cat << 'EOF'
|
||||
═══════════════════════════════════════════════════
|
||||
veilor-os live shell
|
||||
═══════════════════════════════════════════════════
|
||||
|
||||
You are in a live, in-memory environment.
|
||||
Nothing persists across reboot.
|
||||
|
||||
Re-run the installer: sudo veilor-installer
|
||||
Reboot: sudo systemctl reboot
|
||||
Power off: sudo systemctl poweroff
|
||||
|
||||
EOF
|
||||
exec /bin/bash --login
|
||||
}
|
||||
|
||||
# ── Entry ──
|
||||
require_tty
|
||||
banner
|
||||
|
||||
launch_desktop() {
|
||||
clear
|
||||
echo "Launching KDE Plasma..."
|
||||
sleep 1
|
||||
systemctl isolate graphical.target
|
||||
# systemd-isolate switches target; sddm spawns on tty1.
|
||||
# If user logs out, they come back here. Loop continues.
|
||||
}
|
||||
|
||||
while true; do
|
||||
case "$(main_menu)" in
|
||||
1)
|
||||
if collect_answers && generate_ks; then
|
||||
run_install || continue
|
||||
fi
|
||||
;;
|
||||
2) launch_desktop ;;
|
||||
3) drop_to_shell ;;
|
||||
4) systemctl reboot ;;
|
||||
5) systemctl poweroff ;;
|
||||
*) drop_to_shell ;;
|
||||
esac
|
||||
done
|
||||
|
|
@ -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 default (matches reference system)
|
||||
# veilor-os v0.3 — set veilor-black SDDM theme as system default
|
||||
[Theme]
|
||||
Current=veilor-black
|
||||
CursorTheme=Breeze_Light
|
||||
|
|
@ -128,12 +128,44 @@ else
|
|||
fi
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
# 4. Wallpaper — solid black (matches reference system, no SVG asset)
|
||||
# 4. Wallpaper — pure black (default: org.kde.color plugin; image fallback)
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
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.
|
||||
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.
|
||||
KDD=/etc/xdg/kdedefaults
|
||||
install -d -m 0755 "$KDD"
|
||||
cat > "$KDD/plasma-org.kde.plasma.desktop-appletsrc" << 'EOF'
|
||||
|
|
@ -143,8 +175,11 @@ 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 = solid #000000 (Plasma color plugin)"
|
||||
ok "default wallpaper plugin = org.kde.color (#000000); image fallback wired"
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
# 5. Breeze decoration override
|
||||
|
|
@ -173,7 +208,25 @@ else
|
|||
fi
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
# 6. Sanity: brand leak check (mirrors kickstart %post sanity)
|
||||
# 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)
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
info "Sanity: scanning installed v0.3 paths for brand leaks"
|
||||
LEAK_PATHS=(
|
||||
|
|
@ -185,6 +238,8 @@ 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
|
||||
|
|
|
|||
77
scripts/40-apparmor.sh
Normal file
77
scripts/40-apparmor.sh
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
#!/usr/bin/env bash
|
||||
# veilor-os — 40-apparmor: load veilor-shipped AppArmor profiles in
|
||||
# COMPLAIN mode. v0.6 scope: "loaded, present, nothing breaks".
|
||||
#
|
||||
# Per docs/research/2026-05-05-agent-wave/04-hardening-tier-2.md, v0.6
|
||||
# ships AppArmor stacked alongside SELinux, but every veilor-shipped
|
||||
# profile stays in complain mode (logs only, no enforce). Real policy
|
||||
# authoring is post-v0.6.
|
||||
#
|
||||
# Idempotent: profiles already in complain mode are skipped. Run as
|
||||
# root during kickstart %post or post-install.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; NC='\033[0m'
|
||||
ok() { echo -e "${GREEN}[OK]${NC} $*"; }
|
||||
info() { echo -e "${YELLOW}[INFO]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
err() { echo -e "${RED}[ERR]${NC} $*"; }
|
||||
|
||||
[[ $EUID -eq 0 ]] || { err "Must run as root"; exit 1; }
|
||||
|
||||
echo "════════════════════════════════════════════════════════"
|
||||
echo " veilor-os :: 40-apparmor (complain mode only)"
|
||||
echo "════════════════════════════════════════════════════════"
|
||||
|
||||
PROFILE_DIR=/etc/apparmor.d/veilor.d
|
||||
|
||||
# ── Sanity: tools present? ──
|
||||
if ! command -v apparmor_parser >/dev/null 2>&1; then
|
||||
warn "apparmor_parser not installed — skipping (package step missed?)"
|
||||
exit 0
|
||||
fi
|
||||
if ! command -v aa-complain >/dev/null 2>&1; then
|
||||
warn "aa-complain not installed (apparmor-utils missing) — skipping"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ ! -d $PROFILE_DIR ]]; then
|
||||
info "$PROFILE_DIR not present — no veilor profiles to load"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── Walk every profile we ship and force complain mode ──
|
||||
shopt -s nullglob
|
||||
loaded=0
|
||||
skipped=0
|
||||
failed=0
|
||||
|
||||
for profile in "$PROFILE_DIR"/*; do
|
||||
[[ -f $profile ]] || continue
|
||||
name=$(basename "$profile")
|
||||
|
||||
# Already in complain mode? aa-status reports loaded profiles by
|
||||
# internal profile name, not file path — best-effort match against
|
||||
# the file basename to avoid re-parsing on repeat runs.
|
||||
if command -v aa-status >/dev/null 2>&1 \
|
||||
&& aa-status --complaining 2>/dev/null | grep -qE "(^|/)veilor-${name}([[:space:]]|$)"; then
|
||||
info "$name already in complain mode — skipping"
|
||||
skipped=$((skipped + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
info "loading $name (complain mode)"
|
||||
if aa-complain "$profile" >/dev/null 2>&1; then
|
||||
ok "$name → complain"
|
||||
loaded=$((loaded + 1))
|
||||
else
|
||||
warn "$name failed to load (parser may reject stub on this kernel)"
|
||||
failed=$((failed + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
echo "────────────────────────────────────────────────────────"
|
||||
info "summary: loaded=$loaded skipped=$skipped failed=$failed"
|
||||
ok "v0.6 AppArmor stub: complain-mode only — no enforcement, log-only"
|
||||
exit 0
|
||||
114
scripts/apparmor/usr.bin.thorium
Normal file
114
scripts/apparmor/usr.bin.thorium
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
# 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,
|
||||
}
|
||||
}
|
||||
78
scripts/apparmor/usr.bin.veilor-power
Normal file
78
scripts/apparmor/usr.bin.veilor-power
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
# 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,
|
||||
}
|
||||
}
|
||||
96
scripts/apparmor/usr.local.bin.lm-studio
Normal file
96
scripts/apparmor/usr.local.bin.lm-studio
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
# 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,
|
||||
}
|
||||
|
|
@ -1,10 +1,43 @@
|
|||
#!/usr/bin/env bash
|
||||
# Build + load veilor-systemd SELinux policy module.
|
||||
# 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
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
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"
|
||||
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"
|
||||
|
|
|
|||
136
scripts/selinux/veilor-firstboot.te
Normal file
136
scripts/selinux/veilor-firstboot.te
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
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;
|
||||
85
test/METHOD-CHANGELOG.md
Normal file
85
test/METHOD-CHANGELOG.md
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
# 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)
|
||||
66
test/README.md
Normal file
66
test/README.md
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
# 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: 20–30 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.
|
||||
187
test/TESTING.md
Normal file
187
test/TESTING.md
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
# 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 · 10–30 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.
|
||||
167
test/auto-install-keymap.sh
Executable file
167
test/auto-install-keymap.sh
Executable file
|
|
@ -0,0 +1,167 @@
|
|||
#!/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')"
|
||||
}
|
||||
673
test/auto-install.sh
Executable file
673
test/auto-install.sh
Executable file
|
|
@ -0,0 +1,673 @@
|
|||
#!/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 "$@"
|
||||
191
test/run-vm.sh
191
test/run-vm.sh
|
|
@ -5,6 +5,34 @@
|
|||
# ./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
|
||||
|
||||
|
|
@ -12,6 +40,8 @@ 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; }
|
||||
|
|
@ -28,19 +58,173 @@ fi
|
|||
|
||||
# Reset on FRESH=1
|
||||
if [[ "${FRESH:-0}" == "1" ]]; then
|
||||
rm -f "$DISK" "$NVRAM"
|
||||
rm -f "$DISK" "$NVRAM" "$SEED_ISO"
|
||||
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 \
|
||||
|
|
@ -54,7 +238,10 @@ 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 \
|
||||
-cdrom "$ISO" \
|
||||
-drive file="$ISO",media=cdrom,readonly=on \
|
||||
"${SEED_ARGS[@]}" \
|
||||
"${MONITOR_ARGS[@]}" \
|
||||
"${ANACONDA_LOG_ARGS[@]}" \
|
||||
-boot menu=on,splash-time=2000 \
|
||||
-netdev user,id=net0,hostfwd=tcp::2222-:22 \
|
||||
-device virtio-net-pci,netdev=net0 \
|
||||
|
|
|
|||
142
test/test-runs/2026-05-06-v0.5.32-build.md
Normal file
142
test/test-runs/2026-05-06-v0.5.32-build.md
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
# Test run — v0.5.32
|
||||
|
||||
- **Date:** 2026-05-06
|
||||
- **ISO:** `veilor-os-43-20260506-HHMMSS.iso` (sha256: `TBD — fill in once A1 reports the build artifact`)
|
||||
- **Tester:** A1 (build) + operator (P) + A5 (report scribe)
|
||||
- **Build host:** forgejo-runner on nullstone (runner label `ubuntu-24.04`,
|
||||
image `catthehacker/ubuntu:act-24.04`); first ISO produced off the
|
||||
Forgejo build pipeline after the GH Actions mirror was disabled
|
||||
2026-05-06.
|
||||
- **Environment:** VM (qemu/q35/ovmf, 4 vCPU, 4 GiB RAM, virtio-vga,
|
||||
virtio-9p host log mount). Real-hardware run is a separate report —
|
||||
this file is the VM run only.
|
||||
|
||||
---
|
||||
|
||||
## Result
|
||||
|
||||
⏳ **Pending A1 build.** Operator + A5 fill in pass/fail per-step once
|
||||
the actual VM test is walked through against the v0.5.32 ISO. Until
|
||||
the ISO sha256 lands here, treat every row in the per-step table as
|
||||
unverified.
|
||||
|
||||
One-line summary (write here once known): _TBD_.
|
||||
|
||||
---
|
||||
|
||||
## Regressions vs previous run
|
||||
|
||||
(v0.5.31 was the last tagged release; compare against any pass-with-issues
|
||||
notes from that test run if a report exists. Empty otherwise — fill in
|
||||
during the actual test walkthrough.)
|
||||
|
||||
- _TBD_
|
||||
|
||||
---
|
||||
|
||||
## Per-step results
|
||||
|
||||
Walk `test/TESTING.md` step-by-step. Mark each pass/fail with a brief
|
||||
note when failed. Until the test runs, every row is `⏳ pending`.
|
||||
|
||||
| # | Step | Result | Notes |
|
||||
|----|-----------------------------------|--------|-------|
|
||||
| 1 | Live boot to installer banner | ⏳ pending | |
|
||||
| 2 | Installer menu render | ⏳ pending | |
|
||||
| 3 | Disk picker | ⏳ pending | |
|
||||
| 4 | LUKS + admin passwords | ⏳ pending | Operator types directly into QEMU window — plymouth ignores synthesised keys. |
|
||||
| 5 | Locale | ⏳ pending | |
|
||||
| 6 | Confirm | ⏳ pending | |
|
||||
| 7 | Anaconda transaction | ⏳ pending | |
|
||||
| 8 | Reboot | ⏳ pending | |
|
||||
| 9 | GRUB single veilor-os entry | ⏳ pending | |
|
||||
| 10 | LUKS unlock prompt | ⏳ pending | |
|
||||
| 11 | First boot → SDDM → KDE | ⏳ pending | |
|
||||
| 12 | Hardening checks | ⏳ pending | |
|
||||
|
||||
---
|
||||
|
||||
## Hardening verification
|
||||
|
||||
```text
|
||||
$ getenforce
|
||||
TBD
|
||||
|
||||
$ systemctl is-active fail2ban usbguard tuned auditd firewalld
|
||||
TBD
|
||||
|
||||
$ cat /proc/cmdline
|
||||
TBD — must include rd.luks.uuid=luks-... and the v0.5.32 cmdline set.
|
||||
|
||||
$ lsblk -f
|
||||
TBD
|
||||
|
||||
$ systemctl is-enabled veilor-firstboot.service
|
||||
TBD — must report enabled with WantedBy=graphical.target (blocker #2).
|
||||
|
||||
$ nft list ruleset | grep -i tailscale
|
||||
TBD — tailscale0 must be in the trusted zone (blocker #5).
|
||||
|
||||
$ cat /etc/skel/.config/kdeglobals 2>/dev/null | head
|
||||
TBD — branding must be present (blocker #6).
|
||||
|
||||
$ ls /var/log/anaconda/host-9p-mount/
|
||||
TBD — virtio-9p Anaconda log capture (blocker #7).
|
||||
```
|
||||
|
||||
Paste real output. If any service is inactive, any cmdline arg is
|
||||
missing, or any blocker artifact is absent, raise as a Regression
|
||||
above.
|
||||
|
||||
---
|
||||
|
||||
## Findings
|
||||
|
||||
The 7 v0.5.32 blocker fixes from the
|
||||
[2026-05-05 9-agent research wave](../../docs/research/2026-05-05-agent-wave/README.md)
|
||||
land in this build. Each is listed here as an **expected behaviour**
|
||||
the tester must observe — if any of these regress, log it under
|
||||
Regressions above.
|
||||
|
||||
1. **Suspend/resume wifi survives lid-close.** `kernel.modules_disabled=1`
|
||||
no longer fires before the wifi module reloads on resume. Test:
|
||||
suspend the VM (or lid-close on real HW), wake, reconnect to the
|
||||
same network without manual `modprobe`.
|
||||
2. **`veilor-firstboot.service` is `WantedBy=graphical.target`.** The
|
||||
first-boot admin password flow must run on real installs, not just
|
||||
on multi-user.target boots. Test: fresh install boots straight to
|
||||
the TTY password prompt before SDDM lights up.
|
||||
3. **Kernel-upgrade does not drift GRUB.** First `dnf upgrade kernel`
|
||||
must leave the system bootable — `grub2-mkconfig` is wired into the
|
||||
kernel-install hook. Test: install, run `sudo dnf upgrade kernel`,
|
||||
reboot, system comes up.
|
||||
4. **USBGuard rules are id-based, not hash + parent-hash.** Mirrors the
|
||||
onyx dock-replug fix in `feedback_usbguard_dock.md`. Test:
|
||||
unplug/replug a known device — it stays allowed. The hash variant
|
||||
re-blocks on every replug; the id variant must not.
|
||||
5. **firewalld trusts `tailscale0`.** The interface is in the trusted
|
||||
zone out-of-the-box. Test: bring tailscale up, ping a peer in the
|
||||
mesh — no firewall mods required.
|
||||
6. **`/etc/skel/` carries veilor branding.** New users get the black
|
||||
colour scheme, Konsole profile, and Plasma layout on first login.
|
||||
Test: `useradd test`; log in as `test`; KDE comes up branded, no
|
||||
white flash, Fira Code system font.
|
||||
7. **virtio-9p Anaconda log capture is active by default.**
|
||||
`test/run-vm.sh` mounts a host directory into the VM; Anaconda logs
|
||||
land there during install. Replaces the broken virtio-serial path
|
||||
from earlier runs. Test: run install in VM; host-side mount has
|
||||
`program.log`, `storage.log`, `packaging.log` populated.
|
||||
|
||||
Free-form notes from the actual walkthrough — cosmetic glitches, slow
|
||||
paths, surprising behaviour — append below.
|
||||
|
||||
- _TBD — fill in during the operator-driven VM run._
|
||||
|
||||
---
|
||||
|
||||
## Action items for next release
|
||||
|
||||
(Empty until the test exposes something. PRs / commits opened during
|
||||
the run go here.)
|
||||
|
||||
- [ ] _TBD_
|
||||
80
test/test-runs/_TEMPLATE.md
Normal file
80
test/test-runs/_TEMPLATE.md
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
# 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.)
|
||||
Loading…
Reference in a new issue