MintOS - Post Script
The following code is shared and is based on the BazziteOS script that was rewritten to take into account the uniquie changes in Bazzite.
The script was then reengineered back to MintOS with a few changes
- MintOS is my daily driver
- MintOS has advantages for me that BazziteOS doesnt accommodate easily
- This code has numerous changes to accommodate my needs
- Clam AV hardening is not in this script - but is to be rewritten shortly
Post MintOS installation script
#!/bin/bash
# ================================================================================
# Base Setup Script for Linux Mint (LMDE 7 / Ubuntu Edition)
# Filename : setup-laptop-mintos.sh
# Updated : 2026-05-17
# Version : 3.0.0
# ================================================================================
#
# PURPOSE:
# Post-install hardening and configuration for Linux Mint laptops.
# Supports two Mint variants, detected automatically at runtime:
#
# • Linux Mint LMDE 7 — Debian base (apt / dpkg)
# • Linux Mint Ubuntu — Ubuntu base (apt / dpkg)
#
# The script aborts with a clear error if run on any other OS.
# All operations are idempotent — safe to re-run.
#
# WHAT THIS SCRIPT DOES:
#
# SECURITY & HARDENING:
# - AppArmor installation and activation
# (LMDE 7: installs and enables; Ubuntu Mint: already active, verifies)
# - UFW firewall: default-deny inbound, default-allow outbound,
# WireGuard egress rule (UDP 51820 out)
# - Hardened SSH via sshd_config.d drop-in (update-resistant)
# - SSH host key regeneration (ED25519 + RSA 4096)
# - ClamAV antivirus with freshclam initial update and weekly timer
# - UDM SE block-page certificate installed to system trust store
#
# PACKAGE MANAGEMENT:
# - Bluetooth package held to prevent driver updates breaking Dell BT
# - Firefox, inetutils-telnet, and system LibreOffice purged
# (LibreOffice Flatpak installed instead for current version)
# - i386 architecture added (Wine / Steam compatibility)
#
# SOFTWARE INSTALLATION:
# - Base tools: curl, wget, nmap, smartmontools, ffmpeg, rsyslog, mtr,
# fwupd, openssh-server, openssh-client, gnupg, cups, avahi-daemon
# - WireGuard VPN (wireguard + wireguard-tools via apt)
# - Flatpak apps: GIMP, Inkscape, Krita, LibreOffice, VLC, Pinta,
# Google Chrome
# - Insync (Google Drive/OneDrive client) via official Debian/Ubuntu
# apt repo — native .deb install with file manager integration
#
# USER-SPECIFIC (braedach only):
# - Android Studio via Flatpak
# - Pods (Podman GUI) via Flatpak
# - Google Antigravity IDE via official APT repo
#
# SYSTEM CONFIGURATION:
# - Epson WF-4830 printer via IPP Everywhere
# - IPv6 stable address preference (disables temp addresses on wlo1)
# - Unattended-upgrades enabled for automatic security updates
# - 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
# - Fallback instructions if no LVFS updates found
#
# DIAGNOSTICS:
# - Final health check covering firewall, SSH, AppArmor, ClamAV,
# Insync, Flatpak apps, auto-updates, and failed systemd units
#
# SUPPORTED DISTROS:
# - Linux Mint LMDE 7 (Debian Trixie base) ID=linuxmint ID_LIKE=debian
# - Linux Mint 22.x (Ubuntu 24.04 base) ID=linuxmint ID_LIKE=ubuntu
# Any other OS causes an immediate abort.
#
# NOTES:
# a) INSYNC: Commercial Google Drive/OneDrive sync client ($29.99/account,
# 15-day trial). Uses the official apt.insync.io repo for .deb packages.
# File manager integration installed for Nemo (Mint default).
#
# b) APPARMOR: On Ubuntu-based Mint it is already enabled in the kernel.
# The script installs extra profiles and verifies status on both variants.
# On LMDE 7 it installs, enables, and activates AppArmor fully.
#
# c) BLUETOOTH HOLD: Dell laptops have known breakage when the bluetooth
# package updates pull new firmware. The hold prevents this.
#
# d) AUTOMATIC UPDATES: unattended-upgrades is configured for security
# updates. Full upgrades remain manual (apt upgrade) to avoid surprise
# reboots on a laptop.
#
# ================================================================================
set -euo pipefail
# -------------------------------------------------------------------------------
# Privilege check
if [[ "${EUID}" -ne 0 ]]; then
echo "[ERROR] This script must be run as root (sudo ./setup-laptop-mintos.sh)"
exit 1
fi
# -------------------------------------------------------------------------------
# Logging
LOGFILE="/var/log/mintos-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 "MintOS setup started at $(date)"
info "Log file: ${LOGFILE}"
# -------------------------------------------------------------------------------
# OS detection and validation
#
# Abort immediately if not running on a supported Linux Mint variant.
# We check ID and ID_LIKE from /etc/os-release.
step "OS Detection"
if [[ ! -f /etc/os-release ]]; then
error "/etc/os-release not found — cannot determine OS. Aborting."
fi
source /etc/os-release
OS_ID="${ID:-unknown}"
OS_ID_LIKE="${ID_LIKE:-unknown}"
OS_PRETTY="${PRETTY_NAME:-unknown}"
OS_CODENAME="${VERSION_CODENAME:-unknown}"
info "Detected OS: ${OS_PRETTY}"
info "ID: ${OS_ID} | ID_LIKE: ${OS_ID_LIKE} | Codename: ${OS_CODENAME}"
if [[ "${OS_ID}" != "linuxmint" ]]; then
error "Unsupported OS: '${OS_ID}'. This script requires Linux Mint (linuxmint)."
error "For Bazzite, use setup-laptop-bazzite.sh instead."
fi
# Determine Mint variant for distro-specific behaviour
if echo "${OS_ID_LIKE}" | grep -qi "debian"; then
MINT_BASE="debian"
info "Mint base: Debian (LMDE)"
elif echo "${OS_ID_LIKE}" | grep -qi "ubuntu"; then
MINT_BASE="ubuntu"
info "Mint base: Ubuntu"
else
error "Linux Mint detected but base distro '${OS_ID_LIKE}' is not recognised."
error "Expected 'debian' (LMDE) or 'ubuntu'. Aborting."
fi
# -------------------------------------------------------------------------------
# Resolve the actual 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})"
# -------------------------------------------------------------------------------
# Network connectivity check
step "Network Connectivity"
info "Checking network connectivity..."
apt-get update -qq 2>/dev/null \
|| error "apt-get update failed — check DNS/network before continuing."
info "Network OK."
# -------------------------------------------------------------------------------
# Systemd housekeeping
step "Systemd Housekeeping"
info "Checking and resetting failed systemd units..."
systemctl reset-failed || true
# ===============================================================================
# SECTION 1: Package Management — Hold, Purge, and Base Install
# ===============================================================================
step "Package Management"
# ---- i386 architecture (Wine / Steam compatibility) ----
info "Adding i386 architecture..."
if ! dpkg --print-foreign-architectures | grep -q i386; then
dpkg --add-architecture i386 || warn "Failed to add i386 architecture."
apt-get update -qq || true
else
info "i386 architecture already present."
fi
# ---- Hold Bluetooth to prevent Dell BT breakage on driver updates ----
info "Holding bluetooth package (prevents Dell BT breakage on updates)..."
apt-mark hold bluetooth 2>/dev/null || warn "bluetooth package not found — skipping hold."
# ---- Purge unwanted packages ----
# Firefox: replaced by Flatpak Chrome
# inetutils-telnet: insecure, replaced by SSH
# LibreOffice (system): replaced by Flatpak edition (current version, sandboxed)
info "Purging unwanted packages (Firefox, telnet, system LibreOffice)..."
apt-get purge -y --ignore-missing \
firefox \
firefox-esr \
firefox-locale-en \
inetutils-telnet \
'libreoffice*' \
|| true
apt-get autoremove --purge -y
apt-get autoclean -y
apt-get clean -y
# ---- Install base system packages ----
info "Installing base packages..."
apt-get install -y -qq \
curl \
wget \
gnupg \
nmap \
mtr \
smartmontools \
gsmartcontrol \
ffmpeg \
rsyslog \
fwupd \
openssh-server \
openssh-client \
cups \
avahi-daemon \
unattended-upgrades \
apt-listchanges \
|| error "Base package installation failed."
# ===============================================================================
# SECTION 2: WireGuard VPN
#
# The WireGuard kernel module is included in the Linux kernel since 5.6.
# We install the userspace tools only.
# ===============================================================================
step "WireGuard VPN"
info "Installing WireGuard userspace tools..."
apt-get install -y wireguard wireguard-tools \
|| warn "WireGuard installation failed — kernel module may still be available."
info "WireGuard installed. To connect using a config file:"
info " sudo wg-quick up /etc/wireguard/wg0.conf"
info "Or use Settings > Network > VPN to add a WireGuard connection."
# ===============================================================================
# SECTION 3: UFW Firewall
#
# UFW is Linux Mint's standard firewall frontend (sits on top of iptables/nftables).
#
# Policy:
# - Default incoming: DENY (block all unsolicited inbound)
# - Default outgoing: ALLOW (all egress permitted)
# - SSH allowed from LAN (192.168.0.0/16) only
# - WireGuard egress UDP 51820 explicitly permitted outbound
# - All rules persistent across reboots
# ===============================================================================
step "UFW Firewall"
info "Installing UFW..."
apt-get install -y ufw || error "UFW installation failed."
info "Configuring UFW policy (default deny inbound, allow outbound)..."
ufw --force reset
ufw default deny incoming
ufw default allow outgoing
info "Adding SSH rule: permit from LAN (192.168.0.0/16) only..."
ufw allow from 192.168.0.0/16 to any port 22 proto tcp comment "SSH LAN only"
info "Adding WireGuard egress rule (UDP 51820 out)..."
ufw allow out 51820/udp comment "WireGuard egress"
info "Adding mDNS rule for printer discovery (LAN only)..."
ufw allow from 192.168.0.0/16 to any port 5353 proto udp comment "mDNS LAN printer discovery"
info "Enabling UFW..."
ufw --force enable
info "UFW status:"
ufw status verbose
# ===============================================================================
# SECTION 4: AppArmor
#
# LMDE 7 (Debian base): AppArmor is available but not always active by default.
# Script installs, enables, and starts it.
# Ubuntu Mint: AppArmor is active by default in the Ubuntu kernel.
# Script installs extra profiles and verifies status.
#
# Either way: apparmor-profiles-extra provides community-maintained profiles
# for common applications beyond the base set.
# ===============================================================================
step "AppArmor"
info "Installing AppArmor packages..."
apt-get install -y \
apparmor \
apparmor-utils \
apparmor-profiles \
apparmor-profiles-extra \
|| error "AppArmor installation failed."
if [[ "${MINT_BASE}" == "debian" ]]; then
info "LMDE base: enabling and starting AppArmor service..."
systemctl enable apparmor || warn "Failed to enable AppArmor."
systemctl start apparmor || warn "Failed to start AppArmor."
else
info "Ubuntu base: AppArmor is kernel-integrated — verifying service status..."
systemctl is-active --quiet apparmor \
&& info "AppArmor is active." \
|| warn "AppArmor service not active — a reboot may be required."
fi
if aa-enabled 2>/dev/null; then
info "AppArmor is enabled and running."
aa-status 2>/dev/null | head -5 || true
else
warn "AppArmor may not be fully active. A reboot may be required."
fi
# ===============================================================================
# SECTION 5: Insync (Google Drive / OneDrive client)
#
# Insync is a commercial sync client ($29.99/account, 15-day trial).
# It provides an official apt repo at apt.insync.io for Debian and Ubuntu.
#
# On Mint the correct base distro must be specified:
# LMDE 7 → debian / codename: trixie
# Mint 22 → ubuntu / codename: noble (Ubuntu 24.04 base)
# Mint 21 → ubuntu / codename: jammy (Ubuntu 22.04 base)
#
# File manager integration: Nemo is Mint's default file manager.
# insync-nemo is installed for both variants.
# ===============================================================================
step "Insync (Google Drive / OneDrive Client)"
INSYNC_GPG_DST="/etc/apt/trusted.gpg.d/insynchq.gpg"
INSYNC_LIST="/etc/apt/sources.list.d/insync.list"
INSYNC_GPG_URL="https://apt.insync.io/insynchq.gpg"
info "Adding Insync GPG key..."
if curl -fsSL "${INSYNC_GPG_URL}" | gpg --dearmor | tee "${INSYNC_GPG_DST}" > /dev/null; then
info "Insync GPG key installed to ${INSYNC_GPG_DST}"
else
warn "Failed to download Insync GPG key — Insync install may fail."
fi
# Determine repo line based on Mint base and codename
if [[ "${MINT_BASE}" == "debian" ]]; then
INSYNC_DISTRO="debian"
INSYNC_CODENAME="${OS_CODENAME}" # e.g. trixie
else
INSYNC_DISTRO="ubuntu"
# Ubuntu Mint reports its own codename; we need the Ubuntu base codename
# Mint 22.x = noble (24.04), Mint 21.x = jammy (22.04)
case "${OS_CODENAME}" in
wilma|virginia) INSYNC_CODENAME="noble" ;; # Mint 22.x
vera|vanessa|una) INSYNC_CODENAME="jammy" ;; # Mint 21.x
*) INSYNC_CODENAME="noble"
warn "Unknown Mint codename '${OS_CODENAME}' — defaulting to noble." ;;
esac
fi
info "Configuring Insync repo (${INSYNC_DISTRO} / ${INSYNC_CODENAME})..."
echo "deb [signed-by=${INSYNC_GPG_DST}] https://apt.insync.io/${INSYNC_DISTRO} ${INSYNC_CODENAME} non-free contrib" \
> "${INSYNC_LIST}"
apt-get update -qq || warn "apt-get update after Insync repo addition failed."
info "Installing Insync and Nemo integration..."
apt-get install -y insync insync-nemo \
|| warn "Insync install failed — install manually: sudo apt install insync insync-nemo"
# ===============================================================================
# SECTION 6: Flatpak Applications
#
# Flatpak is used for:
# - Google Chrome (avoids the Google APT repo entirely — easier maintenance)
# - LibreOffice (current version, replacing the purged system package)
# - Creative apps (GIMP, Inkscape, Krita, VLC, Pinta)
#
# Flatpak is pre-installed on Linux Mint. Flathub is the default remote.
# ===============================================================================
step "Flatpak Application Installation"
info "Ensuring Flathub remote is present..."
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 7: ClamAV
#
# Native host install — no container needed on a mutable Mint system.
# freshclam downloads the initial virus definitions.
# clamtk provides a GUI frontend.
# A weekly systemd timer keeps definitions current.
# ===============================================================================
step "ClamAV Antivirus"
info "Installing ClamAV and ClamTK..."
apt-get install -y clamav clamav-daemon clamtk \
|| error "ClamAV installation failed."
info "Stopping freshclam service for initial update..."
systemctl stop clamav-freshclam 2>/dev/null || true
info "Downloading initial ClamAV virus definitions (this may take a moment)..."
freshclam || warn "ClamAV database update failed — definitions may be out of date."
info "Restarting clamav-freshclam service..."
systemctl enable --now clamav-freshclam || warn "clamav-freshclam service failed to start."
info "ClamAV daemon status:"
systemctl is-active --quiet clamav-daemon \
&& info "clamav-daemon is running." \
|| warn "clamav-daemon not running — may need manual start: sudo systemctl start clamav-daemon"
# ===============================================================================
# SECTION 8: SSH Hardening
#
# Uses sshd_config.d/ drop-in so the hardened config survives package updates.
# Debian/Ubuntu sftp subsystem path: /usr/lib/openssh/sftp-server
# Service name on Debian/Ubuntu: ssh (not sshd)
# ===============================================================================
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 sshd_config and host keys to ${BACKUP_DIR}..."
cp -a "${SSHD_CFG}" "${BACKUP_DIR}/sshd_config.bak" 2>/dev/null || \
warn "sshd_config not found — openssh-server may not yet be installed."
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
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"
# Ensure main sshd_config includes the drop-in directory
if [[ -f "${SSHD_CFG}" ]] && ! grep -q "^Include ${SSHD_CUSTOM_DIR}/\*.conf" "${SSHD_CFG}"; then
if grep -q "^Include" "${SSHD_CFG}"; then
info "Include directive already present in sshd_config."
else
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 — Linux Mint
# Managed by setup-laptop-mintos.sh (${BACKUP_TS})
# Drop-in: survives openssh-server package 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 Debian/Ubuntu (differs from Fedora)
Subsystem sftp /usr/lib/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}"
info "Validating sshd_config..."
if sshd -t; then
info "sshd_config syntax OK — restarting SSH service..."
# Debian/Ubuntu service is named 'ssh', not 'sshd'
systemctl restart ssh 2>/dev/null \
|| systemctl restart sshd 2>/dev/null \
|| warn "SSH service restart failed — check service status manually."
info "SSH service restarted."
else
warn "sshd_config test failed — not restarting SSH."
warn "Backup available at: ${BACKUP_DIR}/sshd_config.bak"
fi
# ===============================================================================
# SECTION 9: UDM SE Block-Page Certificate
#
# Installs the Ubiquiti UDM SE block-page CA certificate.
# On Debian/Ubuntu the tool is update-ca-certificates.
# Certificate store: /usr/local/share/ca-certificates/
# ===============================================================================
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="/usr/local/share/ca-certificates/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-certificates; then
info "UDM SE block-page certificate installed successfully."
info "Installed to: ${CERT_DST}"
else
warn "update-ca-certificates failed — check if the file is valid PEM."
fi
rm -f "${CERT_TMP}"
fi
fi
# ===============================================================================
# SECTION 10: IPv6 Stable Address Configuration
# ===============================================================================
step "IPv6 Configuration"
info "Configuring IPv6 stable addressing (disabling temp/privacy addresses)..."
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 11: Epson WF-4830 Printer
# ===============================================================================
step "Epson WF-4830 Printer"
info "Enabling CUPS and Avahi for printer discovery..."
systemctl enable --now cups || warn "CUPS service failed to start."
systemctl enable --now avahi-daemon || warn "Avahi service failed to start."
info "Installing Epson WF-4830 via IPP Everywhere..."
if lpadmin -p "EPSON-WF4830" \
-E \
-v "ipp://192.168.1.11/ipp/print" \
-m everywhere \
-D "EPSON WF-4830 Series"; then
lpoptions -d EPSON-WF4830 || true
info "Printer installed and set as default."
else
warn "lpadmin failed — CUPS may not be fully started. Retry after reboot:"
warn " sudo lpadmin -p EPSON-WF4830 -E -v ipp://192.168.1.11/ipp/print -m everywhere -D 'EPSON WF-4830 Series'"
fi
# ===============================================================================
# SECTION 12: Firmware Updates (fwupd)
# ===============================================================================
step "Firmware Updates (fwupd)"
if ! command -v fwupdmgr &>/dev/null; then
warn "fwupdmgr not found — skipping firmware updates."
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 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..."
if ! fwupdmgr refresh --force; then
warn "LVFS metadata refresh failed — check internet 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."
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)..."
fwupdmgr update -y || warn "Firmware update failed — 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..."
fwupdmgr update -y || warn "One or more firmware updates failed."
fi
fi
fi
fi
# ===============================================================================
# SECTION 13: Automatic Updates
#
# unattended-upgrades handles security updates automatically.
# Full upgrades (apt upgrade) remain manual to avoid surprise reboots.
# ===============================================================================
step "Automatic Security Updates"
info "Configuring unattended-upgrades for automatic security updates..."
cat > /etc/apt/apt.conf.d/20auto-upgrades <<'EOF'
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
APT::Periodic::AutocleanInterval "7";
EOF
# Ensure unattended-upgrades service is running
systemctl enable --now unattended-upgrades \
|| warn "unattended-upgrades service failed to enable."
info "Automatic security updates configured."
info "Full upgrades remain manual: sudo apt upgrade"
# ===============================================================================
# SECTION 14: User-Specific Software (The programmer only)
#
# Additional development and productivity tools installed only for the
# The programmer user account. Skipped for all other users.
# ===============================================================================
step "User-Specific Software"
if [[ "${CURRENT_USER}" == "The programmer" ]]; then
info "User is The programmer — installing user-specific packages..."
info "Installing Android Studio via Flatpak..."
flatpak install -y --noninteractive flathub com.google.AndroidStudio \
|| warn "AndroidStudio Flatpak install failed."
info "Installing Pods (Podman GUI) via Flatpak..."
flatpak install -y --noninteractive flathub com.github.marhkb.Pods \
|| warn "Pods Flatpak install failed."
info "Installing Google Antigravity IDE..."
mkdir -p /etc/apt/keyrings
curl -fsSL https://us-central1-apt.pkg.dev/doc/repo-signing-key.gpg \
| gpg --dearmor -o /etc/apt/keyrings/antigravity-repo-key.gpg \
|| warn "Failed to import Antigravity GPG key."
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/antigravity-repo-key.gpg] \
https://us-central1-apt.pkg.dev/projects/antigravity-auto-updater-dev/ antigravity-debian main" \
> /etc/apt/sources.list.d/antigravity.list
apt-get update -y
apt-get install -y antigravity \
|| warn "Antigravity install failed."
else
info "User is ${CURRENT_USER} — skipping braedach-specific installs."
fi
# ===============================================================================
# SECTION 15: MOTD
# ===============================================================================
step "MOTD Configuration"
info "Configuring MOTD system information display..."
rm -f /etc/update-motd.d/*
cat > /etc/update-motd.d/80-sysinfo <<'EOF'
#!/bin/bash
echo "=== System Information ==="
echo ""
echo " 🖥️ Operating System : $(lsb_release -ds 2>/dev/null || grep -oP '(?<=PRETTY_NAME=\").*(?=\")' /etc/os-release)"
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 ""
EOF
chmod +x /etc/update-motd.d/80-sysinfo
# ===============================================================================
# SECTION 16: Final Diagnostics
# ===============================================================================
step "Final Diagnostics"
DIAG_PASS=0
DIAG_WARN=0
DIAG_FAIL=0
diag_ok() { echo -e " \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 " ── OS Detection ────────────────────────────────────────"
diag_ok "Running on: ${OS_PRETTY} (base: ${MINT_BASE})"
echo ""
echo " ── Firewall (UFW) ──────────────────────────────────────"
if systemctl is-active --quiet ufw 2>/dev/null || ufw status | grep -q "Status: active"; then
diag_ok "UFW is active"
if ufw status | grep -q "deny (incoming)"; then
diag_ok "Default incoming policy: deny"
else
diag_warn "UFW incoming policy not confirmed as deny"
fi
else
diag_fail "UFW is NOT active"
fi
echo ""
echo " ── AppArmor ────────────────────────────────────────────"
if aa-enabled 2>/dev/null; then
diag_ok "AppArmor is enabled"
PROFILES=$(aa-status 2>/dev/null | grep "profiles are loaded" | awk '{print $1}' || echo "unknown")
diag_ok "${PROFILES} AppArmor profiles loaded"
else
diag_warn "AppArmor not confirmed active — reboot may be required"
fi
echo ""
echo " ── SSH ─────────────────────────────────────────────────"
if [[ -f "${SSHD_CUSTOM_CFG}" ]]; then
diag_ok "SSH hardened drop-in: ${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_fail "ED25519 host key NOT found"
fi
if systemctl is-active --quiet ssh 2>/dev/null || systemctl is-active --quiet sshd 2>/dev/null; then
diag_ok "SSH service is running"
else
diag_warn "SSH service not active"
fi
echo ""
echo " ── ClamAV ──────────────────────────────────────────────"
if command -v clamscan &>/dev/null; then
diag_ok "clamscan binary present"
else
diag_fail "clamscan not found"
fi
if systemctl is-active --quiet clamav-freshclam 2>/dev/null; then
diag_ok "clamav-freshclam (definition updater) is running"
else
diag_warn "clamav-freshclam not running"
fi
if [[ -d /var/lib/clamav ]] && ls /var/lib/clamav/*.cvd /var/lib/clamav/*.cld 2>/dev/null | head -1 &>/dev/null; then
diag_ok "ClamAV virus definitions present"
else
diag_warn "ClamAV definitions not found — freshclam may still be downloading"
fi
echo ""
echo " ── Insync ──────────────────────────────────────────────"
if command -v insync &>/dev/null; then
diag_ok "insync binary present"
else
diag_warn "insync not found — install may have failed"
fi
if [[ -f "${INSYNC_LIST}" ]]; then
diag_ok "Insync apt repo configured: ${INSYNC_LIST}"
else
diag_warn "Insync apt repo not found"
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"
fi
echo ""
echo " ── Flatpak Apps ────────────────────────────────────────"
for app in org.gimp.GIMP org.inkscape.Inkscape org.kde.krita \
com.google.Chrome org.libreoffice.LibreOffice \
org.videolan.VLC com.github.PintaProject.Pinta; do
if flatpak info "${app}" &>/dev/null; then
diag_ok "${app}"
else
diag_warn "${app} — not installed"
fi
done
echo ""
echo " ── WireGuard ───────────────────────────────────────────"
if command -v wg &>/dev/null; then
diag_ok "wg (WireGuard tools) present"
else
diag_warn "wg not found — WireGuard tools may not be installed"
fi
echo ""
echo " ── Auto-Updates ────────────────────────────────────────"
if systemctl is-active --quiet unattended-upgrades; then
diag_ok "unattended-upgrades service is active"
else
diag_warn "unattended-upgrades not active"
fi
if [[ -f /etc/apt/apt.conf.d/20auto-upgrades ]]; then
diag_ok "auto-upgrades config present"
else
diag_warn "auto-upgrades config not found"
fi
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 " ── Bluetooth Hold ──────────────────────────────────────"
if apt-mark showhold 2>/dev/null | grep -q "bluetooth"; then
diag_ok "bluetooth package is held"
else
diag_warn "bluetooth package hold 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 RECOMMENDED
AppArmor and sysctl changes take full effect after a reboot.
Run: sudo reboot
2. INSYNC SETUP (after reboot)
Open Insync from the application launcher.
Sign in with your Google account(s) and configure sync folders.
Trial: 15 days free. Licence: \$29.99/Google account.
3. WIREGUARD VPN
Place your .conf file in /etc/wireguard/ then:
sudo wg-quick up /etc/wireguard/wg0.conf
Or use Settings > Network > VPN to add a WireGuard connection.
4. CLAMAV SCANNING
Scan home directory:
clamscan --recursive --infected ~/
Update definitions manually:
sudo freshclam
5. PRINTER (if lpadmin failed above)
sudo lpadmin -p EPSON-WF4830 -E \\
-v ipp://192.168.1.11/ipp/print \\
-m everywhere -D "EPSON WF-4830 Series"
6. MANUAL SYSTEM UPDATE
sudo apt update && sudo apt upgrade
════════════════════════════════════════════════════════════════
POSTINSTALL
info "Setup complete. Log: ${LOGFILE}"
info "SSH backup: ${BACKUP_DIR}"
echo ""
#endscriptHope this helps someone
#enoughsaid