Compare commits

..

38 commits

Author SHA1 Message Date
s8n
5c961eba88 Update docs/DOCS-DOCS.md
Some checks failed
Lint / Kickstart syntax (push) Failing after 1s
Lint / Shell scripts (push) Failing after 6s
Lint / No personal/onyx leaks (push) Failing after 3s
2026-05-06 18:03:37 +01:00
obsidian-ai
8c70030d80 docs(ROADMAP): pivot — v0.6 cancelled, v0.7 BlueBuild OCI is mainline
Strategy pivot 2026-05-06: v0.5.32 produced a green ISO on Forgejo
runner. That's the kickstart-path proof point. Continuing v0.6
kickstart polish is sunk-cost work on tooling retired at v1.0.

Pivot:
- v0.5.0 is the FINAL kickstart-path release. Tag, freeze, ship.
- v0.6 cancelled as a milestone. Original plan kept inline as
  HISTORICAL reference.
- v0.7 promoted to primary active milestone. Absorbs the v0.6
  ergonomic CLI tools (veilor-postinstall / veilor-doctor /
  veilor-update) with bootc upgrade replacing dnf upgrade.
- Active branch: v0.7-bluebuild-spike. All future feature work lands
  there, not on main.
2026-05-06 15:55:08 +01:00
obsidian-ai
89c7df0ecc docs: add PROOF-OF-WORK.md — receipts of work, tooling, and decisions
Single document that surfaces the depth of work behind veilor-os:
metrics, distros studied, every tool traversed in the build chain,
all 35+ failure classes hit and beaten, key engineering decisions and
why, what's in the repo beyond the kickstart, and the self-hosted
nullstone CI infrastructure built to support it.

Receipts not narrative — every claim links back to a file path,
commit, error, or config. Useful as portfolio anchor and as a single
read-this-first for anyone returning to the project after a gap.
2026-05-06 15:51:40 +01:00
obsidian-ai
c2b4df8ef9 ci: gate cosign/sbom/attest steps to github only
cosign keyless sign uses Sigstore Fulcio which requires a
Fulcio-trusted OIDC issuer. Forgejo runs don't have one, so cosign
falls back to the interactive device flow and times out
(error obtaining token: expired_token). Same applies to
attest-build-provenance and the SBOM action's signed attestation.

Skip all three on Forgejo for now; ISO + sha256 are sufficient for
v0.5.x test releases. Re-add when we self-host a Sigstore stack or
sign with a key-pair instead of keyless.
2026-05-06 15:41:00 +01:00
obsidian-ai
b9df392fbc docs(README): tone down secureblue credit (no code lifted yet)
We layer on their OCI image as v0.7 base; we don't redistribute their
source. Drop the AGPLv3-attribution prose — that becomes relevant only
if/when we ship a verbatim chunk of their config/policy in our repo.
2026-05-06 15:38:35 +01:00
obsidian-ai
84fa325e46 docs(README): add secureblue column + upstream credit section
secureblue (AGPLv3) is the upstream hardened atomic Fedora that the
v0.7 BlueBuild spike layers on top of. Comparison table now includes
secureblue alongside Kicksecure + stock Fedora KDE. New "Credit &
relationship to secureblue" section spells out where their work
already solves problems we don't need to reinvent (Trivalent,
SELinux policy, kernel cmdline, signed OCI), how veilor-os differs
(kickstart install path + branding + Forgejo CI), and the AGPLv3
attribution rule for any code we lift verbatim.
2026-05-06 15:32:22 +01:00
obsidian-ai
1e4ca2b56b ci: symlink /work -> GITHUB_WORKSPACE for ks %post SRC probe 2026-05-06 14:58:24 +01:00
obsidian-ai
446c602683 ks: drop apparmor-* packages — not in Fedora 43 repos
Build error: 'Failed to find package apparmor-parser : No match for
argument'. Fedora 43 base and updates do not ship AppArmor packages;
the prior comment was incorrect. Defer AppArmor to v0.7 secureblue OCI
hybrid (which has its own LSM stack), or land via COPR overlay later.
2026-05-06 14:48:06 +01:00
obsidian-ai
ac5c29df42 ci: run build directly in Fedora job container, drop addnab nest
forgejo-runner labels nullstone -> fedora:43 image. Switching
runs-on: ubuntu-24.04 -> nullstone makes the job container itself
the build environment, eliminating the docker-in-docker workspace
bind-mount problem (host path != act-container path).

Build now runs as root in fedora:43, installs livecd-tools directly
via dnf, and writes outputs to $GITHUB_WORKSPACE which is the natural
runner workdir on host. No nested docker, no userns juggling, no
explicit -v workspace bind needed.
2026-05-06 14:35:44 +01:00
obsidian-ai
6f4842a75c ci: add /work diagnostic before sed-redirect to surface bind/perm issue 2026-05-06 14:19:13 +01:00
obsidian-ai
4b90e7e00b ci: repin fedora:43 build container to amd64 digest
Prior pin was the arm64 manifest digest (linux/arm64/v8); on x86_64
host it failed with `exec /usr/bin/sh: exec format error`. Pinned to
the amd64 manifest entry from the same fat-manifest.
2026-05-06 14:10:25 +01:00
obsidian-ai
7a0c665cf0 ci: add --userns=host to nested Fedora build container
Forgejo runner on nullstone runs against a daemon with
userns-remap=default. addnab/docker-run-action launches the Fedora 43
build container with --privileged, which is incompatible with
userns-remap unless --userns=host is also set.
2026-05-06 14:07:22 +01:00
obsidian-ai
d38fce4cb8 ci: pin sbom/cosign/attest actions to node20-safe versions
forgejo-runner v6.4.0 ships node20; floating tags @v0/@v3/@v2 now
resolve to actions whose runs.using=node24, which the runner cannot
exec. Pin to last node20-shipping release of each:

