BazziteOS - Draft Script

BazziteOS - Draft Script
BassziteOS - Gaming on LInux

The folllowing script is a draft script and is provided as a guide.

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 a draft. It is in testing, but offered as a guide

The followng is noted:

  1. The printer section has been left in as a guide
  2. 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.
  3. ClamAV has not been hardened in this script.

Draft BazziteOS setup script.

#!/bin/bash

# ================================================================================
# Base Setup Script for Bazzite OS (Fedora Atomic / rpm-ostree)
# Filename : setup-laptop-bazzite.sh
# Updated  : 2026-05-15
# Version  : 1.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 installed via rpm-ostree, run as a rootful Distrobox
#       container to avoid conflicts with the immutable OS image
#     - Configures firewalld (Bazzite's native firewall) with default-deny
#       incoming policy across all zones, preserving SSH and WireGuard egress
#     - UDM SE block-page certificate installed to system trust store
#
#   SOFTWARE INSTALLATION (Flatpak via Flathub):
#     - GIMP, Inkscape, Krita, LibreOffice, VLC, Pinta
#     - 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)
#
#   INSYNC (Google Drive client):
#     - Adds insynchq Fedora RPM repo to /etc/yum.repos.d/
#     - Layers insync via rpm-ostree (requires reboot)
#     - NOTE: Insync will be active after the post-script reboot
#
#   SYSTEM CONFIGURATION:
#     - Epson WF-4830 printer via IPP Everywhere
#     - IPv6 stable address preference (disables temp addresses on wlo1)
#     - Automatic OS and Flatpak updates via ujust / rpm-ostree
#     - Custom MOTD (OS, IP, uptime, IPv4/IPv6)
#
#   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
#
#   DIAGNOSTICS:
#     - Final health check covering firewall, SSH, ClamAV, Insync,
#       systemd units, and rpm-ostree deployment status
#
# 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
#
# 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:
#   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-time192.168.1.11 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
#
# All packages listed here are added to the pending deployment.
# They 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"

info "Layering system packages via rpm-ostree..."
info "NOTE: These packages activate after the post-script reboot."

# 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
)

# Build a space-separated list
PKG_LIST="${RPM_PACKAGES[*]}"

if rpm-ostree install --idempotent --allow-inactive ${PKG_LIST}; then
    info "rpm-ostree layering queued successfully."
    info "Packages will be active after reboot."
else
    warn "rpm-ostree install reported an issue — check output above."
    warn "Some packages may already be installed (--idempotent handles this)."
fi

# ===============================================================================
# SECTION 2: Insync (Google Drive client)
#
# Insync provides an RPM repo for Fedora. We drop the repo file into
# /etc/yum.repos.d/ and layer the package via rpm-ostree.
#
# NOTE: Insync is a commercial/proprietary app ($29.99/account, 15-day trial).
#       It is the recommended native GUI Google Drive client for Fedora.
# ===============================================================================

step "Insync (Google Drive Client)"

INSYNC_REPO="/etc/yum.repos.d/insync.repo"
INSYNC_GPG_URL="https://d2t3ff60b2tol4.cloudfront.net/repomd.xml.key"
INSYNC_REPO_URL="https://yum.insynchq.com/fedora/\$releasever/"

info "Adding Insync RPM repository..."

# Import GPG key
if curl -fsSL "${INSYNC_GPG_URL}" | rpm --import -; then
    info "Insync GPG key imported."
else192.168.1.11
    warn "Failed to import Insync GPG key — Insync install may fail."
fi

# Write repo file
cat > "${INSYNC_REPO}" <<EOF
[insync]
name=Insync repo (Fedora)
baseurl=${INSYNC_REPO_URL}
gpgcheck=1
gpgkey=${INSYNC_GPG_URL}
enabled=1
repo_gpgcheck=1
metadata_expire=120m
EOF

info "Insync repo written to ${INSYNC_REPO}"

# Layer insync - detect desktop environment for file manager integration
# KDE: insync-dolphin  |  GNOME: insync-nautilus
DE="${XDG_CURRENT_DESKTOP:-}"
INSYNC_INTEGRATION="insync"

