Compare commits
73 commits
main
...
v0.7-blueb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
505b5f0006 | ||
|
|
c0ea2b3911 | ||
|
|
702702650a | ||
|
|
c272050890 | ||
|
|
865c9507af | ||
|
|
fa4db50680 | ||
|
|
800afaec2f | ||
|
|
8ffd6fb48d | ||
|
|
58cd6c2d84 | ||
|
|
2a95502898 | ||
|
|
ac69a8a351 | ||
|
|
7df9dc08f4 | ||
|
|
0e99a32084 | ||
|
|
e71ccaf198 | ||
|
|
cfd2eb69fd | ||
|
|
a06f54dd79 | ||
|
|
e6aa2d14a8 | ||
|
|
5910989f12 | ||
|
|
9a087ae0da | ||
|
|
266090ea0d | ||
|
|
b4c0feb30d | ||
|
|
c152953089 | ||
|
|
4966a65e37 | ||
|
|
2c197796e3 | ||
|
|
237968bfac | ||
|
|
f50f427ff8 | ||
|
|
ded80c6e15 | ||
|
|
48ccabe914 | ||
|
|
756b03aa5c | ||
|
|
1e70cc5461 | ||
|
|
9ee2cec20e | ||
|
|
8926894ceb | ||
|
|
6d8164c199 | ||
|
|
bbdafbce94 | ||
|
|
6391b1104b | ||
|
|
4d53d76442 | ||
|
|
606806f82f | ||
|
|
61fec5e1a9 | ||
|
|
5e94a61ea0 | ||
|
|
d48e59f05b | ||
|
|
ecd374ab1a | ||
|
|
e17c04007d | ||
|
|
97939d76f8 | ||
|
|
abaff9d3c3 | ||
|
|
29a6677d54 | ||
|
|
b3572565e2 | ||
|
|
9bf063a178 | ||
|
|
3f138e7435 | ||
|
|
7d6054311b | ||
|
|
6b0828d692 | ||
| a59f1f026a | |||
|
|
beef32a77c | ||
|
|
0a70eea950 | ||
|
|
877ad91096 | ||
|
|
a3b3d29b38 | ||
|
|
55221a6af2 | ||
| d76597c57a | |||
|
|
631e7bd040 | ||
|
|
9158532c9d | ||
|
|
e93ef644e1 | ||
|
|
21f2b4da9a | ||
| 91d5d26473 | |||
|
|
c7c0a0bcc8 | ||
| 9011fd2dbf | |||
| 20b3541d38 | |||
|
|
1fa45c3749 | ||
|
|
d9b206e46b | ||
|
|
89949dc8f2 | ||
| 0b568b016b | |||
|
|
e50c9a3b43 | ||
| 9dc2846316 | |||
| f2e36bfead | |||
|
|
3c247bc601 |
23 changed files with 2094 additions and 99 deletions
268
.github/workflows/build-bluebuild.yml
vendored
Normal file
268
.github/workflows/build-bluebuild.yml
vendored
Normal file
|
|
@ -0,0 +1,268 @@
|
|||
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: 360
|
||||
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
|
||||
echo "-- brand-leak scan (text files only, bounded paths)"
|
||||
HITS=$(find /etc/veilor* /etc/tuned/profiles/veilor-* /usr/share/veilor-os /usr/local/bin/veilor-* -type f \( -name "*.sh" -o -name "*.conf" -o -name "*.service" -o -name "*.timer" -o -name "*.txt" -o -name "*.md" -o -name "*.json" -o -name "*.yml" -o -name "*.yaml" -o -name "os-release" \) -exec grep -liE "onyx|192\.168\.0\.|fedora\.local|xynki\.dev" {} + 2>/dev/null || true)
|
||||
if [ -n "$HITS" ]; then echo "[ERR] brand leak detected:"; echo "$HITS"; exit 1; fi
|
||||
'
|
||||
|
||||
# ── 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 }}
|
||||
208
.github/workflows/build-installer-iso.yml
vendored
Normal file
208
.github/workflows/build-installer-iso.yml
vendored
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
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 podman jq
|
||||
|
||||
- name: Login to Forgejo registry (pull veilor-os OCI)
|
||||
env:
|
||||
FORGEJO_REGISTRY_TOKEN: ${{ secrets.FORGEJO_REGISTRY_TOKEN }}
|
||||
FORGEJO_REGISTRY_USER: ${{ secrets.FORGEJO_REGISTRY_USER }}
|
||||
run: |
|
||||
set -euo 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
|
||||
|
||||
- name: Build installer ISO with bootc-image-builder
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
# livemedia-creator does NOT support ostreecontainer (only
|
||||
# ostreesetup / url / nfs install methods). bootc-image-builder
|
||||
# is the canonical tool for ostreecontainer-based installer
|
||||
# ISOs; consumes our OCI image directly.
|
||||
OUT="/tmp/bib-out-$$"
|
||||
rm -rf "$OUT"
|
||||
mkdir -p "$OUT"
|
||||
# Pull the veilor-os OCI we built; bootc-image-builder needs
|
||||
# it locally to compose the installer ISO.
|
||||
podman pull ghcr.io/veilor-org/veilor-os:43 || \
|
||||
podman pull git.s8n.ru/veilor-org/veilor-os:43
|
||||
# Generate config.toml for bootc-image-builder.
|
||||
#
|
||||
# We use [customizations.installer.kickstart] (NOT
|
||||
# [customizations.user]) because we need our own %post --nochroot
|
||||
# block to persist install logs back to the boot USB. Per upstream
|
||||
# docs, [customizations.user] and [customizations.installer.kickstart]
|
||||
# are mutually exclusive (see osbuild/bootc-image-builder#528) — so
|
||||
# the admin user is now created by a kickstart `user` directive
|
||||
# below, locked + chage 0 so first SDDM login forces a real pw.
|
||||
#
|
||||
# bootc-image-builder auto-appends `ostreecontainer ...` to the
|
||||
# contents we provide; we MUST NOT include that line ourselves
|
||||
# (we strip it from the source kickstart with sed).
|
||||
#
|
||||
# NOTE on kernel cmdline default: ideally we'd set
|
||||
# `veilor.install_logs=on` as an installer-kernel default, but
|
||||
# `[customizations.kernel].append` targets the INSTALLED system's
|
||||
# kargs.d, not the live ISO's grub.cfg (osbuild/bootc-image-builder
|
||||
# #899 still open). The persist-install-logs.sh helper defaults to
|
||||
# ON when the toggle is absent, so the desired default is achieved
|
||||
# without needing installer-cmdline injection. Operators flip to
|
||||
# off at boot via GRUB edit: append `veilor.install_logs=off`.
|
||||
KS_SRC="kickstart/install-ostreecontainer-installer.ks"
|
||||
KS_FILTERED="$(grep -v '^ostreecontainer' "$KS_SRC")"
|
||||
# Insert a locked admin user directive under the rootpw block —
|
||||
# Anaconda's interactive Users spoke is unavailable in unattended
|
||||
# bib mode, so we pre-create admin and let chage -d 0 force a pw
|
||||
# change at first login.
|
||||
USER_LINE='user --name=admin --groups=wheel --plaintext --password="" --lock'
|
||||
KS_FILTERED="$(printf '%s\n' "$KS_FILTERED" | awk -v ul="$USER_LINE" '/^rootpw --lock$/ { print; print ul; next } { print }')"
|
||||
{
|
||||
echo '[customizations.installer.kickstart]'
|
||||
echo 'contents = """'
|
||||
printf '%s\n' "$KS_FILTERED"
|
||||
echo '"""'
|
||||
} > /tmp/bib-config.toml
|
||||
podman run --rm \
|
||||
--privileged \
|
||||
--pull=newer \
|
||||
--security-opt label=type:unconfined_t \
|
||||
-v "$OUT:/output" \
|
||||
-v /tmp/bib-config.toml:/config.toml:ro \
|
||||
-v /var/lib/containers/storage:/var/lib/containers/storage \
|
||||
quay.io/centos-bootc/bootc-image-builder:latest \
|
||||
--type anaconda-iso \
|
||||
--config /config.toml \
|
||||
--rootfs btrfs \
|
||||
ghcr.io/veilor-org/veilor-os:43
|
||||
mkdir -p build/out
|
||||
find "$OUT" -name '*.iso' -exec cp {} build/out/ \;
|
||||
ls -lh build/out/
|
||||
|
||||
- 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
|
||||
122
.github/workflows/smoke-test-oci.yml
vendored
Normal file
122
.github/workflows/smoke-test-oci.yml
vendored
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
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
1
.gitignore
vendored
|
|
@ -16,3 +16,4 @@ test/veilor-vm.nvram*
|
|||
test/auto-install-vm.qcow2
|
||||
test/auto-install-vm.nvram*
|
||||
.claude/worktrees/
|
||||
**/cosign.key
|
||||
|
|
|
|||
129
CHANGELOG.md
129
CHANGELOG.md
|
|
@ -11,7 +11,75 @@ future maintainers can see why a change exists, not just what it changes.
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Planned
|
||||
### v0.7 BlueBuild OCI spike (active — `v0.7-bluebuild-spike`)
|
||||
|
||||
CI plumbing landed (~13 fixes) to unblock the first green BlueBuild
|
||||
run on the self-hosted Forgejo runner. **Build still red** as of
|
||||
2026-05-08; OCI artifact + installer ISO pending green run.
|
||||
|
||||
#### Forgejo runner + build-image plumbing
|
||||
|
||||
- Forgejo runner upgraded to **v6.4.0** with `userns-remap=default`.
|
||||
Buildah needs `--userns=host` to undo the remap inside the job; added
|
||||
to every `bluebuild build` invocation.
|
||||
- Custom build image **`veilor-build:43`** (fedora:43 + nodejs +
|
||||
buildah deps). Replaces the upstream BlueBuild image, which lacked
|
||||
Forgejo-runner-friendly tooling.
|
||||
- Workflow now **`runs-on: nullstone`** (single self-hosted runner,
|
||||
no nested docker).
|
||||
- Build timeout bumped **60 min → 360 min** to absorb first-time
|
||||
secureblue base pulls on a cold runner.
|
||||
|
||||
#### Signing + registry auth
|
||||
|
||||
- **cosign v2.4.1** installed from upstream binary (no Fedora RPM yet
|
||||
for v2.4.x).
|
||||
- **GHCR PAT login** added so the BlueBuild step can pull
|
||||
`ghcr.io/secureblue/kinoite-main-hardened` (rate-limited anonymous).
|
||||
- **cosign keypair signing** — keyless OIDC fails on Forgejo (no
|
||||
Sigstore Fulcio integration), so we ship a static keypair under
|
||||
the repo and sign with `cosign sign --key`. Public key checked in
|
||||
for verification.
|
||||
|
||||
#### BlueBuild recipe pivots
|
||||
|
||||
- Base image switched to **`ghcr.io/secureblue/kinoite-main-hardened`**
|
||||
(the actual published image). Prior reference to
|
||||
`securecore-kinoite-hardened-userns` was a planning-phase guess and
|
||||
did not exist.
|
||||
- Module type pivots driven by buildah-privileged + bind-mounted helper
|
||||
scripts hitting chmod-permitted blockers:
|
||||
- `type: files` → **`type: copy`** (files module's chmod step
|
||||
failed under bind-mount).
|
||||
- `type: script` + `type: systemd` → **`type: containerfile` RUN**
|
||||
(single layer, no helper-script bind-mount).
|
||||
|
||||
#### Installer ISO — pivoted
|
||||
|
||||
- **livemedia-creator → bootc-image-builder.** livemedia-creator does
|
||||
not support the `ostreecontainer` install method (only
|
||||
`ostreesetup`/`url`/`nfs`), so the v0.7 path required the swap.
|
||||
Build pending OCI artifact.
|
||||
|
||||
#### Docs
|
||||
|
||||
- This CHANGELOG entry.
|
||||
- ROADMAP refresh — v0.5.0 marked done, v0.7 OCI marked in-flight,
|
||||
installer-iso pivot recorded, USB install-log persistence default-on
|
||||
promise documented, v1.0 ship criteria carried over.
|
||||
|
||||
### Infra (out-of-tree, recorded for traceability)
|
||||
|
||||
- **2026-05-08** — Headscale OIDC 403 fixed by adding
|
||||
`172.20.0.0/24` (docker proxy bridge gateway) to the
|
||||
`no-guest@file` Traefik middleware allowlist on nullstone.
|
||||
Unblocks `tag:guest` provisioning for veilor-os clients.
|
||||
- **All GitHub remotes removed** from veilor-os local clones, six
|
||||
worktrees, and sibling projects (auth-limbo, minecraft-launcher,
|
||||
minecraft-server, infra). GH push-mirrors disabled. Forgejo-only
|
||||
since 2026-05-05.
|
||||
|
||||
### Planned (deferred / parking)
|
||||
|
||||
- v0.3 polish — Plymouth black theme, SDDM theme, Konsole profile,
|
||||
wallpaper SVG. Re-enable `init_on_alloc=1 init_on_free=1` post-install
|
||||
|
|
@ -22,6 +90,65 @@ future maintainers can see why a change exists, not just what it changes.
|
|||
|
||||
---
|
||||
|
||||
## [0.5.0] — 2026-05-06
|
||||
|
||||
**Tag:** `v0.5.0` — **final kickstart-path release**.
|
||||
|
||||
The hardened-Fedora-43 kickstart line ships. Future work moves to
|
||||
the v0.7 BlueBuild OCI spike; the kickstart retires at v1.0.
|
||||
|
||||
### Added
|
||||
|
||||
- First green Forgejo-CI ISO build (~2.7 GB live ISO, EFI + BIOS
|
||||
bootable). Released as `ci-latest` artifact at
|
||||
`git.s8n.ru/veilor-org/veilor-os/releases/tag/ci-latest`.
|
||||
- **gum TUI installer** wrapping Anaconda — single LUKS prompt,
|
||||
locale locked to `en_US.UTF-8`, admin-password first-boot flow.
|
||||
- **LUKS2 argon2id + btrfs subvols** install via Anaconda, written
|
||||
through `/etc/kernel/cmdline` so BLS entries carry the cmdline
|
||||
veilor needs.
|
||||
- **3-mode `veilor-power` CLI** (`save | mid | perf`) with AC/battery
|
||||
udev auto-switching, lifted into the overlay.
|
||||
- **KDE black theme** + Fira Code system font, branded
|
||||
`/etc/os-release`, GRUB rebrand, plymouth detail-text boot.
|
||||
- Hardening: SELinux enforcing, USBGuard default-block, fail2ban +
|
||||
auditd, firewalld drop zone, NTS chrony, DNS-over-TLS, locked
|
||||
root.
|
||||
- Self-hosted **Forgejo CI** on nullstone replaces the GitHub
|
||||
Actions build pipeline.
|
||||
|
||||
### Fixed (delta from v0.2.5 → v0.5.0 — 35+ failure classes)
|
||||
|
||||
The full v0.5.x grind is documented per-release in commit messages
|
||||
(v0.5.21–v0.5.32). Headline fixes:
|
||||
|
||||
- **`--location=none` skipped `CollectKernelArgumentsTask`.** Anaconda
|
||||
shipped BLS entries with empty cmdline. Fix: write
|
||||
`/etc/kernel/cmdline` directly + `/etc/default/grub` + grubby +
|
||||
explicit `kernel-install add`. (v0.5.31)
|
||||
- **`transaction_progress.py` install scroll** masked real failures
|
||||
when patched too broadly. Narrowed the patch to only suppress
|
||||
`Configuring xxx.x86_64`. (v0.5.28 → v0.5.29)
|
||||
- **Locale dialog raced anaconda startup.** Lock to en_US.UTF-8,
|
||||
defer locale choice to `veilor-postinstall` (v0.7 scope). (v0.5.28)
|
||||
- **`fbcon=nodefer`** + GRUB rebrand + ASCII gum cursor make the
|
||||
install flow legible on linux fbcon. (v0.5.27)
|
||||
- **`rd.luks.uuid`** injected via `grubby --update-kernel=ALL` in
|
||||
chroot `%post` — earlier releases relied on Anaconda which silently
|
||||
dropped it. (v0.5.23, v0.5.27)
|
||||
- **9-agent research wave** identified the v0.5.32 blocker map; 7
|
||||
blockers shipped in one bundle.
|
||||
|
||||
### Notes
|
||||
|
||||
- Treat v0.5.0 as the **portfolio anchor** for the kickstart path.
|
||||
v0.5.32-rc was the last test-run; v0.5.0 was tagged on
|
||||
2026-05-06 as the freeze point.
|
||||
- v0.6 was **cancelled** the same day (folded into v0.7). See
|
||||
`docs/ROADMAP.md` strategy-pivot section.
|
||||
|
||||
---
|
||||
|
||||
## [0.2.5] — 2026-05-01
|
||||
|
||||
**Commit:** `8515bdb`
|
||||
|
|
|
|||
42
README.md
42
README.md
|
|
@ -48,26 +48,46 @@ spike at v0.7**, **bootc-only at v1.0**.
|
|||
|
||||
---
|
||||
|
||||
## Quick install
|
||||
## Quick install — v0.7+ (recommended, atomic / OCI)
|
||||
|
||||
```bash
|
||||
# 1. Download the ISO from the latest Forgejo release.
|
||||
# 1. Download the bootstrap installer ISO from Forgejo.
|
||||
# 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.
|
||||
# 2. Flash to USB. Replace /dev/sdX — triple-check.
|
||||
sudo dd if=veilor-os-43-*.iso of=/dev/sdX bs=4M status=progress conv=fsync
|
||||
sync
|
||||
|
||||
# 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`.
|
||||
# 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).
|
||||
```
|
||||
|
||||
Full install + first-boot walkthrough: [docs/INSTALL.md](docs/INSTALL.md).
|
||||
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).
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -147,7 +167,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
|
||||
`securecore-kinoite-hardened-userns` OCI image as its base — we don't
|
||||
`kinoite-main-hardened` 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.
|
||||
|
|
|
|||
96
bluebuild/README.md
Normal file
96
bluebuild/README.md
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
# 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/>
|
||||
73
bluebuild/config/just/60-veilor.just
Normal file
73
bluebuild/config/just/60-veilor.just
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
# 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)"
|
||||
4
bluebuild/cosign.pub
Normal file
4
bluebuild/cosign.pub
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
-----BEGIN PUBLIC KEY-----
|
||||
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE5xQcyP7FHNSiG7+VLsN2ViWlvvIB
|
||||
FYmu2XmPah7/VBlmuQ88H0ZbqCqqnS2u9x5+P1OMaMK+//k89V0Blrx65Q==
|
||||
-----END PUBLIC KEY-----
|
||||
163
bluebuild/recipe.yml
Normal file
163
bluebuild/recipe.yml
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
# 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/
|
||||
#
|
||||
# ── Module collapse history ──────────────────────────────────────
|
||||
# Run 183 (2026-05-08) ate 3h10min before runner timeout: each RUN/COPY
|
||||
# layer COMMIT under fuse-overlayfs over secureblue's 130-layer hardened
|
||||
# base costs ~40min wallclock (STEP 10..13 each 38–43min). Ergo: every
|
||||
# saved module = ~40min saved. Collapsed (A1b):
|
||||
# - 5× rpm-ostree → 1× (-4 layers)
|
||||
# - 2× containerfile (brand sed + systemctl enable) → 1× (-1 layer)
|
||||
# - 4× copy left as-is — BlueBuild copy module is one src/dest per
|
||||
# entry per https://blue-build.org/reference/modules/copy/
|
||||
# Net: 12 → 7 modules, ~5×40min ≈ 3h20min off wallclock budget.
|
||||
#
|
||||
# Run 189 + 191 (2026-05-08) — surviving rpm-ostree module hit the same
|
||||
# `chmod: Operation not permitted` bug we already worked around for
|
||||
# type:files / type:script / type:systemd: BlueBuild's helper scripts
|
||||
# (here `/tmp/modules/rpm-ostree/rpm-ostree.sh`) try to chmod themselves
|
||||
# inside their own buildah bind-mount under userns=host and fail.
|
||||
#
|
||||
# A1c fix: drop type:rpm-ostree, fold its install list into the existing
|
||||
# containerfile module as a raw RUN. Per BB containerfile docs each
|
||||
# `snippets:` entry = its own layer, so we MERGE pkg-install + brand +
|
||||
# systemctl into ONE snippet (= one RUN, one layer). Ordering: install
|
||||
# packages first (yggdrasil/tailscale/etc must exist before systemctl
|
||||
# enable/disable touches their units), then brand sed, then unit toggles.
|
||||
# `ostree container commit` ends the snippet because BB's rpm-ostree
|
||||
# module wraps it implicitly; raw RUN must do it manually for parity.
|
||||
# Mullvad + Tailscale repo files curl'd in same RUN — secureblue base
|
||||
# does not ship either repo, and the previous type:rpm-ostree must have
|
||||
# silently failed earlier (build never got that far in 189/191).
|
||||
# Net: 7 → 6 modules, one more layer commit avoided.
|
||||
---
|
||||
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.
|
||||
#
|
||||
# NOTE: Each copy module = one COPY layer (~40min commit on our
|
||||
# runner). BlueBuild's copy module accepts a single src/dest pair
|
||||
# only, so these four entries are the floor unless we move to a
|
||||
# hand-rolled Containerfile.
|
||||
- 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
|
||||
|
||||
- type: copy
|
||||
source: config/just
|
||||
destination: /usr/share/ublue-os/just
|
||||
|
||||
# ── 2. Packages + branding + unit toggles in ONE RUN snippet ────
|
||||
# secureblue removes sudo + replaces with run0 (too disruptive for
|
||||
# daily-driver) — restore. Xwayland was disabled for attack-surface
|
||||
# reduction — restore for Element/Slack/Qt5 apps. Mullvad Browser
|
||||
# layered alongside Trivalent (Trivalent default per STRATEGY.md;
|
||||
# Mullvad for pseudonymous browsing). Mesh stack: Tailscale (Layer
|
||||
# 1, daily driver, pre-disabled), Yggdrasil-go (Layer 2, idle warm-
|
||||
# fallback). Reticulum/RetiNet stays opt-in via ujust. Memory
|
||||
# hygiene + ergonomic deps for veilor-postinstall + veilor-doctor.
|
||||
#
|
||||
# Repos: secureblue base ships neither mullvad nor tailscale repos.
|
||||
# curl them into /etc/yum.repos.d/ inside the same RUN, before the
|
||||
# rpm-ostree install. Both pinned to upstream stable for Fedora.
|
||||
#
|
||||
# Branding + unit toggles run in the same RUN (= same layer) AFTER
|
||||
# rpm-ostree install so systemctl enable yggdrasil / disable tailscaled
|
||||
# see their unit files.
|
||||
#
|
||||
# Helper-script avoidance: BlueBuild's `type: rpm-ostree` /
|
||||
# `type: files` / `type: script` / `type: systemd` modules all hit
|
||||
# `chmod: Operation not permitted` on their own bind-mounted helper
|
||||
# script under buildah userns=host (run 189 + 191, last-frame error:
|
||||
# `chmod: changing permissions of '/tmp/modules/rpm-ostree/rpm-ostree.sh':
|
||||
# Operation not permitted`). Raw `type: containerfile` RUN bypasses
|
||||
# the whole helper-script layer.
|
||||
#
|
||||
# ostree container commit at the end mirrors what BB's wrapped
|
||||
# rpm-ostree module does implicitly — finalizes the layer for the
|
||||
# secureblue / Universal Blue base expectation.
|
||||
#
|
||||
# brand-leak grep moved to CI smoke-test in build-bluebuild.yml
|
||||
# (STEP 14 hung under buildah overlayfs, run 171 2026-05-07).
|
||||
- type: containerfile
|
||||
snippets:
|
||||
- |
|
||||
RUN set -euo pipefail ; \
|
||||
curl -fsSL https://repository.mullvad.net/rpm/stable/mullvad.repo \
|
||||
-o /etc/yum.repos.d/mullvad.repo ; \
|
||||
curl -fsSL https://pkgs.tailscale.com/stable/fedora/tailscale.repo \
|
||||
-o /etc/yum.repos.d/tailscale.repo ; \
|
||||
rpm-ostree install \
|
||||
sudo \
|
||||
xorg-x11-server-Xwayland \
|
||||
mullvad-browser \
|
||||
tailscale \
|
||||
yggdrasil \
|
||||
zram-generator \
|
||||
jq \
|
||||
vim-enhanced \
|
||||
tmux \
|
||||
htop ; \
|
||||
{ sed -i -e 's|^GRUB_DISTRIBUTOR=.*|GRUB_DISTRIBUTOR="veilor-os"|' /etc/default/grub 2>/dev/null || true ; \
|
||||
bash /usr/share/veilor-os/scripts/kde-theme-apply.sh 2>/dev/null || 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 ; \
|
||||
chmod +x /usr/share/veilor-os/scripts/*.sh \
|
||||
/usr/share/veilor-os/scripts/selinux/*.sh \
|
||||
/usr/local/bin/veilor-* 2>/dev/null || true ; \
|
||||
fc-cache -f 2>/dev/null || true ; \
|
||||
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 ; \
|
||||
systemctl enable yggdrasil.service 2>/dev/null || true ; \
|
||||
systemctl disable tailscaled.service 2>/dev/null || true ; \
|
||||
systemctl enable veilor-firstboot.service 2>/dev/null || true ; \
|
||||
systemctl enable veilor-modules-lock.service 2>/dev/null || true ; \
|
||||
systemctl enable veilor-postinstall.service 2>/dev/null || true ; \
|
||||
systemctl enable veilor-doctor.timer 2>/dev/null || true ; \
|
||||
} ; \
|
||||
rpm-ostree cleanup -m ; \
|
||||
ostree container commit
|
||||
|
||||
# ── 4. 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
|
||||
138
docs/INSTALL-V07.md
Normal file
138
docs/INSTALL-V07.md
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
# 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.
|
||||
|
|
@ -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 `securecore-kinoite-hardened-userns` |
|
||||
| [secureblue](https://github.com/secureblue/secureblue) | Upstream hardened atomic Fedora; v0.7 BlueBuild spike layers our overlay on top of `kinoite-main-hardened` |
|
||||
| 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 securecore-kinoite-hardened-userns) |
|
||||
| `bluebuild/recipe.yml` | v0.7 OCI recipe (base = secureblue kinoite-main-hardened) |
|
||||
| `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 |
|
||||
|
|
@ -9,6 +9,22 @@ For the historical record of what landed in each release, see
|
|||
|
||||
---
|
||||
|
||||
## Status snapshot — 2026-05-08
|
||||
|
||||
| Milestone | State | Notes |
|
||||
|-----------|-------|-------|
|
||||
| v0.2.x — green ISO + base hardening | DONE | shipped 2026-05-01 (`v0.2.5`) |
|
||||
| v0.3 — UX polish (Plymouth/SDDM/Konsole) | parked | rolls into v0.7 overlay |
|
||||
| v0.4 — distribution + signing | not started | cosign keypair already in v0.7 CI |
|
||||
| v0.5 — hardening tier 2 | DONE (kickstart line) | tagged `v0.5.0` 2026-05-06 — final kickstart-path release |
|
||||
| v0.6 — ergonomics | CANCELLED 2026-05-06 | folded into v0.7 |
|
||||
| v0.7 — BlueBuild OCI mainline | IN FLIGHT — blocked on green CI run | ~13 CI plumbing fixes landed; OCI artifact + installer ISO pending first green build |
|
||||
| v0.7 — installer-ISO tooling pivot | DONE (tooling) | livemedia-creator → bootc-image-builder; build pending OCI |
|
||||
| v0.7 — USB install-log persistence | TODO | default ON until v1.0; see "Installer logs" item below |
|
||||
| v1.0 — production | not started | multi-arch, LTS, recovery ISO, TPM2 |
|
||||
|
||||
---
|
||||
|
||||
## ⚡ STRATEGY PIVOT — 2026-05-06
|
||||
|
||||
**Decision: skip v0.6 kickstart polish. Pivot directly to v0.7
|
||||
|
|
@ -27,10 +43,12 @@ Reasons:
|
|||
`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.**
|
||||
**v0.5.0 is the final kickstart-path release.** Tagged on 2026-05-06,
|
||||
shipped as proof-of-work / portfolio anchor. **v0.6 cancelled as a
|
||||
milestone.**
|
||||
|
||||
Active focus: `v0.7-bluebuild-spike` branch.
|
||||
Active focus: `v0.7-bluebuild-spike` branch — first green CI run is
|
||||
the gating blocker for everything downstream.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -100,20 +118,31 @@ failures before greening.
|
|||
(`/etc/kernel/cmdline` + `/etc/default/grub` + grubby) plus explicit
|
||||
`kernel-install add`.
|
||||
|
||||
## v0.5.32 — next ship (active)
|
||||
## v0.5.0 — final kickstart release (DONE 2026-05-06)
|
||||
|
||||
Outstanding from the grind, immediate priority for the next tag:
|
||||
Tagged `v0.5.0` on 2026-05-06 as the final kickstart-path release.
|
||||
The v0.5.27→v0.5.31 install grind closed out via v0.5.32-rc, and the
|
||||
9-agent verification wave bundle landed before the freeze.
|
||||
|
||||
- **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.
|
||||
Shipped:
|
||||
- ~2.7 GB live ISO via Forgejo CI on nullstone (EFI + BIOS bootable)
|
||||
- `ci-latest` artifact at
|
||||
`git.s8n.ru/veilor-org/veilor-os/releases/tag/ci-latest`
|
||||
- gum TUI installer wrapping Anaconda; LUKS2 argon2id + btrfs
|
||||
- Full hardening overlay: SELinux enforcing, USBGuard default-block,
|
||||
fail2ban + auditd, firewalld drop, NTS chrony, DoT
|
||||
- 3-mode `veilor-power`, KDE black theme, Fira Code, branded
|
||||
os-release / GRUB / plymouth
|
||||
|
||||
Carry-overs into v0.7 (NOT shipped in v0.5.0):
|
||||
|
||||
- **Real-hardware run on the spare laptop** — VM-only signoff. KMS,
|
||||
fbcon, USB controller, real-firmware Secure Boot still need
|
||||
validation on the spare or the friend's laptop.
|
||||
- **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.
|
||||
Carries to v0.7 installer ISO, which inherits the gum TUI.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -244,15 +273,35 @@ distro from a kickstart.
|
|||
|
||||
---
|
||||
|
||||
## v0.7 — BlueBuild OCI mainline (ACTIVE — primary focus 2026-05-06+)
|
||||
## v0.7 — BlueBuild OCI mainline (IN FLIGHT — blocked on green CI run, 2026-05-08)
|
||||
|
||||
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.
|
||||
|
||||
### Status as of 2026-05-08
|
||||
|
||||
- **CI plumbing:** ~13 fixes landed on `v0.7-bluebuild-spike` to make
|
||||
the BlueBuild build run on the self-hosted Forgejo runner. See
|
||||
`CHANGELOG.md` `[Unreleased]` for the full breakdown.
|
||||
- **First green build:** **NOT YET.** Blocking everything downstream
|
||||
(OCI artifact publish, installer ISO build, real-hardware install
|
||||
test, public flex items).
|
||||
- **Installer ISO tooling pivot:** **DONE** — livemedia-creator does
|
||||
not support `ostreecontainer`; switched to `bootc-image-builder`.
|
||||
Build itself is pending the first green OCI artifact.
|
||||
- **Build host:** workflow runs on `nullstone` (single self-hosted
|
||||
Forgejo runner v6.4.0, `userns-remap=default`, buildah needs
|
||||
`--userns=host`).
|
||||
- **Base image:** `ghcr.io/secureblue/kinoite-main-hardened` (locked
|
||||
2026-05-08; corrected from earlier draft naming).
|
||||
- **Signing:** cosign keypair (keyless OIDC fails on Forgejo — no
|
||||
Sigstore Fulcio).
|
||||
- **Build timeout:** 60 min → 360 min (cold-runner first pulls).
|
||||
|
||||
Scope:
|
||||
- BlueBuild recipe (`bluebuild/recipe.yml`) layering on
|
||||
`ghcr.io/secureblue/securecore-kinoite-hardened-userns`
|
||||
`ghcr.io/secureblue/kinoite-main-hardened`
|
||||
- `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,6 +313,21 @@ 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 —
|
||||
TODO, in-flight in a separate agent thread): 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 `veilor.install_logs=on|off`; **default ON 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:
|
||||
|
||||
|
|
@ -292,7 +356,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
|
||||
`securecore-kinoite-hardened-userns` OCI image** via a BlueBuild
|
||||
`kinoite-main-hardened` OCI image** via a BlueBuild
|
||||
recipe, and install it directly during the Anaconda pass via the
|
||||
`ostreecontainer` kickstart directive (no first-boot rebase).
|
||||
|
||||
|
|
|
|||
|
|
@ -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 `securecore-kinoite-hardened-userns`) **during the
|
||||
of secureblue's `kinoite-main-hardened`) **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 `securecore-kinoite-hardened-userns`. With `ostreecontainer`
|
||||
on `kinoite-main-hardened`. 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/securecore-kinoite-hardened-userns:latest`.
|
||||
2. `from`: `ghcr.io/secureblue/kinoite-main-hardened:latest`.
|
||||
3. Override modules:
|
||||
- `type: files` — stamp our `overlay/*` tree (branding, themes,
|
||||
veilor scripts, sddm theme, plymouth theme).
|
||||
|
|
@ -334,3 +334,29 @@ 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.
|
||||
|
|
|
|||
123
kickstart/install-ostreecontainer-installer.ks
Normal file
123
kickstart/install-ostreecontainer-installer.ks
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
# 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.
|
||||
|
||||
# ── Packages for the LIVE BOOT ENVIRONMENT ──
|
||||
# These are NOT installed on the target system. They populate the
|
||||
# squashfs that boots Anaconda. The target is populated by
|
||||
# `ostreecontainer` below from the OCI image.
|
||||
%packages
|
||||
@^minimal-environment
|
||||
@core
|
||||
@anaconda-tools
|
||||
anaconda-live
|
||||
anaconda-tui
|
||||
livesys-scripts
|
||||
dracut-live
|
||||
dracut-config-generic
|
||||
kernel
|
||||
kernel-modules
|
||||
kernel-modules-extra
|
||||
glibc-all-langpacks
|
||||
ostree
|
||||
rpm-ostree
|
||||
bootupd
|
||||
grub2-efi-x64
|
||||
grub2-efi-x64-modules
|
||||
grub2-pc
|
||||
grub2-pc-modules
|
||||
grub2-tools
|
||||
grub2-tools-extra
|
||||
shim-x64
|
||||
efibootmgr
|
||||
syslinux
|
||||
isomd5sum
|
||||
xorriso
|
||||
%end
|
||||
|
||||
# ── 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
|
||||
|
||||
# ── %post --nochroot — persist install logs to USB (toggle: veilor.install_logs=on|off) ──
|
||||
#
|
||||
# Runs OUTSIDE the target chroot so /tmp/anaconda.log etc. on the live
|
||||
# ramdisk are accessible alongside /mnt/sysroot. Calls the helper that
|
||||
# ships in the veilor-os OCI image overlay; if the helper is missing
|
||||
# (corrupt overlay, stripped image, etc.) we fall back to a minimal
|
||||
# inline copy. NEVER fail the install over log persistence.
|
||||
#
|
||||
# Default: ON until v1.0 final. Disable per-boot:
|
||||
# edit GRUB / press 'e', append: veilor.install_logs=off
|
||||
%post --nochroot --erroronfail=no
|
||||
set -uo pipefail
|
||||
|
||||
VEILOR_HELPER="/mnt/sysroot/usr/share/veilor-os/scripts/persist-install-logs.sh"
|
||||
[ -x "$VEILOR_HELPER" ] || VEILOR_HELPER="/mnt/sysimage/usr/share/veilor-os/scripts/persist-install-logs.sh"
|
||||
|
||||
if [ -x "$VEILOR_HELPER" ]; then
|
||||
"$VEILOR_HELPER" || true
|
||||
else
|
||||
# Inline fallback — toggle-aware, backup-only (no USB write attempt).
|
||||
TS="$(date -u +%Y-%m-%dT%H-%M-%SZ)"
|
||||
SR=/mnt/sysroot; [ -d "$SR" ] || SR=/mnt/sysimage
|
||||
DST="${SR}/var/log/veilor-install-logs/${TS}"
|
||||
TOGGLE=on
|
||||
for tok in $(cat /proc/cmdline 2>/dev/null); do
|
||||
case "$tok" in veilor.install_logs=off|veilor.install_logs=0|veilor.install_logs=false|veilor.install_logs=no) TOGGLE=off ;; esac
|
||||
done
|
||||
if [ "$TOGGLE" = "on" ]; then
|
||||
mkdir -p "$DST" 2>/dev/null || true
|
||||
for f in /tmp/anaconda.log /tmp/program.log /tmp/storage.log \
|
||||
/tmp/packaging.log /tmp/syslog /tmp/dnf.log \
|
||||
/tmp/ks.cfg /run/veilor-installer.log; do
|
||||
[ -e "$f" ] && cp -a "$f" "$DST/" 2>/dev/null || true
|
||||
done
|
||||
dmesg > "$DST/dmesg.txt" 2>/dev/null || true
|
||||
journalctl --no-pager -b > "$DST/journalctl-b.txt" 2>/dev/null || true
|
||||
echo "[veilor] inline fallback used — helper missing at $VEILOR_HELPER" \
|
||||
> "$DST/manifest.txt"
|
||||
fi
|
||||
fi
|
||||
exit 0
|
||||
%end
|
||||
80
kickstart/install-ostreecontainer.ks
Normal file
80
kickstart/install-ostreecontainer.ks
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
# 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
|
||||
7
overlay/etc/systemd/system/veilor-doctor.service
Normal file
7
overlay/etc/systemd/system/veilor-doctor.service
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
[Unit]
|
||||
Description=veilor-doctor — system health + drift check
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/local/bin/veilor-doctor --quiet
|
||||
10
overlay/etc/systemd/system/veilor-doctor.timer
Normal file
10
overlay/etc/systemd/system/veilor-doctor.timer
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
[Unit]
|
||||
Description=veilor-doctor weekly drift check
|
||||
|
||||
[Timer]
|
||||
OnCalendar=weekly
|
||||
Persistent=true
|
||||
RandomizedDelaySec=30m
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
17
overlay/etc/systemd/system/veilor-postinstall.service
Normal file
17
overlay/etc/systemd/system/veilor-postinstall.service
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
[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
|
||||
|
|
@ -147,23 +147,44 @@ PUBLIC_IP=$(curl -s --max-time 3 ifconfig.me 2>/dev/null || echo "")
|
|||
|| 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
|
||||
# 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\`)"
|
||||
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
|
||||
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
|
||||
|
|
|
|||
178
overlay/usr/local/bin/veilor-postinstall
Executable file
178
overlay/usr/local/bin/veilor-postinstall
Executable file
|
|
@ -0,0 +1,178 @@
|
|||
#!/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."
|
||||
|
|
@ -1,25 +1,22 @@
|
|||
#!/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.
|
||||
# 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.
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 success
|
||||
# 1 dnf failed
|
||||
# 2 flatpak failed (dnf still ran successfully)
|
||||
# 0 success (with or without pending reboot)
|
||||
# 1 bootc upgrade failed
|
||||
# 2 flatpak failed (bootc 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
|
||||
|
|
@ -27,46 +24,50 @@ say() {
|
|||
fi
|
||||
}
|
||||
|
||||
run_with_spinner() {
|
||||
local title=$1; shift
|
||||
confirm() {
|
||||
local prompt=$1
|
||||
if [[ -n $GUM ]]; then
|
||||
gum spin --spinner dot --title "$title" -- "$@"
|
||||
gum confirm "$prompt"
|
||||
else
|
||||
echo "[+] $title"
|
||||
"$@"
|
||||
read -r -p "$prompt [y/N] " yn
|
||||
[[ ${yn,,} == y* ]]
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Pre-flight: network check ───────────────────────────────────────
|
||||
# ── Pre-flight: network ─────────────────────────────────────────────
|
||||
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\`."
|
||||
if ! ping -c 1 -W 2 1.1.1.1 >/dev/null 2>&1; then
|
||||
echo " No network. Connect 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."
|
||||
# ── 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."
|
||||
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 }')
|
||||
# ── 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
|
||||
|
||||
# ── Flatpak (best-effort) ───────────────────────────────────────────
|
||||
FLATPAK_RC=0
|
||||
|
|
@ -74,21 +75,20 @@ if have flatpak; then
|
|||
say "veilor-update: updating flatpaks"
|
||||
if ! flatpak update -y; then
|
||||
FLATPAK_RC=2
|
||||
echo " flatpak update failed; continuing anyway."
|
||||
echo " flatpak update failed; continuing."
|
||||
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 }')
|
||||
|
||||
# ── Post-update summary ─────────────────────────────────────────────
|
||||
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"
|
||||
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
|
||||
fi
|
||||
|
||||
exit $FLATPAK_RC
|
||||
|
|
|
|||
249
overlay/usr/share/veilor-os/scripts/persist-install-logs.sh
Executable file
249
overlay/usr/share/veilor-os/scripts/persist-install-logs.sh
Executable file
|
|
@ -0,0 +1,249 @@
|
|||
#!/usr/bin/env bash
|
||||
# persist-install-logs.sh — copy Anaconda install logs back to the boot USB
|
||||
#
|
||||
# Runs from %post --nochroot near the end of the Anaconda install. At that
|
||||
# point /tmp/*.log on the live ramdisk has the full evidence trail
|
||||
# (anaconda.log, program.log, storage.log, packaging.log, dnf.log,
|
||||
# syslog, etc.) — and is about to be lost forever when the user reboots
|
||||
# into the freshly installed system.
|
||||
#
|
||||
# We:
|
||||
# 1. Honour the kernel cmdline toggle veilor.install_logs=on|off
|
||||
# (default: on, until v1.0 final flips the default to off).
|
||||
# 2. Detect the boot USB device (BOOT=, BOOT_IMAGE=, /run/install/repo,
|
||||
# then /sys/block/*/removable=1 fallback).
|
||||
# 3. Try to remount it rw and copy logs into
|
||||
# /veilor-install-logs/<UTC-ISO8601>/ on the USB.
|
||||
# 4. ALSO copy a backup into /mnt/sysroot/var/log/veilor-install-logs/
|
||||
# so logs survive in the installed system even if the USB is RO,
|
||||
# missing, or write-failed.
|
||||
# 5. NEVER fail the install over this. Every error is logged + ignored.
|
||||
#
|
||||
# Disable at boot: edit GRUB / press 'e', append: veilor.install_logs=off
|
||||
# Disable in kickstart: comment out the call in install-ostreecontainer-installer.ks
|
||||
#
|
||||
# Author: veilor-os agent A2 (2026-05-08)
|
||||
# License: AGPLv3 — same as veilor-os
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
# ── trace ── everything to stderr; Anaconda captures stderr to program.log
|
||||
log() { printf '[persist-install-logs] %s\n' "$*" >&2; }
|
||||
trap 'log "WARN: line $LINENO failed (rc=$?) — continuing"' ERR
|
||||
|
||||
TS="$(date -u +%Y-%m-%dT%H-%M-%SZ)"
|
||||
SYSROOT="${VEILOR_SYSROOT:-/mnt/sysroot}"
|
||||
[ -d "$SYSROOT" ] || SYSROOT="/mnt/sysimage" # legacy Anaconda path
|
||||
|
||||
BACKUP_DIR="${SYSROOT}/var/log/veilor-install-logs/${TS}"
|
||||
mkdir -p "$BACKUP_DIR" 2>/dev/null || true
|
||||
|
||||
# ── 1. toggle ──────────────────────────────────────────────────────────────
|
||||
parse_toggle() {
|
||||
# default ON until v1.0 final
|
||||
local cmdline val
|
||||
cmdline="$(cat /proc/cmdline 2>/dev/null || true)"
|
||||
for tok in $cmdline; do
|
||||
case "$tok" in
|
||||
veilor.install_logs=*) val="${tok#veilor.install_logs=}" ;;
|
||||
esac
|
||||
done
|
||||
val="${val:-on}"
|
||||
case "$val" in
|
||||
on|true|1|yes) echo on ;;
|
||||
off|false|0|no) echo off ;;
|
||||
*) log "unknown veilor.install_logs=$val — defaulting to on"; echo on ;;
|
||||
esac
|
||||
}
|
||||
|
||||
TOGGLE="$(parse_toggle)"
|
||||
if [ "$TOGGLE" = "off" ]; then
|
||||
log "veilor.install_logs=off — log persistence skipped"
|
||||
exit 0
|
||||
fi
|
||||
log "veilor.install_logs=on — persisting install logs (ts=${TS})"
|
||||
|
||||
# ── 2. collect log payload into staging dir ───────────────────────────────
|
||||
STAGE="$(mktemp -d -t veilor-install-logs.XXXXXX 2>/dev/null || echo /tmp/veilor-install-logs-stage)"
|
||||
mkdir -p "$STAGE"
|
||||
|
||||
collect() {
|
||||
local src="$1" dst="$2"
|
||||
if [ -e "$src" ]; then
|
||||
cp -a "$src" "$STAGE/$dst" 2>/dev/null || \
|
||||
log "could not copy $src"
|
||||
fi
|
||||
}
|
||||
|
||||
# Anaconda /tmp logs (live env)
|
||||
for f in anaconda.log program.log storage.log packaging.log syslog \
|
||||
dnf.log dnf.librepo.log dnf.rpm.log dnf.hawkey.log \
|
||||
X.log ifcfg.log lvm.log yum.log; do
|
||||
collect "/tmp/$f" "$f"
|
||||
done
|
||||
# Kickstart-related
|
||||
collect /tmp/ks.cfg ks.cfg
|
||||
collect /tmp/ks-script.log ks-script.log
|
||||
collect /tmp/kickstart_pre.log kickstart_pre.log
|
||||
collect /tmp/kickstart_post.log kickstart_post.log
|
||||
# veilor TUI installer log (live ISO writes this to /run)
|
||||
collect /run/veilor-installer.log veilor-installer.log
|
||||
|
||||
# Runtime evidence
|
||||
{
|
||||
echo "── /proc/cmdline ──"
|
||||
cat /proc/cmdline 2>/dev/null
|
||||
echo
|
||||
echo "── /proc/version ──"
|
||||
cat /proc/version 2>/dev/null
|
||||
echo
|
||||
echo "── /etc/os-release ──"
|
||||
cat /etc/os-release 2>/dev/null
|
||||
echo
|
||||
echo "── timestamp (UTC) ──"
|
||||
date -u
|
||||
} > "$STAGE/system-info.txt" 2>/dev/null || true
|
||||
|
||||
dmesg --ctime > "$STAGE/dmesg.txt" 2>/dev/null || \
|
||||
dmesg > "$STAGE/dmesg.txt" 2>/dev/null || true
|
||||
|
||||
journalctl --no-pager -b > "$STAGE/journalctl-b.txt" 2>/dev/null || true
|
||||
|
||||
lsblk -fJ > "$STAGE/lsblk.json" 2>/dev/null || true
|
||||
blkid > "$STAGE/blkid.txt" 2>/dev/null || true
|
||||
mount > "$STAGE/mount.txt" 2>/dev/null || true
|
||||
|
||||
# Manifest
|
||||
{
|
||||
echo "veilor-os install log bundle"
|
||||
echo "timestamp_utc=${TS}"
|
||||
echo "host_uname=$(uname -a 2>/dev/null)"
|
||||
echo "files:"
|
||||
(cd "$STAGE" && ls -la 2>/dev/null)
|
||||
} > "$STAGE/manifest.txt"
|
||||
|
||||
# Backup copy regardless of USB success
|
||||
cp -a "$STAGE/." "$BACKUP_DIR/" 2>/dev/null && \
|
||||
log "backup written to ${BACKUP_DIR}" || \
|
||||
log "WARN: could not write backup to ${BACKUP_DIR}"
|
||||
|
||||
# ── 3. detect boot USB ─────────────────────────────────────────────────────
|
||||
detect_usb_dev() {
|
||||
local cmdline tok val dev
|
||||
cmdline="$(cat /proc/cmdline 2>/dev/null || true)"
|
||||
|
||||
# 3a) BOOT=LABEL=... or BOOT=UUID=... explicit
|
||||
for tok in $cmdline; do
|
||||
case "$tok" in
|
||||
BOOT=*)
|
||||
val="${tok#BOOT=}"
|
||||
dev="$(findfs "$val" 2>/dev/null || true)"
|
||||
[ -n "$dev" ] && [ -b "$dev" ] && { echo "$dev"; return 0; }
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# 3b) Anaconda mounts the install medium at /run/install/repo
|
||||
if mountpoint -q /run/install/repo 2>/dev/null; then
|
||||
dev="$(findmnt -no SOURCE /run/install/repo 2>/dev/null || true)"
|
||||
[ -n "$dev" ] && [ -b "$dev" ] && { echo "$dev"; return 0; }
|
||||
fi
|
||||
if mountpoint -q /run/install/sources/mount-0000-iso 2>/dev/null; then
|
||||
dev="$(findmnt -no SOURCE /run/install/sources/mount-0000-iso 2>/dev/null || true)"
|
||||
[ -n "$dev" ] && [ -b "$dev" ] && { echo "$dev"; return 0; }
|
||||
fi
|
||||
|
||||
# 3c) BOOT_IMAGE=(hdX,Y)/path — extract base device from kernel arg via
|
||||
# /run/initramfs/livedev (dracut-live writes this)
|
||||
if [ -r /run/initramfs/livedev ]; then
|
||||
dev="$(cat /run/initramfs/livedev 2>/dev/null)"
|
||||
[ -n "$dev" ] && [ -b "$dev" ] && { echo "$dev"; return 0; }
|
||||
fi
|
||||
|
||||
# 3d) /sys/block walk for first removable device with mounted partition
|
||||
local d part
|
||||
for d in /sys/block/*/removable; do
|
||||
[ "$(cat "$d" 2>/dev/null)" = "1" ] || continue
|
||||
local base
|
||||
base="$(basename "$(dirname "$d")")"
|
||||
for part in /sys/block/"$base"/"$base"*; do
|
||||
[ -d "$part" ] || continue
|
||||
local pname="/dev/$(basename "$part")"
|
||||
[ -b "$pname" ] && { echo "$pname"; return 0; }
|
||||
done
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
USB_DEV="$(detect_usb_dev || true)"
|
||||
if [ -z "${USB_DEV:-}" ]; then
|
||||
log "could not detect boot USB device — backup-only mode (see ${BACKUP_DIR})"
|
||||
exit 0
|
||||
fi
|
||||
log "detected boot USB partition: ${USB_DEV}"
|
||||
|
||||
# Walk to parent disk if we got a partition — we want the data partition not
|
||||
# the ESP. For an Anaconda-spun installer USB the ISO is hybrid: the ISO9660
|
||||
# partition holds the squashfs (RO), and there's usually an ESP. Strategy:
|
||||
# try mounting the partition we got first; if it's RO we accept that and
|
||||
# attempt remount; if remount fails we give up gracefully.
|
||||
|
||||
# ── 4. mount USB and write logs ────────────────────────────────────────────
|
||||
MOUNT_POINT="/run/veilor-install-logs-mount"
|
||||
mkdir -p "$MOUNT_POINT"
|
||||
|
||||
mount_rw() {
|
||||
local dev="$1"
|
||||
if mount "$dev" "$MOUNT_POINT" 2>/dev/null; then
|
||||
# check if rw
|
||||
if touch "$MOUNT_POINT/.veilor-write-test" 2>/dev/null; then
|
||||
rm -f "$MOUNT_POINT/.veilor-write-test"
|
||||
return 0
|
||||
fi
|
||||
# try remount rw
|
||||
if mount -o remount,rw "$MOUNT_POINT" 2>/dev/null && \
|
||||
touch "$MOUNT_POINT/.veilor-write-test" 2>/dev/null; then
|
||||
rm -f "$MOUNT_POINT/.veilor-write-test"
|
||||
return 0
|
||||
fi
|
||||
log "USB mounted RO and remount-rw failed: ${dev}"
|
||||
umount "$MOUNT_POINT" 2>/dev/null || true
|
||||
return 1
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
if mount_rw "$USB_DEV"; then
|
||||
DEST="${MOUNT_POINT}/veilor-install-logs/${TS}"
|
||||
if mkdir -p "$DEST" 2>/dev/null && cp -a "$STAGE/." "$DEST/" 2>/dev/null; then
|
||||
sync
|
||||
log "logs persisted to USB: ${USB_DEV}:/veilor-install-logs/${TS}"
|
||||
else
|
||||
log "WARN: USB mounted rw but write failed — keeping backup at ${BACKUP_DIR}"
|
||||
fi
|
||||
umount "$MOUNT_POINT" 2>/dev/null || true
|
||||
else
|
||||
# Try the parent disk's other partitions (some installer USBs have a
|
||||
# writable data partition separate from the ISO9660 squashfs partition).
|
||||
parent="$(echo "$USB_DEV" | sed -E 's/[0-9]+$//; s/p$//')"
|
||||
if [ -b "$parent" ]; then
|
||||
for cand in "$parent"*[0-9]; do
|
||||
[ -b "$cand" ] || continue
|
||||
[ "$cand" = "$USB_DEV" ] && continue
|
||||
if mount_rw "$cand"; then
|
||||
DEST="${MOUNT_POINT}/veilor-install-logs/${TS}"
|
||||
if mkdir -p "$DEST" 2>/dev/null && cp -a "$STAGE/." "$DEST/" 2>/dev/null; then
|
||||
sync
|
||||
log "logs persisted to USB partition: ${cand}:/veilor-install-logs/${TS}"
|
||||
fi
|
||||
umount "$MOUNT_POINT" 2>/dev/null || true
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
log "USB write path unavailable — relying on backup at ${BACKUP_DIR}"
|
||||
fi
|
||||
|
||||
rmdir "$MOUNT_POINT" 2>/dev/null || true
|
||||
rm -rf "$STAGE" 2>/dev/null || true
|
||||
exit 0
|
||||
Loading…
Reference in a new issue