Compare commits
40 commits
feat/cpu-i
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 05c041b18f | |||
| 886b2d6f84 | |||
| 5c961eba88 | |||
|
|
8c70030d80 | ||
|
|
89c7df0ecc | ||
|
|
c2b4df8ef9 | ||
|
|
b9df392fbc | ||
|
|
84fa325e46 | ||
|
|
1e4ca2b56b | ||
|
|
446c602683 | ||
|
|
ac5c29df42 | ||
|
|
6f4842a75c | ||
|
|
4b90e7e00b | ||
|
|
7a0c665cf0 | ||
|
|
d38fce4cb8 | ||
| bc738c1c7b | |||
| a3f6c1a1a6 | |||
| 356013e1ca | |||
| 417acb5585 | |||
| df574e00f5 | |||
| 3e660534a1 | |||
| 749bcef5b4 | |||
| 77ed91ed8e | |||
| ad059ec73e | |||
| 05d37f6419 | |||
| eafb8b7aa1 | |||
| d0738970e0 | |||
|
|
7974ed7a6e | ||
|
|
f06ee5cc1c | ||
|
|
130f0432dd | ||
|
|
08f16bb2ee | ||
|
|
25b8d30f35 | ||
|
|
aa731f9daa | ||
|
|
441f7d057f | ||
|
|
816fc0ee68 | ||
|
|
44f0c787a7 | ||
|
|
900f5465b3 | ||
|
|
63c5e199d9 | ||
|
|
abb67841f1 | ||
|
|
b86b4f9ec3 |
39 changed files with 331 additions and 2408 deletions
229
.forgejo/workflows/secret-scan.yml
Normal file
229
.forgejo/workflows/secret-scan.yml
Normal file
|
|
@ -0,0 +1,229 @@
|
||||||
|
# forgejo-actions-secret-scan.yml
|
||||||
|
#
|
||||||
|
# Drop into each repo at: .forgejo/workflows/secret-scan.yml
|
||||||
|
# (Forgejo Actions reads .forgejo/workflows/ natively; .github/workflows/
|
||||||
|
# also works as fallback if a repo has both. Prefer .forgejo/.)
|
||||||
|
#
|
||||||
|
# Layer-2 (CI) of the audit cadence — runs on every push + on pull-request.
|
||||||
|
# Two scanners (gitleaks + detect-secrets) for belt-and-braces coverage.
|
||||||
|
# On hit: opens a Forgejo Issue in this repo (assigned to operator)
|
||||||
|
# with redacted preview, then fails the workflow so any auto-merge stops.
|
||||||
|
#
|
||||||
|
# Required repo secrets:
|
||||||
|
# FORGEJO_TOKEN — PAT with scope `issue:write` for THIS repo only.
|
||||||
|
# Bot account preferred (obsidian-ai), not operator's PAT.
|
||||||
|
#
|
||||||
|
# Runner label: nullstone (the existing self-hosted runner per memory).
|
||||||
|
# If runner is offline / privileged-runner-design rejects this,
|
||||||
|
# fall back to label `docker` and use a vanilla container runner.
|
||||||
|
|
||||||
|
name: secret-scan
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- "**"
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- "**"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
# Don't run twice on the same SHA.
|
||||||
|
concurrency:
|
||||||
|
group: secret-scan-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
gitleaks:
|
||||||
|
name: gitleaks (HEAD + history)
|
||||||
|
runs-on: nullstone
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
issues: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout (full history for --log-opts=all)
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Install gitleaks
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
if ! command -v gitleaks >/dev/null 2>&1; then
|
||||||
|
curl -sSL -o /tmp/gitleaks.tgz \
|
||||||
|
"https://github.com/gitleaks/gitleaks/releases/download/v8.21.2/gitleaks_8.21.2_linux_x64.tar.gz"
|
||||||
|
mkdir -p /tmp/gl && tar -xzf /tmp/gitleaks.tgz -C /tmp/gl
|
||||||
|
sudo install -m 0755 /tmp/gl/gitleaks /usr/local/bin/gitleaks
|
||||||
|
fi
|
||||||
|
gitleaks version
|
||||||
|
|
||||||
|
- name: Pull s8n-stack ruleset
|
||||||
|
run: |
|
||||||
|
# Operator-tuned ruleset lives in s8n/security-vault.
|
||||||
|
# If the repo is offline, fall back to gitleaks defaults.
|
||||||
|
set -eu
|
||||||
|
if curl -sSL -H "Authorization: token ${FORGEJO_TOKEN}" \
|
||||||
|
-o .gitleaks.toml \
|
||||||
|
"https://git.s8n.ru/s8n/security-vault/raw/branch/main/prevention/.gitleaks.toml"; then
|
||||||
|
echo "loaded operator-tuned ruleset"
|
||||||
|
else
|
||||||
|
echo "fallback to gitleaks defaults" >&2
|
||||||
|
rm -f .gitleaks.toml
|
||||||
|
fi
|
||||||
|
env:
|
||||||
|
FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }}
|
||||||
|
|
||||||
|
- name: Scan HEAD (staged + uncommitted only takes commits)
|
||||||
|
id: gl-head
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
mkdir -p .scan
|
||||||
|
gitleaks detect --source . \
|
||||||
|
--no-banner --redact \
|
||||||
|
${GITLEAKS_CONFIG_FLAG} \
|
||||||
|
--report-format json \
|
||||||
|
--report-path .scan/gitleaks-head.json \
|
||||||
|
--exit-code 0
|
||||||
|
# Count findings:
|
||||||
|
n=$(jq 'length' .scan/gitleaks-head.json 2>/dev/null || echo 0)
|
||||||
|
echo "head_count=$n" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "gitleaks HEAD findings: $n"
|
||||||
|
env:
|
||||||
|
GITLEAKS_CONFIG_FLAG: ${{ hashFiles('.gitleaks.toml') != '' && '--config .gitleaks.toml' || '' }}
|
||||||
|
|
||||||
|
- name: Scan history (--log-opts=--all)
|
||||||
|
id: gl-hist
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
gitleaks detect --source . \
|
||||||
|
--no-banner --redact \
|
||||||
|
${GITLEAKS_CONFIG_FLAG} \
|
||||||
|
--log-opts="--all" \
|
||||||
|
--report-format json \
|
||||||
|
--report-path .scan/gitleaks-history.json \
|
||||||
|
--exit-code 0
|
||||||
|
n=$(jq 'length' .scan/gitleaks-history.json 2>/dev/null || echo 0)
|
||||||
|
echo "history_count=$n" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "gitleaks history findings: $n"
|
||||||
|
env:
|
||||||
|
GITLEAKS_CONFIG_FLAG: ${{ hashFiles('.gitleaks.toml') != '' && '--config .gitleaks.toml' || '' }}
|
||||||
|
|
||||||
|
- name: Upload gitleaks reports (artefact)
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: gitleaks-reports
|
||||||
|
path: .scan/
|
||||||
|
retention-days: 30
|
||||||
|
|
||||||
|
- name: Open Forgejo issue on hit (gitleaks)
|
||||||
|
if: steps.gl-head.outputs.head_count != '0' || steps.gl-hist.outputs.history_count != '0'
|
||||||
|
env:
|
||||||
|
FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }}
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
REF: ${{ github.ref }}
|
||||||
|
SHA: ${{ github.sha }}
|
||||||
|
HEAD_COUNT: ${{ steps.gl-head.outputs.head_count }}
|
||||||
|
HIST_COUNT: ${{ steps.gl-hist.outputs.history_count }}
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
# Build a redacted preview (rule-id + file + line, no values).
|
||||||
|
preview="$(jq -r '.[] | "- rule:" + .RuleID + " file:" + .File + " line:" + (.StartLine|tostring) + " commit:" + (.Commit // "HEAD")' .scan/gitleaks-head.json .scan/gitleaks-history.json | head -50)"
|
||||||
|
body=$(jq -nR --arg ref "$REF" --arg sha "$SHA" --arg hc "$HEAD_COUNT" --arg histc "$HIST_COUNT" --arg prev "$preview" \
|
||||||
|
'{
|
||||||
|
title: ("[secret-scan] gitleaks hit on " + $ref),
|
||||||
|
body: ("**Automated secret-scan hit.**\n\nRef: `" + $ref + "`\nSHA: `" + $sha + "`\nHEAD findings: " + $hc + "\nHistory findings: " + $histc + "\n\n## Redacted preview\n\n```\n" + $prev + "\n```\n\nFull report: workflow run artefacts (gitleaks-reports).\n\n## Triage\n\n1. False-positive? Add a `.gitleaksignore` entry with justifying comment + close.\n2. True-positive? Trigger incident response per `rules/incident-response-rules.md`. Rotate the affected credential. Then redact + history-rewrite.\n\n/cc @s8n"),
|
||||||
|
labels: ["security","secret-scan"]
|
||||||
|
}')
|
||||||
|
curl -sS -X POST \
|
||||||
|
-H "Authorization: token ${FORGEJO_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$body" \
|
||||||
|
"https://git.s8n.ru/api/v1/repos/${REPO}/issues" | jq '.html_url'
|
||||||
|
|
||||||
|
- name: Fail workflow on hit
|
||||||
|
if: steps.gl-head.outputs.head_count != '0' || steps.gl-hist.outputs.history_count != '0'
|
||||||
|
run: |
|
||||||
|
echo "::error::gitleaks found secrets — see opened issue + workflow artefact"
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
detect-secrets:
|
||||||
|
name: detect-secrets (entropy + cross-tool)
|
||||||
|
runs-on: nullstone
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
issues: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Install detect-secrets
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
python3 -m pip install --user --upgrade pip detect-secrets
|
||||||
|
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
||||||
|
|
||||||
|
- name: Scan
|
||||||
|
id: ds
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
mkdir -p .scan
|
||||||
|
detect-secrets scan --all-files \
|
||||||
|
--exclude-files '(^|/)(node_modules|venv|\.venv|dist|build|target|out|coverage|\.terraform)/' \
|
||||||
|
--exclude-files '(^|/)(package-lock\.json|yarn\.lock|pnpm-lock\.yaml|Cargo\.lock|go\.sum)$' \
|
||||||
|
> .scan/detect-secrets.json
|
||||||
|
# Count findings:
|
||||||
|
n=$(jq '.results | to_entries | map(.value | length) | add // 0' .scan/detect-secrets.json)
|
||||||
|
echo "count=$n" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "detect-secrets findings: $n"
|
||||||
|
|
||||||
|
- name: Upload detect-secrets report
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: detect-secrets-report
|
||||||
|
path: .scan/detect-secrets.json
|
||||||
|
retention-days: 30
|
||||||
|
|
||||||
|
- name: Open Forgejo issue on hit (detect-secrets)
|
||||||
|
if: steps.ds.outputs.count != '0'
|
||||||
|
env:
|
||||||
|
FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }}
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
REF: ${{ github.ref }}
|
||||||
|
COUNT: ${{ steps.ds.outputs.count }}
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
preview="$(jq -r '.results | to_entries[] | .key as $f | .value[] | "- " + $f + ":" + (.line_number|tostring) + " type:" + .type' .scan/detect-secrets.json | head -50)"
|
||||||
|
body=$(jq -nR --arg ref "$REF" --arg c "$COUNT" --arg prev "$preview" \
|
||||||
|
'{
|
||||||
|
title: ("[secret-scan] detect-secrets hit on " + $ref),
|
||||||
|
body: ("**Automated secret-scan hit (detect-secrets).**\n\nRef: `" + $ref + "`\nFindings: " + $c + "\n\n## Redacted preview (file:line type — no values)\n\n```\n" + $prev + "\n```\n\nFull report: workflow run artefacts (detect-secrets-report).\n\n## Triage\n\n1. False-positive? Run locally `detect-secrets audit .scan/detect-secrets.json` and commit the audited baseline.\n2. True-positive? Trigger incident response per `rules/incident-response-rules.md`.\n\n/cc @s8n"),
|
||||||
|
labels: ["security","secret-scan"]
|
||||||
|
}')
|
||||||
|
curl -sS -X POST \
|
||||||
|
-H "Authorization: token ${FORGEJO_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$body" \
|
||||||
|
"https://git.s8n.ru/api/v1/repos/${REPO}/issues" | jq '.html_url'
|
||||||
|
|
||||||
|
- name: Fail workflow on hit
|
||||||
|
if: steps.ds.outputs.count != '0'
|
||||||
|
run: |
|
||||||
|
echo "::error::detect-secrets found candidates — see opened issue + workflow artefact"
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
summary:
|
||||||
|
name: summary
|
||||||
|
needs: [gitleaks, detect-secrets]
|
||||||
|
if: always()
|
||||||
|
runs-on: nullstone
|
||||||
|
steps:
|
||||||
|
- name: Outcome
|
||||||
|
run: |
|
||||||
|
echo "secret-scan complete."
|
||||||
|
echo " gitleaks: ${{ needs.gitleaks.result }}"
|
||||||
|
echo " detect-secrets: ${{ needs['detect-secrets'].result }}"
|
||||||
268
.github/workflows/build-bluebuild.yml
vendored
268
.github/workflows/build-bluebuild.yml
vendored
|
|
@ -1,268 +0,0 @@
|
||||||
name: Build veilor-os OCI (BlueBuild)
|
|
||||||
|
|
||||||
# v0.7 spike — builds the bootable OCI image used by the bootstrap
|
|
||||||
# kickstart's `ostreecontainer` directive. Runs on the Forgejo
|
|
||||||
# self-hosted runner (label `nullstone`); GitHub-side cosign/SBOM/
|
|
||||||
# attest steps are gated off because Forgejo has no Sigstore Fulcio-
|
|
||||||
# trusted OIDC issuer (see docs/PROOF-OF-WORK.md, build-iso.yml fix).
|
|
||||||
#
|
|
||||||
# Reference: https://blue-build.org/how-to/setup-build-action/
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [v0.7-bluebuild-spike]
|
|
||||||
paths:
|
|
||||||
- 'bluebuild/**'
|
|
||||||
- 'overlay/**'
|
|
||||||
- 'assets/**'
|
|
||||||
- 'scripts/**'
|
|
||||||
- '.github/workflows/build-bluebuild.yml'
|
|
||||||
pull_request:
|
|
||||||
branches: [main, v0.7-bluebuild-spike]
|
|
||||||
schedule:
|
|
||||||
# Rebuild weekly so we pick up upstream secureblue + Fedora updates.
|
|
||||||
- cron: '0 6 * * 1'
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: Build + push OCI
|
|
||||||
# nullstone label resolves to veilor-build:43 (fedora43 + nodejs)
|
|
||||||
# via runner config. Privileged + userns=host + sock pass-through
|
|
||||||
# already wired in the runner config (see infra/forgejo/).
|
|
||||||
runs-on: nullstone
|
|
||||||
timeout-minutes: 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
208
.github/workflows/build-installer-iso.yml
vendored
|
|
@ -1,208 +0,0 @@
|
||||||
name: Build veilor-os Installer ISO
|
|
||||||
|
|
||||||
# v0.7+ — produces a small Anaconda installer ISO that consumes
|
|
||||||
# kickstart/install-ostreecontainer-installer.ks. The ISO boots
|
|
||||||
# Anaconda, asks for LUKS pw + admin pw interactively, then
|
|
||||||
# `ostreecontainer` populates / from the v0.7 OCI image at
|
|
||||||
# ghcr.io/veilor-org/veilor-os:43.
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [v0.7-bluebuild-spike]
|
|
||||||
paths:
|
|
||||||
- 'kickstart/install-ostreecontainer.ks'
|
|
||||||
- 'kickstart/install-ostreecontainer-installer.ks'
|
|
||||||
- 'bluebuild/recipe.yml'
|
|
||||||
- '.github/workflows/build-installer-iso.yml'
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
releasever:
|
|
||||||
description: 'Fedora release version'
|
|
||||||
required: false
|
|
||||||
default: '43'
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write # needed to create+update installer-latest release
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: Build installer ISO
|
|
||||||
runs-on: nullstone
|
|
||||||
timeout-minutes: 120
|
|
||||||
|
|
||||||
env:
|
|
||||||
RELEASEVER: ${{ github.event.inputs.releasever || '43' }}
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4.1.7
|
|
||||||
|
|
||||||
- name: Install build tooling (Fedora)
|
|
||||||
run: |
|
|
||||||
set -euxo pipefail
|
|
||||||
dnf -y upgrade --refresh
|
|
||||||
dnf -y install --skip-unavailable 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
122
.github/workflows/smoke-test-oci.yml
vendored
|
|
@ -1,122 +0,0 @@
|
||||||
name: Smoke-test veilor-os OCI
|
|
||||||
|
|
||||||
# Pulls git.s8n.ru/veilor-org/veilor-os:43 and asserts that the image
|
|
||||||
# contains the veilor brand + the v0.5.x hardening overlay + the v0.7
|
|
||||||
# CLI tools, and that cosign verifies it against bluebuild/cosign.pub.
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_run:
|
|
||||||
workflows: ["Build veilor-os OCI (BlueBuild)"]
|
|
||||||
types: [completed]
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
smoke:
|
|
||||||
name: OCI smoke test
|
|
||||||
runs-on: nullstone
|
|
||||||
if: >-
|
|
||||||
github.event_name == 'workflow_dispatch' ||
|
|
||||||
(github.event_name == 'workflow_run' &&
|
|
||||||
github.event.workflow_run.conclusion == 'success')
|
|
||||||
timeout-minutes: 20
|
|
||||||
|
|
||||||
env:
|
|
||||||
IMAGE: git.s8n.ru/veilor-org/veilor-os:43
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4.1.7
|
|
||||||
|
|
||||||
- name: Fix sudo perms
|
|
||||||
run: chown -R 0:0 /etc/sudo.conf /etc/sudoers /etc/sudoers.d 2>/dev/null || true
|
|
||||||
|
|
||||||
- name: Install podman + cosign
|
|
||||||
run: |
|
|
||||||
set -euxo pipefail
|
|
||||||
command -v podman >/dev/null || dnf -y install --skip-unavailable podman
|
|
||||||
if ! command -v cosign >/dev/null 2>&1; then
|
|
||||||
curl -fsSL "https://github.com/sigstore/cosign/releases/download/v2.4.1/cosign-linux-amd64" \
|
|
||||||
-o /usr/local/bin/cosign
|
|
||||||
chmod +x /usr/local/bin/cosign
|
|
||||||
fi
|
|
||||||
podman --version
|
|
||||||
cosign version
|
|
||||||
|
|
||||||
- name: Login + pull OCI image
|
|
||||||
env:
|
|
||||||
FORGEJO_REGISTRY_TOKEN: ${{ secrets.FORGEJO_REGISTRY_TOKEN }}
|
|
||||||
FORGEJO_REGISTRY_USER: ${{ secrets.FORGEJO_REGISTRY_USER }}
|
|
||||||
run: |
|
|
||||||
set -euxo pipefail
|
|
||||||
if [ -n "${FORGEJO_REGISTRY_TOKEN:-}" ]; then
|
|
||||||
echo "$FORGEJO_REGISTRY_TOKEN" | podman login \
|
|
||||||
--username "${FORGEJO_REGISTRY_USER:-veilor-org}" \
|
|
||||||
--password-stdin git.s8n.ru
|
|
||||||
fi
|
|
||||||
podman pull "${IMAGE}"
|
|
||||||
|
|
||||||
- name: Verify cosign signature
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
[ -f bluebuild/cosign.pub ] || { echo "[ERR] bluebuild/cosign.pub missing"; exit 1; }
|
|
||||||
cosign verify --key bluebuild/cosign.pub "${IMAGE}" 2>&1 | tail -10
|
|
||||||
|
|
||||||
- name: Run OCI assertions
|
|
||||||
run: |
|
|
||||||
set -uo pipefail
|
|
||||||
PASS=0; FAIL=0; ERRORS=""
|
|
||||||
pass() { echo "[PASS] $1"; PASS=$((PASS+1)); }
|
|
||||||
fail() { echo "[FAIL] $1"; FAIL=$((FAIL+1)); ERRORS="${ERRORS} - $1\n"; }
|
|
||||||
img() { podman run --rm "${IMAGE}" /bin/bash -c "$1" 2>/dev/null; }
|
|
||||||
|
|
||||||
OS=$(img 'cat /etc/os-release 2>/dev/null')
|
|
||||||
echo "$OS" | grep -q 'ID=veilor' && pass "ID=veilor" || fail "ID=veilor missing"
|
|
||||||
echo "$OS" | grep -q 'NAME="veilor-os"' && pass 'NAME="veilor-os"' || fail 'NAME="veilor-os" missing'
|
|
||||||
|
|
||||||
img 'which sudo' >/dev/null && pass "sudo present" || fail "sudo missing"
|
|
||||||
img 'rpm -q mullvad-browser' >/dev/null && pass "mullvad-browser present" || fail "mullvad-browser missing"
|
|
||||||
img 'rpm -q tailscale' >/dev/null && pass "tailscale present" || fail "tailscale missing"
|
|
||||||
img 'rpm -q yggdrasil' >/dev/null && pass "yggdrasil present" || fail "yggdrasil missing"
|
|
||||||
|
|
||||||
if img 'grep -qi "^SELINUX=enforcing" /etc/selinux/config'; then
|
|
||||||
pass "SELinux config = enforcing"
|
|
||||||
else
|
|
||||||
fail "SELinux not enforcing"
|
|
||||||
fi
|
|
||||||
|
|
||||||
img 'test -e /etc/systemd/system/multi-user.target.wants/veilor-firstboot.service \
|
|
||||||
-o -e /etc/systemd/system/graphical.target.wants/veilor-firstboot.service' >/dev/null \
|
|
||||||
&& pass "veilor-firstboot enabled" || fail "veilor-firstboot not enabled"
|
|
||||||
img 'test -e /etc/systemd/system/multi-user.target.wants/veilor-postinstall.service \
|
|
||||||
-o -e /etc/systemd/system/graphical.target.wants/veilor-postinstall.service' >/dev/null \
|
|
||||||
&& pass "veilor-postinstall enabled" || fail "veilor-postinstall not enabled"
|
|
||||||
|
|
||||||
for b in veilor-power veilor-update veilor-doctor veilor-postinstall; do
|
|
||||||
img "test -x /usr/local/bin/${b}" >/dev/null \
|
|
||||||
&& pass "${b} executable" || fail "${b} missing"
|
|
||||||
done
|
|
||||||
|
|
||||||
if img 'ls /usr/share/veilor-os/scripts/ 2>/dev/null' | grep -qE '(10-harden|20-harden|30-apply)'; then
|
|
||||||
pass "/usr/share/veilor-os/scripts populated"
|
|
||||||
else
|
|
||||||
fail "/usr/share/veilor-os/scripts missing"
|
|
||||||
fi
|
|
||||||
|
|
||||||
LEAKS=$(img "grep -rIni 'onyx\|192\.168\.0\.\|fedora\.local\|xynki\.dev' /etc/veilor* /usr/share/veilor-os 2>/dev/null")
|
|
||||||
if [ -z "$LEAKS" ]; then
|
|
||||||
pass "no brand leaks"
|
|
||||||
else
|
|
||||||
fail "brand leaks found"
|
|
||||||
echo "$LEAKS"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "═══ ${PASS} passed, ${FAIL} failed ═══"
|
|
||||||
if [ "$FAIL" -gt 0 ]; then
|
|
||||||
printf "%b" "$ERRORS"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "✓ veilor-os:43 smoke test passed"
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -16,4 +16,3 @@ test/veilor-vm.nvram*
|
||||||
test/auto-install-vm.qcow2
|
test/auto-install-vm.qcow2
|
||||||
test/auto-install-vm.nvram*
|
test/auto-install-vm.nvram*
|
||||||
.claude/worktrees/
|
.claude/worktrees/
|
||||||
**/cosign.key
|
|
||||||
|
|
|
||||||
233
CHANGELOG.md
233
CHANGELOG.md
|
|
@ -11,179 +11,7 @@ future maintainers can see why a change exists, not just what it changes.
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
### Hardening: CPU/IO slice isolation for background services
|
### Planned
|
||||||
|
|
||||||
Companion to the memory-pressure tuning (see prior entry). Memory was
|
|
||||||
only half the story — once OOM thrash was solved, a second class of
|
|
||||||
"why is my expensive laptop typing like a Chromebook" symptom emerged:
|
|
||||||
post-boot CPU/IO contention.
|
|
||||||
|
|
||||||
#### Bug found
|
|
||||||
|
|
||||||
Live incident on a 24-thread Ryzen AI 9 HX 370 / 30 GiB workstation,
|
|
||||||
2026-05-13: ~16 minutes after login, load avg climbed to ~6.5, typing
|
|
||||||
in konsole and the address bar lagged by hundreds of ms. RAM and swap
|
|
||||||
were uncontended (8 GiB used / 30 GiB total, zero swap), so the
|
|
||||||
memory-pressure work was holding. PSI showed `cpu some=0.34` — pure
|
|
||||||
scheduler contention.
|
|
||||||
|
|
||||||
Root cause: every Fedora unit ships with `CPUWeight=[not set]`
|
|
||||||
(defaults to 100), so under contention the kernel's CFQ splits CPU
|
|
||||||
evenly between every leaf cgroup. With the post-boot storm running
|
|
||||||
concurrently:
|
|
||||||
|
|
||||||
- `plasma-discover` (KDE update GUI, autostarted via
|
|
||||||
`/etc/xdg/autostart/org.kde.discover.notifier.desktop`) — ~80 % CPU
|
|
||||||
doing repo metadata refresh
|
|
||||||
- `packagekitd` (the discover backend) — ~33 %
|
|
||||||
- `fwupd` + `fwupd-refresh` — ~20 %
|
|
||||||
- `dnf-makecache.timer` firing in the same window
|
|
||||||
- `kwin_wayland` (~33 %) and `plasmashell` (~19 %) competing on equal
|
|
||||||
footing with all of the above
|
|
||||||
|
|
||||||
The compositor lost scheduling fights against package metadata, hence
|
|
||||||
the typing lag. zram-only swap and `vm.swappiness=180` are correct for
|
|
||||||
this stack but do nothing for a CPU-bound storm.
|
|
||||||
|
|
||||||
#### Fix applied
|
|
||||||
|
|
||||||
Two new slices in `overlay/etc/systemd/system/`:
|
|
||||||
|
|
||||||
1. **`system-bg.slice`** — `CPUWeight=20`, `IOWeight=50`,
|
|
||||||
`MemoryHigh=4G`. Drop-ins assign `packagekit.service`,
|
|
||||||
`fwupd.service`, `fwupd-refresh.service`, `dnf-makecache.service`,
|
|
||||||
and `dnf5-automatic.service` into it with `Nice=10` and
|
|
||||||
`IOSchedulingClass=idle`.
|
|
||||||
2. **`user-.slice.d/10-boost.conf`** — `CPUWeight=300`,
|
|
||||||
`IOWeight=200` on every logged-in user session. Combined with
|
|
||||||
above, gives a **15:1** interactive:background CPU ratio under
|
|
||||||
contention. Idle systems still get full speed; weights are
|
|
||||||
proportional, not hard caps.
|
|
||||||
|
|
||||||
Two boot-storm sources defused:
|
|
||||||
|
|
||||||
- `overlay/etc/skel/.config/autostart/org.kde.discover.notifier.desktop`
|
|
||||||
shadows the system autostart with `Hidden=true`. Updates still flow
|
|
||||||
via `dnf5-automatic.timer`; users can launch Discover manually. No
|
|
||||||
GUI fires at session start.
|
|
||||||
- `dnf-makecache.timer.d/10-delay.conf` pushes `OnBootSec=20min` so
|
|
||||||
metadata refresh lands past peak session bring-up.
|
|
||||||
|
|
||||||
One opt-in artifact for users:
|
|
||||||
|
|
||||||
- `overlay/etc/skel/.config/systemd/user/user-bg.slice`
|
|
||||||
(`CPUWeight=30`, `IOWeight=50`, `MemoryHigh=3G`). Veilor-os does not
|
|
||||||
ship sync tools by default, but anyone installing Syncthing /
|
|
||||||
rclone / a file indexer can drop a `Slice=user-bg.slice` drop-in
|
|
||||||
on the service and inherit the same protection at the user level.
|
|
||||||
|
|
||||||
Verified live (post-incident workstation, before opening the PR):
|
|
||||||
|
|
||||||
```
|
|
||||||
slice CPUWeight IOWeight MemoryHigh
|
|
||||||
system-bg.slice 20 50 4G
|
|
||||||
user-1000.slice 500 500 infinity
|
|
||||||
user-bg.slice 30 50 3G
|
|
||||||
```
|
|
||||||
|
|
||||||
cgroup placement confirmed via `systemd-cgls`: `packagekit.service`
|
|
||||||
under `/system.slice/system-bg.slice/`, `syncthing.service` under
|
|
||||||
`/user.slice/user-1000.slice/.../user-bg.slice/`. Load dropped from
|
|
||||||
6.53 → 3.55 within minutes of applying, and typing in the compositor
|
|
||||||
recovered immediately on the next contention event.
|
|
||||||
|
|
||||||
#### Follow-up surfaced during this work (not in this PR)
|
|
||||||
|
|
||||||
While debugging "still feels laggy after slice fix" on the same
|
|
||||||
workstation, found two power-profile bugs worth a separate
|
|
||||||
investigation:
|
|
||||||
|
|
||||||
1. `tuned-adm active` reported `balanced` despite the system being on
|
|
||||||
AC + charging. EPP was `balance_performance` and all 24 cores sat
|
|
||||||
pinned at `scaling_min_freq` (605 MHz) — typing latency was the
|
|
||||||
CPU refusing to ramp on short bursts, even with no contention.
|
|
||||||
Manually setting EPP to `performance` and switching to the stock
|
|
||||||
`throughput-performance` profile restored snappy input.
|
|
||||||
2. `tuned-adm profile onyx-performance` (shipped via
|
|
||||||
`overlay/etc/tuned/profiles/`) **silently fell back to `balanced`**
|
|
||||||
instead of activating. No errors in `journalctl -u tuned`. The
|
|
||||||
profile config or its `tuned.conf` script likely has a bad exit
|
|
||||||
somewhere; needs reproduction in CI and a test that asserts
|
|
||||||
`tuned-adm active` matches what was requested.
|
|
||||||
|
|
||||||
Both are tracked for a follow-up branch — out of scope here because
|
|
||||||
this PR only covers cgroup/slice isolation. Filing now so it does not
|
|
||||||
get lost.
|
|
||||||
|
|
||||||
### 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,
|
- v0.3 polish — Plymouth black theme, SDDM theme, Konsole profile,
|
||||||
wallpaper SVG. Re-enable `init_on_alloc=1 init_on_free=1` post-install
|
wallpaper SVG. Re-enable `init_on_alloc=1 init_on_free=1` post-install
|
||||||
|
|
@ -194,65 +22,6 @@ run on the self-hosted Forgejo runner. **Build still red** as of
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## [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
|
## [0.2.5] — 2026-05-01
|
||||||
|
|
||||||
**Commit:** `8515bdb`
|
**Commit:** `8515bdb`
|
||||||
|
|
|
||||||
42
README.md
42
README.md
|
|
@ -48,46 +48,26 @@ spike at v0.7**, **bootc-only at v1.0**.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Quick install — v0.7+ (recommended, atomic / OCI)
|
## Quick install
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Download the bootstrap installer ISO from Forgejo.
|
# 1. Download the ISO from the latest Forgejo release.
|
||||||
# https://git.s8n.ru/veilor-org/veilor-os/releases/tag/ci-latest
|
# 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
|
sha256sum -c veilor-os-43-*.iso.sha256
|
||||||
|
|
||||||
# 2. Flash to USB. Replace /dev/sdX — triple-check.
|
# 2. Flash to USB. Replace /dev/sdX with your USB device — triple-check.
|
||||||
sudo dd if=veilor-os-43-*.iso of=/dev/sdX bs=4M status=progress conv=fsync
|
sudo dd if=veilor-os-43-*.iso of=/dev/sdX bs=4M status=progress conv=fsync
|
||||||
sync
|
sync
|
||||||
|
|
||||||
# 3. Boot from USB. Anaconda asks for LUKS passphrase + admin password.
|
# 3. Boot from USB, pick "Install veilor-os" from the menu.
|
||||||
# Anaconda then runs `ostreecontainer --url=git.s8n.ru/veilor-org/veilor-os:43`
|
# 4. Set a strong LUKS passphrase — the only prompt during install.
|
||||||
# which populates / from the signed BlueBuild OCI image.
|
# 5. Reboot, remove USB.
|
||||||
|
# 6. On first boot: TTY prompts for an admin password (≥14 chars, mixed case,
|
||||||
# 4. Reboot. Log in as `admin`. The first-login TUI (veilor-postinstall)
|
# digit, symbol). Once accepted, SDDM starts. Log in as `admin`.
|
||||||
# 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 v0.7 walkthrough: [docs/INSTALL-V07.md](docs/INSTALL-V07.md).
|
Full install + first-boot walkthrough: [docs/INSTALL.md](docs/INSTALL.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).
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -167,7 +147,7 @@ clean, locked down, with no manual post-install hardening required.
|
||||||
[secureblue](https://github.com/secureblue/secureblue) is an upstream
|
[secureblue](https://github.com/secureblue/secureblue) is an upstream
|
||||||
hardened atomic Fedora project we benchmark against and plan to **build
|
hardened atomic Fedora project we benchmark against and plan to **build
|
||||||
on top of** at v0.7. The v0.7 BlueBuild spike uses their
|
on top of** at v0.7. The v0.7 BlueBuild spike uses their
|
||||||
`kinoite-main-hardened` OCI image as its base — we don't
|
`securecore-kinoite-hardened-userns` OCI image as its base — we don't
|
||||||
ship their source code in this repo, we layer veilor branding,
|
ship their source code in this repo, we layer veilor branding,
|
||||||
theming, the gum installer, and the kickstart bootstrap on top of
|
theming, the gum installer, and the kickstart bootstrap on top of
|
||||||
their already-signed image.
|
their already-signed image.
|
||||||
|
|
|
||||||
|
|
@ -1,96 +0,0 @@
|
||||||
# bluebuild/ — v0.7 spike
|
|
||||||
|
|
||||||
This directory contains the BlueBuild recipe + supporting config that
|
|
||||||
builds the veilor-os bootable OCI image. **Active on the
|
|
||||||
`v0.7-bluebuild-spike` branch only.** Does NOT land in v0.5.x main
|
|
||||||
until the spike passes its success criteria (see
|
|
||||||
`docs/STRATEGY.md`).
|
|
||||||
|
|
||||||
## What's here
|
|
||||||
|
|
||||||
```
|
|
||||||
bluebuild/
|
|
||||||
├── recipe.yml # primary BlueBuild recipe
|
|
||||||
├── config/
|
|
||||||
│ └── just/
|
|
||||||
│ └── 60-veilor.just # ujust recipes for opt-in components
|
|
||||||
└── README.md # this file
|
|
||||||
```
|
|
||||||
|
|
||||||
The recipe extends
|
|
||||||
`ghcr.io/secureblue/kinoite-main-hardened:latest`. We
|
|
||||||
inherit secureblue's hardening (sysctl + kargs + custom SELinux
|
|
||||||
policy + USBGuard + hardened-malloc + Unbound DoT + chronyd NTS +
|
|
||||||
Trivalent browser + cosign-signed image chain). On top, we layer:
|
|
||||||
|
|
||||||
- veilor branding (overlay/, theme, plymouth, sddm, os-release)
|
|
||||||
- mullvad-browser (anti-fingerprint companion to Trivalent)
|
|
||||||
- xorg-x11-server-Xwayland (re-enable; secureblue disables it)
|
|
||||||
- sudo (re-enable; secureblue replaces with run0)
|
|
||||||
- tailscale + yggdrasil (mesh stack layer 1 + 2)
|
|
||||||
- ujust recipes for Reticulum (mesh layer 3) + Thorium (opt-in browser)
|
|
||||||
|
|
||||||
Trivalent stays as the default browser (correcting an earlier draft).
|
|
||||||
|
|
||||||
## Build locally
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Requires bluebuild CLI:
|
|
||||||
# curl -fsSL https://raw.githubusercontent.com/blue-build/cli/main/install.sh | sh
|
|
||||||
cd bluebuild
|
|
||||||
bluebuild build recipe.yml
|
|
||||||
```
|
|
||||||
|
|
||||||
Output: `localhost/veilor-os:43` in podman storage. Push to GHCR
|
|
||||||
via the workflow.
|
|
||||||
|
|
||||||
## Test the OCI image
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Smoke-test (boots into the rootfs; no kernel, no init):
|
|
||||||
podman run --rm -it ghcr.io/veilor-org/veilor-os:43 /bin/bash
|
|
||||||
|
|
||||||
# Inside, sanity:
|
|
||||||
cat /etc/os-release # PRETTY_NAME=veilor-os
|
|
||||||
which sudo # /usr/bin/sudo (re-enabled)
|
|
||||||
which trivalent # secureblue's COPR (default browser)
|
|
||||||
which mullvad-browser # /usr/bin/mullvad-browser
|
|
||||||
systemctl is-enabled yggdrasil # enabled (idle)
|
|
||||||
systemctl is-enabled tailscaled # disabled (awaits ujust veilor-mesh-join)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Test the installer ISO
|
|
||||||
|
|
||||||
The installer ISO is built separately by livecd-creator (current path)
|
|
||||||
or bootc-image-builder (v1.0+). Its kickstart's `%packages` block is
|
|
||||||
replaced with:
|
|
||||||
|
|
||||||
```
|
|
||||||
ostreecontainer --url=ghcr.io/veilor-org/veilor-os:43 --transport=registry
|
|
||||||
```
|
|
||||||
|
|
||||||
That populates the target's `/` directly from this OCI image during
|
|
||||||
the install pass. No first-boot rebase. No transition window.
|
|
||||||
|
|
||||||
## Spike success criteria (1 day)
|
|
||||||
|
|
||||||
- [ ] `bluebuild build recipe.yml` exits 0
|
|
||||||
- [ ] `bootc container lint` exits 0 on the resulting image
|
|
||||||
- [ ] `podman run` smoke-test (commands above) all pass
|
|
||||||
- [ ] `.github/workflows/build-bluebuild.yml` builds + cosign-signs +
|
|
||||||
pushes to `ghcr.io/veilor-org/veilor-os:43`
|
|
||||||
- [ ] An installer ISO using `ostreecontainer` against this OCI
|
|
||||||
reaches SDDM with admin login on first boot
|
|
||||||
|
|
||||||
If all five land, merge `v0.7-bluebuild-spike` → `main` as v0.7.0.
|
|
||||||
If any fail in ways that aren't trivially fixable, file each as a GH
|
|
||||||
issue + return to v0.5.x kickstart path.
|
|
||||||
|
|
||||||
## See also
|
|
||||||
|
|
||||||
- `docs/STRATEGY.md` — the strategic decision + override list
|
|
||||||
- `docs/ROADMAP.md` v0.7 — full schedule
|
|
||||||
- `docs/THREAT-MODEL.md` — what we publish before launch
|
|
||||||
- secureblue: <https://github.com/secureblue/secureblue>
|
|
||||||
- BlueBuild: <https://blue-build.org>
|
|
||||||
- bootc / ostreecontainer: <https://docs.fedoraproject.org/en-US/bootc/>
|
|
||||||
|
|
@ -1,73 +0,0 @@
|
||||||
# veilor-os ujust recipes — opt-in components
|
|
||||||
# Loaded into /usr/share/ublue-os/just/ at image build time;
|
|
||||||
# `ujust install-X` discovers + dispatches.
|
|
||||||
|
|
||||||
# install Reticulum / RetiNet AGPL fork + Sideband (mesh layer 3)
|
|
||||||
install-reticulum:
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
echo "═══ Reticulum (RetiNet AGPL fork) install ═══"
|
|
||||||
echo
|
|
||||||
echo "Installs RetiNet (AGPL fork — NOT upstream RNS due to anti-AI"
|
|
||||||
echo "license) plus Sideband messenger. Default config: AutoInterface"
|
|
||||||
echo "(LAN multicast) + 1-2 TCP backbone peers. RNode hardware (LoRa"
|
|
||||||
echo "transceiver) is a separate install."
|
|
||||||
echo
|
|
||||||
read -p "Proceed? [y/N]: " confirm
|
|
||||||
if [[ "$confirm" != "y" ]]; then echo "Cancelled."; exit 0; fi
|
|
||||||
rpm-ostree install python3-pip
|
|
||||||
pip install --user retinet sideband-cli
|
|
||||||
echo
|
|
||||||
echo "Done. To attach an RNode (LoRa transceiver), run:"
|
|
||||||
echo " ujust install-reticulum-rnode"
|
|
||||||
|
|
||||||
# install Reticulum RNode hardware support (LoRa transceiver)
|
|
||||||
install-reticulum-rnode:
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
echo "═══ RNode (LoRa transceiver) hardware install ═══"
|
|
||||||
echo
|
|
||||||
echo "Adds RNode firmware-update tooling + udev rules for the LoRa"
|
|
||||||
echo "USB hardware. Required only if you have an RNode device."
|
|
||||||
echo
|
|
||||||
read -p "Proceed? [y/N]: " confirm
|
|
||||||
if [[ "$confirm" != "y" ]]; then echo "Cancelled."; exit 0; fi
|
|
||||||
pip install --user rnodeconf
|
|
||||||
echo "Done. Plug in your RNode via USB; it will appear as a serial device."
|
|
||||||
|
|
||||||
# install Thorium browser (OPT-IN, with explicit CVE-lag warning)
|
|
||||||
install-thorium:
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
echo "═══ Thorium browser install ═══"
|
|
||||||
echo
|
|
||||||
echo "WARNING: Thorium is a perf/media-focused fork of Chromium that"
|
|
||||||
echo "uses LTS Chromium as its base. As of 2026-05 it lags upstream"
|
|
||||||
echo "stable by ~9 milestones (months of CVE backlog)."
|
|
||||||
echo
|
|
||||||
echo "veilor-os ships Trivalent (secureblue's hardened Chromium fork,"
|
|
||||||
echo "tracking upstream M147+ within hours) as the default browser."
|
|
||||||
echo "Thorium is provided as an OPT-IN profile for users who"
|
|
||||||
echo "explicitly need its perf characteristics (e.g. WebGL games,"
|
|
||||||
echo "media decode profiles)."
|
|
||||||
echo
|
|
||||||
echo "DO NOT use Thorium as your daily-driver browser. Use Trivalent"
|
|
||||||
echo "or Mullvad Browser for that."
|
|
||||||
echo
|
|
||||||
read -p "Acknowledge CVE-lag risk and continue? [y/N]: " confirm
|
|
||||||
if [[ "$confirm" != "y" ]]; then echo "Cancelled."; exit 0; fi
|
|
||||||
flatpak install --user -y org.thorium.Thorium 2>/dev/null || \
|
|
||||||
rpm-ostree install thorium-browser
|
|
||||||
echo "Done. Launch via Plasma menu or `flatpak run org.thorium.Thorium`."
|
|
||||||
|
|
||||||
# join the veilor mesh (Tailscale via Headscale)
|
|
||||||
veilor-mesh-join:
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
echo "═══ Join veilor mesh (Tailscale via Headscale) ═══"
|
|
||||||
echo
|
|
||||||
echo "Pre-auth keys are minted by the Misskey signup page at"
|
|
||||||
echo "x.veilor (TTL 24h, single-use). You can paste the hex key"
|
|
||||||
echo "directly OR scan the QR code shown after signup."
|
|
||||||
echo
|
|
||||||
read -p "Hex key (paste): " preauth
|
|
||||||
if [[ -z "$preauth" ]]; then echo "Empty key. Cancelled."; exit 0; fi
|
|
||||||
sudo systemctl enable --now tailscaled
|
|
||||||
sudo tailscale up --login-server=https://hs.s8n.ru --auth-key="$preauth"
|
|
||||||
echo "Done. Status: $(sudo tailscale status | head -1)"
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
-----BEGIN PUBLIC KEY-----
|
|
||||||
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE5xQcyP7FHNSiG7+VLsN2ViWlvvIB
|
|
||||||
FYmu2XmPah7/VBlmuQ88H0ZbqCqqnS2u9x5+P1OMaMK+//k89V0Blrx65Q==
|
|
||||||
-----END PUBLIC KEY-----
|
|
||||||
|
|
@ -1,165 +0,0 @@
|
||||||
# veilor-os — BlueBuild recipe (v0.7 spike, 1-day target)
|
|
||||||
#
|
|
||||||
# Extends secureblue's hardened Kinoite OCI image with veilor branding,
|
|
||||||
# threat-model-driven UX choices, and the three-layer mesh stack
|
|
||||||
# (Tailscale + Yggdrasil + opt-in Reticulum). This is the OCI image
|
|
||||||
# that the v0.7+ kickstart's `ostreecontainer` directive pulls into
|
|
||||||
# the target root during the install pass.
|
|
||||||
#
|
|
||||||
# Build: bluebuild build recipe.yml
|
|
||||||
# Test: podman run --rm -it ghcr.io/veilor-org/veilor-os:43 /bin/bash
|
|
||||||
# CI: .github/workflows/build-bluebuild.yml signs + pushes to GHCR.
|
|
||||||
#
|
|
||||||
# Reference: https://blue-build.org/reference/recipe/
|
|
||||||
#
|
|
||||||
# ── 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 \
|
|
||||||
systemd-oomd-defaults \
|
|
||||||
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 ; \
|
|
||||||
systemctl enable systemd-oomd.service 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
|
|
||||||
|
|
@ -30,7 +30,7 @@
|
||||||
| Project | Role in veilor-os |
|
| Project | Role in veilor-os |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Fedora 43 KDE | Base OS for v0.5.x kickstart-installed flat builds |
|
| Fedora 43 KDE | Base OS for v0.5.x kickstart-installed flat builds |
|
||||||
| [secureblue](https://github.com/secureblue/secureblue) | Upstream hardened atomic Fedora; v0.7 BlueBuild spike layers our overlay on top of `kinoite-main-hardened` |
|
| [secureblue](https://github.com/secureblue/secureblue) | Upstream hardened atomic Fedora; v0.7 BlueBuild spike layers our overlay on top of `securecore-kinoite-hardened-userns` |
|
||||||
| Kicksecure / Whonix | Reference for AppArmor + apt-transport-tor model (we don't ship Tor; we did read their docs) |
|
| 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 |
|
| Bluefin / Bazzite (uBlue) | Reference for BlueBuild recipe shape and OCI publishing pattern |
|
||||||
| Tails | Reference for live-only install model — explicitly **not** veilor's path |
|
| 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/selinux/veilor-systemd.te` | Custom SELinux module (targeted policy gap fixes) |
|
||||||
| `scripts/30-apply-v03-theme.sh` | Plymouth + SDDM + Konsole + wallpaper apply |
|
| `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) |
|
| `scripts/40-apparmor.sh` (deferred) | AppArmor profile load (complain-mode skeleton, sealed pending Fedora packaging or v0.7 secureblue) |
|
||||||
| `bluebuild/recipe.yml` | v0.7 OCI recipe (base = secureblue kinoite-main-hardened) |
|
| `bluebuild/recipe.yml` | v0.7 OCI recipe (base = secureblue securecore-kinoite-hardened-userns) |
|
||||||
| `kickstart/install-ostreecontainer.ks` | v0.7 install ks: 10 lines, just `ostreecontainer --url=ghcr.io/veilor-org/veilor-os:43 --transport=registry` |
|
| `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/installer/{banner.txt,colors.gum}` | Pure-block VEILOR OS wordmark + branded gum colour palette |
|
||||||
| `assets/branding/` | Logo, wallpapers, plymouth theme assets |
|
| `assets/branding/` | Logo, wallpapers, plymouth theme assets |
|
||||||
|
|
@ -188,35 +188,6 @@ Splunk via HEC bridge.
|
||||||
## What's *not* enabled by default
|
## What's *not* enabled by default
|
||||||
|
|
||||||
- **Disk swap** — replaced by zram (RAM-only, no key leak risk).
|
- **Disk swap** — replaced by zram (RAM-only, no key leak risk).
|
||||||
|
|
||||||
## Memory pressure
|
|
||||||
|
|
||||||
veilor-os runs **zram-only swap** (see THREAT-MODEL.md — keeps cleartext
|
|
||||||
session keys out of any persistent allocation that would survive
|
|
||||||
suspend-to-disk or a yanked drive). That stance has a sharp edge: once
|
|
||||||
zram fills, there is no overflow tier. With stock kernel defaults the
|
|
||||||
result is a multi-minute thrash — input compositor frozen, mouse stuck,
|
|
||||||
keyboard ignored — followed by a kernel OOM kill that picks the wrong
|
|
||||||
victim (often `plasmashell` or the foreground terminal) because the
|
|
||||||
runaway browser tab has a lower oom_score than the long-lived session
|
|
||||||
process. The user's desktop dies; the leaking app survives.
|
|
||||||
|
|
||||||
Three layers of mitigation ship by default:
|
|
||||||
|
|
||||||
| Layer | File | What it does | Failure mode if absent |
|
|
||||||
|-------|------|--------------|------------------------|
|
|
||||||
| **systemd-oomd** | enabled in `kickstart/veilor-os.ks` `%post` and in `bluebuild/recipe.yml` unit-toggle RUN | PSI-based pre-OOM killer — picks the cgroup under highest memory+IO pressure and terminates it *before* the kernel's global reaper fires. Reads from `/proc/pressure/*`, kills at the cgroup boundary so siblings survive. | Kernel waits until total exhaustion. Picks by oom_score → plasmashell / terminal die, browser tab keeps leaking. Mouse locks during the wait. |
|
|
||||||
| **zram-generator** override | `overlay/etc/systemd/zram-generator.conf` (and matching `%post` write) | 16 GiB compressed with `zstd` (~3:1 → ~48 GiB effective). Replaces Fedora default 8 GiB / lzo-rle. | 8 GiB fills under sustained pressure on 32+ GiB laptops running Chromium + LSP + chat. No overflow (no disk swap) → straight to OOM. |
|
|
||||||
| **vm.* sysctl** | `overlay/etc/sysctl.d/95-memory-pressure.conf` | `swappiness=180` (use zram early — it's RAM-fast), `watermark_scale_factor=125` (kswapd starts reclaim ~1.25 % headroom vs default 0.1 %), `page-cluster=0` (no read-ahead — pointless on RAM-backed swap, wastes decompress cycles). | Defaults `60 / 10 / 3` assume slow HDD swap. Kernel refuses to swap until allocations stall in direct-reclaim → thrash window before either oomd or kernel OOM acts. |
|
|
||||||
|
|
||||||
All three are co-dependent: oomd without zram tuning still wedges
|
|
||||||
briefly waiting for PSI to climb; zram tuning without oomd still gets
|
|
||||||
kernel-OOM victim selection wrong. Verified by `test/boot-checklist.md`
|
|
||||||
"Memory pressure" section.
|
|
||||||
|
|
||||||
Layer rationale logged in `overlay/etc/sysctl.d/95-memory-pressure.conf`
|
|
||||||
and `overlay/etc/systemd/zram-generator.conf` headers — kept inline so
|
|
||||||
the *why* survives even if this doc is deleted.
|
|
||||||
- **Bluetooth** — disabled. Enable with `systemctl enable --now bluetooth`.
|
- **Bluetooth** — disabled. Enable with `systemctl enable --now bluetooth`.
|
||||||
- **Printing** — CUPS removed. Reinstall if needed: `dnf install cups`.
|
- **Printing** — CUPS removed. Reinstall if needed: `dnf install cups`.
|
||||||
- **Snapd, Flatpak** — not installed (Flatpak optional add-on).
|
- **Snapd, Flatpak** — not installed (Flatpak optional add-on).
|
||||||
|
|
|
||||||
|
|
@ -1,138 +0,0 @@
|
||||||
# Installing veilor-os (v0.7+)
|
|
||||||
|
|
||||||
> v0.7 is the first OCI / atomic release. The kickstart-installed
|
|
||||||
> v0.5.x path still ships as legacy — if you want that flow, see
|
|
||||||
> [INSTALL.md](INSTALL.md). Both paths produce a hardened veilor-os
|
|
||||||
> system; the v0.7 path is what we recommend going forward.
|
|
||||||
|
|
||||||
## What's different from v0.5
|
|
||||||
|
|
||||||
| Topic | v0.5.x (kickstart) | v0.7+ (BlueBuild OCI) |
|
|
||||||
|---|---|---|
|
|
||||||
| Root filesystem | mutable, `/usr` writable | atomic / immutable, layered via `rpm-ostree` |
|
|
||||||
| Updates | `sudo dnf upgrade` | `sudo bootc upgrade` (atomic A/B, instant rollback) |
|
|
||||||
| Adding a package | `sudo dnf install foo` | `sudo rpm-ostree install foo` (layered into next deployment) |
|
|
||||||
| Base hardening | re-derived in our `%post` scripts | inherited from secureblue OCI image |
|
|
||||||
| Build artefact | `~2.7 GB` live ISO | small bootstrap ISO + signed OCI image at registry |
|
|
||||||
|
|
||||||
## Step-by-step
|
|
||||||
|
|
||||||
### 1. Download the bootstrap installer ISO
|
|
||||||
|
|
||||||
The bootstrap ISO is a tiny Anaconda-driven installer. It does
|
|
||||||
nothing more than collect a LUKS passphrase + admin password and
|
|
||||||
then call `ostreecontainer --url=...:43 --transport=registry` to
|
|
||||||
populate `/` from the pre-built signed OCI image.
|
|
||||||
|
|
||||||
Download from the Forgejo release:
|
|
||||||
|
|
||||||
<https://git.s8n.ru/veilor-org/veilor-os/releases/tag/ci-latest>
|
|
||||||
|
|
||||||
Reassemble the chunked ISO if needed (legacy artefact format):
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cat veilor-os-*.iso.part-* > veilor-os.iso
|
|
||||||
sha256sum -c veilor-os-*.iso.parts.sha256
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Verify the OCI image signature (optional, recommended)
|
|
||||||
|
|
||||||
The OCI image is cosign-signed at build time. If you have `cosign`
|
|
||||||
installed:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cosign verify --key cosign.pub git.s8n.ru/veilor-org/veilor-os:43
|
|
||||||
```
|
|
||||||
|
|
||||||
The public key `cosign.pub` ships with the bootstrap ISO and is also
|
|
||||||
on the Forgejo release page.
|
|
||||||
|
|
||||||
### 3. Flash to USB
|
|
||||||
|
|
||||||
Replace `/dev/sdX` with your USB device — triple-check the path.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
sudo dd if=veilor-os.iso of=/dev/sdX bs=4M status=progress conv=fsync
|
|
||||||
sync
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Boot from USB
|
|
||||||
|
|
||||||
Pick **Install veilor-os** from the boot menu. Anaconda starts and
|
|
||||||
asks two things, no more:
|
|
||||||
|
|
||||||
- **LUKS passphrase** for the encrypted root
|
|
||||||
- **admin password** (≥14 chars, mixed case, digit, symbol)
|
|
||||||
|
|
||||||
Anaconda then runs the `ostreecontainer` directive — pulls the
|
|
||||||
signed OCI image, writes it to disk, configures bootloader.
|
|
||||||
|
|
||||||
### 5. Reboot, remove USB
|
|
||||||
|
|
||||||
The first boot lands on SDDM with `admin` pre-filled. Log in.
|
|
||||||
|
|
||||||
### 6. First-login TUI
|
|
||||||
|
|
||||||
`veilor-postinstall` runs once, asks for the small set of things we
|
|
||||||
defer from install time:
|
|
||||||
|
|
||||||
- Keyboard / locale (defaults are fine for most operators)
|
|
||||||
- Hostname (default `veilor`)
|
|
||||||
- GPU drivers (NVIDIA layered via `rpm-ostree install`; mesa = no-op)
|
|
||||||
- Package presets (`dev` / `media` / `homelab`, all opt-in)
|
|
||||||
- Bluetooth (opt-in)
|
|
||||||
- USBGuard snapshot (plug in trusted devices first)
|
|
||||||
- `veilor-doctor` first run
|
|
||||||
|
|
||||||
Each step is skippable. The TUI writes a marker file and disables
|
|
||||||
itself; it never runs again.
|
|
||||||
|
|
||||||
If you need to re-run it: `sudo veilor-postinstall --force`.
|
|
||||||
|
|
||||||
### 7. Day-to-day
|
|
||||||
|
|
||||||
```sh
|
|
||||||
# update (atomic, A/B, instant rollback)
|
|
||||||
sudo veilor-update
|
|
||||||
|
|
||||||
# layer a package (takes effect after reboot)
|
|
||||||
sudo rpm-ostree install foo
|
|
||||||
|
|
||||||
# remove a layered package
|
|
||||||
sudo rpm-ostree uninstall foo
|
|
||||||
|
|
||||||
# health check + drift report
|
|
||||||
veilor-doctor
|
|
||||||
|
|
||||||
# rollback to previous deployment
|
|
||||||
sudo bootc rollback
|
|
||||||
|
|
||||||
# inspect current and staged deployments
|
|
||||||
bootc status
|
|
||||||
```
|
|
||||||
|
|
||||||
### Troubleshooting
|
|
||||||
|
|
||||||
| Symptom | Try |
|
|
||||||
|---|---|
|
|
||||||
| `veilor-update` says "no rollback target" | First boot — bootc only has rollback after the first successful upgrade. Normal. |
|
|
||||||
| Network down inside Anaconda | Bootstrap ISO uses NetworkManager defaults; plug in ethernet for the first install. WiFi support post-first-boot. |
|
|
||||||
| `rpm-ostree install foo` fails | Run `bootc status` — if a staged deployment exists, reboot first, then re-try. rpm-ostree won't layer onto a staged tree. |
|
|
||||||
| First-login TUI didn't appear | Marker check: `ls /var/lib/veilor/postinstall-complete`. If present, run `sudo veilor-postinstall --force`. |
|
|
||||||
| GPU is black after NVIDIA layer + reboot | `bootc rollback` and try mesa first; check `journalctl -b -1 -u sddm` from the previous boot. |
|
|
||||||
|
|
||||||
### Where the OCI image comes from
|
|
||||||
|
|
||||||
The image is built by `.github/workflows/build-bluebuild.yml` on the
|
|
||||||
self-hosted Forgejo runner (label `nullstone`). Build inputs:
|
|
||||||
|
|
||||||
- Base: `ghcr.io/secureblue/kinoite-main-hardened`
|
|
||||||
- Recipe: [`bluebuild/recipe.yml`](../bluebuild/recipe.yml)
|
|
||||||
- Veilor overlay: stamped via BlueBuild `type: files` modules
|
|
||||||
- Layered RPMs: `sudo`, `xorg-x11-server-Xwayland`, `mullvad-browser`,
|
|
||||||
`tailscale`, `yggdrasil`
|
|
||||||
- Output: `git.s8n.ru/veilor-org/veilor-os:{43,latest}`
|
|
||||||
|
|
||||||
The build is cosign-signed (key-pair on Forgejo, keyless on GitHub
|
|
||||||
parallel mirror). See [`bluebuild/README.md`](../bluebuild/README.md)
|
|
||||||
for the recipe walk-through.
|
|
||||||
|
|
@ -9,22 +9,6 @@ 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
|
## ⚡ STRATEGY PIVOT — 2026-05-06
|
||||||
|
|
||||||
**Decision: skip v0.6 kickstart polish. Pivot directly to v0.7
|
**Decision: skip v0.6 kickstart polish. Pivot directly to v0.7
|
||||||
|
|
@ -43,12 +27,10 @@ Reasons:
|
||||||
`veilor-update`) translate cleanly to v0.7: `bootc upgrade` replaces
|
`veilor-update`) translate cleanly to v0.7: `bootc upgrade` replaces
|
||||||
`dnf upgrade`. Move them into v0.7 scope.
|
`dnf upgrade`. Move them into v0.7 scope.
|
||||||
|
|
||||||
**v0.5.0 is the final kickstart-path release.** Tagged on 2026-05-06,
|
**v0.5.0 is the final kickstart-path release.** Tag, freeze, ship as
|
||||||
shipped as proof-of-work / portfolio anchor. **v0.6 cancelled as a
|
proof-of-work / portfolio anchor. **v0.6 cancelled as a milestone.**
|
||||||
milestone.**
|
|
||||||
|
|
||||||
Active focus: `v0.7-bluebuild-spike` branch — first green CI run is
|
Active focus: `v0.7-bluebuild-spike` branch.
|
||||||
the gating blocker for everything downstream.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -118,31 +100,20 @@ failures before greening.
|
||||||
(`/etc/kernel/cmdline` + `/etc/default/grub` + grubby) plus explicit
|
(`/etc/kernel/cmdline` + `/etc/default/grub` + grubby) plus explicit
|
||||||
`kernel-install add`.
|
`kernel-install add`.
|
||||||
|
|
||||||
## v0.5.0 — final kickstart release (DONE 2026-05-06)
|
## v0.5.32 — next ship (active)
|
||||||
|
|
||||||
Tagged `v0.5.0` on 2026-05-06 as the final kickstart-path release.
|
Outstanding from the grind, immediate priority for the next tag:
|
||||||
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.
|
|
||||||
|
|
||||||
Shipped:
|
- **End-to-end VM green run** — v0.5.31 lands the kernel-cmdline fix
|
||||||
- ~2.7 GB live ISO via Forgejo CI on nullstone (EFI + BIOS bootable)
|
but no full hybrid-VM pass has signed it off. Run the procedure in
|
||||||
- `ci-latest` artifact at
|
`test/TESTING.md` to install + reboot + login, file the report in
|
||||||
`git.s8n.ru/veilor-org/veilor-os/releases/tag/ci-latest`
|
`test/test-runs/`, then tag.
|
||||||
- gum TUI installer wrapping Anaconda; LUKS2 argon2id + btrfs
|
- **Real-hardware run on the spare laptop** — VM is necessary not
|
||||||
- Full hardening overlay: SELinux enforcing, USBGuard default-block,
|
sufficient. Friend's laptop is mate's-test, spare is ours. KMS,
|
||||||
fail2ban + auditd, firewalld drop, NTS chrony, DoT
|
fbcon, USB controller, real-firmware Secure Boot only show up here.
|
||||||
- 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
|
- **gum input render glitch** — duplicate "Install", stray T in
|
||||||
password fields on linux fbcon. Replace `gum input --password` with
|
password fields on linux fbcon. Replace `gum input --password` with
|
||||||
bash `read -srp`; cosmetic only but visible on every install.
|
bash `read -srp`; cosmetic only but visible on every install.
|
||||||
Carries to v0.7 installer ISO, which inherits the gum TUI.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -273,35 +244,15 @@ distro from a kickstart.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## v0.7 — BlueBuild OCI mainline (IN FLIGHT — blocked on green CI run, 2026-05-08)
|
## v0.7 — BlueBuild OCI mainline (ACTIVE — primary focus 2026-05-06+)
|
||||||
|
|
||||||
This was originally planned as "public flex + bootc spike". Post-pivot,
|
This was originally planned as "public flex + bootc spike". Post-pivot,
|
||||||
v0.7 is now the **primary active milestone** — it absorbs all v0.6
|
v0.7 is now the **primary active milestone** — it absorbs all v0.6
|
||||||
ergonomic work and becomes the next ship target.
|
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:
|
Scope:
|
||||||
- BlueBuild recipe (`bluebuild/recipe.yml`) layering on
|
- BlueBuild recipe (`bluebuild/recipe.yml`) layering on
|
||||||
`ghcr.io/secureblue/kinoite-main-hardened`
|
`ghcr.io/secureblue/securecore-kinoite-hardened-userns`
|
||||||
- `kickstart/install-ostreecontainer.ks` — 10-line kickstart that calls
|
- `kickstart/install-ostreecontainer.ks` — 10-line kickstart that calls
|
||||||
`ostreecontainer --url=ghcr.io/veilor-org/veilor-os:43 --transport=registry`
|
`ostreecontainer --url=ghcr.io/veilor-org/veilor-os:43 --transport=registry`
|
||||||
and lets Anaconda's LUKS UX drive the install
|
and lets Anaconda's LUKS UX drive the install
|
||||||
|
|
@ -313,21 +264,6 @@ Scope:
|
||||||
- `veilor-update` rewritten on `bootc upgrade` (was `dnf upgrade`)
|
- `veilor-update` rewritten on `bootc upgrade` (was `dnf upgrade`)
|
||||||
- Forgejo registry as primary OCI publish target; GHCR mirror optional
|
- Forgejo registry as primary OCI publish target; GHCR mirror optional
|
||||||
- cosign key-pair signing of OCI image (replaces broken keyless flow)
|
- 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:
|
Public-flex items kept from original v0.7 entry:
|
||||||
|
|
||||||
|
|
@ -356,7 +292,7 @@ spike on `quay.io/fedora/fedora-bootc:43`. Research on 2026-05-05
|
||||||
`docs/research/2026-05-05-agent-wave/`), then a parent-operator
|
`docs/research/2026-05-05-agent-wave/`), then a parent-operator
|
||||||
refinement same day, locked the path: **layer veilor's branding +
|
refinement same day, locked the path: **layer veilor's branding +
|
||||||
threat model + UX on top of secureblue's already-shipping
|
threat model + UX on top of secureblue's already-shipping
|
||||||
`kinoite-main-hardened` OCI image** via a BlueBuild
|
`securecore-kinoite-hardened-userns` OCI image** via a BlueBuild
|
||||||
recipe, and install it directly during the Anaconda pass via the
|
recipe, and install it directly during the Anaconda pass via the
|
||||||
`ostreecontainer` kickstart directive (no first-boot rebase).
|
`ostreecontainer` kickstart directive (no first-boot rebase).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ Locked at: **v0.5.31 → v0.7 spike → v1.0**
|
||||||
works).
|
works).
|
||||||
- Anaconda's `ostreecontainer` directive populates the root filesystem
|
- Anaconda's `ostreecontainer` directive populates the root filesystem
|
||||||
directly from a **veilor-os OCI image** (built via BlueBuild on top
|
directly from a **veilor-os OCI image** (built via BlueBuild on top
|
||||||
of secureblue's `kinoite-main-hardened`) **during the
|
of secureblue's `securecore-kinoite-hardened-userns`) **during the
|
||||||
install pass — no first-boot rebase, no mutable→atomic transition**.
|
install pass — no first-boot rebase, no mutable→atomic transition**.
|
||||||
- All future updates flow through `bootc upgrade` — atomic A/B,
|
- All future updates flow through `bootc upgrade` — atomic A/B,
|
||||||
instant rollback, cosign-signed.
|
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
|
The Containerfile-from-scratch spike plan (Agent 3 of 2026-05-05
|
||||||
wave) is **superseded** by this hybrid: don't build a Containerfile
|
wave) is **superseded** by this hybrid: don't build a Containerfile
|
||||||
from scratch on `fedora-bootc:43`. Instead, write a BlueBuild recipe
|
from scratch on `fedora-bootc:43`. Instead, write a BlueBuild recipe
|
||||||
on `kinoite-main-hardened`. With `ostreecontainer`
|
on `securecore-kinoite-hardened-userns`. With `ostreecontainer`
|
||||||
swap, spike compresses 1 week → 1 day.
|
swap, spike compresses 1 week → 1 day.
|
||||||
|
|
||||||
## Next concrete steps
|
## Next concrete steps
|
||||||
|
|
@ -254,7 +254,7 @@ in the v0.7 spike branch only.
|
||||||
### v0.7-spike (1 day, separate branch)
|
### v0.7-spike (1 day, separate branch)
|
||||||
|
|
||||||
1. New repo dir: `bluebuild/recipe.yml`.
|
1. New repo dir: `bluebuild/recipe.yml`.
|
||||||
2. `from`: `ghcr.io/secureblue/kinoite-main-hardened:latest`.
|
2. `from`: `ghcr.io/secureblue/securecore-kinoite-hardened-userns:latest`.
|
||||||
3. Override modules:
|
3. Override modules:
|
||||||
- `type: files` — stamp our `overlay/*` tree (branding, themes,
|
- `type: files` — stamp our `overlay/*` tree (branding, themes,
|
||||||
veilor scripts, sddm theme, plymouth theme).
|
veilor scripts, sddm theme, plymouth theme).
|
||||||
|
|
@ -307,7 +307,7 @@ Primary git host moved off github.com. **Forgejo** runs on nullstone
|
||||||
at `git.s8n.ru`, with **forgejo-runner** doing the build work. GH free-
|
at `git.s8n.ru`, with **forgejo-runner** doing the build work. GH free-
|
||||||
tier minute quota was hammering veilor-os iteration; we self-host now.
|
tier minute quota was hammering veilor-os iteration; we self-host now.
|
||||||
|
|
||||||
- Primary remote: `ssh://git@192.168.0.100:222/veilor-org/veilor-os.git`
|
- Primary remote: `ssh://git@<NULLSTONE_LAN_IP>:222/veilor-org/veilor-os.git`
|
||||||
(Forgejo, LAN-only until router port-forward 222 → nullstone:222
|
(Forgejo, LAN-only until router port-forward 222 → nullstone:222
|
||||||
added — TODO; or use tailnet hostname once tailscale logged in).
|
added — TODO; or use tailnet hostname once tailscale logged in).
|
||||||
- Public mirror: `https://github.com/veilor-org/veilor-os.git`. Forgejo
|
- Public mirror: `https://github.com/veilor-org/veilor-os.git`. Forgejo
|
||||||
|
|
@ -334,29 +334,3 @@ dir.
|
||||||
- Yggdrasil: <https://github.com/yggdrasil-network/yggdrasil-go>
|
- Yggdrasil: <https://github.com/yggdrasil-network/yggdrasil-go>
|
||||||
- Reticulum manual: <https://reticulum.network/manual/>
|
- Reticulum manual: <https://reticulum.network/manual/>
|
||||||
- Iroh blobs design: <https://github.com/n0-computer/iroh-blobs/blob/main/DESIGN.md>
|
- 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.
|
|
||||||
|
|
|
||||||
|
|
@ -1,123 +0,0 @@
|
||||||
# veilor-os installer kickstart — v0.7 CI build variant
|
|
||||||
#
|
|
||||||
# Derived from kickstart/install-ostreecontainer.ks by stripping all
|
|
||||||
# __PLACEHOLDER__ tokens that the runtime gum TUI substitutes at install
|
|
||||||
# time. Anaconda's interactive TUI handles disk selection, LUKS passphrase,
|
|
||||||
# and user account creation in their place.
|
|
||||||
#
|
|
||||||
# Consumed by livemedia-creator --make-iso to produce
|
|
||||||
# veilor-os-installer-43-*.iso. Do NOT add __PLACEHOLDER__ tokens here —
|
|
||||||
# they cannot be filled at build time. See install-ostreecontainer.ks
|
|
||||||
# for the runtime template the gum TUI fills in.
|
|
||||||
|
|
||||||
# ── Locale / keyboard / time ──
|
|
||||||
keyboard --xlayouts='us'
|
|
||||||
lang en_US.UTF-8
|
|
||||||
timezone Europe/London --utc
|
|
||||||
|
|
||||||
# ── Install mode ──
|
|
||||||
text
|
|
||||||
firstboot --disable
|
|
||||||
eula --agreed
|
|
||||||
selinux --enforcing
|
|
||||||
|
|
||||||
# ── Network ──
|
|
||||||
network --bootproto=dhcp --device=link --activate --hostname=veilor-install
|
|
||||||
firewall --enabled --service=ssh
|
|
||||||
|
|
||||||
# ── Identity ──
|
|
||||||
# rootpw --lock only. No user directive — Anaconda's user spoke handles
|
|
||||||
# admin account creation interactively. Runtime ks substitutes
|
|
||||||
# --password=__ADMIN_PW__ for unattended installs.
|
|
||||||
rootpw --lock
|
|
||||||
|
|
||||||
# ── Disk / partitioning ──
|
|
||||||
# Intentionally absent. Anaconda's disk spoke presents interactive
|
|
||||||
# disk + LUKS + btrfs selection. Runtime ks (gum TUI) provides the
|
|
||||||
# full partition spec at real-install time.
|
|
||||||
|
|
||||||
# ── 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
|
|
||||||
|
|
@ -1,80 +0,0 @@
|
||||||
# veilor-os install kickstart — v0.7 spike (ostreecontainer path)
|
|
||||||
#
|
|
||||||
# This is the install-time kickstart for the v0.7 hybrid path. The live
|
|
||||||
# ISO boots; the gum TUI collects user answers (disk, LUKS pw, admin pw);
|
|
||||||
# this template gets the answers substituted in and is fed to anaconda.
|
|
||||||
#
|
|
||||||
# Anaconda partitions the disk + creates LUKS + btrfs subvols + mounts
|
|
||||||
# /boot/efi + /boot, then `ostreecontainer` populates `/` directly from
|
|
||||||
# the cosign-signed veilor-os OCI image at `ghcr.io/veilor-org/veilor-os:43`.
|
|
||||||
#
|
|
||||||
# No `%packages` block. No first-boot rebase. No
|
|
||||||
# `veilor-firstboot-rebase.service`. The ostreecontainer install pass is
|
|
||||||
# the entire transition from "Fedora live ISO" to "veilor-os on disk".
|
|
||||||
#
|
|
||||||
# Reference: pykickstart docs ostreecontainer command;
|
|
||||||
# https://docs.fedoraproject.org/en-US/bootc/anaconda-install/
|
|
||||||
|
|
||||||
# ── Locale / keyboard / time ──
|
|
||||||
keyboard --xlayouts='us'
|
|
||||||
lang en_US.UTF-8
|
|
||||||
timezone Europe/London --utc
|
|
||||||
|
|
||||||
# ── Install mode / behaviour ──
|
|
||||||
firstboot --disable
|
|
||||||
eula --agreed
|
|
||||||
# SELinux state inherited from the OCI image; --enforcing is implicit
|
|
||||||
# since secureblue's image ships /etc/selinux/config = enforcing.
|
|
||||||
selinux --enforcing
|
|
||||||
|
|
||||||
# ── Network / hostname ──
|
|
||||||
network --bootproto=dhcp --device=link --activate --hostname=__HOSTNAME__
|
|
||||||
firewall --enabled --service=ssh
|
|
||||||
|
|
||||||
# ── Identity (single LUKS prompt asked at install via gum TUI) ──
|
|
||||||
rootpw --lock
|
|
||||||
user --name=admin --groups=wheel --gecos="veilor admin" --password=__ADMIN_PW__ --plaintext
|
|
||||||
|
|
||||||
# ── Bootloader ──
|
|
||||||
# fbcon=nodefer for laptop KMS handoff (real-hardware audit, agent 9 of
|
|
||||||
# 2026-05-05 wave). rd.luks.options=tries=5,timeout=0 for UX.
|
|
||||||
# rd.luks.uuid is auto-injected by anaconda based on the encrypted
|
|
||||||
# part directive below.
|
|
||||||
#
|
|
||||||
# All other hardening kargs (lockdown=integrity, slab_nomerge, etc.)
|
|
||||||
# come from /usr/lib/bootc/kargs.d/ inside the OCI image — bootc
|
|
||||||
# applies them at install time. We only add what the OCI image can't
|
|
||||||
# know (laptop-specific KMS flag).
|
|
||||||
bootloader --append="fbcon=nodefer"
|
|
||||||
|
|
||||||
# ── Disk: LUKS2 (argon2id) + btrfs subvols ──
|
|
||||||
zerombr
|
|
||||||
clearpart --all --initlabel --drives=__DISK_BASENAME__
|
|
||||||
part /boot/efi --fstype=efi --size=600
|
|
||||||
part /boot --fstype=ext4 --size=1024
|
|
||||||
part btrfs.veilor --grow --encrypted --luks-version=luks2 --pbkdf=argon2id --passphrase=__LUKS_PW__
|
|
||||||
btrfs none --label=veilor btrfs.veilor
|
|
||||||
btrfs / --subvol --name=root LABEL=veilor
|
|
||||||
btrfs /home --subvol --name=home LABEL=veilor
|
|
||||||
|
|
||||||
# ── ostreecontainer: populate / from the veilor-os OCI image ──
|
|
||||||
# `--transport=registry` pulls from ghcr.io directly. Authentication
|
|
||||||
# token can be supplied via /etc/ostree/auth.json baked into the live
|
|
||||||
# rootfs OR via a kickstart `--remote-token` if the registry is private.
|
|
||||||
# At v0.7 spike the OCI image is public, so no auth needed.
|
|
||||||
#
|
|
||||||
# DO NOT migrate to the new `bootc` kickstart command until v1.0 — it
|
|
||||||
# blocks multi-disk and authenticated registries (per parent-operator
|
|
||||||
# handoff 2026-05-05).
|
|
||||||
ostreecontainer --url=ghcr.io/veilor-org/veilor-os:43 --transport=registry
|
|
||||||
|
|
||||||
# ── %post (chroot) — minimal; OCI image already has everything ──
|
|
||||||
# What we keep:
|
|
||||||
# - chage -d 0 admin so first SDDM login forces password change
|
|
||||||
# - hostname write (anaconda's --hostname doesn't always survive)
|
|
||||||
# - veilor-firstboot.service is enabled in the OCI image already
|
|
||||||
%post
|
|
||||||
set -uo pipefail
|
|
||||||
echo veilor > /etc/hostname
|
|
||||||
chage -d 0 admin || true
|
|
||||||
%end
|
|
||||||
|
|
@ -271,41 +271,14 @@ sed -i \
|
||||||
plymouth-set-default-theme details 2>/dev/null || true
|
plymouth-set-default-theme details 2>/dev/null || true
|
||||||
[ -f /boot/grub2/grub.cfg ] && grub2-mkconfig -o /boot/grub2/grub.cfg 2>/dev/null || true
|
[ -f /boot/grub2/grub.cfg ] && grub2-mkconfig -o /boot/grub2/grub.cfg 2>/dev/null || true
|
||||||
|
|
||||||
# zram swap (no disk swap; keys never leak to platter).
|
# zram swap (no disk swap; keys never leak to platter)
|
||||||
#
|
|
||||||
# Sizing: 16 GiB compressed (zstd ~3:1 → ~48 GiB effective). Default 8G
|
|
||||||
# filled under sustained pressure on 32+ GiB laptops running browsers +
|
|
||||||
# LSP + chat → kernel OOM (no disk-swap fallback per threat model). See
|
|
||||||
# overlay/etc/systemd/zram-generator.conf and docs/HARDENING.md "Memory
|
|
||||||
# pressure" for full rationale.
|
|
||||||
dnf install -y zram-generator || true
|
dnf install -y zram-generator || true
|
||||||
cat > /etc/systemd/zram-generator.conf << 'EOF'
|
cat > /etc/systemd/zram-generator.conf << 'EOF'
|
||||||
[zram0]
|
[zram0]
|
||||||
zram-size = min(ram, 16384)
|
zram-size = min(ram, 8192)
|
||||||
compression-algorithm = zstd
|
compression-algorithm = zstd
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Memory-pressure sysctl tuning for zram-only stack. Default vm.swappiness
|
|
||||||
# assumes a slow disk; on zram the kernel must be told to swap early
|
|
||||||
# (180) and reclaim early (watermark_scale_factor=125) so it never gets
|
|
||||||
# cornered into kernel-OOM. page-cluster=0 disables read-ahead which is
|
|
||||||
# pointless on RAM-backed swap. See overlay/etc/sysctl.d/95-memory-pressure.conf
|
|
||||||
# and docs/HARDENING.md "Memory pressure" for the rationale + failure mode.
|
|
||||||
cat > /etc/sysctl.d/95-memory-pressure.conf << 'EOF'
|
|
||||||
vm.swappiness = 180
|
|
||||||
vm.watermark_scale_factor = 125
|
|
||||||
vm.page-cluster = 0
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# systemd-oomd: userspace OOM killer that uses PSI (pressure stall info)
|
|
||||||
# to pick a victim cgroup BEFORE the kernel's global OOM reaper fires.
|
|
||||||
# Without oomd the kernel waits until total exhaustion then picks by
|
|
||||||
# oom_score, often killing plasmashell or the active terminal instead of
|
|
||||||
# the runaway browser tab. Fedora ships systemd-oomd-defaults with sane
|
|
||||||
# thresholds for user.slice cgroups.
|
|
||||||
dnf install -y systemd-oomd-defaults || true
|
|
||||||
systemctl enable systemd-oomd.service || true
|
|
||||||
|
|
||||||
# Patch anaconda's transaction_progress.py inside the live rootfs so that
|
# Patch anaconda's transaction_progress.py inside the live rootfs so that
|
||||||
# when the user clicks "Install", a non-fatal RPM 6.0 *scriptlet* warning
|
# when the user clicks "Install", a non-fatal RPM 6.0 *scriptlet* warning
|
||||||
# does not get escalated to "An error occurred during the transaction"
|
# does not get escalated to "An error occurred during the transaction"
|
||||||
|
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
[Desktop Entry]
|
|
||||||
# Shadow /etc/xdg/autostart/org.kde.discover.notifier.desktop.
|
|
||||||
# Auto-launching the Discover updater at session start stacks
|
|
||||||
# CPU/IO load with packagekit + dnf-makecache + fwupd-refresh.
|
|
||||||
# Users can still launch Discover manually; updates also happen
|
|
||||||
# via dnf5-automatic.timer. This only suppresses the autostart.
|
|
||||||
Type=Application
|
|
||||||
Name=Discover Update Notifier
|
|
||||||
Exec=true
|
|
||||||
Hidden=true
|
|
||||||
X-KDE-autostart-condition=
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
[Unit]
|
|
||||||
Description=User background services (low priority)
|
|
||||||
|
|
||||||
# For per-user cloud-sync / indexer / backup tools the user opts into
|
|
||||||
# (Syncthing, rclone, file indexers, etc). Drop a service drop-in at
|
|
||||||
# ~/.config/systemd/user/<unit>.service.d/10-bg.conf with:
|
|
||||||
# [Service]
|
|
||||||
# Slice=user-bg.slice
|
|
||||||
# Nice=10
|
|
||||||
# IOSchedulingClass=idle
|
|
||||||
|
|
||||||
[Slice]
|
|
||||||
CPUWeight=30
|
|
||||||
IOWeight=50
|
|
||||||
MemoryHigh=3G
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
# veilor-os — memory-pressure tuning for zram-only swap
|
|
||||||
#
|
|
||||||
# Rationale: veilor-os ships zram swap with NO disk swap (see THREAT-MODEL.md
|
|
||||||
# §"Lost or stolen laptop"). The kernel's default vm.* knobs assume a slow
|
|
||||||
# spinning disk and refuse to swap until physical RAM is nearly exhausted.
|
|
||||||
# Under a zram-only stack that policy is wrong on two axes:
|
|
||||||
#
|
|
||||||
# 1. zram is RAM-fast — there is no penalty for swapping early, only a
|
|
||||||
# small CPU cost for zstd compress/decompress.
|
|
||||||
# 2. Once zram fills, there is no overflow (no disk swap by design), so
|
|
||||||
# the kernel falls through to OOM. With default knobs the OOM trigger
|
|
||||||
# is slow and reactive: by the time it fires, the system has spent
|
|
||||||
# minutes in thrash (compositor/input frozen, mouse stuck) and the
|
|
||||||
# kernel picks a victim by oom_score which is often plasmashell or
|
|
||||||
# the terminal — i.e. the user's session goes down, not the runaway.
|
|
||||||
#
|
|
||||||
# What these knobs do:
|
|
||||||
#
|
|
||||||
# vm.swappiness = 180
|
|
||||||
# Tell the kernel to prefer evicting anonymous pages to (zram) swap
|
|
||||||
# over reclaiming file-backed pages. Fedora's zram-generator upstream
|
|
||||||
# recommends 180 for zram-only systems. Default 60 is tuned for HDD
|
|
||||||
# swap and leaves zram unused until too late.
|
|
||||||
#
|
|
||||||
# vm.watermark_scale_factor = 125
|
|
||||||
# Start kswapd reclaim earlier (~1.25% of RAM headroom vs default
|
|
||||||
# 0.1%). On a 32 GiB box that's ~400 MiB head start before allocations
|
|
||||||
# would otherwise stall in direct-reclaim. Trades a tiny amount of
|
|
||||||
# usable RAM for much smoother latency under bursty allocators
|
|
||||||
# (Chromium/Electron tab spawns, language server warm-up).
|
|
||||||
#
|
|
||||||
# vm.page-cluster = 0
|
|
||||||
# Read one page per swap-in instead of the default 8. Read-ahead is a
|
|
||||||
# win on rotational media because seeks dominate; on zram the seek
|
|
||||||
# cost is zero and grabbing 7 extra pages just wastes decompress
|
|
||||||
# cycles and CPU cache. Setting to 0 is the documented zram tuning.
|
|
||||||
#
|
|
||||||
# Companion: systemd-oomd is enabled in the same change so PSI-based
|
|
||||||
# pre-OOM kills land on the right cgroup before the kernel OOM reaper
|
|
||||||
# fires. Without it, even with these knobs the system can still wedge
|
|
||||||
# briefly while the kernel waits for the global watermark.
|
|
||||||
|
|
||||||
vm.swappiness = 180
|
|
||||||
vm.watermark_scale_factor = 125
|
|
||||||
vm.page-cluster = 0
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
[Service]
|
|
||||||
Slice=system-bg.slice
|
|
||||||
Nice=10
|
|
||||||
IOSchedulingClass=idle
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
[Timer]
|
|
||||||
# Default OnBootSec fires the makecache job near login, stacking
|
|
||||||
# CPU/IO load with the desktop session bring-up. 20min delay puts
|
|
||||||
# the refresh past peak session-start activity.
|
|
||||||
OnBootSec=20min
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
[Service]
|
|
||||||
Slice=system-bg.slice
|
|
||||||
Nice=10
|
|
||||||
IOSchedulingClass=idle
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
[Service]
|
|
||||||
Slice=system-bg.slice
|
|
||||||
Nice=10
|
|
||||||
IOSchedulingClass=idle
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
[Service]
|
|
||||||
Slice=system-bg.slice
|
|
||||||
Nice=10
|
|
||||||
IOSchedulingClass=idle
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
[Service]
|
|
||||||
Slice=system-bg.slice
|
|
||||||
Nice=10
|
|
||||||
IOSchedulingClass=idle
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
[Unit]
|
|
||||||
Description=Background system services (low priority)
|
|
||||||
Documentation=https://git.s8n.ru/veilor-org/veilor-os/src/branch/main/docs
|
|
||||||
Before=slices.target
|
|
||||||
|
|
||||||
# Holds dnf metadata refresh, PackageKit, fwupd, and other deferrable
|
|
||||||
# system maintenance. CPUWeight=20 vs default 100 means these yield
|
|
||||||
# 5:1 to the rest of system.slice under contention; idle systems still
|
|
||||||
# get full speed. MemoryHigh=4G is a soft cap — kernel reclaims pages
|
|
||||||
# rather than evicting interactive workloads when these grow.
|
|
||||||
|
|
||||||
[Slice]
|
|
||||||
CPUWeight=20
|
|
||||||
IOWeight=50
|
|
||||||
MemoryHigh=4G
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
[Slice]
|
|
||||||
# Logged-in user sessions get 3x weight vs default. Combined with
|
|
||||||
# system-bg.slice CPUWeight=20, ratio is 15:1 in the interactive
|
|
||||||
# session's favour when CPU is contended — kwin/plasmashell win
|
|
||||||
# scheduling over dnf-makecache / fwupd-refresh / packagekit.
|
|
||||||
CPUWeight=300
|
|
||||||
IOWeight=200
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
[Unit]
|
|
||||||
Description=veilor-doctor — system health + drift check
|
|
||||||
After=network-online.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=oneshot
|
|
||||||
ExecStart=/usr/local/bin/veilor-doctor --quiet
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
[Unit]
|
|
||||||
Description=veilor-doctor weekly drift check
|
|
||||||
|
|
||||||
[Timer]
|
|
||||||
OnCalendar=weekly
|
|
||||||
Persistent=true
|
|
||||||
RandomizedDelaySec=30m
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=timers.target
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
[Unit]
|
|
||||||
Description=veilor-os one-time post-install TUI (first login)
|
|
||||||
After=graphical.target
|
|
||||||
ConditionPathExists=!/var/lib/veilor/postinstall-complete
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=oneshot
|
|
||||||
ExecStart=/usr/local/bin/veilor-postinstall
|
|
||||||
StandardInput=tty
|
|
||||||
StandardOutput=tty
|
|
||||||
StandardError=journal
|
|
||||||
TTYPath=/dev/tty1
|
|
||||||
TTYReset=yes
|
|
||||||
TTYVHangup=yes
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=graphical.target multi-user.target
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
# veilor-os — zram swap override
|
|
||||||
#
|
|
||||||
# Replaces the Fedora default config (which would otherwise set
|
|
||||||
# zram-size = min(ram, 8192) with whatever compression algorithm
|
|
||||||
# zram-generator picked, historically lzo-rle).
|
|
||||||
#
|
|
||||||
# Sizing rationale: 16 GiB compressed (typical 3:1 with zstd → ~48 GiB
|
|
||||||
# effective). Default 8 GiB filled under sustained pressure on modern
|
|
||||||
# 32+ GiB laptops running browsers + LSP + chat clients, leaving the
|
|
||||||
# kernel with no swap headroom and triggering OOM (since veilor-os has
|
|
||||||
# no disk swap fallback — see THREAT-MODEL.md "no key leak risk").
|
|
||||||
#
|
|
||||||
# Algorithm: zstd. lzo-rle is faster but ratio ~2:1; zstd is ~3:1 with
|
|
||||||
# negligible CPU cost on any post-2018 x86_64. The extra 50% effective
|
|
||||||
# swap capacity is worth more than the microseconds of compress time.
|
|
||||||
|
|
||||||
[zram0]
|
|
||||||
zram-size = min(ram, 16384)
|
|
||||||
compression-algorithm = zstd
|
|
||||||
|
|
@ -147,30 +147,12 @@ PUBLIC_IP=$(curl -s --max-time 3 ifconfig.me 2>/dev/null || echo "")
|
||||||
|| check Network public_ip fail "lookup timed out"
|
|| check Network public_ip fail "lookup timed out"
|
||||||
|
|
||||||
# ── 5. Updates ──────────────────────────────────────────────────────
|
# ── 5. Updates ──────────────────────────────────────────────────────
|
||||||
# v0.7+ atomic — bootc replaces dnf as the update channel. Parse
|
|
||||||
# `bootc status --json` for the booted deployment + staged/cached image
|
|
||||||
# age. Fall back to dnf history if bootc not present (legacy v0.5.x).
|
|
||||||
if have bootc; then
|
|
||||||
BOOTC_JSON=$(sudo -n bootc status --json 2>/dev/null || echo "")
|
|
||||||
if [[ -n $BOOTC_JSON ]] && have jq; then
|
|
||||||
BOOTED_IMG=$(jq -r '.status.booted.image.image.image // "unknown"' <<<"$BOOTC_JSON")
|
|
||||||
BOOTED_DIGEST=$(jq -r '.status.booted.image.imageDigest // ""' <<<"$BOOTC_JSON")
|
|
||||||
check Updates booted_image pass "${BOOTED_IMG}@${BOOTED_DIGEST:0:12}"
|
|
||||||
STAGED=$(jq -r '.status.staged.image.image.image // ""' <<<"$BOOTC_JSON")
|
|
||||||
if [[ -n $STAGED ]]; then
|
|
||||||
check Updates staged_image fail "staged: $STAGED — reboot to apply"
|
|
||||||
else
|
|
||||||
check Updates staged_image pass "no staged update"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
check Updates bootc_state pass "bootc present (jq missing — install for richer detail)"
|
|
||||||
fi
|
|
||||||
elif have dnf; then
|
|
||||||
# Legacy v0.5.x kickstart-installed system.
|
|
||||||
LAST_DNF=$(sudo -n dnf history list 2>/dev/null \
|
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}')
|
| 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" \
|
[[ -n $LAST_DNF ]] && check Updates last_dnf pass "$LAST_DNF" \
|
||||||
|| check Updates last_dnf pass "(unknown — try \`sudo dnf history\`)"
|
|| 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
|
sudo -n dnf check-update -q >/dev/null 2>&1
|
||||||
RC=$?
|
RC=$?
|
||||||
case $RC in
|
case $RC in
|
||||||
|
|
@ -182,9 +164,6 @@ elif have dnf; then
|
||||||
;;
|
;;
|
||||||
*) check Updates pending fail "dnf check-update returned $RC (need sudo?)" ;;
|
*) check Updates pending fail "dnf check-update returned $RC (need sudo?)" ;;
|
||||||
esac
|
esac
|
||||||
else
|
|
||||||
check Updates channel fail "neither bootc nor dnf available"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── 6. veilor services ──────────────────────────────────────────────
|
# ── 6. veilor services ──────────────────────────────────────────────
|
||||||
for unit in veilor-firstboot.service veilor-modules-lock.service; do
|
for unit in veilor-firstboot.service veilor-modules-lock.service; do
|
||||||
|
|
|
||||||
|
|
@ -1,178 +0,0 @@
|
||||||
#!/usr/bin/bash
|
|
||||||
# veilor-postinstall — first-login TUI on v0.7+ atomic systems.
|
|
||||||
#
|
|
||||||
# Runs ONCE on first SDDM login via the user-mode systemd unit
|
|
||||||
# `veilor-postinstall.service`. Asks the operator for the small set
|
|
||||||
# of decisions we deliberately defer from install time:
|
|
||||||
# - keyboard / locale
|
|
||||||
# - hostname override
|
|
||||||
# - GPU drivers (NVIDIA layered via rpm-ostree, mesa = no-op)
|
|
||||||
# - package preset (dev / media / homelab — additive, opt-out)
|
|
||||||
# - bluetooth opt-in
|
|
||||||
# - USBGuard policy snapshot
|
|
||||||
# - veilor-doctor first run
|
|
||||||
# Writes /var/lib/veilor/postinstall-complete on success and disables
|
|
||||||
# its own autostart unit. Idempotent: safe to re-run.
|
|
||||||
#
|
|
||||||
# Style: gum if present, plain bash read fallback. No decorative ASCII.
|
|
||||||
|
|
||||||
set -uo pipefail
|
|
||||||
export TERM="${TERM:-linux}"
|
|
||||||
|
|
||||||
STATE_DIR=/var/lib/veilor
|
|
||||||
DONE_MARKER="$STATE_DIR/postinstall-complete"
|
|
||||||
LOG=/var/log/veilor-postinstall.log
|
|
||||||
|
|
||||||
have() { command -v "$1" >/dev/null 2>&1; }
|
|
||||||
GUM=$(have gum && echo gum || echo "")
|
|
||||||
|
|
||||||
# Always log + tee to stdout for live progress.
|
|
||||||
mkdir -p "$STATE_DIR" 2>/dev/null || true
|
|
||||||
exec > >(tee -a "$LOG") 2>&1
|
|
||||||
|
|
||||||
if [[ -e $DONE_MARKER && ${1:-} != "--force" ]]; then
|
|
||||||
echo "veilor-postinstall already ran (marker: $DONE_MARKER). Pass --force to re-run."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── Wrappers ────────────────────────────────────────────────────────
|
|
||||||
choose() {
|
|
||||||
local header=$1; shift
|
|
||||||
if [[ -n $GUM ]]; then
|
|
||||||
gum choose --header "$header" "$@"
|
|
||||||
else
|
|
||||||
echo
|
|
||||||
echo "$header"
|
|
||||||
local i=1
|
|
||||||
for opt in "$@"; do printf ' %d) %s\n' "$i" "$opt"; ((i++)); done
|
|
||||||
local n
|
|
||||||
read -rp " choice (1-$#): " n
|
|
||||||
[[ $n -ge 1 && $n -le $# ]] || return 1
|
|
||||||
eval "echo \${$n}"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
ask() {
|
|
||||||
local prompt=$1 default=${2:-}
|
|
||||||
if [[ -n $GUM ]]; then
|
|
||||||
gum input --header "$prompt" --value "$default"
|
|
||||||
else
|
|
||||||
local v
|
|
||||||
read -rp "$prompt [$default] " v
|
|
||||||
echo "${v:-$default}"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
confirm() {
|
|
||||||
local prompt=$1
|
|
||||||
if [[ -n $GUM ]]; then
|
|
||||||
gum confirm "$prompt" && return 0 || return 1
|
|
||||||
else
|
|
||||||
read -rp "$prompt [y/N] " y
|
|
||||||
[[ ${y,,} == y* ]]
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
say() {
|
|
||||||
if [[ -n $GUM ]]; then
|
|
||||||
gum style --foreground 212 --bold "$1"
|
|
||||||
else
|
|
||||||
printf '\n=== %s ===\n' "$1"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Need root for several actions; re-exec under sudo if not root.
|
|
||||||
if [[ $EUID -ne 0 ]]; then
|
|
||||||
say "veilor-postinstall: sudo required"
|
|
||||||
exec sudo -E bash "$0" "$@"
|
|
||||||
fi
|
|
||||||
|
|
||||||
say "veilor-postinstall — one-time setup"
|
|
||||||
echo " This runs once. Each step is skippable. Defaults are sane."
|
|
||||||
echo
|
|
||||||
|
|
||||||
# ── 1. Keyboard layout ──────────────────────────────────────────────
|
|
||||||
KB=$(choose "Keyboard layout" us gb de fr es ru "skip") || KB=skip
|
|
||||||
if [[ $KB != skip ]]; then
|
|
||||||
localectl set-keymap "$KB" 2>/dev/null || true
|
|
||||||
echo " [OK] keymap = $KB"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── 2. Locale ───────────────────────────────────────────────────────
|
|
||||||
LOC=$(choose "Locale" en_US.UTF-8 en_GB.UTF-8 de_DE.UTF-8 fr_FR.UTF-8 "skip") || LOC=skip
|
|
||||||
if [[ $LOC != skip ]]; then
|
|
||||||
localectl set-locale LANG="$LOC" 2>/dev/null || true
|
|
||||||
echo " [OK] locale = $LOC"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── 3. Hostname ─────────────────────────────────────────────────────
|
|
||||||
HN=$(ask "Hostname" "veilor")
|
|
||||||
if [[ -n $HN && $HN != $(hostnamectl --static 2>/dev/null) ]]; then
|
|
||||||
hostnamectl set-hostname "$HN"
|
|
||||||
echo " [OK] hostname = $HN"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── 4. GPU drivers ──────────────────────────────────────────────────
|
|
||||||
GPU=$(choose "GPU drivers" "Skip (use mesa defaults)" "NVIDIA proprietary (akmod-nvidia)" "Intel/AMD mesa (no-op)") || GPU=skip
|
|
||||||
case "$GPU" in
|
|
||||||
*NVIDIA*)
|
|
||||||
say "Layering NVIDIA driver — this takes a few minutes"
|
|
||||||
rpm-ostree install --idempotent akmod-nvidia xorg-x11-drv-nvidia-cuda \
|
|
||||||
&& echo " [OK] NVIDIA driver layered (reboot to use)" \
|
|
||||||
|| echo " [WARN] NVIDIA layer failed; check rpm-ostree status"
|
|
||||||
;;
|
|
||||||
*) echo " (skipped GPU layering)" ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# ── 5. Package presets (multi-select) ───────────────────────────────
|
|
||||||
say "Package presets — pick any combination (skip = none)"
|
|
||||||
PRESET_DEV="git tmux vim-enhanced htop podman skopeo"
|
|
||||||
PRESET_MEDIA="vlc obs-studio"
|
|
||||||
PRESET_HOMELAB="wireguard-tools jq yq tmux"
|
|
||||||
|
|
||||||
PICKED=()
|
|
||||||
confirm "Install dev preset? ($PRESET_DEV)" && PICKED+=($PRESET_DEV) || true
|
|
||||||
confirm "Install media preset? ($PRESET_MEDIA)" && PICKED+=($PRESET_MEDIA) || true
|
|
||||||
confirm "Install homelab preset? ($PRESET_HOMELAB)" && PICKED+=($PRESET_HOMELAB) || true
|
|
||||||
if (( ${#PICKED[@]} > 0 )); then
|
|
||||||
# de-dupe
|
|
||||||
UNIQ=$(printf '%s\n' "${PICKED[@]}" | sort -u | tr '\n' ' ')
|
|
||||||
say "Layering: $UNIQ"
|
|
||||||
rpm-ostree install --idempotent $UNIQ \
|
|
||||||
&& echo " [OK] preset packages layered (reboot to use)" \
|
|
||||||
|| echo " [WARN] preset layer failed; check rpm-ostree status"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── 6. Bluetooth ────────────────────────────────────────────────────
|
|
||||||
if confirm "Enable Bluetooth?"; then
|
|
||||||
systemctl enable --now bluetooth.service 2>/dev/null || true
|
|
||||||
echo " [OK] bluetooth enabled"
|
|
||||||
else
|
|
||||||
echo " (skipped bluetooth)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── 7. USBGuard snapshot ────────────────────────────────────────────
|
|
||||||
say "USBGuard policy snapshot"
|
|
||||||
echo " Plug in EVERY USB device you trust right now (keyboard,"
|
|
||||||
echo " mouse, dock, yubikey, etc.) before continuing."
|
|
||||||
if confirm "Snapshot current USB devices into the allowlist?"; then
|
|
||||||
usbguard generate-policy > /etc/usbguard/rules.conf \
|
|
||||||
&& echo " [OK] policy written to /etc/usbguard/rules.conf" \
|
|
||||||
|| echo " [WARN] generate-policy failed"
|
|
||||||
systemctl restart usbguard 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── 8. veilor-doctor ────────────────────────────────────────────────
|
|
||||||
if confirm "Run veilor-doctor now?"; then
|
|
||||||
veilor-doctor || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── Done ────────────────────────────────────────────────────────────
|
|
||||||
date -u +"%Y-%m-%dT%H:%M:%SZ" > "$DONE_MARKER"
|
|
||||||
say "veilor-postinstall complete"
|
|
||||||
echo " Marker written: $DONE_MARKER"
|
|
||||||
echo " Disabling autostart unit so this never runs again."
|
|
||||||
systemctl --user --global disable veilor-postinstall.service 2>/dev/null || true
|
|
||||||
systemctl disable veilor-postinstall.service 2>/dev/null || true
|
|
||||||
echo
|
|
||||||
echo " If you layered any packages or drivers, reboot to activate."
|
|
||||||
|
|
@ -1,22 +1,25 @@
|
||||||
#!/usr/bin/bash
|
#!/usr/bin/bash
|
||||||
# veilor-update — atomic update wrapper for v0.7+ (bootc + rpm-ostree).
|
# veilor-update — system update wrapper.
|
||||||
#
|
# Wraps `dnf upgrade --refresh` + `flatpak update` behind a single command.
|
||||||
# Wraps `bootc upgrade` + flatpak update behind a single command.
|
# User-facing CLI shipped in /usr/local/bin/. v0.6 ergonomic tooling.
|
||||||
# Pre-checks rollback availability, pauses auditd while staging the
|
|
||||||
# new image, prints a clear post-state summary, and offers reboot.
|
|
||||||
#
|
#
|
||||||
# Exit codes:
|
# Exit codes:
|
||||||
# 0 success (with or without pending reboot)
|
# 0 success
|
||||||
# 1 bootc upgrade failed
|
# 1 dnf failed
|
||||||
# 2 flatpak failed (bootc still ran successfully)
|
# 2 flatpak failed (dnf still ran successfully)
|
||||||
# 3 no network
|
# 3 no network
|
||||||
|
#
|
||||||
|
# Uses `gum` for spinner output if present, falls back to plain stdout.
|
||||||
|
|
||||||
set -uo pipefail
|
set -uo pipefail
|
||||||
|
|
||||||
|
# ── Helpers ─────────────────────────────────────────────────────────
|
||||||
have() { command -v "$1" >/dev/null 2>&1; }
|
have() { command -v "$1" >/dev/null 2>&1; }
|
||||||
|
|
||||||
GUM=$(have gum && echo gum || echo "")
|
GUM=$(have gum && echo gum || echo "")
|
||||||
|
|
||||||
say() {
|
say() {
|
||||||
|
# Print a status line. Coloured if gum present, else plain.
|
||||||
if [[ -n $GUM ]]; then
|
if [[ -n $GUM ]]; then
|
||||||
gum style --foreground 212 --bold "$1"
|
gum style --foreground 212 --bold "$1"
|
||||||
else
|
else
|
||||||
|
|
@ -24,50 +27,46 @@ say() {
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
confirm() {
|
run_with_spinner() {
|
||||||
local prompt=$1
|
local title=$1; shift
|
||||||
if [[ -n $GUM ]]; then
|
if [[ -n $GUM ]]; then
|
||||||
gum confirm "$prompt"
|
gum spin --spinner dot --title "$title" -- "$@"
|
||||||
else
|
else
|
||||||
read -r -p "$prompt [y/N] " yn
|
echo "[+] $title"
|
||||||
[[ ${yn,,} == y* ]]
|
"$@"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Pre-flight: network ─────────────────────────────────────────────
|
# ── Pre-flight: network check ───────────────────────────────────────
|
||||||
say "veilor-update: checking network"
|
say "veilor-update: checking network"
|
||||||
if ! ping -c 1 -W 2 1.1.1.1 >/dev/null 2>&1; then
|
if ! ping -c 1 -W 2 mirrors.fedoraproject.org >/dev/null 2>&1; then
|
||||||
echo " No network. Connect and re-run \`veilor-update\`."
|
echo
|
||||||
|
echo " No route to mirrors.fedoraproject.org."
|
||||||
|
echo " Connect to a network and re-run \`veilor-update\`."
|
||||||
exit 3
|
exit 3
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── Pre-flight: rollback target available ───────────────────────────
|
# ── Snapshot kernel before upgrade so we can warn about reboot need ─
|
||||||
# bootc has two deployments by design (booted + rollback). If
|
KERNEL_BEFORE=$(uname -r)
|
||||||
# something's wrong we want the user to see it before staging more.
|
|
||||||
if have bootc; then
|
# ── DNF upgrade ─────────────────────────────────────────────────────
|
||||||
say "veilor-update: bootc status"
|
say "veilor-update: refreshing DNF metadata + applying updates"
|
||||||
bootc status || true
|
# Capture upgrade output so we can count packages afterwards. Tee to
|
||||||
else
|
# stdout for live progress; swallow into a tempfile for the count.
|
||||||
echo " bootc not present — this CLI targets v0.7+ atomic systems."
|
LOG=$(mktemp -t veilor-update.XXXXXX)
|
||||||
|
trap 'rm -f "$LOG"' EXIT
|
||||||
|
|
||||||
|
if ! sudo dnf upgrade --refresh -y 2>&1 | tee "$LOG"; then
|
||||||
|
echo
|
||||||
|
echo " dnf upgrade failed. See output above."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── Pause auditd while staging ──────────────────────────────────────
|
# ── Count packages updated ──────────────────────────────────────────
|
||||||
# Reduces audit log noise during the heavy fs writes; resume after.
|
# DNF prints "Upgraded: N", "Installed: N", "Removed: N" at end.
|
||||||
AUDIT_PAUSED=0
|
# Sum the upgrade/install lines for the user-visible total.
|
||||||
if systemctl is-active auditd >/dev/null 2>&1; then
|
UPDATED=$(grep -E '^(Upgraded|Installed)\b' "$LOG" 2>/dev/null \
|
||||||
if sudo systemctl stop auditd 2>/dev/null; then
|
| awk -F: '{ gsub(/[^0-9]/,"",$2); s+=$2 } END { print s+0 }')
|
||||||
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 (best-effort) ───────────────────────────────────────────
|
||||||
FLATPAK_RC=0
|
FLATPAK_RC=0
|
||||||
|
|
@ -75,20 +74,21 @@ if have flatpak; then
|
||||||
say "veilor-update: updating flatpaks"
|
say "veilor-update: updating flatpaks"
|
||||||
if ! flatpak update -y; then
|
if ! flatpak update -y; then
|
||||||
FLATPAK_RC=2
|
FLATPAK_RC=2
|
||||||
echo " flatpak update failed; continuing."
|
echo " flatpak update failed; continuing anyway."
|
||||||
fi
|
fi
|
||||||
|
else
|
||||||
|
echo " (flatpak not installed — skipping)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── Post-update summary ─────────────────────────────────────────────
|
# ── Post-update: reboot hint if kernel changed ──────────────────────
|
||||||
|
KERNEL_AFTER_LATEST=$(rpm -q kernel --last 2>/dev/null \
|
||||||
|
| awk 'NR==1 { sub(/^kernel-/,"",$1); print $1 }')
|
||||||
|
|
||||||
say "veilor-update: complete"
|
say "veilor-update: complete"
|
||||||
bootc status 2>/dev/null | head -20 || true
|
printf ' Packages updated : %s\n' "${UPDATED:-0}"
|
||||||
|
printf ' Running kernel : %s\n' "$KERNEL_BEFORE"
|
||||||
# ── Reboot prompt ───────────────────────────────────────────────────
|
if [[ -n ${KERNEL_AFTER_LATEST:-} && $KERNEL_AFTER_LATEST != "$KERNEL_BEFORE" ]]; then
|
||||||
# bootc always writes the new image into the staged deployment; reboot
|
printf ' Newest kernel : %s (reboot suggested)\n' "$KERNEL_AFTER_LATEST"
|
||||||
# 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
|
fi
|
||||||
|
|
||||||
exit $FLATPAK_RC
|
exit $FLATPAK_RC
|
||||||
|
|
|
||||||
|
|
@ -1,249 +0,0 @@
|
||||||
#!/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
|
|
||||||
|
|
@ -91,18 +91,6 @@ before the build is considered green.
|
||||||
- [ ] `lsblk -f` shows LUKS2 on the main partition
|
- [ ] `lsblk -f` shows LUKS2 on the main partition
|
||||||
- [ ] `cryptsetup luksDump /dev/...` shows argon2id, aes-xts-plain64
|
- [ ] `cryptsetup luksDump /dev/...` shows argon2id, aes-xts-plain64
|
||||||
- [ ] `swapon` shows `zram` device, no disk swap
|
- [ ] `swapon` shows `zram` device, no disk swap
|
||||||
- [ ] `zramctl` shows `ALGORITHM=zstd` and `DISKSIZE=16G` (= 16 GiB,
|
|
||||||
not Fedora's 8 GiB default — see `overlay/etc/systemd/zram-generator.conf`)
|
|
||||||
|
|
||||||
## Memory pressure
|
|
||||||
|
|
||||||
- [ ] `systemctl is-active systemd-oomd` → `active` (PSI-based pre-OOM
|
|
||||||
killer; without it the kernel waits until total RAM exhaustion
|
|
||||||
then often kills plasmashell or the active terminal instead of
|
|
||||||
the runaway tab)
|
|
||||||
- [ ] `sysctl vm.swappiness vm.watermark_scale_factor vm.page-cluster`
|
|
||||||
shows `180 / 125 / 0` (default `60 / 10 / 3` is wrong for
|
|
||||||
zram-only — kernel refuses to swap until exhausted, then thrashes)
|
|
||||||
|
|
||||||
## SELinux module
|
## SELinux module
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue