BazziteOS - Post Script
The folllowing script is now operational.
IAW our ditching of the Microsoft eco system and the problems with MintOS in gaming we are moving various systems to BazziteOS. So far we have not been disappointed
The script is now production ie.. live. I have found now major issues with it.
The following is noted:
- The printer section has been pulled
- The UDM certificate section has been left in as a guide. This allows the UDM firewall to show the blocked page within systems that utilise a Ubiquiti network for end users. This is irrelevant for most networks. Remove if not required.
- ClamAV code has been replaced, garbage solution - configuration requires addtional script.
- Script has been tested and modifications made.
Post BazziteOS Installation script.
Fully rewritten to fix a number of issues that presented themselves, ClamAV solution among them.
There should be no serious issues between this version and the previous version of the script other than clamav and possibly the deployment of additional software. If you are showing "inactive" deployments in the ostree clean them up manually .
#!/bin/bash
# ================================================================================
# Base Setup Script for Bazzite OS (Fedora Atomic / rpm-ostree)
# Filename : setup-laptop-bazzite.sh
# Updated : 2026-06-16
# Version : 2.0.0
# ================================================================================
#
# PURPOSE:
# Post-install hardening and configuration for a Bazzite desktop/laptop.
# Bazzite is an IMMUTABLE OS — the root filesystem is read-only and managed
# via rpm-ostree atomic image updates. This script respects that model:
#
# • System packages → rpm-ostree install (requires reboot to activate)
# • GUI applications → Flatpak (Flathub)
# • CLI/TUI tools → Homebrew (brew) where possible
# • Anything else → Distrobox containers
#
# DO NOT use apt-get, dpkg, dnf, or yum directly on the host.
# DO NOT attempt to install .deb packages — this is Fedora/RPM based.
#
# WHAT THIS SCRIPT DOES:
#
# SECURITY & HARDENING:
# - Hardens SSH via sshd_config.d drop-in (survives image updates)
# - Regenerates SSH host keys (ED25519 + RSA 4096)
# - ClamAV antivirus layered natively via rpm-ostree (clamav,
# clamav-freshclam, clamd). This section layers PACKAGES ONLY;
# configuration and service activation are handled post-reboot by
# setup-clamav.sh. NOTE: a clean of any prior stale ClamAV overlay
# requests may be required first — see the ClamAV section notes.
# - Configures firewalld (Bazzite's native firewall): sets the DEFAULT
# zone to 'drop' (default-deny inbound) and adds LAN/ULA/link-local
# rich rules for SSH, mDNS, and DHCPv6. Egress remains open.
# - UDM SE block-page certificate installed to system trust store
#
# SOFTWARE INSTALLATION (Flatpak via Flathub):
# - GIMP, Inkscape, Krita, LibreOffice, VLC, Pinta, Kdenlive, Blender
# - Google Chrome (Flatpak — avoids APT/rpm-ostree repo complexity)
#
# SYSTEM TOOLS (rpm-ostree layered — requires reboot):
# - wireguard-tools (kernel module already included in Bazzite)
# - curl, wget, nmap, smartmontools, mtr, rsyslog, fwupd
# - openssh-server (usually pre-installed; layered defensively)
# - cups + avahi (printer / mDNS discovery prerequisites)
#
# INSYNC (Google Drive client):
# - Downloads Insync Fedora 44 RPM directly from cdn.insynchq.com
# (bypasses the yum repo — avoids GPG DB write on immutable root and
# the fc44 repo 404 that affects $releasever-based repo configs)
# - Falls back to the working fc43 repo URL if the CDN download fails
# - Detects KDE vs GNOME at runtime for file manager integration
# - Layers insync via rpm-ostree (requires reboot)
#
# SYSTEM CONFIGURATION:
# - Printer prerequisites (cups, avahi) layered. The Epson WF-4830
# lpadmin / IPP-Everywhere command is PRINTED as a manual post-reboot
# step (see POST-SCRIPT INSTRUCTIONS) - it is NOT auto-applied here.
# - Automatic OS and Flatpak updates via ujust / rpm-ostree
# - Custom MOTD (OS, IPv4, IPv6 global + link-local, uptime, rpm-ostree)
#
# FIRMWARE UPDATES (fwupd):
# - LVFS metadata refresh
# - AC power and battery pre-flight checks
# - BIOS/UEFI capsule gating on AC power
# - Non-BIOS firmware applied unconditionally
#
# WHAT THIS SCRIPT DOES NOT DO:
# - AppArmor: not applicable — Bazzite uses SELinux (Fedora default)
# - UFW: not applicable — Bazzite uses firewalld
# - apt/dpkg/APT repos: not applicable — Fedora RPM ecosystem only
# - i386 multiarch: not applicable — handled by Flatpak/Steam runtime
# - Novabench: no safe RPM/Flatpak available; use Phoronix Test Suite
# inside a Distrobox container if benchmarking is required
# - IPv6 temp-address preference (wlo1): claimed in the LMDE-era header but
# never ported to this script - no code present. Add manually if required.
# - Final diagnostics / health-check pass: NOT implemented as a standalone
# section. The firewalld, fwupd, and ClamAV sections report their own
# status inline instead.
#
# ADDITIONAL NOTES:
# a) AUTOMATIC UPDATES: Bazzite updates are atomic via rpm-ostree. This
# script enables the rpm-ostreed-automatic timer and configures Flatpak
# auto-updates. OS updates are staged and applied on next reboot.
#
# b) WAYDROID (Android / Play Store): Bazzite has native Waydroid support
# via `ujust setup-waydroid`. This is a graphical, interactive setup and
# CANNOT be fully automated from a non-interactive script. Instructions
# are printed at the end of this script. Run them manually after reboot.
#
# c) SECURITY NOTE: All Debian/LMDE-specific constructs (apt hold, dpkg,
# /etc/apt/*, AppArmor, update-ca-certificates) have been replaced with
# their Fedora/Bazzite equivalents. No Debian-origin commands remain.
#
# REBOOT REQUIRED:
# rpm-ostree layers packages into a new pending deployment. A reboot is
# REQUIRED at the end of this script for all layered packages to activate.
# The script will prompt before rebooting, or you can reboot manually.
#
# ================================================================================
#
# CHANGELOG:
# 2.0.0 - 2026-06-16 - Major revision: header/code reconciliation + cleanup.
# (1) Version bumped to 2.0.0.
# (2) Audited "WHAT THIS SCRIPT DOES" against the actual
# code and corrected the mismatches:
# - Flatpak list now includes Kdenlive and Blender.
# - rpm-ostree list now notes cups + avahi.
# - Firewalld line corrected: it sets the DEFAULT zone
# to 'drop' (it does not touch all zones).
# - Printer line corrected: cups/avahi are layered but
# the lpadmin step is a MANUAL post-reboot action,
# not auto-applied by the script.
# - MOTD line corrected to match the fields shown.
# (3) Moved two unbacked header claims into "WHAT THIS
# SCRIPT DOES NOT DO": the IPv6 temp-address preference
# on wlo1 (never ported from the LMDE base) and the
# standalone final diagnostics/health-check pass (not
# present; sections report status inline instead).
# (4) Renumbered sections to be sequential (1-10). The
# previous file ran 1-7 then 12, 11, 12.
# NOTE: ClamAV live configuration continues on a separate
# machine via setup-clamav.sh; this script layers packages
# only and warns on stale/inactive overlay requests.
# 1.2.4 - 2026-06-16 - ClamAV section documentation update:
# (1) Added a CLEAN-UP note. If ClamAV ships in the
# Bazzite BASE IMAGE, earlier runs that did
# `rpm-ostree install clamav ...` leave stale /
# inactive overlay requests that surface as errors
# in `rpm-ostree status`. The rpm -q guard prevents
# RE-queuing, but cannot remove requests already
# queued by an older script version. Documented the
# remediation (`rpm-ostree uninstall ...` + reboot).
# (2) Added a non-destructive detection block to SECTION
# 6 that warns (does NOT auto-remove) when existing
# clamav/clamd overlay requests are present.
# (3) Corrected the summary header: ClamAV is layered
# natively via rpm-ostree, not run as a Distrobox
# container (the old summary line was stale).
# NOTE: live configuration of ClamAV on Bazzite is still
# being worked out on a separate machine via setup-clamav.sh;
# this script continues to layer packages only.
# 1.1.0 - 2026-05-17 - Three fixes confirmed via live test run:
# (1) Insync: replaced rpm --import + yum repo approach
# with direct CDN RPM download (cdn.insynchq.com).
# rpm --import fails on Bazzite — RPM DB is on the
# immutable image layer. CDN download also bypasses
# the fc44 yum repo 404 ($releasever resolving to
# 44 when Insync's repo only published up to fc43).
# Fallback to fc43 repo URL added if CDN is down.
# (2) Firewalld: removed --permanent from
# --set-default-zone (firewalld rejects that combo;
# the default zone change is always permanent).
# Added DHCPv6-client rule omitted in v1.0.0.
# (3) DE detection improved: now checks running desktop
# processes (plasmashell, gnome-shell) as fallback
# when XDG_CURRENT_DESKTOP is unset under sudo.
# 1.0.0 - 2026-05-15 - Initial Bazzite port from LMDE 7 base-setup v2.3.0.
# Complete rewrite for rpm-ostree/Fedora Atomic model.
# Removed: apt/dpkg, AppArmor, UFW, i386, Novabench,
# user-specific braedach block, Chrome APT repo.
# Added: firewalld hardening, rpm-ostree layering,
# Insync RPM repo, ClamAV Distrobox strategy,
# automatic update configuration, final diagnostics.
#
# ================================================================================
set -euo pipefail
# -------------------------------------------------------------------------------
# Privilege check
if [[ "${EUID}" -ne 0 ]]; then
echo "[ERROR] This script must be run as root (sudo ./setup-laptop-bazzite.sh)"
exit 1
fi
# -------------------------------------------------------------------------------
# Logging
LOGFILE="/var/log/bazzite-setup.log"
BACKUP_TS="$(date +%Y%m%d-%H%M%S)"
exec > >(tee -a "$LOGFILE") 2>&1
info() { echo -e "\033[0;32m[INFO]\033[0m $*"; }
warn() { echo -e "\033[0;33m[WARN]\033[0m $*" >&2; }
error() { echo -e "\033[0;31m[ERROR]\033[0m $*" >&2; exit 1; }
step() { echo -e "\n\033[1;34m[====]\033[0m $*\n"; }
info "Bazzite setup started at $(date)"
info "Log file: ${LOGFILE}"
# -------------------------------------------------------------------------------
# Resolve logged-in user (not root, even when run via sudo)
CURRENT_USER=$(logname 2>/dev/null || echo "${SUDO_USER:-root}")
CURRENT_HOME=$(getent passwd "${CURRENT_USER}" | cut -d: -f6)
info "Running as root. Detected user context: ${CURRENT_USER} (home: ${CURRENT_HOME})"
# -------------------------------------------------------------------------------
# Sanity check: confirm we are on a Fedora/Bazzite system
if ! command -v rpm-ostree &>/dev/null; then
error "rpm-ostree not found. This script is for Bazzite / Fedora Atomic only."
fi
OS_ID=$(grep -oP '(?<=^ID=).*' /etc/os-release | tr -d '"' || echo "unknown")
OS_PRETTY=$(grep -oP '(?<=^PRETTY_NAME=).*' /etc/os-release | tr -d '"' || echo "unknown")
info "Detected OS: ${OS_PRETTY}"
# -------------------------------------------------------------------------------
# Network connectivity check
step "Network Connectivity"
info "Checking network connectivity..."
if ! curl -fsS --max-time 10 https://flathub.org > /dev/null 2>&1; then
error "Network check failed — cannot reach flathub.org. Check DNS/network before continuing."
fi
info "Network OK."
# -------------------------------------------------------------------------------
# Reset any failed systemd units from a prior run
step "Systemd Housekeeping"
info "Checking and resetting failed systemd units..."
systemctl reset-failed || true
# ===============================================================================
# SECTION 1: rpm-ostree package layering
#
# Packages are checked with rpm -q before layering to avoid queuing
# already-installed packages as stale/inactive rpm-ostree entries.
#
# Only missing packages are passed to rpm-ostree install.
# Packages will only be ACTIVE after the reboot at the end of this script.
# Keep this list minimal — prefer Flatpak, Homebrew, or Distrobox instead.
# ===============================================================================
step "rpm-ostree Package Layering"
# wireguard-tools: kernel WireGuard module is already in Bazzite's kernel.
# This adds the wg / wg-quick CLI tools for managing configs.
RPM_PACKAGES=(
curl
wget
nmap
mtr
smartmontools
rsyslog
fwupd
openssh-server
wireguard-tools
cups # required for printer setup
avahi # required for IPP Everywhere / mDNS printer discovery
)
RPM_MISSING=()
for pkg in "${RPM_PACKAGES[@]}"; do
if ! rpm -q "$pkg" &>/dev/null; then
RPM_MISSING+=("$pkg")
fi
done
if [[ ${#RPM_MISSING[@]} -eq 0 ]]; then
info "All rpm-ostree packages already installed — nothing to layer."
else
info "Layering missing packages via rpm-ostree: ${RPM_MISSING[*]}"
info "NOTE: These packages activate after the post-script reboot."
if rpm-ostree install --idempotent --allow-inactive "${RPM_MISSING[@]}"; then
info "rpm-ostree layering queued successfully."
info "Packages will be active after reboot."
REBOOT_REQUIRED=true
else
warn "rpm-ostree install reported an issue — check output above."
warn "Run: sudo rpm-ostree status"
fi
fi
# ===============================================================================
# SECTION 2: Insync (Google Drive client)
#
# Insync is a commercial GUI Google Drive/OneDrive sync client for Linux.
# ($29.99/account, 15-day free trial)
#
# WHY DIRECT CDN DOWNLOAD (not the yum repo):
# Two confirmed failures with the repo approach on Bazzite/Fedora 44:
# 1. rpm --import cannot write to /usr/share/rpm/.rpm.lock — that path is
# on the immutable image layer and is read-only on Bazzite.
# 2. https://yum.insynchq.com/fedora/44/ returns 404. Insync's repo lags
# behind Fedora releases; the $releasever variable resolves to 44 but
# Insync's infrastructure only had fc43 at Fedora 44 GA.
#
# Solution: download the native fc44 RPM directly from cdn.insynchq.com
# and install it as a local file via rpm-ostree. No repo or GPG DB write
# required. A fallback to the working fc43 repo URL is attempted if the
# CDN download fails.
# ===============================================================================
step "Insync (Google Drive Client)"
INSYNC_CDN_BASE="https://cdn.insynchq.com/builds/linux"
INSYNC_RPM_TMP="/tmp/insync-fc44.x86_64.rpm"
INSYNC_KDE_RPM_TMP="/tmp/insync-kde-fc44.rpm"
INSYNC_RPM_URL="${INSYNC_CDN_BASE}/insync-3.9.8.60034-fc44.x86_64.rpm"
# Detect desktop environment — XDG_CURRENT_DESKTOP is usually unset when
# running as root, so fall back to checking running desktop processes.
ACTUAL_DE="${XDG_CURRENT_DESKTOP:-}"
if [[ -z "${ACTUAL_DE}" ]] && [[ -n "${SUDO_USER:-}" ]]; then
ACTUAL_DE=$(sudo -u "${SUDO_USER}" bash -c 'echo "${XDG_CURRENT_DESKTOP:-}"' 2>/dev/null || true)
fi
if [[ -z "${ACTUAL_DE}" ]]; then
if pgrep -x plasmashell &>/dev/null || pgrep -x kwin_wayland &>/dev/null; then
ACTUAL_DE="KDE"
elif pgrep -x gnome-shell &>/dev/null; then
ACTUAL_DE="GNOME"
fi
fi
info "Detected desktop environment: ${ACTUAL_DE:-unknown}"
info "Downloading Insync Fedora 44 RPM from CDN..."
info "URL: ${INSYNC_RPM_URL}"
if curl -fsSL --retry 3 --retry-delay 5 -o "${INSYNC_RPM_TMP}" "${INSYNC_RPM_URL}"; then
info "Insync RPM downloaded successfully."
if rpm-ostree install --idempotent --allow-inactive "${INSYNC_RPM_TMP}"; then
info "Insync layered successfully — will be active after reboot."
else
warn "rpm-ostree install of Insync RPM failed."
warn "Install manually after reboot:"
warn " curl -fsSL ${INSYNC_RPM_URL} -o /tmp/insync.rpm"
warn " sudo rpm-ostree install /tmp/insync.rpm"
fi
rm -f "${INSYNC_RPM_TMP}"
else
warn "CDN download failed — attempting fallback via fc43 repo URL..."
cat > /etc/yum.repos.d/insync.repo <<'EOF'
[insync]
name=Insync repo (Fedora 43 — fc44 compatible)
baseurl=http://yum.insync.io/fedora/43/
gpgcheck=0
enabled=1
metadata_expire=120m
EOF
info "Fallback repo written (/etc/yum.repos.d/insync.repo)."
if rpm-ostree install --idempotent --allow-inactive insync; then
info "Insync installed via fallback fc43 repo."
else
warn "Insync install failed via fallback repo too."
warn "Download manually from: https://www.insynchq.com/downloads/linux#fedora"
warn "Then: sudo rpm-ostree install /path/to/insync-*.fc44.x86_64.rpm"
fi
fi
# File manager integration (KDE only — GNOME integration is bundled in main package)
if echo "${ACTUAL_DE}" | grep -qi "kde\|plasma"; then
info "KDE detected — installing insync-dolphin integration..."
INSYNC_KDE_URL="${INSYNC_CDN_BASE}/insync-dolphin-3.9.8.60034-fc44.x86_64.rpm"
if curl -fsSL --retry 2 -o "${INSYNC_KDE_RPM_TMP}" "${INSYNC_KDE_URL}" 2>/dev/null; then
rpm-ostree install --idempotent --allow-inactive "${INSYNC_KDE_RPM_TMP}" \
&& info "insync-dolphin layered." \
|| warn "insync-dolphin layer failed — install manually after reboot."
rm -f "${INSYNC_KDE_RPM_TMP}"
else
warn "insync-dolphin RPM not found at CDN — install manually after reboot:"
warn " sudo rpm-ostree install insync-dolphin"
fi
else
info "GNOME or unknown DE — Nautilus integration is bundled in the main Insync package."
fi
# ===============================================================================
# SECTION 3: Flatpak Applications
#
# Flatpak is the primary app delivery mechanism on Bazzite.
# These apps run sandboxed and update independently of the OS image.
# ===============================================================================
step "Flatpak Application Installation"
# Ensure Flathub remote is present (it should be on Bazzite, but be safe)
flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo || true
FLATPAK_APPS=(
"org.gimp.GIMP"
"org.inkscape.Inkscape"
"org.kde.krita"
"org.libreoffice.LibreOffice"
"org.videolan.VLC"
"com.github.PintaProject.Pinta"
"com.google.Chrome"
"org.kde.kdenlive"
"org.blender.Blender"
)
for app in "${FLATPAK_APPS[@]}"; do
info "Installing Flatpak: ${app}..."
if flatpak install -y --noninteractive flathub "${app}" 2>/dev/null; then
info " ✓ ${app}"
else
warn " ✗ ${app} — install failed or already installed."
fi
done
# ===============================================================================
# SECTION 4: Firewalld Configuration
#
# Bazzite uses firewalld (not UFW or nftables directly).
# firewalld sits on top of nftables on modern Fedora.
#
# Policy:
# - Default incoming: DROP (block all unsolicited inbound)
# - Default outgoing: ACCEPT (all egress permitted)
# - SSH allowed from LAN (192.168.0.0/16) only
# - WireGuard egress UDP 51820 allowed outbound (inherent in default-allow out)
# - WireGuard inbound port opened only in 'home' zone if needed
# - All changes saved to permanent config (survive reboot)
# ===============================================================================
step "Firewalld Configuration"
if ! command -v firewall-cmd &>/dev/null; then
warn "firewall-cmd not found — skipping firewall configuration."
warn "Install firewalld and rerun this section manually."
else
info "Ensuring firewalld is enabled and running..."
systemctl enable --now firewalld || warn "firewalld enable/start failed."
# ---- Set default zone to 'drop' (silently discard all inbound) ----
# NOTE: --set-default-zone is ALWAYS permanent (written to firewalld.conf).
# firewalld rejects --permanent combined with --set-default-zone — do not add it.
info "Setting default zone to 'drop' (default-deny all inbound)..."
firewall-cmd --set-default-zone=drop
# ---- SSH access from LAN (RFC1918 IPv4) ----
info "Adding SSH rich rule: permit from RFC1918 only..."
firewall-cmd --permanent --zone=drop \
--add-rich-rule='rule family="ipv4" source address="192.168.0.0/16" service name="ssh" accept' \
2>/dev/null || info "SSH IPv4 rule already exists."
# ---- IPv6 ULA and link-local SSH access ----
firewall-cmd --permanent --zone=drop \
--add-rich-rule='rule family="ipv6" source address="fe80::/10" service name="ssh" accept' \
2>/dev/null || info "IPv6 link-local SSH rule already exists."
firewall-cmd --permanent --zone=drop \
--add-rich-rule='rule family="ipv6" source address="fc00::/7" service name="ssh" accept' \
2>/dev/null || info "IPv6 ULA SSH rule already exists."
# ---- mDNS for printer discovery (LAN only, not internet-facing) ----
firewall-cmd --permanent --zone=drop \
--add-rich-rule='rule family="ipv4" source address="192.168.0.0/16" service name="mdns" accept' \
2>/dev/null || info "mDNS rule already exists."
# ---- DHCPv6 client (required for IPv6 address assignment on most networks) ----
firewall-cmd --permanent --zone=drop \
--add-service=dhcpv6-client \
2>/dev/null || info "DHCPv6-client rule already exists."
# ---- WireGuard inbound port ----
# This machine is a WireGuard CLIENT only — egress to UDP 51820 is covered
# by the default-allow-out policy. Uncomment only if this machine needs to
# ACCEPT inbound WireGuard connections (i.e. acts as a server/endpoint):
# firewall-cmd --permanent --zone=drop --add-port=51820/udp
# ---- Reload to apply permanent changes ----
info "Reloading firewalld to apply configuration..."
firewall-cmd --reload
info "Firewalld configuration complete."
info "Active configuration:"
firewall-cmd --list-all --zone=drop || true
fi
# ===============================================================================
# SECTION 5: SSH Hardening
#
# OpenSSH server is pre-installed on Bazzite. We harden it using a drop-in
# config in /etc/ssh/sshd_config.d/ which survives image updates.
#
# Key changes from the LMDE version:
# - Service name is 'sshd' (not 'ssh') on Fedora/Bazzite
# - sftp subsystem path is /usr/libexec/openssh/sftp-server on Fedora
# - sshd_config is not writable on the immutable root — we use sshd_config.d
# ===============================================================================
step "SSH Hardening"
SSHD_DIR="/etc/ssh"
SSHD_CFG="${SSHD_DIR}/sshd_config"
SSHD_CUSTOM_DIR="${SSHD_DIR}/sshd_config.d"
SSHD_CUSTOM_CFG="${SSHD_CUSTOM_DIR}/99-hardened-custom.conf"
BACKUP_DIR="${SSHD_DIR}/backup-${BACKUP_TS}"
mkdir -p "${BACKUP_DIR}"
mkdir -p "${SSHD_CUSTOM_DIR}"
info "Backing up existing sshd_config and host keys to ${BACKUP_DIR}..."
cp -a "${SSHD_CFG}" "${BACKUP_DIR}/sshd_config.bak" 2>/dev/null || \
warn "sshd_config not yet present (pending rpm-ostree reboot) — will configure keys post-reboot."
for key in ssh_host_ed25519_key ssh_host_rsa_key ssh_host_ecdsa_key; do
[[ -f "${SSHD_DIR}/${key}" ]] && cp -a "${SSHD_DIR}/${key}" "${BACKUP_DIR}/" || true
[[ -f "${SSHD_DIR}/${key}.pub" ]] && cp -a "${SSHD_DIR}/${key}.pub" "${BACKUP_DIR}/" || true
done
# Regenerate host keys only if the openssh-server keys exist
# (they may not exist until after the rpm-ostree reboot if openssh-server was just layered)
if [[ -d /etc/ssh ]]; then
info "Removing old SSH host keys..."
rm -f /etc/ssh/ssh_host_*
info "Generating new ED25519 host key..."
ssh-keygen -t ed25519 -f "${SSHD_DIR}/ssh_host_ed25519_key" -N "" -o -a 100
info "Generating new RSA 4096 host key..."
ssh-keygen -t rsa -b 4096 -f "${SSHD_DIR}/ssh_host_rsa_key" -N "" -o
chmod 600 "${SSHD_DIR}/ssh_host_ed25519_key" "${SSHD_DIR}/ssh_host_rsa_key"
chmod 644 "${SSHD_DIR}/ssh_host_ed25519_key.pub" "${SSHD_DIR}/ssh_host_rsa_key.pub"
fi
# Ensure main sshd_config includes the drop-in directory
if [[ -f "${SSHD_CFG}" ]] && ! grep -q "^Include ${SSHD_CUSTOM_DIR}/\*.conf" "${SSHD_CFG}"; then
# On Fedora Atomic the base config file is on the immutable layer.
# The Include directive may already exist. Check before patching.
if grep -q "^Include" "${SSHD_CFG}"; then
info "Include directive already present in sshd_config."
else
# /etc/ is writable (it's a separate overlay), so this is safe.
sed -i "1i Include ${SSHD_CUSTOM_DIR}/*.conf" "${SSHD_CFG}"
info "Added Include directive to ${SSHD_CFG}."
fi
fi
info "Writing hardened SSH drop-in to ${SSHD_CUSTOM_CFG}..."
cat > "${SSHD_CUSTOM_CFG}" <<EOF
# Hardened SSH Configuration — Bazzite
# Managed by setup-laptop-bazzite.sh (${BACKUP_TS})
# Drop-in file: survives rpm-ostree image updates.
Protocol 2
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
HostbasedAuthentication no
PermitEmptyPasswords no
ChallengeResponseAuthentication no
KbdInteractiveAuthentication no
UsePAM yes
LoginGraceTime 30
MaxAuthTries 4
MaxSessions 10
HostKey ${SSHD_DIR}/ssh_host_ed25519_key
HostKey ${SSHD_DIR}/ssh_host_rsa_key
AllowAgentForwarding no
AllowTcpForwarding no
X11Forwarding no
ClientAliveInterval 300
ClientAliveCountMax 2
UseDNS no
PrintMotd no
PrintLastLog yes
AcceptEnv LANG LC_*
# Subsystem path on Fedora/Bazzite (differs from Debian)
Subsystem sftp /usr/libexec/openssh/sftp-server
# Permit password auth from LAN (RFC1918), ULA, and link-local IPv6 only
Match Address 192.168.0.0/16,fe80::/10,fc00::/7
PermitRootLogin no
PasswordAuthentication yes
AllowTcpForwarding no
EOF
chmod 644 "${SSHD_CUSTOM_CFG}"
# Enable and start sshd
info "Enabling and starting sshd service..."
systemctl enable sshd || warn "sshd enable failed (may not be active until reboot)."
systemctl start sshd 2>/dev/null || warn "sshd start failed — openssh-server may not be active until after reboot."
# Validate config if sshd binary is present
if command -v sshd &>/dev/null; then
if sshd -t 2>/dev/null; then
info "sshd_config syntax OK."
systemctl restart sshd 2>/dev/null || warn "sshd restart failed."
else
warn "sshd_config test failed — check ${SSHD_CUSTOM_CFG}"
warn "Backup at: ${BACKUP_DIR}/sshd_config.bak"
fi
else
info "sshd binary not yet active (will be after reboot). Config written."
fi
# ===============================================================================
# SECTION 6: ClamAV
#
# ClamAV is installed natively via rpm-ostree layering.
# Three packages are required: clamav, clamav-freshclam, clamd
#
# VERIFIED INSTALL (Bazzite / Fedora 44, clamav 1.4.4): all five binaries land
# from these three packages - clamscan, clamdscan, freshclam, clamd, clamonacc.
# clamd-1.4.4 provides BOTH the clamd@ daemon template and the clamonacc
# on-access client. No clamav-update package is needed (clamav-freshclam is
# named directly).
#
# Architecture (DETECTION on access - scan + quarantine, not open-blocking):
# clamd@scan.service - clamd daemon (template unit, /etc/clamd.d/scan.conf)
# clamav-freshclam.service - signature updates
# clamav-clamonacc.service - on-access scanner (connects to clamd LocalSocket)
#
# This section ONLY layers the packages. INSTALL IS COMPLETE AFTER REBOOT BUT
# NOT YET FUNCTIONAL - it is WAITING ON CONFIGURATION. No service is enabled or
# configured here. A REBOOT IS REQUIRED after this script completes; then run
# setup-clamav.sh (as root) to configure scan.conf, enable services in the
# correct order (clamd@scan before clamonacc), and run the EICAR test.
#
# NOTE: Do not enable or configure ClamAV services in this script - the layered
# packages are not active until after reboot. All configuration, service
# enablement, and testing is handled by setup-clamav.sh post-reboot.
#
# -------------------------------------------------------------------------------
# CLEAN-UP REQUIRED (remove previous errors):
# A clean of any PRIOR ClamAV rpm-ostree layering may be required before this
# section runs cleanly. If ClamAV (or any of its packages) is already present
# in the Bazzite BASE IMAGE, an earlier run of this script - or an older
# version that did `rpm-ostree install clamav ...` unconditionally - will have
# left stale / inactive overlay requests. These surface as errors or
# "inactive request" entries in `rpm-ostree status` and can block or confuse
# subsequent deployments.
#
# The `rpm -q` guard below prevents this script from RE-queuing packages that
# are already present, but it CANNOT remove requests already queued by an
# older script version. Clean those manually:
#
# sudo rpm-ostree uninstall clamav clamav-freshclam clamd
# sudo systemctl reboot
#
# (uninstall only the entries that actually show as layered/inactive in
# `rpm-ostree status` - do not blindly uninstall packages that are part of
# the base image). After the reboot, re-run this script.
#
# The detection block below is NON-DESTRUCTIVE: it only WARNS when existing
# clamav/clamd overlay requests are found. It deliberately does not run
# `rpm-ostree uninstall` automatically.
#
# NOTE: live configuration of ClamAV on Bazzite is still being finalised on a
# separate machine via setup-clamav.sh. This section continues to layer
# packages only; do not add service configuration here.
# ===============================================================================
step "ClamAV (native rpm-ostree)"
# ---- Clean-up note: detect stale ClamAV overlay requests from previous runs ----
# Non-destructive: warn only. See the CLEAN-UP REQUIRED note above for the
# manual remediation (rpm-ostree uninstall + reboot).
if rpm-ostree status 2>/dev/null | grep -Eiq 'clamav|clamd'; then
warn "Existing ClamAV-related rpm-ostree overlay request(s) detected."
warn "If these show as inactive/errored in 'rpm-ostree status', clean them"
warn "to remove previous errors BEFORE re-layering:"
warn " sudo rpm-ostree uninstall clamav clamav-freshclam clamd"
warn " sudo systemctl reboot"
warn "Only uninstall entries that are actually layered/inactive - ClamAV may"
warn "already be part of the Bazzite base image, in which case leave it be."
fi
CLAMAV_PACKAGES=(clamav clamav-freshclam clamd)
CLAMAV_MISSING=()
for pkg in "${CLAMAV_PACKAGES[@]}"; do
if ! rpm -q "$pkg" &>/dev/null; then
CLAMAV_MISSING+=("$pkg")
fi
done
if [[ ${#CLAMAV_MISSING[@]} -eq 0 ]]; then
info "ClamAV packages already installed: ${CLAMAV_PACKAGES[*]}"
info "Install is complete - waiting on configuration."
info "Run setup-clamav.sh (as root) to configure and activate services."
else
info "Layering ClamAV packages via rpm-ostree: ${CLAMAV_MISSING[*]}"
if rpm-ostree install "${CLAMAV_MISSING[@]}"; then
warn "ClamAV packages queued - REBOOT REQUIRED to activate."
warn "Install will be complete after reboot, but NOT yet configured."
warn "After reboot, run: sudo ./setup-clamav.sh"
REBOOT_REQUIRED=true
else
warn "rpm-ostree install failed for ClamAV - check: sudo rpm-ostree status"
warn "If the failure is a stale/inactive request from a previous run,"
warn "clean it first: sudo rpm-ostree uninstall clamav clamav-freshclam clamd"
fi
fi
# ===============================================================================
# SECTION 7: UDM SE Certificate
#
# Install the Ubiquiti UDM SE block-page CA certificate into the system
# trust store. On Fedora/Bazzite the tool is 'update-ca-trust' (not
# 'update-ca-certificates' which is Debian-specific).
#
# Certificate store location: /etc/pki/ca-trust/source/anchors/
# ===============================================================================
step "UDM SE Block-Page Certificate"
CERT_URL="http://blog.baden.braedach.com/content/files/2026/06/UniFi-SSL-Certificate.cer.zip"
CERT_ZIP="/tmp/UniFi-SSL-Certificate.cer.zip"
CERT_EXTRACT_DIR="/tmp/udmse-cert-extract"
CERT_TMP="/tmp/UniFi-SSL-Certificate.cer"
CERT_DST="/etc/pki/ca-trust/source/anchors/udmse-blockpage-ca.crt"
# Clean any artifacts from a previous run (idempotent re-run safety)
rm -rf "${CERT_EXTRACT_DIR}"
rm -f "${CERT_ZIP}" "${CERT_TMP}" "${CERT_TMP}.pem"
info "Downloading UDM SE certificate archive from ${CERT_URL}..."
if ! wget -q -O "${CERT_ZIP}" "${CERT_URL}"; then
warn "Failed to download certificate from ${CERT_URL}"
warn "Ensure blog.baden.braedach.com is reachable from this machine."
else
info "Extracting certificate from archive..."
mkdir -p "${CERT_EXTRACT_DIR}"
EXTRACTED=0
if command -v unzip >/dev/null 2>&1; then
if unzip -o -q "${CERT_ZIP}" -d "${CERT_EXTRACT_DIR}"; then
EXTRACTED=1
fi
elif command -v python3 >/dev/null 2>&1; then
info "unzip not found -- falling back to python3 zipfile module..."
if python3 -m zipfile -e "${CERT_ZIP}" "${CERT_EXTRACT_DIR}"; then
EXTRACTED=1
fi
else
warn "No unzip or python3 available -- cannot extract archive."
fi
if [[ "${EXTRACTED}" -ne 1 ]]; then
warn "Failed to extract archive -- skipping cert install."
else
# Locate the .cer inside the archive; fall back to first file found
FOUND_CERT="$(find "${CERT_EXTRACT_DIR}" -type f -iname '*.cer' | head -n1)"
if [[ -z "${FOUND_CERT}" ]]; then
FOUND_CERT="$(find "${CERT_EXTRACT_DIR}" -type f | head -n1)"
fi
if [[ -z "${FOUND_CERT}" ]]; then
warn "No certificate file found in archive -- skipping cert install."
else
cp "${FOUND_CERT}" "${CERT_TMP}"
# Convert DER to PEM if needed
if ! grep -q "BEGIN CERTIFICATE" "${CERT_TMP}"; then
info "Attempting DER-to-PEM conversion..."
if openssl x509 -inform DER -in "${CERT_TMP}" -out "${CERT_TMP}.pem" 2>/dev/null; then
mv "${CERT_TMP}.pem" "${CERT_TMP}"
else
warn "Downloaded file is not a valid PEM or DER certificate -- skipping install."
rm -f "${CERT_TMP}" "${CERT_TMP}.pem"
fi
fi
if [[ -f "${CERT_TMP}" ]] && grep -q "BEGIN CERTIFICATE" "${CERT_TMP}"; then
cp "${CERT_TMP}" "${CERT_DST}"
if update-ca-trust; then
info "UDM SE block-page certificate installed successfully."
info "Installed to: ${CERT_DST}"
else
warn "update-ca-trust failed -- check if the file is valid PEM."
fi
fi
fi
fi
fi
# Tidy up temporary files
rm -rf "${CERT_EXTRACT_DIR}"
rm -f "${CERT_ZIP}" "${CERT_TMP}" "${CERT_TMP}.pem"
# ===============================================================================
# SECTION 8: Firmware Updates (fwupd)
# ===============================================================================
step "Firmware Updates (fwupd)"
if ! command -v fwupdmgr &>/dev/null; then
warn "fwupdmgr not found — skipping firmware updates."
else
# --- Daemon pre-flight ---
if ! systemctl is-active --quiet fwupd; then
info "fwupd service not running — attempting to start..."
if ! timeout 15 systemctl start fwupd 2>/dev/null; then
warn "fwupd service failed to start — skipping firmware updates."
fwupdmgr_ok=0
else
info "fwupd service started."
fwupdmgr_ok=1
fi
else
fwupdmgr_ok=1
fi
if [[ "${fwupdmgr_ok}" -eq 1 ]]; then
# --- AC power pre-flight ---
AC_ONLINE=0
for ps in /sys/class/power_supply/AC*/online /sys/class/power_supply/ADP*/online; do
[[ -f "${ps}" ]] && AC_ONLINE=$(cat "${ps}") && break
done
if [[ "${AC_ONLINE}" -eq 1 ]]; then
info "AC power confirmed — BIOS/UEFI capsule updates eligible."
else
warn "AC power NOT detected. BIOS/UEFI firmware updates will be blocked by fwupd."
warn "Connect AC adapter if a BIOS update is needed, then re-run: sudo fwupdmgr update"
fi
# --- Battery pre-flight ---
BAT_CAPACITY=""
for bat in /sys/class/power_supply/BAT*/capacity; do
[[ -f "${bat}" ]] && BAT_CAPACITY=$(cat "${bat}") && break
done
if [[ -n "${BAT_CAPACITY}" ]]; then
if [[ "${BAT_CAPACITY}" -ge 10 ]]; then
info "Battery level: ${BAT_CAPACITY}% — meets fwupd minimum (10%)."
else
warn "Battery: ${BAT_CAPACITY}% — below fwupd minimum. Charge before updating BIOS."
fi
else
info "No battery detected — skipping battery check."
fi
# --- Refresh LVFS metadata ---
info "Refreshing LVFS firmware metadata (timeout: 60s)..."
if ! timeout 60 fwupdmgr refresh --force 2>&1; then
warn "LVFS metadata refresh failed or timed out — check internet connectivity."
else
# --- Enumerate devices ---
info "Querying devices visible to fwupd (timeout: 30s)..."
timeout 30 fwupdmgr get-devices 2>&1 || warn "Could not enumerate fwupd devices."
# --- Check for updates ---
info "Checking for available firmware updates (timeout: 60s)..."
UPDATE_OUTPUT=$(timeout 60 fwupdmgr get-updates 2>&1 || true)
if [[ -z "${UPDATE_OUTPUT}" ]]; then
warn "fwupdmgr get-updates returned no output — may have timed out or failed."
elif echo "${UPDATE_OUTPUT}" | grep -qi "no upgrades\|nothing to do\|no.*update"; then
info "System firmware is already up to date."
info "If a BIOS update is required but not showing via LVFS:"
info " 1. Download BIOS from the manufacturer support page"
info " 2. Boot Hiren's BootCD PE from USB"
info " 3. Run the flasher from within Windows PE"
else
info "Firmware updates available:"
echo "${UPDATE_OUTPUT}"
if echo "${UPDATE_OUTPUT}" | grep -qi "system firmware\|uefi\|bios"; then
info "BIOS/UEFI firmware update detected."
if [[ "${AC_ONLINE}" -ne 1 ]]; then
warn "Skipping BIOS update — AC power required."
warn "Connect AC and run: sudo fwupdmgr update"
else
info "Applying BIOS update (will apply on next reboot via EFI capsule, timeout: 300s)..."
timeout 300 fwupdmgr update -y 2>&1 || warn "Firmware update failed or timed out — check output above."
info "IMPORTANT: Reboot required to apply BIOS/UEFI update."
info "Do NOT interrupt power during the reboot cycle."
fi
else
info "Applying non-BIOS firmware updates (timeout: 300s)..."
timeout 300 fwupdmgr update -y 2>&1 || warn "One or more firmware updates failed or timed out."
fi
fi
fi
fi
fi
# ===============================================================================
# SECTION 9: Automatic Updates
#
# Bazzite's OS updates are atomic via rpm-ostree. The rpm-ostreed service
# can stage updates automatically; they apply on reboot (never mid-session).
#
# Flatpak updates are managed by Bazzite's bazzite-flatpak-manager on boot,
# and can also be triggered via `flatpak update` or `ujust update`.
# ===============================================================================
step "Automatic Updates Configuration"
# Enable rpm-ostree automatic update staging
OSTREE_CONF="/etc/rpm-ostreed.conf"
if [[ -f "${OSTREE_CONF}" ]]; then
# Set AutomaticUpdatePolicy to 'stage' — downloads and stages updates
# automatically. They are applied on the next user-initiated reboot.
if grep -q "^AutomaticUpdatePolicy=" "${OSTREE_CONF}"; then
sed -i 's/^AutomaticUpdatePolicy=.*/AutomaticUpdatePolicy=stage/' "${OSTREE_CONF}"
else
echo "AutomaticUpdatePolicy=stage" >> "${OSTREE_CONF}"
fi
info "rpm-ostree AutomaticUpdatePolicy set to 'stage'."
else
# Bazzite may put this in /usr/etc/ (immutable). Write to /etc/.
mkdir -p /etc/rpm-ostreed.conf.d 2>/dev/null || true
cat > /etc/rpm-ostreed.conf <<EOF
[Daemon]
AutomaticUpdatePolicy=stage
EOF
info "Created /etc/rpm-ostreed.conf with AutomaticUpdatePolicy=stage."
fi
# Enable the rpm-ostreed update timer
systemctl enable --now rpm-ostreed-automatic.timer 2>/dev/null \
|| warn "rpm-ostreed-automatic.timer not found — updates will stage on login."
# Flatpak: Bazzite handles this at boot, but ensure the background update
# service is enabled for user-session Flatpaks
systemctl --user enable --now flatpak-system-update.timer 2>/dev/null \
|| info "Flatpak auto-update timer: managed by Bazzite (bazzite-flatpak-manager)."
info "Automatic update configuration complete."
info "OS updates stage silently and apply on next reboot."
info "Run 'ujust update' at any time to manually check and stage updates."
# ===============================================================================
# SECTION 10: MOTD
# ===============================================================================
step "MOTD Configuration"
info "Configuring MOTD system information display..."
# Bazzite uses pam_motd; drop scripts in /etc/update-motd.d/
mkdir -p /etc/update-motd.d/
rm -f /etc/update-motd.d/*
cat > /etc/update-motd.d/80-sysinfo <<'EOF'
#!/bin/bash
echo "=== System Information ==="
echo ""
echo " 🖥️ Operating System : $(grep PRETTY_NAME /etc/os-release | cut -d= -f2 | tr -d '"')"
echo " 🌐 IPv4 Address : $(hostname -I 2>/dev/null | awk '{print $1}')"
echo " 🔗 IPv6 Global : $(ip -6 addr show scope global 2>/dev/null | grep inet6 | awk '{print $2}' | cut -d'/' -f1 | head -n1)"
echo " 🪢 IPv6 Link-local : $(ip -6 addr show scope link 2>/dev/null | grep inet6 | awk '{print $2}' | cut -d'/' -f1 | head -n1)"
echo " ⏱️ System Uptime : $(uptime -p 2>/dev/null)"
echo " 📦 rpm-ostree : $(rpm-ostree status --booted 2>/dev/null | grep Version | head -n1 | awk '{print $2}' || echo 'unknown')"
echo ""
EOF
chmod +x /etc/update-motd.d/80-sysinfo
# ===============================================================================
# POST-SCRIPT INSTRUCTIONS
# ===============================================================================
cat <<'POSTINSTALL'
════════════════════════════════════════════════════════════════
IMPORTANT POST-SCRIPT ACTIONS
════════════════════════════════════════════════════════════════
1. REBOOT REQUIRED
rpm-ostree packages (wireguard-tools, insync, openssh-server,
fwupd, etc.) are staged and will activate on next reboot.
Run: systemctl reboot
Or just reboot normally.
2. AFTER REBOOT — Complete printer setup (if CUPS wasn't running):
sudo lpadmin -p EPSON-WF4830 -E \
-v ipp://192.168.1.11/ipp/print \
-m everywhere -D "EPSON WF-4830 Series"
3. AFTER REBOOT — Set up Insync:
Open the Insync GUI from your application launcher.
Authenticate your Google account(s) and configure sync folders.
Insync is a paid app ($29.99/Google account, 15-day trial).
4. WAYDROID (Android / Google Play Store):
Bazzite has native Waydroid support. After reboot, run:
ujust setup-waydroid
This launches an interactive setup wizard in the terminal.
Follow the prompts to enable Android container and Google Play.
ARM translation (for most apps) is included automatically.
NOTE: This CANNOT be automated — it requires graphical interaction.
5. WIREGUARD VPN:
WireGuard kernel support is built into Bazzite's kernel.
The 'wg' and 'wg-quick' CLI tools are now layered.
To connect using a config file:
sudo wg-quick up /etc/wireguard/wg0.conf
Or use Settings > Network > VPN to add a WireGuard connection.
6. CLAMAV SCANNING:
ClamAV packages are layered only. After reboot, configure and
activate it with setup-clamav.sh (handles scan.conf, service order,
and the EICAR test). If a previous run left stale/errored ClamAV
overlay requests, clean them first to remove previous errors:
sudo rpm-ostree uninstall clamav clamav-freshclam clamd
sudo systemctl reboot
Then run: sudo ./setup-clamav.sh
7. MANUAL SYSTEM UPDATE:
ujust update
8. VERIFY SECURE BOOT / SIGNED IMAGE:
ujust verify-image
════════════════════════════════════════════════════════════════
POSTINSTALL
info "Setup complete. Log: ${LOGFILE}"
info "SSH backup: ${BACKUP_DIR}"
echo ""
# Optional automatic reboot prompt
read -r -t 30 -p "[?] Reboot now to activate layered packages? [y/N] (auto-N in 30s): " REBOOT_CONFIRM || true
if [[ "${REBOOT_CONFIRM,,}" == "y" ]]; then
info "Rebooting in 5 seconds..."
sleep 5
systemctl reboot
else
info "Reboot skipped. Please reboot manually when ready."
fi
#endscriptHope this helps someone. Probaby not a wise idea to share the internal network domains but if I get hacked it will get reported to Ubiquiti for the betterment of everyone else.
#enoughsaid