if echo "${DE}" | grep -qi "kde\|plasma"; then
    INSYNC_INTEGRATION="insync insync-dolphin"
    info "KDE/Plasma detected — adding insync-dolphin integration."
elif echo "${DE}" | grep -qi "gnome"; then
    INSYNC_INTEGRATION="insync insync-nautilus"
    info "GNOME detected — adding insync-nautilus integration."
else
    info "Desktop environment not detected at script runtime (expected when run as root)."
    info "Install insync-dolphin (KDE) or insync-nautilus (GNOME) manually after reboot."
fi

if rpm-ostree install --idempotent --allow-inactive ${INSYNC_INTEGRATION}; then
    info "Insync layering queued — will be active after reboot."
else
    warn "Insync rpm-ostree layer failed. You can install it manually after reboot:"
    warn "  sudo rpm-ostree install insync insync-dolphin  # KDE"
    warn "  sudo rpm-ostree install insync insync-nautilus # GNOME"
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"
)

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."

    # ---- Drop default zone to 'drop' (silently discard all inbound) ----
    # Bazzite ships with 'FedoraWorkstation' as active zone; we move to 'drop'
    # as the base policy and layer exceptions on top.

    info "Setting default zone to 'drop' (default-deny all inbound)..."
    firewall-cmd --permanent --set-default-zone=drop

    # ---- Create a dedicated zone for LAN management (SSH, etc.) ----
    # This zone applies to the LAN subnet — RFC1918 sources only.
    # We use a rich rule rather than a zone source to keep it clean.

    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 || warn "SSH rich rule may already exist."

    # ---- 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 || warn "IPv6 link-local SSH rule may already exist."

    firewall-cmd --permanent --zone=drop \
        --add-rich-rule='rule family="ipv6" source address="fc00::/7" service name="ssh" accept' \
        2>/dev/null || warn "IPv6 ULA SSH rule may already exist."

    # ---- Allow 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 || warn "mDNS rule may already exist."

    # ---- WireGuard inbound port (for if this machine is a WireGuard endpoint) ----
    # Only open this if the machine needs to accept inbound WireGuard connections.
    # If this machine is a WireGuard CLIENT ONLY (egress only), comment this out.
    # Egress (outbound to port 51820) is always allowed under default-allow-out.
    # Uncomment the line below if you need inbound WireGuard:
    # 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
#
# The Bazzite immutable image ships without ClamAV. Layering it via rpm-ostree
# is possible but freshclam's systemd unit conflicts with image update timers.
#
# SAFE APPROACH: Run ClamAV inside a rootful Distrobox (Fedora) container.
# This avoids any conflict with the host image and is fully functional.
#
# What this section does:
#   - Creates a rootful Distrobox container 'clamav-box' (Fedora latest)
#   - Installs clamav + clamav-update inside the container
#   - Runs freshclam to pull initial definitions
#   - Creates a host-side scan wrapper at /usr/local/bin/clamscan-host
#   - Creates a systemd timer to auto-update definitions weekly
# ===============================================================================

step "ClamAV (via Distrobox Container)"

if ! command -v distrobox &>/dev/null; then
    warn "Distrobox not found. ClamAV container setup skipped."
    warn "Install Distrobox and re-run, or install clamav via:"
    warn "  sudo rpm-ostree install clamav clamav-update  (then reboot)"
else

    info "Creating rootful Distrobox container for ClamAV..."
    # --root flag = rootful container (required for freshclam systemd integration)
    if distrobox create \
        --name clamav-box \
        --image fedora:latest \
        --root \
        --yes \192.168.1.11
        --no-entry 2>/dev/null; then
        info "clamav-box container created."
    else
        warn "clamav-box creation may have failed or already exists — continuing."
    fi

    info "Installing ClamAV inside clamav-box..."
    distrobox enter --root clamav-box -- bash -c \
        "dnf install -y clamav clamav-update clamd && freshclam && echo 'ClamAV ready.'" \
        || warn "ClamAV install inside container failed — check container status."

    # Create a wrapper so you can scan host directories from the host shell
    info "Creating host scan wrapper at /usr/local/bin/clamscan-host..."
    cat > /usr/local/bin/clamscan-host <<'WRAPPER'