- anchore/sbom-action@v0.17.2
- sigstore/cosign-installer@v3.7.0
- actions/attest-build-provenance@v2.2.3
2026-05-06 13:57:49 +01:00
s8n
bc738c1c7b Merge pull request 'ci: gate softprops release steps + add Forgejo API equivalents' (#5) from feat/a1-forgejo-ci-adapt into main 2026-05-06 13:53:02 +01:00
s8n
a3f6c1a1a6 ci: gate softprops release steps + add Forgejo API equivalents
The build-iso workflow used softprops/action-gh-release@v2 unconditionally,
which only speaks the GitHub Releases REST API. When the workflow runs on
the Forgejo runner registered on nullstone, those steps would fail.

Add a server_url check so the GH-only path runs only on github.com, and
mirror it with a curl-based step that hits the Forgejo /api/v1/releases
endpoints. Behaviour:
  - github.com: identical to before (action-gh-release@v2).
  - git.s8n.ru: drop+recreate ci-latest release, upload chunked assets
                via the Forgejo attachments API.

Tag-driven "Attach to release" path mirrored the same way.

Refs: A1 build-eng task — Forgejo runner adaptation.
2026-05-06 13:52:43 +01:00
s8n
356013e1ca Merge pull request 'feat(installer): v0.6 ergonomics + polish — 5 quick wins' (#3) from feat/ux-installer-v06-polish into main 2026-05-06 13:47:35 +01:00
s8n
417acb5585 Merge pull request 'sec: AppArmor v0.6 stub — load profiles in complain mode' (#11) from feat/sec-apparmor-v06-stubs into main 2026-05-06 13:47:31 +01:00
s8n
df574e00f5 Merge pull request 'ci: cosign keyless sigs, SBOM, provenance + fedora digest pin' (#7) from feat/sre-cosign-sbom-attestation into main 2026-05-06 13:47:27 +01:00
s8n
3e660534a1 Merge pull request 'ci: pin actions to node20-safe tags + runner sock pass-through' (#8) from feat/runner-fix-docker-sock-and-node20 into main 2026-05-06 13:47:20 +01:00
s8n
749bcef5b4 Merge pull request 'sec: polish THREAT-MODEL.md for v0.7 public launch' (#10) from feat/sec-threat-model-polish into main 2026-05-06 13:46:58 +01:00
s8n
77ed91ed8e Merge pull request 'docs: test run report skeleton for v0.5.32 (Forgejo build)' (#4) from feat/docs-test-run-v0.5.32 into main 2026-05-06 13:46:12 +01:00
s8n
ad059ec73e docs: README de-GH + Forgejo build status 2026-05-06 13:45:33 +01:00
s8n
05d37f6419 docs: METHOD-CHANGELOG 2026-05-06 forgejo entry 2026-05-06 13:45:29 +01:00
s8n
eafb8b7aa1 sec: AppArmor v0.6 stub — load profiles in complain mode
Per docs/research/2026-05-05-agent-wave/04-hardening-tier-2.md (v0.6
scope item 1).

Adds:
  - apparmor-parser apparmor-utils apparmor-profiles to %packages in
    BOTH kickstart/veilor-os.ks (live ks) and overlay/usr/local/bin/
    veilor-installer (generated install ks heredoc).
  - scripts/40-apparmor.sh — wires aa-complain on every veilor-shipped
    profile. Idempotent. "loaded, present, nothing breaks".
  - overlay/etc/apparmor.d/veilor.d/firefox — 1-liner stub (binary
    confinement marker only; full policy post-v0.6).
  - overlay/etc/apparmor.d/veilor.d/thunderbird — same pattern.
  - Wired 40-apparmor.sh into install %post chain after
    30-apply-v03-theme.sh.

Complain mode means: profiles loaded, kernel logs syscall denials but
does NOT enforce. Operator can review audit.log post-install to
inform v0.7 policy authoring.
2026-05-06 11:15:30 +01:00
s8n
d0738970e0 sec: polish THREAT-MODEL.md for v0.7 public launch
Status flipped Draft → Final.

In-scope rows now cite specific config files / settings (auditable
from clean checkout):
  - LUKS2 params from kickstart/veilor-os.ks
  - sysctl knobs file path
  - USBGuard policy mode + rule type
  - sshd_config drop-in path + every directive
  - auditd rule path + watched paths
  - chrony NTS endpoints
  - systemd-resolved DoT settings
  - bootloader kernel args (lockdown, slab_nomerge, init_on_alloc/free, etc.)

Out-of-scope rows un-hedged. 'May not always' phrasings removed; each
adversary states unambiguously what veilor-os does NOT do.
2026-05-06 11:14:34 +01:00
obsidian-ai
7974ed7a6e ci: pin actions to node20-safe tags + runner sock pass-through
forgejo-runner v6.4.0 ships a node20 javascript engine. v4.2+ of
actions/checkout and v2.0.5+ of softprops/action-gh-release moved to
node24, which the runner refuses to exec. Pin both to last node20
release.

Pairs with a runner-side config change (separately deployed on
nullstone /home/docker/forgejo-runner/conf/config.yaml) that adds
`-v /var/run/docker.sock:/var/run/docker.sock` to per-job container
options + whitelists the socket via valid_volumes — without that
addnab/docker-run-action@v3 inside the catthehacker/ubuntu job
container can't reach the docker engine.

- actions/checkout v4 -> v4.1.7
- softprops/action-gh-release v2 -> v2.0.4
- addnab/docker-run-action v3 unchanged (composite/docker, no node)
- ludeeus/action-shellcheck@master unchanged (docker-based)
2026-05-06 10:50:15 +01:00
veilor-org
f06ee5cc1c chore: gitignore auto-install-vm test artifacts
Test rig was leaking test/auto-install-vm.{nvram,qcow2} into untracked
state. Pattern matches existing veilor-vm.* exclusion.
2026-05-06 10:50:04 +01:00
veilor-org
130f0432dd ci: TODO marker for SHA-pinning third-party actions
Note that all `uses:` directives still resolve to mutable major-
version tags. SHA-pinning is the Agent 8 audit recommendation but
requires per-action web lookups that stalled the previous SRE
attempt; tracked separately so this PR can land first.
2026-05-06 10:41:19 +01:00
veilor-org
08f16bb2ee ci: pin fedora:43 base image to digest
Pin registry.fedoraproject.org/fedora:43 to its current manifest
digest so a malicious or accidental tag-rewrite upstream cannot
silently change the base layer of every CI build. Digest was
captured via `skopeo inspect --raw` on 2026-05-06. Refresh
procedure documented inline.
2026-05-06 10:41:10 +01:00
veilor-org
25b8d30f35 ci: add cosign keyless sigs, SBOM, and provenance attestation
Sign each ISO chunk with cosign keyless OIDC, generate an SPDX SBOM
of the build output, and attach an in-toto build-provenance
attestation. Sigs/certs/SBOM are uploaded alongside the ISO parts in
the ci-latest rolling prerelease so the test/auto-install.sh path
can verify before reassembling.

Action versions are major-version tags (@v3, @v0, @v2). SHA-pinning
is tracked separately to keep this PR small and avoid the long web
lookups that stalled the previous attempt.
2026-05-06 10:40:56 +01:00
veilor-org
aa731f9daa docs: test run report skeleton for v0.5.32 (Forgejo build)
First test-runs/ report off the new template. Records the build host
(forgejo-runner on nullstone, ubuntu-24.04 / catthehacker:act-24.04),
notes that v0.5.32 is the first ISO produced after the GH Actions
mirror was disabled, and pre-populates the Findings section with the
7 v0.5.32 blocker fixes from the 2026-05-05 9-agent wave as expected
behaviours the tester must verify.

Result is left as "pending A1 build" — the operator + A5 fill in
per-step pass/fail and hardening output once the actual VM walkthrough
runs against the produced ISO. This is intentional: the report is the
scaffold; the test is a separate step.
2026-05-06 10:34:06 +01:00
veilor-org
441f7d057f feat(installer): promote eject-media reminder to its own box
In v0.5 the "Remove the install media" reminder was a single line
inside the green success box, and operators on both onyx and the
friend's RTX 4080 rig missed it — rebooted into the live ISO and
re-ran the installer thinking the install had silently failed.

Promote the reminder to its own loud yellow thick-bordered gum-style
box stacked directly below the success/countdown box, with three
lines of explanation. Renders for the full 10s of the countdown so
it stays in the operator's face the entire window.
2026-05-06 10:32:30 +01:00
veilor-org
816fc0ee68 feat(installer): 10s reboot countdown with per-tick redraw
v0.5 used `sleep 5` after a static "System will reboot in 5 seconds."
box, which left the operator guessing how much time was left to grab
the USB stick. The new loop runs 10 → 1, clearing + redrawing the
gum-style success box each tick with the remaining-seconds figure,
giving the operator a visible window to act.

10 seconds (vs 5) because real hardware operators were missing the
window — laptops with the USB on the far side of the dock take
4-5 seconds to physically reach. 10 is comfortable, not annoying.
2026-05-06 10:32:11 +01:00
veilor-org
44f0c787a7 feat(installer): confirm-twice for LUKS passphrase + admin password
A typo in the LUKS passphrase is unrecoverable — the disk is
unmountable without it and we don't escrow the key. Re-prompting
until the two reads match catches keyboard-layout surprises (the
US/UK quote-key position is the most common one) before they brick
the install.

Admin password gets the same treatment for consistency. Less
catastrophic (resettable from a recovery shell) but a mismatch
still locks the user out of their fresh install on first boot.

Loop bails on cancel/ESC and re-prompts on validate_pw failure.
2026-05-06 10:31:21 +01:00
veilor-org
900f5465b3 feat(installer): staged banner reveal at 40ms/line
Read banner.txt line by line with a 40ms sleep between each, then
clear and redraw the bordered gum-style version. 5-line banner ×
40ms = 200ms total reveal — slow enough to land an aesthetic on the
first frame, fast enough that the operator never feels it as lag.

Pure cosmetic; no functional change to the install flow.
2026-05-06 10:31:02 +01:00
veilor-org
63c5e199d9 fix(installer): swap gum input --password for bash read -srp
`gum input --password` corrupts the linux fbcon since v0.5.27 — the
bubbletea screen-restore writes back the previous menu buffer because
the framebuffer terminfo entry lacks `civis/cnorm` cursor-hide
sequences, leaving a duplicate "Install" plus a stray "T" rendered on
top of the password field. The fix is a single termios echo-off via
`read -srp`: no redraw, no glitch, no dependency on gum's TUI layer
for the one screen where it broke.

Header still rendered through `gum style` so visual parity with the
disk picker / confirm box is preserved. Whiptail fallback path
unchanged (passwordbox there has always rendered cleanly).
2026-05-06 10:30:06 +01:00
veilor-org
abb67841f1 docs: STRATEGY.md — primary git host moved to git.s8n.ru (Forgejo)
Self-hosted Forgejo + forgejo-runner on nullstone now primary.
GitHub becomes public mirror (Forgejo push-mirrors every commit
+ every 8h). 0 GH Actions minutes consumed.

Runner labels:
  ubuntu-24.04 — drop-in for existing build-iso.yml workflow
  nullstone    — privileged Fedora 43 (opt-in via runs-on: nullstone)

Deploy artifacts: ~/ai-lab/nullstone-server/forgejo/.

External TODO (parent operator owns):
  - router port-forward 222 → nullstone:222 for public SSH push
  - no-guest@file allowlist update for external web UI access
2026-05-06 02:01:06 +01:00
veilor-org
b86b4f9ec3 v0.5.32: ship 7 blockers from 9-agent wave
Per docs/research/2026-05-05-agent-wave/README.md priority list.
All 7 land together to keep iteration cycles useful — partial fixes
bury the lookahead findings agents already mapped.

## 1. CRITICAL — suspend/resume wifi death (Agent 9, B2)

`veilor-modules-lock.service` runs `kernel.modules_disabled=1` 30s
after graphical.target. iwlwifi/iwlmvm/cfg80211 reload on resume
from S3/S0ix → with modules locked, resume breaks wifi until
reboot. Same architectural class as the LUKS bug — security feature
breaks legitimate kernel state transitions.

The unit already has `ConditionKernelCommandLine=!module.sig_enforce=1`
(self-skip when signed-modules enforcement is on cmdline). Adding
`module.sig_enforce=1` to the kernel cmdline retains the security
property (no unsigned modules) without runtime lock-down → resume
works.

Files: kickstart/veilor-os.ks line 61 + overlay/usr/local/bin/veilor-installer
generated bootloader directive both gain `module.sig_enforce=1`.

## 2. veilor-firstboot.service WantedBy=graphical.target (Agent 2)

Was `WantedBy=multi-user.target` only. Real installs default to
graphical.target so the unit never ran on installed systems — admin
pw stayed at install-time + chage -d 0 expired, SDDM PAM bounced
to chauthtok screen (recoverable but ugly UX).

Now `WantedBy=graphical.target multi-user.target`. Live ISO +
multi-user installs both resolve via this list.

## 3. USBGuard hash → id-based baseline (Agent 9, A3)

Mirrors memory feedback_usbguard_dock.md — onyx had hash+parent-hash
rules that broke on dock replug; we shipped no rules.conf so first
boot blocks the USB keyboard.

Adds overlay/etc/usbguard/rules.conf with HID-class allow rule
(`allow with-interface match-all { 03:*:* }`) — covers every USB
keyboard, mouse, gamepad, fingerprint reader, NFC. Survives dock
replug + kernel-bump vendor renumeration. Mass-storage stays
implicit-block; user explicitly allows post-firstboot via
`ujust veilor-usbguard-enroll` (planned v0.6).

## 4. firewalld trusted zone with tailscale0 pre-bound (Agent 9, D1)

User uses Tailscale daily (memory: project_tailscale_mesh.md).
Default firewalld zone = drop, blocks tailnet traffic on tailscale0.

Adds overlay/etc/firewalld/zones/trusted.xml with
`<interface name="tailscale0"/>`. After `tailscale up` brings the
interface up, NetworkManager dispatcher associates it with the
trusted zone automatically — no user intervention.

Default zone stays drop. Only the tailscale0 interface gets ACCEPT.

## 5. /etc/skel branding (Agent 7)

Was completely empty. Result: per-user KDE config (~/.config/kdeglobals
etc.) pre-empty, so the moment user opened System Settings, KDE wrote
fresh ~/.config/* and silently shadowed our /etc/xdg/kdedefaults/*.
Visual brand evaporated on first click.

Seeds:
  /etc/skel/.config/kdeglobals    (copy of assets/kde/veilor-default.kdeglobals)
  /etc/skel/.config/breezerc      (copy of assets/kde/breezerc)
  /etc/skel/.config/kwinrc        (Plasma 6 wayland defaults: opengl, animspeed=0,
                                    blur off, click-to-focus)
  /etc/skel/.config/konsolerc     (default profile = Veilor)
  /etc/skel/.local/share/konsole/Veilor.profile + .colorscheme

User who opens System Settings now writes against branded baseline,
not against vanilla Breeze.

## 6. KMS modeset args + initramfs keymap (Agents 1 + 9)

Real laptop boot has a 5-15s blank between vt switch and SDDM start
because simpledrm releases before i915/nvidia-drm/amdgpu claim. Plus
non-US users get locked out at LUKS prompt because initramfs ships
en-US keymap by default (RHBZ 1405539, RHBZ 1890085).

Adds to bootloader cmdline (live + installed):
  i915.modeset=1 amdgpu.modeset=1 nvidia-drm.modeset=1
  rd.vconsole.keymap=us

`rd.vconsole.keymap=us` is a placeholder; the v0.6 firstboot keymap
picker will rewrite it from /etc/vconsole.conf. Until then, en-US
users get correct LUKS keyboard; non-US users still need the v0.6
fix (per Agent 1).

## 7. virtio-9p log capture (Agent 6)

The v0.5.30 virtio-serial wiring depends on rsyslog inside the live
ISO (anaconda's setupVirtio writes a rsyslog forward rule), which
the live ks doesn't install — files were 0-byte across three
install runs.

test/run-vm.sh now adds a `-virtfs local,...,mount_tag=hostlogs`
share pointing at `test/test-runs/<timestamp>/`. veilor-installer
runs `_dump_logs_to_host` via EXIT trap that mounts the share at
/mnt/hostlogs and rsyncs /tmp/{anaconda,program,storage,packaging,dnf}.log
+ /var/log/veilor-installer.log + dmesg + journalctl + the generated
ks. Runs on success AND failure AND ^C.

No-op on real hardware (9p tag absent) — VM-only debug.

## Validate

  bash -n overlay/usr/local/bin/veilor-installer  # OK
  ksvalidator kickstart/veilor-os.ks               # clean

## Out-of-scope for v0.5.32 (deferred to v0.6)

Per Agent 1 follow-ups: argon2id retune for slow CPUs, recovery key
generation in firstboot, TPM2/FIDO2 unlock helpers. Per Agent 9
follow-ups: Plasma Wayland fallback X11 install, lid-close handling,
SELinux relabel progress UX. Per Agent 4: AppArmor stack +
nftables preset + audit log shipping CLI.

Per Agent 8 (CI hardening): SHA-pin actions + dependabot + SBOM +
SLSA L3 attestation — separate workflow-only commit.
2026-05-05 15:36:24 +01:00
22 changed files with 85 additions and 1539 deletions

View file

@ -1,265 +0,0 @@
name: Build veilor-os OCI (BlueBuild)
# v0.7 spike — builds the bootable OCI image used by the bootstrap
# kickstart's `ostreecontainer` directive. Runs on the Forgejo
# self-hosted runner (label `nullstone`); GitHub-side cosign/SBOM/
# attest steps are gated off because Forgejo has no Sigstore Fulcio-
# trusted OIDC issuer (see docs/PROOF-OF-WORK.md, build-iso.yml fix).
#
# Reference: https://blue-build.org/how-to/setup-build-action/
on:
push:
branches: [v0.7-bluebuild-spike]
paths:
- 'bluebuild/**'
- 'overlay/**'
- 'assets/**'
- 'scripts/**'
- '.github/workflows/build-bluebuild.yml'
pull_request:
branches: [main, v0.7-bluebuild-spike]
schedule:
# Rebuild weekly so we pick up upstream secureblue + Fedora updates.
- cron: '0 6 * * 1'
workflow_dispatch:
permissions:
contents: read
jobs:
build:
name: Build + push OCI
# nullstone label resolves to veilor-build:43 (fedora43 + nodejs)
# via runner config. Privileged + userns=host + sock pass-through
# already wired in the runner config (see infra/forgejo/).
runs-on: nullstone
timeout-minutes: 60
permissions:
contents: read
packages: write
id-token: write # for GH-only cosign keyless (skipped on Forgejo)
attestations: write
env:
# Forgejo container registry path. PAT in FORGEJO_REGISTRY_TOKEN
# secret has package:write on veilor-org.
FORGEJO_REGISTRY: git.s8n.ru
FORGEJO_IMAGE: git.s8n.ru/veilor-org/veilor-os
OCI_TAG: "43"
# GH parallel target — only used when run on github.com.
GHCR_IMAGE: ghcr.io/${{ github.repository_owner }}/veilor-os
steps:
- name: Checkout
# Pinned to last v4 tag confirmed to ship on node20.
uses: actions/checkout@v4.1.7
- name: Fix sudo perms (userns=host artefact)
run: |
# Daemon has userns-remap=default; the act job container is
# launched with --userns=host. The image was pulled under
# remap so /etc/sudo.conf + /etc/sudoers ship as uid 100000.
# sudo refuses to read either unless owned by uid 0. Restore.
chown -R 0:0 /etc/sudo.conf /etc/sudoers /etc/sudoers.d 2>/dev/null || true
ls -la /etc/sudo.conf /etc/sudoers 2>&1 | head -5
- name: Install build tooling (Fedora)
run: |
set -euxo pipefail
dnf -y upgrade --refresh
# veilor-build:43 already ships git, curl, tar, sudo, nodejs.
# cosign is not packaged in Fedora 43; we install it from the
# upstream release tarball below in a separate step.
dnf -y install --skip-unavailable \
podman \
buildah \
skopeo \
jq
# blue-build/github-action shells out to `docker`; Fedora ships
# podman. Symlink so the action finds the CLI.
if ! command -v docker >/dev/null; then
ln -sf "$(command -v podman)" /usr/local/bin/docker
docker --version
fi
- name: Install cosign binary (upstream release)
run: |
set -euxo pipefail
# Fedora 43 has no cosign rpm. Pull static x86_64 binary
# from sigstore/cosign GitHub releases. Pinned to v2.4.1.
COSIGN_VERSION="2.4.1"
curl -fsSL \
"https://github.com/sigstore/cosign/releases/download/v${COSIGN_VERSION}/cosign-linux-amd64" \
-o /usr/local/bin/cosign
chmod +x /usr/local/bin/cosign
cosign version
- name: Pre-pull secureblue base image
env:
GHCR_PULL_TOKEN: ${{ secrets.GHCR_PULL_TOKEN }}
run: |
set -euxo pipefail
# GHCR rate-limits anonymous CI pulls (403 on bearer-token).
# Login with a read-only PAT (forgejo secret GHCR_PULL_TOKEN)
# so bluebuild's buildah inside the CLI container also sees a
# valid auth.json via shared storage bind-mount below.
if [ -n "${GHCR_PULL_TOKEN:-}" ]; then
echo "$GHCR_PULL_TOKEN" | podman login \
--username s8n-ru \
--password-stdin ghcr.io
else
echo "[WARN] GHCR_PULL_TOKEN secret empty; trying anonymous pull"
fi
podman pull ghcr.io/secureblue/kinoite-main-hardened:latest
- name: Stage cosign private key for signing module
env:
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
run: |
set -euo pipefail
if [ -z "${COSIGN_PRIVATE_KEY:-}" ]; then
echo "[ERR] COSIGN_PRIVATE_KEY secret missing"
exit 1
fi
# bluebuild signing module reads from this env var when
# building the cosign.key bind stage. Also write to bluebuild/
# so it sits next to cosign.pub for local reproducible runs.
mkdir -p bluebuild
printf '%s' "$COSIGN_PRIVATE_KEY" > bluebuild/cosign.key
chmod 600 bluebuild/cosign.key
# bluebuild's generated Containerfile uses `FROM scratch as
# stage-keys; COPY cosign.pub /keys/`. Buildah's build context
# is the cwd ($PWD) — symlink the keys to repo root so COPY
# finds them there too.
ln -sf bluebuild/cosign.pub cosign.pub
ln -sf bluebuild/cosign.key cosign.key
ls -la cosign.pub cosign.key 2>&1 | head -4
- name: Build OCI image with BlueBuild CLI container
id: bluebuild
# blue-build/github-action requires docker buildx which podman
# doesn't ship. Run the official BlueBuild CLI container with
# buildah driver instead — works against rootless or rootful
# podman, no docker dependency.
run: |
set -euxo pipefail
# Pull cli image; pinned to v0.9.x at action time.
podman pull ghcr.io/blue-build/cli:latest
# Mount the repo + podman socket; build with buildah driver.
# Bind host /var/lib/containers/storage into the bluebuild
# CLI container so buildah inside it can see the pre-pulled
# secureblue base layer (avoids GHCR auth round-trip during
# templating).
# podman login writes to $XDG_RUNTIME_DIR/containers/auth.json
# by default, which is volatile. Find it + copy to a stable
# path that we then bind into the bluebuild container.
AUTH_SRC=""
for cand in \
"${XDG_RUNTIME_DIR:-/run/user/0}/containers/auth.json" \
"/run/containers/0/auth.json" \
"/root/.config/containers/auth.json" \
"/root/.docker/config.json"; do
if [ -f "$cand" ]; then AUTH_SRC="$cand"; break; fi
done
if [ -z "$AUTH_SRC" ]; then
echo "[ERR] no podman/docker auth.json found post-login"
find / -name auth.json -o -name 'config.json' 2>/dev/null | head -10
exit 1
fi
mkdir -p /root/.config/containers
cp "$AUTH_SRC" /root/.config/containers/auth.json
ls -la /root/.config/containers/auth.json
# Diagnostic: confirm the keypair landed where bluebuild expects.
ls -la bluebuild/
head -1 bluebuild/cosign.pub
head -1 bluebuild/cosign.key | cut -c1-30
podman run --rm \
--privileged \
--security-opt label=disable \
--security-opt seccomp=unconfined \
--entrypoint /usr/bin/bluebuild \
-v "$PWD:/work" \
-v /var/lib/containers/storage:/var/lib/containers/storage \
-v /root/.config/containers/auth.json:/root/.config/containers/auth.json:ro \
-w /work \
-e BB_BUILD_DRIVER=buildah \
ghcr.io/blue-build/cli:latest \
build \
--build-driver buildah \
-vv \
bluebuild/recipe.yml
# bluebuild CLI tags as <recipe-name>:<tag> in local podman
# storage. List + verify, then re-tag for the registries.
podman images
podman tag localhost/veilor-os:latest "${FORGEJO_IMAGE}:${OCI_TAG}" || true
podman tag localhost/veilor-os:latest "${FORGEJO_IMAGE}:latest" || true
- name: Push to Forgejo registry (primary)
if: success() && github.event_name != 'pull_request' && github.server_url != 'https://github.com'
env:
FORGEJO_REGISTRY_TOKEN: ${{ secrets.FORGEJO_REGISTRY_TOKEN }}
FORGEJO_REGISTRY_USER: ${{ secrets.FORGEJO_REGISTRY_USER }}
run: |
set -euo pipefail
if [ -z "${FORGEJO_REGISTRY_TOKEN:-}" ]; then
echo "[WARN] FORGEJO_REGISTRY_TOKEN secret is empty; skipping push"
exit 0
fi
echo "$FORGEJO_REGISTRY_TOKEN" | podman login \
--username "${FORGEJO_REGISTRY_USER:-veilor-org}" \
--password-stdin "$FORGEJO_REGISTRY"
podman push "${FORGEJO_IMAGE}:${OCI_TAG}"
podman push "${FORGEJO_IMAGE}:latest"
echo "[OK] pushed ${FORGEJO_IMAGE}:{${OCI_TAG},latest}"
- name: Push to GHCR (mirror, GitHub-only)
if: success() && github.event_name != 'pull_request' && github.server_url == 'https://github.com'
run: |
set -euo pipefail
podman tag localhost/veilor-os:latest "${GHCR_IMAGE}:${OCI_TAG}"
podman tag localhost/veilor-os:latest "${GHCR_IMAGE}:latest"
echo "${{ secrets.GITHUB_TOKEN }}" | podman login \
--username "${{ github.repository_owner }}" \
--password-stdin ghcr.io
podman push "${GHCR_IMAGE}:${OCI_TAG}"
podman push "${GHCR_IMAGE}:latest"
- name: Smoke-test OCI image
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
run: |
set -euxo pipefail
podman run --rm "localhost/veilor-os:latest" /bin/bash -c '
set -e
echo "-- os-release"
head -5 /etc/os-release
echo "-- sudo present"; which sudo
echo "-- mullvad-browser path"; rpm -q mullvad-browser || echo "not installed"
echo "-- yggdrasil"; rpm -q yggdrasil || echo "not installed"
echo "-- tailscale"; rpm -q tailscale || echo "not installed"
echo "-- veilor-firstboot unit"; ls -la /etc/systemd/system/veilor-firstboot.service 2>&1 || true
'
# ── GitHub-only signing/SBOM/attest ────────────────────────────
# cosign keyless needs Sigstore Fulcio-trusted OIDC. Forgejo
# has none, so these are GH-only. v0.7+ TODO: cosign key-pair
# signing for Forgejo using a stored secret.
- name: SBOM (SPDX, GitHub-only)
if: github.event_name == 'push' && github.server_url == 'https://github.com'
# Pinned to last v0.17 release that ships node20.
uses: anchore/sbom-action@v0.17.2
with:
image: ${{ env.GHCR_IMAGE }}:${{ env.OCI_TAG }}
format: spdx-json
output-file: veilor-os-oci.spdx.json
- name: Build provenance attestation (GitHub-only)
if: github.event_name == 'push' && 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-name: ${{ env.GHCR_IMAGE }}
subject-digest: ${{ steps.bluebuild.outputs.digest }}

View file

@ -1,167 +0,0 @@
name: Build veilor-os Installer ISO
# v0.7+ — produces a small Anaconda installer ISO that consumes
# kickstart/install-ostreecontainer-installer.ks. The ISO boots
# Anaconda, asks for LUKS pw + admin pw interactively, then
# `ostreecontainer` populates / from the v0.7 OCI image at
# ghcr.io/veilor-org/veilor-os:43.
on:
push:
branches: [v0.7-bluebuild-spike]
paths:
- 'kickstart/install-ostreecontainer.ks'
- 'kickstart/install-ostreecontainer-installer.ks'
- 'bluebuild/recipe.yml'
- '.github/workflows/build-installer-iso.yml'
workflow_dispatch:
inputs:
releasever:
description: 'Fedora release version'
required: false
default: '43'
permissions:
contents: write # needed to create+update installer-latest release
jobs:
build:
name: Build installer ISO
runs-on: nullstone
timeout-minutes: 120
env:
RELEASEVER: ${{ github.event.inputs.releasever || '43' }}
steps:
- name: Checkout
uses: actions/checkout@v4.1.7
- name: Install build tooling (Fedora)
run: |
set -euxo pipefail
dnf -y upgrade --refresh
dnf -y install --skip-unavailable \
lorax \
pykickstart \
anaconda-tui \
syslinux \
xorriso \
grub2-efi-x64 \
grub2-efi-x64-modules \
grub2-pc \
grub2-pc-modules \
shim-x64 \
efibootmgr
- name: Validate installer kickstart
run: |
set -euxo pipefail
ksvalidator kickstart/install-ostreecontainer-installer.ks
- name: Build installer ISO with livemedia-creator
run: |
set -euxo pipefail
# livemedia-creator refuses an existing non-empty resultdir.
rm -rf build/out
mkdir -p /var/lmc
ln -sfn "$GITHUB_WORKSPACE" /work
# livemedia-creator does NOT support --title (that's livecd-creator).
# --volid replaces it for the ISO volume label.
livemedia-creator \
--make-iso \
--no-virt \
--ks kickstart/install-ostreecontainer-installer.ks \
--resultdir build/out \
--tmp /var/lmc \
--volid "veilor-os-installer-${RELEASEVER}" \
--project "veilor-os" \
--releasever "$RELEASEVER" \
--logfile build/out/build.log \
2>&1 | tee -a build/out/build.log
- name: Rename ISO + sha256
run: |
set -euxo pipefail
ISO_FILE=$(ls build/out/*.iso 2>/dev/null | head -1)
[ -n "$ISO_FILE" ] || { echo "[ERR] no ISO produced"; exit 1; }
ISO_NAME="veilor-os-installer-${RELEASEVER}-$(date +%Y%m%d-%H%M%S).iso"
mv "$ISO_FILE" "build/out/$ISO_NAME"
cd build/out
sha256sum "$ISO_NAME" > "$ISO_NAME.sha256"
ls -lh "$ISO_NAME"
- name: Split ISO into 1900M chunks
if: success() && github.ref == 'refs/heads/v0.7-bluebuild-spike'
run: |
set -euo pipefail
cd build/out
ISO=$(ls *.iso | head -1)
[ -n "$ISO" ] || { echo "[ERR] no ISO"; exit 1; }
split -b 1900M -d --suffix-length=2 "$ISO" "${ISO}.part-"
rm -f "$ISO"
sha256sum *.part-* > "${ISO}.parts.sha256"
ls "${ISO}".part-*
- name: Publish to installer-latest rolling prerelease (Forgejo)
if: success() && github.ref == 'refs/heads/v0.7-bluebuild-spike' && 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="installer-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
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 v0.7-bluebuild-spike. Latest commit: ${GIT_SHA}.
Installer ISO — boots Anaconda, prompts for LUKS pw + admin pw,
then ostreecontainer-pulls / from ghcr.io/veilor-org/veilor-os:43.
Reassemble:
cat veilor-os-installer-*.iso.part-* > veilor-os-installer.iso
sha256sum -c veilor-os-installer-*.iso.parts.sha256
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': 'v0.7-bluebuild-spike',
'name': 'installer-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 release"; exit 1; }
cd build/out
for f in *.iso.part-* *.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
- name: Print build log on failure
if: failure()
run: |
echo "─── build/out/build.log ───"
tail -200 build/out/build.log 2>/dev/null || echo "(no build.log)"
find build/out -name 'program.log' -exec tail -100 {} \; 2>/dev/null || true
find /var/lmc -name '*.log' -exec tail -50 {} \; 2>/dev/null || true

View file

@ -1,122 +0,0 @@
name: Smoke-test veilor-os OCI
# Pulls git.s8n.ru/veilor-org/veilor-os:43 and asserts that the image
# contains the veilor brand + the v0.5.x hardening overlay + the v0.7
# CLI tools, and that cosign verifies it against bluebuild/cosign.pub.
on:
workflow_run:
workflows: ["Build veilor-os OCI (BlueBuild)"]
types: [completed]
workflow_dispatch:
permissions:
contents: read
jobs:
smoke:
name: OCI smoke test
runs-on: nullstone
if: >-
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'workflow_run' &&
github.event.workflow_run.conclusion == 'success')
timeout-minutes: 20
env:
IMAGE: git.s8n.ru/veilor-org/veilor-os:43
steps:
- name: Checkout
uses: actions/checkout@v4.1.7
- name: Fix sudo perms
run: chown -R 0:0 /etc/sudo.conf /etc/sudoers /etc/sudoers.d 2>/dev/null || true
- name: Install podman + cosign
run: |
set -euxo pipefail
command -v podman >/dev/null || dnf -y install --skip-unavailable podman
if ! command -v cosign >/dev/null 2>&1; then
curl -fsSL "https://github.com/sigstore/cosign/releases/download/v2.4.1/cosign-linux-amd64" \
-o /usr/local/bin/cosign
chmod +x /usr/local/bin/cosign
fi
podman --version
cosign version
- name: Login + pull OCI image
env:
FORGEJO_REGISTRY_TOKEN: ${{ secrets.FORGEJO_REGISTRY_TOKEN }}
FORGEJO_REGISTRY_USER: ${{ secrets.FORGEJO_REGISTRY_USER }}
run: |
set -euxo pipefail
if [ -n "${FORGEJO_REGISTRY_TOKEN:-}" ]; then
echo "$FORGEJO_REGISTRY_TOKEN" | podman login \
--username "${FORGEJO_REGISTRY_USER:-veilor-org}" \
--password-stdin git.s8n.ru
fi
podman pull "${IMAGE}"
- name: Verify cosign signature
run: |
set -euo pipefail
[ -f bluebuild/cosign.pub ] || { echo "[ERR] bluebuild/cosign.pub missing"; exit 1; }
cosign verify --key bluebuild/cosign.pub "${IMAGE}" 2>&1 | tail -10
- name: Run OCI assertions
run: |
set -uo pipefail
PASS=0; FAIL=0; ERRORS=""
pass() { echo "[PASS] $1"; PASS=$((PASS+1)); }
fail() { echo "[FAIL] $1"; FAIL=$((FAIL+1)); ERRORS="${ERRORS} - $1\n"; }
img() { podman run --rm "${IMAGE}" /bin/bash -c "$1" 2>/dev/null; }
OS=$(img 'cat /etc/os-release 2>/dev/null')
echo "$OS" | grep -q 'ID=veilor' && pass "ID=veilor" || fail "ID=veilor missing"
echo "$OS" | grep -q 'NAME="veilor-os"' && pass 'NAME="veilor-os"' || fail 'NAME="veilor-os" missing'
img 'which sudo' >/dev/null && pass "sudo present" || fail "sudo missing"
img 'rpm -q mullvad-browser' >/dev/null && pass "mullvad-browser present" || fail "mullvad-browser missing"
img 'rpm -q tailscale' >/dev/null && pass "tailscale present" || fail "tailscale missing"
img 'rpm -q yggdrasil' >/dev/null && pass "yggdrasil present" || fail "yggdrasil missing"
if img 'grep -qi "^SELINUX=enforcing" /etc/selinux/config'; then
pass "SELinux config = enforcing"
else
fail "SELinux not enforcing"
fi
img 'test -e /etc/systemd/system/multi-user.target.wants/veilor-firstboot.service \
-o -e /etc/systemd/system/graphical.target.wants/veilor-firstboot.service' >/dev/null \
&& pass "veilor-firstboot enabled" || fail "veilor-firstboot not enabled"
img 'test -e /etc/systemd/system/multi-user.target.wants/veilor-postinstall.service \
-o -e /etc/systemd/system/graphical.target.wants/veilor-postinstall.service' >/dev/null \
&& pass "veilor-postinstall enabled" || fail "veilor-postinstall not enabled"
for b in veilor-power veilor-update veilor-doctor veilor-postinstall; do
img "test -x /usr/local/bin/${b}" >/dev/null \
&& pass "${b} executable" || fail "${b} missing"
done
if img 'ls /usr/share/veilor-os/scripts/ 2>/dev/null' | grep -qE '(10-harden|20-harden|30-apply)'; then
pass "/usr/share/veilor-os/scripts populated"
else
fail "/usr/share/veilor-os/scripts missing"
fi
LEAKS=$(img "grep -rIni 'onyx\|192\.168\.0\.\|fedora\.local\|xynki\.dev' /etc/veilor* /usr/share/veilor-os 2>/dev/null")
if [ -z "$LEAKS" ]; then
pass "no brand leaks"
else
fail "brand leaks found"
echo "$LEAKS"
fi
echo
echo "═══ ${PASS} passed, ${FAIL} failed ═══"
if [ "$FAIL" -gt 0 ]; then
printf "%b" "$ERRORS"
exit 1
fi
echo "✓ veilor-os:43 smoke test passed"

1
.gitignore vendored
View file

@ -16,4 +16,3 @@ test/veilor-vm.nvram*
test/auto-install-vm.qcow2
test/auto-install-vm.nvram*
.claude/worktrees/
**/cosign.key

View file

@ -11,18 +11,6 @@ future maintainers can see why a change exists, not just what it changes.
## [Unreleased]
### v0.7 BlueBuild OCI spike (active)
- Promote `v0.7-bluebuild-spike` to active mainline; v0.6 cancelled.
- Port `build-bluebuild.yml` to the Forgejo runner (`runs-on: nullstone`):
install BlueBuild CLI in-job, push to `git.s8n.ru/veilor-org/veilor-os`,
gate cosign keyless / SBOM / attest steps to GitHub-only.
- Atomic CLI tools: `veilor-update` rewritten on `bootc upgrade`,
new `veilor-postinstall` first-login TUI, `veilor-doctor` learns
`bootc status --json` while keeping the legacy dnf path.
- Docs: `docs/INSTALL-V07.md`, `docs/STRATEGY.md` PIVOT EXECUTION
section, README quick-install rewritten for v0.7.
### Planned
- v0.3 polish — Plymouth black theme, SDDM theme, Konsole profile,

View file

@ -48,46 +48,26 @@ spike at v0.7**, **bootc-only at v1.0**.
---
## Quick install — v0.7+ (recommended, atomic / OCI)
## Quick install
```bash
# 1. Download the bootstrap installer ISO from Forgejo.
# 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 — triple-check.
# 2. Flash to USB. Replace /dev/sdX with your USB device — triple-check.
sudo dd if=veilor-os-43-*.iso of=/dev/sdX bs=4M status=progress conv=fsync
sync
# 3. Boot from USB. Anaconda asks for LUKS passphrase + admin password.
# Anaconda then runs `ostreecontainer --url=git.s8n.ru/veilor-org/veilor-os:43`
# which populates / from the signed BlueBuild OCI image.
# 4. Reboot. Log in as `admin`. The first-login TUI (veilor-postinstall)
# asks for the small set of decisions we defer from install:
# keyboard, locale, hostname, GPU drivers, package presets,
# bluetooth, USBGuard policy snapshot. Each step skippable.
# 5. Day-to-day: `sudo veilor-update` (atomic, A/B, instant rollback).
# 3. Boot from USB, pick "Install veilor-os" from the menu.
# 4. Set a strong LUKS passphrase — the only prompt during install.
# 5. Reboot, remove USB.
# 6. On first boot: TTY prompts for an admin password (≥14 chars, mixed case,
# digit, symbol). Once accepted, SDDM starts. Log in as `admin`.
```
Full v0.7 walkthrough: [docs/INSTALL-V07.md](docs/INSTALL-V07.md).
---
### Legacy v0.5.0 install (kickstart-flat path)
The kickstart-installed v0.5.0 ISO ships as a frozen proof-of-work
release. Same hardening, no bootc/rpm-ostree atomic layer. Updates
go through `dnf upgrade` instead of `bootc upgrade`.
```bash
# Same flash + boot, then pick "Install veilor-os".
# Single LUKS passphrase prompt during install; admin password set
# on first boot via TTY.
```
Walkthrough: [docs/INSTALL.md](docs/INSTALL.md).
Full install + first-boot walkthrough: [docs/INSTALL.md](docs/INSTALL.md).
---
@ -167,7 +147,7 @@ clean, locked down, with no manual post-install hardening required.
[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
`kinoite-main-hardened` OCI image as its base — we don't
`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.

View file

@ -1,96 +0,0 @@
# bluebuild/ — v0.7 spike
This directory contains the BlueBuild recipe + supporting config that
builds the veilor-os bootable OCI image. **Active on the
`v0.7-bluebuild-spike` branch only.** Does NOT land in v0.5.x main
until the spike passes its success criteria (see
`docs/STRATEGY.md`).
## What's here
```
bluebuild/
├── recipe.yml # primary BlueBuild recipe
├── config/
│ └── just/
│ └── 60-veilor.just # ujust recipes for opt-in components
└── README.md # this file
```
The recipe extends
`ghcr.io/secureblue/kinoite-main-hardened:latest`. We
inherit secureblue's hardening (sysctl + kargs + custom SELinux
policy + USBGuard + hardened-malloc + Unbound DoT + chronyd NTS +
Trivalent browser + cosign-signed image chain). On top, we layer:
- veilor branding (overlay/, theme, plymouth, sddm, os-release)
- mullvad-browser (anti-fingerprint companion to Trivalent)
- xorg-x11-server-Xwayland (re-enable; secureblue disables it)
- sudo (re-enable; secureblue replaces with run0)
- tailscale + yggdrasil (mesh stack layer 1 + 2)
- ujust recipes for Reticulum (mesh layer 3) + Thorium (opt-in browser)
Trivalent stays as the default browser (correcting an earlier draft).
## Build locally
```bash
# Requires bluebuild CLI:
# curl -fsSL https://raw.githubusercontent.com/blue-build/cli/main/install.sh | sh
cd bluebuild
bluebuild build recipe.yml
```
Output: `localhost/veilor-os:43` in podman storage. Push to GHCR
via the workflow.
## Test the OCI image
```bash
# Smoke-test (boots into the rootfs; no kernel, no init):
podman run --rm -it ghcr.io/veilor-org/veilor-os:43 /bin/bash
# Inside, sanity:
cat /etc/os-release # PRETTY_NAME=veilor-os
which sudo # /usr/bin/sudo (re-enabled)
which trivalent # secureblue's COPR (default browser)
which mullvad-browser # /usr/bin/mullvad-browser
systemctl is-enabled yggdrasil # enabled (idle)
systemctl is-enabled tailscaled # disabled (awaits ujust veilor-mesh-join)
```
## Test the installer ISO
The installer ISO is built separately by livecd-creator (current path)
or bootc-image-builder (v1.0+). Its kickstart's `%packages` block is
replaced with:
```
ostreecontainer --url=ghcr.io/veilor-org/veilor-os:43 --transport=registry
```
That populates the target's `/` directly from this OCI image during
the install pass. No first-boot rebase. No transition window.
## Spike success criteria (1 day)
- [ ] `bluebuild build recipe.yml` exits 0
- [ ] `bootc container lint` exits 0 on the resulting image
- [ ] `podman run` smoke-test (commands above) all pass
- [ ] `.github/workflows/build-bluebuild.yml` builds + cosign-signs +
pushes to `ghcr.io/veilor-org/veilor-os:43`
- [ ] An installer ISO using `ostreecontainer` against this OCI
reaches SDDM with admin login on first boot
If all five land, merge `v0.7-bluebuild-spike``main` as v0.7.0.
If any fail in ways that aren't trivially fixable, file each as a GH
issue + return to v0.5.x kickstart path.
## See also
- `docs/STRATEGY.md` — the strategic decision + override list
- `docs/ROADMAP.md` v0.7 — full schedule
- `docs/THREAT-MODEL.md` — what we publish before launch
- secureblue: <https://github.com/secureblue/secureblue>
- BlueBuild: <https://blue-build.org>
- bootc / ostreecontainer: <https://docs.fedoraproject.org/en-US/bootc/>

View file

@ -1,73 +0,0 @@
# veilor-os ujust recipes — opt-in components
# Loaded into /usr/share/ublue-os/just/ at image build time;
# `ujust install-X` discovers + dispatches.
# install Reticulum / RetiNet AGPL fork + Sideband (mesh layer 3)
install-reticulum:
#!/usr/bin/env bash
echo "═══ Reticulum (RetiNet AGPL fork) install ═══"
echo
echo "Installs RetiNet (AGPL fork — NOT upstream RNS due to anti-AI"
echo "license) plus Sideband messenger. Default config: AutoInterface"
echo "(LAN multicast) + 1-2 TCP backbone peers. RNode hardware (LoRa"
echo "transceiver) is a separate install."
echo
read -p "Proceed? [y/N]: " confirm
if [[ "$confirm" != "y" ]]; then echo "Cancelled."; exit 0; fi
rpm-ostree install python3-pip
pip install --user retinet sideband-cli
echo
echo "Done. To attach an RNode (LoRa transceiver), run:"
echo " ujust install-reticulum-rnode"
# install Reticulum RNode hardware support (LoRa transceiver)
install-reticulum-rnode:
#!/usr/bin/env bash
echo "═══ RNode (LoRa transceiver) hardware install ═══"
echo
echo "Adds RNode firmware-update tooling + udev rules for the LoRa"
echo "USB hardware. Required only if you have an RNode device."
echo
read -p "Proceed? [y/N]: " confirm
if [[ "$confirm" != "y" ]]; then echo "Cancelled."; exit 0; fi
pip install --user rnodeconf
echo "Done. Plug in your RNode via USB; it will appear as a serial device."
# install Thorium browser (OPT-IN, with explicit CVE-lag warning)
install-thorium:
#!/usr/bin/env bash
echo "═══ Thorium browser install ═══"
echo
echo "WARNING: Thorium is a perf/media-focused fork of Chromium that"
echo "uses LTS Chromium as its base. As of 2026-05 it lags upstream"
echo "stable by ~9 milestones (months of CVE backlog)."
echo
echo "veilor-os ships Trivalent (secureblue's hardened Chromium fork,"
echo "tracking upstream M147+ within hours) as the default browser."
echo "Thorium is provided as an OPT-IN profile for users who"
echo "explicitly need its perf characteristics (e.g. WebGL games,"
echo "media decode profiles)."
echo
echo "DO NOT use Thorium as your daily-driver browser. Use Trivalent"
echo "or Mullvad Browser for that."
echo
read -p "Acknowledge CVE-lag risk and continue? [y/N]: " confirm
if [[ "$confirm" != "y" ]]; then echo "Cancelled."; exit 0; fi
flatpak install --user -y org.thorium.Thorium 2>/dev/null || \
rpm-ostree install thorium-browser
echo "Done. Launch via Plasma menu or `flatpak run org.thorium.Thorium`."
# join the veilor mesh (Tailscale via Headscale)
veilor-mesh-join:
#!/usr/bin/env bash
echo "═══ Join veilor mesh (Tailscale via Headscale) ═══"
echo
echo "Pre-auth keys are minted by the Misskey signup page at"
echo "x.veilor (TTL 24h, single-use). You can paste the hex key"
echo "directly OR scan the QR code shown after signup."
echo
read -p "Hex key (paste): " preauth
if [[ -z "$preauth" ]]; then echo "Empty key. Cancelled."; exit 0; fi
sudo systemctl enable --now tailscaled
sudo tailscale up --login-server=https://hs.s8n.ru --auth-key="$preauth"
echo "Done. Status: $(sudo tailscale status | head -1)"

View file

@ -1,4 +0,0 @@
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE5xQcyP7FHNSiG7+VLsN2ViWlvvIB
FYmu2XmPah7/VBlmuQ88H0ZbqCqqnS2u9x5+P1OMaMK+//k89V0Blrx65Q==
-----END PUBLIC KEY-----

View file

@ -1,155 +0,0 @@
# veilor-os — BlueBuild recipe (v0.7 spike, 1-day target)
#
# Extends secureblue's hardened Kinoite OCI image with veilor branding,
# threat-model-driven UX choices, and the three-layer mesh stack
# (Tailscale + Yggdrasil + opt-in Reticulum). This is the OCI image
# that the v0.7+ kickstart's `ostreecontainer` directive pulls into
# the target root during the install pass.
#
# Build: bluebuild build recipe.yml
# Test: podman run --rm -it ghcr.io/veilor-org/veilor-os:43 /bin/bash
# CI: .github/workflows/build-bluebuild.yml signs + pushes to GHCR.
#
# Reference: https://blue-build.org/reference/recipe/
---
name: veilor-os
description: Hardened security-branded Fedora KDE on top of secureblue.
# Base image: secureblue's hardened Kinoite variant with userns sandboxing.
# That brings in: sysctl + kargs + custom SELinux policy + USBGuard +
# hardened-malloc + Unbound DoT + chronyd NTS + Trivalent browser.
base-image: ghcr.io/secureblue/kinoite-main-hardened
image-version: latest
modules:
# ── 1. veilor branding overlay ──────────────────────────────────
# `type: copy` is a low-level direct COPY (no chmod, no script).
# `type: files` was failing with `chmod: Operation not permitted` on
# the BlueBuild-shipped /tmp/modules/files/files.sh under buildah +
# podman privileged in our runner — the script tries to make itself
# executable inside its own bind-mounted layer.
- type: copy
source: ../overlay
destination: /
- type: copy
source: ../assets
destination: /usr/share/veilor-os/assets
- type: copy
source: ../scripts
destination: /usr/share/veilor-os/scripts
# ── 2. Branding overrides at build time ─────────────────────────
- type: script
snippets:
- |
# os-release brand
sed -i \
-e 's|^GRUB_DISTRIBUTOR=.*|GRUB_DISTRIBUTOR="veilor-os"|' \
/etc/default/grub 2>/dev/null || true
# Apply our kde-theme + plymouth in build
bash /usr/share/veilor-os/scripts/kde-theme-apply.sh || true
bash /usr/share/veilor-os/scripts/30-apply-v03-theme.sh 2>/dev/null || true
plymouth-set-default-theme details 2>/dev/null || true
# Mark all our shipped scripts + CLIs executable. cp -a from the
# repo preserves perms but BlueBuild's `type: files` sometimes
# drops the +x bit on the way through; belt-and-braces here.
chmod +x /usr/share/veilor-os/scripts/*.sh \
/usr/share/veilor-os/scripts/selinux/*.sh \
/usr/local/bin/veilor-* 2>/dev/null || true
# Refresh fontconfig cache so Fira Code is picked up by KDE
fc-cache -f 2>/dev/null || true
# os-release brand override (atomic /etc is r/w; safe to overwrite)
if [ -f /etc/os-release ]; then
sed -i \
-e 's|^NAME=.*|NAME="veilor-os"|' \
-e 's|^PRETTY_NAME=.*|PRETTY_NAME="veilor-os 0.7 (atomic)"|' \
-e 's|^ID=.*|ID=veilor|' \
-e 's|^ID_LIKE=.*|ID_LIKE="fedora kinoite"|' \
/etc/os-release || true
fi
# Sanity: brand-leak check, fail build if any onyx/personal data slipped in
if grep -rqi 'onyx\|192\.168\.0\.\|fedora\.local\|xynki\.dev' \
/etc/veilor* /etc/tuned/profiles/veilor-* /usr/share/veilor-os 2>/dev/null; then
echo "[ERR] brand leak detected in shipped state"
exit 1
fi
# ── 3. Override secureblue's run0-only — restore sudo ───────────
# secureblue removes sudo + replaces with run0. Too disruptive for
# daily-driver workflows. Restore sudo, keep run0 available.
- type: rpm-ostree
install:
- sudo
# ── 4. Re-enable Xwayland ───────────────────────────────────────
# secureblue disables Xwayland for attack-surface reduction. Some
# apps (Element, Slack-likes, older Qt5 tools) still need it.
# User who wants it removed back can `rpm-ostree override remove`.
- type: rpm-ostree
install:
- xorg-x11-server-Xwayland
# ── 5. Mullvad Browser as anti-fingerprint companion ────────────
# Layered alongside Trivalent (kept as default per STRATEGY.md).
# Trivalent for daily browsing, Mullvad for pseudonymous browsing.
# Thorium remains opt-in only via `ujust install-thorium` — see
# config/thorium.just for the warning + install logic.
- type: rpm-ostree
install:
- mullvad-browser
# ── 6. Mesh stack packages ──────────────────────────────────────
# Layer 1 (Day 1 daily driver, service pre-disabled): Tailscale
# Layer 2 (Day 1 idle warm-fallback): Yggdrasil-go
# Layer 3 (opt-in via ujust): Reticulum / RetiNet — handled in just/
- type: rpm-ostree
install:
- tailscale
- yggdrasil
# ── 6b. Memory hygiene + ergonomic deps ─────────────────────────
# zram-generator gives us zram swap (no disk swap, no cold-boot
# leak). gum is the TUI primitive used by veilor-postinstall +
# veilor-update + veilor-doctor — vendor binary at build time so
# post-install layering doesn't need it.
- type: rpm-ostree
install:
- zram-generator
- jq
- vim-enhanced
- tmux
- htop
# ── 7. ujust recipes for opt-in components ──────────────────────
- type: copy
source: config/just
destination: /usr/share/ublue-os/just
# ── 8. Service tuning: tailscale pre-disabled, yggdrasil idle ───
- type: systemd
system:
enabled:
- yggdrasil.service # idle warm-fallback (config = empty Listen[])
disabled:
- tailscaled.service # awaits first-boot prompt for join
# secureblue parents already enable: sshd, fail2ban, usbguard,
# auditd, firewalld, chronyd, sddm — no re-enable needed.
# ── 9. veilor-os specific systemd units ─────────────────────────
# All veilor-* units come in via overlay/etc/systemd/system/ —
# explicit enable here since they aren't part of secureblue's set.
- type: systemd
system:
enabled:
- veilor-firstboot.service
- veilor-modules-lock.service
- veilor-postinstall.service
- veilor-doctor.timer
# ── 10. signing config ──────────────────────────────────────────
# cosign.pub committed alongside this recipe; cosign.key kept off
# repo and provided to CI as Forgejo secret COSIGN_PRIVATE_KEY.
# The action exports it to /tmp at build time.
- type: signing

View file

@ -30,7 +30,7 @@
| 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 `kinoite-main-hardened` |
| [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 |
@ -194,7 +194,7 @@ The repo carries more than just an ISO recipe:
| `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 kinoite-main-hardened) |
| `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 |

View file

@ -1,138 +0,0 @@
# Installing veilor-os (v0.7+)
> v0.7 is the first OCI / atomic release. The kickstart-installed
> v0.5.x path still ships as legacy — if you want that flow, see
> [INSTALL.md](INSTALL.md). Both paths produce a hardened veilor-os
> system; the v0.7 path is what we recommend going forward.
## What's different from v0.5
| Topic | v0.5.x (kickstart) | v0.7+ (BlueBuild OCI) |
|---|---|---|
| Root filesystem | mutable, `/usr` writable | atomic / immutable, layered via `rpm-ostree` |
| Updates | `sudo dnf upgrade` | `sudo bootc upgrade` (atomic A/B, instant rollback) |
| Adding a package | `sudo dnf install foo` | `sudo rpm-ostree install foo` (layered into next deployment) |
| Base hardening | re-derived in our `%post` scripts | inherited from secureblue OCI image |
| Build artefact | `~2.7 GB` live ISO | small bootstrap ISO + signed OCI image at registry |
## Step-by-step
### 1. Download the bootstrap installer ISO
The bootstrap ISO is a tiny Anaconda-driven installer. It does
nothing more than collect a LUKS passphrase + admin password and
then call `ostreecontainer --url=...:43 --transport=registry` to
populate `/` from the pre-built signed OCI image.
Download from the Forgejo release:
<https://git.s8n.ru/veilor-org/veilor-os/releases/tag/ci-latest>
Reassemble the chunked ISO if needed (legacy artefact format):
```sh
cat veilor-os-*.iso.part-* > veilor-os.iso
sha256sum -c veilor-os-*.iso.parts.sha256
```
### 2. Verify the OCI image signature (optional, recommended)
The OCI image is cosign-signed at build time. If you have `cosign`
installed:
```sh
cosign verify --key cosign.pub git.s8n.ru/veilor-org/veilor-os:43
```
The public key `cosign.pub` ships with the bootstrap ISO and is also
on the Forgejo release page.
### 3. Flash to USB
Replace `/dev/sdX` with your USB device — triple-check the path.
```sh
sudo dd if=veilor-os.iso of=/dev/sdX bs=4M status=progress conv=fsync
sync
```
### 4. Boot from USB
Pick **Install veilor-os** from the boot menu. Anaconda starts and
asks two things, no more:
- **LUKS passphrase** for the encrypted root
- **admin password** (≥14 chars, mixed case, digit, symbol)
Anaconda then runs the `ostreecontainer` directive — pulls the
signed OCI image, writes it to disk, configures bootloader.
### 5. Reboot, remove USB
The first boot lands on SDDM with `admin` pre-filled. Log in.
### 6. First-login TUI
`veilor-postinstall` runs once, asks for the small set of things we
defer from install time:
- Keyboard / locale (defaults are fine for most operators)
- Hostname (default `veilor`)
- GPU drivers (NVIDIA layered via `rpm-ostree install`; mesa = no-op)
- Package presets (`dev` / `media` / `homelab`, all opt-in)
- Bluetooth (opt-in)
- USBGuard snapshot (plug in trusted devices first)
- `veilor-doctor` first run
Each step is skippable. The TUI writes a marker file and disables
itself; it never runs again.
If you need to re-run it: `sudo veilor-postinstall --force`.
### 7. Day-to-day
```sh
# update (atomic, A/B, instant rollback)
sudo veilor-update
# layer a package (takes effect after reboot)
sudo rpm-ostree install foo
# remove a layered package
sudo rpm-ostree uninstall foo
# health check + drift report
veilor-doctor
# rollback to previous deployment
sudo bootc rollback
# inspect current and staged deployments
bootc status
```
### Troubleshooting
| Symptom | Try |
|---|---|
| `veilor-update` says "no rollback target" | First boot — bootc only has rollback after the first successful upgrade. Normal. |
| Network down inside Anaconda | Bootstrap ISO uses NetworkManager defaults; plug in ethernet for the first install. WiFi support post-first-boot. |
| `rpm-ostree install foo` fails | Run `bootc status` — if a staged deployment exists, reboot first, then re-try. rpm-ostree won't layer onto a staged tree. |
| First-login TUI didn't appear | Marker check: `ls /var/lib/veilor/postinstall-complete`. If present, run `sudo veilor-postinstall --force`. |
| GPU is black after NVIDIA layer + reboot | `bootc rollback` and try mesa first; check `journalctl -b -1 -u sddm` from the previous boot. |
### Where the OCI image comes from
The image is built by `.github/workflows/build-bluebuild.yml` on the
self-hosted Forgejo runner (label `nullstone`). Build inputs:
- Base: `ghcr.io/secureblue/kinoite-main-hardened`
- Recipe: [`bluebuild/recipe.yml`](../bluebuild/recipe.yml)
- Veilor overlay: stamped via BlueBuild `type: files` modules
- Layered RPMs: `sudo`, `xorg-x11-server-Xwayland`, `mullvad-browser`,
`tailscale`, `yggdrasil`
- Output: `git.s8n.ru/veilor-org/veilor-os:{43,latest}`
The build is cosign-signed (key-pair on Forgejo, keyless on GitHub
parallel mirror). See [`bluebuild/README.md`](../bluebuild/README.md)
for the recipe walk-through.

View file

@ -252,7 +252,7 @@ ergonomic work and becomes the next ship target.
Scope:
- BlueBuild recipe (`bluebuild/recipe.yml`) layering on
`ghcr.io/secureblue/kinoite-main-hardened`
`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
@ -264,21 +264,6 @@ Scope:
- `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)
- **Installer logs persisted to USB stick by default** (debug mode):
the bootstrap ISO writes `/var/log/anaconda/*` + the resolved
kickstart + ostreecontainer pull log + dmesg back onto the USB
install medium (mounted rw at `/run/install/repo` during install)
into a `veilor-install-logs/<timestamp>/` folder. Toggleable via
kernel cmdline `inst.veilor.savelogs=0` for opt-out, or
`inst.veilor.savelogs=1` (default). Stays **ON by default through
v0.7+v0.8+v0.9; flips OFF for v1.0 final release**. Why: any failed
install, the operator boots back to a working OS, plugs the USB,
reads the logs offline — no need to take screenshots of dracut on a
bricked machine. Implementation: `%post --nochroot` block in
`kickstart/install-ostreecontainer.ks` that detects the install
medium via `/run/install/repo` rw remount, copies the log set,
syncs, then unmounts. If the medium is read-only (DVD), skip
silently with a `journalctl` warning.
Public-flex items kept from original v0.7 entry:
@ -307,7 +292,7 @@ spike on `quay.io/fedora/fedora-bootc:43`. Research on 2026-05-05
`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
`kinoite-main-hardened` OCI image** via a BlueBuild
`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).

View file

@ -12,7 +12,7 @@ Locked at: **v0.5.31 → v0.7 spike → v1.0**
works).
- Anaconda's `ostreecontainer` directive populates the root filesystem
directly from a **veilor-os OCI image** (built via BlueBuild on top
of secureblue's `kinoite-main-hardened`) **during the
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.
@ -236,7 +236,7 @@ distro: **honest, scoped, public threat model**.
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 `kinoite-main-hardened`. With `ostreecontainer`
on `securecore-kinoite-hardened-userns`. With `ostreecontainer`
swap, spike compresses 1 week → 1 day.
## Next concrete steps
@ -254,7 +254,7 @@ 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/kinoite-main-hardened:latest`.
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).
@ -334,29 +334,3 @@ dir.
- 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>
---
## PIVOT EXECUTION — 2026-05-06
The hybrid strategy locked at v0.5 is now in execution.
- **v0.5.0 shipped** as the proof-of-work / portfolio release of the
kickstart-flat path. Self-hosted Forgejo CI green-built a 2.7 GB
ISO; tag pushed; download lives at the ci-latest release.
- **v0.6 milestone cancelled.** Continuing to debug
`livecd-creator + anaconda` quirks for v0.6 polish would be sunk-
cost work on tooling we retire at v1.0. Original v0.6 plan kept in
ROADMAP.md as historical reference.
- **v0.7 BlueBuild OCI is the active mainline.** The
`v0.7-bluebuild-spike` branch carries the BlueBuild recipe layered
on `ghcr.io/secureblue/kinoite-main-hardened`, the
`ostreecontainer` kickstart bootstrap, and the new `bootc upgrade`-
driven update channel.
- **v0.6 ergonomic CLIs ported, not rewritten.** `veilor-update`
rewrites onto `bootc upgrade`; `veilor-postinstall` becomes the
first-login TUI on the atomic system; `veilor-doctor` learns
`bootc status --json` while keeping the legacy dnf path for v0.5.x.
- **v1.0 retires the kickstart entirely.** Only `kickstart/install-
ostreecontainer.ks` (10 lines) ships forward — bootstrap installer
for ostreecontainer pulls.

View file

@ -1,47 +0,0 @@
# veilor-os installer kickstart — v0.7 CI build variant
#
# Derived from kickstart/install-ostreecontainer.ks by stripping all
# __PLACEHOLDER__ tokens that the runtime gum TUI substitutes at install
# time. Anaconda's interactive TUI handles disk selection, LUKS passphrase,
# and user account creation in their place.
#
# Consumed by livemedia-creator --make-iso to produce
# veilor-os-installer-43-*.iso. Do NOT add __PLACEHOLDER__ tokens here —
# they cannot be filled at build time. See install-ostreecontainer.ks
# for the runtime template the gum TUI fills in.
# ── Locale / keyboard / time ──
keyboard --xlayouts='us'
lang en_US.UTF-8
timezone Europe/London --utc
# ── Install mode ──
text
firstboot --disable
eula --agreed
selinux --enforcing
# ── Network ──
network --bootproto=dhcp --device=link --activate --hostname=veilor-install
firewall --enabled --service=ssh
# ── Identity ──
# rootpw --lock only. No user directive — Anaconda's user spoke handles
# admin account creation interactively. Runtime ks substitutes
# --password=__ADMIN_PW__ for unattended installs.
rootpw --lock
# ── Disk / partitioning ──
# Intentionally absent. Anaconda's disk spoke presents interactive
# disk + LUKS + btrfs selection. Runtime ks (gum TUI) provides the
# full partition spec at real-install time.
# ── ostreecontainer: populate / from veilor-os OCI ──
ostreecontainer --url=ghcr.io/veilor-org/veilor-os:43 --transport=registry
# ── %post (chroot) ──
%post
set -uo pipefail
echo veilor-install > /etc/hostname
chage -d 0 admin 2>/dev/null || true
%end

View file

@ -1,80 +0,0 @@
# veilor-os install kickstart — v0.7 spike (ostreecontainer path)
#
# This is the install-time kickstart for the v0.7 hybrid path. The live
# ISO boots; the gum TUI collects user answers (disk, LUKS pw, admin pw);
# this template gets the answers substituted in and is fed to anaconda.
#
# Anaconda partitions the disk + creates LUKS + btrfs subvols + mounts
# /boot/efi + /boot, then `ostreecontainer` populates `/` directly from
# the cosign-signed veilor-os OCI image at `ghcr.io/veilor-org/veilor-os:43`.
#
# No `%packages` block. No first-boot rebase. No
# `veilor-firstboot-rebase.service`. The ostreecontainer install pass is
# the entire transition from "Fedora live ISO" to "veilor-os on disk".
#
# Reference: pykickstart docs ostreecontainer command;
# https://docs.fedoraproject.org/en-US/bootc/anaconda-install/
# ── Locale / keyboard / time ──
keyboard --xlayouts='us'
lang en_US.UTF-8
timezone Europe/London --utc
# ── Install mode / behaviour ──
firstboot --disable
eula --agreed
# SELinux state inherited from the OCI image; --enforcing is implicit
# since secureblue's image ships /etc/selinux/config = enforcing.
selinux --enforcing
# ── Network / hostname ──
network --bootproto=dhcp --device=link --activate --hostname=__HOSTNAME__
firewall --enabled --service=ssh
# ── Identity (single LUKS prompt asked at install via gum TUI) ──
rootpw --lock
user --name=admin --groups=wheel --gecos="veilor admin" --password=__ADMIN_PW__ --plaintext
# ── Bootloader ──
# fbcon=nodefer for laptop KMS handoff (real-hardware audit, agent 9 of
# 2026-05-05 wave). rd.luks.options=tries=5,timeout=0 for UX.
# rd.luks.uuid is auto-injected by anaconda based on the encrypted
# part directive below.
#
# All other hardening kargs (lockdown=integrity, slab_nomerge, etc.)
# come from /usr/lib/bootc/kargs.d/ inside the OCI image — bootc
# applies them at install time. We only add what the OCI image can't
# know (laptop-specific KMS flag).
bootloader --append="fbcon=nodefer"
# ── Disk: LUKS2 (argon2id) + btrfs subvols ──
zerombr
clearpart --all --initlabel --drives=__DISK_BASENAME__
part /boot/efi --fstype=efi --size=600
part /boot --fstype=ext4 --size=1024
part btrfs.veilor --grow --encrypted --luks-version=luks2 --pbkdf=argon2id --passphrase=__LUKS_PW__
btrfs none --label=veilor btrfs.veilor
btrfs / --subvol --name=root LABEL=veilor
btrfs /home --subvol --name=home LABEL=veilor
# ── ostreecontainer: populate / from the veilor-os OCI image ──
# `--transport=registry` pulls from ghcr.io directly. Authentication
# token can be supplied via /etc/ostree/auth.json baked into the live
# rootfs OR via a kickstart `--remote-token` if the registry is private.
# At v0.7 spike the OCI image is public, so no auth needed.
#
# DO NOT migrate to the new `bootc` kickstart command until v1.0 — it
# blocks multi-disk and authenticated registries (per parent-operator
# handoff 2026-05-05).
ostreecontainer --url=ghcr.io/veilor-org/veilor-os:43 --transport=registry
# ── %post (chroot) — minimal; OCI image already has everything ──
# What we keep:
# - chage -d 0 admin so first SDDM login forces password change
# - hostname write (anaconda's --hostname doesn't always survive)
# - veilor-firstboot.service is enabled in the OCI image already
%post
set -uo pipefail
echo veilor > /etc/hostname
chage -d 0 admin || true
%end

View file

@ -1,7 +0,0 @@
[Unit]
Description=veilor-doctor — system health + drift check
After=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/veilor-doctor --quiet

View file

@ -1,10 +0,0 @@
[Unit]
Description=veilor-doctor weekly drift check
[Timer]
OnCalendar=weekly
Persistent=true
RandomizedDelaySec=30m
[Install]
WantedBy=timers.target

View file

@ -1,17 +0,0 @@
[Unit]
Description=veilor-os one-time post-install TUI (first login)
After=graphical.target
ConditionPathExists=!/var/lib/veilor/postinstall-complete
[Service]
Type=oneshot
ExecStart=/usr/local/bin/veilor-postinstall
StandardInput=tty
StandardOutput=tty
StandardError=journal
TTYPath=/dev/tty1
TTYReset=yes
TTYVHangup=yes
[Install]
WantedBy=graphical.target multi-user.target

View file

@ -147,30 +147,12 @@ PUBLIC_IP=$(curl -s --max-time 3 ifconfig.me 2>/dev/null || echo "")
|| check Network public_ip fail "lookup timed out"
# ── 5. Updates ──────────────────────────────────────────────────────
# v0.7+ atomic — bootc replaces dnf as the update channel. Parse
# `bootc status --json` for the booted deployment + staged/cached image
# age. Fall back to dnf history if bootc not present (legacy v0.5.x).
if have bootc; then
BOOTC_JSON=$(sudo -n bootc status --json 2>/dev/null || echo "")
if [[ -n $BOOTC_JSON ]] && have jq; then
BOOTED_IMG=$(jq -r '.status.booted.image.image.image // "unknown"' <<<"$BOOTC_JSON")
BOOTED_DIGEST=$(jq -r '.status.booted.image.imageDigest // ""' <<<"$BOOTC_JSON")
check Updates booted_image pass "${BOOTED_IMG}@${BOOTED_DIGEST:0:12}"
STAGED=$(jq -r '.status.staged.image.image.image // ""' <<<"$BOOTC_JSON")
if [[ -n $STAGED ]]; then
check Updates staged_image fail "staged: $STAGED — reboot to apply"
else
check Updates staged_image pass "no staged update"
fi
else
check Updates bootc_state pass "bootc present (jq missing — install for richer detail)"
fi
elif have dnf; then
# Legacy v0.5.x kickstart-installed system.
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
@ -182,9 +164,6 @@ elif have dnf; then
;;
*) check Updates pending fail "dnf check-update returned $RC (need sudo?)" ;;
esac
else
check Updates channel fail "neither bootc nor dnf available"
fi
# ── 6. veilor services ──────────────────────────────────────────────
for unit in veilor-firstboot.service veilor-modules-lock.service; do

View file

@ -1,178 +0,0 @@
#!/usr/bin/bash
# veilor-postinstall — first-login TUI on v0.7+ atomic systems.
#
# Runs ONCE on first SDDM login via the user-mode systemd unit
# `veilor-postinstall.service`. Asks the operator for the small set
# of decisions we deliberately defer from install time:
# - keyboard / locale
# - hostname override
# - GPU drivers (NVIDIA layered via rpm-ostree, mesa = no-op)
# - package preset (dev / media / homelab — additive, opt-out)
# - bluetooth opt-in
# - USBGuard policy snapshot
# - veilor-doctor first run
# Writes /var/lib/veilor/postinstall-complete on success and disables
# its own autostart unit. Idempotent: safe to re-run.
#
# Style: gum if present, plain bash read fallback. No decorative ASCII.
set -uo pipefail
export TERM="${TERM:-linux}"
STATE_DIR=/var/lib/veilor
DONE_MARKER="$STATE_DIR/postinstall-complete"
LOG=/var/log/veilor-postinstall.log
have() { command -v "$1" >/dev/null 2>&1; }
GUM=$(have gum && echo gum || echo "")
# Always log + tee to stdout for live progress.
mkdir -p "$STATE_DIR" 2>/dev/null || true
exec > >(tee -a "$LOG") 2>&1
if [[ -e $DONE_MARKER && ${1:-} != "--force" ]]; then
echo "veilor-postinstall already ran (marker: $DONE_MARKER). Pass --force to re-run."
exit 0
fi
# ── Wrappers ────────────────────────────────────────────────────────
choose() {
local header=$1; shift
if [[ -n $GUM ]]; then
gum choose --header "$header" "$@"
else
echo
echo "$header"
local i=1
for opt in "$@"; do printf ' %d) %s\n' "$i" "$opt"; ((i++)); done
local n
read -rp " choice (1-$#): " n
[[ $n -ge 1 && $n -le $# ]] || return 1
eval "echo \${$n}"
fi
}
ask() {
local prompt=$1 default=${2:-}
if [[ -n $GUM ]]; then
gum input --header "$prompt" --value "$default"
else
local v
read -rp "$prompt [$default] " v
echo "${v:-$default}"
fi
}
confirm() {
local prompt=$1
if [[ -n $GUM ]]; then
gum confirm "$prompt" && return 0 || return 1
else
read -rp "$prompt [y/N] " y
[[ ${y,,} == y* ]]
fi
}
say() {
if [[ -n $GUM ]]; then
gum style --foreground 212 --bold "$1"
else
printf '\n=== %s ===\n' "$1"
fi
}
# Need root for several actions; re-exec under sudo if not root.
if [[ $EUID -ne 0 ]]; then
say "veilor-postinstall: sudo required"
exec sudo -E bash "$0" "$@"
fi
say "veilor-postinstall — one-time setup"
echo " This runs once. Each step is skippable. Defaults are sane."
echo
# ── 1. Keyboard layout ──────────────────────────────────────────────
KB=$(choose "Keyboard layout" us gb de fr es ru "skip") || KB=skip
if [[ $KB != skip ]]; then
localectl set-keymap "$KB" 2>/dev/null || true
echo " [OK] keymap = $KB"
fi
# ── 2. Locale ───────────────────────────────────────────────────────
LOC=$(choose "Locale" en_US.UTF-8 en_GB.UTF-8 de_DE.UTF-8 fr_FR.UTF-8 "skip") || LOC=skip
if [[ $LOC != skip ]]; then
localectl set-locale LANG="$LOC" 2>/dev/null || true
echo " [OK] locale = $LOC"
fi
# ── 3. Hostname ─────────────────────────────────────────────────────
HN=$(ask "Hostname" "veilor")
if [[ -n $HN && $HN != $(hostnamectl --static 2>/dev/null) ]]; then
hostnamectl set-hostname "$HN"
echo " [OK] hostname = $HN"
fi
# ── 4. GPU drivers ──────────────────────────────────────────────────
GPU=$(choose "GPU drivers" "Skip (use mesa defaults)" "NVIDIA proprietary (akmod-nvidia)" "Intel/AMD mesa (no-op)") || GPU=skip
case "$GPU" in
*NVIDIA*)
say "Layering NVIDIA driver — this takes a few minutes"
rpm-ostree install --idempotent akmod-nvidia xorg-x11-drv-nvidia-cuda \
&& echo " [OK] NVIDIA driver layered (reboot to use)" \
|| echo " [WARN] NVIDIA layer failed; check rpm-ostree status"
;;
*) echo " (skipped GPU layering)" ;;
esac
# ── 5. Package presets (multi-select) ───────────────────────────────
say "Package presets — pick any combination (skip = none)"
PRESET_DEV="git tmux vim-enhanced htop podman skopeo"
PRESET_MEDIA="vlc obs-studio"
PRESET_HOMELAB="wireguard-tools jq yq tmux"
PICKED=()
confirm "Install dev preset? ($PRESET_DEV)" && PICKED+=($PRESET_DEV) || true
confirm "Install media preset? ($PRESET_MEDIA)" && PICKED+=($PRESET_MEDIA) || true
confirm "Install homelab preset? ($PRESET_HOMELAB)" && PICKED+=($PRESET_HOMELAB) || true
if (( ${#PICKED[@]} > 0 )); then
# de-dupe
UNIQ=$(printf '%s\n' "${PICKED[@]}" | sort -u | tr '\n' ' ')
say "Layering: $UNIQ"
rpm-ostree install --idempotent $UNIQ \
&& echo " [OK] preset packages layered (reboot to use)" \
|| echo " [WARN] preset layer failed; check rpm-ostree status"
fi
# ── 6. Bluetooth ────────────────────────────────────────────────────
if confirm "Enable Bluetooth?"; then
systemctl enable --now bluetooth.service 2>/dev/null || true
echo " [OK] bluetooth enabled"
else
echo " (skipped bluetooth)"
fi
# ── 7. USBGuard snapshot ────────────────────────────────────────────
say "USBGuard policy snapshot"
echo " Plug in EVERY USB device you trust right now (keyboard,"
echo " mouse, dock, yubikey, etc.) before continuing."
if confirm "Snapshot current USB devices into the allowlist?"; then
usbguard generate-policy > /etc/usbguard/rules.conf \
&& echo " [OK] policy written to /etc/usbguard/rules.conf" \
|| echo " [WARN] generate-policy failed"
systemctl restart usbguard 2>/dev/null || true
fi
# ── 8. veilor-doctor ────────────────────────────────────────────────
if confirm "Run veilor-doctor now?"; then
veilor-doctor || true
fi
# ── Done ────────────────────────────────────────────────────────────
date -u +"%Y-%m-%dT%H:%M:%SZ" > "$DONE_MARKER"
say "veilor-postinstall complete"
echo " Marker written: $DONE_MARKER"
echo " Disabling autostart unit so this never runs again."
systemctl --user --global disable veilor-postinstall.service 2>/dev/null || true
systemctl disable veilor-postinstall.service 2>/dev/null || true
echo
echo " If you layered any packages or drivers, reboot to activate."

View file

@ -1,22 +1,25 @@
#!/usr/bin/bash
# veilor-update — atomic update wrapper for v0.7+ (bootc + rpm-ostree).
#
# Wraps `bootc upgrade` + flatpak update behind a single command.
# Pre-checks rollback availability, pauses auditd while staging the
# new image, prints a clear post-state summary, and offers reboot.
# 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 (with or without pending reboot)
# 1 bootc upgrade failed
# 2 flatpak failed (bootc still ran successfully)
# 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
@ -24,50 +27,46 @@ say() {
fi
}
confirm() {
local prompt=$1
run_with_spinner() {
local title=$1; shift
if [[ -n $GUM ]]; then
gum confirm "$prompt"
gum spin --spinner dot --title "$title" -- "$@"
else
read -r -p "$prompt [y/N] " yn
[[ ${yn,,} == y* ]]
echo "[+] $title"
"$@"
fi
}
# ── Pre-flight: network ─────────────────────────────────────────────
# ── Pre-flight: network check ───────────────────────────────────────
say "veilor-update: checking network"
if ! ping -c 1 -W 2 1.1.1.1 >/dev/null 2>&1; then
echo " No network. Connect and re-run \`veilor-update\`."
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
# ── Pre-flight: rollback target available ───────────────────────────
# bootc has two deployments by design (booted + rollback). If
# something's wrong we want the user to see it before staging more.
if have bootc; then
say "veilor-update: bootc status"
bootc status || true
else
echo " bootc not present — this CLI targets v0.7+ atomic systems."
# ── 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
# ── Pause auditd while staging ──────────────────────────────────────
# Reduces audit log noise during the heavy fs writes; resume after.
AUDIT_PAUSED=0
if systemctl is-active auditd >/dev/null 2>&1; then
if sudo systemctl stop auditd 2>/dev/null; then
AUDIT_PAUSED=1
fi
fi
trap '[[ $AUDIT_PAUSED == 1 ]] && sudo systemctl start auditd 2>/dev/null || true' EXIT
# ── bootc upgrade ───────────────────────────────────────────────────
say "veilor-update: bootc upgrade"
if ! sudo bootc upgrade; then
echo " bootc 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
@ -75,20 +74,21 @@ if have flatpak; then
say "veilor-update: updating flatpaks"
if ! flatpak update -y; then
FLATPAK_RC=2
echo " flatpak update failed; continuing."
echo " flatpak update failed; continuing anyway."
fi
else
echo " (flatpak not installed — skipping)"
fi
# ── Post-update summary ─────────────────────────────────────────────
# ── 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"
bootc status 2>/dev/null | head -20 || true
# ── Reboot prompt ───────────────────────────────────────────────────
# bootc always writes the new image into the staged deployment; reboot
# is required for it to become the running root.
if confirm " Reboot now to activate the new image?"; then
say "veilor-update: rebooting"
sudo systemctl reboot
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