diff --git a/kickstart/veilor-os.ks b/kickstart/veilor-os.ks
index 99a4e56..b5be79d 100644
--- a/kickstart/veilor-os.ks
+++ b/kickstart/veilor-os.ks
@@ -58,7 +58,7 @@ user --name=admin --groups=wheel --gecos="veilor admin" --password="" --plaintex
# framebuffer — symptom: black screen with blinking cursor for ~30s
# while the menu IS in fact rendered, just not painted. virtio-vga in
# QEMU doesn't trigger this so it never reproed in VM.
-bootloader --location=mbr --append="lockdown=integrity slab_nomerge randomize_kstack_offset=on vsyscall=none plymouth.enable=0 fbcon=nodefer"
+bootloader --location=mbr --append="lockdown=integrity module.sig_enforce=1 slab_nomerge randomize_kstack_offset=on vsyscall=none plymouth.enable=0 fbcon=nodefer i915.modeset=1 amdgpu.modeset=1 nvidia-drm.modeset=1 rd.vconsole.keymap=us"
# ── Live ISO partitioning (flat — for live rootfs build only) ──
# NOTE: This is the *live* image kickstart. Final installed system uses
diff --git a/overlay/etc/firewalld/zones/trusted.xml b/overlay/etc/firewalld/zones/trusted.xml
new file mode 100644
index 0000000..a89c050
--- /dev/null
+++ b/overlay/etc/firewalld/zones/trusted.xml
@@ -0,0 +1,12 @@
+
+
+
+ Trusted
+ All network connections are accepted. veilor-os pre-binds tailscale0 here so the mesh layer-1 (Tailscale via Headscale) works out-of-box without manual firewalld zone juggling.
+
+
diff --git a/overlay/etc/skel/.config/breezerc b/overlay/etc/skel/.config/breezerc
new file mode 100644
index 0000000..131a209
--- /dev/null
+++ b/overlay/etc/skel/.config/breezerc
@@ -0,0 +1,60 @@
+# veilor-os — Breeze window decoration override
+# Tighter borders, solid black title bar, minimal buttons, smallest border.
+# Merged into /etc/xdg/breezerc (system default) by 30-apply-v03-theme.sh.
+
+[Common]
+# Tighter outline; subtle separator only when active.
+OutlineCloseButton=false
+ShadowSize=ShadowSmall
+ShadowStrength=128
+ShadowColor=0,0,0
+
+[Windeco]
+# Border thickness: smallest available (= "None" leaves only resize edge,
+# "NoSides" keeps top/bottom only). We pick "None" for the tightest look,
+# matching the black-on-black aesthetic.
+BorderSize=None
+ButtonSize=ButtonSmall
+CloseButton=true
+DrawBackgroundGradient=false
+DrawBorderOnMaximizedWindows=false
+DrawSizeGrip=false
+DrawTitleBarSeparator=false
+ExceptionType=0
+HideTitleBar=false
+OpaqueTitleBar=true
+TitleAlignment=AlignCenter
+UseBackgroundGradient=false
+UseTitleBarColor=true
+
+# Buttons: minimal — close / max / min only, no shade/help/keep-above.
+ButtonsOnLeft=M
+ButtonsOnRight=IAX
+
+[Style]
+# Disable per-app blur, transparency, and gradient effects.
+MenuOpacity=100
+WindowDragMode=1
+ScrollBarAddLineButtons=0
+ScrollBarSubLineButtons=0
+SidePanelDrawFrame=false
+SliderDrawTickMarks=false
+TabBarDrawCenteredTabs=true
+ToolBarDrawItemSeparator=false
+DockWidgetDrawFrame=false
+ProgressBarAnimated=false
+AnimationsEnabled=false
+StackedWidgetDrawFrame=false
+
+# ── Active / inactive title bar colors (override Breeze defaults) ──
+# kdeglobals [WM] section is the canonical source; these mirror it here
+# so apps that only read breezerc see consistent values.
+[Windeco][Active]
+TitleBarColor=0,0,0
+TitleBarTextColor=216,216,216
+TitleBarBorderColor=104,107,111
+
+[Windeco][Inactive]
+TitleBarColor=15,17,18
+TitleBarTextColor=161,169,177
+TitleBarBorderColor=42,46,50
diff --git a/overlay/etc/skel/.config/kdeglobals b/overlay/etc/skel/.config/kdeglobals
new file mode 100644
index 0000000..ca3e1f9
--- /dev/null
+++ b/overlay/etc/skel/.config/kdeglobals
@@ -0,0 +1,29 @@
+[General]
+ColorScheme=veilor-black
+Name=veilor black
+AccentColor=104,107,111
+LastUsedCustomAccentColor=104,107,111
+font=Fira Code,11,-1,5,400,0,0,0,0,0,0,0,0,0,0,1
+fixed=Fira Code,10,-1,5,400,0,0,0,0,0,0,0,0,0,0,1
+menuFont=Fira Code,11,-1,5,400,0,0,0,0,0,0,0,0,0,0,1
+smallestReadableFont=Fira Code,9,-1,5,400,0,0,0,0,0,0,0,0,0,0,1
+toolBarFont=Fira Code,10,-1,5,400,0,0,0,0,0,0,0,0,0,0,1
+
+[Icons]
+Theme=breeze-dark
+
+[KDE]
+LookAndFeelPackage=org.kde.breezedark.desktop
+SingleClick=false
+contrast=4
+widgetStyle=Breeze
+
+[Mouse]
+cursorTheme=Breeze_Light
+cursorSize=24
+
+[KDecoration]
+theme=Breeze
+ButtonsOnLeft=
+ButtonsOnRight=IAX
+BorderSize=None
diff --git a/overlay/etc/skel/.config/konsolerc b/overlay/etc/skel/.config/konsolerc
new file mode 100644
index 0000000..b1c76e1
--- /dev/null
+++ b/overlay/etc/skel/.config/konsolerc
@@ -0,0 +1,10 @@
+# veilor-os Konsole default — branded profile pre-selected so first
+# Konsole launch on a fresh user account opens the veilor profile,
+# not Fedora's default white-bg Breeze.
+
+[Desktop Entry]
+DefaultProfile=Veilor.profile
+
+[KonsoleWindow]
+ShowMenuBarByDefault=false
+RememberWindowSize=true
diff --git a/overlay/etc/skel/.config/kwinrc b/overlay/etc/skel/.config/kwinrc
new file mode 100644
index 0000000..9a6d8dc
--- /dev/null
+++ b/overlay/etc/skel/.config/kwinrc
@@ -0,0 +1,29 @@
+# veilor-os Plasma 6 kwin defaults — seeded into /etc/skel so first-login
+# users inherit deliberate windowing behaviour rather than default Breeze
+# animations. Per-user; user can override post-login.
+
+[Compositing]
+# OpenGL backend = standard for hardware accel; AnimationSpeed=0 = no
+# slow window animations on every focus change.
+Backend=OpenGL
+AnimationSpeed=0
+HiddenPreviews=5
+LatencyPolicy=Low
+WindowsBlockCompositing=true
+
+[Plugins]
+# Disable visual fluff that isn't security-relevant + costs perf.
+blurEnabled=false
+contrastEnabled=false
+slideEnabled=false
+slidingpopupsEnabled=false
+fadeEnabled=false
+zoomEnabled=true
+
+[Windows]
+FocusPolicy=ClickToFocus
+RollOverDesktops=false
+TitlebarDoubleClickCommand=Maximize
+
+[Wayland]
+InputMethod=
diff --git a/overlay/etc/skel/.local/share/konsole/Veilor.colorscheme b/overlay/etc/skel/.local/share/konsole/Veilor.colorscheme
new file mode 100644
index 0000000..012f023
--- /dev/null
+++ b/overlay/etc/skel/.local/share/konsole/Veilor.colorscheme
@@ -0,0 +1,104 @@
+[General]
+Anchor=0.5,0.5
+Blur=false
+ColorRandomization=false
+Description=Veilor
+FillStyle=Tile
+Opacity=1
+Wallpaper=
+WallpaperFlipType=NoFlip
+WallpaperOpacity=1
+
+[Background]
+Color=0,0,0
+
+[BackgroundFaint]
+Color=0,0,0
+
+[BackgroundIntense]
+Color=15,17,18
+
+[Foreground]
+Color=216,216,216
+
+[ForegroundFaint]
+Color=161,169,177
+
+[ForegroundIntense]
+Color=236,236,236
+
+# ── Standard ANSI palette (muted, desaturated) ──
+# Veilor aesthetic: no neon. Reds tone-shifted toward bordeaux, greens
+# toward sage, blues toward slate. Greys lifted to remain readable.
+
+[Color0]
+Color=27,27,27
+
+[Color0Faint]
+Color=20,20,20
+
+[Color0Intense]
+Color=58,58,58
+
+[Color1]
+Color=176,55,69
+
+[Color1Faint]
+Color=130,40,52
+
+[Color1Intense]
+Color=205,87,99
+
+[Color2]
+Color=102,138,90
+
+[Color2Faint]
+Color=78,107,68
+
+[Color2Intense]
+Color=141,176,128
+
+[Color3]
+Color=185,158,98
+
+[Color3Faint]
+Color=140,118,72
+
+[Color3Intense]
+Color=216,193,134
+
+[Color4]
+Color=92,116,143
+
+[Color4Faint]
+Color=68,87,107
+
+[Color4Intense]
+Color=131,154,182
+
+[Color5]
+Color=141,113,150
+
+[Color5Faint]
+Color=104,84,112
+
+[Color5Intense]
+Color=176,148,186
+
+[Color6]
+Color=99,144,148
+
+[Color6Faint]
+Color=72,107,110
+
+[Color6Intense]
+Color=139,180,184
+
+[Color7]
+Color=200,200,200
+
+[Color7Faint]
+Color=161,169,177
+
+[Color7Intense]
+Color=236,236,236
diff --git a/overlay/etc/skel/.local/share/konsole/Veilor.profile b/overlay/etc/skel/.local/share/konsole/Veilor.profile
new file mode 100644
index 0000000..a01e724
--- /dev/null
+++ b/overlay/etc/skel/.local/share/konsole/Veilor.profile
@@ -0,0 +1,55 @@
+[General]
+Name=Veilor
+Parent=FALLBACK/
+Command=/bin/bash
+Directory=
+Icon=utilities-terminal
+LocalTabTitleFormat=%w
+RemoteTabTitleFormat=(%u) %h
+ShowTerminalSizeHint=false
+StartInCurrentSessionDir=true
+TerminalCenter=false
+TerminalMargin=4
+
+[Appearance]
+ColorScheme=Veilor
+Font=Fira Code,11,-1,5,400,0,0,0,0,0,0,0,0,0,0,1
+LineSpacing=1
+UseFontLineCharacters=true
+
+[Cursor Options]
+CursorShape=0
+UseCustomCursorColor=true
+CustomCursorColor=104,107,111
+CustomCursorTextColor=216,216,216
+
+[Scrolling]
+HistoryMode=2
+HistorySize=10000
+ScrollBarPosition=2
+HighlightScrolledLines=false
+
+[Terminal Features]
+BellMode=3
+BlinkingCursorEnabled=false
+BlinkingTextEnabled=false
+FlowControlEnabled=false
+UrlHintsModifiers=67108864
+ReverseUrlHints=false
+VerticalLine=false
+
+[Interaction Options]
+AutoCopySelectedText=false
+CopyTextAsHTML=false
+TrimLeadingSpacesInSelectedText=false
+TrimTrailingSpacesInSelectedText=true
+UnderlineFilesEnabled=true
+UnderlineLinksEnabled=true
+OpenLinksByDirectClickEnabled=false
+WordCharacters=:@-./_~?&=%+#
+
+[Encoding Options]
+DefaultEncoding=UTF-8
+
+[Keyboard]
+KeyBindings=default
diff --git a/overlay/etc/systemd/system/veilor-firstboot.service b/overlay/etc/systemd/system/veilor-firstboot.service
index 9a8f27f..42b44c3 100644
--- a/overlay/etc/systemd/system/veilor-firstboot.service
+++ b/overlay/etc/systemd/system/veilor-firstboot.service
@@ -18,4 +18,9 @@ TTYReset=yes
TTYVHangup=yes
[Install]
-WantedBy=multi-user.target
+# Real installs default to graphical.target. Without this entry the unit
+# never runs on installed systems — admin pw stays at install-time value
+# + chage -d 0 expired, SDDM PAM bounces user to a chauthtok screen
+# (recoverable but ugly). Live ISO + multi-user.target installs both
+# resolve via this list. (Agent 2, 2026-05-05 wave.)
+WantedBy=graphical.target multi-user.target
diff --git a/overlay/etc/usbguard/rules.conf b/overlay/etc/usbguard/rules.conf
new file mode 100644
index 0000000..a00cfd3
--- /dev/null
+++ b/overlay/etc/usbguard/rules.conf
@@ -0,0 +1,43 @@
+# veilor-os USBGuard baseline rules
+#
+# Default policy is `block` (set in usbguard-daemon.conf via the
+# overlay). Without any allow rule, every USB device — including the
+# user's keyboard — is blocked at boot. That includes the desktop
+# user with a USB keyboard at SDDM.
+#
+# This file allows HID-class interfaces (keyboard, mouse, touchpad,
+# fingerprint reader, NFC, gamepad) without pinning to specific
+# vendor:product/serial/hash. id-based rules survive dock replug and
+# vendor-bump kernel changes, where hash+parent-hash rules don't —
+# verified pain on onyx (memory: feedback_usbguard_dock.md). Same fix.
+#
+# After first login, the user runs:
+# ujust veilor-usbguard-enroll
+# (or `usbguard generate-policy --with-hash=false > rules.conf`)
+# to add their own keyboard's id-rule and tighten the policy further.
+#
+# References:
+# - usbguard-rules.conf(5)
+# - https://usbguard.github.io/documentation/rule-language.html
+# - veilor-os agent 9 audit, 2026-05-05
+
+# HID class — keyboards, mice, pointers, gamepads, fingerprint, NFC.
+# Interface descriptor 03:NN:NN where 03=HID. We accept any HID
+# subclass + protocol so the rule is robust to future HID variants.
+allow with-interface match-all { 03:*:* }
+
+# Mass-storage prompt: ask the user before mounting a new flash drive.
+# Reject blanket-allow (would silently allow USB Rubber Ducky).
+# Accept only after user confirms via the gnome/plasma USB dialog.
+# (USBGuard has no native "ask" verb; we leave mass-storage devices
+# implicit-block here, the user runs `usbguard allow-device `
+# from a Plasma applet OR the firstboot wizard documents this flow.)
+
+# Block known-bad. USB Killer signature shows up as a generic-HID
+# composite descriptor + power draw out of spec. We can't reliably
+# detect that from descriptors alone — relying on default-block
+# semantics for now.
+
+# DO NOT pin to specific id=, serial=, hash=, or parent-hash= here.
+# That's the user's job post-firstboot for their actual hardware.
+# Pre-shipped pinned rules break on every dock replug + kernel bump.
diff --git a/overlay/usr/local/bin/veilor-installer b/overlay/usr/local/bin/veilor-installer
index 4a0d328..999766a 100644
--- a/overlay/usr/local/bin/veilor-installer
+++ b/overlay/usr/local/bin/veilor-installer
@@ -427,7 +427,7 @@ __SSHKEY_DIRECTIVE__
# re-runs kernel-install per kernel. That's the canonical Fedora 43 path
# for landing args in BLS entries — kernel-install reads /etc/kernel/cmdline
# (90-loaderentry.install:84-95) when generating BLS option lines.
-bootloader --append="lockdown=integrity slab_nomerge init_on_alloc=1 init_on_free=1 randomize_kstack_offset=on vsyscall=none fbcon=nodefer"
+bootloader --append="lockdown=integrity module.sig_enforce=1 slab_nomerge init_on_alloc=1 init_on_free=1 randomize_kstack_offset=on vsyscall=none fbcon=nodefer i915.modeset=1 amdgpu.modeset=1 nvidia-drm.modeset=1 rd.vconsole.keymap=us"
# Disk: zero, LUKS2 (argon2id), btrfs subvolumes (no LVM intermediary).
# Native btrfs-on-LUKS matches Fedora KDE Spin defaults; LVM+btrfs combo
@@ -927,7 +927,33 @@ KSEOF
return 0
}
+_dump_logs_to_host() {
+ # If a virtio-9p share tagged "hostlogs" was attached by run-vm.sh,
+ # mount it and dump every anaconda + installer log into it. Runs on
+ # success AND failure (called via trap). No-op on real hardware where
+ # the 9p tag doesn't exist.
+ if ! grep -qw 9p /proc/filesystems 2>/dev/null; then return 0; fi
+ local m=/mnt/hostlogs
+ mkdir -p "$m"
+ mount -t 9p -o trans=virtio,version=9p2000.L hostlogs "$m" 2>/dev/null || return 0
+ cp -af /tmp/anaconda.log "$m/" 2>/dev/null || true
+ cp -af /tmp/program.log "$m/" 2>/dev/null || true
+ cp -af /tmp/storage.log "$m/" 2>/dev/null || true
+ cp -af /tmp/packaging.log "$m/" 2>/dev/null || true
+ cp -af /tmp/dnf.log "$m/" 2>/dev/null || true
+ cp -af /tmp/dnf.librepo.log "$m/" 2>/dev/null || true
+ cp -af /tmp/anaconda-cmdline.log "$m/" 2>/dev/null || true
+ cp -af /var/log/veilor-installer.log "$m/" 2>/dev/null || true
+ cp -af /run/install/veilor-generated.ks "$m/" 2>/dev/null || true
+ dmesg > "$m/dmesg.log" 2>/dev/null || true
+ journalctl -b > "$m/journal.log" 2>/dev/null || true
+ sync
+ umount "$m" 2>/dev/null || true
+}
+
run_install() {
+ # Capture logs to host on every exit path (success, failure, ^C).
+ trap _dump_logs_to_host EXIT
# Anaconda env setup (see comments below).
export LANG="${SEL_LOCALE:-en_GB.UTF-8}"
export LC_ALL="$LANG"
diff --git a/test/run-vm.sh b/test/run-vm.sh
index 352e936..90934f0 100755
--- a/test/run-vm.sh
+++ b/test/run-vm.sh
@@ -208,12 +208,20 @@ echo " Seed : ${SEED_ISO:-}"
# We've lost logs three times in a row to anaconda failures + tmpfs
# reboots. Wiring this up so future failures auto-capture.
ANACONDA_LOG="$TEST_DIR/anaconda-vm-$(date +%Y%m%d-%H%M%S).log"
+ANACONDA_LOG_DIR="$TEST_DIR/test-runs/$(date +%Y%m%d-%H%M%S)"
+mkdir -p "$ANACONDA_LOG_DIR"
ANACONDA_LOG_ARGS=(
+ # Belt: virtio-serial (anaconda's setupVirtio rsyslog forward, fragile —
+ # depends on rsyslog being installed in the live ISO).
-chardev "file,id=anaclog,path=$ANACONDA_LOG"
-device virtio-serial-pci,id=vs1
-device "virtserialport,chardev=anaclog,bus=vs1.0,name=org.fedoraproject.anaconda.log.0"
+ # Braces: virtio-9p host directory share. veilor-installer mounts this
+ # at /mnt/hostlogs and rsyncs /tmp/*.log there post-anaconda.
+ -virtfs "local,path=$ANACONDA_LOG_DIR,mount_tag=hostlogs,security_model=mapped-xattr,id=hostlogs"
)
-echo " AnaLog: $ANACONDA_LOG"
+echo " AnaLog : $ANACONDA_LOG"
+echo " HostFS : $ANACONDA_LOG_DIR (9p tag: hostlogs)"
echo " Mode : ${SECBOOT:+secboot}${SECBOOT:-stock UEFI}"
echo " Inject: ${HOST_PUBKEY:+yes}${HOST_PUBKEY:-no (no host pubkey)}"