#!/bin/bash
# Scan a host directory using ClamAV in the clamav-box Distrobox container.
# Usage: clamscan-host /path/to/scan
TARGET="${1:-$HOME}"
echo "[clamscan-host] Scanning: ${TARGET}"
distrobox enter --root clamav-box -- clamscan --recursive --infected "${TARGET}"
WRAPPER
    chmod +x /usr/local/bin/clamscan-host
    info "  Usage: clamscan-host /home/${CURRENT_USER}"

    # Weekly definitions update timer
    info "Creating systemd timer for weekly ClamAV definition updates..."
    cat > /etc/systemd/system/clamav-update.service <<EOF
[Unit]
Description=Update ClamAV definitions in clamav-box container
After=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/bin/distrobox enter --root clamav-box -- freshclam
EOF

    cat > /etc/systemd/system/clamav-update.timer <<EOF
[Unit]
Description=Weekly ClamAV definition update

[Timer]
OnCalendar=weekly
Persistent=true

[Install]
WantedBy=timers.target
EOF

    systemctl daemon-reload
    systemctl enable --now clamav-update.timer || warn "ClamAV update timer failed to start."
    info "ClamAV update timer enabled (weekly)."

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://ghost.baden.braedach.com/content/files/2025/12/UniFi-SSL-Certificate.cer"
CERT_TMP="/tmp/UniFi-SSL-Certificate.cer"
CERT_DST="/etc/pki/ca-trust/source/anchors/udmse-blockpage-ca.crt"

info "Downloading UDM SE certificate from ${CERT_URL}..."

if ! wget -q -O "${CERT_TMP}" "${CERT_URL}"; then
    warn "Failed to download certificate from ${CERT_URL}"
    warn "Ensure ghost.baden.braedach.com is reachable from this machine."
else
    if ! grep -q "BEGIN CERTIFICATE" "${CERT_TMP}"; then
        warn "Downloaded file is not a valid PEM certificate — skipping install."
        rm -f "${CERT_TMP}"
    else
        cp "${CERT_TMP}" "${CERT_DST}"
        if update-ca-trust extract; 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
        rm -f "${CERT_TMP}"
    fi
fi

# ===============================================================================
# SECTION 8: IPv6 Stable Address Configuration
#
# Disables IPv6 temporary/privacy addresses on wlo1 to prevent mDNS/Avahi
# resolution failures caused by address churn. Uses sysctl drop-in.
# /etc/sysctl.d/ is writable on Bazzite (/etc is an overlay).
# ===============================================================================

step "IPv6 Configuration"

info "Configuring IPv6 stable addressing..."

cat > /etc/sysctl.d/99-ipv6-laptop.conf <<'EOF'
# Prefer stable IPv6 addresses over temporary privacy addresses.
# Reduces mDNS/Avahi resolution failures caused by address churn on wlo1.
net.ipv6.conf.wlo1.use_tempaddr=0
net.ipv6.conf.all.use_tempaddr=0
net.ipv6.conf.default.use_tempaddr=0
EOF

sysctl -p /etc/sysctl.d/99-ipv6-laptop.conf || warn "IPv6 sysctl apply failed."

# ===============================================================================
# SECTION 9: Printer Setup (Epson WF-4830)
#
# CUPS / avahi have been queued in the rpm-ostree layer above.
# lpadmin will fail if CUPS is not yet active (needs the rpm-ostree reboot).
# We attempt it here; if it fails, a post-reboot reminder is printed.
# ===============================================================================

step "Epson WF-4830 Printer"

info "Attempting to install Epson WF-4830 via IPP Everywhere..."

if systemctl is-active --quiet cups 2>/dev/null; then
    if lpadmin -p "EPSON-WF4830" \
        -E \
        -v "ipp://192.168.1.11/ipp/print" \
        -m everywhere \
        -D "EPSON WF-4830 Series"; then
        info "Printer installed successfully."
        lpoptions -d EPSON-WF4830 || true
    else
        warn "lpadmin failed. Try again after reboot once CUPS is active."
    fi
