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-06-10
# Version : 3.3.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,
# SSH inbound (TCP 22), WireGuard egress (UDP 51820),
# IPP inbound from Epson WF-4830 (TCP 631),
# IPPS inbound from Epson WF-4830 (TCP 443),
# mDNS inbound from LAN (UDP 5353 to 224.0.0.251)
# - Hardened SSH via sshd_config.d drop-in (update-resistant)
# - SSH host key regeneration (ED25519 + RSA 4096)
# - ClamAV antivirus with freshclam initial update
# - 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
# - gsmartcontrol added for disk health monitoring
#
# SOFTWARE INSTALLATION:
# - Base tools: curl, wget, nmap, mtr, smartmontools, gsmartcontrol,
# ffmpeg, rsyslog, fwupd, openssh-server, openssh-client, gnupg,
# cups, avahi-daemon, unattended-upgrades
# - WireGuard VPN (wireguard + wireguard-tools via apt)
# - Insync (Google Drive/OneDrive client) via official apt.insync.io repo
# with Nemo file manager integration
# - Flatpak apps: GIMP, Inkscape, Krita, LibreOffice, VLC, Pinta,
# Google Chrome
#
# USER-SPECIFIC (braedach only):
# - Android Studio via Flatpak
# - Pods (Podman GUI) via Flatpak
# - VS Code via Flatpak
#
# SYSTEM CONFIGURATION:
# - Unattended-upgrades for automatic security updates
# - Custom MOTD (OS, IPv4, IPv6, uptime)
#
# FIRMWARE UPDATES (fwupd):
# - LVFS metadata refresh
# - AC power check before BIOS/UEFI capsule updates
# - Non-BIOS firmware applied unconditionally
#
# DIAGNOSTICS:
# - Network connectivity (IPv4 default route, DNS resolution)
# - 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
#
# CHANGELOG:
# 3.2.0 2026-05-30 Fixed CERT_URL typo (missing C) that silently skipped
# cert install. Changed cert URL from http to https.
# Fixed inverted firmware update logic (grep -qiv -> -qi).
# Removed deprecated resolvconf from WireGuard install.
# Moved Insync GPG key to /usr/share/keyrings/ (modern apt).
# Renumbered sections sequentially (1-11). Stripped
# unnecessary inline comments. Reduced diagnostics to
# networking, DNS, and failed systemd units.
# 3.0.4 2026-05-26 UFW printer rules (IPP/IPPS/mDNS). VSCode moved to
# Flatpak. Antigravity removed. gsmartcontrol added.
# DER-to-PEM conversion added for cert install.
# 3.0.0 2026-05-17 Initial release (ported from setup-laptop-bazzite.sh).
#
# ================================================================================
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 and helpers
# ================================================================================
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 v3.2.0 started at $(date)"
info "Log file: ${LOGFILE}"
# ================================================================================
# OS detection
# ================================================================================
[[ -f /etc/os-release ]] || error "/etc/os-release not found -- cannot determine OS."
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}"
[[ "${OS_ID}" == "linuxmint" ]] \
|| error "Unsupported OS: '${OS_ID}'. This script requires Linux Mint."
if echo "${OS_ID_LIKE}" | grep -qi "debian"; then
MINT_BASE="debian"
elif echo "${OS_ID_LIKE}" | grep -qi "ubuntu"; then
MINT_BASE="ubuntu"
else
error "Mint base '${OS_ID_LIKE}' not recognised. Expected 'debian' or 'ubuntu'."
fi
info "Mint base: ${MINT_BASE}"
# ================================================================================
# User context
# ================================================================================
CURRENT_USER=$(logname 2>/dev/null || echo "${SUDO_USER:-root}")
CURRENT_HOME=$(getent passwd "${CURRENT_USER}" | cut -d: -f6)
info "User context: ${CURRENT_USER} (home: ${CURRENT_HOME})"
# ================================================================================
# Pre-flight: network and systemd
# ================================================================================
step "Pre-flight"
info "Checking network connectivity..."
apt-get update -qq 2>/dev/null || error "apt-get update failed -- check DNS/network."
info "Network OK."
info "Resetting failed systemd units..."
systemctl reset-failed || true
# ================================================================================
# SECTION 1: Package Management
# ================================================================================
step "Section 1: Package Management"
info "Holding bluetooth package..."
apt-mark hold bluetooth 2>/dev/null || warn "bluetooth package not found -- skipping hold."
info "Purging unwanted packages..."
apt-get purge -y --ignore-missing \
firefox \
firefox-esr \
firefox-locale-en \
inetutils-telnet \
'libreoffice-*' \
'libreoffice*' \
|| true
apt-get autoremove --purge -y
apt-get autoclean -y
apt-get clean -y
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
# ================================================================================
step "Section 2: WireGuard VPN"
info "Installing WireGuard..."
apt-get install -y --no-install-recommends \
wireguard \
wireguard-tools \
iptables \
iproute2 \
|| warn "WireGuard installation failed -- kernel module may still be available."
info "WireGuard installed. Configure via /etc/wireguard/wg0.conf or Settings > Network > VPN."
# ================================================================================
# SECTION 3: UFW Firewall
# ================================================================================
step "Section 3: UFW Firewall"
info "Installing UFW..."
apt-get install -y ufw || error "UFW installation failed."
info "Setting defaults: deny inbound, allow outbound..."
ufw --force reset
ufw default deny incoming
ufw default allow outgoing
ufw allow in proto tcp to any port 22 \
comment "SSH inbound"
# WireGuard client only -- no inbound port required.
# To accept inbound WireGuard connections uncomment:
# ufw allow in proto udp to any port 51820 comment "WireGuard inbound"
ufw allow out proto udp to any port 51820 \
comment "WireGuard egress"
# IPP: CUPS sends print jobs to the printer; TCP responses return on 631.
# Without this rule UFW drops the responses and CUPS marks the queue offline.
ufw allow in proto tcp from 192.168.1.11 to any port 631 \
comment "Epson WF-4830 IPP response"
# IPPS: TLS-encrypted IPP.
ufw allow in proto tcp from 192.168.1.11 to any port 443 \
comment "Epson WF-4830 IPPS response"
# mDNS: Avahi listens on the multicast address even with a static CUPS queue.
# Asymmetric blocking causes resolution timeouts that eventually drop the queue.
ufw allow in proto udp from 192.168.0.0/16 to 224.0.0.251 port 5353 \
comment "mDNS LAN"
info "Enabling UFW..."
ufw --force enable
info "UFW status:"
ufw status verbose
# ================================================================================
# SECTION 4: AppArmor
# ================================================================================
step "Section 4: 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 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."
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)
# ================================================================================
step "Section 5: Insync"
INSYNC_GPG_DST="/usr/share/keyrings/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
if [[ "${MINT_BASE}" == "debian" ]]; then
INSYNC_DISTRO="debian"
if grep -q "^DEBIAN_CODENAME=" /etc/os-release 2>/dev/null; then
INSYNC_CODENAME=$(grep "^DEBIAN_CODENAME=" /etc/os-release | cut -d= -f2)
else
INSYNC_CODENAME="${OS_CODENAME}"
fi
else
INSYNC_DISTRO="ubuntu"
case "${OS_CODENAME}" in
wilma|virginia) INSYNC_CODENAME="noble" ;;
vera|vanessa|una) INSYNC_CODENAME="jammy" ;;
*) 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
# ================================================================================
step "Section 6: Flatpak Applications"
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"
"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 7: ClamAV
# ================================================================================
step "Section 7: ClamAV"
info "Installing ClamAV..."
apt-get install -y clamav clamav-daemon clamtk || error "ClamAV installation failed."
info "Updating virus definitions..."
systemctl stop clamav-freshclam 2>/dev/null || true
freshclam || warn "ClamAV database update failed -- will retry on next scheduled run."
systemctl enable --now clamav-freshclam || warn "clamav-freshclam failed to start."
systemctl is-active --quiet clamav-daemon \
&& info "clamav-daemon is running." \
|| warn "clamav-daemon not running -- run: sudo systemctl start clamav-daemon"
# ================================================================================
# SECTION 8: SSH Hardening
# ================================================================================
step "Section 8: 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}" "${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 "Regenerating SSH host keys (ED25519 + RSA 4096)..."
rm -f /etc/ssh/ssh_host_*
ssh-keygen -t ed25519 -f "${SSHD_DIR}/ssh_host_ed25519_key" -N "" -o -a 100
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"
if [[ -f "${SSHD_CFG}" ]] && ! grep -q "^Include ${SSHD_CUSTOM_DIR}/\*.conf" "${SSHD_CFG}"; then
grep -q "^Include" "${SSHD_CFG}" \
|| sed -i "1i Include ${SSHD_CUSTOM_DIR}/*.conf" "${SSHD_CFG}"
fi
info "Writing hardened SSH drop-in to ${SSHD_CUSTOM_CFG}..."
cat > "${SSHD_CUSTOM_CFG}" << EOF
# Hardened SSH drop-in -- managed by setup-laptop-mintos.sh (${BACKUP_TS})
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 sftp /usr/lib/openssh/sftp-server
# Password auth permitted from LAN, 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 "Config OK -- restarting SSH..."
systemctl restart ssh 2>/dev/null \
|| systemctl restart sshd 2>/dev/null \
|| warn "SSH service restart failed."
else
warn "sshd config test failed -- not restarting. Backup: ${BACKUP_DIR}/sshd_config.bak"
fi
# ================================================================================
# SECTION 9: UDM SE Block-Page Certificate
# ================================================================================
step "Section 9: UDM SE 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="/usr/local/share/ca-certificates/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"
# Ensure unzip is available
if ! command -v unzip >/dev/null 2>&1; then
info "unzip not present -- installing..."
if ! apt-get install -y unzip >/dev/null 2>&1; then
warn "Failed to install unzip -- skipping cert install."
fi
fi
if command -v unzip >/dev/null 2>&1; then
info "Downloading UDM SE certificate archive..."
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}"
if ! unzip -o -q "${CERT_ZIP}" -d "${CERT_EXTRACT_DIR}"; 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}"
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 "File is not valid PEM or DER -- skipping cert 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}"
update-ca-certificates \
&& info "UDM SE certificate installed to ${CERT_DST}" \
|| warn "update-ca-certificates failed."
fi
fi
fi
fi
fi
# Tidy up temporary files
rm -rf "${CERT_EXTRACT_DIR}"
rm -f "${CERT_ZIP}" "${CERT_TMP}" "${CERT_TMP}.pem"
# ================================================================================
# SECTION 10: Firmware Updates (fwupd)
# ================================================================================
step "Section 10: Firmware Updates"
if ! command -v fwupdmgr &>/dev/null; then
warn "fwupdmgr not found -- skipping firmware updates."
else
if ! systemctl is-active --quiet fwupd; then
timeout 15 systemctl start fwupd 2>/dev/null \
|| { warn "fwupd failed to start -- skipping firmware updates."; fwupdmgr_ok=0; }
fwupdmgr_ok=1
else
fwupdmgr_ok=1
fi
if [[ "${fwupdmgr_ok:-0}" -eq 1 ]]; then
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
[[ "${AC_ONLINE}" -eq 1 ]] \
&& info "AC power confirmed -- BIOS/UEFI updates eligible." \
|| warn "AC power not detected -- BIOS updates will be blocked by fwupd."
info "Refreshing LVFS metadata..."
timeout 60 fwupdmgr refresh --force 2>&1 \
|| warn "LVFS metadata refresh failed -- check internet connectivity."
info "Checking for firmware updates..."
UPDATE_OUTPUT=$(timeout 60 fwupdmgr get-updates 2>&1 || true)
if echo "${UPDATE_OUTPUT}" | grep -qi "no upgrades\|nothing to do\|no.*update"; then
info "Firmware is up to date."
elif [[ -z "${UPDATE_OUTPUT}" ]]; then
warn "fwupdmgr get-updates returned no output."
else
info "Updates available:"
echo "${UPDATE_OUTPUT}"
if echo "${UPDATE_OUTPUT}" | grep -qi "system firmware\|uefi\|bios"; then
if [[ "${AC_ONLINE}" -eq 1 ]]; then
info "Applying BIOS update (will apply on next reboot)..."
timeout 300 fwupdmgr update -y 2>&1 || warn "BIOS update failed."
info "IMPORTANT: Reboot required to apply BIOS/UEFI update. Do not interrupt power."
else
warn "Skipping BIOS update -- AC power required. Run: sudo fwupdmgr update"
fi
else
info "Applying non-BIOS firmware updates..."
timeout 300 fwupdmgr update -y 2>&1 || warn "One or more firmware updates failed."
fi
fi
fi
fi
# ================================================================================
# SECTION 11: Automatic Security Updates
# ================================================================================
step "Section 11: Automatic Security Updates"
info "Configuring unattended-upgrades..."
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
systemctl enable --now unattended-upgrades \
|| warn "unattended-upgrades service failed to enable."
info "Automatic security updates configured. Full upgrades remain manual: sudo apt upgrade"
# ================================================================================
# USER-SPECIFIC: braedach only
# ================================================================================
step "User-Specific Software"
if [[ "${CURRENT_USER}" == "braedach" ]]; then
info "User is braedach -- installing user-specific packages..."
flatpak install -y --noninteractive flathub com.google.AndroidStudio \
|| warn "AndroidStudio Flatpak install failed."
flatpak install -y --noninteractive flathub com.github.marhkb.Pods \
|| warn "Pods Flatpak install failed."
flatpak install -y --noninteractive flathub com.visualstudio.code \
|| warn "VS Code Flatpak install failed."
apt install -y git \
|| warn "git installation failed."
info "User-specific software installation complete."
info "braedach: Android Studio, Pods, VS Code (Flatpak), git (apt)"
warn "Little Snitch and Microsoft VS Code are available but manual installation is recommended at this time."
else
info "User is ${CURRENT_USER} -- skipping braedach-specific installs."
fi
# ================================================================================
# MOTD
# ================================================================================
step "MOTD Configuration"
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
# ================================================================================
# DIAGNOSTICS
# ================================================================================
step "Diagnostics"
echo ""
echo "============================================================"
echo " DIAGNOSTIC CHECK"
echo "============================================================"
echo ""
echo " -- Network ------------------------------------------"
DEFAULT_IF=$(ip -4 route show default 2>/dev/null | awk '{print $5; exit}')
DEFAULT_GW=$(ip -4 route show default 2>/dev/null | awk '{print $3; exit}')
if [[ -n "${DEFAULT_IF}" ]]; then
info "Default interface : ${DEFAULT_IF}"
info "Default gateway : ${DEFAULT_GW}"
else
warn "No IPv4 default route found."
fi
IPV6_GW=$(ip -6 route show default 2>/dev/null | awk '{print $3; exit}')
if [[ -n "${IPV6_GW}" ]]; then
info "IPv6 default gateway: ${IPV6_GW}"
else
info "No IPv6 default route (not required)."
fi
echo ""
echo " -- DNS ----------------------------------------------"
if command -v resolvectl &>/dev/null; then
resolvectl status 2>/dev/null | grep -E "DNS Servers|Current DNS" | head -5 || true
fi
DNS_TEST=$(dig +short +timeout=5 braedach.com 2>/dev/null | head -1)
if [[ -n "${DNS_TEST}" ]]; then
info "DNS resolution OK (braedach.com -> ${DNS_TEST})"
else
warn "DNS resolution failed for braedach.com -- check /etc/resolv.conf"
fi
echo ""
echo " -- UFW ----------------------------------------------"
if ufw status | grep -q "Status: active"; then
info "UFW active"
ufw status | grep -E "631|443|5353|22|51820"
else
warn "UFW not active"
fi
echo ""
echo " -- Failed Systemd Units -----------------------------"
FAILED_UNITS=$(systemctl --failed --no-legend --plain 2>/dev/null | awk '{print $1}')
if [[ -z "${FAILED_UNITS}" ]]; then
info "No failed systemd units."
else
while IFS= read -r unit; do
warn "Failed unit: ${unit}"
done <<< "${FAILED_UNITS}"
fi
echo ""
echo "============================================================"
# ================================================================================
# POST-SCRIPT INSTRUCTIONS
# ================================================================================
cat << 'POSTINSTALL'
============================================================
POST-SCRIPT ACTIONS
============================================================
1. REBOOT RECOMMENDED
AppArmor 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/OneDrive account(s) and configure sync.
3. WIREGUARD VPN
Place your .conf file in /etc/wireguard/ then:
sudo wg-quick up /etc/wireguard/wg0.conf
4. CLAMAV
Scan home: clamscan --recursive --infected ~/
Update: sudo freshclam
5. MANUAL SYSTEM UPDATE
sudo apt update && sudo apt upgrade
============================================================
POSTINSTALL
info "Setup complete. Version 3.2.0"
info "Log: ${LOGFILE}"
info "SSH backup: ${BACKUP_DIR}"
echo ""
#endscriptHope this helps someone
#enoughsaid