else
    warn "CUPS service not running yet (expected before rpm-ostree reboot)."
    warn "Run after reboot: sudo lpadmin -p EPSON-WF4830 -E -v ipp://192.168.1.11/ipp/print -m everywhere -D 'EPSON WF-4830 Series'"
fi

# ===============================================================================
# SECTION 10: Firmware Updates (fwupd)
# ===============================================================================

step "Firmware Updates (fwupd)"

if ! command -v fwupdmgr &>/dev/null; then
    warn "fwupdmgr not found — queued via rpm-ostree, will be available after reboot."
else

    # 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 updates will be blocked by fwupd."
        warn "Connect AC adapter before running if a BIOS update is needed."
    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."
        else
            warn "Battery: ${BAT_CAPACITY}% — below fwupd minimum of 10%."
        fi
    else
        info "No battery detected — skipping battery check."
    fi

    # Refresh LVFS metadata
    info "Refreshing LVFS firmware metadata..."
    if ! fwupdmgr refresh --force; then
        warn "LVFS metadata refresh failed — check connectivity."
    else
        info "Querying devices visible to fwupd..."
        fwupdmgr get-devices || warn "Could not enumerate fwupd devices."

        info "Checking for available firmware updates..."
        UPDATE_OUTPUT=$(fwupdmgr get-updates 2>&1 || true)

        if echo "${UPDATE_OUTPUT}" | grep -qi "no upgrades\|nothing to do\|no.*update"; then
            info "System firmware is already up to date."
        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)..."
                    fwupdmgr update -y || warn "Firmware update failed."
                    info "IMPORTANT: Reboot required to apply BIOS/UEFI capsule update."
                fi
            else
                info "Applying non-BIOS firmware updates..."
                fwupdmgr update -y || warn "One or more firmware updates failed."
            fi
        fi
    fi

fi

# ===============================================================================
# SECTION 11: 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 trigger192.168.1.11ed 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 12: 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

# ===============================================================================
# SECTION 13: Final Diagnostics
# ===============================================================================

step "Final Diagnostics"

DIAG_PASS=0
DIAG_WARN=0
DIAG_FAIL=0

diag_ok()   { echo -e "  192.168.1.11192.168.1.11192.168.1.11\033[0;32m[PASS]\033[0m $*"; (( DIAG_PASS++ )); }
diag_warn() { echo -e "  \033[0;33m[WARN]\033[0m $*"; (( DIAG_WARN++ )); }
diag_fail() { echo -e "  \033[0;31m[FAIL]\033[0m $*"; (( DIAG_FAIL++ )); }

echo ""
echo "  ── rpm-ostree ──────────────────────────────────────────"

if rpm-ostree status &>/dev/null; then
    PENDING=$(rpm-ostree status 2>/dev/null | grep -c "pending" || true)
    if [[ "${PENDING}" -gt 0 ]]; then
        diag_ok  "rpm-ostree has a pending deployment (reboot required to activate)"
    else
        diag_warn "rpm-ostree: no pending deployment detected (layering may have failed)"
    fi
else
    diag_fail "rpm-ostree status failed"
fi

echo ""
echo "  ── Firewall ────────────────────────────────────────────"

if systemctl is-active --quiet firewalld; then
    diag_ok "firewalld is running"
    DEFAULT_ZONE=$(firewall-cmd --get-default-zone 2>/dev/null || echo "unknown")
    if [[ "${DEFAULT_ZONE}" == "drop" ]]; then
        diag_ok "Default zone is 'drop' (default-deny inbound)"
    else
        diag_warn "Default zone is '${DEFAULT_ZONE}' — expected 'drop'"
    fi
else
    diag_fail "firewalld is NOT running"
fi

echo ""
echo "  ── SSH ─────────────────────────────────────────────────"

if [[ -f "${SSHD_CUSTOM_CFG}" ]]; then
    diag_ok "SSH hardened drop-in exists: ${SSHD_CUSTOM_CFG}"
else
    diag_fail "SSH drop-in config NOT found"
fi

if [[ -f "${SSHD_DIR}/ssh_host_ed25519_key" ]]; then
    diag_ok "ED25519 host key present"
else
    diag_warn "ED25519 host key NOT found (normal if openssh-server pending reboot)"
fi
192.168.1.11192.168.1.11192.168.1.11
if systemctl is-active --quiet sshd 2>/dev/null; then
    diag_ok "sshd is running"
else
    diag_warn "sshd is NOT running (expected — activate after rpm-ostree reboot)"
fi

echo ""
echo "  ── UDM SE Certificate ──────────────────────────────────"

if [[ -f "${CERT_DST}" ]]; then
    diag_ok "UDM SE certificate installed: ${CERT_DST}"
else
    diag_warn "UDM SE certificate NOT installed (check connectivity to ghost.baden.braedach.com)"
fi

echo ""
echo "  ── ClamAV Container ────────────────────────────────────"

if command -v distrobox &>/dev/null; then
    if distrobox list --root 2>/dev/null | grep -q "clamav-box"; then
        diag_ok "clamav-box Distrobox container exists"
    else
        diag_warn "clamav-box container not found (may still be initializing)"
    fi
    if [[ -x /usr/local/bin/clamscan-host ]]; then
        diag_ok "clamscan-host wrapper installed"
    else
        diag_warn "clamscan-host wrapper NOT found"
    fi
else
    diag_warn "Distrobox not available — ClamAV container check skipped"
fi

echo ""
echo "  ── Insync ──────────────────────────────────────────────"

if [[ -f /etc/yum.repos.d/insync.repo ]]; then
    diag_ok "Insync RPM repo configured: /etc/yum.repos.d/insync.repo"
else
    diag_warn "Insync RPM repo NOT found"
fi

echo ""
echo "  ── Flatpak Apps ────────────────────────────────────────"

for app in org.gimp.GIMP org.inkscape.Inkscape org.kde.krita com.google.Chrome org.libreoffice.LibreOffice; do
    if flatpak info "${app}" &>/dev/null; then
        diag_ok "${app}"
    else
        diag_warn "${app} — not installed (may have failed)"
    fi
done

echo ""
echo "  ── IPv6 Configuration ──────────────────────────────────"

if [[ -f /etc/sysctl.d/99-ipv6-laptop.conf ]]; then
    diag_ok "IPv6 stable address sysctl installed"
else
    diag_warn "IPv6 sysctl config not found"
fi

echo ""
echo "  ── Auto-Updates ────────────────────────────────────────"

if grep -q "AutomaticUpdatePolicy=stage" /etc/rpm-ostreed.conf 2>/dev/null; then
    diag_ok "rpm-ostree AutomaticUpdatePolicy=stage configured"
else
    diag_warn "rpm-ostree auto-update policy not confirmed"
fi

echo ""
echo "  ── Systemd Failed Units ────────────────────────────────"

FAILED_UNITS=$(systemctl --failed --no-legend --plain 2>/dev/null | awk '{print $1}')
if [[ -z "${FAILED_UNITS}" ]]; then
    diag_ok "No failed systemd units detected"
else
    while IFS= read -r unit; do
        diag_warn "Failed unit: ${unit}"
    done <<< "${FAILED_UNITS}"
fi

echo ""
echo "════════════════════════════════════════════════════════════"
echo "  DIAGNOSTIC SUMMARY"
echo "  Pass: ${DIAG_PASS}  |  Warn: ${DIAG_WARN}  |  Fail: ${DIAG_FAIL}"
echo "════════════════════════════════════════════════════════════"

# ===============================================================================
# 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 m192.168.1.11192.168.1.11192.168.1.11ost 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:
   Scan your home directory:
     clamscan-host /home/YOUR_USERNAME
   Update definitions manually:
     sudo systemctl start clamav-update.service

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

#endscript

Hope 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.

Tough. Not the first time I have shared code and wont be the last.

#enoughsaid