Switch Over Instructions
Server Setup Script - copy and paste into terminal to execute the script
Script with options
#!/bin/bash
# --- Single, Robust Command to Mount, Copy, Execute, and Unmount ---
#
# This command is designed to be safely copied and pasted into any server terminal.
# It handles all the necessary steps to run the main setup script from your fileserver.
# It will prompt you to select which mode to run the main script in.
#
# Password for the mount command is included. Ensure this is run in a secure environment.
# --- Configuration ---
MOUNT_POINT="/mnt/fileserver"
SERVER_PATH="//172.16.21.16/fileserver2"
SCRIPT_SOURCE_PATH="${MOUNT_POINT}/General/IT FILES/script3.sh"
SCRIPT_DEST_PATH="/tmp/script3.sh"
MOUNT_USER="Cipher.m21"
MOUNT_PASS=")\1y;634'NJ%i+"
# --- Logic ---
# Ensure the mount point is unmounted on script exit (success or failure)
trap "echo 'Unmounting fileserver...'; sudo umount '${MOUNT_POINT}' &>/dev/null || true" EXIT
# Check if already mounted. If not, create directory and mount.
if ! grep -qs "${MOUNT_POINT}" /proc/mounts; then
echo "Mounting fileserver..."
sudo mkdir -p "${MOUNT_POINT}"
sudo mount -t cifs "${SERVER_PATH}" "${MOUNT_POINT}" -o username="${MOUNT_USER}",password="${MOUNT_PASS}"
fi
echo "Copying script (overwriting if exists)..."
sudo cp "${SCRIPT_SOURCE_PATH}" "${SCRIPT_DEST_PATH}"
echo "Making script executable..."
sudo chmod +x "${SCRIPT_DEST_PATH}"
# --- Interactive Mode Selection ---
echo ""
echo "Please choose which setup to run:"
echo " 1) Full Setup (default)"
echo " 2) Development Stack Only (--dev)"
echo " 3) Security Hardening Only (--security)"
echo " 4) Shell & UX Setup Only (--shell)"
echo " 5) System Updates Only (--updates)"
read -rp "Enter your choice [1-5]: " run_choice
EXECUTION_FLAG="--full" # Default value
case "$run_choice" in
2) EXECUTION_FLAG="--dev" ;;
3) EXECUTION_FLAG="--security" ;;
4) EXECUTION_FLAG="--shell" ;;
5) EXECUTION_FLAG="--updates" ;;
1) EXECUTION_FLAG="--full" ;;
*) # Default to full for any other input
echo "Invalid choice or no choice entered. Defaulting to Full Setup."
EXECUTION_FLAG="--full"
;;
esac
echo "Executing the main setup script with flag: ${EXECUTION_FLAG}..."
sudo "${SCRIPT_DEST_PATH}" "${EXECUTION_FLAG}"
# The trap will handle the unmount automatically.
echo "Script execution finished. Unmounting is handled automatically."
Simple script
sudo mkdir -p /mnt/fileserver && \
sudo mount -t cifs //172.16.21.16/fileserver2 /mnt/fileserver -o username="Cipher.m21",password=")\1y;634'NJ%i+" && \
cp /mnt/fileserver/General/IT\ FILES//script3.sh /tmp/ && \
sudo chmod +x /tmp/script3.sh && \
sudo /tmp/script3.sh --full && \
sudo umount /mnt/fileserver
Server Setup Script
#!/usr/bin/env bash
##########################
#
####
#######################
#
# Comprehensive Domain Join & Configuration Script
#
# Version: 6.5 (Phoenix - The Final Cut)
# Last Modified: 2025-07-31
#
# Features
# [CRITICAL FIX] Zsh theme switching and plugin enabling is now fully robust.
# [CRITICAL FIX] Reboot prompt no longer hangs.
# [FIX] Enhanced Nano with persistent status bar and comprehensive syntax highlighting from a dedicated repository.
# [FIX] Vi/Vim syntax highlighting is now guaranteed by installing a full vim package.
# [ENH] Added a pre-configured "Powerline" theme for Starship, showing date, time, and hostname.
# [ENH] Added Powerlevel10k as a Zsh theme option with automatic installation and configuration wizard setup.
# [CRITICAL FIX] Proxy variables are now exported immediately, fixing all subsequent download failures.
# [CRITICAL FIX] Ctrl+C cancellation is now robust and reliably skips optional sections without exiting the script.
#
###############################
#####################################
#---
# CONFIGURATION
# Adjust these variables for your environment!
#---
DOMAIN_FQDN="m21.gov.local"
DOMAIN_NETBIOS="M21" # NetBIOS name of your domain
DC_DNS_IP="172.16.21.161" # Your Domain Controller's IP (for DNS & domain ops)
NTP_SERVER="172.16.121.9" # Your dedicated NTP server IP
FILE_SERVER_IP="172.16.21.16" # Your File Server's IP
FILE_SERVER_HOSTNAME="mydns-0ic16" # Short hostname for the file server
FILE_SERVER_FQDN="${FILE_SERVER_HOSTNAME}.${DOMAIN_FQDN}" # FQDN for the file server
HTTP_PROXY_URL="http://172.40.4.14:8080/" # Set to "" if no proxy
NO_PROXY_INITIAL="127.0.0.1,localhost,localhost.localdomain" # Base no_proxy entries
NO_PROXY_CUSTOM="172.30.0.0/20,172.26.21.0/24,172.16.121.0/24,10.21.0.0/21" # Your custom NO PROXY CIDRS
INSECURE_REGISTRIES='"172.16.121.119:5000", "docker-repo.mydns.gov.tt"' # Comma-separated, quoted Docker insecure registries
TIMEZONE="America/Port_of_Spain" # Your desired timezone
AD_SUDO_GROUP_RAW_NAME="ICT Staff SG" # AD Group for Sudoers (Raw name, spaces are okay here. Script will escape.)
#---
# INITIALIZE SCRIPT
#---
# Color Definitions
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
PURPLE='\033[0;35m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
# Global flag for reboot
REBOOT_REQUIRED_FLAG=false
# Exit on error for most commands, but we will handle some manually.
set -o pipefail
LOG_FILE="/var/log/setup-domain-$(date +%Y-%m-%d_%H-%M-%S).log"
# Log to file, but keep stderr on the console to see errors immediately.
exec > >(tee -a "$LOG_FILE") 2>&1
echo -e "${GREEN}=== Script started at $(date --iso-8601=seconds) by $(whoami) ===${NC}"
echo -e "${GREEN}=== Logging to ${LOG_FILE} ===${NC}"
#---
# PRELIMINARY CHECKS & GLOBAL VARIABLES
#---
[[ $EUID -ne 0 ]] && { echo -e "${RED}ERROR: This script must be run as root or with sudo.${NC}" >&2; exit 1; }
PKG_MANAGER=""
if command -v dnf &>/dev/null; then PKG_MANAGER="dnf";
elif command -v yum &>/dev/null; then PKG_MANAGER="yum";
elif command -v apt-get &>/dev/null; then PKG_MANAGER="apt";
else echo -e "${RED}ERROR: Neither DNF, YUM, nor APT package manager found. Exiting.${NC}" >&2; exit 1; fi
source /etc/os-release
OS_ID_LOWER=$(echo "$ID" | tr '[:upper:]' '[:lower:]')
OS_VER="${VERSION_ID%%.*}"
HOSTNAME_VAR=$(hostname -f)
#---
# SCRIPT FUNCTIONS
#
log_step() { echo -e "\n${GREEN}--- [STEP $1 on ${PURPLE}${HOSTNAME_VAR}${NC}] $2 ---${NC}"; }
#
# SECTION 0: CREDENTIAL GATHERING
#
gather_credentials() {
log_step "0/X" "Gathering Credentials (Not Logged)"
local creds_ok=false
while ! $creds_ok; do
( # Start subshell for cancellable read
trap 'echo -e "\n${RED}Credential entry cancelled. Exiting script.${NC}"; exit 1;' INT
echo -e "\n${CYAN}--- Domain Credentials (will not be logged) ---${NC}"
read -rp "$(echo -e "${CYAN}Enter the SERVICE part of the hostname (e.g., mydns-it-c12-1): ${NC}")" SERVICE_NAME_PART < /dev/tty
if [[ ! "$SERVICE_NAME_PART" =~ ^[a-zA-Z0-9-]+$ ]]; then
echo -e "${RED}ERROR: Invalid service name part.${NC}" >&2; exit 1;
fi
read -rp "$(echo -e "${CYAN}Enter your AD username SUFFIX (the part after 'ent_'): ${NC}")" AD_USER_SUFFIX < /dev/tty
if [ -z "$AD_USER_SUFFIX" ]; then echo -e "${RED}ERROR: AD username suffix cannot be empty.${NC}" >&2; exit 1; fi
read -rsp "$(echo -e "${CYAN}Enter AD password for 'ent_${AD_USER_SUFFIX}': ${NC}")" AD_PASSWORD_TEMP < /dev/tty
echo
if [ -z "$AD_PASSWORD_TEMP" ]; then echo -e "${RED}ERROR: AD Password cannot be empty.${NC}" >&2; exit 1; fi
# Export variables from subshell to the main script via a temp file
echo "export SERVICE_NAME_PART='${SERVICE_NAME_PART}'" > /tmp/creds.sh
echo "export AD_USER_SUFFIX='${AD_USER_SUFFIX}'" >> /tmp/creds.sh
echo "export AD_PASSWORD='${AD_PASSWORD_TEMP}'" >> /tmp/creds.sh
)
if [ $? -ne 0 ]; then exit 1; fi
source /tmp/creds.sh
rm /tmp/creds.sh
creds_ok=true
done
TARGET_HOSTNAME_FQDN="${SERVICE_NAME_PART}.${DOMAIN_FQDN}"
TARGET_HOSTNAME_FQDN_LC=$(echo "$TARGET_HOSTNAME_FQDN" | tr '[:upper:]' '[:lower:]')
AD_USER_FOR_JOIN="ent_${AD_USER_SUFFIX}"
echo -e "${BLUE}INFO:${NC} Using full AD username: ${PURPLE}${AD_USER_FOR_JOIN}${NC}"
NO_PROXY_FULL="${NO_PROXY_INITIAL},${DOMAIN_FQDN,,},.${DOMAIN_FQDN,,},${DC_DNS_IP},${NTP_SERVER},${FILE_SERVER_IP}"
if [[ -n "$NO_PROXY_CUSTOM" ]]; then NO_PROXY_FULL="${NO_PROXY_FULL},${NO_PROXY_CUSTOM}"; fi
NO_PROXY_FULL=$(echo "$NO_PROXY_FULL" | tr ',' '\n' | sort -u | tr '\n' ',' | sed 's/,$//')
}
#
#SECTION 1: CORE SYSTEM & NETWORK FUNCTIONS
#
change_hostname() {
log_step "1/X" "Setting Hostname"
echo -e "${BLUE}INFO:${NC} Setting hostname to ${PURPLE}${TARGET_HOSTNAME_FQDN_LC}${NC}"
hostnamectl set-hostname "$TARGET_HOSTNAME_FQDN_LC"
echo -e "${GREEN}SUCCESS:${NC} Hostname set to: ${PURPLE}$(hostnamectl hostname)${NC}"
}
configure_proxy() {
log_step "2/X" "Configuring System-Wide Proxy"
if [ -z "$HTTP_PROXY_URL" ]; then
echo -e "${BLUE}INFO:${NC} HTTP_PROXY_URL is not set. Skipping proxy configuration."
return
fi
echo -e "${BLUE}INFO:${NC} Applying proxy for current script session..."
export http_proxy="${HTTP_PROXY_URL}"
export https_proxy="${HTTP_PROXY_URL}"
export ftp_proxy="${HTTP_PROXY_URL}"
export no_proxy="${NO_PROXY_FULL}"
export HTTP_PROXY="${HTTP_PROXY_URL}"
export HTTPS_PROXY="${HTTP_PROXY_URL}"
export FTP_PROXY="${HTTP_PROXY_URL}"
export NO_PROXY="${NO_PROXY_FULL}"
echo -e "${BLUE}INFO:${NC} Configuring proxy for future interactive shells (/etc/profile.d/proxy.sh)..."
cat > /etc/profile.d/proxy.sh <<EOF
export http_proxy="${HTTP_PROXY_URL}"
export https_proxy="${HTTP_PROXY_URL}"
export ftp_proxy="${HTTP_PROXY_URL}"
export no_proxy="${NO_PROXY_FULL}"
export HTTP_PROXY="\${http_proxy}"
export HTTPS_PROXY="\${https_proxy}"
export FTP_PROXY="\${ftp_proxy}"
export NO_PROXY="\${no_proxy}"
EOF
chmod +x /etc/profile.d/proxy.sh
echo -e "${BLUE}INFO:${NC} Configuring system-wide environment file (/etc/environment)..."
sed -i '/^http_proxy=/d;/^https_proxy=/d;/^ftp_proxy=/d;/^no_proxy=/d' /etc/environment
sed -i '/^HTTP_PROXY=/d;/^HTTPS_PROXY=/d;/^FTP_PROXY=/d;/^NO_PROXY=/d' /etc/environment
echo "http_proxy=\"${HTTP_PROXY_URL}\"" >> /etc/environment
echo "https_proxy=\"${HTTP_PROXY_URL}\"" >> /etc/environment
echo "ftp_proxy=\"${HTTP_PROXY_URL}\"" >> /etc/environment
echo "no_proxy=\"${NO_PROXY_FULL}\"" >> /etc/environment
echo "HTTP_PROXY=\"${HTTP_PROXY_URL}\"" >> /etc/environment
echo "HTTPS_PROXY=\"${HTTP_PROXY_URL}\"" >> /etc/environment
echo "FTP_PROXY=\"${HTTP_PROXY_URL}\"" >> /etc/environment
echo "NO_PROXY=\"${NO_PROXY_FULL}\"" >> /etc/environment
echo -e "${BLUE}INFO:${NC} Configuring package manager proxy..."
case "$PKG_MANAGER" in
dnf|yum)
if ! grep -q "proxy=" /etc/dnf/dnf.conf; then
echo "proxy=${HTTP_PROXY_URL}" >> /etc/dnf/dnf.conf
fi
;;
apt)
cat > /etc/apt/apt.conf.d/80proxy <<EOF
Acquire::http::proxy "${HTTP_PROXY_URL}";
Acquire::https::proxy "${HTTP_PROXY_URL}";
Acquire::ftp::proxy "${HTTP_PROXY_URL}";
EOF
;;
esac
echo -e "${GREEN}SUCCESS:${NC} System-wide proxy configured."
}
configure_dns_and_hosts() {
log_step "3/X" "Configuring DNS and NetworkManager"
echo -e "${BLUE}INFO:${NC} Configuring /etc/hosts file..."
sed -i "/${DOMAIN_FQDN}/d" /etc/hosts
cat >> /etc/hosts <<EOF
# AD Domain Configuration
${DC_DNS_IP} ${DOMAIN_FQDN}
${FILE_SERVER_IP} ${FILE_SERVER_FQDN} ${FILE_SERVER_HOSTNAME}
EOF
echo -e "${BLUE}INFO:${NC} Configuring DNS via NetworkManager..."
local conn
conn=$(nmcli -t -f NAME,DEVICE connection show --active | grep -v "lo$" | head -n1 | cut -d':' -f1)
if [ -z "$conn" ]; then
echo -e "${RED}ERROR:${NC} Could not find an active network connection to configure." >&2
return 1
fi
echo -e "${BLUE}INFO:${NC} Modifying connection: ${PURPLE}${conn}${NC}"
nmcli connection modify "$conn" ipv4.dns "$DC_DNS_IP"
nmcli connection modify "$conn" ipv4.ignore-auto-dns yes
nmcli connection up "$conn"
echo -e "${GREEN}SUCCESS:${NC} DNS configured to ${DC_DNS_IP} and /etc/hosts updated."
}
check_connectivity() {
log_step "4/X" "Checking Network Connectivity"
local has_error=0
echo -e "${BLUE}INFO:${NC} Pinging Domain Controller (${DC_DNS_IP})..."
if ! ping -c 3 "$DC_DNS_IP"; then
echo -e "${RED}ERROR:${NC} Domain Controller is not reachable." >&2; has_error=1
fi
echo -e "${BLUE}INFO:${NC} Checking DNS resolution for ${DOMAIN_FQDN}..."
if ! getent hosts "$DOMAIN_FQDN"; then
echo -e "${RED}ERROR:${NC} Could not resolve domain FQDN." >&2; has_error=1
fi
if [ -n "$HTTP_PROXY_URL" ]; then
echo -e "${BLUE}INFO:${NC} Testing connection to google.com via proxy..."
if ! curl -s --head --connect-timeout 5 http://www.google.com | head -n 1 | grep "200 OK" > /dev/null; then
echo -e "${YELLOW}WARNING:${NC} Could not connect to the internet via proxy. External repos may fail."
fi
fi
if [ $has_error -eq 0 ]; then
echo -e "${GREEN}SUCCESS:${NC} All connectivity checks passed."
else
echo -e "${RED}ERROR:${NC} One or more connectivity checks failed. Please review the logs." >&2; exit 1
fi
}
install_packages() {
log_step "5/X" "Installing Core & Utility Packages"
local common_pkgs="nano curl wget htop btop net-tools git zip unzip tar tmux chrony open-vm-tools traceroute ncdu policycoreutils-python-utils logrotate tree bash-completion bat jq fontconfig util-linux-user"
local pkgs_to_install
if [[ "$PKG_MANAGER" == "dnf" || "$PKG_MANAGER" == "yum" ]]; then
common_pkgs+=" bind-utils dnf-utils vim-enhanced"
echo -e "${BLUE}INFO:${NC} Ensuring core DNF plugins are installed..."
$PKG_MANAGER -y install dnf-plugins-core
echo -e "${BLUE}INFO:${NC} Enabling CRB/PowerTools repository..."
if [[ "$OS_VER" -ge 9 ]]; then
dnf config-manager --set-enabled crb -y
else
dnf config-manager --set-enabled powertools -y || dnf config-manager --set-enabled PowerTools -y
fi
echo -e "${BLUE}INFO:${NC} Installing EPEL repository..."
if ! $PKG_MANAGER -y install epel-release; then
echo -e "${RED}ERROR: Failed to install EPEL repository. Cannot continue.${NC}" >&2; exit 1;
fi
local dnf_base_pkgs="realmd sssd oddjob oddjob-mkhomedir adcli samba-common-tools authselect"
pkgs_to_install="${dnf_base_pkgs} ${common_pkgs}"
elif [[ "$PKG_MANAGER" == "apt" ]]; then
common_pkgs+=" dnsutils debian-goodies vim"
[[ "$OS_ID_LOWER" == "ubuntu" ]] && common_pkgs=${common_pkgs/bat/batcat}
local apt_base_pkgs="realmd sssd sssd-tools libnss-sss libpam-sss adcli samba-common-bin oddjob oddjob-mkhomedir packagekit apt-transport-https ca-certificates software-properties-common gnupg lsb-release"
pkgs_to_install="${apt_base_pkgs} ${common_pkgs}"
echo -e "${BLUE}INFO:${NC} Updating package lists for APT..."
apt-get update -qq
fi
echo -e "${BLUE}INFO:${NC} Installing main packages..."
if ! $PKG_MANAGER -y install ${pkgs_to_install}; then
echo -e "${RED}ERROR: Package installation failed. This is often due to network, proxy, or repository issues.${NC}" >&2; exit 1;
fi
if command -v batcat &>/dev/null && ! command -v bat &>/dev/null; then
ln -sf /usr/bin/batcat /usr/local/bin/bat
fi
echo -e "${GREEN}SUCCESS:${NC} Core packages installed."
}
configure_time() {
log_step "6/X" "Configuring System Time (NTP & Timezone)"
echo -e "${BLUE}INFO:${NC} Setting timezone to ${TIMEZONE}..."
timedatectl set-timezone "$TIMEZONE"
echo -e "${BLUE}INFO:${NC} Configuring chrony to use NTP server ${NTP_SERVER}..."
sed -i '/^pool/d' /etc/chrony.conf
sed -i '/^server/d' /etc/chrony.conf
echo "server ${NTP_SERVER} iburst" >> /etc/chrony.conf
echo -e "${BLUE}INFO:${NC} Restarting and enabling chronyd service..."
systemctl restart chronyd
systemctl enable chronyd
timedatectl set-ntp true
echo -e "${GREEN}SUCCESS:${NC} System time configured."
}
#
#SECTION 2: DOMAIN & AUTHENTICATION FUNCTIONS
#
join_ad_domain() {
log_step "7/X" "Joining Active Directory Domain"
( # Start subshell for cancellation
trap 'echo -e "\n${YELLOW}Operation cancelled. Skipping Domain Join...${NC}"; exit 0;' INT
if realm list | grep -q "$DOMAIN_FQDN"; then
read -rp "$(echo -e "${YELLOW}WARNING:${NC} Server is already joined to ${DOMAIN_FQDN}. Action? ([S]kip, [R]e-join): ${NC}")" choice < /dev/tty
case "$(echo "$choice" | tr '[:upper:]' '[:lower:]')" in
r|re-join)
echo -e "${BLUE}INFO:${NC} Leaving the domain first..."
if ! realm leave; then
echo -e "${RED}ERROR:${NC} Failed to leave the domain. Please check logs. Aborting re-join." >&2
exit 1
fi
echo -e "${GREEN}SUCCESS:${NC} Left the domain."
;;
*)
echo -e "${BLUE}INFO:${NC} Skipping domain join step."
exit 0
;;
esac
fi
echo -e "${BLUE}INFO:${NC} Attempting to join domain ${DOMAIN_FQDN} as user ${AD_USER_FOR_JOIN}..."
if ! echo -n "$AD_PASSWORD" | realm join --user="$AD_USER_FOR_JOIN" "$DOMAIN_FQDN"; then
echo -e "${RED}ERROR:${NC} Failed to join the Active Directory domain." >&2
echo -e "${YELLOW}Check username, password, and connectivity to the DC.${NC}" >&2
exit 1
fi
echo -e "${GREEN}SUCCESS:${NC} Successfully joined the domain."
)
}
configure_sssd_mkhomedir() {
log_step "8/X" "Configuring SSSD & Home Directories"
local sssd_conf="/etc/sssd/sssd.conf"
if [ ! -f "$sssd_conf" ]; then
echo -e "${RED}ERROR:${NC} SSSD configuration file not found at ${sssd_conf}" >&2; return 1;
fi
echo -e "${BLUE}INFO:${NC} Modifying ${sssd_conf}..."
sed -i '/^use_fully_qualified_names/d' "$sssd_conf"
sed -i "/\[domain\/${DOMAIN_FQDN,,}\]/a use_fully_qualified_names = False" "$sssd_conf"
sed -i '/^fallback_homedir/d' "$sssd_conf"
sed -i "/\[domain\/${DOMAIN_FQDN,,}\]/a fallback_homedir = /home/%u" "$sssd_conf"
echo -e "${BLUE}INFO:${NC} Enabling automatic home directory creation..."
authselect enable-feature with-mkhomedir
systemctl restart sssd oddjobd
systemctl enable oddjobd
echo -e "${GREEN}SUCCESS:${NC} SSSD and home directory creation configured."
}
configure_sudoers() {
log_step "9/X" "Configuring Sudoers for AD Group"
local escaped_group_name
escaped_group_name=$(echo "$AD_SUDO_GROUP_RAW_NAME" | sed 's/ /\\ /g')
local sudoer_file="/etc/sudoers.d/90-ad-admins"
echo -e "${BLUE}INFO:${NC} Granting sudo rights to AD group '${AD_SUDO_GROUP_RAW_NAME}'..."
echo "\"%${escaped_group_name}\" ALL=(ALL) ALL" > "$sudoer_file"
chmod 440 "$sudoer_file"
echo -e "${GREEN}SUCCESS:${NC} Sudoers configured. Rule added to ${sudoer_file}."
}
#
#SECTION 3: SECURITY HARDENING FUNCTIONS
#
optimize_sshd() {
log_step "10/X" "Optimizing SSH Daemon for Faster Logins"
local sshd_config="/etc/ssh/sshd_config"
echo -e "${BLUE}INFO:${NC} Setting 'UseDNS no' in ${sshd_config}..."
if grep -q "^#\?UseDNS" "$sshd_config"; then
sed -i 's/^#\?UseDNS.*/UseDNS no/' "$sshd_config"
else
echo "UseDNS no" >> "$sshd_config"
fi
systemctl restart sshd
echo -e "${GREEN}SUCCESS:${NC} SSHD optimized for faster logins."
}
install_fail2ban() {
log_step "11/X" "Installing Fail2ban (Optional, Ctrl+C to skip)"
( # Start subshell for cancellation
trap 'echo -e "\n${YELLOW}Operation cancelled. Skipping Fail2ban...${NC}"; exit 0;' INT
if [ -f /etc/fail2ban/jail.local ]; then
read -rp "$(echo -e "${YELLOW}WARNING:${NC} Fail2ban configuration already exists. Action? ([S]kip, [O]verwrite): ${NC}")" choice < /dev/tty
if [[ "$(echo "$choice" | tr '[:upper:]' '[:lower:]')" != "o" ]]; then
echo -e "${BLUE}INFO:${NC} Skipping Fail2ban setup."; exit 0;
fi
else
read -rp "$(echo -e "${CYAN}Install and configure Fail2ban for SSH protection? [y/N]: ${NC}")" choice < /dev/tty
if [[ "$(echo "$choice" | tr '[:upper:]' '[:lower:]')" != "y" ]]; then
echo -e "${BLUE}INFO:${NC} Skipping Fail2ban installation."; exit 0;
fi
fi
echo -e "${BLUE}INFO:${NC} Installing Fail2ban..."
$PKG_MANAGER -y install fail2ban
echo -e "${BLUE}INFO:${NC} Creating local jail configuration for SSHD..."
cat > /etc/fail2ban/jail.local <<EOF
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
bantime = 3600
EOF
systemctl enable --now fail2ban
echo -e "${GREEN}SUCCESS:${NC} Fail2ban installed and enabled for SSHD."
)
}
#
#SECTION 4: SHELL & USER EXPERIENCE FUNCTIONS
#
install_nano_syntax() {
(
trap 'echo -e "\n${YELLOW}Operation cancelled. Skipping extra Nano syntax...${NC}"; exit 0;' INT
echo -e "${BLUE}INFO:${NC} Installing enhanced syntax highlighting for Nano..."
local nano_syntax_dir="/tmp/nanorc"
if git clone https://github.com/scopatz/nanorc.git "$nano_syntax_dir"; then
sudo cp -r ${nano_syntax_dir}/*.nanorc /usr/share/nano/
rm -rf "$nano_syntax_dir"
echo -e "${GREEN}SUCCESS:${NC} Enhanced Nano syntax installed."
else
echo -e "${RED}ERROR:${NC} Failed to download enhanced Nano syntax files."
fi
)
}
configure_nano() {
log_step "12/X" "Configuring Nano Editor"
echo -e "${BLUE}INFO:${NC} Applying system-wide Nano configuration..."
cat > /etc/nanorc <<EOF
## Nano Editor Default Configuration
set linenumbers
set softwrap
set tabsize 4
set casesensitive
set constantshow # Always show line/col info
## Include all standard syntax definitions
include "/usr/share/nano/*.nanorc"
EOF
install_nano_syntax
echo -e "${GREEN}SUCCESS:${NC} Nano configured with defaults and syntax highlighting."
}
configure_vim() {
log_step "12.1/X" "Configuring Vim/Vi"
echo -e "${BLUE}INFO:${NC} Applying system-wide Vim configuration..."
cat > /etc/vimrc <<EOF
" System-wide .vimrc file
syntax on
set background=dark
set number
set ruler
set showcmd
set incsearch
set wildmenu
EOF
echo -e "${GREEN}SUCCESS:${NC} Vim configured with syntax highlighting."
}
enhance_bash() {
log_step "13/X" "Enhancing Bash Experience (Optional, Ctrl+C to skip)"
( # Start subshell for cancellation
trap 'echo -e "\n${YELLOW}Operation cancelled. Skipping Bash enhancements...${NC}"; exit 0;' INT
read -rp "$(echo -e "${CYAN}Enhance the Bash shell with a better prompt and aliases? [y/N]: ${NC}")" choice < /dev/tty
if [[ "$(echo "$choice" | tr '[:upper:]' '[:lower:]')" != "y" ]]; then
echo -e "${BLUE}INFO:${NC} Skipping Bash enhancements."; exit 0;
fi
echo -e "${BLUE}INFO:${NC} Creating /etc/profile.d/enhanced_bash.sh..."
cat > /etc/profile.d/enhanced_bash.sh <<'EOF'
# Custom Bash prompt
PS1='\[\e[32m\]\u@\h \[\e[33m\]\w\[\e[0m\]\n\$ '
# Useful Aliases
alias ls='ls --color=auto'
alias ll='ls -alF'
alias la='ls -A'
alias l='ls -CF'
alias grep='grep --color=auto'
alias ..='cd ..'
EOF
echo -e "${GREEN}SUCCESS:${NC} Bash enhancements will be applied on next login."
)
}
setup_tmux() {
log_step "14/X" "Setting up Automated Tmux Environment (Optional, Ctrl+C to skip)"
( # Start subshell for cancellation
trap 'echo -e "\n${YELLOW}Operation cancelled. Skipping Tmux setup...${NC}"; exit 0;' INT
read -rp "$(echo -e "${CYAN}Set up a default Tmux configuration? [y/N]: ${NC}")" choice < /dev/tty
if [[ "$(echo "$choice" | tr '[:upper:]' '[:lower:]')" != "y" ]]; then
echo -e "${BLUE}INFO:${NC} Skipping Tmux setup."; exit 0;
fi
echo -e "${BLUE}INFO:${NC} Creating system-wide /etc/tmux.conf..."
cat > /etc/tmux.conf <<'EOF'
# Set prefix to Ctrl-a
set -g prefix C-a
unbind C-b
bind C-a send-prefix
# Enable mouse mode
set -g mouse on
# Improve status bar
set -g status-bg black
set -g status-fg white
set -g status-left '#[fg=green]#H'
set -g status-right '#[fg=yellow]%Y-%m-%d %H:%M'
EOF
echo -e "${GREEN}SUCCESS:${NC} Default Tmux configuration created."
)
}
setup_motd() {
log_step "15/X" "Setting Up Dynamic MOTD (Optional, Ctrl+C to skip)"
( # Start subshell for cancellation
trap 'echo -e "\n${YELLOW}Operation cancelled. Skipping MOTD setup...${NC}"; exit 0;' INT
if [ -f /etc/profile.d/99-custom-motd.sh ]; then
read -rp "$(echo -e "${YELLOW}WARNING:${NC} Custom MOTD script already exists. Action? ([S]kip, [O]verwrite): ${NC}")" choice < /dev/tty
if [[ "$(echo "$choice" | tr '[:upper:]' '[:lower:]')" != "o" ]]; then
echo -e "${BLUE}INFO:${NC} Skipping MOTD setup."; exit 0;
fi
else
read -rp "$(echo -e "${CYAN}Setup a dynamic MOTD (Message of the Day)? [y/N]: ${NC}")" choice < /dev/tty
if [[ "$(echo "$choice" | tr '[:upper:]' '[:lower:]')" != "y" ]]; then
echo -e "${BLUE}INFO:${NC} Skipping MOTD setup."; exit 0;
fi
fi
echo -e "${BLUE}INFO:${NC} Creating a dynamic message of the day..."
chmod -x /etc/update-motd.d/* &>/dev/null || true
sed -i '/session\s\+optional\s\+pam_motd.so/s/^/#/' /etc/pam.d/sshd 2>/dev/null || true
cat > /etc/profile.d/99-custom-motd.sh <<'EOF'
# This script runs for interactive shells to display a dynamic MOTD.
if [[ $- == *i* ]] && [[ "${SHLVL:-1}" -le 1 ]]; then
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[0;33m'; BLUE='\033[0;34m';
PURPLE='\033[0;35m'; CYAN='\033[0;36m'; NC='\033[0m';
echo -e "\nWelcome to ${GREEN}$(hostname -f)${NC}"
echo -e "System time is: ${CYAN}$(date --iso-8601=seconds)${NC}\n"
# Display OS Info
if [ -f /etc/os-release ]; then
OS_INFO=$(grep PRETTY_NAME /etc/os-release | cut -d'"' -f2)
echo -e "${PURPLE}System Information${NC}"
echo -e " OS: ${YELLOW}${OS_INFO}${NC}"
echo -e " Uptime: $(uptime -p | sed 's/up //')\n"
fi
# Display Network Information
echo -e "${PURPLE}Network Information${NC}"
ip -4 addr | grep -oP '(?<=inet\s)\d+(\.\d+){3}/\d+\s.*\s\K\w+$' | while read -r dev;
do
ip_addr=$(ip -4 addr show dev "$dev" | grep -oP '(?<=inet\s)\d+(\.\d+){3}')
if [[ -n "$ip_addr" ]]; then echo -e " Interface ${GREEN}$dev${NC}: ${CYAN}$ip_addr${NC}"; fi
done || echo -e " ${RED}No active IPv4 interfaces found.${NC}"
echo
# Display System Usage
echo -e "${PURPLE}System Usage${NC}"
df -h / | awk '$NR==2 {print " Disk (/): " $2 " total, " $3 " used (" $5 " full), " $4 " free"}'
free -h | awk '/^Mem:/ {print " Memory: " $2 " total, " $3 " used, " $7 " available"}'
echo -e " CPU Load: $(uptime | awk -F'load average:' '{ print $2}' | sed 's/ //g')\n"
# Display Installed Software Versions
echo -e "${PURPLE}Installed Software${NC}"
declare -A progs=(
["Docker"]="docker --version" ["Podman"]="podman --version"
["Nginx"]="nginx -v" ["Apache"]="httpd -v" ["PHP"]="php -v"
["Node"]="node -v" ["NPM"]="npm -v" ["Go"]="go version"
["Java"]="java -version" ["MySQL"]="mysql --version" ["PostgreSQL"]="psql --version"
)
output=""
for name in "${!progs[@]}"; do
if command -v ${progs[$name]%% *} &>/dev/null; then
version=$(${progs[$name]} 2>&1 | grep -oP '(\d+\.){1,}\d+' | head -n1)
[[ -n "$version" ]] && output+=" ${GREEN}${name}${NC}:${CYAN}${version}${NC}"
fi
done
echo -e "${output:- None detected}\n"
fi
EOF
chmod +x /etc/profile.d/99-custom-motd.sh
echo -e "${GREEN}SUCCESS:${NC} Dynamic MOTD script created."
)
}
install_nerd_fonts() {
log_step "16.1/X" "Installing Nerd Fonts (Sub-step)"
( # Start subshell for cancellation
trap 'echo -e "\n${YELLOW}Operation cancelled. Skipping Nerd Fonts...${NC}"; exit 124;' INT
echo -e "${BLUE}INFO:${NC} Checking for Nerd Fonts..."
local font_dir="/usr/local/share/fonts/FiraCodeNerdFont"
if [ -d "$font_dir" ]; then
echo -e "${BLUE}INFO:${NC} Nerd Font directory already exists. Skipping download."
exit 0
fi
read -rp "$(echo -e "${CYAN}Install FiraCode Nerd Font for Zsh themes? [y/N]: ${NC}")" choice < /dev/tty
if [[ "$(echo "$choice" | tr '[:upper:]' '[:lower:]')" != "y" ]]; then
echo -e "${BLUE}INFO:${NC} Skipping Nerd Font installation."; exit 0;
fi
mkdir -p "$font_dir"
local tmp_zip="/tmp/FiraCode.zip"
local retries=3; local count=0; local success=false
until [ $count -ge $retries ]
do
echo -e "${BLUE}INFO:${NC} Attempting to download FiraCode Nerd Font (attempt $((count+1))/${retries})..."
ping -c 1 google.com &>/dev/null || true
sleep 1
curl --connect-timeout 20 -L "https://github.com/ryanoasis/nerd-fonts/releases/download/v3.2.1/FiraCode.zip" -o "$tmp_zip"
if [ $? -eq 0 ]; then success=true; break; fi
count=$((count+1))
echo -e "${YELLOW}WARNING:${NC} Download failed. Retrying in 5 seconds..."
sleep 5
done
if ! $success; then
echo -e "${RED}ERROR:${NC} Failed to download Nerd Fonts after $retries attempts." >&2; rm -f "$tmp_zip"; exit 1;
fi
unzip -o "$tmp_zip" -d "$font_dir"; rm -f "$tmp_zip"
echo -e "${BLUE}INFO:${NC} Rebuilding font cache..."; fc-cache -fv &>/dev/null
echo -e "${GREEN}SUCCESS:${NC} Nerd Fonts installed."
)
return $?
}
configure_starship() {
local user=$1
local home_dir
home_dir=$(eval echo ~$user)
local config_dir="${home_dir}/.config"
local starship_config="${config_dir}/starship.toml"
echo -e "${BLUE}INFO:${NC} Creating Starship 'Powerline' config for ${user}..."
sudo -u "$user" mkdir -p "$config_dir"
sudo -u "$user" tee "$starship_config" > /dev/null <<'EOF'
# Starship "Powerline" configuration
# Shows: [USER@HOST] [DATE TIME] [DIRECTORY] [GIT] [CMD_DURATION]
# >>>
# A minimal left prompt
format = """$username$hostname$time$directory$git_branch$cmd_duration$character"""
# Move the directory to the second line
# format = """$username$hostname$time$directory$git_branch$cmd_duration$fill$character"""
[username]
style_user = "yellow bold"
style_root = "red bold"
format = "[$user]($style_user)@"
show_always = true
[hostname]
style = "green bold"
format = "[$hostname]($style) "
ssh_only = false
disabled = false
[time]
disabled = false
format = '[\[$time\]]($style) '
style = "blue bold"
time_format = "%Y-%m-%d %H:%M:%S"
[directory]
style = "cyan bold"
format = "[$path]($style) "
truncation_length = 4
[git_branch]
style = "bold purple"
format = "[$branch]($style) "
[cmd_duration]
min_time = 500
style = "bold italic yellow"
format = "[$duration]($style) "
[character]
success_symbol = "[>](bold green)"
error_symbol = "[x](bold red)"
EOF
}
install_zsh_omz() {
log_step "16/X" "Installing Zsh & Oh My Zsh (Optional, Ctrl+C to skip)"
( # Start subshell for cancellation
trap 'echo -e "\n${YELLOW}Operation cancelled. Skipping Zsh setup...${NC}"; exit 124;' INT
local choice
if command -v zsh &>/dev/null; then
read -rp "$(echo -e "${CYAN}Zsh is already installed. Action? ([S]kip, [R]econfigure): ${NC}")" choice < /dev/tty
if [[ "$(echo "$choice" | tr '[:upper:]' '[:lower:]')" == "s" ]]; then
echo -e "${BLUE}INFO:${NC} Skipping Zsh setup."; exit 0;
fi
else
read -rp "$(echo -e "${CYAN}Install Zsh and Oh My Zsh? [y/N]: ${NC}")" choice < /dev/tty
if [[ "$(echo "$choice" | tr '[:upper:]' '[:lower:]')" != "y" ]]; then
echo -e "${BLUE}INFO:${NC} Skipping Zsh installation."; exit 0;
fi
fi
$PKG_MANAGER -y install zsh git
install_nerd_fonts
if [ $? -ne 0 ]; then
echo -e "${YELLOW}WARNING:${NC} Nerd font installation failed or was skipped. Zsh themes may not render correctly."
fi
local users_to_configure=()
users_to_configure+=("root")
if [[ -n "${SUDO_USER:-}" ]] && [[ "$SUDO_USER" != "root" ]]; then
users_to_configure+=("$SUDO_USER")
fi
local prompt_choice
read -rp "$(echo -e "${CYAN}Which Zsh prompt? ([1] Oh My Zsh (rkj-repos), [2] Starship, [3] Powerlevel10k): ${NC}")" prompt_choice < /dev/tty
for user in "${users_to_configure[@]}"; do
echo -e "${BLUE}INFO:${NC} Configuring Zsh for user ${PURPLE}${user}${NC}..."
local home_dir; home_dir=$(eval echo ~$user)
local zsh_dir="${home_dir}/.oh-my-zsh"
if [ ! -d "$zsh_dir" ]; then
local installer_sh="/tmp/omz_install.sh"
local retries=3; local count=0; local success=false
echo -e "${BLUE}INFO:${NC} Downloading Oh My Zsh installer..."
until [ $count -ge $retries ]; do
curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh -o "$installer_sh"
if [ $? -eq 0 ]; then success=true; break; fi
count=$((count+1)); echo -e "${YELLOW}WARNING:${NC} Download failed. Retrying..."; sleep 3
done
if $success; then
echo -e "${BLUE}INFO:${NC} Running Oh My Zsh installer for ${user}..."
sudo -u "$user" sh "$installer_sh" --unattended
rm "$installer_sh"
else
echo -e "${RED}ERROR:${NC} Failed to download Oh My Zsh installer for ${user}." >&2; continue
fi
fi
local custom_plugins_dir="${zsh_dir}/custom/plugins"
if [ ! -d "${custom_plugins_dir}/zsh-autosuggestions" ]; then
sudo -u "$user" git clone https://github.com/zsh-users/zsh-autosuggestions "$custom_plugins_dir/zsh-autosuggestions"
fi
if [ ! -d "${custom_plugins_dir}/zsh-syntax-highlighting" ]; then
sudo -u "$user" git clone https://github.com/zsh-users/zsh-syntax-highlighting.git "$custom_plugins_dir/zsh-syntax-highlighting"
fi
local zshrc_file="${home_dir}/.zshrc"
# FIX: Robustly set plugins
sed -i 's/^plugins=(.*)$/plugins=(git docker npm nvm zsh-autosuggestions zsh-syntax-highlighting)/' "$zshrc_file"
# FIX: Clean up old theme settings before applying a new one
sed -i '/^# Init Starship Prompt/d' "$zshrc_file"
sed -i '/eval "$(starship init zsh)"/d' "$zshrc_file"
case "$prompt_choice" in
2) # Starship
if ! command -v starship &>/dev/null; then
local installer_sh="/tmp/starship_install.sh"
echo -e "${BLUE}INFO:${NC} Downloading Starship installer..."
if curl -sS https://starship.rs/install.sh -o "$installer_sh"; then
sh "$installer_sh" -y
rm "$installer_sh"
else
echo -e "${RED}ERROR:${NC} Failed to download starship installer." >&2
fi
fi
echo -e '\n# Init Starship Prompt\neval "$(starship init zsh)"' >> "$zshrc_file"
configure_starship "$user"
;;
3) # Powerlevel10k
local p10k_dir="${zsh_dir}/custom/themes/powerlevel10k"
if [ ! -d "$p10k_dir" ]; then
echo -e "${BLUE}INFO:${NC} Cloning Powerlevel10k theme..."
sudo -u "$user" git clone --depth=1 https://github.com/romkatv/powerlevel10k.git "$p10k_dir"
fi
echo -e "${BLUE}INFO:${NC} Setting Powerlevel10k theme in .zshrc..."
sed -i 's|^ZSH_THEME=.*|ZSH_THEME="powerlevel10k/powerlevel10k"|' "$zshrc_file"
if ! grep -q 'POWERLEVEL9K_DISABLE_CONFIGURATION_WIZARD=true' "$zshrc_file"; then
echo -e '\n# To customize prompt, run `p10k configure` or edit ~/.p10k.zsh.\n[[ ! -f ~/.p10k.zsh ]] && p10k configure' >> "$zshrc_file"
fi
;;
*) # Default to rkj-repos
echo -e "${BLUE}INFO:${NC} Setting ZSH_THEME to rkj-repos..."
sed -i 's|^ZSH_THEME=.*|ZSH_THEME="rkj-repos"|' "$zshrc_file"
;;
esac
if ! grep -q 'setopt EXTENDED_HISTORY' "$zshrc_file"; then
echo -e '\n# Custom settings by setup script\nsetopt EXTENDED_HISTORY\nHIST_STAMPS="yyyy-mm-dd"\n' >> "$zshrc_file"
fi
if ! grep -q "alias ls='ls --color=auto'" "$zshrc_file"; then
echo -e '\n# Color Aliases\nalias ls="ls --color=auto"\nalias grep="grep --color=auto"' >> "$zshrc_file"
fi
local current_shell; current_shell=$(getent passwd "$user" | cut -d: -f7)
if [[ "$current_shell" != "$(which zsh)" ]]; then
echo -e "${BLUE}INFO:${NC} Changing shell for user ${user} to Zsh..."
chsh -s "$(which zsh)" "$user"
echo -e "${GREEN}SUCCESS:${NC} Shell changed."
else
echo -e "${BLUE}INFO:${NC} Shell for user ${user} is already zsh."
fi
done
)
}
#
#SECTION 5: APPLICATION STACK FUNCTIONS
#
install_dev_stack() {
log_step "18/X" "Installing Development Stack (Optional, Ctrl+C to skip)"
( # Start subshell for cancellation
trap 'echo -e "\n${YELLOW}Operation cancelled. Skipping Dev Stack...${NC}"; exit 0;' INT
read -rp "$(echo -e "${CYAN}Install a development stack (Nginx, PHP, Node.js)? [y/N]: ${NC}")" choice < /dev/tty
if [[ "$(echo "$choice" | tr '[:upper:]' '[:lower:]')" != "y" ]]; then
echo -e "${BLUE}INFO:${NC} Skipping dev stack installation."; exit 0;
fi
echo -e "${BLUE}INFO:${NC} Installing Nginx..."
$PKG_MANAGER -y install nginx
systemctl enable nginx; systemctl start nginx
echo -e "${BLUE}INFO:${NC} Installing Node.js (LTS)..."
curl -fsSL https://rpm.nodesource.com/setup_lts.x | bash -
$PKG_MANAGER -y install nodejs
echo -e "${BLUE}INFO:${NC} Installing PHP..."
if [[ "$PKG_MANAGER" == "dnf" || "$PKG_MANAGER" == "yum" ]]; then
$PKG_MANAGER -y install http://rpms.remirepo.net/enterprise/remi-release-8.rpm
$PKG_MANAGER -y module reset php; $PKG_MANAGER -y module install php:remi-8.1
$PKG_MANAGER -y install php-cli php-fpm php-mysqlnd php-json php-gd php-mbstring
else # APT
add-apt-repository ppa:ondrej/php -y; $PKG_MANAGER update
$PKG_MANAGER -y install php8.1-cli php8.1-fpm php8.1-mysql php8.1-json php8.1-gd php8.1-mbstring
fi
echo -e "${GREEN}SUCCESS:${NC} Development stack installed."
)
}
install_cockpit() {
log_step "19/X" "Installing Cockpit Web Console (Optional, Ctrl+C to skip)"
( # Start subshell for cancellation
trap 'echo -e "\n${YELLOW}Operation cancelled. Skipping Cockpit...${NC}"; exit 0;' INT
read -rp "$(echo -e "${CYAN}Install the Cockpit web administration console? [y/N]: ${NC}")" choice < /dev/tty
if [[ "$(echo "$choice" | tr '[:upper:]' '[:lower:]')" != "y" ]]; then
echo -e "${BLUE}INFO:${NC} Skipping Cockpit installation."; exit 0;
fi
echo -e "${BLUE}INFO:${NC} Installing Cockpit..."
$PKG_MANAGER -y install cockpit cockpit-storaged cockpit-networkmanager
echo -e "${BLUE}INFO:${NC} Starting and enabling Cockpit socket..."
systemctl enable --now cockpit.socket
echo -e "${GREEN}SUCCESS:${NC} Cockpit is installed. Access it at https://$(hostname -f):9090"
)
}
install_container_runtime() {
log_step "20/X" "Installing Container Runtime (Optional, Ctrl+C to skip)"
( # Start subshell for cancellation
trap 'echo -e "\n${YELLOW}Operation cancelled. Skipping Container Runtime...${NC}"; exit 0;' INT
read -rp "$(echo -e "${CYAN}Install a container runtime? ([D]ocker, [P]odman, [N]one): ${NC}")" choice < /dev/tty
case "$(echo "$choice" | tr '[:upper:]' '[:lower:]')" in
d|docker)
echo -e "${BLUE}INFO:${NC} Installing Docker..."
if [[ "$PKG_MANAGER" == "dnf" || "$PKG_MANAGER" == "yum" ]]; then
$PKG_MANAGER config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
$PKG_MANAGER -y install docker-ce docker-ce-cli containerd.io
else # APT
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" > /etc/apt/sources.list.d/docker.list
$PKG_MANAGER update
$PKG_MANAGER -y install docker-ce docker-ce-cli containerd.io
fi
systemctl enable --now docker
if [[ -n "${SUDO_USER:-}" ]]; then
echo -e "${BLUE}INFO:${NC} Adding user ${SUDO_USER} to the docker group..."
usermod -aG docker "$SUDO_USER"
fi
echo -e "${GREEN}SUCCESS:${NC} Docker installed."
;;
p|podman)
echo -e "${BLUE}INFO:${NC} Installing Podman..."
$PKG_MANAGER -y install podman
echo -e "${GREEN}SUCCESS:${NC} Podman installed."
;;
*)
echo -e "${BLUE}INFO:${NC} Skipping container runtime installation.";;
esac
)
}
#
#SECTION 6: UTILITY & FINALIZATION FUNCTIONS
#
setup_logrotate() {
log_step "21/X" "Setting up Logrotate (Optional, Ctrl+C to skip)"
( # Start subshell for cancellation
trap 'echo -e "\n${YELLOW}Operation cancelled. Skipping Logrotate setup...${NC}"; exit 0;' INT
read -rp "$(echo -e "${CYAN}Configure log rotation for this script's log files? [y/N]: ${NC}")" choice < /dev/tty
if [[ "$(echo "$choice" | tr '[:upper:]' '[:lower:]')" != "y" ]]; then
echo -e "${BLUE}INFO:${NC} Skipping logrotate setup."; exit 0;
fi
echo -e "${BLUE}INFO:${NC} Creating /etc/logrotate.d/setup-domain..."
cat > /etc/logrotate.d/setup-domain <<EOF
/var/log/setup-domain-*.log {
monthly
rotate 4
compress
delaycompress
missingok
notifempty
create 640 root root
}
EOF
echo -e "${GREEN}SUCCESS:${NC} Logrotate configured."
)
}
final_summary() {
log_step "22/X" "Final Setup Summary"
echo -e "${GREEN}================== SUMMARY ==================${NC}"
echo -e " Hostname: ${PURPLE}$(hostname -f)${NC}"
echo -e " IP Address: ${CYAN}$(hostname -I | awk '{print $1}')${NC}"
echo -e " Timezone: ${CYAN}${TIMEZONE}${NC}"
echo -e " Domain Membership: ${PURPLE}${DOMAIN_FQDN}${NC}"
realm list | grep "configured: yes" &>/dev/null
if [ $? -eq 0 ]; then
echo -e " Domain Join Status: ${GREEN}Success${NC}"
echo -e " Login with AD users as: ${CYAN}username${NC}"
echo -e " Sudo enabled for group: ${PURPLE}${AD_SUDO_GROUP_RAW_NAME}${NC}"
else
echo -e " Domain Join Status: ${RED}Failed or Not Performed${NC}"
fi
echo -e " Log File: ${YELLOW}${LOG_FILE}${NC}"
echo -e "${GREEN}=============================================${NC}"
}
system_updates_interactive() {
log_step "23/X" "System Updates (Optional, Ctrl+C to skip)"
( # Start subshell for cancellation
trap 'echo -e "\n${YELLOW}Operation cancelled. Skipping System Updates...${NC}"; exit 0;' INT
read -rp "$(echo -e "${CYAN}Check for and apply all available system updates? [y/N]: ${NC}")" choice < /dev/tty
if [[ "$(echo "$choice" | tr '[:upper:]' '[:lower:]')" != "y" ]]; then
echo -e "${BLUE}INFO:${NC} Skipping system updates."; exit 0;
fi
echo -e "${BLUE}INFO:${NC} Checking for updates..."
$PKG_MANAGER -y update
echo -e "${GREEN}SUCCESS:${NC} System is up-to-date."
echo -e "${BLUE}INFO:${NC} Checking if a reboot is required..."
if [[ "$PKG_MANAGER" == "dnf" || "$PKG_MANAGER" == "yum" ]]; then
if needs-restarting -r &>/dev/null; then
# Using an external file to communicate back to the main script
echo "true" > /tmp/reboot_required.flag
fi
elif [[ "$PKG_MANAGER" == "apt" ]]; then
if [ -f /var/run/reboot-required ]; then
echo "true" > /tmp/reboot_required.flag
fi
fi
if [ -f /tmp/reboot_required.flag ]; then
echo -e "\n${YELLOW}####################################################################"
echo -e "# WARNING: System updates have been installed that require a reboot."
echo -e "####################################################################${NC}"
else
echo -e "${GREEN}INFO:${NC} No reboot is required at this time."
fi
)
}
cleanup() {
unset AD_PASSWORD
rm -f /tmp/reboot_required.flag
echo -e "${BLUE}INFO:${NC} Sensitive variables cleared from memory."
}
usage() {
echo -e "${CYAN}Usage: $0 [OPTION]${NC}"
echo " --full Run the complete end-to-end installation and configuration."
echo " --dev Install the development stack (PHP, Node, Nginx, etc.)."
echo " --security Apply security hardening (Fail2ban, SSH optimization)."
echo " --shell Configure user experience (Bash, Zsh, Tmux, MOTD)."
echo " --updates Check for and apply system updates."
echo " --help Display this help message."
echo
echo -e "${YELLOW}If no option is provided, the script will run the full installation interactively.${NC}"
}
#---
# MODULAR EXECUTION RUNNERS
#
run_full_install() {
echo -e "${PURPLE}Starting Full System Setup...${NC}"
gather_credentials
# Core System & Network (Not cancellable - these are critical)
change_hostname
configure_proxy
configure_dns_and_hosts
check_connectivity
install_packages
configure_time
# Domain & Auth (Not cancellable - these are critical)
join_ad_domain
configure_sssd_mkhomedir
configure_sudoers
# Security (Optional sections are now cancellable)
optimize_sshd
install_fail2ban
#Shell & UX
configure_nano
configure_vim
enhance_bash
setup_tmux
setup_motd
install_zsh_omz
# App Stacks
install_dev_stack
install_cockpit
install_container_runtime
# Utilities & Finalization
setup_logrotate
system_updates_interactive
final_summary
}
run_dev_stack() {
echo -e "${PURPLE}Starting Development Stack Setup...${NC}"
install_packages
install_dev_stack
final_summary
}
run_security_hardening() {
echo -e "${PURPLE}Starting Security Hardening...${NC}"
install_packages
optimize_sshd
install_fail2ban
final_summary
}
run_shell_ux() {
echo -e "${PURPLE}Starting Shell & UX Setup...${NC}"
install_packages
configure_nano
configure_vim
enhance_bash
setup_tmux
setup_motd
install_zsh_omz
final_summary
}
#---
# MAIN EXECUTION FLOW
#---
main() {
trap cleanup EXIT
if [ $# -eq 0 ]; then
run_full_install
else
case "$1" in
--full) run_full_install ;;
--dev) run_dev_stack ;;
--security) run_security_hardening ;;
--shell) run_shell_ux ;;
--updates) system_updates_interactive ;;
--help) usage ;;
*)
echo -e "${RED}Error: Invalid option '$1'${NC}" >&2
usage
exit 1
;;
esac
fi
echo -e "\n${GREEN}========= Script finished at $(date --iso-8601=seconds) =========${NC}"
if [ -f /tmp/reboot_required.flag ]; then
REBOOT_REQUIRED_FLAG=true
fi
if $REBOOT_REQUIRED_FLAG; then
read -rp "$(echo -e "${YELLOW}A reboot is required to apply updates. Reboot now? [y/N]: ${NC}")" choice < /dev/tty
if [[ "$(echo "$choice" | tr '[:upper:]' '[:lower:]')" == "y" ]]; then
echo -e "${RED}Rebooting now...${NC}"
reboot
else
echo -e "${YELLOW}Please reboot the server manually to apply all changes.${NC}"
fi
else
echo -e "${GREEN}Script complete. No reboot required.${NC}"
fi
}
# Only run main if the script is executed directly
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi
master_script.sh - v43
#!/usr/bin/env bash
#
# MASTER INFRASTRUCTURE SETUP v43
# Enhancements: Modularized Docker, Web, Database, and Terminal Tools
#
###############################################################################
# 1. CONFIGURATION
###############################################################################
DOMAIN_FQDN="m21.gov.local"
DOMAIN_ALT="m21.gov.tt"
DOMAIN_SHORT="M21"
DC_DNS_IP="172.16.21.161"
NTP_SERVER="172.16.121.9"
TARGET_TIMEZONE="America/Port_of_Spain"
# File Server Info
FILE_SERVER_IP="172.16.21.16"
FILE_SERVER_NAME="fileserver2"
# Proxy
PROXY_URL="http://172.40.4.14:8080"
NO_PROXY_LIST="127.0.0.1,localhost,localhost.localdomain,${DOMAIN_FQDN},${DOMAIN_ALT},.${DOMAIN_FQDN},.${DOMAIN_ALT},${DC_DNS_IP},172.30.0.0/20,172.26.21.0/24,10.21.0.0/21,172.16.121.0/24"
# Docker Settings
INSECURE_REGISTRIES='"172.16.121.119:5000", "docker-repo.msya.gov.tt"'
# AD Access Control
AD_SUDO_GROUP="ICT Staff SG M21"
ALLOWED_LOGIN_GROUP="ICT Staff SG M21"
# Share Credentials
SHARE_PATH="//172.16.21.16/fileserver2"
SHARE_USER="Cipher.m21"
SHARE_PASS=")\ly; 634'NJ%i+"
CERT_SOURCE_PATH="/General/IT FILES/prx/Gortt_certificate_V4.cer"
TARGET_CERT_NAME="GORTT_Root_Exp2029"
# Failsafe User
LOCAL_USER="pcsupport"
LOCAL_PASS="ProIT321*"
# LVM Settings
HOME_TARGET_SIZE="8G"
# Versions
PHP_VERSION="8.3"
JAVA_VERSION="21"
MARIADB_VERSION="10.11"
###############################################################################
# 2. HELPER FUNCTIONS
###############################################################################
set -e
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[0;33m'; BLUE='\033[0;34m'; NC='\033[0m'
log() { echo -e "${BLUE}[$(date +'%H:%M:%S')] [INFO]${NC} $1"; }
step() { echo -e "\n${YELLOW}[$(date +'%H:%M:%S')] >>> $1${NC}"; }
success() { echo -e "${GREEN}[$(date +'%H:%M:%S')] [OK]${NC} $1"; }
error() { echo -e "${RED}[$(date +'%H:%M:%S')] [ERROR]${NC} $1"; }
run_retry() {
local n=1; local max=3; local delay=2
while true; do
"$@" && return 0
if [[ $n -lt $max ]]; then
((n++)); log "Command failed. Retrying ($n/$max)..."; sleep $delay
else
return 1
fi
done
}
###############################################################################
# 3. PRE-FLIGHT CHECKS
###############################################################################
detect_and_fix_os() {
source /etc/os-release
OS_ID=$(echo "$ID" | tr '[:upper:]' '[:lower:]')
VERSION_MAJOR=$(echo "$VERSION_ID" | cut -d. -f1)
if timeout 10s systemctl is-active --quiet packagekit.service 2>/dev/null; then
timeout 15s systemctl stop packagekit.service || true
fi
if [[ "$OS_ID" == "centos" && "$VERSION_MAJOR" == "7" ]]; then
PKG="yum"
if grep -q "linux/rhel" /etc/yum.repos.d/docker-ce.repo 2>/dev/null; then rm -f /etc/yum.repos.d/docker-ce.repo; fi
if [ ! -f /etc/yum.repos.d/CentOS-Base.repo.backup ]; then
cp /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.backup 2>/dev/null || true
run_retry curl -o /etc/yum.repos.d/CentOS-Base.repo https://el7.repo.almalinux.org/centos/CentOS-Base.repo
fi
elif [[ "$OS_ID" =~ (rhel|centos|almalinux|rocky) ]]; then PKG="dnf"
elif [[ "$OS_ID" =~ (ubuntu|debian|zorin) ]]; then PKG="apt-get"
else error "Unsupported OS: $OS_ID"; exit 1; fi
}
###############################################################################
# 4. CORE MODULES
###############################################################################
mod_proxy() {
step "Configuring System Proxy"
cat > /etc/profile.d/proxy.sh <<EOF
export http_proxy="${PROXY_URL}"
export https_proxy="${PROXY_URL}"
export ftp_proxy="${PROXY_URL}"
export no_proxy="${NO_PROXY_LIST}"
export HTTP_PROXY="${PROXY_URL}"
export HTTPS_PROXY="${PROXY_URL}"
export FTP_PROXY="${PROXY_URL}"
export NO_PROXY="${NO_PROXY_LIST}"
EOF
source /etc/profile.d/proxy.sh
if [[ "$PKG" == "dnf" || "$PKG" == "yum" ]]; then
CONF_FILE="/etc/dnf/dnf.conf"
[[ ! -f "$CONF_FILE" ]] && CONF_FILE="/etc/yum.conf"
grep -q "proxy=" "$CONF_FILE" 2>/dev/null || echo "proxy=${PROXY_URL}" >> "$CONF_FILE"
if ! grep -q "minrate" "$CONF_FILE" 2>/dev/null; then
echo -e "timeout=60\nretries=10\nminrate=1" >> "$CONF_FILE"
fi
else
echo -e "Acquire::http::Proxy \"${PROXY_URL}\";\nAcquire::https::Proxy \"${PROXY_URL}\";" > /etc/apt/apt.conf.d/80proxy
fi
}
mod_clock_fix() {
step "Synchronizing System Clock"
timedatectl set-timezone "$TARGET_TIMEZONE" || true
timedatectl set-ntp true || true
if systemctl list-unit-files | grep -q systemd-timesyncd; then
timeout 30s systemctl restart systemd-timesyncd || true
fi
}
mod_certs() {
step "Installing Certificates"
MNT="/mnt/share_certs_tmp"
mkdir -p "$MNT"
if ! command -v mount.cifs &>/dev/null; then
if [[ "$PKG" == "apt-get" ]]; then run_retry apt-get update -qq >/dev/null 2>&1 || true; run_retry apt-get install -y cifs-utils
else run_retry $PKG install -y cifs-utils; fi
fi
if mountpoint -q "$MNT"; then umount -l "$MNT"; fi
if timeout 30s mount -t cifs "$SHARE_PATH" "$MNT" -o username="$SHARE_USER",password="$SHARE_PASS",vers=3.0; then
SOURCE_FULL="$MNT$CERT_SOURCE_PATH"
TEMP_PEM="/tmp/${TARGET_CERT_NAME}_staging.pem"
if [[ -f "$SOURCE_FULL" ]]; then
if ! openssl x509 -inform der -in "$SOURCE_FULL" -out "$TEMP_PEM" 2>/dev/null; then cp "$SOURCE_FULL" "$TEMP_PEM"; fi
if [[ "$PKG" == "dnf" || "$PKG" == "yum" ]]; then
cp "$TEMP_PEM" "/etc/pki/ca-trust/source/anchors/${TARGET_CERT_NAME}.pem"
[[ "$VERSION_MAJOR" -lt 9 ]] && update-ca-trust force-enable 2>/dev/null || true
update-ca-trust extract
else
cp "$TEMP_PEM" "/usr/local/share/ca-certificates/${TARGET_CERT_NAME}.crt"
update-ca-certificates
fi
fi
timeout 15s umount "$MNT" || true
fi
rmdir "$MNT" 2>/dev/null || true
}
mod_base_repos() {
step "Configuring Base OS Repositories"
if [[ "$PKG" == "dnf" || "$PKG" == "yum" ]]; then
if ! rpm -q epel-release >/dev/null 2>&1; then run_retry $PKG install -y epel-release; fi
if [[ "$PKG" == "dnf" ]]; then
if ! dnf repolist enabled 2>/dev/null | grep -E "crb|powertools" >/dev/null; then
run_retry $PKG install -y 'dnf-command(config-manager)'
$PKG config-manager --set-enabled crb 2>/dev/null || $PKG config-manager --set-enabled powertools 2>/dev/null || true
fi
fi
else
export DEBIAN_FRONTEND=noninteractive
rm -f /etc/apt/sources.list.d/45drives.list
apt-get update -qq || true
run_retry apt-get install -y software-properties-common curl wget gnupg lsb-release
fi
}
mod_base_tools() {
step "Installing Base System Tools"
if [[ "$PKG" == "dnf" || "$PKG" == "yum" ]]; then
PACKAGES="git curl wget nano neovim zsh util-linux-user bind-utils net-tools openssl policycoreutils-python-utils psmisc PackageKit pcp pcp-conf pcp-libs pcp-selinux"
run_retry $PKG install -y $PACKAGES
else
PACKAGES="git curl wget nano neovim zsh openssl net-tools dnsutils psmisc packagekit pcp network-manager"
run_retry apt-get install -y $PACKAGES
timeout 30s systemctl enable --now NetworkManager || true
fi
systemctl unmask packagekit 2>/dev/null || true
timeout 30s systemctl start packagekit 2>/dev/null || true
}
mod_network() {
step "Configuring Network & DNS"
if [[ "$PKG" == "apt-get" ]]; then
if ls /etc/netplan/*.yaml >/dev/null 2>&1 && grep -q "addresses:" /etc/netplan/*.yaml; then
log "Static Netplan detected. Skipping wipe to prevent lockout."
else
mkdir -p /etc/netplan
cat > /etc/netplan/01-network-manager-all.yaml <<EOF
network:
version: 2
renderer: NetworkManager
EOF
netplan apply || true
fi
fi
sed -i "/${DOMAIN_FQDN}/d; /${DOMAIN_ALT}/d; /${DC_DNS_IP}/d; /${FILE_SERVER_NAME}/d" /etc/hosts
cat >> /etc/hosts <<EOF
${DC_DNS_IP} ${DOMAIN_FQDN} ${DOMAIN_ALT} ${DOMAIN_SHORT}
${FILE_SERVER_IP} ${FILE_SERVER_NAME}.${DOMAIN_FQDN} ${FILE_SERVER_NAME}.${DOMAIN_ALT} ${FILE_SERVER_NAME}
EOF
if [[ -L /etc/resolv.conf ]]; then rm -f /etc/resolv.conf; fi
echo -e "search ${DOMAIN_FQDN} ${DOMAIN_ALT}\nnameserver ${DC_DNS_IP}" > /etc/resolv.conf
if command -v nmcli &>/dev/null; then
TARGET_IFACE=$(ip -4 -o addr show | grep "172.16." | awk '{print $2}' | head -n1)
if [[ -n "$TARGET_IFACE" ]]; then
CONN=$(nmcli -t -f NAME,DEVICE con show --active | grep ":${TARGET_IFACE}" | cut -d: -f1 | head -n1)
if [[ -n "$CONN" ]]; then
nmcli con mod "$CONN" ipv4.dns "$DC_DNS_IP" ipv4.dns-search "${DOMAIN_FQDN},${DOMAIN_ALT}" ipv4.ignore-auto-dns yes
timeout 15s nmcli con up "$CONN" >/dev/null 2>&1
fi
fi
fi
echo -e "net.ipv6.conf.all.disable_ipv6 = 1\nnet.ipv6.conf.default.disable_ipv6 = 1" > /etc/sysctl.d/90-disable-ipv6.conf
sysctl --system &>/dev/null || true
if command -v systemctl &>/dev/null; then
if [[ "$PKG" == "apt-get" ]]; then run_retry apt-get install -y chrony; CHRONY_CONF="/etc/chrony/chrony.conf"
else run_retry $PKG install -y chrony; CHRONY_CONF="/etc/chrony.conf"; fi
if [[ -f "$CHRONY_CONF" ]]; then
sed -i '/server/d; /pool/d' "$CHRONY_CONF" 2>/dev/null || true
echo "server ${NTP_SERVER} iburst" >> "$CHRONY_CONF"
fi
timeout 30s systemctl restart chronyd 2>/dev/null || timeout 30s systemctl restart chrony || true
fi
}
mod_firewall() {
step "Configuring Firewalld (Defense in Depth)"
if [[ "$PKG" == "apt-get" ]]; then
run_retry apt-get install -y firewalld
systemctl disable ufw --now 2>/dev/null || true
else
run_retry $PKG install -y firewalld
fi
systemctl enable --now firewalld
# Trust Docker Subnets
firewall-cmd --permanent --zone=trusted --add-source=172.17.0.0/16
firewall-cmd --permanent --zone=trusted --add-source=172.18.0.0/16
firewall-cmd --permanent --zone=trusted --add-source=172.19.0.0/16
firewall-cmd --permanent --zone=trusted --add-source=172.20.0.0/16
firewall-cmd --permanent --zone=trusted --add-source=192.168.250.0/24
# Web Ports
firewall-cmd --permanent --add-service=http
firewall-cmd --permanent --add-service=https
# SSH Lockdown
firewall-cmd --permanent --remove-service=ssh
firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="10.21.0.0/21" service name="ssh" accept'
firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="172.16.121.0/24" service name="ssh" accept'
firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="172.16.21.0/24" service name="ssh" accept'
firewall-cmd --reload
}
mod_resize_home() {
step "LVM Home Resizer"
if ! command -v lvs &>/dev/null; then return; fi
if ! mountpoint -q /home; then return; fi
HOME_DEV=$(findmnt -n -o SOURCE /home)
if [[ "$HOME_DEV" != *"/mapper/"* ]]; then return; fi
LV_NAME=$(lvs --noheadings -o lv_name "$HOME_DEV" | tr -d ' ')
VG_NAME=$(lvs --noheadings -o vg_name "$HOME_DEV" | tr -d ' ')
LV_PATH="/dev/$VG_NAME/$LV_NAME"
ROOT_LV_PATH="/dev/$VG_NAME/root"
MAPPER_PATH="/dev/mapper/${VG_NAME}-${LV_NAME}"
CURRENT_SIZE=$(lvs --noheadings -o lv_size --units g "$LV_PATH" 2>/dev/null | tr -d 'g ' || lvs --noheadings -o L_SIZE --units g "$LV_PATH" | tr -d 'g ')
if [[ ${CURRENT_SIZE%.*} -le 9 ]]; then return; fi
tar czf /tmp/home_backup.tar.gz -C /home .
fuser -km /home || true
timeout 30s umount /home || timeout 15s umount -l /home || true
lvremove -y "$LV_PATH"
lvcreate -L "$HOME_TARGET_SIZE" -n "$LV_NAME" "$VG_NAME" -y
mkfs.ext4 "$LV_PATH"
sed -i '/\/home/d' /etc/fstab
echo "$MAPPER_PATH /home ext4 defaults 0 0" >> /etc/fstab
systemctl daemon-reload || true
timeout 30s mount /home || true
tar xzf /tmp/home_backup.tar.gz -C /home
if command -v restorecon &>/dev/null; then restorecon -R /home; fi
lvextend -l +100%FREE "$ROOT_LV_PATH"
xfs_growfs / || resize2fs "$ROOT_LV_PATH" || true
rm -f /tmp/home_backup.tar.gz
}
mod_domain_users() {
step "Domain Join & User Setup"
if ! timeout 15s id "$LOCAL_USER" &>/dev/null; then timeout 15s useradd -m -s /bin/bash "$LOCAL_USER" || true; fi
echo "$LOCAL_USER:$LOCAL_PASS" | chpasswd || true
timeout 15s usermod -aG sudo "$LOCAL_USER" 2>/dev/null || timeout 15s usermod -aG wheel "$LOCAL_USER" 2>/dev/null || true
if [[ "$PKG" == "apt-get" ]]; then
run_retry apt-get install -y realmd sssd sssd-tools libnss-sss libpam-sss adcli packagekit
if ! grep -q "pam_mkhomedir.so" /etc/pam.d/common-session; then
echo "session optional pam_mkhomedir.so skel=/etc/skel umask=077" >> /etc/pam.d/common-session
fi
else
run_retry $PKG install -y realmd sssd oddjob oddjob-mkhomedir adcli samba-common-tools
fi
if ! ping -c 1 -W 2 "$DOMAIN_FQDN" &>/dev/null; then error "DNS setup failed. Cannot join domain."; return; fi
if command -v update-crypto-policies &>/dev/null; then
update-crypto-policies --set DEFAULT:AD-SUPPORT >/dev/null 2>&1 || true
fi
if ! timeout 15s realm list | grep -q "$DOMAIN_FQDN"; then
echo -e "\n${YELLOW}Enter AD Admin Username (e.g., ent_joeld):${NC}"
read -p "User: " JOIN_USER
realm join --verbose --user="$JOIN_USER" "$DOMAIN_FQDN"
else
success "Already joined. Enforcing state..."
fi
SSSD_CONF="/etc/sssd/sssd.conf"
if [[ -f "$SSSD_CONF" ]]; then
timeout 15s systemctl stop sssd || true
# SSSD Bulletproofing
if grep -q "^services" "$SSSD_CONF"; then
sed -i 's/^services.*/services = nss, pam, ssh/' "$SSSD_CONF"
else
sed -i '/\[sssd\]/a services = nss, pam, ssh' "$SSSD_CONF"
fi
grep -q "access_provider" "$SSSD_CONF" && sed -i 's/access_provider.*/access_provider = simple/' "$SSSD_CONF" || sed -i '/\[domain/a access_provider = simple' "$SSSD_CONF"
grep -q "simple_allow_groups" "$SSSD_CONF" && sed -i "s/simple_allow_groups.*/simple_allow_groups = ${ALLOWED_LOGIN_GROUP}/" "$SSSD_CONF" || sed -i "/access_provider = simple/a simple_allow_groups = ${ALLOWED_LOGIN_GROUP}" "$SSSD_CONF"
sed -i '/ldap_user_ssh_public_key/d' "$SSSD_CONF"
sed -i '/ldap_user_extra_attrs/d' "$SSSD_CONF"
sed -i '/\[domain/a ldap_user_extra_attrs = info:sshPublicKey\nldap_user_ssh_public_key = info' "$SSSD_CONF"
sed -i 's/use_fully_qualified_names.*/use_fully_qualified_names = False/' "$SSSD_CONF"
sed -i 's/fallback_homedir.*/fallback_homedir = \/home\/%u/' "$SSSD_CONF"
sed -i '/ignore_group_members/d' "$SSSD_CONF"
sed -i '/subdomain_enumerate/d' "$SSSD_CONF"
sed -i '/\[domain/a ignore_group_members = True\nsubdomain_enumerate = False' "$SSSD_CONF"
timeout 30s systemctl start sssd || true
if command -v sss_cache &>/dev/null; then sss_cache -E || true; fi
log "Configuring SSH daemon for AD-based keys..."
sed -i '/AuthorizedKeysCommand/d' /etc/ssh/sshd_config
echo -e "\nAuthorizedKeysCommand /usr/bin/sss_ssh_authorizedkeys\nAuthorizedKeysCommandUser nobody" >> /etc/ssh/sshd_config
if systemctl list-unit-files | grep -q "^ssh.service"; then systemctl restart ssh || true
else systemctl restart sshd || true; fi
fi
}
###############################################################################
# 5. MODULAR COMPONENTS
###############################################################################
mod_docker() {
step "Installing & Configuring Docker"
# 1. OS-Aware Repositories
if [[ "$PKG" == "dnf" || "$PKG" == "yum" ]]; then
if [[ ! -f /etc/yum.repos.d/docker-ce.repo ]]; then
run_retry $PKG install -y yum-utils
run_retry yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
fi
$PKG remove -y podman buildah docker docker-client docker-common docker-engine >/dev/null 2>&1 || true
else
if [[ ! -f /etc/apt/sources.list.d/docker.list ]]; then
install -m 0755 -d /etc/apt/keyrings
run_retry curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc
source /etc/os-release
REPO_OS=${ID}
[[ "$ID" == "zorin" ]] && REPO_OS="ubuntu"
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/${REPO_OS} ${VERSION_CODENAME} stable" > /etc/apt/sources.list.d/docker.list
apt-get update -qq || true
fi
fi
# 2. Install Engine
if ! command -v docker &>/dev/null; then
if [[ "$PKG" == "apt-get" ]]; then run_retry apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
else run_retry $PKG install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin; fi
fi
# 3. Daemon Config
mkdir -p /etc/docker
cat > /etc/docker/daemon.json <<EOF
{
"insecure-registries": [ ${INSECURE_REGISTRIES} ]
}
EOF
# 4. Systemd Proxy
mkdir -p /etc/systemd/system/docker.service.d
cat > /etc/systemd/system/docker.service.d/http-proxy.conf <<EOF
[Service]
Environment="HTTP_PROXY=${PROXY_URL}"
Environment="HTTPS_PROXY=${PROXY_URL}"
Environment="NO_PROXY=${NO_PROXY_LIST}"
EOF
systemctl daemon-reload || true
timeout 30s systemctl enable --now docker || true
timeout 60s systemctl restart docker || true
# 5. Access Control
timeout 15s usermod -aG docker root 2>/dev/null || true
if timeout 15s id "$LOCAL_USER" &>/dev/null; then timeout 15s usermod -aG docker "$LOCAL_USER" 2>/dev/null || true; fi
# 6. Client Proxy
mkdir -p /root/.docker
cat > /root/.docker/config.json <<EOF
{
"proxies": {
"default": {
"httpProxy": "${PROXY_URL}",
"httpsProxy": "${PROXY_URL}",
"noProxy": "${NO_PROXY_LIST}"
}
}
}
EOF
if timeout 15s id "$LOCAL_USER" &>/dev/null; then
USER_HOME=$(eval echo ~$LOCAL_USER)
mkdir -p "$USER_HOME/.docker"
cp /root/.docker/config.json "$USER_HOME/.docker/config.json"
chown -R "$LOCAL_USER:$LOCAL_USER" "$USER_HOME/.docker" || true
fi
}
mod_lazydocker() {
step "Installing LazyDocker"
if ! command -v lazydocker &>/dev/null; then
run_retry curl -sSL https://raw.githubusercontent.com/jesseduffield/lazydocker/master/scripts/install_update_linux.sh | bash
fi
}
mod_web_stack() {
step "Installing Web Stack (PHP, Nginx, Node)"
if [[ "$PKG" == "dnf" || "$PKG" == "yum" ]]; then
if ! rpm -q remi-release >/dev/null 2>&1; then
if [[ "$PKG" == "dnf" ]]; then run_retry $PKG install -y "https://rpms.remirepo.net/enterprise/remi-release-${VERSION_MAJOR}.rpm"
else run_retry $PKG install -y http://rpms.remirepo.net/enterprise/remi-release-7.rpm yum-utils; fi
fi
$PKG clean packages >/dev/null 2>&1 || true
if [[ "$PKG" == "dnf" ]]; then
$PKG module reset php -y || true
$PKG module install -y php:remi-${PHP_VERSION}
else
yum-config-manager --enable remi-php83 || true
$PKG install -y php php-cli php-fpm php-mysqlnd php-gd
fi
$PKG install -y java-${JAVA_VERSION}-openjdk nginx nodejs
else
if ! grep -q "ondrej/php" /etc/apt/sources.list.d/* 2>/dev/null; then run_retry add-apt-repository -y ppa:ondrej/php; fi
apt-get update -qq || true
run_retry apt-get install -y php${PHP_VERSION} php${PHP_VERSION}-{cli,fpm,mysql,gd,mbstring,xml,curl,zip}
run_retry apt-get install -y openjdk-${JAVA_VERSION}-jdk nginx nodejs npm
fi
# Inject Proxy into PHP Configs
if command -v php &>/dev/null; then
find /etc/php* -name "php.ini" 2>/dev/null | while read -r INI_FILE; do
sed -i '/^http_proxy/d; /^https_proxy/d' "$INI_FILE"
echo -e "\n; Proxy Settings\nhttp_proxy = \"${PROXY_URL}\"\nhttps_proxy = \"${PROXY_URL}\"" >> "$INI_FILE"
if grep -q "allow_url_fopen" "$INI_FILE"; then sed -i 's/^allow_url_fopen.*/allow_url_fopen = On/' "$INI_FILE"
else echo "allow_url_fopen = On" >> "$INI_FILE"; fi
done
if systemctl list-unit-files | grep -q php-fpm; then timeout 30s systemctl restart php-fpm || true; fi
if systemctl list-unit-files | grep -q php${PHP_VERSION}-fpm; then timeout 30s systemctl restart php${PHP_VERSION}-fpm || true; fi
fi
}
mod_db_stack() {
step "Installing Databases"
if [[ "$PKG" == "dnf" || "$PKG" == "yum" ]]; then
if [[ ! -f /etc/yum.repos.d/mariadb.repo ]]; then
cat > /etc/yum.repos.d/mariadb.repo <<EOF
[mariadb]
name = MariaDB
baseurl = https://rpm.mariadb.org/${MARIADB_VERSION}/rhel/\$releasever/\$basearch
module_hotfixes=1
gpgkey=https://rpm.mariadb.org/RPM-GPG-KEY-MariaDB
gpgcheck=1
EOF
fi
run_retry $PKG install -y MariaDB-server MariaDB-client postgresql-server
else
run_retry apt-get install -y mariadb-server postgresql
fi
}
mod_cockpit() {
step "Installing Cockpit"
if [[ "$PKG" == "apt-get" ]]; then run_retry apt-get install -y cockpit cockpit-storaged cockpit-pcp cockpit-packagekit
else run_retry $PKG install -y cockpit cockpit-storaged cockpit-pcp 2>/dev/null || run_retry $PKG install -y cockpit; fi
mkdir -p /etc/systemd/system/cockpit.service.d
echo -e "[Service]\nEnvironment=\"HTTP_PROXY=${PROXY_URL}\"\nEnvironment=\"HTTPS_PROXY=${PROXY_URL}\"\nEnvironment=\"NO_PROXY=${NO_PROXY_LIST}\"" > /etc/systemd/system/cockpit.service.d/proxy.conf
systemctl daemon-reload || true
timeout 30s systemctl enable --now cockpit.socket || true
}
mod_cleanup() {
step "Final Cleanup & Hardening"
if command -v tmux &>/dev/null; then $PKG remove -y tmux 2>/dev/null || true; fi
rm -f /etc/tmux.conf
if [[ "$PKG" == "apt-get" ]]; then run_retry apt-get install -y fish fail2ban; else run_retry $PKG install -y fish fail2ban; fi
systemctl disable systemd-networkd-wait-online.service 2>/dev/null || true
systemctl mask systemd-networkd-wait-online.service 2>/dev/null || true
if [[ -f /etc/rc.d/rc.local ]]; then chmod +x /etc/rc.d/rc.local; fi
if grep -q "172.16.21.16" /etc/fstab; then sed -i '/172.16.21.16/d' /etc/fstab; fi
if systemctl is-failed sssd-nss.socket &>/dev/null; then
systemctl reset-failed || true
timeout 30s systemctl restart sssd || true
fi
ESCAPED_GROUP=$(echo "$AD_SUDO_GROUP" | sed 's/ /\\ /g')
echo "%${ESCAPED_GROUP} ALL=(ALL) NOPASSWD: ALL" > "/etc/sudoers.d/10-ad-admins"
chmod 440 "/etc/sudoers.d/10-ad-admins"
cat > /etc/fail2ban/jail.local <<EOF
[sshd]
enabled = true
port = ssh
logpath = %(sshd_log)s
maxretry = 3
bantime = 3600
EOF
timeout 30s systemctl enable --now fail2ban || true
}
###############################################################################
# 6. CLI ROUTER
###############################################################################
detect_and_fix_os
show_help() {
echo "Usage: $0 [OPTION]"
echo ""
echo "Core Deployment:"
echo " --basics Proxy, Certs, Repos, Network, Firewalld, AD, Cleanup."
echo " --full Everything (Basics + Docker + Web/DB Stack + Cockpit)."
echo ""
echo "Modular Execution:"
echo " --docker Install and configure Docker Engine with Proxy/Subnets."
echo " --ad-join Run the SSSD and Realmd AD Join sequence."
echo " --certs Mount CIFS, fetch root cert, update CA trust."
echo " --web-stack Install PHP, Nginx, Node, and Java."
echo " --db-stack Install MariaDB and PostgreSQL."
echo " --tools Install zsh, fish, neovim, git, nano, lazydocker."
echo " --resize-home Shrink LVM /home to ${HOME_TARGET_SIZE} (Backup/Restore)."
echo ""
}
if [[ $# -eq 0 ]]; then show_help; exit 0; fi
while [[ "$#" -gt 0 ]]; do
case $1 in
--basics) mod_proxy; mod_clock_fix; mod_certs; mod_base_repos; mod_base_tools; mod_network; mod_firewall; mod_domain_users; mod_cleanup ;;
--full) mod_proxy; mod_clock_fix; mod_certs; mod_base_repos; mod_base_tools; mod_network; mod_firewall; mod_domain_users; mod_docker; mod_web_stack; mod_db_stack; mod_cockpit; mod_cleanup ;;
--docker) mod_proxy; mod_docker ;;
--ad-join) mod_domain_users ;;
--certs) mod_certs ;;
--web-stack) mod_proxy; mod_web_stack ;;
--db-stack) mod_proxy; mod_db_stack ;;
--tools) mod_proxy; mod_base_tools; mod_lazydocker ;;
--resize-home) mod_resize_home ;;
*) echo "Unknown option: $1"; show_help; exit 1 ;;
esac
shift
done
echo -e "\n${GREEN}[$(date +'%H:%M:%S')] === Setup Complete ===${NC}"
master_script.sh - v49
#!/usr/bin/env bash
#
# MASTER INFRASTRUCTURE SETUP v49
# Enhancements: Firefox Enterprise Policy Proxying, Flatpak Ecosystem, DE Auto-Detect
#
###############################################################################
# 1. CONFIGURATION
###############################################################################
DOMAIN_FQDN="m21.gov.local"
DOMAIN_ALT="m21.gov.tt"
DOMAIN_SHORT="M21"
DC_DNS_IP="172.16.21.161"
NTP_SERVER="172.16.121.9"
TARGET_TIMEZONE="America/Port_of_Spain"
# File Server Info
FILE_SERVER_IP="172.16.21.16"
FILE_SERVER_NAME="fileserver2"
# Proxy
PROXY_URL="http://172.40.4.14:8080"
NO_PROXY_LIST="127.0.0.1,localhost,localhost.localdomain,${DOMAIN_FQDN},${DOMAIN_ALT},.${DOMAIN_FQDN},.${DOMAIN_ALT},${DC_DNS_IP},172.30.0.0/20,172.26.21.0/24,10.21.0.0/21,172.16.121.0/24"
# Docker Settings
INSECURE_REGISTRIES='"172.16.121.119:5000", "docker-repo.msya.gov.tt"'
# AD Access Control
AD_SUDO_GROUP="ICT Staff SG M21"
ALLOWED_LOGIN_GROUP="ICT Staff SG M21"
# Share Credentials
SHARE_PATH="//172.16.21.16/fileserver2"
SHARE_USER="Cipher.m21"
SHARE_PASS=")\ly; 634'NJ%i+"
CERT_SOURCE_PATH="/General/IT FILES/prx/Gortt_certificate_V4.cer"
TARGET_CERT_NAME="GORTT_Root_Exp2029"
# Failsafe User
LOCAL_USER="pcsupport"
LOCAL_PASS="ProIT321*"
# LVM Settings
HOME_TARGET_SIZE="8G"
# Versions
PHP_VERSION="8.3"
JAVA_VERSION="21"
MARIADB_VERSION="10.11"
###############################################################################
# 2. HELPER FUNCTIONS
###############################################################################
set -e
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[0;33m'; BLUE='\033[0;34m'; NC='\033[0m'
log() { echo -e "${BLUE}[$(date +'%H:%M:%S')] [INFO]${NC} $1"; }
step() { echo -e "\n${YELLOW}[$(date +'%H:%M:%S')] >>> $1${NC}"; }
success() { echo -e "${GREEN}[$(date +'%H:%M:%S')] [OK]${NC} $1"; }
error() { echo -e "${RED}[$(date +'%H:%M:%S')] [ERROR]${NC} $1"; }
run_retry() {
local n=1; local max=3; local delay=2
while true; do
"$@" && return 0
if [[ $n -lt $max ]]; then
((n++)); log "Command failed. Retrying ($n/$max)..."; sleep $delay
else
return 1
fi
done
}
###############################################################################
# 3. PRE-FLIGHT CHECKS
###############################################################################
detect_and_fix_os() {
if [[ ! -f /etc/os-release ]]; then error "Cannot detect OS. /etc/os-release missing."; exit 1; fi
source /etc/os-release
OS_ID=$(echo "$ID" | tr '[:upper:]' '[:lower:]')
VERSION_MAJOR=$(echo "$VERSION_ID" | cut -d. -f1)
if timeout 10s systemctl is-active --quiet packagekit.service 2>/dev/null; then
timeout 15s systemctl stop packagekit.service || true
fi
if [[ "$OS_ID" == "centos" && "$VERSION_MAJOR" == "7" ]]; then
PKG="yum"
if grep -q "linux/rhel" /etc/yum.repos.d/docker-ce.repo 2>/dev/null; then rm -f /etc/yum.repos.d/docker-ce.repo; fi
if [ ! -f /etc/yum.repos.d/CentOS-Base.repo.backup ]; then
cp /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.backup 2>/dev/null || true
run_retry curl -o /etc/yum.repos.d/CentOS-Base.repo https://el7.repo.almalinux.org/centos/CentOS-Base.repo
fi
elif [[ "$OS_ID" =~ (rhel|centos|almalinux|rocky|fedora) ]]; then PKG="dnf"
elif [[ "$OS_ID" =~ (ubuntu|debian|zorin) ]]; then PKG="apt-get"; export DEBIAN_FRONTEND=noninteractive
elif [[ "$OS_ID" == "arch" || "$ID_LIKE" == *"arch"* ]]; then PKG="pacman"; run_retry pacman -Sy
else error "Unsupported OS: $OS_ID"; exit 1; fi
}
###############################################################################
# 4. CORE MODULES
###############################################################################
mod_proxy() {
step "Configuring System Proxy"
cat > /etc/profile.d/proxy.sh <<EOF
export http_proxy="${PROXY_URL}"
export https_proxy="${PROXY_URL}"
export ftp_proxy="${PROXY_URL}"
export no_proxy="${NO_PROXY_LIST}"
export HTTP_PROXY="${PROXY_URL}"
export HTTPS_PROXY="${PROXY_URL}"
export FTP_PROXY="${PROXY_URL}"
export NO_PROXY="${NO_PROXY_LIST}"
EOF
source /etc/profile.d/proxy.sh
mkdir -p /etc/sudoers.d
echo 'Defaults env_keep += "http_proxy https_proxy ftp_proxy no_proxy HTTP_PROXY HTTPS_PROXY FTP_PROXY NO_PROXY"' > /etc/sudoers.d/10-proxy-env
chmod 440 /etc/sudoers.d/10-proxy-env
if [[ "$PKG" == "dnf" || "$PKG" == "yum" ]]; then
CONF_FILE="/etc/dnf/dnf.conf"
[[ ! -f "$CONF_FILE" ]] && CONF_FILE="/etc/yum.conf"
grep -q "proxy=" "$CONF_FILE" 2>/dev/null || echo "proxy=${PROXY_URL}" >> "$CONF_FILE"
if ! grep -q "minrate" "$CONF_FILE" 2>/dev/null; then
echo -e "timeout=60\nretries=10\nminrate=1" >> "$CONF_FILE"
fi
elif [[ "$PKG" == "apt-get" ]]; then
echo -e "Acquire::http::Proxy \"${PROXY_URL}\";\nAcquire::https::Proxy \"${PROXY_URL}\";" > /etc/apt/apt.conf.d/80proxy
fi
}
mod_gui_proxy() {
step "Configuring GUI Proxy Settings (System-Wide)"
PROXY_HOST=$(echo "$PROXY_URL" | awk -F/ '{print $3}' | cut -d: -f1)
PROXY_PORT=$(echo "$PROXY_URL" | awk -F: '{print $NF}')
DCONF_NO_PROXY="['$(echo "$NO_PROXY_LIST" | sed "s/,/','/g")']"
if [[ "$PKG" == "apt-get" ]]; then run_retry apt-get install -y dconf-cli
elif [[ "$PKG" == "dnf" || "$PKG" == "yum" ]]; then run_retry $PKG install -y dconf
elif [[ "$PKG" == "pacman" ]]; then run_retry pacman -S --noconfirm dconf
fi
# 1. GNOME / Cinnamon / Mate
mkdir -p /etc/dconf/profile
mkdir -p /etc/dconf/db/local.d
echo -e "user-db:user\nsystem-db:local" > /etc/dconf/profile/user
cat > /etc/dconf/db/local.d/01-proxy <<EOF
[system/proxy]
mode='manual'
ignore-hosts=${DCONF_NO_PROXY}
[system/proxy/http]
host='${PROXY_HOST}'
port=${PROXY_PORT}
[system/proxy/https]
host='${PROXY_HOST}'
port=${PROXY_PORT}
[system/proxy/ftp]
host='${PROXY_HOST}'
port=${PROXY_PORT}
EOF
dconf update || log "Warning: dconf update failed, GUI settings may require reboot."
# 2. KDE Plasma
mkdir -p /etc/xdg
cat > /etc/xdg/kioslaverc <<EOF
[Proxy Settings]
ProxyType=1
httpProxy=${PROXY_URL}
httpsProxy=${PROXY_URL}
ftpProxy=${PROXY_URL}
NoProxyFor=${NO_PROXY_LIST}
EOF
# 3. Firefox Enterprise Policy Setup
mkdir -p /etc/firefox/policies
cat > /etc/firefox/policies/policies.json <<FFEOF
{
"policies": {
"Proxy": {
"Mode": "manual",
"HTTPProxy": "${PROXY_HOST}:${PROXY_PORT}",
"HTTPSProxy": "${PROXY_HOST}:${PROXY_PORT}",
"FTPProxy": "${PROXY_HOST}:${PROXY_PORT}",
"Passthrough": "${NO_PROXY_LIST}"
}
}
}
FFEOF
}
mod_proxy_toggle() {
step "Installing Proxy Toggle Tool"
cat > /usr/local/bin/toggle-proxy <<EOF
#!/usr/bin/env bash
# System-Wide Proxy Toggle
# Usage: sudo toggle-proxy [on|off]
if [[ "\$EUID" -ne 0 ]]; then
echo "Please run as root (sudo toggle-proxy on|off)"
exit 1
fi
MODE=\$1
PROXY_URL="${PROXY_URL}"
PROXY_HOST="\$(echo "\$PROXY_URL" | awk -F/ '{print \$3}' | cut -d: -f1)"
PROXY_PORT="\$(echo "\$PROXY_URL" | awk -F: '{print \$NF}')"
NO_PROXY_LIST="${NO_PROXY_LIST}"
# Scrub hardcoded package manager proxies in BOTH states
if command -v apt-get &>/dev/null; then rm -f /etc/apt/apt.conf.d/80proxy; fi
if command -v dnf &>/dev/null; then sed -i '/^proxy=/d' /etc/dnf/dnf.conf 2>/dev/null || true; fi
if [[ "\$MODE" == "on" ]]; then
echo "Enabling System Proxy..."
cat > /etc/profile.d/proxy.sh <<ENVEOF
export http_proxy="\${PROXY_URL}"
export https_proxy="\${PROXY_URL}"
export ftp_proxy="\${PROXY_URL}"
export no_proxy="\${NO_PROXY_LIST}"
export HTTP_PROXY="\${PROXY_URL}"
export HTTPS_PROXY="\${PROXY_URL}"
export FTP_PROXY="\${PROXY_URL}"
export NO_PROXY="\${NO_PROXY_LIST}"
ENVEOF
if [[ -d /etc/systemd/system/docker.service.d ]]; then
cat > /etc/systemd/system/docker.service.d/http-proxy.conf <<DOCKEREOF
[Service]
Environment="HTTP_PROXY=\${PROXY_URL}"
Environment="HTTPS_PROXY=\${PROXY_URL}"
Environment="NO_PROXY=\${NO_PROXY_LIST}"
DOCKEREOF
systemctl daemon-reload && systemctl restart docker || true
fi
if command -v dconf &>/dev/null; then
mkdir -p /etc/dconf/db/local.d
sed -i "s/mode='none'/mode='manual'/" /etc/dconf/db/local.d/01-proxy 2>/dev/null || true
dconf update
fi
if [[ -f /etc/xdg/kioslaverc ]]; then
sed -i "s/ProxyType=0/ProxyType=1/" /etc/xdg/kioslaverc 2>/dev/null || true
fi
mkdir -p /etc/firefox/policies
cat > /etc/firefox/policies/policies.json <<FFEOF
{
"policies": {
"Proxy": {
"Mode": "manual",
"HTTPProxy": "\${PROXY_HOST}:\${PROXY_PORT}",
"HTTPSProxy": "\${PROXY_HOST}:\${PROXY_PORT}",
"FTPProxy": "\${PROXY_HOST}:\${PROXY_PORT}",
"Passthrough": "\${NO_PROXY_LIST}"
}
}
}
FFEOF
echo "[OK] Proxy is ON. Log out and back in for all terminal sessions to update."
elif [[ "\$MODE" == "off" ]]; then
echo "Disabling System Proxy..."
> /etc/profile.d/proxy.sh
if [[ -d /etc/systemd/system/docker.service.d ]]; then
> /etc/systemd/system/docker.service.d/http-proxy.conf
systemctl daemon-reload && systemctl restart docker || true
fi
if command -v dconf &>/dev/null; then
mkdir -p /etc/dconf/db/local.d
sed -i "s/mode='manual'/mode='none'/" /etc/dconf/db/local.d/01-proxy 2>/dev/null || true
dconf update
fi
if [[ -f /etc/xdg/kioslaverc ]]; then
sed -i "s/ProxyType=1/ProxyType=0/" /etc/xdg/kioslaverc 2>/dev/null || true
fi
mkdir -p /etc/firefox/policies
cat > /etc/firefox/policies/policies.json <<FFEOF
{
"policies": {
"Proxy": {
"Mode": "none"
}
}
}
FFEOF
echo "[OK] Proxy is OFF. Log out and back in for all terminal sessions to update."
else
echo "Usage: toggle-proxy [on|off]"
fi
EOF
chmod +x /usr/local/bin/toggle-proxy
}
mod_flatpak() {
step "Configuring Flatpak, Flathub & DE Integrations"
# Check for DE footprint
HAS_GNOME=false
HAS_KDE=false
if command -v gnome-shell &>/dev/null || dpkg -l | grep -q "gnome-shell" 2>/dev/null || rpm -q gnome-shell 2>/dev/null; then HAS_GNOME=true; fi
if command -v plasmashell &>/dev/null || dpkg -l | grep -q "plasma-workspace" 2>/dev/null || rpm -q plasma-workspace 2>/dev/null; then HAS_KDE=true; fi
if [[ "$PKG" == "apt-get" ]]; then
run_retry apt-get install -y flatpak
if [ "$HAS_GNOME" = true ]; then run_retry apt-get install -y gnome-software-plugin-flatpak; fi
if [ "$HAS_KDE" = true ]; then run_retry apt-get install -y plasma-discover-backend-flatpak; fi
elif [[ "$PKG" == "dnf" || "$PKG" == "yum" ]]; then
run_retry $PKG install -y flatpak
if [ "$HAS_GNOME" = true ]; then run_retry $PKG install -y gnome-software; fi
if [ "$HAS_KDE" = true ]; then run_retry $PKG install -y plasma-discover-flatpak; fi
elif [[ "$PKG" == "pacman" ]]; then
run_retry pacman -S --noconfirm flatpak
if [ "$HAS_GNOME" = true ]; then run_retry pacman -S --noconfirm gnome-software; fi
if [ "$HAS_KDE" = true ]; then run_retry pacman -S --noconfirm discover; fi
fi
# Add Flathub Repository System-Wide
run_retry flatpak remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
# Install Auto-Extensions
if [ "$HAS_GNOME" = true ]; then
log "GNOME detected. Installing Extension Manager from Flathub..."
run_retry flatpak install -y flathub com.mattjakeman.ExtensionManager
fi
}
mod_clock_fix() {
step "Synchronizing System Clock"
timedatectl set-timezone "$TARGET_TIMEZONE" || true
timedatectl set-ntp true || true
if systemctl list-unit-files | grep -q systemd-timesyncd; then
timeout 30s systemctl restart systemd-timesyncd || true
fi
}
mod_certs() {
step "Installing Certificates"
MNT="/mnt/share_certs_tmp"
mkdir -p "$MNT"
if ! command -v mount.cifs &>/dev/null; then
if [[ "$PKG" == "apt-get" ]]; then run_retry apt-get update -qq >/dev/null 2>&1 || true; run_retry apt-get install -y cifs-utils
elif [[ "$PKG" == "pacman" ]]; then run_retry pacman -S --noconfirm cifs-utils
else run_retry $PKG install -y cifs-utils; fi
fi
if mountpoint -q "$MNT"; then umount -l "$MNT"; fi
if timeout 30s mount -t cifs "$SHARE_PATH" "$MNT" -o username="$SHARE_USER",password="$SHARE_PASS",vers=3.0; then
SOURCE_FULL="$MNT$CERT_SOURCE_PATH"
TEMP_PEM="/tmp/${TARGET_CERT_NAME}_staging.pem"
if [[ -f "$SOURCE_FULL" ]]; then
if ! openssl x509 -inform der -in "$SOURCE_FULL" -out "$TEMP_PEM" 2>/dev/null; then cp "$SOURCE_FULL" "$TEMP_PEM"; fi
if [[ "$PKG" == "dnf" || "$PKG" == "yum" ]]; then
cp "$TEMP_PEM" "/etc/pki/ca-trust/source/anchors/${TARGET_CERT_NAME}.pem"
[[ "$VERSION_MAJOR" -lt 9 ]] && update-ca-trust force-enable 2>/dev/null || true
update-ca-trust extract
elif [[ "$PKG" == "pacman" ]]; then
cp "$TEMP_PEM" "/etc/ca-certificates/trust-source/anchors/${TARGET_CERT_NAME}.crt"
trust extract-compat
else
cp "$TEMP_PEM" "/usr/local/share/ca-certificates/${TARGET_CERT_NAME}.crt"
update-ca-certificates
fi
fi
timeout 15s umount "$MNT" || true
fi
rmdir "$MNT" 2>/dev/null || true
}
mod_base_repos() {
step "Configuring Base OS Repositories"
if [[ "$PKG" == "dnf" || "$PKG" == "yum" ]]; then
if [[ "$OS_ID" == "fedora" ]]; then
log "Setting up Fedora 3rd Party Repos (RPM Fusion & Workstation Repos)..."
run_retry dnf install -y dnf-plugins-core fedora-workstation-repositories || true
run_retry dnf install -y "https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-${VERSION_MAJOR}.noarch.rpm" \
"https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-${VERSION_MAJOR}.noarch.rpm" || true
dnf config-manager --set-enabled rpmfusion-free rpmfusion-nonfree || true
else
if ! rpm -q epel-release >/dev/null 2>&1; then run_retry $PKG install -y epel-release; fi
if [[ "$PKG" == "dnf" ]]; then
if ! dnf repolist enabled 2>/dev/null | grep -E "crb|powertools" >/dev/null; then
run_retry $PKG install -y 'dnf-command(config-manager)'
$PKG config-manager --set-enabled crb 2>/dev/null || $PKG config-manager --set-enabled powertools 2>/dev/null || true
fi
fi
fi
elif [[ "$PKG" == "apt-get" ]]; then
export DEBIAN_FRONTEND=noninteractive
rm -f /etc/apt/sources.list.d/45drives.list
apt-get update -qq || true
BASE_APT_PKGS="curl wget gnupg lsb-release ca-certificates"
if [[ "$OS_ID" != "debian" ]]; then BASE_APT_PKGS="software-properties-common $BASE_APT_PKGS"; fi
run_retry apt-get install -y $BASE_APT_PKGS
fi
}
mod_base_tools() {
step "Installing Base System Tools"
if [[ "$PKG" == "dnf" || "$PKG" == "yum" ]]; then
PACKAGES="git curl wget nano neovim zsh util-linux-user bind-utils net-tools openssl policycoreutils-python-utils psmisc PackageKit pcp pcp-conf pcp-libs pcp-selinux"
run_retry $PKG install -y $PACKAGES
elif [[ "$PKG" == "pacman" ]]; then
PACKAGES="git curl wget nano neovim zsh openssl net-tools bind psmisc networkmanager"
run_retry pacman -S --noconfirm $PACKAGES
timeout 30s systemctl enable --now NetworkManager || true
else
PACKAGES="git curl wget nano neovim zsh openssl net-tools dnsutils psmisc packagekit pcp network-manager"
run_retry apt-get install -y $PACKAGES
timeout 30s systemctl enable --now NetworkManager || true
fi
systemctl unmask packagekit 2>/dev/null || true
timeout 30s systemctl start packagekit 2>/dev/null || true
}
mod_network() {
step "Configuring Network & DNS"
if [[ "$PKG" == "apt-get" ]] && command -v netplan >/dev/null 2>&1; then
if ls /etc/netplan/*.yaml >/dev/null 2>&1 && grep -q "addresses:" /etc/netplan/*.yaml; then
log "Static Netplan detected. Skipping wipe to prevent lockout."
else
mkdir -p /etc/netplan
cat > /etc/netplan/01-network-manager-all.yaml <<EOF
network:
version: 2
renderer: NetworkManager
EOF
netplan apply || true
fi
fi
sed -i "/${DOMAIN_FQDN}/d; /${DOMAIN_ALT}/d; /${DC_DNS_IP}/d; /${FILE_SERVER_NAME}/d" /etc/hosts
cat >> /etc/hosts <<EOF
${DC_DNS_IP} ${DOMAIN_FQDN} ${DOMAIN_ALT} ${DOMAIN_SHORT}
${FILE_SERVER_IP} ${FILE_SERVER_NAME}.${DOMAIN_FQDN} ${FILE_SERVER_NAME}.${DOMAIN_ALT} ${FILE_SERVER_NAME}
EOF
if [[ -L /etc/resolv.conf ]]; then rm -f /etc/resolv.conf; fi
echo -e "search ${DOMAIN_FQDN} ${DOMAIN_ALT}\nnameserver ${DC_DNS_IP}" > /etc/resolv.conf
if command -v nmcli &>/dev/null; then
TARGET_IFACE=$(ip -4 -o addr show | grep "172.16." | awk '{print $2}' | head -n1)
if [[ -n "$TARGET_IFACE" ]]; then
CONN=$(nmcli -t -f NAME,DEVICE con show --active | grep ":${TARGET_IFACE}" | cut -d: -f1 | head -n1)
if [[ -n "$CONN" ]]; then
nmcli con mod "$CONN" ipv4.dns "$DC_DNS_IP" ipv4.dns-search "${DOMAIN_FQDN},${DOMAIN_ALT}" ipv4.ignore-auto-dns yes
timeout 15s nmcli con up "$CONN" >/dev/null 2>&1
fi
fi
fi
echo -e "net.ipv6.conf.all.disable_ipv6 = 1\nnet.ipv6.conf.default.disable_ipv6 = 1" > /etc/sysctl.d/90-disable-ipv6.conf
sysctl --system &>/dev/null || true
if command -v systemctl &>/dev/null; then
if [[ "$PKG" == "apt-get" ]]; then run_retry apt-get install -y chrony; CHRONY_CONF="/etc/chrony/chrony.conf"
elif [[ "$PKG" == "pacman" ]]; then run_retry pacman -S --noconfirm chrony; CHRONY_CONF="/etc/chrony.conf"
else run_retry $PKG install -y chrony; CHRONY_CONF="/etc/chrony.conf"; fi
if [[ -f "$CHRONY_CONF" ]]; then
sed -i '/server/d; /pool/d' "$CHRONY_CONF" 2>/dev/null || true
echo "server ${NTP_SERVER} iburst" >> "$CHRONY_CONF"
fi
timeout 30s systemctl restart chronyd 2>/dev/null || timeout 30s systemctl restart chrony || true
fi
}
mod_firewall() {
step "Configuring Firewalld (Defense in Depth)"
if [[ "$PKG" == "apt-get" ]]; then
run_retry apt-get install -y firewalld
systemctl disable ufw --now 2>/dev/null || true
elif [[ "$PKG" == "pacman" ]]; then
run_retry pacman -S --noconfirm firewalld
else
run_retry $PKG install -y firewalld
fi
systemctl enable --now firewalld
firewall-cmd --permanent --zone=trusted --add-source=172.17.0.0/16
firewall-cmd --permanent --zone=trusted --add-source=172.18.0.0/16
firewall-cmd --permanent --zone=trusted --add-source=172.19.0.0/16
firewall-cmd --permanent --zone=trusted --add-source=172.20.0.0/16
firewall-cmd --permanent --zone=trusted --add-source=192.168.250.0/24
firewall-cmd --permanent --add-service=http
firewall-cmd --permanent --add-service=https
firewall-cmd --permanent --remove-service=ssh
firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="10.21.0.0/21" service name="ssh" accept'
firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="172.16.121.0/24" service name="ssh" accept'
firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="172.16.21.0/24" service name="ssh" accept'
firewall-cmd --reload
}
mod_resize_home() {
step "LVM Home Resizer"
if ! command -v lvs &>/dev/null; then return; fi
if ! mountpoint -q /home; then return; fi
HOME_DEV=$(findmnt -n -o SOURCE /home)
if [[ "$HOME_DEV" != *"/mapper/"* ]]; then return; fi
LV_NAME=$(lvs --noheadings -o lv_name "$HOME_DEV" | tr -d ' ')
VG_NAME=$(lvs --noheadings -o vg_name "$HOME_DEV" | tr -d ' ')
LV_PATH="/dev/$VG_NAME/$LV_NAME"
ROOT_LV_PATH="/dev/$VG_NAME/root"
MAPPER_PATH="/dev/mapper/${VG_NAME}-${LV_NAME}"
CURRENT_SIZE=$(lvs --noheadings -o lv_size --units g "$LV_PATH" 2>/dev/null | tr -d 'g ' || lvs --noheadings -o L_SIZE --units g "$LV_PATH" | tr -d 'g ')
if [[ ${CURRENT_SIZE%.*} -le 9 ]]; then return; fi
tar czf /tmp/home_backup.tar.gz -C /home .
fuser -km /home || true
timeout 30s umount /home || timeout 15s umount -l /home || true
lvremove -y "$LV_PATH"
lvcreate -L "$HOME_TARGET_SIZE" -n "$LV_NAME" "$VG_NAME" -y
mkfs.ext4 "$LV_PATH"
sed -i '/\/home/d' /etc/fstab
echo "$MAPPER_PATH /home ext4 defaults 0 0" >> /etc/fstab
systemctl daemon-reload || true
timeout 30s mount /home || true
tar xzf /tmp/home_backup.tar.gz -C /home
if command -v restorecon &>/dev/null; then restorecon -R /home; fi
lvextend -l +100%FREE "$ROOT_LV_PATH"
xfs_growfs / || resize2fs "$ROOT_LV_PATH" || true
rm -f /tmp/home_backup.tar.gz
}
mod_domain_users() {
step "Domain Join & User Setup"
if ! timeout 15s id "$LOCAL_USER" &>/dev/null; then timeout 15s useradd -m -s /bin/bash "$LOCAL_USER" || true; fi
echo "$LOCAL_USER:$LOCAL_PASS" | chpasswd || true
timeout 15s usermod -aG sudo "$LOCAL_USER" 2>/dev/null || timeout 15s usermod -aG wheel "$LOCAL_USER" 2>/dev/null || true
if [[ "$PKG" == "apt-get" ]]; then
run_retry apt-get install -y realmd sssd sssd-tools libnss-sss libpam-sss adcli packagekit
if ! grep -q "pam_mkhomedir.so" /etc/pam.d/common-session; then
echo "session optional pam_mkhomedir.so skel=/etc/skel umask=077" >> /etc/pam.d/common-session
fi
elif [[ "$PKG" == "pacman" ]]; then
run_retry pacman -S --noconfirm sssd adcli smbclient
if ! command -v realm &>/dev/null; then
log "Warning: 'realmd' is not in standard Arch repos. Please install it via AUR (e.g., yay -S realmd) to join the domain later."
fi
else
run_retry $PKG install -y realmd sssd oddjob oddjob-mkhomedir adcli samba-common-tools
fi
if ! ping -c 1 -W 2 "$DOMAIN_FQDN" &>/dev/null; then error "DNS setup failed. Cannot join domain."; return; fi
if command -v update-crypto-policies &>/dev/null; then
update-crypto-policies --set DEFAULT:AD-SUPPORT >/dev/null 2>&1 || true
fi
if command -v realm &>/dev/null; then
if ! timeout 15s realm list | grep -q "$DOMAIN_FQDN"; then
echo -e "\n${YELLOW}Enter AD Admin Username (e.g., ent_joeld):${NC}"
read -p "User: " JOIN_USER
realm join --verbose --user="$JOIN_USER" "$DOMAIN_FQDN"
else
success "Already joined. Enforcing state..."
fi
fi
if ! command -v sshd &>/dev/null || [[ ! -f /etc/ssh/sshd_config ]]; then
log "OpenSSH Server missing or unconfigured. Installing explicitly..."
if [[ "$PKG" == "apt-get" ]]; then run_retry apt-get install -y openssh-server
elif [[ "$PKG" == "pacman" ]]; then run_retry pacman -S --noconfirm openssh
else run_retry $PKG install -y openssh-server; fi
if systemctl list-unit-files | grep -q "^ssh.service"; then
systemctl enable ssh --now || true
else
systemctl enable sshd --now || true
fi
sleep 2
fi
if [[ ! -f /etc/ssh/sshd_config ]]; then
error "/etc/ssh/sshd_config still not found after installation attempts. SSH AD key injection bypassed."
else
log "Configuring SSH daemon for AD-based keys..."
sed -i '/AuthorizedKeysCommand/d' /etc/ssh/sshd_config
echo -e "\nAuthorizedKeysCommand /usr/bin/sss_ssh_authorizedkeys\nAuthorizedKeysCommandUser nobody" >> /etc/ssh/sshd_config
if systemctl list-unit-files | grep -q "^ssh.service"; then systemctl restart ssh || true
else systemctl restart sshd || true; fi
fi
SSSD_CONF="/etc/sssd/sssd.conf"
if [[ -f "$SSSD_CONF" ]]; then
timeout 15s systemctl stop sssd || true
if grep -q "^services" "$SSSD_CONF"; then
sed -i 's/^services.*/services = nss, pam, ssh/' "$SSSD_CONF"
else
sed -i '/\[sssd\]/a services = nss, pam, ssh' "$SSSD_CONF"
fi
grep -q "access_provider" "$SSSD_CONF" && sed -i 's/access_provider.*/access_provider = simple/' "$SSSD_CONF" || sed -i '/\[domain/a access_provider = simple' "$SSSD_CONF"
grep -q "simple_allow_groups" "$SSSD_CONF" && sed -i "s/simple_allow_groups.*/simple_allow_groups = ${ALLOWED_LOGIN_GROUP}/" "$SSSD_CONF" || sed -i "/access_provider = simple/a simple_allow_groups = ${ALLOWED_LOGIN_GROUP}" "$SSSD_CONF"
sed -i '/ldap_user_ssh_public_key/d' "$SSSD_CONF"
sed -i '/ldap_user_extra_attrs/d' "$SSSD_CONF"
sed -i '/\[domain/a ldap_user_extra_attrs = info:sshPublicKey\nldap_user_ssh_public_key = info' "$SSSD_CONF"
sed -i 's/use_fully_qualified_names.*/use_fully_qualified_names = False/' "$SSSD_CONF"
sed -i 's/fallback_homedir.*/fallback_homedir = \/home\/%u/' "$SSSD_CONF"
sed -i '/ignore_group_members/d' "$SSSD_CONF"
sed -i '/subdomain_enumerate/d' "$SSSD_CONF"
sed -i '/\[domain/a ignore_group_members = True\nsubdomain_enumerate = False' "$SSSD_CONF"
timeout 30s systemctl start sssd || true
if command -v sss_cache &>/dev/null; then sss_cache -E || true; fi
fi
}
###############################################################################
# 5. MODULAR COMPONENTS
###############################################################################
mod_docker() {
step "Installing & Configuring Docker"
if [[ "$PKG" == "dnf" || "$PKG" == "yum" ]]; then
if [[ ! -f /etc/yum.repos.d/docker-ce.repo ]]; then
run_retry $PKG install -y yum-utils
if [[ "$OS_ID" == "fedora" ]]; then
run_retry yum-config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo
else
run_retry yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
fi
fi
$PKG remove -y podman buildah docker docker-client docker-common docker-engine >/dev/null 2>&1 || true
elif [[ "$PKG" == "apt-get" ]]; then
if [[ ! -f /etc/apt/sources.list.d/docker.list ]]; then
source /etc/os-release
REPO_OS=${ID}
case "$REPO_OS" in
debian|ubuntu) : ;;
*) REPO_OS="ubuntu" ;;
esac
REPO_CODENAME="${VERSION_CODENAME:-$(command -v lsb_release >/dev/null 2>&1 && lsb_release -cs || echo stable)}"
install -m 0755 -d /etc/apt/keyrings
run_retry curl -fsSL "https://download.docker.com/linux/${REPO_OS}/gpg" -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/${REPO_OS} ${REPO_CODENAME} stable" > /etc/apt/sources.list.d/docker.list
apt-get update -qq || true
fi
fi
if ! command -v docker &>/dev/null; then
if [[ "$PKG" == "apt-get" ]]; then run_retry apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
elif [[ "$PKG" == "pacman" ]]; then run_retry pacman -S --noconfirm docker docker-compose docker-buildx
else run_retry $PKG install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin; fi
fi
mkdir -p /etc/docker
cat > /etc/docker/daemon.json <<EOF
{
"insecure-registries": [ ${INSECURE_REGISTRIES} ]
}
EOF
mkdir -p /etc/systemd/system/docker.service.d
cat > /etc/systemd/system/docker.service.d/http-proxy.conf <<EOF
[Service]
Environment="HTTP_PROXY=${PROXY_URL}"
Environment="HTTPS_PROXY=${PROXY_URL}"
Environment="NO_PROXY=${NO_PROXY_LIST}"
EOF
systemctl daemon-reload || true
timeout 30s systemctl enable --now docker || true
timeout 60s systemctl restart docker || true
timeout 15s usermod -aG docker root 2>/dev/null || true
if timeout 15s id "$LOCAL_USER" &>/dev/null; then timeout 15s usermod -aG docker "$LOCAL_USER" 2>/dev/null || true; fi
mkdir -p /root/.docker
cat > /root/.docker/config.json <<EOF
{
"proxies": {
"default": {
"httpProxy": "${PROXY_URL}",
"httpsProxy": "${PROXY_URL}",
"noProxy": "${NO_PROXY_LIST}"
}
}
}
EOF
if timeout 15s id "$LOCAL_USER" &>/dev/null; then
USER_HOME=$(eval echo ~$LOCAL_USER)
mkdir -p "$USER_HOME/.docker"
cp /root/.docker/config.json "$USER_HOME/.docker/config.json"
chown -R "$LOCAL_USER:$LOCAL_USER" "$USER_HOME/.docker" || true
fi
}
mod_lazydocker() {
step "Installing LazyDocker"
if ! command -v lazydocker &>/dev/null; then
run_retry curl -sSL https://raw.githubusercontent.com/jesseduffield/lazydocker/master/scripts/install_update_linux.sh | bash
fi
}
mod_web_stack() {
step "Installing Web Stack (PHP, Nginx, Node)"
if [[ "$PKG" == "dnf" || "$PKG" == "yum" ]]; then
if ! rpm -q remi-release >/dev/null 2>&1; then
if [[ "$OS_ID" == "fedora" ]]; then
run_retry dnf install -y "https://rpms.remirepo.net/fedora/remi-release-${VERSION_MAJOR}.rpm"
elif [[ "$PKG" == "dnf" ]]; then
run_retry $PKG install -y "https://rpms.remirepo.net/enterprise/remi-release-${VERSION_MAJOR}.rpm"
else
run_retry $PKG install -y http://rpms.remirepo.net/enterprise/remi-release-7.rpm yum-utils
fi
fi
$PKG clean packages >/dev/null 2>&1 || true
if [[ "$PKG" == "dnf" ]]; then
$PKG module reset php -y || true
$PKG module install -y php:remi-${PHP_VERSION}
else
yum-config-manager --enable remi-php83 || true
$PKG install -y php php-cli php-fpm php-mysqlnd php-gd
fi
$PKG install -y java-${JAVA_VERSION}-openjdk nginx nodejs
elif [[ "$PKG" == "pacman" ]]; then
run_retry pacman -S --noconfirm php php-fpm php-gd php-pgsql nginx nodejs npm jre-openjdk
else
source /etc/os-release
if [[ "$ID" == "debian" ]]; then
if [[ ! -f /etc/apt/sources.list.d/sury-php.list ]]; then
install -m 0755 -d /etc/apt/keyrings
run_retry curl -fsSL https://packages.sury.org/php/apt.gpg -o /etc/apt/keyrings/sury-php.gpg
chmod a+r /etc/apt/keyrings/sury-php.gpg
PHP_CODENAME="${VERSION_CODENAME:-$(command -v lsb_release >/dev/null 2>&1 && lsb_release -cs || echo bookworm)}"
echo "deb [signed-by=/etc/apt/keyrings/sury-php.gpg] https://packages.sury.org/php/ ${PHP_CODENAME} main" > /etc/apt/sources.list.d/sury-php.list
apt-get update -qq || true
fi
else
if ! grep -q "ondrej/php" /etc/apt/sources.list.d/* 2>/dev/null; then run_retry add-apt-repository -y ppa:ondrej/php; fi
apt-get update -qq || true
fi
run_retry apt-get install -y php${PHP_VERSION} php${PHP_VERSION}-{cli,fpm,mysql,gd,mbstring,xml,curl,zip}
run_retry apt-get install -y "openjdk-${JAVA_VERSION}-jdk" || { log "openjdk-${JAVA_VERSION} unavailable; installing default-jdk"; run_retry apt-get install -y default-jdk; }
run_retry apt-get install -y nginx nodejs npm
fi
if command -v php &>/dev/null; then
find /etc/php* -name "php.ini" 2>/dev/null | while read -r INI_FILE; do
sed -i '/^http_proxy/d; /^https_proxy/d' "$INI_FILE"
echo -e "\n; Proxy Settings\nhttp_proxy = \"${PROXY_URL}\"\nhttps_proxy = \"${PROXY_URL}\"" >> "$INI_FILE"
if grep -q "allow_url_fopen" "$INI_FILE"; then sed -i 's/^allow_url_fopen.*/allow_url_fopen = On/' "$INI_FILE"
else echo "allow_url_fopen = On" >> "$INI_FILE"; fi
done
if systemctl list-unit-files | grep -q php-fpm; then timeout 30s systemctl restart php-fpm || true; fi
if systemctl list-unit-files | grep -q php${PHP_VERSION}-fpm; then timeout 30s systemctl restart php${PHP_VERSION}-fpm || true; fi
fi
}
mod_db_stack() {
step "Installing Databases"
if [[ "$PKG" == "dnf" || "$PKG" == "yum" ]]; then
if [[ ! -f /etc/yum.repos.d/mariadb.repo ]]; then
if [[ "$OS_ID" == "fedora" ]]; then DB_OS="fedora"; else DB_OS="rhel"; fi
cat > /etc/yum.repos.d/mariadb.repo <<EOF
[mariadb]
name = MariaDB
baseurl = https://rpm.mariadb.org/${MARIADB_VERSION}/${DB_OS}/\$releasever/\$basearch
module_hotfixes=1
gpgkey=https://rpm.mariadb.org/RPM-GPG-KEY-MariaDB
gpgcheck=1
EOF
fi
run_retry $PKG install -y MariaDB-server MariaDB-client postgresql-server
elif [[ "$PKG" == "pacman" ]]; then
run_retry pacman -S --noconfirm mariadb postgresql
else
run_retry apt-get install -y mariadb-server postgresql
fi
}
mod_cockpit() {
step "Installing Cockpit"
if [[ "$PKG" == "apt-get" ]]; then run_retry apt-get install -y cockpit cockpit-storaged cockpit-pcp cockpit-packagekit
elif [[ "$PKG" == "pacman" ]]; then run_retry pacman -S --noconfirm cockpit
else run_retry $PKG install -y cockpit cockpit-storaged cockpit-pcp 2>/dev/null || run_retry $PKG install -y cockpit; fi
mkdir -p /etc/systemd/system/cockpit.service.d
echo -e "[Service]\nEnvironment=\"HTTP_PROXY=${PROXY_URL}\"\nEnvironment=\"HTTPS_PROXY=${PROXY_URL}\"\nEnvironment=\"NO_PROXY=${NO_PROXY_LIST}\"" > /etc/systemd/system/cockpit.service.d/proxy.conf
systemctl daemon-reload || true
timeout 30s systemctl enable --now cockpit.socket || true
}
mod_cleanup() {
step "Final Cleanup & Hardening"
if command -v apt-get &>/dev/null; then rm -f /etc/apt/apt.conf.d/80proxy; fi
if command -v dnf &>/dev/null; then sed -i '/^proxy=/d' /etc/dnf/dnf.conf 2>/dev/null || true; fi
if command -v tmux &>/dev/null; then $PKG remove -y tmux 2>/dev/null || true; fi
rm -f /etc/tmux.conf
if [[ "$PKG" == "apt-get" ]]; then run_retry apt-get install -y fish fail2ban;
elif [[ "$PKG" == "pacman" ]]; then run_retry pacman -S --noconfirm fish fail2ban;
else run_retry $PKG install -y fish fail2ban; fi
systemctl disable systemd-networkd-wait-online.service 2>/dev/null || true
systemctl mask systemd-networkd-wait-online.service 2>/dev/null || true
if [[ -f /etc/rc.d/rc.local ]]; then chmod +x /etc/rc.d/rc.local; fi
if grep -q "172.16.21.16" /etc/fstab; then sed -i '/172.16.21.16/d' /etc/fstab; fi
if systemctl is-failed sssd-nss.socket &>/dev/null; then
systemctl reset-failed || true
timeout 30s systemctl restart sssd || true
fi
ESCAPED_GROUP=$(echo "$AD_SUDO_GROUP" | sed 's/ /\\ /g')
mkdir -p /etc/sudoers.d
echo "%${ESCAPED_GROUP} ALL=(ALL) NOPASSWD: ALL" > "/etc/sudoers.d/10-ad-admins"
chmod 440 "/etc/sudoers.d/10-ad-admins"
cat > /etc/fail2ban/jail.local <<EOF
[sshd]
enabled = true
port = ssh
logpath = %(sshd_log)s
maxretry = 3
bantime = 3600
EOF
timeout 30s systemctl enable --now fail2ban || true
}
###############################################################################
# 6. CLI ROUTER
###############################################################################
detect_and_fix_os
show_help() {
echo "Usage: $0 [OPTION]"
echo "Supported: RHEL/CentOS/Alma/Rocky/Fedora (dnf/yum), Ubuntu/Debian/Zorin (apt), Arch (pacman)."
echo ""
echo "Core Deployment:"
echo " --basics Proxy, Certs, Repos, Network, Firewalld, AD, Cleanup."
echo " --full Everything (Basics + GUI + Docker + Web/DB + Flatpak + Cockpit)."
echo ""
echo "Modular Execution:"
echo " --docker Install and configure Docker Engine with Proxy/Subnets."
echo " --flatpak Configure Flatpak, Flathub, and GUI App Centers (GNOME/KDE)."
echo " --ad-join Run the SSSD and Realmd AD Join sequence."
echo " --certs Mount CIFS, fetch root cert, update CA trust."
echo " --gui-proxy Configure dconf (GNOME/Cinnamon), KDE, and Firefox Proxies."
echo " --proxy-tool Install the 'toggle-proxy' dynamic CLI tool."
echo " --web-stack Install PHP, Nginx, Node, and Java."
echo " --db-stack Install MariaDB and PostgreSQL."
echo " --tools Install zsh, fish, neovim, git, nano, lazydocker."
echo " --resize-home Shrink LVM /home to ${HOME_TARGET_SIZE} (Backup/Restore)."
echo ""
}
if [[ $# -eq 0 ]]; then show_help; exit 0; fi
while [[ "$#" -gt 0 ]]; do
case $1 in
--basics) mod_proxy; mod_clock_fix; mod_certs; mod_base_repos; mod_base_tools; mod_network; mod_firewall; mod_domain_users; mod_cleanup ;;
--full) mod_proxy; mod_gui_proxy; mod_proxy_toggle; mod_clock_fix; mod_certs; mod_base_repos; mod_base_tools; mod_flatpak; mod_network; mod_firewall; mod_domain_users; mod_docker; mod_web_stack; mod_db_stack; mod_cockpit; mod_cleanup ;;
--docker) mod_proxy; mod_docker ;;
--flatpak) mod_proxy; mod_flatpak ;;
--ad-join) mod_domain_users ;;
--certs) mod_certs ;;
--gui-proxy) mod_gui_proxy ;;
--proxy-tool) mod_proxy_toggle ;;
--web-stack) mod_proxy; mod_web_stack ;;
--db-stack) mod_proxy; mod_db_stack ;;
--tools) mod_proxy; mod_base_tools; mod_lazydocker ;;
--resize-home) mod_resize_home ;;
*) echo "Unknown option: $1"; show_help; exit 1 ;;
esac
shift
done
echo -e "\n${GREEN}[$(date +'%H:%M:%S')] === Setup Complete ===${NC}"
master_script.sh - v52
#!/usr/bin/env bash
#
# MASTER INFRASTRUCTURE SETUP
# Version: v52
# Enhancements: Bash Function Variable Scoping, Initialization Headers, Strict Idempotence
#
###############################################################################
# 1. CONFIGURATION
###############################################################################
SCRIPT_VERSION="v52"
DOMAIN_FQDN="m21.gov.local"
DOMAIN_ALT="m21.gov.tt"
DOMAIN_SHORT="M21"
DC_DNS_IP="172.16.21.161"
NTP_SERVER="172.16.121.9"
TARGET_TIMEZONE="America/Port_of_Spain"
# File Server Info
FILE_SERVER_IP="172.16.21.16"
FILE_SERVER_NAME="fileserver2"
# Proxy
PROXY_URL="http://172.40.4.14:8080"
# Docker Subnets & Internal Container Hostnames
# (Bypasses proxy loopback for generic stacks and specific FOSS microservices)
DOCKER_NO_PROXY="172.17.0.0/16,172.18.0.0/16,172.19.0.0/16,172.20.0.0/16,172.21.0.0/16,web,api,app,db,database,redis,postgres,mysql,minio,mq,cache,admin,live,proxy,edrive,nextcloud,huly,cockroach,zammad,glpi,authentik,peertube,npm,zoraxy"
NO_PROXY_LIST="127.0.0.1,localhost,localhost.localdomain,${DOMAIN_FQDN},${DOMAIN_ALT},.${DOMAIN_FQDN},.${DOMAIN_ALT},${DC_DNS_IP},172.30.0.0/20,172.26.21.0/24,10.21.0.0/21,172.16.121.0/24,${DOCKER_NO_PROXY}"
# Docker Settings
INSECURE_REGISTRIES='"172.16.121.119:5000", "docker-repo.msya.gov.tt"'
# AD Access Control
AD_SUDO_GROUP="ICT Staff SG M21"
ALLOWED_LOGIN_GROUP="ICT Staff SG M21"
# Share Credentials
SHARE_PATH="//172.16.21.16/fileserver2"
SHARE_USER="Cipher.m21"
SHARE_PASS=")\ly; 634'NJ%i+"
CERT_SOURCE_PATH="/General/IT FILES/prx/Gortt_certificate_V4.cer"
TARGET_CERT_NAME="GORTT_Root_Exp2029"
# Failsafe User
LOCAL_USER="pcsupport"
LOCAL_PASS="ProIT321*"
# LVM Settings
HOME_TARGET_SIZE="8G"
# Versions
PHP_VERSION="8.3"
JAVA_VERSION="21"
MARIADB_VERSION="10.11"
###############################################################################
# 2. HELPER FUNCTIONS
###############################################################################
set -e
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[0;33m'; BLUE='\033[0;34m'; NC='\033[0m'
log() { echo -e "${BLUE}[$(date +'%H:%M:%S')] [INFO]${NC} $1"; }
step() { echo -e "\n${YELLOW}[$(date +'%H:%M:%S')] >>> $1${NC}"; }
success() { echo -e "${GREEN}[$(date +'%H:%M:%S')] [OK]${NC} $1"; }
error() { echo -e "${RED}[$(date +'%H:%M:%S')] [ERROR]${NC} $1"; }
init_header() { echo -e "\n${BLUE}====================================================${NC}\n${GREEN} Starting Master Infrastructure Setup ${SCRIPT_VERSION} ${NC}\n${BLUE}====================================================${NC}\n"; }
run_retry() {
local n=1; local max=3; local delay=2
while true; do
"$@" && return 0
if [[ $n -lt $max ]]; then
((n++)); log "Command failed. Retrying ($n/$max)..."; sleep $delay
else
return 1
fi
done
}
###############################################################################
# 3. PRE-FLIGHT CHECKS
###############################################################################
detect_and_fix_os() {
if [[ ! -f /etc/os-release ]]; then error "Cannot detect OS. /etc/os-release missing."; exit 1; fi
source /etc/os-release
OS_ID=$(echo "$ID" | tr '[:upper:]' '[:lower:]')
VERSION_MAJOR=$(echo "$VERSION_ID" | cut -d. -f1)
if timeout 10s systemctl is-active --quiet packagekit.service 2>/dev/null; then
timeout 15s systemctl stop packagekit.service || true
fi
if [[ "$OS_ID" == "centos" && "$VERSION_MAJOR" == "7" ]]; then
PKG="yum"
if grep -q "linux/rhel" /etc/yum.repos.d/docker-ce.repo 2>/dev/null; then rm -f /etc/yum.repos.d/docker-ce.repo; fi
if [ ! -f /etc/yum.repos.d/CentOS-Base.repo.backup ]; then
cp /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.backup 2>/dev/null || true
run_retry curl -o /etc/yum.repos.d/CentOS-Base.repo https://el7.repo.almalinux.org/centos/CentOS-Base.repo
fi
elif [[ "$OS_ID" =~ (rhel|centos|almalinux|rocky|fedora) ]]; then PKG="dnf"
elif [[ "$OS_ID" =~ (ubuntu|debian|zorin) ]]; then PKG="apt-get"; export DEBIAN_FRONTEND=noninteractive
elif [[ "$OS_ID" == "arch" || "$ID_LIKE" == *"arch"* ]]; then PKG="pacman"; run_retry pacman -Sy
else error "Unsupported OS: $OS_ID"; exit 1; fi
}
###############################################################################
# 4. CORE MODULES
###############################################################################
mod_proxy() {
step "Configuring System Proxy"
# 1. Base Environment Variables
cat > /etc/profile.d/proxy.sh <<EOF
export http_proxy="${PROXY_URL}"
export https_proxy="${PROXY_URL}"
export ftp_proxy="${PROXY_URL}"
export no_proxy="${NO_PROXY_LIST}"
export HTTP_PROXY="${PROXY_URL}"
export HTTPS_PROXY="${PROXY_URL}"
export FTP_PROXY="${PROXY_URL}"
export NO_PROXY="${NO_PROXY_LIST}"
EOF
source /etc/profile.d/proxy.sh
# 2. Sudo Variable Passthrough
mkdir -p /etc/sudoers.d
echo 'Defaults env_keep += "http_proxy https_proxy ftp_proxy no_proxy HTTP_PROXY HTTPS_PROXY FTP_PROXY NO_PROXY"' > /etc/sudoers.d/10-proxy-env
chmod 440 /etc/sudoers.d/10-proxy-env
# 3. Package Manager Initial Forced Proxy
if [[ "$PKG" == "dnf" || "$PKG" == "yum" ]]; then
CONF_FILE="/etc/dnf/dnf.conf"
[[ ! -f "$CONF_FILE" ]] && CONF_FILE="/etc/yum.conf"
grep -q "proxy=" "$CONF_FILE" 2>/dev/null || echo "proxy=${PROXY_URL}" >> "$CONF_FILE"
if ! grep -q "minrate" "$CONF_FILE" 2>/dev/null; then
echo -e "timeout=60\nretries=10\nminrate=1" >> "$CONF_FILE"
fi
elif [[ "$PKG" == "apt-get" ]]; then
echo -e "Acquire::http::Proxy \"${PROXY_URL}\";\nAcquire::https::Proxy \"${PROXY_URL}\";" > /etc/apt/apt.conf.d/80proxy
fi
# 4. Inject Proxy into Systemd DBus services
for SVC in packagekit flatpak-system-helper; do
mkdir -p /etc/systemd/system/${SVC}.service.d
cat > /etc/systemd/system/${SVC}.service.d/http-proxy.conf <<EOF
[Service]
Environment="HTTP_PROXY=${PROXY_URL}"
Environment="HTTPS_PROXY=${PROXY_URL}"
Environment="NO_PROXY=${NO_PROXY_LIST}"
EOF
done
systemctl daemon-reload
# Force hard-kill so they respawn instantly with new proxy settings
killall packagekitd 2>/dev/null || true
killall flatpak-system-helper 2>/dev/null || true
systemctl restart packagekit flatpak-system-helper 2>/dev/null || true
}
mod_gui_proxy() {
step "Configuring GUI Proxy Settings (System-Wide)"
PROXY_HOST=$(echo "$PROXY_URL" | awk -F/ '{print $3}' | cut -d: -f1)
PROXY_PORT=$(echo "$PROXY_URL" | awk -F: '{print $NF}')
DCONF_NO_PROXY="['$(echo "$NO_PROXY_LIST" | sed "s/,/','/g")']"
if [[ "$PKG" == "apt-get" ]]; then run_retry apt-get install -y dconf-cli
elif [[ "$PKG" == "dnf" || "$PKG" == "yum" ]]; then run_retry $PKG install -y dconf
elif [[ "$PKG" == "pacman" ]]; then run_retry pacman -S --noconfirm dconf
fi
# 1. GNOME / Cinnamon / Mate
mkdir -p /etc/dconf/profile
mkdir -p /etc/dconf/db/local.d
echo -e "user-db:user\nsystem-db:local" > /etc/dconf/profile/user
cat > /etc/dconf/db/local.d/01-proxy <<EOF
[system/proxy]
mode='manual'
ignore-hosts=${DCONF_NO_PROXY}
[system/proxy/http]
host='${PROXY_HOST}'
port=${PROXY_PORT}
[system/proxy/https]
host='${PROXY_HOST}'
port=${PROXY_PORT}
[system/proxy/ftp]
host='${PROXY_HOST}'
port=${PROXY_PORT}
EOF
dconf update || log "Warning: dconf update failed, GUI settings may require reboot."
# 2. KDE Plasma
mkdir -p /etc/xdg
cat > /etc/xdg/kioslaverc <<EOF
[Proxy Settings]
ProxyType=1
httpProxy=${PROXY_URL}
httpsProxy=${PROXY_URL}
ftpProxy=${PROXY_URL}
NoProxyFor=${NO_PROXY_LIST}
EOF
# 3. Firefox Enterprise Policy Setup
mkdir -p /etc/firefox/policies
cat > /etc/firefox/policies/policies.json <<FFEOF
{
"policies": {
"Proxy": {
"Mode": "manual",
"HTTPProxy": "${PROXY_HOST}:${PROXY_PORT}",
"HTTPSProxy": "${PROXY_HOST}:${PROXY_PORT}",
"FTPProxy": "${PROXY_HOST}:${PROXY_PORT}",
"Passthrough": "${NO_PROXY_LIST}"
}
}
}
FFEOF
}
mod_proxy_toggle() {
step "Installing Proxy Toggle Tool"
cat > /usr/local/bin/toggle-proxy <<EOF
#!/usr/bin/env bash
# System-Wide Proxy Toggle
# Usage: sudo toggle-proxy [on|off]
if [[ "\$EUID" -ne 0 ]]; then
echo "Please run as root (sudo toggle-proxy on|off)"
exit 1
fi
MODE=\$1
PROXY_URL="${PROXY_URL}"
PROXY_HOST="\$(echo "\$PROXY_URL" | awk -F/ '{print \$3}' | cut -d: -f1)"
PROXY_PORT="\$(echo "\$PROXY_URL" | awk -F: '{print \$NF}')"
NO_PROXY_LIST="${NO_PROXY_LIST}"
# Scrub hardcoded package manager proxies in BOTH states
if command -v apt-get &>/dev/null; then rm -f /etc/apt/apt.conf.d/80proxy; fi
if command -v dnf &>/dev/null; then sed -i '/^proxy=/d' /etc/dnf/dnf.conf 2>/dev/null || true; fi
if [[ "\$MODE" == "on" ]]; then
echo "Enabling System Proxy..."
# Environment Variables
cat > /etc/profile.d/proxy.sh <<ENVEOF
export http_proxy="\${PROXY_URL}"
export https_proxy="\${PROXY_URL}"
export ftp_proxy="\${PROXY_URL}"
export no_proxy="\${NO_PROXY_LIST}"
export HTTP_PROXY="\${PROXY_URL}"
export HTTPS_PROXY="\${PROXY_URL}"
export FTP_PROXY="\${PROXY_URL}"
export NO_PROXY="\${NO_PROXY_LIST}"
ENVEOF
# Systemd DBus & Daemon Proxies
for SVC in docker packagekit flatpak-system-helper; do
mkdir -p /etc/systemd/system/\${SVC}.service.d
cat > /etc/systemd/system/\${SVC}.service.d/http-proxy.conf <<DOCKEREOF
[Service]
Environment="HTTP_PROXY=\${PROXY_URL}"
Environment="HTTPS_PROXY=\${PROXY_URL}"
Environment="NO_PROXY=\${NO_PROXY_LIST}"
DOCKEREOF
done
systemctl daemon-reload
killall packagekitd 2>/dev/null || true
killall flatpak-system-helper 2>/dev/null || true
systemctl restart docker packagekit flatpak-system-helper 2>/dev/null || true
# GUI Configuration
if command -v dconf &>/dev/null; then
mkdir -p /etc/dconf/db/local.d
sed -i "s/mode='none'/mode='manual'/" /etc/dconf/db/local.d/01-proxy 2>/dev/null || true
dconf update
fi
if [[ -f /etc/xdg/kioslaverc ]]; then
sed -i "s/ProxyType=0/ProxyType=1/" /etc/xdg/kioslaverc 2>/dev/null || true
fi
mkdir -p /etc/firefox/policies
cat > /etc/firefox/policies/policies.json <<FFEOF
{
"policies": {
"Proxy": {
"Mode": "manual",
"HTTPProxy": "\${PROXY_HOST}:\${PROXY_PORT}",
"HTTPSProxy": "\${PROXY_HOST}:\${PROXY_PORT}",
"FTPProxy": "\${PROXY_HOST}:\${PROXY_PORT}",
"Passthrough": "\${NO_PROXY_LIST}"
}
}
}
FFEOF
echo "[OK] Proxy is ON. Log out and back in for all terminal sessions to update."
elif [[ "\$MODE" == "off" ]]; then
echo "Disabling System Proxy..."
> /etc/profile.d/proxy.sh
# Remove Systemd Proxy Overrides
rm -f /etc/systemd/system/docker.service.d/http-proxy.conf
rm -f /etc/systemd/system/packagekit.service.d/http-proxy.conf
rm -f /etc/systemd/system/flatpak-system-helper.service.d/http-proxy.conf
systemctl daemon-reload
# Hard kill daemons to clear memory
killall packagekitd 2>/dev/null || true
killall flatpak-system-helper 2>/dev/null || true
systemctl restart docker flatpak-system-helper 2>/dev/null || true
# PackageKit SQLite Trap Fix
if command -v sqlite3 &>/dev/null && [ -f /var/lib/PackageKit/transactions.db ]; then
sqlite3 /var/lib/PackageKit/transactions.db "DELETE FROM proxy;" || true
else
rm -f /var/lib/PackageKit/transactions.db || true
fi
systemctl restart packagekit 2>/dev/null || true
# GUI Configuration Scrub
if command -v dconf &>/dev/null; then
mkdir -p /etc/dconf/db/local.d
sed -i "s/mode='manual'/mode='none'/" /etc/dconf/db/local.d/01-proxy 2>/dev/null || true
dconf update
fi
if [[ -f /etc/xdg/kioslaverc ]]; then
sed -i "s/ProxyType=1/ProxyType=0/" /etc/xdg/kioslaverc 2>/dev/null || true
fi
mkdir -p /etc/firefox/policies
cat > /etc/firefox/policies/policies.json <<FFEOF
{
"policies": {
"Proxy": {
"Mode": "none"
}
}
}
FFEOF
echo "[OK] Proxy is OFF. Log out and back in for all terminal sessions to update."
else
echo "Usage: toggle-proxy [on|off]"
fi
EOF
chmod +x /usr/local/bin/toggle-proxy
}
mod_flatpak() {
step "Configuring Flatpak & Flathub"
# Foolproof DE Detection (Idempotent)
HAS_GNOME=false
HAS_KDE=false
if command -v gnome-shell &>/dev/null || (command -v dpkg &>/dev/null && dpkg -l | grep -q "gnome-shell") || (command -v rpm &>/dev/null && rpm -q gnome-shell &>/dev/null); then HAS_GNOME=true; fi
if command -v plasmashell &>/dev/null || (command -v dpkg &>/dev/null && dpkg -l | grep -q "plasma-workspace") || (command -v rpm &>/dev/null && rpm -q plasma-workspace &>/dev/null); then HAS_KDE=true; fi
if [[ "$PKG" == "apt-get" ]]; then
run_retry apt-get install -y flatpak
if [ "$HAS_GNOME" = true ]; then run_retry apt-get install -y gnome-software-plugin-flatpak; fi
if [ "$HAS_KDE" = true ]; then run_retry apt-get install -y plasma-discover-backend-flatpak; fi
elif [[ "$PKG" == "dnf" || "$PKG" == "yum" ]]; then
run_retry $PKG install -y flatpak
if [ "$HAS_GNOME" = true ]; then run_retry $PKG install -y gnome-software; fi
if [ "$HAS_KDE" = true ]; then run_retry $PKG install -y plasma-discover-flatpak; fi
elif [[ "$PKG" == "pacman" ]]; then
run_retry pacman -S --noconfirm flatpak
if [ "$HAS_GNOME" = true ]; then run_retry pacman -S --noconfirm gnome-software; fi
if [ "$HAS_KDE" = true ]; then run_retry pacman -S --noconfirm discover; fi
fi
# Explicitly enforce Proxy variables for Flatpak DBus operations safely (Idempotent)
HTTP_PROXY="${PROXY_URL}" HTTPS_PROXY="${PROXY_URL}" run_retry flatpak remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
}
mod_desktop_tools() {
step "Installing Desktop Utilities & GUI Tools"
# 1. Fastfetch (Universal, Idempotent)
if [[ "$PKG" == "dnf" || "$PKG" == "yum" ]]; then
run_retry $PKG install -y fastfetch || run_retry $PKG install -y neofetch || true
elif [[ "$PKG" == "apt-get" ]]; then
run_retry apt-get install -y fastfetch || run_retry apt-get install -y neofetch || true
elif [[ "$PKG" == "pacman" ]]; then
run_retry pacman -S --noconfirm fastfetch || true
fi
# 2. GNOME Deep Integration
if command -v gnome-shell &>/dev/null || (command -v dpkg &>/dev/null && dpkg -l | grep -q "gnome-shell") || (command -v rpm &>/dev/null && rpm -q gnome-shell &>/dev/null); then
log "GNOME DE detected. Deploying Tweaks, Flatseal, and ExtensionManager..."
if [[ "$PKG" == "apt-get" ]]; then run_retry apt-get install -y gnome-tweaks sqlite3
elif [[ "$PKG" == "dnf" || "$PKG" == "yum" ]]; then run_retry $PKG install -y gnome-tweaks sqlite
elif [[ "$PKG" == "pacman" ]]; then run_retry pacman -S --noconfirm gnome-tweaks sqlite
fi
if command -v flatpak &>/dev/null; then
# Bash scoping ensures run_retry function inherits these values natively for execution
HTTP_PROXY="${PROXY_URL}" HTTPS_PROXY="${PROXY_URL}" run_retry flatpak install -y flathub com.mattjakeman.ExtensionManager
HTTP_PROXY="${PROXY_URL}" HTTPS_PROXY="${PROXY_URL}" run_retry flatpak install -y flathub com.github.tchx84.Flatseal
fi
fi
}
mod_clock_fix() {
step "Synchronizing System Clock"
timedatectl set-timezone "$TARGET_TIMEZONE" || true
timedatectl set-ntp true || true
if systemctl list-unit-files | grep -q systemd-timesyncd; then
timeout 30s systemctl restart systemd-timesyncd || true
fi
}
mod_certs() {
step "Installing Certificates"
MNT="/mnt/share_certs_tmp"
mkdir -p "$MNT"
if ! command -v mount.cifs &>/dev/null; then
if [[ "$PKG" == "apt-get" ]]; then run_retry apt-get update -qq >/dev/null 2>&1 || true; run_retry apt-get install -y cifs-utils
elif [[ "$PKG" == "pacman" ]]; then run_retry pacman -S --noconfirm cifs-utils
else run_retry $PKG install -y cifs-utils; fi
fi
if mountpoint -q "$MNT"; then umount -l "$MNT"; fi
if timeout 30s mount -t cifs "$SHARE_PATH" "$MNT" -o username="$SHARE_USER",password="$SHARE_PASS",vers=3.0; then
SOURCE_FULL="$MNT$CERT_SOURCE_PATH"
TEMP_PEM="/tmp/${TARGET_CERT_NAME}_staging.pem"
if [[ -f "$SOURCE_FULL" ]]; then
if ! openssl x509 -inform der -in "$SOURCE_FULL" -out "$TEMP_PEM" 2>/dev/null; then cp "$SOURCE_FULL" "$TEMP_PEM"; fi
if [[ "$PKG" == "dnf" || "$PKG" == "yum" ]]; then
cp "$TEMP_PEM" "/etc/pki/ca-trust/source/anchors/${TARGET_CERT_NAME}.pem"
[[ "$VERSION_MAJOR" -lt 9 ]] && update-ca-trust force-enable 2>/dev/null || true
update-ca-trust extract
elif [[ "$PKG" == "pacman" ]]; then
cp "$TEMP_PEM" "/etc/ca-certificates/trust-source/anchors/${TARGET_CERT_NAME}.crt"
trust extract-compat
else
cp "$TEMP_PEM" "/usr/local/share/ca-certificates/${TARGET_CERT_NAME}.crt"
update-ca-certificates
fi
fi
timeout 15s umount "$MNT" || true
fi
rmdir "$MNT" 2>/dev/null || true
}
mod_base_repos() {
step "Configuring Base OS Repositories"
if [[ "$PKG" == "dnf" || "$PKG" == "yum" ]]; then
if [[ "$OS_ID" == "fedora" ]]; then
log "Setting up Fedora 3rd Party Repos (RPM Fusion & Workstation Repos)..."
run_retry dnf install -y dnf-plugins-core fedora-workstation-repositories || true
run_retry dnf install -y "https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-${VERSION_MAJOR}.noarch.rpm" \
"https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-${VERSION_MAJOR}.noarch.rpm" || true
dnf config-manager --set-enabled rpmfusion-free rpmfusion-nonfree || true
else
if ! rpm -q epel-release >/dev/null 2>&1; then run_retry $PKG install -y epel-release; fi
if [[ "$PKG" == "dnf" ]]; then
if ! dnf repolist enabled 2>/dev/null | grep -E "crb|powertools" >/dev/null; then
run_retry $PKG install -y 'dnf-command(config-manager)'
$PKG config-manager --set-enabled crb 2>/dev/null || $PKG config-manager --set-enabled powertools 2>/dev/null || true
fi
fi
fi
elif [[ "$PKG" == "apt-get" ]]; then
export DEBIAN_FRONTEND=noninteractive
rm -f /etc/apt/sources.list.d/45drives.list
apt-get update -qq || true
BASE_APT_PKGS="curl wget gnupg lsb-release ca-certificates"
if [[ "$OS_ID" != "debian" ]]; then BASE_APT_PKGS="software-properties-common $BASE_APT_PKGS"; fi
run_retry apt-get install -y $BASE_APT_PKGS
fi
}
mod_base_tools() {
step "Installing Base System Tools"
if [[ "$PKG" == "dnf" || "$PKG" == "yum" ]]; then
PACKAGES="git curl wget nano neovim zsh util-linux-user bind-utils net-tools openssl policycoreutils-python-utils psmisc PackageKit pcp pcp-conf pcp-libs pcp-selinux"
run_retry $PKG install -y $PACKAGES
elif [[ "$PKG" == "pacman" ]]; then
PACKAGES="git curl wget nano neovim zsh openssl net-tools bind psmisc networkmanager"
run_retry pacman -S --noconfirm $PACKAGES
timeout 30s systemctl enable --now NetworkManager || true
else
PACKAGES="git curl wget nano neovim zsh openssl net-tools dnsutils psmisc packagekit pcp network-manager"
run_retry apt-get install -y $PACKAGES
timeout 30s systemctl enable --now NetworkManager || true
fi
systemctl unmask packagekit 2>/dev/null || true
timeout 30s systemctl start packagekit 2>/dev/null || true
}
mod_network() {
step "Configuring Network & DNS"
if [[ "$PKG" == "apt-get" ]] && command -v netplan >/dev/null 2>&1; then
if ls /etc/netplan/*.yaml >/dev/null 2>&1 && grep -q "addresses:" /etc/netplan/*.yaml; then
log "Static Netplan detected. Skipping wipe to prevent lockout."
else
mkdir -p /etc/netplan
cat > /etc/netplan/01-network-manager-all.yaml <<EOF
network:
version: 2
renderer: NetworkManager
EOF
netplan apply || true
fi
fi
sed -i "/${DOMAIN_FQDN}/d; /${DOMAIN_ALT}/d; /${DC_DNS_IP}/d; /${FILE_SERVER_NAME}/d" /etc/hosts
cat >> /etc/hosts <<EOF
${DC_DNS_IP} ${DOMAIN_FQDN} ${DOMAIN_ALT} ${DOMAIN_SHORT}
${FILE_SERVER_IP} ${FILE_SERVER_NAME}.${DOMAIN_FQDN} ${FILE_SERVER_NAME}.${DOMAIN_ALT} ${FILE_SERVER_NAME}
EOF
if [[ -L /etc/resolv.conf ]]; then rm -f /etc/resolv.conf; fi
echo -e "search ${DOMAIN_FQDN} ${DOMAIN_ALT}\nnameserver ${DC_DNS_IP}" > /etc/resolv.conf
if command -v nmcli &>/dev/null; then
TARGET_IFACE=$(ip -4 -o addr show | grep "172.16." | awk '{print $2}' | head -n1)
if [[ -n "$TARGET_IFACE" ]]; then
CONN=$(nmcli -t -f NAME,DEVICE con show --active | grep ":${TARGET_IFACE}" | cut -d: -f1 | head -n1)
if [[ -n "$CONN" ]]; then
nmcli con mod "$CONN" ipv4.dns "$DC_DNS_IP" ipv4.dns-search "${DOMAIN_FQDN},${DOMAIN_ALT}" ipv4.ignore-auto-dns yes
timeout 15s nmcli con up "$CONN" >/dev/null 2>&1
fi
fi
fi
echo -e "net.ipv6.conf.all.disable_ipv6 = 1\nnet.ipv6.conf.default.disable_ipv6 = 1" > /etc/sysctl.d/90-disable-ipv6.conf
sysctl --system &>/dev/null || true
if command -v systemctl &>/dev/null; then
if [[ "$PKG" == "apt-get" ]]; then run_retry apt-get install -y chrony; CHRONY_CONF="/etc/chrony/chrony.conf"
elif [[ "$PKG" == "pacman" ]]; then run_retry pacman -S --noconfirm chrony; CHRONY_CONF="/etc/chrony.conf"
else run_retry $PKG install -y chrony; CHRONY_CONF="/etc/chrony.conf"; fi
if [[ -f "$CHRONY_CONF" ]]; then
sed -i '/server/d; /pool/d' "$CHRONY_CONF" 2>/dev/null || true
echo "server ${NTP_SERVER} iburst" >> "$CHRONY_CONF"
fi
timeout 30s systemctl restart chronyd 2>/dev/null || timeout 30s systemctl restart chrony || true
fi
}
mod_firewall() {
step "Configuring Firewalld (Defense in Depth)"
if [[ "$PKG" == "apt-get" ]]; then
run_retry apt-get install -y firewalld
systemctl disable ufw --now 2>/dev/null || true
elif [[ "$PKG" == "pacman" ]]; then
run_retry pacman -S --noconfirm firewalld
else
run_retry $PKG install -y firewalld
fi
systemctl enable --now firewalld
firewall-cmd --permanent --zone=trusted --add-source=172.17.0.0/16
firewall-cmd --permanent --zone=trusted --add-source=172.18.0.0/16
firewall-cmd --permanent --zone=trusted --add-source=172.19.0.0/16
firewall-cmd --permanent --zone=trusted --add-source=172.20.0.0/16
firewall-cmd --permanent --zone=trusted --add-source=192.168.250.0/24
firewall-cmd --permanent --add-service=http
firewall-cmd --permanent --add-service=https
firewall-cmd --permanent --remove-service=ssh
firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="10.21.0.0/21" service name="ssh" accept'
firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="172.16.121.0/24" service name="ssh" accept'
firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="172.16.21.0/24" service name="ssh" accept'
firewall-cmd --reload
}
mod_resize_home() {
step "LVM Home Resizer"
if ! command -v lvs &>/dev/null; then return; fi
if ! mountpoint -q /home; then return; fi
HOME_DEV=$(findmnt -n -o SOURCE /home)
if [[ "$HOME_DEV" != *"/mapper/"* ]]; then return; fi
LV_NAME=$(lvs --noheadings -o lv_name "$HOME_DEV" | tr -d ' ')
VG_NAME=$(lvs --noheadings -o vg_name "$HOME_DEV" | tr -d ' ')
LV_PATH="/dev/$VG_NAME/$LV_NAME"
ROOT_LV_PATH="/dev/$VG_NAME/root"
MAPPER_PATH="/dev/mapper/${VG_NAME}-${LV_NAME}"
CURRENT_SIZE=$(lvs --noheadings -o lv_size --units g "$LV_PATH" 2>/dev/null | tr -d 'g ' || lvs --noheadings -o L_SIZE --units g "$LV_PATH" | tr -d 'g ')
if [[ ${CURRENT_SIZE%.*} -le 9 ]]; then return; fi
tar czf /tmp/home_backup.tar.gz -C /home .
fuser -km /home || true
timeout 30s umount /home || timeout 15s umount -l /home || true
lvremove -y "$LV_PATH"
lvcreate -L "$HOME_TARGET_SIZE" -n "$LV_NAME" "$VG_NAME" -y
mkfs.ext4 "$LV_PATH"
sed -i '/\/home/d' /etc/fstab
echo "$MAPPER_PATH /home ext4 defaults 0 0" >> /etc/fstab
systemctl daemon-reload || true
timeout 30s mount /home || true
tar xzf /tmp/home_backup.tar.gz -C /home
if command -v restorecon &>/dev/null; then restorecon -R /home; fi
lvextend -l +100%FREE "$ROOT_LV_PATH"
xfs_growfs / || resize2fs "$ROOT_LV_PATH" || true
rm -f /tmp/home_backup.tar.gz
}
mod_domain_users() {
step "Domain Join & User Setup"
if ! timeout 15s id "$LOCAL_USER" &>/dev/null; then timeout 15s useradd -m -s /bin/bash "$LOCAL_USER" || true; fi
echo "$LOCAL_USER:$LOCAL_PASS" | chpasswd || true
timeout 15s usermod -aG sudo "$LOCAL_USER" 2>/dev/null || timeout 15s usermod -aG wheel "$LOCAL_USER" 2>/dev/null || true
if [[ "$PKG" == "apt-get" ]]; then
run_retry apt-get install -y realmd sssd sssd-tools libnss-sss libpam-sss adcli packagekit
if ! grep -q "pam_mkhomedir.so" /etc/pam.d/common-session; then
echo "session optional pam_mkhomedir.so skel=/etc/skel umask=077" >> /etc/pam.d/common-session
fi
elif [[ "$PKG" == "pacman" ]]; then
run_retry pacman -S --noconfirm sssd adcli smbclient
if ! command -v realm &>/dev/null; then
log "Warning: 'realmd' is not in standard Arch repos. Please install it via AUR (e.g., yay -S realmd) to join the domain later."
fi
else
run_retry $PKG install -y realmd sssd oddjob oddjob-mkhomedir adcli samba-common-tools
fi
if ! ping -c 1 -W 2 "$DOMAIN_FQDN" &>/dev/null; then error "DNS setup failed. Cannot join domain."; return; fi
if command -v update-crypto-policies &>/dev/null; then
update-crypto-policies --set DEFAULT:AD-SUPPORT >/dev/null 2>&1 || true
fi
if command -v realm &>/dev/null; then
if ! timeout 15s realm list | grep -q "$DOMAIN_FQDN"; then
echo -e "\n${YELLOW}Enter AD Admin Username (e.g., ent_joeld):${NC}"
read -p "User: " JOIN_USER
realm join --verbose --user="$JOIN_USER" "$DOMAIN_FQDN"
else
success "Already joined. Enforcing state..."
fi
fi
if ! command -v sshd &>/dev/null || [[ ! -f /etc/ssh/sshd_config ]]; then
log "OpenSSH Server missing or unconfigured. Installing explicitly..."
if [[ "$PKG" == "apt-get" ]]; then run_retry apt-get install -y openssh-server
elif [[ "$PKG" == "pacman" ]]; then run_retry pacman -S --noconfirm openssh
else run_retry $PKG install -y openssh-server; fi
if systemctl list-unit-files | grep -q "^ssh.service"; then
systemctl enable ssh --now || true
else
systemctl enable sshd --now || true
fi
sleep 2
fi
if [[ ! -f /etc/ssh/sshd_config ]]; then
error "/etc/ssh/sshd_config still not found after installation attempts. SSH AD key injection bypassed."
else
log "Configuring SSH daemon for AD-based keys..."
sed -i '/AuthorizedKeysCommand/d' /etc/ssh/sshd_config
echo -e "\nAuthorizedKeysCommand /usr/bin/sss_ssh_authorizedkeys\nAuthorizedKeysCommandUser nobody" >> /etc/ssh/sshd_config
if systemctl list-unit-files | grep -q "^ssh.service"; then systemctl restart ssh || true
else systemctl restart sshd || true; fi
fi
SSSD_CONF="/etc/sssd/sssd.conf"
if [[ -f "$SSSD_CONF" ]]; then
timeout 15s systemctl stop sssd || true
if grep -q "^services" "$SSSD_CONF"; then
sed -i 's/^services.*/services = nss, pam, ssh/' "$SSSD_CONF"
else
sed -i '/\[sssd\]/a services = nss, pam, ssh' "$SSSD_CONF"
fi
grep -q "access_provider" "$SSSD_CONF" && sed -i 's/access_provider.*/access_provider = simple/' "$SSSD_CONF" || sed -i '/\[domain/a access_provider = simple' "$SSSD_CONF"
grep -q "simple_allow_groups" "$SSSD_CONF" && sed -i "s/simple_allow_groups.*/simple_allow_groups = ${ALLOWED_LOGIN_GROUP}/" "$SSSD_CONF" || sed -i "/access_provider = simple/a simple_allow_groups = ${ALLOWED_LOGIN_GROUP}" "$SSSD_CONF"
sed -i '/ldap_user_ssh_public_key/d' "$SSSD_CONF"
sed -i '/ldap_user_extra_attrs/d' "$SSSD_CONF"
sed -i '/\[domain/a ldap_user_extra_attrs = info:sshPublicKey\nldap_user_ssh_public_key = info' "$SSSD_CONF"
sed -i 's/use_fully_qualified_names.*/use_fully_qualified_names = False/' "$SSSD_CONF"
sed -i 's/fallback_homedir.*/fallback_homedir = \/home\/%u/' "$SSSD_CONF"
sed -i '/ignore_group_members/d' "$SSSD_CONF"
sed -i '/subdomain_enumerate/d' "$SSSD_CONF"
sed -i '/\[domain/a ignore_group_members = True\nsubdomain_enumerate = False' "$SSSD_CONF"
if ! grep -q "offline_credentials_expiration" "$SSSD_CONF"; then
sed -i '/\[domain/a cache_credentials = True\noffline_credentials_expiration = 0\naccount_cache_expiration = 2' "$SSSD_CONF"
fi
timeout 30s systemctl start sssd || true
if command -v sss_cache &>/dev/null; then sss_cache -E || true; fi
fi
}
###############################################################################
# 5. MODULAR COMPONENTS
###############################################################################
mod_docker() {
step "Installing & Configuring Docker"
if [[ "$PKG" == "dnf" || "$PKG" == "yum" ]]; then
if [[ ! -f /etc/yum.repos.d/docker-ce.repo ]]; then
run_retry $PKG install -y yum-utils
if [[ "$OS_ID" == "fedora" ]]; then
run_retry yum-config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo
else
run_retry yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
fi
fi
$PKG remove -y podman buildah docker docker-client docker-common docker-engine >/dev/null 2>&1 || true
elif [[ "$PKG" == "apt-get" ]]; then
if [[ ! -f /etc/apt/sources.list.d/docker.list ]]; then
source /etc/os-release
REPO_OS=${ID}
case "$REPO_OS" in
debian|ubuntu) : ;;
*) REPO_OS="ubuntu" ;;
esac
REPO_CODENAME="${VERSION_CODENAME:-$(command -v lsb_release >/dev/null 2>&1 && lsb_release -cs || echo stable)}"
install -m 0755 -d /etc/apt/keyrings
run_retry curl -fsSL "https://download.docker.com/linux/${REPO_OS}/gpg" -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/${REPO_OS} ${REPO_CODENAME} stable" > /etc/apt/sources.list.d/docker.list
apt-get update -qq || true
fi
fi
if ! command -v docker &>/dev/null; then
if [[ "$PKG" == "apt-get" ]]; then run_retry apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
elif [[ "$PKG" == "pacman" ]]; then run_retry pacman -S --noconfirm docker docker-compose docker-buildx
else run_retry $PKG install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin; fi
fi
mkdir -p /etc/docker
cat > /etc/docker/daemon.json <<EOF
{
"insecure-registries": [ ${INSECURE_REGISTRIES} ]
}
EOF
mkdir -p /etc/systemd/system/docker.service.d
cat > /etc/systemd/system/docker.service.d/http-proxy.conf <<EOF
[Service]
Environment="HTTP_PROXY=${PROXY_URL}"
Environment="HTTPS_PROXY=${PROXY_URL}"
Environment="NO_PROXY=${NO_PROXY_LIST}"
EOF
systemctl daemon-reload || true
timeout 30s systemctl enable --now docker || true
timeout 60s systemctl restart docker || true
timeout 15s usermod -aG docker root 2>/dev/null || true
if timeout 15s id "$LOCAL_USER" &>/dev/null; then timeout 15s usermod -aG docker "$LOCAL_USER" 2>/dev/null || true; fi
mkdir -p /root/.docker
cat > /root/.docker/config.json <<EOF
{
"proxies": {
"default": {
"httpProxy": "${PROXY_URL}",
"httpsProxy": "${PROXY_URL}",
"noProxy": "${NO_PROXY_LIST}"
}
}
}
EOF
if timeout 15s id "$LOCAL_USER" &>/dev/null; then
USER_HOME=$(eval echo ~$LOCAL_USER)
mkdir -p "$USER_HOME/.docker"
cp /root/.docker/config.json "$USER_HOME/.docker/config.json"
chown -R "$LOCAL_USER:$LOCAL_USER" "$USER_HOME/.docker" || true
fi
}
mod_lazydocker() {
step "Installing LazyDocker"
if ! command -v lazydocker &>/dev/null; then
run_retry curl -sSL https://raw.githubusercontent.com/jesseduffield/lazydocker/master/scripts/install_update_linux.sh | bash
fi
}
mod_web_stack() {
step "Installing Web Stack (PHP, Nginx, Node)"
if [[ "$PKG" == "dnf" || "$PKG" == "yum" ]]; then
if ! rpm -q remi-release >/dev/null 2>&1; then
if [[ "$OS_ID" == "fedora" ]]; then
run_retry dnf install -y "https://rpms.remirepo.net/fedora/remi-release-${VERSION_MAJOR}.rpm"
elif [[ "$PKG" == "dnf" ]]; then
run_retry $PKG install -y "https://rpms.remirepo.net/enterprise/remi-release-${VERSION_MAJOR}.rpm"
else
run_retry $PKG install -y http://rpms.remirepo.net/enterprise/remi-release-7.rpm yum-utils
fi
fi
$PKG clean packages >/dev/null 2>&1 || true
if [[ "$PKG" == "dnf" ]]; then
$PKG module reset php -y || true
$PKG module install -y php:remi-${PHP_VERSION}
else
yum-config-manager --enable remi-php83 || true
$PKG install -y php php-cli php-fpm php-mysqlnd php-gd
fi
$PKG install -y java-${JAVA_VERSION}-openjdk nginx nodejs
elif [[ "$PKG" == "pacman" ]]; then
run_retry pacman -S --noconfirm php php-fpm php-gd php-pgsql nginx nodejs npm jre-openjdk
else
source /etc/os-release
if [[ "$ID" == "debian" ]]; then
if [[ ! -f /etc/apt/sources.list.d/sury-php.list ]]; then
install -m 0755 -d /etc/apt/keyrings
run_retry curl -fsSL https://packages.sury.org/php/apt.gpg -o /etc/apt/keyrings/sury-php.gpg
chmod a+r /etc/apt/keyrings/sury-php.gpg
PHP_CODENAME="${VERSION_CODENAME:-$(command -v lsb_release >/dev/null 2>&1 && lsb_release -cs || echo bookworm)}"
echo "deb [signed-by=/etc/apt/keyrings/sury-php.gpg] https://packages.sury.org/php/ ${PHP_CODENAME} main" > /etc/apt/sources.list.d/sury-php.list
apt-get update -qq || true
fi
else
if ! grep -q "ondrej/php" /etc/apt/sources.list.d/* 2>/dev/null; then run_retry add-apt-repository -y ppa:ondrej/php; fi
apt-get update -qq || true
fi
run_retry apt-get install -y php${PHP_VERSION} php${PHP_VERSION}-{cli,fpm,mysql,gd,mbstring,xml,curl,zip}
run_retry apt-get install -y "openjdk-${JAVA_VERSION}-jdk" || { log "openjdk-${JAVA_VERSION} unavailable; installing default-jdk"; run_retry apt-get install -y default-jdk; }
run_retry apt-get install -y nginx nodejs npm
fi
if command -v php &>/dev/null; then
find /etc/php* -name "php.ini" 2>/dev/null | while read -r INI_FILE; do
sed -i '/^http_proxy/d; /^https_proxy/d' "$INI_FILE"
echo -e "\n; Proxy Settings\nhttp_proxy = \"${PROXY_URL}\"\nhttps_proxy = \"${PROXY_URL}\"" >> "$INI_FILE"
if grep -q "allow_url_fopen" "$INI_FILE"; then sed -i 's/^allow_url_fopen.*/allow_url_fopen = On/' "$INI_FILE"
else echo "allow_url_fopen = On" >> "$INI_FILE"; fi
done
if systemctl list-unit-files | grep -q php-fpm; then timeout 30s systemctl restart php-fpm || true; fi
if systemctl list-unit-files | grep -q php${PHP_VERSION}-fpm; then timeout 30s systemctl restart php${PHP_VERSION}-fpm || true; fi
fi
}
mod_db_stack() {
step "Installing Databases"
if [[ "$PKG" == "dnf" || "$PKG" == "yum" ]]; then
if [[ ! -f /etc/yum.repos.d/mariadb.repo ]]; then
if [[ "$OS_ID" == "fedora" ]]; then DB_OS="fedora"; else DB_OS="rhel"; fi
cat > /etc/yum.repos.d/mariadb.repo <<EOF
[mariadb]
name = MariaDB
baseurl = https://rpm.mariadb.org/${MARIADB_VERSION}/${DB_OS}/\$releasever/\$basearch
module_hotfixes=1
gpgkey=https://rpm.mariadb.org/RPM-GPG-KEY-MariaDB
gpgcheck=1
EOF
fi
run_retry $PKG install -y MariaDB-server MariaDB-client postgresql-server
elif [[ "$PKG" == "pacman" ]]; then
run_retry pacman -S --noconfirm mariadb postgresql
else
run_retry apt-get install -y mariadb-server postgresql
fi
}
mod_cockpit() {
step "Installing Cockpit"
if [[ "$PKG" == "apt-get" ]]; then run_retry apt-get install -y cockpit cockpit-storaged cockpit-pcp cockpit-packagekit
elif [[ "$PKG" == "pacman" ]]; then run_retry pacman -S --noconfirm cockpit
else run_retry $PKG install -y cockpit cockpit-storaged cockpit-pcp 2>/dev/null || run_retry $PKG install -y cockpit; fi
mkdir -p /etc/systemd/system/cockpit.service.d
echo -e "[Service]\nEnvironment=\"HTTP_PROXY=${PROXY_URL}\"\nEnvironment=\"HTTPS_PROXY=${PROXY_URL}\"\nEnvironment=\"NO_PROXY=${NO_PROXY_LIST}\"" > /etc/systemd/system/cockpit.service.d/proxy.conf
systemctl daemon-reload || true
timeout 30s systemctl enable --now cockpit.socket || true
}
mod_cleanup() {
step "Final Cleanup & Hardening"
if command -v apt-get &>/dev/null; then rm -f /etc/apt/apt.conf.d/80proxy; fi
if command -v dnf &>/dev/null; then sed -i '/^proxy=/d' /etc/dnf/dnf.conf 2>/dev/null || true; fi
if command -v tmux &>/dev/null; then $PKG remove -y tmux 2>/dev/null || true; fi
rm -f /etc/tmux.conf
if [[ "$PKG" == "apt-get" ]]; then run_retry apt-get install -y fish fail2ban;
elif [[ "$PKG" == "pacman" ]]; then run_retry pacman -S --noconfirm fish fail2ban;
else run_retry $PKG install -y fish fail2ban; fi
systemctl disable systemd-networkd-wait-online.service 2>/dev/null || true
systemctl mask systemd-networkd-wait-online.service 2>/dev/null || true
if [[ -f /etc/rc.d/rc.local ]]; then chmod +x /etc/rc.d/rc.local; fi
if grep -q "172.16.21.16" /etc/fstab; then sed -i '/172.16.21.16/d' /etc/fstab; fi
if systemctl is-failed sssd-nss.socket &>/dev/null; then
systemctl reset-failed || true
timeout 30s systemctl restart sssd || true
fi
ESCAPED_GROUP=$(echo "$AD_SUDO_GROUP" | sed 's/ /\\ /g')
mkdir -p /etc/sudoers.d
echo "%${ESCAPED_GROUP} ALL=(ALL) NOPASSWD: ALL" > "/etc/sudoers.d/10-ad-admins"
chmod 440 "/etc/sudoers.d/10-ad-admins"
cat > /etc/fail2ban/jail.local <<EOF
[sshd]
enabled = true
port = ssh
logpath = %(sshd_log)s
maxretry = 3
bantime = 3600
EOF
timeout 30s systemctl enable --now fail2ban || true
}
###############################################################################
# 6. CLI ROUTER
###############################################################################
detect_and_fix_os
show_help() {
echo "Usage: $0 [OPTION]"
echo "Supported: RHEL/CentOS/Alma/Rocky/Fedora (dnf/yum), Ubuntu/Debian/Zorin (apt), Arch (pacman)."
echo ""
echo "Core Deployment:"
echo " --basics Proxy, Certs, Repos, Network, Firewalld, AD, Cleanup."
echo " --full Everything (Basics + GUI + Docker + Web/DB + Flatpak/Tools + Cockpit)."
echo ""
echo "Modular Execution:"
echo " --docker Install and configure Docker Engine with Proxy/Subnets."
echo " --flatpak Configure Flatpak, Flathub, and GUI App Centers (GNOME/KDE)."
echo " --ad-join Run the SSSD and Realmd AD Join sequence."
echo " --certs Mount CIFS, fetch root cert, update CA trust."
echo " --gui-proxy Configure dconf (GNOME/Cinnamon), KDE, and Firefox Proxies."
echo " --proxy-tool Install the 'toggle-proxy' dynamic CLI tool."
echo " --desktop-tools Install Fastfetch, GNOME Tweaks, Flatseal, ExtensionManager."
echo " --web-stack Install PHP, Nginx, Node, and Java."
echo " --db-stack Install MariaDB and PostgreSQL."
echo " --tools Install zsh, fish, neovim, git, nano, lazydocker."
echo " --resize-home Shrink LVM /home to ${HOME_TARGET_SIZE} (Backup/Restore)."
echo ""
}
if [[ $# -eq 0 ]]; then show_help; exit 0; fi
init_header
while [[ "$#" -gt 0 ]]; do
case $1 in
--basics) mod_proxy; mod_clock_fix; mod_certs; mod_base_repos; mod_base_tools; mod_network; mod_firewall; mod_domain_users; mod_cleanup ;;
--full) mod_proxy; mod_gui_proxy; mod_proxy_toggle; mod_clock_fix; mod_certs; mod_base_repos; mod_base_tools; mod_flatpak; mod_desktop_tools; mod_network; mod_firewall; mod_domain_users; mod_docker; mod_web_stack; mod_db_stack; mod_cockpit; mod_cleanup ;;
--docker) mod_proxy; mod_docker ;;
--flatpak) mod_proxy; mod_flatpak ;;
--desktop-tools) mod_proxy; mod_desktop_tools ;;
--ad-join) mod_domain_users ;;
--certs) mod_certs ;;
--gui-proxy) mod_gui_proxy ;;
--proxy-tool) mod_proxy_toggle ;;
--web-stack) mod_proxy; mod_web_stack ;;
--db-stack) mod_proxy; mod_db_stack ;;
--tools) mod_proxy; mod_base_tools; mod_lazydocker ;;
--resize-home) mod_resize_home ;;
*) echo "Unknown option: $1"; show_help; exit 1 ;;
esac
shift
done
echo -e "\n${GREEN}[$(date +'%H:%M:%S')] === Setup Complete ===${NC}"
master_script.sh - v66c
#!/usr/bin/env bash
#
# MASTER INFRASTRUCTURE SETUP
# Version: v66
#
# v66 Changelog:
# - FIX: mod_base_repos now repairs any PRE-EXISTING Remi repo config using
# the cdn.remirepo.net mirrorlist (the same 303-redirect problem already
# fixed for repos this script writes itself, in mod_web_stack) — not just
# the config this script creates when it installs PHP. A host like edrive
# had Remi PHP installed long before this script existed, so a plain
# --basics run had no way to know that config was broken until it hit
# this exact failure — recoverable by run_retry some of the time, not
# guaranteed to stay that way. Confirmed on mydns-edrive 2026-08-05.
# Idempotent: only acts on repos still enabled=1 with the old mirrorlist,
# so it fixes once and cleanly no-ops on every run after.
#
###############################################################################
# 1. CONFIGURATION
###############################################################################
SCRIPT_VERSION="v66"
LOG_FILE="/var/log/m21-setup.log"
DOMAIN_FQDN="m21.gov.local"
DOMAIN_ALT="m21.gov.tt"
DOMAIN_SHORT="M21"
# Domain controllers, preferred first. mod_network probes each with a real
# LDAP query and uses only the ones that answer.
DC_LIST="172.40.132.67 172.40.132.66 172.42.132.66 172.16.21.161"
DC_DNS_IP="${DC_LIST%% *}" # provisional; re-set by mod_network after probing
NTP_SERVER="172.16.121.9"
TARGET_TIMEZONE="America/Port_of_Spain"
# File Server Info
FILE_SERVER_IP="172.16.21.16"
FILE_SERVER_NAME="fileserver2"
# Proxy
PROXY_URL="http://172.40.4.14:8080"
# Docker Subnets & Internal Container Hostnames
DOCKER_NO_PROXY="172.17.0.0/16,172.18.0.0/16,172.19.0.0/16,172.20.0.0/16,172.21.0.0/16,web,api,app,db,database,redis,postgres,mysql,minio,mq,cache,admin,live,proxy,edrive,nextcloud,huly,cockroach,zammad,glpi,authentik,peertube,npm,zoraxy"
NO_PROXY_LIST="127.0.0.1,localhost,localhost.localdomain,${DOMAIN_FQDN},${DOMAIN_ALT},.${DOMAIN_FQDN},.${DOMAIN_ALT},172.30.0.0/20,172.26.21.0/24,10.21.0.0/21,172.16.121.0/24,172.16.21.0/24,172.40.132.0/24,172.42.132.0/24,${DOCKER_NO_PROXY}"
# Docker Settings
INSECURE_REGISTRIES='"172.16.121.119:5000", "docker-repo.msya.gov.tt"'
# AD Access Control
AD_SUDO_GROUP="ICT Staff SG M21"
ALLOWED_LOGIN_GROUP="ICT Staff SG M21"
# Share Credentials
# WARNING: Never wrap commands containing these credentials in run_retry, and
# keep them inside if/|| true guards so the ERR trap doesn't log them to /var/log!
SHARE_PATH="//172.16.21.16/fileserver2"
SHARE_USER="Cipher.m21"
SHARE_PASS=")\ly; 634'NJ%i+"
# Certificates. CERT_SOURCE_PATH is read from the CIFS share; CERT_LOCAL_DIR is
# a drop-in folder on the local box. Any .cer/.crt/.pem in either is installed.
# `--certs /path/to/file-or-dir` overrides both for a one-off.
#
# CERT_SOURCE_PATH_ROOT is a SECOND, optional share path for the actual
# self-signed root (subject == issuer). As of 2026-07, Gortt_certificate_V4.cer
# has only ever contained the leaf (CN=dc01intfw.gov.local) — every fresh host
# gets the "no ROOT found" warning until the real root is put on the share at
# this path and this variable points at it. Leave blank to skip.
CERT_SOURCE_PATH="/General/IT FILES/prx/Gortt_certificate_V4.cer"
CERT_SOURCE_PATH_ROOT=""
CERT_LOCAL_DIR="/etc/m21-certs"
TARGET_CERT_NAME="GORTT_Root_Exp2029"
# Desktop homepage, applied via Firefox policy (mod_desktop_network) — see
# v62 changelog for why this can't just be a distro default anymore.
FIREFOX_HOMEPAGE="https://start.fedoraproject.org/"
# Failsafe User
LOCAL_USER="pcsupport"
LOCAL_PASS="ProIT321*"
# Sources permitted to reach SSH once mod_firewall scopes the service.
# If your source is not in here, mod_firewall aborts rather than locking you out.
SSH_ALLOWED_CIDRS="10.21.0.0/21 172.16.121.0/24 172.16.21.0/24"
# DMZ peer-to-peer trust. Every VM on these subnets is fully trusted by every
# other VM's firewall on the same subnet(s) — no port-by-port rules, no
# per-host IP tailoring. These VMs talk to each other constantly (DB access,
# cross-service calls) purely over the DMZ with no external exposure, so a
# shared trusted subnet is the right shape: adding a new DMZ host requires no
# firewall change anywhere, on any existing host. Space-separated if a second
# DMZ segment is ever added.
DMZ_TRUSTED_CIDRS="172.16.121.0/24"
# LVM Settings
HOME_TARGET_SIZE="8G"
# Versions
PHP_VERSION="8.3"
JAVA_VERSION="21"
MARIADB_VERSION="10.11"
# CentOS 7 EOL archive settings. First reachable base wins.
CENTOS7_VAULT_VER="7.9.2009"
EL7_BASE_MIRRORS="https://vault.centos.org/${CENTOS7_VAULT_VER} https://el7.repo.almalinux.org/centos/7 https://linuxsoft.cern.ch/centos-vault/${CENTOS7_VAULT_VER}"
EL7_EPEL_ARCHIVE="https://archives.fedoraproject.org/pub/archive/epel/7"
EL7_REMI_BASE="https://rpms.remirepo.net/enterprise/7"
# PHP package set for EL7/Remi. This is the exact set edrive was running before
# the 2026-07-28 `yum remove php*`; keep it in sync if you add extensions.
PHP_PACKAGES_EL7="php php-cli php-common php-fpm php-devel php-pear \
php-opcache php-process php-mysqlnd php-pdo php-gd php-intl php-gmp \
php-bcmath php-mbstring php-xml php-ldap php-imap php-sodium php-smbclient \
php-pecl-apcu php-pecl-igbinary php-pecl-msgpack php-pecl-redis6 \
php-pecl-zip php-pecl-imagick-im6"
# PHP tuning written to /etc/php.d/99-m21.ini (not php.ini — no package owns
# the drop-in, so a php reinstall cannot silently reset these).
PHP_MEMORY_LIMIT="2G"
PHP_UPLOAD_MAX="800M"
PHP_MAX_EXEC="3600"
# Systemd services that must inherit the proxy
PROXY_SERVICES="packagekit flatpak-system-helper fwupd"
# Set by --yes; skips the "this host is serving users" countdown.
ASSUME_YES=false
# Resolved at startup.
SYSTEM_CA_BUNDLE=""
for _B in /etc/pki/tls/certs/ca-bundle.crt /etc/ssl/certs/ca-certificates.crt; do
[[ -f "$_B" ]] && SYSTEM_CA_BUNDLE="$_B" && break
done
unset _B
###############################################################################
# 2. HELPER FUNCTIONS
###############################################################################
set -e
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[0;33m'; BLUE='\033[0;34m'; NC='\033[0m'
log() { echo -e "${BLUE}[$(date +'%H:%M:%S')] [INFO]${NC} $1"; }
step() { echo -e "\n${YELLOW}[$(date +'%H:%M:%S')] >>> $1${NC}"; }
success() { echo -e "${GREEN}[$(date +'%H:%M:%S')] [OK]${NC} $1"; }
error() { echo -e "${RED}[$(date +'%H:%M:%S')] [ERROR]${NC} $1"; }
warn() { echo -e "${YELLOW}[$(date +'%H:%M:%S')] [WARN]${NC} $1"; }
trap 'error "Script aborted at line ${LINENO} (last command: ${BASH_COMMAND})"' ERR
trap 'sleep 0.2' EXIT
run_retry() {
local n=1; local max=3; local delay=2
while true; do
"$@" && return 0
if [[ $n -lt $max ]]; then
((n++)); log "Command failed: [$*]. Retrying ($n/$max)..."; sleep $delay
else
error "Command failed permanently after ${max} attempts: [$*]"
return 1
fi
done
}
_ip2int() {
local a b c d
IFS=. read -r a b c d <<< "$1"
echo $(( (a<<24) + (b<<16) + (c<<8) + d ))
}
ip_in_cidr() {
local IP="$1" CIDR="$2"
local NET="${CIDR%/*}" BITS="${CIDR#*/}" MASK IPN NETN
[[ "$IP" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]] || return 1
[[ "$BITS" =~ ^[0-9]+$ ]] || return 1
MASK=$(( (0xFFFFFFFF << (32 - BITS)) & 0xFFFFFFFF ))
IPN=$(_ip2int "$IP"); NETN=$(_ip2int "$NET")
[[ $(( IPN & MASK )) -eq $(( NETN & MASK )) ]]
}
# Follows redirects and reports the FINAL status. yum's "Trying other mirror"
# hides 301/303 responses, which is how a dead baseurl looks like a slow one.
url_alive() {
local CODE
CODE=$(curl -sL -o /dev/null -w '%{http_code}' --max-time 20 -x "${PROXY_URL}" "$1" 2>/dev/null || echo 000)
[[ "$CODE" == "200" ]]
}
# A domain controller that completes the TCP handshake and then goes silent is
# indistinguishable from a healthy one to ping(8) or /dev/tcp. Only a real
# RootDSE query tells the truth. This cost a working day on 2026-07-28.
probe_dc() {
local IP="$1"
if command -v ldapsearch &>/dev/null; then
timeout 8s ldapsearch -x -LLL -H "ldap://${IP}" \
-s base -b "" defaultNamingContext &>/dev/null && return 0
return 1
fi
warn "ldapsearch unavailable — falling back to an unreliable TCP probe."
warn " Run --tools to install openldap-clients for a real health check."
timeout 3s bash -c "exec 3<>/dev/tcp/${IP}/389" 2>/dev/null || return 1
timeout 3s bash -c "exec 3<>/dev/tcp/${IP}/53" 2>/dev/null || return 1
return 0
}
# This script rewrites proxy, DNS, resolv.conf, firewalld and repositories.
# On a host already serving users that is an outage, not a deployment, so give
# the operator a window to abort. Not a hard block — pass --yes to skip.
warn_if_serving() {
[[ "$ASSUME_YES" == true ]] && return 0
local REASONS=() LISTEN P R i
LISTEN=$(ss -lnt 2>/dev/null | awk 'NR>1 {print $4}' | sed 's/.*://' | sort -un)
for P in 80 443 3306 5432; do
grep -qx "$P" <<< "$LISTEN" && REASONS+=("serving on TCP/${P}")
done
[[ -d /var/www/html/nextcloud ]] && REASONS+=("Nextcloud webroot present")
[[ ${#REASONS[@]} -eq 0 ]] && return 0
warn "This host looks live:"
for R in "${REASONS[@]}"; do echo " - ${R}"; done
warn "Proxy, DNS, resolv.conf, firewalld and repos are about to be rewritten."
echo -ne "${YELLOW} Ctrl-C to abort: ${NC}"
for i in 10 9 8 7 6 5 4 3 2 1; do echo -n "${i} "; sleep 1; done
echo ""
}
pin_flatpak_proxy() {
local REPO="/var/lib/flatpak/repo"
if [[ ! -f "$REPO/config" ]]; then
warn "Flatpak repo not initialized yet (${REPO}/config missing) — skipping proxy pin."
return 0
fi
if ! command -v ostree &>/dev/null; then
log "ostree CLI not installed (only ostree-libs) — installing for repo proxy pin..."
if [[ "$PKG" == "apt-get" ]]; then run_retry apt-get install -y ostree || true
elif [[ "$PKG" == "pacman" ]]; then run_retry pacman -S --noconfirm ostree || true
else run_retry $PKG install -y ostree || true; fi
fi
if command -v ostree &>/dev/null; then
ostree --repo="$REPO" config set 'remote "flathub".proxy' "${PROXY_URL}" || true
else
sed -i '/^\[remote "flathub"\]/,/^\[/{ /^proxy=/d }' "$REPO/config" 2>/dev/null || true
sed -i "/^\[remote \"flathub\"\]/a proxy=${PROXY_URL}" "$REPO/config" 2>/dev/null || true
fi
if awk -v want="proxy=${PROXY_URL}" '
/^\[remote "flathub"\]/ {f=1; next}
/^\[/ {f=0}
f && $0 == want {found=1}
END {exit !found}
' "$REPO/config"; then
success "flathub ostree proxy pinned: ${PROXY_URL}"
else
warn "FAILED to pin flathub proxy in ${REPO}/config — flatpak fetches will bypass the proxy and hang/fail."
fi
}
###############################################################################
# 3. CERTIFICATE ENGINE
###############################################################################
# Handles DER or PEM, single certs or full bundles, from the share or a local
# path. Installing certs is often the only thing a box is missing, so this is
# deliberately forgiving: it installs everything it can parse and tells you
# what each one actually is rather than refusing.
_split_pem() {
awk -v d="$2" '
/-----BEGIN CERTIFICATE-----/ { n++; f = sprintf("%s/part-%02d.pem", d, n) }
f { print > f }
/-----END CERTIFICATE-----/ { close(f); f="" }
' "$1"
}
_anchor_dir() {
if [[ "$PKG" == "dnf" || "$PKG" == "yum" ]]; then echo "/etc/pki/ca-trust/source/anchors"
elif [[ "$PKG" == "pacman" ]]; then echo "/etc/ca-certificates/trust-source/anchors"
else echo "/usr/local/share/ca-certificates"; fi
}
_refresh_trust() {
if [[ "$PKG" == "dnf" || "$PKG" == "yum" ]]; then
[[ "$VERSION_MAJOR" -lt 9 ]] && update-ca-trust force-enable 2>/dev/null || true
update-ca-trust extract
elif [[ "$PKG" == "pacman" ]]; then
trust extract-compat
else
update-ca-certificates
fi
}
# Installs every certificate found in one file. Returns 0 if at least one was
# installed. Sets CERT_ROOTS_FOUND when a self-signed root was among them.
install_cert_file() {
local SRC="$1" BASE="${2:-}"
local WORK PEM PART N=0 INSTALLED=0 SUBJ ISS CN NAME DEST EXT
[[ -f "$SRC" ]] || { warn "Not a file: ${SRC}"; return 1; }
WORK=$(mktemp -d /tmp/m21cert.XXXXXX)
PEM="${WORK}/input.pem"
[[ -z "$BASE" ]] && BASE=$(basename "$SRC"); BASE="${BASE%.*}"
# DER -> PEM, else assume PEM/bundle
if openssl x509 -inform der -in "$SRC" -out "$PEM" 2>/dev/null; then :; else cp "$SRC" "$PEM"; fi
_split_pem "$PEM" "$WORK"
if ! ls "${WORK}"/part-*.pem &>/dev/null; then
warn "${SRC}: no parseable certificate found. Skipping."
rm -rf "$WORK"; return 1
fi
if [[ "$PKG" == "apt-get" ]]; then EXT="crt"; elif [[ "$PKG" == "pacman" ]]; then EXT="crt"; else EXT="pem"; fi
for PART in "${WORK}"/part-*.pem; do
N=$((N+1))
openssl x509 -in "$PART" -noout -subject &>/dev/null || continue
SUBJ=$(openssl x509 -in "$PART" -noout -subject 2>/dev/null); SUBJ="${SUBJ#subject=}"
ISS=$(openssl x509 -in "$PART" -noout -issuer 2>/dev/null); ISS="${ISS#issuer=}"
CN=$(openssl x509 -in "$PART" -noout -subject -nameopt multiline 2>/dev/null \
| sed -n 's/^ *commonName *= *//p' | head -n1)
[[ -z "$CN" ]] && CN="cert${N}"
NAME=$(echo "${BASE}_${CN}" | tr -cs 'A-Za-z0-9._-' '_' | sed 's/_\+/_/g; s/^_//; s/_$//')
if ! openssl x509 -in "$PART" -noout -checkend 0 &>/dev/null; then
warn " ${CN}: EXPIRED — installing anyway, but it will not validate anything."
fi
if [[ "$SUBJ" == "$ISS" ]]; then
log " ${CN}: self-signed ROOT"
CERT_ROOTS_FOUND=$((CERT_ROOTS_FOUND+1))
else
log " ${CN}: intermediate/leaf (issued by ${ISS})"
fi
DEST="$(_anchor_dir)/${NAME}.${EXT}"
if [[ -f "$DEST" ]] && cmp -s "$PART" "$DEST"; then
log " already present, unchanged"
else
cp "$PART" "$DEST"
success " installed -> ${DEST}"
fi
CERT_INSTALLED_PATHS+=("$DEST")
INSTALLED=$((INSTALLED+1))
done
rm -rf "$WORK"
[[ $INSTALLED -gt 0 ]]
}
# Nextcloud ships its own resources/config/ca-bundle.crt and ignores the system
# store entirely. Without this, every outbound HTTPS request through an
# inspecting proxy fails with cURL error 60.
push_certs_to_apps() {
local NCROOT WEBUSER P
for NCROOT in /var/www/html/nextcloud /var/www/nextcloud /usr/share/nextcloud; do
[[ -f "${NCROOT}/occ" ]] || continue
WEBUSER="apache"; id www-data &>/dev/null && WEBUSER="www-data"
for P in "${CERT_INSTALLED_PATHS[@]}"; do
if sudo -u "$WEBUSER" php "${NCROOT}/occ" security:certificates:import "$P" &>/dev/null; then
success "Imported into Nextcloud trust store: $(basename "$P")"
else
warn "Nextcloud import failed for $(basename "$P") — run by hand:"
warn " sudo -u ${WEBUSER} php ${NCROOT}/occ security:certificates:import ${P}"
fi
done
break
done
# RHEL/Debian regenerate the Java keystore from the system anchors during
# update-ca-trust / update-ca-certificates, so keytool is not needed here.
# A privately-unpacked JRE (e.g. LibreSign's Temurin) has its own cacerts
# and must be handled separately.
}
mod_certs() {
step "Installing Certificates"
local SRC_OVERRIDE="${1:-}"
local MNT="/mnt/share_certs_tmp" F ANY=0
CERT_INSTALLED_PATHS=()
CERT_ROOTS_FOUND=0
# 1) explicit path argument
if [[ -n "$SRC_OVERRIDE" ]]; then
if [[ -d "$SRC_OVERRIDE" ]]; then
for F in "$SRC_OVERRIDE"/*.cer "$SRC_OVERRIDE"/*.crt "$SRC_OVERRIDE"/*.pem; do
[[ -f "$F" ]] || continue
log "Reading ${F}"; install_cert_file "$F" && ANY=1
done
elif [[ -f "$SRC_OVERRIDE" ]]; then
log "Reading ${SRC_OVERRIDE}"; install_cert_file "$SRC_OVERRIDE" && ANY=1
else
error "No such file or directory: ${SRC_OVERRIDE}"; return 1
fi
else
# 2) local drop-in directory
if [[ -d "$CERT_LOCAL_DIR" ]]; then
for F in "$CERT_LOCAL_DIR"/*.cer "$CERT_LOCAL_DIR"/*.crt "$CERT_LOCAL_DIR"/*.pem; do
[[ -f "$F" ]] || continue
log "Reading ${F}"; install_cert_file "$F" && ANY=1
done
fi
# 3) the CIFS share
mkdir -p "$MNT"
if ! command -v mount.cifs &>/dev/null; then
if [[ "$PKG" == "apt-get" ]]; then run_retry apt-get update -qq >/dev/null 2>&1 || true; run_retry apt-get install -y cifs-utils
elif [[ "$PKG" == "pacman" ]]; then run_retry pacman -S --noconfirm cifs-utils
else run_retry $PKG install -y cifs-utils; fi
fi
if mountpoint -q "$MNT"; then umount -l "$MNT"; fi
if timeout 30s mount -t cifs "$SHARE_PATH" "$MNT" -o username="$SHARE_USER",password="$SHARE_PASS",vers=3.0; then
if [[ -f "${MNT}${CERT_SOURCE_PATH}" ]]; then
log "Reading ${CERT_SOURCE_PATH} from the share"
install_cert_file "${MNT}${CERT_SOURCE_PATH}" "$TARGET_CERT_NAME" && ANY=1
else
warn "Not found on share: ${CERT_SOURCE_PATH}"
fi
if [[ -n "$CERT_SOURCE_PATH_ROOT" ]]; then
if [[ -f "${MNT}${CERT_SOURCE_PATH_ROOT}" ]]; then
log "Reading ${CERT_SOURCE_PATH_ROOT} from the share"
install_cert_file "${MNT}${CERT_SOURCE_PATH_ROOT}" "${TARGET_CERT_NAME}_root" && ANY=1
else
warn "CERT_SOURCE_PATH_ROOT is set but not found on share: ${CERT_SOURCE_PATH_ROOT}"
fi
fi
timeout 15s umount "$MNT" || true
else
warn "Could not mount ${SHARE_PATH}."
fi
rmdir "$MNT" 2>/dev/null || true
fi
if [[ $ANY -eq 0 ]]; then
warn "No certificates installed. Drop .cer/.crt/.pem into ${CERT_LOCAL_DIR}"
warn "or run: $0 --certs /path/to/cert"
return 0
fi
_refresh_trust
push_certs_to_apps
if [[ ${CERT_ROOTS_FOUND:-0} -eq 0 ]]; then
warn "No self-signed ROOT was among the certificates installed."
warn "Intermediates and leaves alone will NOT make an inspecting proxy trusted."
warn "The GORTT root is the self-signed one:"
warn " CN=Government of the Republic of Trinidad and Tobago Enterprise Root CA v2.0"
fi
log "Verifying outbound TLS through the proxy..."
if url_alive "https://www.google.com/"; then
success "HTTPS through ${PROXY_URL} validates against the system store."
else
warn "HTTPS through the proxy still fails. Either the root CA is missing,"
warn "or the destination is category-blocked (Check Point returns a 303"
warn "redirect to dc01intfw.gov.local/UserCheck for blocked categories)."
fi
}
###############################################################################
# 4. PRE-FLIGHT CHECKS
###############################################################################
detect_and_fix_os() {
if [[ ! -f /etc/os-release ]]; then error "Cannot detect OS. /etc/os-release missing."; exit 1; fi
source /etc/os-release
OS_ID=$(echo "$ID" | tr '[:upper:]' '[:lower:]')
OS_PRETTY="${PRETTY_NAME:-$ID $VERSION_ID}"
VERSION_MAJOR=$(echo "$VERSION_ID" | cut -d. -f1)
IS_EL7=false
if timeout 10s systemctl is-active --quiet packagekit.service 2>/dev/null; then
timeout 15s systemctl stop packagekit.service || true
fi
if [[ "$OS_ID" == "centos" && "$VERSION_MAJOR" == "7" ]]; then
PKG="yum"; IS_EL7=true
if grep -q "linux/rhel" /etc/yum.repos.d/docker-ce.repo 2>/dev/null; then rm -f /etc/yum.repos.d/docker-ce.repo; fi
elif [[ "$OS_ID" =~ (rhel|centos|almalinux|rocky|fedora) ]]; then PKG="dnf"
elif [[ "$OS_ID" =~ (ubuntu|debian|zorin) ]]; then PKG="apt-get"; export DEBIAN_FRONTEND=noninteractive
elif [[ "$OS_ID" == "arch" || "$ID_LIKE" == *"arch"* ]]; then PKG="pacman"; run_retry pacman -Sy
else error "Unsupported OS: $OS_ID"; exit 1; fi
detect_de
}
detect_de() {
HAS_GNOME=false
HAS_KDE=false
DETECTED_DE="Headless/Server"
if command -v gnome-shell &>/dev/null \
|| (command -v dpkg &>/dev/null && dpkg -l 2>/dev/null | grep -q "gnome-shell") \
|| (command -v rpm &>/dev/null && rpm -q gnome-shell &>/dev/null); then
HAS_GNOME=true; DETECTED_DE="GNOME"
fi
if command -v plasmashell &>/dev/null \
|| (command -v dpkg &>/dev/null && dpkg -l 2>/dev/null | grep -q "plasma-workspace") \
|| (command -v rpm &>/dev/null && rpm -q plasma-workspace &>/dev/null); then
HAS_KDE=true
if [ "$HAS_GNOME" = true ]; then DETECTED_DE="GNOME + KDE"; else DETECTED_DE="KDE Plasma"; fi
fi
if command -v cinnamon &>/dev/null; then DETECTED_DE="${DETECTED_DE/Headless\/Server/Cinnamon}"; fi
}
# Refuse to run if this copy is truncated or a merge dropped a function the
# router depends on. v59 shipped without mod_domain_users; never again.
verify_modules() {
local MISSING=0
local FN
for FN in \
run_retry detect_de detect_and_fix_os init_header _chk url_alive \
_ip2int ip_in_cidr probe_dc warn_if_serving \
_split_pem _anchor_dir _refresh_trust install_cert_file push_certs_to_apps \
mod_proxy mod_gui_proxy mod_proxy_toggle mod_flatpak mod_desktop_tools \
mod_gs_fix mod_clock_fix mod_certs mod_base_repos mod_base_tools mod_desktop_network \
mod_network mod_firewall mod_resize_home mod_domain_users mod_docker \
mod_lazydocker mod_web_stack mod_db_stack mod_cockpit mod_cleanup \
mod_shell mod_doctor pin_flatpak_proxy; do
if ! declare -f "$FN" >/dev/null 2>&1; then
error "Module '${FN}' is MISSING from this script copy (truncated file or bad merge)."
MISSING=1
fi
done
if [[ $MISSING -eq 1 ]]; then
error "Refusing to run with missing modules. Restore this script from a known-good copy."
exit 1
fi
}
init_header() {
local KERNEL ARCH HOST PRIMARY_IP PROXY_STATE JOINED UPT
KERNEL=$(uname -r)
ARCH=$(uname -m)
HOST=$(hostname)
PRIMARY_IP=$(ip -4 route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="src") print $(i+1); exit}')
[[ -z "$PRIMARY_IP" ]] && PRIMARY_IP=$(hostname -I 2>/dev/null | awk '{print $1}')
UPT=$(uptime -p 2>/dev/null | sed 's/^up //')
if [[ -s /etc/profile.d/proxy.sh ]]; then PROXY_STATE="ON (${PROXY_URL})"; else PROXY_STATE="OFF"; fi
if command -v realm &>/dev/null && timeout 10s realm list 2>/dev/null | grep -q "$DOMAIN_FQDN"; then
JOINED="Joined (${DOMAIN_FQDN})"
else
JOINED="Not joined"
fi
echo -e "\n${BLUE}=====================================================================${NC}"
echo -e "${GREEN} Master Infrastructure Setup ${SCRIPT_VERSION}${NC}"
echo -e "${BLUE}=====================================================================${NC}"
printf " %-14s %s\n" "OS:" "${OS_PRETTY} (${ARCH})"
printf " %-14s %s\n" "Kernel:" "${KERNEL}"
printf " %-14s %s\n" "Hostname:" "${HOST}"
printf " %-14s %s\n" "IP:" "${PRIMARY_IP:-unknown}"
printf " %-14s %s\n" "Desktop:" "${DETECTED_DE}"
printf " %-14s %s\n" "Pkg Mgr:" "${PKG}$( [[ "$IS_EL7" == true ]] && echo ' (EL7 / EOL)')"
printf " %-14s %s\n" "Proxy:" "${PROXY_STATE}"
printf " %-14s %s\n" "Domain:" "${JOINED}"
printf " %-14s %s\n" "Uptime:" "${UPT:-unknown}"
printf " %-14s %s\n" "Run at:" "$(date '+%Y-%m-%d %H:%M:%S %Z')"
echo -e "${BLUE}=====================================================================${NC}\n"
}
###############################################################################
# 5. CORE MODULES
###############################################################################
mod_proxy() {
step "Configuring System Proxy"
cat > /etc/profile.d/proxy.sh <<EOF
export http_proxy="${PROXY_URL}"
export https_proxy="${PROXY_URL}"
export ftp_proxy="${PROXY_URL}"
export no_proxy="${NO_PROXY_LIST}"
export HTTP_PROXY="${PROXY_URL}"
export HTTPS_PROXY="${PROXY_URL}"
export FTP_PROXY="${PROXY_URL}"
export NO_PROXY="${NO_PROXY_LIST}"
EOF
source /etc/profile.d/proxy.sh
# No quotes here. /etc/environment is parsed by PAM (pam_env), which does
# plain NAME=value splitting and does NOT strip surrounding quotes — a
# quoted value here becomes a literal `"http://...` string containing the
# quote character, which is not a usable URL. Bash sessions never noticed
# because /etc/profile.d/proxy.sh (below, correctly bash-quoted) runs
# afterward and silently overrides it — but anything that reads
# /etc/environment directly and isn't a login bash shell (some display
# managers, sshd with PAM but no interactive shell, etc.) kept the broken
# value until now.
sed -i -E '/^(http_proxy|https_proxy|ftp_proxy|no_proxy|HTTP_PROXY|HTTPS_PROXY|FTP_PROXY|NO_PROXY)=/d' /etc/environment 2>/dev/null || true
cat >> /etc/environment <<EOF
http_proxy=${PROXY_URL}
https_proxy=${PROXY_URL}
ftp_proxy=${PROXY_URL}
no_proxy=${NO_PROXY_LIST}
HTTP_PROXY=${PROXY_URL}
HTTPS_PROXY=${PROXY_URL}
FTP_PROXY=${PROXY_URL}
NO_PROXY=${NO_PROXY_LIST}
EOF
mkdir -p /etc/sudoers.d
echo 'Defaults env_keep += "http_proxy https_proxy ftp_proxy no_proxy HTTP_PROXY HTTPS_PROXY FTP_PROXY NO_PROXY"' > /etc/sudoers.d/10-proxy-env
chmod 440 /etc/sudoers.d/10-proxy-env
if [[ "$PKG" == "dnf" || "$PKG" == "yum" ]]; then
CONF_FILE="/etc/dnf/dnf.conf"
[[ ! -f "$CONF_FILE" ]] && CONF_FILE="/etc/yum.conf"
grep -q "proxy=" "$CONF_FILE" 2>/dev/null || echo "proxy=${PROXY_URL}" >> "$CONF_FILE"
if ! grep -q "minrate" "$CONF_FILE" 2>/dev/null; then
echo -e "timeout=60\nretries=10\nminrate=1" >> "$CONF_FILE"
fi
elif [[ "$PKG" == "apt-get" ]]; then
echo -e "Acquire::http::Proxy \"${PROXY_URL}\";\nAcquire::https::Proxy \"${PROXY_URL}\";" > /etc/apt/apt.conf.d/80proxy
fi
for SVC in ${PROXY_SERVICES}; do
mkdir -p /etc/systemd/system/${SVC}.service.d
cat > /etc/systemd/system/${SVC}.service.d/http-proxy.conf <<EOF
[Service]
Environment="HTTP_PROXY=${PROXY_URL}"
Environment="HTTPS_PROXY=${PROXY_URL}"
Environment="http_proxy=${PROXY_URL}"
Environment="https_proxy=${PROXY_URL}"
Environment="NO_PROXY=${NO_PROXY_LIST}"
Environment="no_proxy=${NO_PROXY_LIST}"
EOF
done
systemctl daemon-reload
killall packagekitd 2>/dev/null || true
killall flatpak-system-helper 2>/dev/null || true
systemctl try-restart packagekit flatpak-system-helper fwupd 2>/dev/null || true
}
mod_gui_proxy() {
step "Configuring GUI Proxy Settings (System-Wide)"
PROXY_HOST=$(echo "$PROXY_URL" | awk -F/ '{print $3}' | cut -d: -f1)
PROXY_PORT=$(echo "$PROXY_URL" | awk -F: '{print $NF}')
DCONF_NO_PROXY="['$(echo "$NO_PROXY_LIST" | sed "s/,/','/g")']"
if [[ "$PKG" == "apt-get" ]]; then run_retry apt-get install -y dconf-cli
elif [[ "$PKG" == "dnf" || "$PKG" == "yum" ]]; then run_retry $PKG install -y dconf
elif [[ "$PKG" == "pacman" ]]; then run_retry pacman -S --noconfirm dconf
fi
mkdir -p /etc/dconf/profile
mkdir -p /etc/dconf/db/local.d
echo -e "user-db:user\nsystem-db:local" > /etc/dconf/profile/user
cat > /etc/dconf/db/local.d/01-proxy <<EOF
[system/proxy]
mode='manual'
ignore-hosts=${DCONF_NO_PROXY}
[system/proxy/http]
host='${PROXY_HOST}'
port=${PROXY_PORT}
[system/proxy/https]
host='${PROXY_HOST}'
port=${PROXY_PORT}
[system/proxy/ftp]
host='${PROXY_HOST}'
port=${PROXY_PORT}
EOF
dconf update || log "Warning: dconf update failed, GUI settings may require reboot."
mkdir -p /etc/xdg
cat > /etc/xdg/kioslaverc <<EOF
[Proxy Settings]
ProxyType=1
httpProxy=${PROXY_URL}
httpsProxy=${PROXY_URL}
ftpProxy=${PROXY_URL}
NoProxyFor=${NO_PROXY_LIST}
EOF
# Firefox 152+ broke Fedora's legacy homepage default (Bugzilla #2047962):
# the distro default wraps the real URL in a
# data:text/plain,browser.startup.homepage=... string that older Firefox
# unwrapped automatically. That unwrapping code is gone, so current
# Firefox just displays the raw pref line as the page. Not fixable by
# network/proxy/cert changes — an explicit Homepage policy overrides the
# broken distro default entirely and sidesteps the legacy path.
#
# Certificates.Install is Firefox's own NSS store (separate from the
# system trust store this script otherwise manages) — include the root
# CA here too if one has actually been installed, so Firefox itself
# trusts it for any site the inspecting proxy does intercept.
local ANCHOR_D FF_CERT_FILE="" FF_CERT_JSON=""
ANCHOR_D="$(_anchor_dir)"
if [[ -d "$ANCHOR_D" ]]; then
FF_CERT_FILE=$(ls "$ANCHOR_D"/*GORTT* "$ANCHOR_D"/*Tobago* 2>/dev/null | head -1)
fi
if [[ -n "$FF_CERT_FILE" ]]; then
FF_CERT_JSON=",
\"Certificates\": {
\"Install\": [\"${FF_CERT_FILE}\"]
}"
fi
mkdir -p /etc/firefox/policies
cat > /etc/firefox/policies/policies.json <<FFEOF
{
"policies": {
"Proxy": {
"Mode": "manual",
"HTTPProxy": "${PROXY_HOST}:${PROXY_PORT}",
"HTTPSProxy": "${PROXY_HOST}:${PROXY_PORT}",
"FTPProxy": "${PROXY_HOST}:${PROXY_PORT}",
"Passthrough": "${NO_PROXY_LIST}"
},
"Homepage": {
"URL": "${FIREFOX_HOMEPAGE}",
"Locked": false,
"StartPage": "homepage"
}${FF_CERT_JSON}
}
}
FFEOF
if command -v python3 &>/dev/null; then
python3 -c "import json; json.load(open('/etc/firefox/policies/policies.json'))" \
&& success "policies.json is valid JSON" \
|| error "policies.json failed to parse — Firefox will ignore it silently. Check FF_CERT_FILE for unescaped characters."
fi
}
mod_proxy_toggle() {
step "Installing Proxy Toggle Tool"
cat > /usr/local/bin/toggle-proxy <<EOF
#!/usr/bin/env bash
# System-Wide Proxy Toggle (${SCRIPT_VERSION})
# Usage: sudo toggle-proxy [on|off]
if [[ "\$EUID" -ne 0 ]]; then
echo "Please run as root (sudo toggle-proxy on|off)"
exit 1
fi
MODE=\$1
PROXY_URL="${PROXY_URL}"
PROXY_HOST="\$(echo "\$PROXY_URL" | awk -F/ '{print \$3}' | cut -d: -f1)"
PROXY_PORT="\$(echo "\$PROXY_URL" | awk -F: '{print \$NF}')"
NO_PROXY_LIST="${NO_PROXY_LIST}"
PROXY_SERVICES="docker packagekit flatpak-system-helper fwupd"
if command -v apt-get &>/dev/null; then rm -f /etc/apt/apt.conf.d/80proxy; fi
if command -v dnf &>/dev/null; then sed -i '/^proxy=/d' /etc/dnf/dnf.conf 2>/dev/null || true; fi
if [[ "\$MODE" == "on" ]]; then
echo "Enabling System Proxy..."
cat > /etc/profile.d/proxy.sh <<ENVEOF
export http_proxy="\${PROXY_URL}"
export https_proxy="\${PROXY_URL}"
export ftp_proxy="\${PROXY_URL}"
export no_proxy="\${NO_PROXY_LIST}"
export HTTP_PROXY="\${PROXY_URL}"
export HTTPS_PROXY="\${PROXY_URL}"
export FTP_PROXY="\${PROXY_URL}"
export NO_PROXY="\${NO_PROXY_LIST}"
ENVEOF
sed -i -E '/^(http_proxy|https_proxy|ftp_proxy|no_proxy|HTTP_PROXY|HTTPS_PROXY|FTP_PROXY|NO_PROXY)=/d' /etc/environment 2>/dev/null || true
cat >> /etc/environment <<ENVEOF2
http_proxy=\${PROXY_URL}
https_proxy=\${PROXY_URL}
ftp_proxy=\${PROXY_URL}
no_proxy=\${NO_PROXY_LIST}
HTTP_PROXY=\${PROXY_URL}
HTTPS_PROXY=\${PROXY_URL}
FTP_PROXY=\${PROXY_URL}
NO_PROXY=\${NO_PROXY_LIST}
ENVEOF2
mkdir -p /etc/fish/conf.d
cat > /etc/fish/conf.d/proxy.fish <<FISHEOF
set -gx http_proxy "\${PROXY_URL}"
set -gx https_proxy "\${PROXY_URL}"
set -gx ftp_proxy "\${PROXY_URL}"
set -gx no_proxy "\${NO_PROXY_LIST}"
set -gx HTTP_PROXY "\${PROXY_URL}"
set -gx HTTPS_PROXY "\${PROXY_URL}"
set -gx FTP_PROXY "\${PROXY_URL}"
set -gx NO_PROXY "\${NO_PROXY_LIST}"
FISHEOF
for SVC in \${PROXY_SERVICES}; do
mkdir -p /etc/systemd/system/\${SVC}.service.d
cat > /etc/systemd/system/\${SVC}.service.d/http-proxy.conf <<DOCKEREOF
[Service]
Environment="HTTP_PROXY=\${PROXY_URL}"
Environment="HTTPS_PROXY=\${PROXY_URL}"
Environment="http_proxy=\${PROXY_URL}"
Environment="https_proxy=\${PROXY_URL}"
Environment="NO_PROXY=\${NO_PROXY_LIST}"
Environment="no_proxy=\${NO_PROXY_LIST}"
DOCKEREOF
done
systemctl daemon-reload
killall packagekitd 2>/dev/null || true
killall flatpak-system-helper 2>/dev/null || true
systemctl try-restart docker packagekit flatpak-system-helper fwupd 2>/dev/null || true
FPREPO="/var/lib/flatpak/repo"
if [[ -f "\$FPREPO/config" ]]; then
if command -v ostree &>/dev/null; then
ostree --repo="\$FPREPO" config set 'remote "flathub".proxy' "\${PROXY_URL}" 2>/dev/null || true
else
sed -i '/^\[remote "flathub"\]/,/^\[/{ /^proxy=/d }' "\$FPREPO/config" 2>/dev/null || true
sed -i "/^\[remote \"flathub\"\]/a proxy=\${PROXY_URL}" "\$FPREPO/config" 2>/dev/null || true
fi
fi
if command -v dconf &>/dev/null; then
mkdir -p /etc/dconf/db/local.d
sed -i "s/mode='none'/mode='manual'/" /etc/dconf/db/local.d/01-proxy 2>/dev/null || true
dconf update
fi
if [[ -f /etc/xdg/kioslaverc ]]; then
sed -i "s/ProxyType=0/ProxyType=1/" /etc/xdg/kioslaverc 2>/dev/null || true
fi
mkdir -p /etc/firefox/policies
cat > /etc/firefox/policies/policies.json <<FFEOF
{
"policies": {
"Proxy": {
"Mode": "manual",
"HTTPProxy": "\${PROXY_HOST}:\${PROXY_PORT}",
"HTTPSProxy": "\${PROXY_HOST}:\${PROXY_PORT}",
"FTPProxy": "\${PROXY_HOST}:\${PROXY_PORT}",
"Passthrough": "\${NO_PROXY_LIST}"
}
}
}
FFEOF
echo "[OK] Proxy is ON. Log out and back in (or reboot) for GUI sessions to update."
elif [[ "\$MODE" == "off" ]]; then
echo "Disabling System Proxy..."
> /etc/profile.d/proxy.sh
rm -f /etc/fish/conf.d/proxy.fish
sed -i -E '/^(http_proxy|https_proxy|ftp_proxy|no_proxy|HTTP_PROXY|HTTPS_PROXY|FTP_PROXY|NO_PROXY)=/d' /etc/environment 2>/dev/null || true
for SVC in \${PROXY_SERVICES}; do
rm -f /etc/systemd/system/\${SVC}.service.d/http-proxy.conf
done
systemctl daemon-reload
killall packagekitd 2>/dev/null || true
killall flatpak-system-helper 2>/dev/null || true
systemctl try-restart docker flatpak-system-helper fwupd 2>/dev/null || true
if command -v sqlite3 &>/dev/null && [ -f /var/lib/PackageKit/transactions.db ]; then
sqlite3 /var/lib/PackageKit/transactions.db "DELETE FROM proxy;" || true
else
rm -f /var/lib/PackageKit/transactions.db || true
fi
systemctl try-restart packagekit 2>/dev/null || true
FPREPO="/var/lib/flatpak/repo"
if [[ -f "\$FPREPO/config" ]]; then
if command -v ostree &>/dev/null; then
ostree --repo="\$FPREPO" config unset 'remote "flathub".proxy' 2>/dev/null || true
else
sed -i '/^\[remote "flathub"\]/,/^\[/{ /^proxy=/d }' "\$FPREPO/config" 2>/dev/null || true
fi
fi
if command -v dconf &>/dev/null; then
mkdir -p /etc/dconf/db/local.d
sed -i "s/mode='manual'/mode='none'/" /etc/dconf/db/local.d/01-proxy 2>/dev/null || true
dconf update
fi
if [[ -f /etc/xdg/kioslaverc ]]; then
sed -i "s/ProxyType=1/ProxyType=0/" /etc/xdg/kioslaverc 2>/dev/null || true
fi
mkdir -p /etc/firefox/policies
cat > /etc/firefox/policies/policies.json <<FFEOF
{
"policies": {
"Proxy": {
"Mode": "none"
}
}
}
FFEOF
echo "[OK] Proxy is OFF. Log out and back in (or reboot) for GUI sessions to update."
else
echo "Usage: toggle-proxy [on|off]"
fi
EOF
chmod +x /usr/local/bin/toggle-proxy
}
mod_flatpak() {
step "Configuring Flatpak & Flathub"
if [[ "$PKG" == "apt-get" ]]; then
run_retry apt-get install -y flatpak
if [ "$HAS_GNOME" = true ]; then run_retry apt-get install -y gnome-software-plugin-flatpak; fi
if [ "$HAS_KDE" = true ]; then run_retry apt-get install -y plasma-discover-backend-flatpak; fi
elif [[ "$PKG" == "dnf" || "$PKG" == "yum" ]]; then
run_retry $PKG install -y flatpak
if [ "$HAS_GNOME" = true ]; then run_retry $PKG install -y gnome-software; fi
if [ "$HAS_KDE" = true ]; then run_retry $PKG install -y plasma-discover-flatpak; fi
elif [[ "$PKG" == "pacman" ]]; then
run_retry pacman -S --noconfirm flatpak
if [ "$HAS_GNOME" = true ]; then run_retry pacman -S --noconfirm gnome-software; fi
if [ "$HAS_KDE" = true ]; then run_retry pacman -S --noconfirm discover; fi
fi
FLATHUB_URL="https://dl.flathub.org/repo/"
FP_CONFIG="/var/lib/flatpak/repo/config"
NEED_READD=false
if flatpak remotes 2>/dev/null | grep -q '^flathub'; then
CUR_URL=$(awk '
/^\[remote "flathub"\]/ {f=1; next}
/^\[/ {f=0}
f && /^url=/ {sub(/^url=/,""); print; exit}
' "$FP_CONFIG" 2>/dev/null)
if [[ "$CUR_URL" != "$FLATHUB_URL" ]]; then
warn "flathub remote has WRONG url ('${CUR_URL}') — deleting and re-adding correctly."
flatpak remote-delete --force flathub || true
NEED_READD=true
fi
else
NEED_READD=true
fi
if [ "$NEED_READD" = true ]; then
run_retry curl -sf -x "${PROXY_URL}" -o /tmp/flathub.flatpakrepo "${FLATHUB_URL}flathub.flatpakrepo"
run_retry flatpak remote-add --if-not-exists flathub /tmp/flathub.flatpakrepo
rm -f /tmp/flathub.flatpakrepo
fi
pin_flatpak_proxy
}
mod_desktop_tools() {
step "Installing Desktop Utilities & GUI Tools"
if [[ "$PKG" == "dnf" || "$PKG" == "yum" ]]; then
run_retry $PKG install -y fastfetch || run_retry $PKG install -y neofetch || true
elif [[ "$PKG" == "apt-get" ]]; then
run_retry apt-get install -y fastfetch || run_retry apt-get install -y neofetch || true
elif [[ "$PKG" == "pacman" ]]; then
run_retry pacman -S --noconfirm fastfetch || true
fi
if [ "$HAS_GNOME" = true ]; then
log "GNOME DE detected. Deploying Tweaks, Flatseal, and ExtensionManager..."
if [[ "$PKG" == "apt-get" ]]; then run_retry apt-get install -y gnome-tweaks sqlite3
elif [[ "$PKG" == "dnf" || "$PKG" == "yum" ]]; then run_retry $PKG install -y gnome-tweaks sqlite
elif [[ "$PKG" == "pacman" ]]; then run_retry pacman -S --noconfirm gnome-tweaks sqlite
fi
if command -v flatpak &>/dev/null; then
pin_flatpak_proxy
http_proxy="${PROXY_URL}" https_proxy="${PROXY_URL}" HTTP_PROXY="${PROXY_URL}" HTTPS_PROXY="${PROXY_URL}" \
run_retry flatpak install -y flathub com.mattjakeman.ExtensionManager
http_proxy="${PROXY_URL}" https_proxy="${PROXY_URL}" HTTP_PROXY="${PROXY_URL}" HTTPS_PROXY="${PROXY_URL}" \
run_retry flatpak install -y flathub com.github.tchx84.Flatseal
fi
fi
}
mod_gs_fix() {
step "GNOME Software / App Center Performance Fixes"
pin_flatpak_proxy
mkdir -p /etc/systemd/system/fwupd.service.d
cat > /etc/systemd/system/fwupd.service.d/http-proxy.conf <<EOF
[Service]
Environment="HTTP_PROXY=${PROXY_URL}"
Environment="HTTPS_PROXY=${PROXY_URL}"
Environment="http_proxy=${PROXY_URL}"
Environment="https_proxy=${PROXY_URL}"
Environment="NO_PROXY=${NO_PROXY_LIST}"
Environment="no_proxy=${NO_PROXY_LIST}"
EOF
systemctl daemon-reload
systemctl try-restart fwupd 2>/dev/null || true
systemctl stop packagekit 2>/dev/null || true
if command -v sqlite3 &>/dev/null && [ -f /var/lib/PackageKit/transactions.db ]; then
sqlite3 /var/lib/PackageKit/transactions.db "DELETE FROM proxy;" 2>/dev/null || true
fi
systemctl start packagekit 2>/dev/null || true
if command -v appstreamcli &>/dev/null; then
http_proxy="${PROXY_URL}" https_proxy="${PROXY_URL}" appstreamcli refresh --force 2>/dev/null || true
fi
pkill -f "gnome-software" 2>/dev/null || true
rm -rf /var/cache/gnome-software 2>/dev/null || true
for USERDIR in /home/*; do
[ -d "$USERDIR/.cache/gnome-software" ] && rm -rf "$USERDIR/.cache/gnome-software" || true
done
success "GNOME Software backends re-pointed at proxy. First relaunch may still take ~30s to rebuild caches; subsequent launches should be fast."
}
mod_desktop_network() {
step "Propagating Proxy to Desktop Session & System Services"
# /etc/environment and /etc/profile.d/proxy.sh only reach interactive
# login shells. GNOME Shell, systemd --user units (the graphical
# session's own service manager), and systemd SYSTEM services like
# geoclue never source either file. Three gaps, three fixes:
# 1. systemd --user units (GNOME Shell, gsettings-backed apps, anything
# launched by the graphical session) — read from /etc/environment.d/
# at login. Unlike /etc/environment, this format does NOT support
# quotes at all — even innocuous ones would become part of the value.
mkdir -p /etc/environment.d
cat > /etc/environment.d/60-m21-proxy.conf <<EOF
http_proxy=${PROXY_URL}
https_proxy=${PROXY_URL}
ftp_proxy=${PROXY_URL}
no_proxy=${NO_PROXY_LIST}
HTTP_PROXY=${PROXY_URL}
HTTPS_PROXY=${PROXY_URL}
FTP_PROXY=${PROXY_URL}
NO_PROXY=${NO_PROXY_LIST}
EOF
success "Wrote /etc/environment.d/60-m21-proxy.conf (systemd --user sessions)"
# 2. Every systemd SYSTEM service, present and future — DefaultEnvironment
# in system.conf.d applies manager-wide. This is the catch-all for
# services we haven't individually thought to add a drop-in for.
# Requires daemon-reexec (not just daemon-reload) to take effect for
# the running manager — daemon-reload only reloads unit files, not
# the manager's own configuration.
mkdir -p /etc/systemd/system.conf.d
cat > /etc/systemd/system.conf.d/60-m21-proxy.conf <<EOF
[Manager]
DefaultEnvironment="http_proxy=${PROXY_URL}" "https_proxy=${PROXY_URL}" "no_proxy=${NO_PROXY_LIST}" "HTTP_PROXY=${PROXY_URL}" "HTTPS_PROXY=${PROXY_URL}" "NO_PROXY=${NO_PROXY_LIST}"
EOF
systemctl daemon-reexec
success "DefaultEnvironment set manager-wide (daemon-reexec applied it now)"
# 3. geoclue specifically, with an explicit per-unit drop-in and an
# immediate restart. It's the concrete symptom (slow/failing location
# lookups behind weather widgets, GNOME Maps, etc.) and DefaultEnvironment
# alone wouldn't touch it until next reboot if it's already running.
if systemctl list-unit-files 2>/dev/null | grep -q '^geoclue\.service'; then
mkdir -p /etc/systemd/system/geoclue.service.d
cat > /etc/systemd/system/geoclue.service.d/proxy.conf <<EOF
[Service]
Environment="http_proxy=${PROXY_URL}"
Environment="https_proxy=${PROXY_URL}"
Environment="no_proxy=${NO_PROXY_LIST}"
Environment="HTTP_PROXY=${PROXY_URL}"
Environment="HTTPS_PROXY=${PROXY_URL}"
Environment="NO_PROXY=${NO_PROXY_LIST}"
EOF
systemctl daemon-reload
systemctl try-restart geoclue.service 2>/dev/null || true
success "geoclue.service proxy drop-in applied and restarted"
else
log "geoclue.service not present on this host — skipping (server, presumably)."
fi
# Quick reachability check against the backend geoclue actually queries,
# so you know immediately whether this fixed it or whether the endpoint
# is category-blocked by the inspecting proxy (same pattern as
# apps.nextcloud.com / services.glpi-network.com elsewhere in this script).
log "Testing reachability of the Mozilla Location Service (geoclue's backend)..."
if url_alive "https://location.services.mozilla.com/"; then
success "location.services.mozilla.com reachable — location lookups should work now."
else
warn "location.services.mozilla.com did not return 200 through the proxy."
warn "If this persists after the fixes above, it's likely category-blocked —"
warn "same as other software-service domains. Request an allowlist entry."
fi
}
mod_clock_fix() {
step "Synchronizing System Clock"
timedatectl set-timezone "$TARGET_TIMEZONE" || true
timedatectl set-ntp true 2>/dev/null || true
# chrony (configured in mod_network) is the authoritative NTP source. On hosts
# where the chrony package has masked systemd-timesyncd, that's correct — do
# not try to restart a masked unit.
if systemctl list-unit-files | grep -q systemd-timesyncd; then
if ! systemctl is-enabled systemd-timesyncd 2>/dev/null | grep -q masked; then
timeout 30s systemctl restart systemd-timesyncd || true
fi
fi
}
mod_base_repos() {
step "Configuring Base OS Repositories"
if [[ "$IS_EL7" == true ]]; then
local M BASE="" GPG="/etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7"
# CentOS 7 went EOL 2024-06-30 and mirror.centos.org is gone. Probe the
# archives in order and use the first that actually answers 200 — yum's
# "Trying other mirror" masks 301/303 responses, and the iGov proxy
# returns 303 (Check Point UserCheck) for blocked categories.
log "CentOS 7 is EOL — locating a reachable archive mirror..."
for M in $EL7_BASE_MIRRORS; do
if url_alive "${M}/os/x86_64/repodata/repomd.xml"; then
BASE="$M"; success "Using archive: ${M}"; break
else
warn "Unreachable: ${M}"
fi
done
if [[ -z "$BASE" ]]; then
error "No CentOS 7 archive mirror is reachable from this host."
error "Every candidate returned a non-200. This is almost certainly the"
error "proxy category-blocking them, not the mirrors being down."
error "Leaving the existing repo files ALONE. Request an allowlist for:"
for M in $EL7_BASE_MIRRORS; do error " ${M}"; done
else
[[ -f /etc/yum.repos.d/CentOS-Base.repo ]] && \
cp -a /etc/yum.repos.d/CentOS-Base.repo "/etc/yum.repos.d/CentOS-Base.repo.m21.bak-$(date +%s)"
cat > /etc/yum.repos.d/CentOS-Base.repo <<EOF
# CentOS 7 EOL archive — written by m21 setup ${SCRIPT_VERSION}
# mirrorlist is deliberately absent: there are no mirrors left, and the proxy
# returns 303 on mirrorlist URLs.
[base]
name=CentOS-7 - Base (archive)
baseurl=${BASE}/os/\$basearch/
gpgcheck=1
gpgkey=file://${GPG}
[updates]
name=CentOS-7 - Updates (archive)
baseurl=${BASE}/updates/\$basearch/
gpgcheck=1
gpgkey=file://${GPG}
[extras]
name=CentOS-7 - Extras (archive)
baseurl=${BASE}/extras/\$basearch/
gpgcheck=1
gpgkey=file://${GPG}
[centosplus]
name=CentOS-7 - Plus (archive)
enabled=1
baseurl=${BASE}/centosplus/\$basearch/
gpgcheck=1
gpgkey=file://${GPG}
EOF
# SCLo lives under the same archive root.
if url_alive "${BASE}/sclo/x86_64/rh/repodata/repomd.xml"; then
cat > /etc/yum.repos.d/CentOS-SCLo-scl.repo <<EOF
# Written by m21 setup ${SCRIPT_VERSION}
[centos-sclo-rh]
name=CentOS-7 - SCLo rh (archive)
baseurl=${BASE}/sclo/\$basearch/rh/
gpgcheck=0
[centos-sclo-sclo]
name=CentOS-7 - SCLo sclo (archive)
baseurl=${BASE}/sclo/\$basearch/sclo/
gpgcheck=0
EOF
fi
yum clean metadata >/dev/null 2>&1 || true
fi
# EPEL 7 is archived too. Keep the working metalink if it responds,
# otherwise pin the archive.
if ! rpm -q epel-release >/dev/null 2>&1; then
run_retry $PKG install -y epel-release || true
fi
if ! url_alive "${EL7_EPEL_ARCHIVE}/x86_64/repodata/repomd.xml"; then
warn "EPEL 7 archive unreachable — leaving the existing epel config in place."
elif ! yum --disablerepo='*' --enablerepo='epel' makecache >/dev/null 2>&1; then
warn "Existing EPEL 7 config is not working — repointing at the archive."
cat > /etc/yum.repos.d/epel.repo <<EOF
# Written by m21 setup ${SCRIPT_VERSION}
[epel]
name=EPEL 7 (archive)
baseurl=${EL7_EPEL_ARCHIVE}/\$basearch/
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-EPEL-7
EOF
yum clean metadata >/dev/null 2>&1 || true
fi
# Any PRE-EXISTING Remi repo config uses the same cdn.remirepo.net
# mirrorlist the iGov proxy 303s on — not just the config mod_web_stack
# writes when THIS script installs PHP. A host like edrive had Remi PHP
# installed long before this script existed, so --basics alone (which
# never touches PHP) had no way to know that config was broken until
# it hit exactly this failure on a plain --basics run. run_retry
# papers over it some of the time; fix it properly instead.
local RF PHPNUM
for RF in /etc/yum.repos.d/remi-php*.repo; do
[[ -f "$RF" ]] || continue
PHPNUM=$(basename "$RF" | sed -n 's/^remi-php\([0-9][0-9]*\)\.repo$/\1/p')
[[ -z "$PHPNUM" ]] && continue
if grep -q '^enabled=1' "$RF" 2>/dev/null && grep -q 'cdn\.remirepo\.net' "$RF" 2>/dev/null; then
log "Repointing pre-existing remi-php${PHPNUM}.repo at an explicit baseurl."
cp -a "$RF" "${RF}.m21.bak-$(date +%s)"
sed -i 's/^enabled=1/enabled=0/' "$RF"
cat > "/etc/yum.repos.d/remi-php${PHPNUM}-m21.repo" <<EOF
# Written by m21 setup ${SCRIPT_VERSION} — explicit baseurl, no mirrorlist.
[remi-php${PHPNUM}]
name=Remi PHP ${PHPNUM} (EL7)
baseurl=${EL7_REMI_BASE}/php${PHPNUM}/\$basearch/
enabled=1
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-remi
EOF
fi
done
if [[ -f /etc/yum.repos.d/remi-safe.repo ]] && grep -q '^enabled=1' /etc/yum.repos.d/remi-safe.repo 2>/dev/null && grep -q 'cdn\.remirepo\.net' /etc/yum.repos.d/remi-safe.repo 2>/dev/null; then
log "Repointing pre-existing remi-safe.repo at an explicit baseurl."
cp -a /etc/yum.repos.d/remi-safe.repo "/etc/yum.repos.d/remi-safe.repo.m21.bak-$(date +%s)"
sed -i 's/^enabled=1/enabled=0/' /etc/yum.repos.d/remi-safe.repo
cat > /etc/yum.repos.d/remi-safe-m21.repo <<EOF
# Written by m21 setup ${SCRIPT_VERSION}
[remi-safe]
name=Remi safe (EL7)
baseurl=${EL7_REMI_BASE}/safe/\$basearch/
enabled=1
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-remi
EOF
fi
yum clean metadata >/dev/null 2>&1 || true
return 0
fi
if [[ "$PKG" == "dnf" ]]; then
if [[ "$OS_ID" == "fedora" ]]; then
log "Setting up Fedora 3rd Party Repos (RPM Fusion & Workstation Repos)..."
run_retry dnf install -y dnf-plugins-core fedora-workstation-repositories || true
run_retry dnf install -y "https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-${VERSION_MAJOR}.noarch.rpm" \
"https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-${VERSION_MAJOR}.noarch.rpm" || true
dnf config-manager --set-enabled rpmfusion-free rpmfusion-nonfree 2>/dev/null \
|| dnf config-manager enable rpmfusion-free rpmfusion-nonfree 2>/dev/null || true
else
if ! rpm -q epel-release >/dev/null 2>&1; then run_retry $PKG install -y epel-release; fi
if ! dnf repolist enabled 2>/dev/null | grep -E "crb|powertools" >/dev/null; then
run_retry $PKG install -y 'dnf-command(config-manager)'
$PKG config-manager --set-enabled crb 2>/dev/null \
|| $PKG config-manager --set-enabled powertools 2>/dev/null \
|| $PKG config-manager enable crb 2>/dev/null \
|| $PKG config-manager enable powertools 2>/dev/null || true
fi
fi
elif [[ "$PKG" == "apt-get" ]]; then
export DEBIAN_FRONTEND=noninteractive
rm -f /etc/apt/sources.list.d/45drives.list
apt-get update -qq || true
BASE_APT_PKGS="curl wget gnupg lsb-release ca-certificates"
if [[ "$OS_ID" != "debian" ]]; then BASE_APT_PKGS="software-properties-common $BASE_APT_PKGS"; fi
run_retry apt-get install -y $BASE_APT_PKGS
fi
}
mod_base_tools() {
step "Installing Base System Tools"
# openldap-clients: probe_dc needs a real LDAP query, not a TCP connect.
# krb5-workstation: kinit, for manual join recovery when realmd times out.
if [[ "$PKG" == "dnf" || "$PKG" == "yum" ]]; then
PACKAGES="git curl wget nano neovim zsh bind-utils net-tools openssl psmisc PackageKit pcp pcp-conf pcp-libs pcp-selinux openldap-clients krb5-workstation"
if [[ "$IS_EL7" == true ]]; then
# el7 has no util-linux-user (folded into util-linux) and names the
# SELinux tooling policycoreutils-python, not -python-utils.
PACKAGES="$PACKAGES policycoreutils-python"
else
PACKAGES="$PACKAGES util-linux-user policycoreutils-python-utils"
fi
run_retry $PKG install -y $PACKAGES
elif [[ "$PKG" == "pacman" ]]; then
PACKAGES="git curl wget nano neovim zsh openssl net-tools bind psmisc networkmanager openldap krb5"
run_retry pacman -S --noconfirm $PACKAGES
timeout 30s systemctl enable --now NetworkManager || true
else
PACKAGES="git curl wget nano neovim zsh openssl net-tools dnsutils psmisc packagekit pcp network-manager ldap-utils krb5-user"
run_retry apt-get install -y $PACKAGES
timeout 30s systemctl enable --now NetworkManager || true
fi
systemctl unmask packagekit 2>/dev/null || true
timeout 30s systemctl start packagekit 2>/dev/null || true
}
mod_network() {
step "Configuring Network & DNS"
local HEALTHY_DCS=() IP DEFAULT_IFACE CONN DNS_CSV CHRONY_CONF
if [[ "$PKG" == "apt-get" ]] && command -v netplan >/dev/null 2>&1; then
if ls /etc/netplan/*.yaml >/dev/null 2>&1 && grep -q "addresses:" /etc/netplan/*.yaml; then
log "Static Netplan detected. Skipping wipe to prevent lockout."
else
mkdir -p /etc/netplan
cat > /etc/netplan/01-network-manager-all.yaml <<EOF
network:
version: 2
renderer: NetworkManager
EOF
netplan apply || true
fi
fi
for IP in $DC_LIST; do
if probe_dc "$IP"; then
HEALTHY_DCS+=("$IP"); success "DC ${IP} answers LDAP"
else
warn "DC ${IP} did NOT answer LDAP — excluded from DNS and /etc/hosts"
fi
done
if [[ ${#HEALTHY_DCS[@]} -eq 0 ]]; then
error "No domain controller answered an LDAP query."
error "Leaving /etc/resolv.conf and /etc/hosts UNTOUCHED."
return 1
fi
DC_DNS_IP="${HEALTHY_DCS[0]}"
log "Primary DC: ${DC_DNS_IP} (${#HEALTHY_DCS[@]} healthy of $(echo $DC_LIST | wc -w))"
# Purge ANY line anywhere in the file that resolves one of our managed
# hostnames — not just inside our own marker block. glibc's hosts
# resolver returns the FIRST match in the file; a stale, unmarked entry
# left by an older script version (or a manual edit) sitting ABOVE where
# we append our managed block silently wins forever, no matter how
# correct our own entry is. This bit m21-ict-pc13 on 2026-07-31: a
# leftover `172.16.21.161 m21.gov.local` line predating the marker
# convention kept the dead DC in play through /etc/hosts even after DNS
# itself was already fixed. Match by hostname TOKEN, not marker position,
# so this self-heals regardless of the file's prior history.
cp -a /etc/hosts "/etc/hosts.m21.bak-$(date +%s)" 2>/dev/null || true
awk -v hosts="${DOMAIN_FQDN} ${DOMAIN_ALT} ${DOMAIN_SHORT} ${FILE_SERVER_NAME}.${DOMAIN_FQDN} ${FILE_SERVER_NAME}.${DOMAIN_ALT} ${FILE_SERVER_NAME}" '
BEGIN { n = split(hosts, a, " "); for (i = 1; i <= n; i++) managed[a[i]] = 1 }
/# >>> m21-hosts >>>/ { next }
/# <<< m21-hosts <<</ { next }
/^[[:space:]]*#/ { print; next }
{
drop = 0
for (i = 2; i <= NF; i++) { if ($i in managed) { drop = 1; break } }
if (!drop) print
}
' /etc/hosts > /etc/hosts.m21.new && mv /etc/hosts.m21.new /etc/hosts
{
echo "# >>> m21-hosts >>> managed by m21 setup — edits here are overwritten"
echo "${DC_DNS_IP} ${DOMAIN_FQDN} ${DOMAIN_ALT} ${DOMAIN_SHORT}"
echo "${FILE_SERVER_IP} ${FILE_SERVER_NAME}.${DOMAIN_FQDN} ${FILE_SERVER_NAME}.${DOMAIN_ALT} ${FILE_SERVER_NAME}"
echo "# <<< m21-hosts <<<"
} >> /etc/hosts
cp -a /etc/resolv.conf "/etc/resolv.conf.m21.bak-$(date +%s)" 2>/dev/null || true
if [[ -L /etc/resolv.conf ]]; then rm -f /etc/resolv.conf; fi
{
echo "search ${DOMAIN_FQDN} ${DOMAIN_ALT}"
for IP in "${HEALTHY_DCS[@]:0:3}"; do echo "nameserver ${IP}"; done
} > /etc/resolv.conf
# Persist to the NM profile but do NOT bring the connection up — that drops
# the admin's SSH session and can silently reassign the firewalld zone.
# resolv.conf above already applies immediately.
if command -v nmcli &>/dev/null; then
DEFAULT_IFACE=$(ip -4 route show default | awk '{for(i=1;i<=NF;i++) if($i=="dev") print $(i+1); exit}')
if [[ -n "$DEFAULT_IFACE" ]]; then
CONN=$(nmcli -t -f NAME,DEVICE con show --active | grep ":${DEFAULT_IFACE}$" | cut -d: -f1 | head -n1)
if [[ -n "$CONN" ]]; then
DNS_CSV=$(IFS=,; echo "${HEALTHY_DCS[*]:0:3}")
nmcli con mod "$CONN" ipv4.dns "$DNS_CSV" \
ipv4.dns-search "${DOMAIN_FQDN},${DOMAIN_ALT}" ipv4.ignore-auto-dns yes || true
log "DNS persisted on '${CONN}'. Interface NOT bounced."
else
warn "No active NM connection for ${DEFAULT_IFACE} — resolv.conf may not survive a reboot."
fi
fi
fi
echo -e "net.ipv6.conf.all.disable_ipv6 = 1\nnet.ipv6.conf.default.disable_ipv6 = 1" > /etc/sysctl.d/90-disable-ipv6.conf
sysctl --system &>/dev/null || true
if command -v systemctl &>/dev/null; then
if [[ "$PKG" == "apt-get" ]]; then run_retry apt-get install -y chrony; CHRONY_CONF="/etc/chrony/chrony.conf"
elif [[ "$PKG" == "pacman" ]]; then run_retry pacman -S --noconfirm chrony; CHRONY_CONF="/etc/chrony.conf"
else run_retry $PKG install -y chrony; CHRONY_CONF="/etc/chrony.conf"; fi
if [[ -f "$CHRONY_CONF" ]]; then
cp -a "$CHRONY_CONF" "${CHRONY_CONF}.m21.bak-$(date +%s)" 2>/dev/null || true
sed -i '/^server /d; /^pool /d' "$CHRONY_CONF" 2>/dev/null || true
grep -q "^server ${NTP_SERVER} iburst" "$CHRONY_CONF" || echo "server ${NTP_SERVER} iburst" >> "$CHRONY_CONF"
fi
timeout 30s systemctl restart chronyd 2>/dev/null || timeout 30s systemctl restart chrony || true
fi
}
mod_firewall() {
step "Configuring Firewalld (Defense in Depth)"
local MY_SRC COVERED C
if [[ "$PKG" == "apt-get" ]]; then
run_retry apt-get install -y firewalld
systemctl disable ufw --now 2>/dev/null || true
elif [[ "$PKG" == "pacman" ]]; then
run_retry pacman -S --noconfirm firewalld
else
run_retry $PKG install -y firewalld
fi
systemctl enable --now firewalld
firewall-cmd --permanent --zone=trusted --add-source=172.17.0.0/16
firewall-cmd --permanent --zone=trusted --add-source=172.18.0.0/16
firewall-cmd --permanent --zone=trusted --add-source=172.19.0.0/16
firewall-cmd --permanent --zone=trusted --add-source=172.20.0.0/16
firewall-cmd --permanent --zone=trusted --add-source=192.168.250.0/24
# DMZ peer-to-peer trust — every VM on DMZ_TRUSTED_CIDRS can reach every
# other VM on the same subnet(s), on any port. Deliberately NOT scoped to
# individual peer IPs: the point of a shared trusted subnet is that any
# DMZ host talks to any DMZ host with zero per-host tailoring, and this
# never needs to change when a new DMZ VM is added. Only trusts internal
# peers — the public zone (http/https + the ssh rich rules below) still
# governs everything arriving from outside the DMZ.
for C in $DMZ_TRUSTED_CIDRS; do
firewall-cmd --permanent --zone=trusted --add-source="$C"
done
firewall-cmd --permanent --add-service=http
firewall-cmd --permanent --add-service=https
# Scoping ssh to rich rules is only safe if your own source is inside one.
# Otherwise --reload disconnects you from a remote host with no console.
MY_SRC="${SSH_CLIENT%% *}"
if [[ -n "$MY_SRC" ]]; then
COVERED=false
for C in $SSH_ALLOWED_CIDRS; do
if ip_in_cidr "$MY_SRC" "$C"; then COVERED=true; break; fi
done
if [[ "$COVERED" != true ]]; then
error "Your SSH source ${MY_SRC} is not inside any allowed range:"
error " ${SSH_ALLOWED_CIDRS}"
error "Scoping ssh would lock you out at --reload. Add your range to"
error "SSH_ALLOWED_CIDRS at the top of this script, or run from console."
return 1
fi
log "SSH source ${MY_SRC} is covered — safe to proceed."
fi
firewall-cmd --permanent --remove-service=ssh
for C in $SSH_ALLOWED_CIDRS; do
firewall-cmd --permanent --add-rich-rule="rule family=\"ipv4\" source address=\"${C}\" service name=\"ssh\" accept"
done
firewall-cmd --reload
}
mod_resize_home() {
step "LVM Home Resizer"
local HOME_DEV LV_NAME VG_NAME LV_PATH ROOT_LV_PATH MAPPER_PATH CURRENT_SIZE
local HOME_KB TMP_AVAIL_KB CONFIRM
if ! command -v lvs &>/dev/null; then return 0; fi
if ! mountpoint -q /home; then return 0; fi
HOME_DEV=$(findmnt -n -o SOURCE /home)
if [[ "$HOME_DEV" != *"/mapper/"* ]]; then return 0; fi
LV_NAME=$(lvs --noheadings -o lv_name "$HOME_DEV" | tr -d ' ')
VG_NAME=$(lvs --noheadings -o vg_name "$HOME_DEV" | tr -d ' ')
LV_PATH="/dev/$VG_NAME/$LV_NAME"
ROOT_LV_PATH="/dev/$VG_NAME/root"
MAPPER_PATH="/dev/mapper/${VG_NAME}-${LV_NAME}"
CURRENT_SIZE=$(lvs --noheadings -o lv_size --units g "$LV_PATH" 2>/dev/null | tr -d 'g ' || lvs --noheadings -o L_SIZE --units g "$LV_PATH" | tr -d 'g ')
if [[ ${CURRENT_SIZE%.*} -le 9 ]]; then
log "/home is already ${CURRENT_SIZE}G — nothing to do."; return 0
fi
# A truncated backup plus a successful lvremove is unrecoverable.
HOME_KB=$(du -sk /home 2>/dev/null | awk '{print $1}')
TMP_AVAIL_KB=$(df -Pk /tmp | awk 'NR==2 {print $4}')
if [[ -z "$HOME_KB" || -z "$TMP_AVAIL_KB" ]] || (( TMP_AVAIL_KB < HOME_KB * 2 )); then
error "Insufficient free space in /tmp to back up /home."
error " /home ~$(( ${HOME_KB:-0} / 1024 )) MB; /tmp free ~$(( ${TMP_AVAIL_KB:-0} / 1024 )) MB."
return 1
fi
warn "About to DESTROY and recreate ${LV_PATH} at ${HOME_TARGET_SIZE}."
read -rp "Type exactly 'resize-home' to proceed: " CONFIRM
[[ "$CONFIRM" == "resize-home" ]] || { log "Aborted by operator."; return 0; }
tar czf /tmp/home_backup.tar.gz -C /home . || { error "Backup failed. Aborting."; return 1; }
tar tzf /tmp/home_backup.tar.gz >/dev/null 2>&1 || { error "Backup failed verification. Aborting."; return 1; }
success "Backup written and verified."
fuser -km /home || true
timeout 30s umount /home || timeout 15s umount -l /home || true
lvremove -y "$LV_PATH"
lvcreate -L "$HOME_TARGET_SIZE" -n "$LV_NAME" "$VG_NAME" -y
mkfs.ext4 "$LV_PATH"
sed -i '/\/home/d' /etc/fstab
echo "$MAPPER_PATH /home ext4 defaults 0 0" >> /etc/fstab
systemctl daemon-reload || true
timeout 30s mount /home || true
tar xzf /tmp/home_backup.tar.gz -C /home
if command -v restorecon &>/dev/null; then restorecon -R /home; fi
lvextend -l +100%FREE "$ROOT_LV_PATH"
xfs_growfs / || resize2fs "$ROOT_LV_PATH" || true
rm -f /tmp/home_backup.tar.gz
}
mod_domain_users() {
step "Domain Join & User Setup"
local RESOLVED_DC JOIN_USER SSSD_CONF
if ! timeout 15s id "$LOCAL_USER" &>/dev/null; then timeout 15s useradd -m -s /bin/bash "$LOCAL_USER" || true; fi
echo "$LOCAL_USER:$LOCAL_PASS" | chpasswd || true
timeout 15s usermod -aG sudo "$LOCAL_USER" 2>/dev/null || timeout 15s usermod -aG wheel "$LOCAL_USER" 2>/dev/null || true
if [[ "$PKG" == "apt-get" ]]; then
run_retry apt-get install -y realmd sssd sssd-tools libnss-sss libpam-sss adcli packagekit
if ! grep -q "pam_mkhomedir.so" /etc/pam.d/common-session; then
echo "session optional pam_mkhomedir.so skel=/etc/skel umask=077" >> /etc/pam.d/common-session
fi
elif [[ "$PKG" == "pacman" ]]; then
run_retry pacman -S --noconfirm sssd adcli smbclient
if ! command -v realm &>/dev/null; then
log "Warning: 'realmd' is not in standard Arch repos. Install via AUR to join the domain later."
fi
else
run_retry $PKG install -y realmd sssd oddjob oddjob-mkhomedir adcli samba-common-tools
fi
# ICMP and bare TCP reachability are NOT sufficient — the configured DC
# answered ping and accepted TCP/389 while dropping every LDAP query.
if ! getent hosts "$DOMAIN_FQDN" &>/dev/null; then
error "DNS cannot resolve ${DOMAIN_FQDN}. Run --network first."; return 1
fi
RESOLVED_DC=$(getent hosts "$DOMAIN_FQDN" | awk '{print $1}' | head -n1)
if ! probe_dc "$RESOLVED_DC"; then
error "${DOMAIN_FQDN} resolves to ${RESOLVED_DC}, which does not answer LDAP."
error "Run --network to re-probe DC_LIST, or escalate."; return 1
fi
success "Join target ${RESOLVED_DC} answers LDAP."
if command -v update-crypto-policies &>/dev/null; then
update-crypto-policies --set DEFAULT:AD-SUPPORT >/dev/null 2>&1 || true
fi
# krb5-libs (and every RHEL/Fedora base install) ships a stock krb5.conf
# with default_realm commented out. adcli's own --domain-realm argument
# is not enough — the GSSAPI/SASL library that performs the actual LDAP
# bind does its own separate profile-based realm lookup and fails with
# "Configuration file does not specify default realm" if the system
# krb5.conf has none set, even though adcli itself authenticated fine one
# step earlier. Hits every never-before-joined host identically; confirmed
# on servicedesk.m21.gov.local 2026-08-04 against the unmodified package
# template. Only touches the file if default_realm is genuinely absent.
if [[ -f /etc/krb5.conf ]] && ! grep -qE '^[[:space:]]*default_realm[[:space:]]*=' /etc/krb5.conf; then
local KRB5_REALM="${DOMAIN_FQDN^^}"
cp -a /etc/krb5.conf "/etc/krb5.conf.m21.bak-$(date +%s)"
if grep -q '^\[libdefaults\]' /etc/krb5.conf; then
sed -i "/^\[libdefaults\]/a\\ default_realm = ${KRB5_REALM}" /etc/krb5.conf
else
{ printf '[libdefaults]\n default_realm = %s\n\n' "$KRB5_REALM"; cat /etc/krb5.conf; } \
> /tmp/krb5.conf.m21.$$ && mv /tmp/krb5.conf.m21.$$ /etc/krb5.conf
fi
success "Set default_realm=${KRB5_REALM} in /etc/krb5.conf (was missing)."
fi
if command -v realm &>/dev/null; then
if ! timeout 15s realm list | grep -q "$DOMAIN_FQDN"; then
echo -e "\n${YELLOW}Enter AD Admin Username (e.g., ent_joeld):${NC}"
read -rp "User: " JOIN_USER
# --membership-software=adcli stops realmd shelling out to
# `net ads join`, which prompts a SECOND time and echoes the
# password in cleartext (it doesn't inherit the no-echo tty state).
realm join --verbose --membership-software=adcli --user="$JOIN_USER" "$DOMAIN_FQDN"
else
success "Already joined. Enforcing state..."
fi
fi
if ! command -v sshd &>/dev/null || [[ ! -f /etc/ssh/sshd_config ]]; then
log "OpenSSH Server missing or unconfigured. Installing explicitly..."
if [[ "$PKG" == "apt-get" ]]; then run_retry apt-get install -y openssh-server
elif [[ "$PKG" == "pacman" ]]; then run_retry pacman -S --noconfirm openssh
else run_retry $PKG install -y openssh-server; fi
if systemctl list-unit-files | grep -q "^ssh.service"; then
systemctl enable ssh --now || true
else
systemctl enable sshd --now || true
fi
sleep 2
fi
if [[ ! -f /etc/ssh/sshd_config ]]; then
error "/etc/ssh/sshd_config still not found. SSH AD key injection bypassed."
else
log "Configuring SSH daemon for AD-based keys..."
cp -a /etc/ssh/sshd_config "/etc/ssh/sshd_config.m21.bak-$(date +%s)" 2>/dev/null || true
sed -i '/AuthorizedKeysCommand/d' /etc/ssh/sshd_config
echo -e "\nAuthorizedKeysCommand /usr/bin/sss_ssh_authorizedkeys\nAuthorizedKeysCommandUser nobody" >> /etc/ssh/sshd_config
if systemctl list-unit-files | grep -q "^ssh.service"; then systemctl restart ssh || true
else systemctl restart sshd || true; fi
fi
SSSD_CONF="/etc/sssd/sssd.conf"
if [[ -f "$SSSD_CONF" ]]; then
cp -a "$SSSD_CONF" "${SSSD_CONF}.m21.bak-$(date +%s)" 2>/dev/null || true
timeout 15s systemctl stop sssd || true
if grep -q "^services" "$SSSD_CONF"; then
sed -i 's/^services.*/services = nss, pam, ssh/' "$SSSD_CONF"
else
sed -i '/\[sssd\]/a services = nss, pam, ssh' "$SSSD_CONF"
fi
grep -q "access_provider" "$SSSD_CONF" && sed -i 's/access_provider.*/access_provider = simple/' "$SSSD_CONF" || sed -i '/\[domain/a access_provider = simple' "$SSSD_CONF"
grep -q "simple_allow_groups" "$SSSD_CONF" && sed -i "s/simple_allow_groups.*/simple_allow_groups = ${ALLOWED_LOGIN_GROUP}/" "$SSSD_CONF" || sed -i "/access_provider = simple/a simple_allow_groups = ${ALLOWED_LOGIN_GROUP}" "$SSSD_CONF"
sed -i '/ldap_user_ssh_public_key/d' "$SSSD_CONF"
sed -i '/ldap_user_extra_attrs/d' "$SSSD_CONF"
sed -i '/\[domain/a ldap_user_extra_attrs = info:sshPublicKey\nldap_user_ssh_public_key = info' "$SSSD_CONF"
sed -i 's/use_fully_qualified_names.*/use_fully_qualified_names = False/' "$SSSD_CONF"
sed -i 's/fallback_homedir.*/fallback_homedir = \/home\/%u/' "$SSSD_CONF"
sed -i '/ignore_group_members/d' "$SSSD_CONF"
sed -i '/subdomain_enumerate/d' "$SSSD_CONF"
sed -i '/\[domain/a ignore_group_members = True\nsubdomain_enumerate = False' "$SSSD_CONF"
if ! grep -q "offline_credentials_expiration" "$SSSD_CONF"; then
sed -i '/\[domain/a cache_credentials = True\noffline_credentials_expiration = 0\naccount_cache_expiration = 2' "$SSSD_CONF"
fi
timeout 30s systemctl start sssd || true
if command -v sss_cache &>/dev/null; then sss_cache -E || true; fi
fi
}
###############################################################################
# 6. MODULAR COMPONENTS
###############################################################################
mod_docker() {
step "Installing & Configuring Docker"
if [[ "$PKG" == "dnf" || "$PKG" == "yum" ]]; then
if [[ ! -f /etc/yum.repos.d/docker-ce.repo ]]; then
run_retry $PKG install -y yum-utils
if [[ "$OS_ID" == "fedora" ]]; then
run_retry yum-config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo
else
run_retry yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
fi
fi
$PKG remove -y podman buildah docker docker-client docker-common docker-engine >/dev/null 2>&1 || true
elif [[ "$PKG" == "apt-get" ]]; then
if [[ ! -f /etc/apt/sources.list.d/docker.list ]]; then
source /etc/os-release
REPO_OS=${ID}
case "$REPO_OS" in
debian|ubuntu) : ;;
*) REPO_OS="ubuntu" ;;
esac
REPO_CODENAME="${VERSION_CODENAME:-$(command -v lsb_release >/dev/null 2>&1 && lsb_release -cs || echo stable)}"
install -m 0755 -d /etc/apt/keyrings
run_retry curl -fsSL "https://download.docker.com/linux/${REPO_OS}/gpg" -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/${REPO_OS} ${REPO_CODENAME} stable" > /etc/apt/sources.list.d/docker.list
apt-get update -qq || true
fi
fi
if ! command -v docker &>/dev/null; then
if [[ "$PKG" == "apt-get" ]]; then run_retry apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
elif [[ "$PKG" == "pacman" ]]; then run_retry pacman -S --noconfirm docker docker-compose docker-buildx
else run_retry $PKG install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin; fi
fi
mkdir -p /etc/docker
cat > /etc/docker/daemon.json <<EOF
{
"insecure-registries": [ ${INSECURE_REGISTRIES} ]
}
EOF
mkdir -p /etc/systemd/system/docker.service.d
cat > /etc/systemd/system/docker.service.d/http-proxy.conf <<EOF
[Service]
Environment="HTTP_PROXY=${PROXY_URL}"
Environment="HTTPS_PROXY=${PROXY_URL}"
Environment="NO_PROXY=${NO_PROXY_LIST}"
EOF
systemctl daemon-reload || true
timeout 30s systemctl enable --now docker || true
timeout 60s systemctl restart docker || true
timeout 15s usermod -aG docker root 2>/dev/null || true
if timeout 15s id "$LOCAL_USER" &>/dev/null; then timeout 15s usermod -aG docker "$LOCAL_USER" 2>/dev/null || true; fi
mkdir -p /root/.docker
cat > /root/.docker/config.json <<EOF
{
"proxies": {
"default": {
"httpProxy": "${PROXY_URL}",
"httpsProxy": "${PROXY_URL}",
"noProxy": "${NO_PROXY_LIST}"
}
}
}
EOF
if timeout 15s id "$LOCAL_USER" &>/dev/null; then
USER_HOME=$(eval echo ~$LOCAL_USER)
mkdir -p "$USER_HOME/.docker"
cp /root/.docker/config.json "$USER_HOME/.docker/config.json"
chown -R "$LOCAL_USER:$LOCAL_USER" "$USER_HOME/.docker" || true
fi
}
mod_lazydocker() {
step "Installing LazyDocker"
if ! command -v lazydocker &>/dev/null; then
run_retry curl -sSL -x "${PROXY_URL}" -o /tmp/install_lazydocker.sh https://raw.githubusercontent.com/jesseduffield/lazydocker/master/scripts/install_update_linux.sh
chmod +x /tmp/install_lazydocker.sh
HTTP_PROXY="${PROXY_URL}" HTTPS_PROXY="${PROXY_URL}" DIR=/usr/local/bin run_retry /tmp/install_lazydocker.sh
rm -f /tmp/install_lazydocker.sh
fi
}
mod_web_stack() {
step "Installing Web Stack (PHP, Nginx, Node)"
local PHP_D INI JV
if command -v httpd &>/dev/null || command -v apache2 &>/dev/null; then
warn "Apache is installed here. This module also installs nginx, which"
warn "will contend for port 80. Review before continuing."
fi
if [[ "$PKG" == "dnf" || "$PKG" == "yum" ]]; then
if ! rpm -q remi-release >/dev/null 2>&1; then
if [[ "$OS_ID" == "fedora" ]]; then
run_retry dnf install -y "https://rpms.remirepo.net/fedora/remi-release-${VERSION_MAJOR}.rpm"
elif [[ "$IS_EL7" == true ]]; then
run_retry $PKG install -y "${EL7_REMI_BASE%/7}/remi-release-7.rpm" yum-utils
else
run_retry $PKG install -y "https://rpms.remirepo.net/enterprise/remi-release-${VERSION_MAJOR}.rpm"
fi
fi
$PKG clean packages >/dev/null 2>&1 || true
if [[ "$IS_EL7" == true ]]; then
# Remi's el7 repos use a cdn.remirepo.net mirrorlist, which the iGov
# proxy answers with HTTP 303 (Check Point UserCheck). Pin explicit
# baseurls so yum never touches a mirrorlist.
cat > /etc/yum.repos.d/remi-m21.repo <<EOF
# Written by m21 setup ${SCRIPT_VERSION} — explicit baseurls, no mirrorlist.
[remi-safe]
name=Remi safe (EL7)
baseurl=${EL7_REMI_BASE}/safe/\$basearch/
enabled=1
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-remi
[remi-php${PHP_VERSION//./}]
name=Remi PHP ${PHP_VERSION} (EL7)
baseurl=${EL7_REMI_BASE}/php${PHP_VERSION//./}/\$basearch/
enabled=1
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-remi
EOF
# Silence the originals so both don't race.
sed -i 's/^enabled=1/enabled=0/' /etc/yum.repos.d/remi-php*.repo 2>/dev/null || true
sed -i 's/^enabled=1/enabled=0/' /etc/yum.repos.d/remi-safe.repo 2>/dev/null || true
yum clean metadata >/dev/null 2>&1 || true
run_retry $PKG install -y $PHP_PACKAGES_EL7
elif [[ "$PKG" == "dnf" ]]; then
$PKG module reset php -y || true
$PKG module install -y php:remi-${PHP_VERSION}
$PKG install -y $PHP_PACKAGES_EL7 || true
fi
# el7 has no java-21; fall back down the chain rather than failing.
for JV in "$JAVA_VERSION" 17 11 1.8.0; do
if $PKG install -y "java-${JV}-openjdk" 2>/dev/null; then
log "Installed java-${JV}-openjdk"; break
fi
done
$PKG install -y nginx nodejs || warn "nginx/nodejs unavailable from configured repos."
elif [[ "$PKG" == "pacman" ]]; then
run_retry pacman -S --noconfirm php php-fpm php-gd php-pgsql nginx nodejs npm jre-openjdk
else
source /etc/os-release
if [[ "$ID" == "debian" ]]; then
if [[ ! -f /etc/apt/sources.list.d/sury-php.list ]]; then
install -m 0755 -d /etc/apt/keyrings
run_retry curl -fsSL https://packages.sury.org/php/apt.gpg -o /etc/apt/keyrings/sury-php.gpg
chmod a+r /etc/apt/keyrings/sury-php.gpg
PHP_CODENAME="${VERSION_CODENAME:-$(command -v lsb_release >/dev/null 2>&1 && lsb_release -cs || echo bookworm)}"
echo "deb [signed-by=/etc/apt/keyrings/sury-php.gpg] https://packages.sury.org/php/ ${PHP_CODENAME} main" > /etc/apt/sources.list.d/sury-php.list
apt-get update -qq || true
fi
else
if ! grep -q "ondrej/php" /etc/apt/sources.list.d/* 2>/dev/null; then run_retry add-apt-repository -y ppa:ondrej/php; fi
apt-get update -qq || true
fi
run_retry apt-get install -y php${PHP_VERSION} php${PHP_VERSION}-{cli,fpm,mysql,gd,mbstring,xml,curl,zip,intl,gmp,bcmath,ldap,imap,opcache,redis,apcu,imagick,smbclient}
run_retry apt-get install -y "openjdk-${JAVA_VERSION}-jdk" || { log "openjdk-${JAVA_VERSION} unavailable; installing default-jdk"; run_retry apt-get install -y default-jdk; }
run_retry apt-get install -y nginx nodejs npm
fi
if command -v php &>/dev/null; then
# Config lives in a drop-in that no package owns. Putting it in php.ini
# means `yum remove php*` renames it to php.ini.rpmsave and the
# reinstall silently drops you back to memory_limit 128M / uploads 2M.
for PHP_D in /etc/php.d \
"/etc/php/${PHP_VERSION}/fpm/conf.d" \
"/etc/php/${PHP_VERSION}/cli/conf.d" \
"/etc/php/${PHP_VERSION}/apache2/conf.d"; do
[[ -d "$PHP_D" ]] || continue
cat > "${PHP_D}/99-m21.ini" <<EOF
; Managed by m21 setup ${SCRIPT_VERSION}. Not owned by any package.
memory_limit = ${PHP_MEMORY_LIMIT}
upload_max_filesize = ${PHP_UPLOAD_MAX}
post_max_size = ${PHP_UPLOAD_MAX}
max_execution_time = ${PHP_MAX_EXEC}
max_input_time = ${PHP_MAX_EXEC}
allow_url_fopen = On
curl.cainfo = ${SYSTEM_CA_BUNDLE}
openssl.cafile = ${SYSTEM_CA_BUNDLE}
; opcache.save_comments must stay On — Nextcloud's DI container reads
; annotations and breaks without them.
opcache.enable = 1
opcache.enable_cli = 1
opcache.memory_consumption = 256
opcache.interned_strings_buffer = 64
opcache.max_accelerated_files = 20000
opcache.revalidate_freq = 5
opcache.save_comments = 1
EOF
done
# The opcache settings above do nothing without the extension itself.
if ! php -m 2>/dev/null | grep -qi 'Zend OPcache'; then
if [[ -d /etc/php.d ]] && ! grep -rqs '^zend_extension.*opcache' /etc/php.d/; then
echo 'zend_extension=opcache.so' > /etc/php.d/10-opcache.ini
log "opcache extension was configured but never loaded — added zend_extension."
fi
fi
# Strip the inert proxy block older versions appended on every run.
# http_proxy is not a PHP ini directive; PHP cannot set env vars.
while IFS= read -r INI; do
[[ -f "$INI" ]] || continue
if grep -q '^; Proxy Settings' "$INI"; then
cp -a "$INI" "${INI}.m21.bak-$(date +%s)"
sed -i '/^; Proxy Settings injected by Setup Script$/d
/^; Proxy Settings$/d
/^http_proxy = /d
/^https_proxy = /d' "$INI"
log "Removed legacy proxy cruft from ${INI}"
fi
done < <(find /etc/php* -maxdepth 3 -name 'php.ini' 2>/dev/null)
if systemctl list-unit-files | grep -q php-fpm; then timeout 30s systemctl restart php-fpm || true; fi
if systemctl list-unit-files | grep -q "php${PHP_VERSION}-fpm"; then timeout 30s systemctl restart "php${PHP_VERSION}-fpm" || true; fi
if systemctl is-active --quiet httpd; then timeout 30s systemctl restart httpd || true; fi
fi
}
mod_db_stack() {
step "Installing Databases"
local DB_OS DB_VER="$MARIADB_VERSION"
if [[ "$PKG" == "dnf" || "$PKG" == "yum" ]]; then
# MariaDB stopped building for EL7 after the 10.11 LTS series. Pinning
# anything newer here yields an empty repo and a confusing failure.
if [[ "$IS_EL7" == true ]] && [[ "${DB_VER%%.*}" -gt 10 ]]; then
warn "MariaDB ${DB_VER} has no EL7 build — falling back to 10.11 LTS."
DB_VER="10.11"
fi
if [[ ! -f /etc/yum.repos.d/mariadb.repo ]]; then
if [[ "$OS_ID" == "fedora" ]]; then DB_OS="fedora"; else DB_OS="rhel"; fi
cat > /etc/yum.repos.d/mariadb.repo <<EOF
# Written by m21 setup ${SCRIPT_VERSION}
# rpm.mariadb.org is a redirector; if it starts 403ing through the proxy,
# swap baseurl for a direct mirror, e.g.
# https://ftp.osuosl.org/pub/mariadb/mariadb-${DB_VER}.18/yum/rhel/7/\$basearch
[mariadb]
name = MariaDB
baseurl = https://rpm.mariadb.org/${DB_VER}/${DB_OS}/\$releasever/\$basearch
module_hotfixes=1
gpgkey=https://rpm.mariadb.org/RPM-GPG-KEY-MariaDB
gpgcheck=1
EOF
fi
run_retry $PKG install -y MariaDB-server MariaDB-client MariaDB-shared MariaDB-backup MariaDB-common
$PKG install -y postgresql-server || warn "postgresql-server unavailable."
elif [[ "$PKG" == "pacman" ]]; then
run_retry pacman -S --noconfirm mariadb postgresql
else
run_retry apt-get install -y mariadb-server postgresql
fi
# Tuning in a drop-in for the same reason as the PHP config: a package
# removal renames server.cnf to .rpmsave and takes your buffer pool with it.
if [[ -d /etc/my.cnf.d ]] && [[ ! -f /etc/my.cnf.d/99-m21.cnf ]]; then
cat > /etc/my.cnf.d/99-m21.cnf <<'EOF'
# Managed by m21 setup. Not owned by any package.
# Set to roughly 25-50% of RAM on a dedicated DB host.
[mariadb]
#innodb_buffer_pool_size=4G
EOF
log "Created /etc/my.cnf.d/99-m21.cnf — put innodb_buffer_pool_size there, not in server.cnf."
fi
}
mod_cockpit() {
step "Installing Cockpit"
if [[ "$PKG" == "apt-get" ]]; then run_retry apt-get install -y cockpit cockpit-storaged cockpit-pcp cockpit-packagekit
elif [[ "$PKG" == "pacman" ]]; then run_retry pacman -S --noconfirm cockpit
else run_retry $PKG install -y cockpit cockpit-storaged cockpit-pcp 2>/dev/null || run_retry $PKG install -y cockpit; fi
mkdir -p /etc/systemd/system/cockpit.service.d
echo -e "[Service]\nEnvironment=\"HTTP_PROXY=${PROXY_URL}\"\nEnvironment=\"HTTPS_PROXY=${PROXY_URL}\"\nEnvironment=\"NO_PROXY=${NO_PROXY_LIST}\"" > /etc/systemd/system/cockpit.service.d/proxy.conf
systemctl daemon-reload || true
timeout 30s systemctl enable --now cockpit.socket || true
}
mod_cleanup() {
step "Final Cleanup & Hardening"
local ESCAPED_GROUP
if command -v apt-get &>/dev/null; then rm -f /etc/apt/apt.conf.d/80proxy; fi
if command -v dnf &>/dev/null; then sed -i '/^proxy=/d' /etc/dnf/dnf.conf 2>/dev/null || true; fi
if [[ "$PKG" == "apt-get" ]]; then run_retry apt-get install -y fish fail2ban
elif [[ "$PKG" == "pacman" ]]; then run_retry pacman -S --noconfirm fish fail2ban
else run_retry $PKG install -y fish fail2ban; fi
systemctl disable systemd-networkd-wait-online.service 2>/dev/null || true
systemctl mask systemd-networkd-wait-online.service 2>/dev/null || true
systemctl disable NetworkManager-wait-online.service 2>/dev/null || true
systemctl mask NetworkManager-wait-online.service 2>/dev/null || true
if [[ -f /etc/rc.d/rc.local ]]; then chmod +x /etc/rc.d/rc.local; fi
# Report only. A blind `sed -i /172.16.21.16/d /etc/fstab` also deletes
# unrelated mounts that happen to contain that substring.
if grep -q "${FILE_SERVER_IP}" /etc/fstab 2>/dev/null; then
warn "/etc/fstab references ${FILE_SERVER_IP}:"
grep -n "${FILE_SERVER_IP}" /etc/fstab | sed 's/^/ /'
warn "NOT removed automatically — edit by hand if unwanted."
fi
if systemctl is-failed sssd-nss.socket &>/dev/null; then
systemctl reset-failed || true
timeout 30s systemctl restart sssd || true
fi
ESCAPED_GROUP=$(echo "$AD_SUDO_GROUP" | sed 's/ /\\ /g')
mkdir -p /etc/sudoers.d
echo "%${ESCAPED_GROUP} ALL=(ALL) NOPASSWD: ALL" > "/etc/sudoers.d/10-ad-admins"
chmod 440 "/etc/sudoers.d/10-ad-admins"
# jail.d, not jail.local — jail.local belongs to the admin.
mkdir -p /etc/fail2ban/jail.d
cat > /etc/fail2ban/jail.d/10-m21-sshd.conf <<EOF
# Managed by m21 setup ${SCRIPT_VERSION}
[sshd]
enabled = true
port = ssh
logpath = %(sshd_log)s
maxretry = 3
bantime = 3600
EOF
timeout 30s systemctl enable --now fail2ban || true
}
mod_shell() {
step "Deploying Universal Shell Environment (Starship + zsh/fish/bash)"
if [[ "$PKG" == "apt-get" ]]; then
run_retry apt-get install -y zsh fish git fontconfig || true
run_retry apt-get install -y fzf || true
elif [[ "$PKG" == "pacman" ]]; then
run_retry pacman -S --noconfirm zsh fish git fzf fontconfig || true
else
run_retry $PKG install -y zsh fish git fontconfig || true
run_retry $PKG install -y fzf || true
fi
if ! command -v starship &>/dev/null; then
log "Installing Starship prompt to /usr/local/bin..."
if ! run_retry sh -c "curl -sS -x '${PROXY_URL}' https://starship.rs/install.sh | sh -s -- -y -b /usr/local/bin"; then
ARCH_S=$(uname -m)
run_retry curl -sfL -x "${PROXY_URL}" -o /tmp/starship.tar.gz \
"https://github.com/starship/starship/releases/latest/download/starship-${ARCH_S}-unknown-linux-musl.tar.gz" \
&& tar xzf /tmp/starship.tar.gz -C /usr/local/bin starship \
&& chmod 755 /usr/local/bin/starship
rm -f /tmp/starship.tar.gz
fi
fi
command -v starship &>/dev/null || { error "Starship install failed — aborting shell module."; return 1; }
PLUG_DIR="/usr/local/share/zsh-plugins"
mkdir -p "$PLUG_DIR"
for REPO in zsh-users/zsh-autosuggestions zsh-users/zsh-syntax-highlighting; do
NAME="${REPO##*/}"
if [[ ! -d "$PLUG_DIR/$NAME" ]]; then
run_retry git -c http.proxy="${PROXY_URL}" clone --depth 1 "https://github.com/${REPO}.git" "$PLUG_DIR/$NAME" || true
else
git -C "$PLUG_DIR/$NAME" -c http.proxy="${PROXY_URL}" pull --ff-only 2>/dev/null || true
fi
done
mkdir -p /etc/starship
cat > /etc/starship/starship.toml <<'STARSHIP_EOF'
add_newline = true
format = """
$username$hostname$directory$git_branch$git_status$cmd_duration$fill$time
$character"""
[fill]
symbol = " "
[username]
style_user = "bold yellow"
style_root = "bold red"
format = "[$user]($style)"
show_always = true
[hostname]
ssh_only = false
format = "[@$hostname](bold green) "
[directory]
truncation_length = 4
truncate_to_repo = true
style = "bold cyan"
format = "[$path]($style)[$read_only](red) "
[git_branch]
symbol = " "
style = "bold purple"
format = "[$symbol$branch]($style) "
[git_status]
style = "bold red"
format = "([$all_status$ahead_behind]($style) )"
[cmd_duration]
min_time = 2000
style = "yellow"
format = "[took $duration]($style) "
[time]
disabled = false
time_format = "%T"
style = "dimmed white"
format = "[$time]($style)"
[character]
success_symbol = "[❯](bold green)"
error_symbol = "[❯](bold red)"
STARSHIP_EOF
cat > /etc/profile.d/zz-m21-shell.sh <<'BASHRC_EOF'
case $- in *i*) ;; *) return ;; esac
export STARSHIP_CONFIG=/etc/starship/starship.toml
export HISTTIMEFORMAT='%F %T '
export HISTSIZE=50000
export HISTFILESIZE=100000
export HISTCONTROL=ignoredups:erasedups
shopt -s histappend 2>/dev/null
PROMPT_COMMAND="history -a; ${PROMPT_COMMAND:-:}"
command -v starship >/dev/null 2>&1 && eval "$(starship init bash)"
BASHRC_EOF
ZSHRC="/etc/zshrc"; [[ -d /etc/zsh ]] && ZSHRC="/etc/zsh/zshrc"
touch "$ZSHRC"
sed -i '/# >>> m21-shell >>>/,/# <<< m21-shell <<</d' "$ZSHRC"
cat >> "$ZSHRC" <<'ZSHRC_EOF'
# >>> m21-shell >>>
export STARSHIP_CONFIG=/etc/starship/starship.toml
HISTFILE=~/.zsh_history
HISTSIZE=50000
SAVEHIST=100000
setopt EXTENDED_HISTORY SHARE_HISTORY HIST_IGNORE_DUPS HIST_REDUCE_BLANKS
autoload -Uz compinit && compinit -u
zstyle ':completion:*' menu select
[[ -r /usr/local/share/zsh-plugins/zsh-autosuggestions/zsh-autosuggestions.zsh ]] && \
source /usr/local/share/zsh-plugins/zsh-autosuggestions/zsh-autosuggestions.zsh
[[ -r /usr/local/share/zsh-plugins/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh ]] && \
source /usr/local/share/zsh-plugins/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh
command -v starship >/dev/null 2>&1 && eval "$(starship init zsh)"
# <<< m21-shell <<<
ZSHRC_EOF
mkdir -p /etc/fish/conf.d
cat > /etc/fish/conf.d/m21-shell.fish <<'FISH_EOF'
set -gx STARSHIP_CONFIG /etc/starship/starship.toml
if type -q starship
starship init fish | source
end
FISH_EOF
cat > /etc/fish/conf.d/proxy.fish <<FISHPROXY_EOF
set -gx http_proxy "${PROXY_URL}"
set -gx https_proxy "${PROXY_URL}"
set -gx ftp_proxy "${PROXY_URL}"
set -gx no_proxy "${NO_PROXY_LIST}"
set -gx HTTP_PROXY "${PROXY_URL}"
set -gx HTTPS_PROXY "${PROXY_URL}"
set -gx FTP_PROXY "${PROXY_URL}"
set -gx NO_PROXY "${NO_PROXY_LIST}"
FISHPROXY_EOF
if [ "$HAS_GNOME" = true ] || [ "$HAS_KDE" = true ]; then
FONT_DIR="/usr/local/share/fonts/MesloNF"
if [[ ! -d "$FONT_DIR" ]]; then
log "Installing Meslo Nerd Font (desktop glyph support)..."
mkdir -p "$FONT_DIR"
if run_retry curl -sfL -x "${PROXY_URL}" -o /tmp/meslo.tar.xz \
"https://github.com/ryanoasis/nerd-fonts/releases/latest/download/Meslo.tar.xz"; then
tar xf /tmp/meslo.tar.xz -C "$FONT_DIR" && fc-cache -f "$FONT_DIR" || true
fi
rm -f /tmp/meslo.tar.xz
fi
log "Set your terminal font to 'MesloLGS Nerd Font'."
fi
success "Shell environment deployed. Default login shell remains bash."
}
###############################################################################
# 7. DIAGNOSTICS (READ-ONLY)
###############################################################################
_chk() {
local LABEL="$1"; shift
if "$@" &>/dev/null; then
echo -e " ${GREEN}[PASS]${NC} ${LABEL}"
else
echo -e " ${RED}[FAIL]${NC} ${LABEL}"
fi
}
mod_doctor() {
step "System Doctor (read-only diagnostics)"
local IP ANCHOR_DIR F S I STALE NCROOT WEBUSER M
echo -e "\n${YELLOW}-- Network & DNS --${NC}"
_chk "resolv.conf points at a known DC" bash -c "grep -qE 'nameserver ($(echo $DC_LIST | tr ' ' '|'))' /etc/resolv.conf"
_chk "DNS resolves ${DOMAIN_FQDN}" timeout 5s getent hosts "${DOMAIN_FQDN}"
_chk "File server reachable (${FILE_SERVER_IP})" timeout 5s ping -c1 -W2 "${FILE_SERVER_IP}"
_chk "IPv6 disabled" bash -c "sysctl -n net.ipv6.conf.all.disable_ipv6 | grep -q 1"
echo -e "\n${YELLOW}-- Domain Controllers --${NC}"
for IP in $DC_LIST; do
_chk "DC ${IP} answers an LDAP RootDSE query" probe_dc "$IP"
done
echo -e "\n${YELLOW}-- Time --${NC}"
_chk "Timezone is ${TARGET_TIMEZONE}" bash -c "timedatectl show -p Timezone --value | grep -qx '${TARGET_TIMEZONE}' 2>/dev/null || timedatectl | grep -q '${TARGET_TIMEZONE}'"
if command -v chronyc &>/dev/null; then
_chk "chrony synchronized" bash -c "chronyc tracking 2>/dev/null | grep -q 'Leap status.*Normal'"
fi
echo -e "\n${YELLOW}-- Proxy Path --${NC}"
_chk "Proxy TCP reachable (${PROXY_URL})" timeout 5s bash -c "exec 3<>/dev/tcp/$(echo "$PROXY_URL" | awk -F/ '{print $3}' | cut -d: -f1)/$(echo "$PROXY_URL" | awk -F: '{print $NF}')"
_chk "HTTPS through proxy validates (google.com)" url_alive "https://www.google.com/"
_chk "/etc/environment has proxy vars" grep -q "^http_proxy=" /etc/environment
_chk "/etc/profile.d/proxy.sh present & non-empty" test -s /etc/profile.d/proxy.sh
_chk "fwupd proxy drop-in present" test -f /etc/systemd/system/fwupd.service.d/http-proxy.conf
_chk "packagekit proxy drop-in present" test -f /etc/systemd/system/packagekit.service.d/http-proxy.conf
if [[ "$HAS_GNOME" == true || "$HAS_KDE" == true ]] || systemctl list-unit-files 2>/dev/null | grep -q '^geoclue\.service'; then
echo -e "\n${YELLOW}-- Desktop Proxy Propagation --${NC}"
_chk "/etc/environment has NO quoted values (PAM env bug)" bash -c "! grep -qE '^[A-Za-z_]+=\"' /etc/environment"
_chk "/etc/environment.d/60-m21-proxy.conf present" test -f /etc/environment.d/60-m21-proxy.conf
_chk "systemd DefaultEnvironment drop-in present" test -f /etc/systemd/system.conf.d/60-m21-proxy.conf
if systemctl list-unit-files 2>/dev/null | grep -q '^geoclue\.service'; then
_chk "geoclue.service proxy drop-in present" test -f /etc/systemd/system/geoclue.service.d/proxy.conf
fi
_chk "Mozilla Location Service reachable (geoclue backend)" url_alive "https://location.services.mozilla.com/"
_chk "Firefox policies.json is valid JSON" bash -c "command -v python3 >/dev/null && python3 -c \"import json; json.load(open('/etc/firefox/policies/policies.json'))\" 2>/dev/null"
fi
if [[ "$IS_EL7" == true ]]; then
echo -e "\n${YELLOW}-- CentOS 7 Archive Repos (EOL) --${NC}"
for M in $EL7_BASE_MIRRORS; do
_chk "reachable: ${M}" url_alive "${M}/os/x86_64/repodata/repomd.xml"
done
_chk "reachable: EPEL 7 archive" url_alive "${EL7_EPEL_ARCHIVE}/x86_64/repodata/repomd.xml"
_chk "reachable: Remi EL7 php${PHP_VERSION//./}" url_alive "${EL7_REMI_BASE}/php${PHP_VERSION//./}/x86_64/repodata/repomd.xml"
_chk "yum can build a cache" bash -c "yum -q makecache >/dev/null 2>&1"
fi
echo -e "\n${YELLOW}-- Certificate Sanity --${NC}"
ANCHOR_DIR="/etc/pki/ca-trust/source/anchors"
[[ "$PKG" == "apt-get" ]] && ANCHOR_DIR="/usr/local/share/ca-certificates"
if [[ -d "$ANCHOR_DIR" ]]; then
for F in "${ANCHOR_DIR}"/*; do
[[ -f "$F" ]] || continue
S=$(openssl x509 -in "$F" -noout -subject 2>/dev/null); S="${S#subject=}"
I=$(openssl x509 -in "$F" -noout -issuer 2>/dev/null); I="${I#issuer=}"
[[ -z "$S" ]] && continue
if ! openssl x509 -in "$F" -noout -checkend 0 &>/dev/null; then
echo -e " ${RED}[FAIL]${NC} $(basename "$F") is EXPIRED — remove it"
elif [[ "$S" != "$I" ]]; then
echo -e " ${YELLOW}[WARN]${NC} $(basename "$F") is an intermediate/leaf, not a root"
else
echo -e " ${GREEN}[PASS]${NC} $(basename "$F") — valid self-signed root"
fi
done
fi
echo -e "\n${YELLOW}-- Trust & Identity --${NC}"
if command -v realm &>/dev/null; then
_chk "Domain joined (${DOMAIN_FQDN})" bash -c "timeout 10s realm list 2>/dev/null | grep -q '${DOMAIN_FQDN}'"
fi
_chk "sssd active" systemctl is-active --quiet sssd
_chk "sshd active" bash -c "systemctl is-active --quiet sshd || systemctl is-active --quiet ssh"
echo -e "\n${YELLOW}-- Services --${NC}"
_chk "firewalld active" systemctl is-active --quiet firewalld
_chk "fail2ban active" systemctl is-active --quiet fail2ban
if command -v docker &>/dev/null; then
_chk "docker active" systemctl is-active --quiet docker
_chk "docker daemon proxy configured" bash -c "docker info 2>/dev/null | grep -qi 'HTTP Proxy'"
fi
if command -v flatpak &>/dev/null; then
_chk "flathub remote configured" bash -c "flatpak remotes 2>/dev/null | grep -q flathub"
fi
for NCROOT in /var/www/html/nextcloud /var/www/nextcloud /usr/share/nextcloud; do
[[ -f "${NCROOT}/occ" ]] || continue
WEBUSER="apache"; id www-data &>/dev/null && WEBUSER="www-data"
echo -e "\n${YELLOW}-- Nextcloud (${NCROOT}) --${NC}"
_chk "opcache extension loaded" bash -c "php -m 2>/dev/null | grep -qi 'Zend OPcache'"
_chk "php-smbclient present (external storage)" bash -c "php -m 2>/dev/null | grep -qi smbclient"
_chk "php-posix present (occ needs it)" bash -c "php -m 2>/dev/null | grep -qi posix"
_chk "memory_limit >= 512M" bash -c 'php -r "\$m=ini_get(\"memory_limit\"); exit((\$m===\"-1\"||(int)\$m>=512)?0:1);"'
_chk "m21 PHP drop-in present" test -f /etc/php.d/99-m21.ini
_chk "Inspection CA in Nextcloud trust store" bash -c "sudo -u ${WEBUSER} php ${NCROOT}/occ security:certificates 2>/dev/null | grep -q 'Certificate Authority'"
break
done
echo -e "\n${YELLOW}-- Known Delay Sources --${NC}"
_chk "systemd-networkd-wait-online masked" bash -c "systemctl is-enabled systemd-networkd-wait-online.service 2>/dev/null | grep -q masked"
_chk "NetworkManager-wait-online masked" bash -c "systemctl is-enabled NetworkManager-wait-online.service 2>/dev/null | grep -q masked"
if command -v sqlite3 &>/dev/null && [ -f /var/lib/PackageKit/transactions.db ]; then
STALE=$(sqlite3 /var/lib/PackageKit/transactions.db "SELECT COUNT(*) FROM proxy WHERE proxy_http IS NULL OR proxy_http = '';" 2>/dev/null || echo "?")
if [[ "$STALE" != "0" && "$STALE" != "?" ]]; then
warn "PackageKit has ${STALE} stale (empty) proxy entries — run --gs-fix to scrub."
else
echo -e " ${GREEN}[PASS]${NC} PackageKit proxy table clean"
fi
fi
echo ""
log "Doctor complete. FAILs above are your troubleshooting starting points."
}
###############################################################################
# 8. CLI ROUTER
###############################################################################
show_help() {
echo "Usage: $0 [OPTION]"
echo "Version: ${SCRIPT_VERSION}"
echo "Supported: RHEL/CentOS/Alma/Rocky/Fedora (dnf/yum), Ubuntu/Debian/Zorin (apt), Arch (pacman)."
echo ""
echo "Core Deployment:"
echo " --basics Proxy, Certs, Repos, Network, Firewalld, AD, Cleanup. (Servers)"
echo " --full Everything (Basics + GUI + Docker + Web/DB + Flatpak/Tools + Shell + Cockpit)."
echo ""
echo "Modular Execution:"
echo " --certs [PATH] Install CA certs. With no PATH: ${CERT_LOCAL_DIR} then the"
echo " CIFS share. With PATH: that file or directory."
echo " --network Probe DCs, then configure DNS/hosts/chrony."
echo " --desktop-net Propagate proxy to systemd --user sessions, system"
echo " services (geoclue etc.) and DefaultEnvironment."
echo " Also runs automatically as part of --basics/--full."
echo " --repos Configure base OS repositories (EL7 archive-aware)."
echo " --docker Install and configure Docker Engine with Proxy/Subnets."
echo " --flatpak Configure Flatpak, Flathub (ostree proxy pin), GUI App Centers."
echo " --gs-fix Fix GNOME Software slowness (fwupd/PackageKit/ostree/appstream)."
echo " --ad-join Run the SSSD and Realmd AD Join sequence."
echo " --gui-proxy Configure dconf (GNOME/Cinnamon), KDE, and Firefox Proxies."
echo " --proxy-tool Install the 'toggle-proxy' dynamic CLI tool."
echo " --desktop-tools Install Fastfetch, GNOME Tweaks, Flatseal, ExtensionManager."
echo " --shell Starship prompt + zsh/fish/bash integration, plugins, fonts."
echo " --web-stack Install PHP (full extension set), Nginx, Node, Java."
echo " --db-stack Install MariaDB and PostgreSQL."
echo " --tools Install zsh, fish, neovim, git, nano, lazydocker."
echo " --resize-home Shrink LVM /home to ${HOME_TARGET_SIZE} (Backup/Restore)."
echo ""
echo "Diagnostics:"
echo " --doctor Read-only: DNS, DCs, proxy path, EL7 repos, certs, AD, services."
echo ""
echo " --yes Skip the 10s abort window when the host is already serving."
echo ""
}
if [[ $# -eq 0 ]]; then show_help; exit 0; fi
if [[ $EUID -ne 0 ]]; then
echo -e "${RED}This script must be run as root (sudo $0 $*).${NC}"
exit 1
fi
# Abort BEFORE touching the system if any function the router needs is missing.
verify_modules
_ARGS=()
for _ARG in "$@"; do
if [[ "$_ARG" == "--yes" || "$_ARG" == "-y" ]]; then ASSUME_YES=true; else _ARGS+=("$_ARG"); fi
done
if [[ ${#_ARGS[@]} -gt 0 ]]; then set -- "${_ARGS[@]}"; else set --; fi
unset _ARG _ARGS
if [[ $# -eq 0 ]]; then show_help; exit 0; fi
touch "$LOG_FILE" || true
chmod 600 "$LOG_FILE" || true
exec > >(tee >(sed -u 's/\x1B\[[0-9;]*m//g' >> "$LOG_FILE")) 2>&1
cat > /etc/logrotate.d/m21-setup <<EOF
${LOG_FILE} {
size 5M
rotate 4
compress
missingok
notifempty
}
EOF
echo -e "\n=== INVOCATION: $0 $* ===" >> "$LOG_FILE"
detect_and_fix_os
init_header
while [[ "$#" -gt 0 ]]; do
case $1 in
--basics) warn_if_serving; mod_proxy; mod_desktop_network; mod_clock_fix; mod_certs; mod_base_repos; mod_base_tools; mod_network; mod_firewall; mod_domain_users; mod_cleanup ;;
--full) warn_if_serving; mod_proxy; mod_desktop_network; mod_gui_proxy; mod_proxy_toggle; mod_clock_fix; mod_certs; mod_base_repos; mod_base_tools; mod_flatpak; mod_desktop_tools; mod_shell; mod_gs_fix; mod_network; mod_firewall; mod_domain_users; mod_docker; mod_web_stack; mod_db_stack; mod_cockpit; mod_cleanup ;;
--certs)
if [[ -n "${2:-}" && "${2:0:2}" != "--" ]]; then mod_certs "$2"; shift; else mod_certs; fi ;;
--network) mod_network ;;
--desktop-net) mod_proxy; mod_desktop_network ;;
--repos) mod_base_repos ;;
--docker) mod_proxy; mod_docker ;;
--flatpak) mod_proxy; mod_flatpak ;;
--desktop-tools) mod_proxy; mod_desktop_tools; mod_gs_fix ;;
--gs-fix) mod_proxy; mod_gs_fix ;;
--ad-join) mod_domain_users ;;
--gui-proxy) mod_gui_proxy ;;
--proxy-tool) mod_proxy_toggle ;;
--shell) mod_proxy; mod_shell ;;
--web-stack) warn_if_serving; mod_proxy; mod_web_stack ;;
--db-stack) warn_if_serving; mod_proxy; mod_db_stack ;;
--tools) mod_proxy; mod_base_tools; mod_lazydocker ;;
--resize-home) mod_resize_home ;;
--doctor) mod_doctor ;;
*) echo "Unknown option: $1"; show_help; exit 1 ;;
esac
shift
done
echo -e "\n${GREEN}[$(date +'%H:%M:%S')] === Setup Complete (${SCRIPT_VERSION}) ===${NC}"
master_script.sh - v59g
#!/usr/bin/env bash
#
# MASTER INFRASTRUCTURE SETUP
# Version: v59
#
# v59 Changelog:
# - FIX: Restored all core server modules (Docker, Web, DB, Cockpit, Cleanup, Shell)
# that were accidentally truncated in previous revisions.
# - ENHANCEMENT: Bulletproofed `mod_lazydocker` proxy routing. The install script
# is now downloaded locally, executed with explicit proxy environment variables.
# - FIX: 172.16.21.0/24 (Prism Central) added to NO_PROXY_LIST.
#
###############################################################################
# 1. CONFIGURATION
###############################################################################
SCRIPT_VERSION="v59"
LOG_FILE="/var/log/m21-setup.log"
DOMAIN_FQDN="m21.gov.local"
DOMAIN_ALT="m21.gov.tt"
DOMAIN_SHORT="M21"
DC_DNS_IP="172.16.21.161"
NTP_SERVER="172.16.121.9"
TARGET_TIMEZONE="America/Port_of_Spain"
# File Server Info
FILE_SERVER_IP="172.16.21.16"
FILE_SERVER_NAME="fileserver2"
# Proxy
PROXY_URL="http://172.40.4.14:8080"
# Docker Subnets & Internal Container Hostnames
DOCKER_NO_PROXY="172.17.0.0/16,172.18.0.0/16,172.19.0.0/16,172.20.0.0/16,172.21.0.0/16,web,api,app,db,database,redis,postgres,mysql,minio,mq,cache,admin,live,proxy,edrive,nextcloud,huly,cockroach,zammad,glpi,authentik,peertube,npm,zoraxy"
NO_PROXY_LIST="127.0.0.1,localhost,localhost.localdomain,${DOMAIN_FQDN},${DOMAIN_ALT},.${DOMAIN_FQDN},.${DOMAIN_ALT},${DC_DNS_IP},172.30.0.0/20,172.26.21.0/24,10.21.0.0/21,172.16.121.0/24,172.16.21.0/24,${DOCKER_NO_PROXY}"
# Docker Settings
INSECURE_REGISTRIES='"172.16.121.119:5000", "docker-repo.msya.gov.tt"'
# AD Access Control
AD_SUDO_GROUP="ICT Staff SG M21"
ALLOWED_LOGIN_GROUP="ICT Staff SG M21"
# Share Credentials
# WARNING: Never wrap commands containing these credentials in run_retry, and
# keep them inside if/|| true guards so the ERR trap doesn't log them to /var/log!
SHARE_PATH="//172.16.21.16/fileserver2"
SHARE_USER="Cipher.m21"
SHARE_PASS=")\ly; 634'NJ%i+"
CERT_SOURCE_PATH="/General/IT FILES/prx/Gortt_certificate_V4.cer"
TARGET_CERT_NAME="GORTT_Root_Exp2029"
# Failsafe User
LOCAL_USER="pcsupport"
LOCAL_PASS="ProIT321*"
# LVM Settings
HOME_TARGET_SIZE="8G"
# Versions
PHP_VERSION="8.3"
JAVA_VERSION="21"
MARIADB_VERSION="10.11"
# Systemd services that must inherit the proxy
PROXY_SERVICES="packagekit flatpak-system-helper fwupd"
###############################################################################
# 2. HELPER FUNCTIONS
###############################################################################
set -e
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[0;33m'; BLUE='\033[0;34m'; NC='\033[0m'
log() { echo -e "${BLUE}[$(date +'%H:%M:%S')] [INFO]${NC} $1"; }
step() { echo -e "\n${YELLOW}[$(date +'%H:%M:%S')] >>> $1${NC}"; }
success() { echo -e "${GREEN}[$(date +'%H:%M:%S')] [OK]${NC} $1"; }
error() { echo -e "${RED}[$(date +'%H:%M:%S')] [ERROR]${NC} $1"; }
warn() { echo -e "${YELLOW}[$(date +'%H:%M:%S')] [WARN]${NC} $1"; }
trap 'error "Script aborted at line ${LINENO} (last command: ${BASH_COMMAND})"' ERR
trap 'sleep 0.2' EXIT
run_retry() {
local n=1; local max=3; local delay=2
while true; do
"$@" && return 0
if [[ $n -lt $max ]]; then
((n++)); log "Command failed: [$*]. Retrying ($n/$max)..."; sleep $delay
else
error "Command failed permanently after ${max} attempts: [$*]"
return 1
fi
done
}
pin_flatpak_proxy() {
local REPO="/var/lib/flatpak/repo"
if [[ ! -f "$REPO/config" ]]; then
warn "Flatpak repo not initialized yet (${REPO}/config missing) — skipping proxy pin."
return 0
fi
if ! command -v ostree &>/dev/null; then
log "ostree CLI not installed (only ostree-libs) — installing for repo proxy pin..."
if [[ "$PKG" == "apt-get" ]]; then run_retry apt-get install -y ostree || true
elif [[ "$PKG" == "pacman" ]]; then run_retry pacman -S --noconfirm ostree || true
else run_retry $PKG install -y ostree || true; fi
fi
if command -v ostree &>/dev/null; then
ostree --repo="$REPO" config set 'remote "flathub".proxy' "${PROXY_URL}" || true
else
sed -i '/^\[remote "flathub"\]/,/^\[/{ /^proxy=/d }' "$REPO/config" 2>/dev/null || true
sed -i "/^\[remote \"flathub\"\]/a proxy=${PROXY_URL}" "$REPO/config" 2>/dev/null || true
fi
if awk -v want="proxy=${PROXY_URL}" '
/^\[remote "flathub"\]/ {f=1; next}
/^\[/ {f=0}
f && $0 == want {found=1}
END {exit !found}
' "$REPO/config"; then
success "flathub ostree proxy pinned: ${PROXY_URL}"
else
warn "FAILED to pin flathub proxy in ${REPO}/config — flatpak fetches will bypass the proxy and hang/fail."
fi
}
###############################################################################
# 3. PRE-FLIGHT CHECKS
###############################################################################
detect_and_fix_os() {
if [[ ! -f /etc/os-release ]]; then error "Cannot detect OS. /etc/os-release missing."; exit 1; fi
source /etc/os-release
OS_ID=$(echo "$ID" | tr '[:upper:]' '[:lower:]')
OS_PRETTY="${PRETTY_NAME:-$ID $VERSION_ID}"
VERSION_MAJOR=$(echo "$VERSION_ID" | cut -d. -f1)
if timeout 10s systemctl is-active --quiet packagekit.service 2>/dev/null; then
timeout 15s systemctl stop packagekit.service || true
fi
if [[ "$OS_ID" == "centos" && "$VERSION_MAJOR" == "7" ]]; then
PKG="yum"
if grep -q "linux/rhel" /etc/yum.repos.d/docker-ce.repo 2>/dev/null; then rm -f /etc/yum.repos.d/docker-ce.repo; fi
if [ ! -f /etc/yum.repos.d/CentOS-Base.repo.backup ]; then
cp /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.backup 2>/dev/null || true
run_retry curl -o /etc/yum.repos.d/CentOS-Base.repo https://el7.repo.almalinux.org/centos/CentOS-Base.repo
fi
elif [[ "$OS_ID" =~ (rhel|centos|almalinux|rocky|fedora) ]]; then PKG="dnf"
elif [[ "$OS_ID" =~ (ubuntu|debian|zorin) ]]; then PKG="apt-get"; export DEBIAN_FRONTEND=noninteractive
elif [[ "$OS_ID" == "arch" || "$ID_LIKE" == *"arch"* ]]; then PKG="pacman"; run_retry pacman -Sy
else error "Unsupported OS: $OS_ID"; exit 1; fi
detect_de
}
detect_de() {
HAS_GNOME=false
HAS_KDE=false
DETECTED_DE="Headless/Server"
if command -v gnome-shell &>/dev/null \
|| (command -v dpkg &>/dev/null && dpkg -l 2>/dev/null | grep -q "gnome-shell") \
|| (command -v rpm &>/dev/null && rpm -q gnome-shell &>/dev/null); then
HAS_GNOME=true; DETECTED_DE="GNOME"
fi
if command -v plasmashell &>/dev/null \
|| (command -v dpkg &>/dev/null && dpkg -l 2>/dev/null | grep -q "plasma-workspace") \
|| (command -v rpm &>/dev/null && rpm -q plasma-workspace &>/dev/null); then
HAS_KDE=true
if [ "$HAS_GNOME" = true ]; then DETECTED_DE="GNOME + KDE"; else DETECTED_DE="KDE Plasma"; fi
fi
if command -v cinnamon &>/dev/null; then DETECTED_DE="${DETECTED_DE/Headless\/Server/Cinnamon}"; fi
}
init_header() {
local KERNEL ARCH HOST PRIMARY_IP PROXY_STATE JOINED UPT
KERNEL=$(uname -r)
ARCH=$(uname -m)
HOST=$(hostname)
PRIMARY_IP=$(ip -4 route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="src") print $(i+1); exit}')
[[ -z "$PRIMARY_IP" ]] && PRIMARY_IP=$(hostname -I 2>/dev/null | awk '{print $1}')
UPT=$(uptime -p 2>/dev/null | sed 's/^up //')
if [[ -s /etc/profile.d/proxy.sh ]]; then PROXY_STATE="ON (${PROXY_URL})"; else PROXY_STATE="OFF"; fi
if command -v realm &>/dev/null && timeout 10s realm list 2>/dev/null | grep -q "$DOMAIN_FQDN"; then
JOINED="Joined (${DOMAIN_FQDN})"
else
JOINED="Not joined"
fi
echo -e "\n${BLUE}=====================================================================${NC}"
echo -e "${GREEN} Master Infrastructure Setup ${SCRIPT_VERSION}${NC}"
echo -e "${BLUE}=====================================================================${NC}"
printf " %-14s %s\n" "OS:" "${OS_PRETTY} (${ARCH})"
printf " %-14s %s\n" "Kernel:" "${KERNEL}"
printf " %-14s %s\n" "Hostname:" "${HOST}"
printf " %-14s %s\n" "IP:" "${PRIMARY_IP:-unknown}"
printf " %-14s %s\n" "Desktop:" "${DETECTED_DE}"
printf " %-14s %s\n" "Pkg Mgr:" "${PKG}"
printf " %-14s %s\n" "Proxy:" "${PROXY_STATE}"
printf " %-14s %s\n" "Domain:" "${JOINED}"
printf " %-14s %s\n" "Uptime:" "${UPT:-unknown}"
printf " %-14s %s\n" "Run at:" "$(date '+%Y-%m-%d %H:%M:%S %Z')"
echo -e "${BLUE}=====================================================================${NC}\n"
}
###############################################################################
# 4. CORE MODULES
###############################################################################
mod_proxy() {
step "Configuring System Proxy"
cat > /etc/profile.d/proxy.sh <<EOF
export http_proxy="${PROXY_URL}"
export https_proxy="${PROXY_URL}"
export ftp_proxy="${PROXY_URL}"
export no_proxy="${NO_PROXY_LIST}"
export HTTP_PROXY="${PROXY_URL}"
export HTTPS_PROXY="${PROXY_URL}"
export FTP_PROXY="${PROXY_URL}"
export NO_PROXY="${NO_PROXY_LIST}"
EOF
source /etc/profile.d/proxy.sh
sed -i -E '/^(http_proxy|https_proxy|ftp_proxy|no_proxy|HTTP_PROXY|HTTPS_PROXY|FTP_PROXY|NO_PROXY)=/d' /etc/environment 2>/dev/null || true
cat >> /etc/environment <<EOF
http_proxy="${PROXY_URL}"
https_proxy="${PROXY_URL}"
ftp_proxy="${PROXY_URL}"
no_proxy="${NO_PROXY_LIST}"
HTTP_PROXY="${PROXY_URL}"
HTTPS_PROXY="${PROXY_URL}"
FTP_PROXY="${PROXY_URL}"
NO_PROXY="${NO_PROXY_LIST}"
EOF
mkdir -p /etc/sudoers.d
echo 'Defaults env_keep += "http_proxy https_proxy ftp_proxy no_proxy HTTP_PROXY HTTPS_PROXY FTP_PROXY NO_PROXY"' > /etc/sudoers.d/10-proxy-env
chmod 440 /etc/sudoers.d/10-proxy-env
if [[ "$PKG" == "dnf" || "$PKG" == "yum" ]]; then
CONF_FILE="/etc/dnf/dnf.conf"
[[ ! -f "$CONF_FILE" ]] && CONF_FILE="/etc/yum.conf"
grep -q "proxy=" "$CONF_FILE" 2>/dev/null || echo "proxy=${PROXY_URL}" >> "$CONF_FILE"
if ! grep -q "minrate" "$CONF_FILE" 2>/dev/null; then
echo -e "timeout=60\nretries=10\nminrate=1" >> "$CONF_FILE"
fi
elif [[ "$PKG" == "apt-get" ]]; then
echo -e "Acquire::http::Proxy \"${PROXY_URL}\";\nAcquire::https::Proxy \"${PROXY_URL}\";" > /etc/apt/apt.conf.d/80proxy
fi
for SVC in ${PROXY_SERVICES}; do
mkdir -p /etc/systemd/system/${SVC}.service.d
cat > /etc/systemd/system/${SVC}.service.d/http-proxy.conf <<EOF
[Service]
Environment="HTTP_PROXY=${PROXY_URL}"
Environment="HTTPS_PROXY=${PROXY_URL}"
Environment="http_proxy=${PROXY_URL}"
Environment="https_proxy=${PROXY_URL}"
Environment="NO_PROXY=${NO_PROXY_LIST}"
Environment="no_proxy=${NO_PROXY_LIST}"
EOF
done
systemctl daemon-reload
killall packagekitd 2>/dev/null || true
killall flatpak-system-helper 2>/dev/null || true
systemctl try-restart packagekit flatpak-system-helper fwupd 2>/dev/null || true
}
mod_gui_proxy() {
step "Configuring GUI Proxy Settings (System-Wide)"
PROXY_HOST=$(echo "$PROXY_URL" | awk -F/ '{print $3}' | cut -d: -f1)
PROXY_PORT=$(echo "$PROXY_URL" | awk -F: '{print $NF}')
DCONF_NO_PROXY="['$(echo "$NO_PROXY_LIST" | sed "s/,/','/g")']"
if [[ "$PKG" == "apt-get" ]]; then run_retry apt-get install -y dconf-cli
elif [[ "$PKG" == "dnf" || "$PKG" == "yum" ]]; then run_retry $PKG install -y dconf
elif [[ "$PKG" == "pacman" ]]; then run_retry pacman -S --noconfirm dconf
fi
mkdir -p /etc/dconf/profile
mkdir -p /etc/dconf/db/local.d
echo -e "user-db:user\nsystem-db:local" > /etc/dconf/profile/user
cat > /etc/dconf/db/local.d/01-proxy <<EOF
[system/proxy]
mode='manual'
ignore-hosts=${DCONF_NO_PROXY}
[system/proxy/http]
host='${PROXY_HOST}'
port=${PROXY_PORT}
[system/proxy/https]
host='${PROXY_HOST}'
port=${PROXY_PORT}
[system/proxy/ftp]
host='${PROXY_HOST}'
port=${PROXY_PORT}
EOF
dconf update || log "Warning: dconf update failed, GUI settings may require reboot."
mkdir -p /etc/xdg
cat > /etc/xdg/kioslaverc <<EOF
[Proxy Settings]
ProxyType=1
httpProxy=${PROXY_URL}
httpsProxy=${PROXY_URL}
ftpProxy=${PROXY_URL}
NoProxyFor=${NO_PROXY_LIST}
EOF
mkdir -p /etc/firefox/policies
cat > /etc/firefox/policies/policies.json <<FFEOF
{
"policies": {
"Proxy": {
"Mode": "manual",
"HTTPProxy": "${PROXY_HOST}:${PROXY_PORT}",
"HTTPSProxy": "${PROXY_HOST}:${PROXY_PORT}",
"FTPProxy": "${PROXY_HOST}:${PROXY_PORT}",
"Passthrough": "${NO_PROXY_LIST}"
}
}
}
FFEOF
}
mod_proxy_toggle() {
step "Installing Proxy Toggle Tool"
cat > /usr/local/bin/toggle-proxy <<EOF
#!/usr/bin/env bash
# System-Wide Proxy Toggle (${SCRIPT_VERSION})
# Usage: sudo toggle-proxy [on|off]
if [[ "\$EUID" -ne 0 ]]; then
echo "Please run as root (sudo toggle-proxy on|off)"
exit 1
fi
MODE=\$1
PROXY_URL="${PROXY_URL}"
PROXY_HOST="\$(echo "\$PROXY_URL" | awk -F/ '{print \$3}' | cut -d: -f1)"
PROXY_PORT="\$(echo "\$PROXY_URL" | awk -F: '{print \$NF}')"
NO_PROXY_LIST="${NO_PROXY_LIST}"
PROXY_SERVICES="docker packagekit flatpak-system-helper fwupd"
if command -v apt-get &>/dev/null; then rm -f /etc/apt/apt.conf.d/80proxy; fi
if command -v dnf &>/dev/null; then sed -i '/^proxy=/d' /etc/dnf/dnf.conf 2>/dev/null || true; fi
if [[ "\$MODE" == "on" ]]; then
echo "Enabling System Proxy..."
cat > /etc/profile.d/proxy.sh <<ENVEOF
export http_proxy="\${PROXY_URL}"
export https_proxy="\${PROXY_URL}"
export ftp_proxy="\${PROXY_URL}"
export no_proxy="\${NO_PROXY_LIST}"
export HTTP_PROXY="\${PROXY_URL}"
export HTTPS_PROXY="\${PROXY_URL}"
export FTP_PROXY="\${PROXY_URL}"
export NO_PROXY="\${NO_PROXY_LIST}"
ENVEOF
sed -i -E '/^(http_proxy|https_proxy|ftp_proxy|no_proxy|HTTP_PROXY|HTTPS_PROXY|FTP_PROXY|NO_PROXY)=/d' /etc/environment 2>/dev/null || true
cat >> /etc/environment <<ENVEOF2
http_proxy="\${PROXY_URL}"
https_proxy="\${PROXY_URL}"
ftp_proxy="\${PROXY_URL}"
no_proxy="\${NO_PROXY_LIST}"
HTTP_PROXY="\${PROXY_URL}"
HTTPS_PROXY="\${PROXY_URL}"
FTP_PROXY="\${PROXY_URL}"
NO_PROXY="\${NO_PROXY_LIST}"
ENVEOF2
mkdir -p /etc/fish/conf.d
cat > /etc/fish/conf.d/proxy.fish <<FISHEOF
set -gx http_proxy "\${PROXY_URL}"
set -gx https_proxy "\${PROXY_URL}"
set -gx ftp_proxy "\${PROXY_URL}"
set -gx no_proxy "\${NO_PROXY_LIST}"
set -gx HTTP_PROXY "\${PROXY_URL}"
set -gx HTTPS_PROXY "\${PROXY_URL}"
set -gx FTP_PROXY "\${PROXY_URL}"
set -gx NO_PROXY "\${NO_PROXY_LIST}"
FISHEOF
for SVC in \${PROXY_SERVICES}; do
mkdir -p /etc/systemd/system/\${SVC}.service.d
cat > /etc/systemd/system/\${SVC}.service.d/http-proxy.conf <<DOCKEREOF
[Service]
Environment="HTTP_PROXY=\${PROXY_URL}"
Environment="HTTPS_PROXY=\${PROXY_URL}"
Environment="http_proxy=\${PROXY_URL}"
Environment="https_proxy=\${PROXY_URL}"
Environment="NO_PROXY=\${NO_PROXY_LIST}"
Environment="no_proxy=\${NO_PROXY_LIST}"
DOCKEREOF
done
systemctl daemon-reload
killall packagekitd 2>/dev/null || true
killall flatpak-system-helper 2>/dev/null || true
systemctl try-restart docker packagekit flatpak-system-helper fwupd 2>/dev/null || true
FPREPO="/var/lib/flatpak/repo"
if [[ -f "\$FPREPO/config" ]]; then
if command -v ostree &>/dev/null; then
ostree --repo="\$FPREPO" config set 'remote "flathub".proxy' "\${PROXY_URL}" 2>/dev/null || true
else
sed -i '/^\[remote "flathub"\]/,/^\[/{ /^proxy=/d }' "\$FPREPO/config" 2>/dev/null || true
sed -i "/^\[remote \"flathub\"\]/a proxy=\${PROXY_URL}" "\$FPREPO/config" 2>/dev/null || true
fi
fi
if command -v dconf &>/dev/null; then
mkdir -p /etc/dconf/db/local.d
sed -i "s/mode='none'/mode='manual'/" /etc/dconf/db/local.d/01-proxy 2>/dev/null || true
dconf update
fi
if [[ -f /etc/xdg/kioslaverc ]]; then
sed -i "s/ProxyType=0/ProxyType=1/" /etc/xdg/kioslaverc 2>/dev/null || true
fi
mkdir -p /etc/firefox/policies
cat > /etc/firefox/policies/policies.json <<FFEOF
{
"policies": {
"Proxy": {
"Mode": "manual",
"HTTPProxy": "\${PROXY_HOST}:\${PROXY_PORT}",
"HTTPSProxy": "\${PROXY_HOST}:\${PROXY_PORT}",
"FTPProxy": "\${PROXY_HOST}:\${PROXY_PORT}",
"Passthrough": "\${NO_PROXY_LIST}"
}
}
}
FFEOF
echo "[OK] Proxy is ON. Log out and back in (or reboot) for GUI sessions to update."
elif [[ "\$MODE" == "off" ]]; then
echo "Disabling System Proxy..."
> /etc/profile.d/proxy.sh
rm -f /etc/fish/conf.d/proxy.fish
sed -i -E '/^(http_proxy|https_proxy|ftp_proxy|no_proxy|HTTP_PROXY|HTTPS_PROXY|FTP_PROXY|NO_PROXY)=/d' /etc/environment 2>/dev/null || true
for SVC in \${PROXY_SERVICES}; do
rm -f /etc/systemd/system/\${SVC}.service.d/http-proxy.conf
done
systemctl daemon-reload
killall packagekitd 2>/dev/null || true
killall flatpak-system-helper 2>/dev/null || true
systemctl try-restart docker flatpak-system-helper fwupd 2>/dev/null || true
if command -v sqlite3 &>/dev/null && [ -f /var/lib/PackageKit/transactions.db ]; then
sqlite3 /var/lib/PackageKit/transactions.db "DELETE FROM proxy;" || true
else
rm -f /var/lib/PackageKit/transactions.db || true
fi
systemctl try-restart packagekit 2>/dev/null || true
FPREPO="/var/lib/flatpak/repo"
if [[ -f "\$FPREPO/config" ]]; then
if command -v ostree &>/dev/null; then
ostree --repo="\$FPREPO" config unset 'remote "flathub".proxy' 2>/dev/null || true
else
sed -i '/^\[remote "flathub"\]/,/^\[/{ /^proxy=/d }' "\$FPREPO/config" 2>/dev/null || true
fi
fi
if command -v dconf &>/dev/null; then
mkdir -p /etc/dconf/db/local.d
sed -i "s/mode='manual'/mode='none'/" /etc/dconf/db/local.d/01-proxy 2>/dev/null || true
dconf update
fi
if [[ -f /etc/xdg/kioslaverc ]]; then
sed -i "s/ProxyType=1/ProxyType=0/" /etc/xdg/kioslaverc 2>/dev/null || true
fi
mkdir -p /etc/firefox/policies
cat > /etc/firefox/policies/policies.json <<FFEOF
{
"policies": {
"Proxy": {
"Mode": "none"
}
}
}
FFEOF
echo "[OK] Proxy is OFF. Log out and back in (or reboot) for GUI sessions to update."
else
echo "Usage: toggle-proxy [on|off]"
fi
EOF
chmod +x /usr/local/bin/toggle-proxy
}
mod_flatpak() {
step "Configuring Flatpak & Flathub"
if [[ "$PKG" == "apt-get" ]]; then
run_retry apt-get install -y flatpak
if [ "$HAS_GNOME" = true ]; then run_retry apt-get install -y gnome-software-plugin-flatpak; fi
if [ "$HAS_KDE" = true ]; then run_retry apt-get install -y plasma-discover-backend-flatpak; fi
elif [[ "$PKG" == "dnf" || "$PKG" == "yum" ]]; then
run_retry $PKG install -y flatpak
if [ "$HAS_GNOME" = true ]; then run_retry $PKG install -y gnome-software; fi
if [ "$HAS_KDE" = true ]; then run_retry $PKG install -y plasma-discover-flatpak; fi
elif [[ "$PKG" == "pacman" ]]; then
run_retry pacman -S --noconfirm flatpak
if [ "$HAS_GNOME" = true ]; then run_retry pacman -S --noconfirm gnome-software; fi
if [ "$HAS_KDE" = true ]; then run_retry pacman -S --noconfirm discover; fi
fi
FLATHUB_URL="https://dl.flathub.org/repo/"
FP_CONFIG="/var/lib/flatpak/repo/config"
NEED_READD=false
if flatpak remotes 2>/dev/null | grep -q '^flathub'; then
CUR_URL=$(awk '
/^\[remote "flathub"\]/ {f=1; next}
/^\[/ {f=0}
f && /^url=/ {sub(/^url=/,""); print; exit}
' "$FP_CONFIG" 2>/dev/null)
if [[ "$CUR_URL" != "$FLATHUB_URL" ]]; then
warn "flathub remote has WRONG url ('${CUR_URL}') — deleting and re-adding correctly."
flatpak remote-delete --force flathub || true
NEED_READD=true
fi
else
NEED_READD=true
fi
if [ "$NEED_READD" = true ]; then
run_retry curl -sf -x "${PROXY_URL}" -o /tmp/flathub.flatpakrepo "${FLATHUB_URL}flathub.flatpakrepo"
run_retry flatpak remote-add --if-not-exists flathub /tmp/flathub.flatpakrepo
rm -f /tmp/flathub.flatpakrepo
fi
pin_flatpak_proxy
}
mod_desktop_tools() {
step "Installing Desktop Utilities & GUI Tools"
if [[ "$PKG" == "dnf" || "$PKG" == "yum" ]]; then
run_retry $PKG install -y fastfetch || run_retry $PKG install -y neofetch || true
elif [[ "$PKG" == "apt-get" ]]; then
run_retry apt-get install -y fastfetch || run_retry apt-get install -y neofetch || true
elif [[ "$PKG" == "pacman" ]]; then
run_retry pacman -S --noconfirm fastfetch || true
fi
if [ "$HAS_GNOME" = true ]; then
log "GNOME DE detected. Deploying Tweaks, Flatseal, and ExtensionManager..."
if [[ "$PKG" == "apt-get" ]]; then run_retry apt-get install -y gnome-tweaks sqlite3
elif [[ "$PKG" == "dnf" || "$PKG" == "yum" ]]; then run_retry $PKG install -y gnome-tweaks sqlite
elif [[ "$PKG" == "pacman" ]]; then run_retry pacman -S --noconfirm gnome-tweaks sqlite
fi
if command -v flatpak &>/dev/null; then
pin_flatpak_proxy
http_proxy="${PROXY_URL}" https_proxy="${PROXY_URL}" HTTP_PROXY="${PROXY_URL}" HTTPS_PROXY="${PROXY_URL}" \
run_retry flatpak install -y flathub com.mattjakeman.ExtensionManager
http_proxy="${PROXY_URL}" https_proxy="${PROXY_URL}" HTTP_PROXY="${PROXY_URL}" HTTPS_PROXY="${PROXY_URL}" \
run_retry flatpak install -y flathub com.github.tchx84.Flatseal
fi
fi
}
mod_gs_fix() {
step "GNOME Software / App Center Performance Fixes"
pin_flatpak_proxy
mkdir -p /etc/systemd/system/fwupd.service.d
cat > /etc/systemd/system/fwupd.service.d/http-proxy.conf <<EOF
[Service]
Environment="HTTP_PROXY=${PROXY_URL}"
Environment="HTTPS_PROXY=${PROXY_URL}"
Environment="http_proxy=${PROXY_URL}"
Environment="https_proxy=${PROXY_URL}"
Environment="NO_PROXY=${NO_PROXY_LIST}"
Environment="no_proxy=${NO_PROXY_LIST}"
EOF
systemctl daemon-reload
systemctl try-restart fwupd 2>/dev/null || true
systemctl stop packagekit 2>/dev/null || true
if command -v sqlite3 &>/dev/null && [ -f /var/lib/PackageKit/transactions.db ]; then
sqlite3 /var/lib/PackageKit/transactions.db "DELETE FROM proxy;" 2>/dev/null || true
fi
systemctl start packagekit 2>/dev/null || true
if command -v appstreamcli &>/dev/null; then
http_proxy="${PROXY_URL}" https_proxy="${PROXY_URL}" appstreamcli refresh --force 2>/dev/null || true
fi
pkill -f "gnome-software" 2>/dev/null || true
rm -rf /var/cache/gnome-software 2>/dev/null || true
for USERDIR in /home/*; do
[ -d "$USERDIR/.cache/gnome-software" ] && rm -rf "$USERDIR/.cache/gnome-software" || true
done
success "GNOME Software backends re-pointed at proxy. First relaunch may still take ~30s to rebuild caches; subsequent launches should be fast."
}
###############################################################################
# CORE SERVER MODULES (Restored in v59)
###############################################################################
mod_docker() {
step "Installing & Configuring Docker"
if [[ "$PKG" == "dnf" || "$PKG" == "yum" ]]; then
if [[ ! -f /etc/yum.repos.d/docker-ce.repo ]]; then
run_retry $PKG install -y yum-utils
if [[ "$OS_ID" == "fedora" ]]; then
run_retry yum-config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo
else
run_retry yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
fi
fi
$PKG remove -y podman buildah docker docker-client docker-common docker-engine >/dev/null 2>&1 || true
elif [[ "$PKG" == "apt-get" ]]; then
if [[ ! -f /etc/apt/sources.list.d/docker.list ]]; then
source /etc/os-release
REPO_OS=${ID}
case "$REPO_OS" in
debian|ubuntu) : ;;
*) REPO_OS="ubuntu" ;;
esac
REPO_CODENAME="${VERSION_CODENAME:-$(command -v lsb_release >/dev/null 2>&1 && lsb_release -cs || echo stable)}"
install -m 0755 -d /etc/apt/keyrings
run_retry curl -fsSL "https://download.docker.com/linux/${REPO_OS}/gpg" -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/${REPO_OS} ${REPO_CODENAME} stable" > /etc/apt/sources.list.d/docker.list
apt-get update -qq || true
fi
fi
if ! command -v docker &>/dev/null; then
if [[ "$PKG" == "apt-get" ]]; then run_retry apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
elif [[ "$PKG" == "pacman" ]]; then run_retry pacman -S --noconfirm docker docker-compose docker-buildx
else run_retry $PKG install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin; fi
fi
mkdir -p /etc/docker
cat > /etc/docker/daemon.json <<EOF
{
"insecure-registries": [ ${INSECURE_REGISTRIES} ]
}
EOF
mkdir -p /etc/systemd/system/docker.service.d
cat > /etc/systemd/system/docker.service.d/http-proxy.conf <<EOF
[Service]
Environment="HTTP_PROXY=${PROXY_URL}"
Environment="HTTPS_PROXY=${PROXY_URL}"
Environment="NO_PROXY=${NO_PROXY_LIST}"
EOF
systemctl daemon-reload || true
timeout 30s systemctl enable --now docker || true
timeout 60s systemctl restart docker || true
timeout 15s usermod -aG docker root 2>/dev/null || true
if timeout 15s id "$LOCAL_USER" &>/dev/null; then timeout 15s usermod -aG docker "$LOCAL_USER" 2>/dev/null || true; fi
mkdir -p /root/.docker
cat > /root/.docker/config.json <<EOF
{
"proxies": {
"default": {
"httpProxy": "${PROXY_URL}",
"httpsProxy": "${PROXY_URL}",
"noProxy": "${NO_PROXY_LIST}"
}
}
}
EOF
if timeout 15s id "$LOCAL_USER" &>/dev/null; then
USER_HOME=$(eval echo ~$LOCAL_USER)
mkdir -p "$USER_HOME/.docker"
cp /root/.docker/config.json "$USER_HOME/.docker/config.json"
chown -R "$LOCAL_USER:$LOCAL_USER" "$USER_HOME/.docker" || true
fi
}
mod_lazydocker() {
step "Installing LazyDocker"
if ! command -v lazydocker &>/dev/null; then
run_retry curl -sSL -x "${PROXY_URL}" -o /tmp/install_lazydocker.sh https://raw.githubusercontent.com/jesseduffield/lazydocker/master/scripts/install_update_linux.sh
chmod +x /tmp/install_lazydocker.sh
HTTP_PROXY="${PROXY_URL}" HTTPS_PROXY="${PROXY_URL}" DIR=/usr/local/bin run_retry /tmp/install_lazydocker.sh
rm -f /tmp/install_lazydocker.sh
fi
}
mod_web_stack() {
step "Installing Web Stack (PHP, Nginx, Node)"
if [[ "$PKG" == "dnf" || "$PKG" == "yum" ]]; then
if ! rpm -q remi-release >/dev/null 2>&1; then
if [[ "$OS_ID" == "fedora" ]]; then
run_retry dnf install -y "https://rpms.remirepo.net/fedora/remi-release-${VERSION_MAJOR}.rpm"
elif [[ "$PKG" == "dnf" ]]; then
run_retry $PKG install -y "https://rpms.remirepo.net/enterprise/remi-release-${VERSION_MAJOR}.rpm"
else
run_retry $PKG install -y http://rpms.remirepo.net/enterprise/remi-release-7.rpm yum-utils
fi
fi
$PKG clean packages >/dev/null 2>&1 || true
if [[ "$PKG" == "dnf" ]]; then
$PKG module reset php -y || true
$PKG module install -y php:remi-${PHP_VERSION}
else
yum-config-manager --enable remi-php83 || true
$PKG install -y php php-cli php-fpm php-mysqlnd php-gd
fi
$PKG install -y java-${JAVA_VERSION}-openjdk nginx nodejs
elif [[ "$PKG" == "pacman" ]]; then
run_retry pacman -S --noconfirm php php-fpm php-gd php-pgsql nginx nodejs npm jre-openjdk
else
source /etc/os-release
if [[ "$ID" == "debian" ]]; then
if [[ ! -f /etc/apt/sources.list.d/sury-php.list ]]; then
install -m 0755 -d /etc/apt/keyrings
run_retry curl -fsSL https://packages.sury.org/php/apt.gpg -o /etc/apt/keyrings/sury-php.gpg
chmod a+r /etc/apt/keyrings/sury-php.gpg
PHP_CODENAME="${VERSION_CODENAME:-$(command -v lsb_release >/dev/null 2>&1 && lsb_release -cs || echo bookworm)}"
echo "deb [signed-by=/etc/apt/keyrings/sury-php.gpg] https://packages.sury.org/php/ ${PHP_CODENAME} main" > /etc/apt/sources.list.d/sury-php.list
apt-get update -qq || true
fi
else
if ! grep -q "ondrej/php" /etc/apt/sources.list.d/* 2>/dev/null; then run_retry add-apt-repository -y ppa:ondrej/php; fi
apt-get update -qq || true
fi
run_retry apt-get install -y php${PHP_VERSION} php${PHP_VERSION}-{cli,fpm,mysql,gd,mbstring,xml,curl,zip}
run_retry apt-get install -y "openjdk-${JAVA_VERSION}-jdk" || { log "openjdk-${JAVA_VERSION} unavailable; installing default-jdk"; run_retry apt-get install -y default-jdk; }
run_retry apt-get install -y nginx nodejs npm
fi
if command -v php &>/dev/null; then
find /etc/php* -name "php.ini" 2>/dev/null | while read -r INI_FILE; do
sed -i '/^http_proxy/d; /^https_proxy/d' "$INI_FILE"
echo -e "\n; Proxy Settings\nhttp_proxy = \"${PROXY_URL}\"\nhttps_proxy = \"${PROXY_URL}\"" >> "$INI_FILE"
if grep -q "allow_url_fopen" "$INI_FILE"; then sed -i 's/^allow_url_fopen.*/allow_url_fopen = On/' "$INI_FILE"
else echo "allow_url_fopen = On" >> "$INI_FILE"; fi
done
if systemctl list-unit-files | grep -q php-fpm; then timeout 30s systemctl restart php-fpm || true; fi
if systemctl list-unit-files | grep -q php${PHP_VERSION}-fpm; then timeout 30s systemctl restart php${PHP_VERSION}-fpm || true; fi
fi
}
mod_db_stack() {
step "Installing Databases"
if [[ "$PKG" == "dnf" || "$PKG" == "yum" ]]; then
if [[ ! -f /etc/yum.repos.d/mariadb.repo ]]; then
if [[ "$OS_ID" == "fedora" ]]; then DB_OS="fedora"; else DB_OS="rhel"; fi
cat > /etc/yum.repos.d/mariadb.repo <<EOF
[mariadb]
name = MariaDB
baseurl = https://rpm.mariadb.org/${MARIADB_VERSION}/${DB_OS}/\$releasever/\$basearch
module_hotfixes=1
gpgkey=https://rpm.mariadb.org/RPM-GPG-KEY-MariaDB
gpgcheck=1
EOF
fi
run_retry $PKG install -y MariaDB-server MariaDB-client postgresql-server
elif [[ "$PKG" == "pacman" ]]; then
run_retry pacman -S --noconfirm mariadb postgresql
else
run_retry apt-get install -y mariadb-server postgresql
fi
}
mod_cockpit() {
step "Installing Cockpit"
if [[ "$PKG" == "apt-get" ]]; then run_retry apt-get install -y cockpit cockpit-storaged cockpit-pcp cockpit-packagekit
elif [[ "$PKG" == "pacman" ]]; then run_retry pacman -S --noconfirm cockpit
else run_retry $PKG install -y cockpit cockpit-storaged cockpit-pcp 2>/dev/null || run_retry $PKG install -y cockpit; fi
mkdir -p /etc/systemd/system/cockpit.service.d
echo -e "[Service]\nEnvironment=\"HTTP_PROXY=${PROXY_URL}\"\nEnvironment=\"HTTPS_PROXY=${PROXY_URL}\"\nEnvironment=\"NO_PROXY=${NO_PROXY_LIST}\"" > /etc/systemd/system/cockpit.service.d/proxy.conf
systemctl daemon-reload || true
timeout 30s systemctl enable --now cockpit.socket || true
}
mod_cleanup() {
step "Final Cleanup & Hardening"
if command -v apt-get &>/dev/null; then rm -f /etc/apt/apt.conf.d/80proxy; fi
if command -v dnf &>/dev/null; then sed -i '/^proxy=/d' /etc/dnf/dnf.conf 2>/dev/null || true; fi
if command -v tmux &>/dev/null; then $PKG remove -y tmux 2>/dev/null || true; fi
rm -f /etc/tmux.conf
if [[ "$PKG" == "apt-get" ]]; then run_retry apt-get install -y fish fail2ban;
elif [[ "$PKG" == "pacman" ]]; then run_retry pacman -S --noconfirm fish fail2ban;
else run_retry $PKG install -y fish fail2ban; fi
systemctl disable systemd-networkd-wait-online.service 2>/dev/null || true
systemctl mask systemd-networkd-wait-online.service 2>/dev/null || true
systemctl disable NetworkManager-wait-online.service 2>/dev/null || true
systemctl mask NetworkManager-wait-online.service 2>/dev/null || true
if [[ -f /etc/rc.d/rc.local ]]; then chmod +x /etc/rc.d/rc.local; fi
if grep -q "172.16.21.16" /etc/fstab; then sed -i '/172.16.21.16/d' /etc/fstab; fi
if systemctl is-failed sssd-nss.socket &>/dev/null; then
systemctl reset-failed || true
timeout 30s systemctl restart sssd || true
fi
ESCAPED_GROUP=$(echo "$AD_SUDO_GROUP" | sed 's/ /\\ /g')
mkdir -p /etc/sudoers.d
echo "%${ESCAPED_GROUP} ALL=(ALL) NOPASSWD: ALL" > "/etc/sudoers.d/10-ad-admins"
chmod 440 "/etc/sudoers.d/10-ad-admins"
cat > /etc/fail2ban/jail.local <<EOF
[sshd]
enabled = true
port = ssh
logpath = %(sshd_log)s
maxretry = 3
bantime = 3600
EOF
timeout 30s systemctl enable --now fail2ban || true
}
mod_shell() {
step "Deploying Universal Shell Environment (Starship + zsh/fish/bash)"
if [[ "$PKG" == "apt-get" ]]; then
run_retry apt-get install -y zsh fish git fontconfig || true
run_retry apt-get install -y fzf || true
elif [[ "$PKG" == "pacman" ]]; then
run_retry pacman -S --noconfirm zsh fish git fzf fontconfig || true
else
run_retry $PKG install -y zsh fish git fontconfig || true
run_retry $PKG install -y fzf || true
fi
if ! command -v starship &>/dev/null; then
log "Installing Starship prompt to /usr/local/bin..."
if ! run_retry sh -c "curl -sS -x '${PROXY_URL}' https://starship.rs/install.sh | sh -s -- -y -b /usr/local/bin"; then
ARCH_S=$(uname -m)
run_retry curl -sfL -x "${PROXY_URL}" -o /tmp/starship.tar.gz \
"https://github.com/starship/starship/releases/latest/download/starship-${ARCH_S}-unknown-linux-musl.tar.gz" \
&& tar xzf /tmp/starship.tar.gz -C /usr/local/bin starship \
&& chmod 755 /usr/local/bin/starship
rm -f /tmp/starship.tar.gz
fi
fi
command -v starship &>/dev/null || { error "Starship install failed — aborting shell module."; return 1; }
PLUG_DIR="/usr/local/share/zsh-plugins"
mkdir -p "$PLUG_DIR"
export GIT_HTTP_PROXY_AUTH=""
for REPO in zsh-users/zsh-autosuggestions zsh-users/zsh-syntax-highlighting; do
NAME="${REPO##*/}"
if [[ ! -d "$PLUG_DIR/$NAME" ]]; then
run_retry git -c http.proxy="${PROXY_URL}" clone --depth 1 "https://github.com/${REPO}.git" "$PLUG_DIR/$NAME" || true
else
git -C "$PLUG_DIR/$NAME" -c http.proxy="${PROXY_URL}" pull --ff-only 2>/dev/null || true
fi
done
mkdir -p /etc/starship
cat > /etc/starship/starship.toml <<'STARSHIP_EOF'
add_newline = true
format = """
$username$hostname$directory$git_branch$git_status$cmd_duration$fill$time
$character"""
[fill]
symbol = " "
[username]
style_user = "bold yellow"
style_root = "bold red"
format = "[$user]($style)"
show_always = true
[hostname]
ssh_only = false
format = "[@$hostname](bold green) "
[directory]
truncation_length = 4
truncate_to_repo = true
style = "bold cyan"
format = "[$path]($style)[$read_only](red) "
[git_branch]
symbol = " "
style = "bold purple"
format = "[$symbol$branch]($style) "
[git_status]
style = "bold red"
format = "([$all_status$ahead_behind]($style) )"
[cmd_duration]
min_time = 2000
style = "yellow"
format = "[took $duration]($style) "
[time]
disabled = false
time_format = "%T"
style = "dimmed white"
format = "[$time]($style)"
[character]
success_symbol = "[❯](bold green)"
error_symbol = "[❯](bold red)"
STARSHIP_EOF
cat > /etc/profile.d/zz-m21-shell.sh <<'BASHRC_EOF'
case $- in *i*) ;; *) return ;; esac
export STARSHIP_CONFIG=/etc/starship/starship.toml
export HISTTIMEFORMAT='%F %T '
export HISTSIZE=50000
export HISTFILESIZE=100000
export HISTCONTROL=ignoredups:erasedups
shopt -s histappend 2>/dev/null
PROMPT_COMMAND="history -a; ${PROMPT_COMMAND:-:}"
command -v starship >/dev/null 2>&1 && eval "$(starship init bash)"
BASHRC_EOF
ZSHRC="/etc/zshrc"; [[ -d /etc/zsh ]] && ZSHRC="/etc/zsh/zshrc"
touch "$ZSHRC"
sed -i '/# >>> m21-shell >>>/,/# <<< m21-shell <<</d' "$ZSHRC"
cat >> "$ZSHRC" <<'ZSHRC_EOF'
# >>> m21-shell >>>
export STARSHIP_CONFIG=/etc/starship/starship.toml
HISTFILE=~/.zsh_history
HISTSIZE=50000
SAVEHIST=100000
setopt EXTENDED_HISTORY SHARE_HISTORY HIST_IGNORE_DUPS HIST_REDUCE_BLANKS
autoload -Uz compinit && compinit -u
zstyle ':completion:*' menu select
[[ -r /usr/local/share/zsh-plugins/zsh-autosuggestions/zsh-autosuggestions.zsh ]] && \
source /usr/local/share/zsh-plugins/zsh-autosuggestions/zsh-autosuggestions.zsh
[[ -r /usr/local/share/zsh-plugins/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh ]] && \
source /usr/local/share/zsh-plugins/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh
command -v starship >/dev/null 2>&1 && eval "$(starship init zsh)"
# <<< m21-shell <<<
ZSHRC_EOF
mkdir -p /etc/fish/conf.d
cat > /etc/fish/conf.d/m21-shell.fish <<'FISH_EOF'
set -gx STARSHIP_CONFIG /etc/starship/starship.toml
if type -q starship
starship init fish | source
end
FISH_EOF
cat > /etc/fish/conf.d/proxy.fish <<FISHPROXY_EOF
set -gx http_proxy "${PROXY_URL}"
set -gx https_proxy "${PROXY_URL}"
set -gx ftp_proxy "${PROXY_URL}"
set -gx no_proxy "${NO_PROXY_LIST}"
set -gx HTTP_PROXY "${PROXY_URL}"
set -gx HTTPS_PROXY "${PROXY_URL}"
set -gx FTP_PROXY "${PROXY_URL}"
set -gx NO_PROXY "${NO_PROXY_LIST}"
FISHPROXY_EOF
if [ "$HAS_GNOME" = true ] || [ "$HAS_KDE" = true ]; then
FONT_DIR="/usr/local/share/fonts/MesloNF"
if [[ ! -d "$FONT_DIR" ]]; then
log "Installing Meslo Nerd Font (desktop glyph support)..."
mkdir -p "$FONT_DIR"
if run_retry curl -sfL -x "${PROXY_URL}" -o /tmp/meslo.tar.xz \
"https://github.com/ryanoasis/nerd-fonts/releases/latest/download/Meslo.tar.xz"; then
tar xf /tmp/meslo.tar.xz -C "$FONT_DIR" && fc-cache -f "$FONT_DIR" || true
fi
rm -f /tmp/meslo.tar.xz
fi
log "Set your terminal font to 'MesloLGS Nerd Font'."
fi
success "Shell environment deployed. Default login shell remains bash."
}
mod_clock_fix() {
step "Synchronizing System Clock"
timedatectl set-timezone "$TARGET_TIMEZONE" || true
timedatectl set-ntp true || true
if systemctl list-unit-files | grep -q systemd-timesyncd; then
timeout 30s systemctl restart systemd-timesyncd || true
fi
}
mod_certs() {
step "Installing Certificates"
MNT="/mnt/share_certs_tmp"
mkdir -p "$MNT"
if ! command -v mount.cifs &>/dev/null; then
if [[ "$PKG" == "apt-get" ]]; then run_retry apt-get update -qq >/dev/null 2>&1 || true; run_retry apt-get install -y cifs-utils
elif [[ "$PKG" == "pacman" ]]; then run_retry pacman -S --noconfirm cifs-utils
else run_retry $PKG install -y cifs-utils; fi
fi
if mountpoint -q "$MNT"; then umount -l "$MNT"; fi
if timeout 30s mount -t cifs "$SHARE_PATH" "$MNT" -o username="$SHARE_USER",password="$SHARE_PASS",vers=3.0; then
SOURCE_FULL="$MNT$CERT_SOURCE_PATH"
TEMP_PEM="/tmp/${TARGET_CERT_NAME}_staging.pem"
if [[ -f "$SOURCE_FULL" ]]; then
if ! openssl x509 -inform der -in "$SOURCE_FULL" -out "$TEMP_PEM" 2>/dev/null; then cp "$SOURCE_FULL" "$TEMP_PEM"; fi
if [[ "$PKG" == "dnf" || "$PKG" == "yum" ]]; then
cp "$TEMP_PEM" "/etc/pki/ca-trust/source/anchors/${TARGET_CERT_NAME}.pem"
[[ "$VERSION_MAJOR" -lt 9 ]] && update-ca-trust force-enable 2>/dev/null || true
update-ca-trust extract
elif [[ "$PKG" == "pacman" ]]; then
cp "$TEMP_PEM" "/etc/ca-certificates/trust-source/anchors/${TARGET_CERT_NAME}.crt"
trust extract-compat
else
cp "$TEMP_PEM" "/usr/local/share/ca-certificates/${TARGET_CERT_NAME}.crt"
update-ca-certificates
fi
fi
timeout 15s umount "$MNT" || true
fi
rmdir "$MNT" 2>/dev/null || true
}
mod_base_repos() {
step "Configuring Base OS Repositories"
if [[ "$PKG" == "dnf" || "$PKG" == "yum" ]]; then
if [[ "$OS_ID" == "fedora" ]]; then
log "Setting up Fedora 3rd Party Repos (RPM Fusion & Workstation Repos)..."
run_retry dnf install -y dnf-plugins-core fedora-workstation-repositories || true
run_retry dnf install -y "https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-${VERSION_MAJOR}.noarch.rpm" \
"https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-${VERSION_MAJOR}.noarch.rpm" || true
dnf config-manager --set-enabled rpmfusion-free rpmfusion-nonfree || true
else
if ! rpm -q epel-release >/dev/null 2>&1; then run_retry $PKG install -y epel-release; fi
if [[ "$PKG" == "dnf" ]]; then
if ! dnf repolist enabled 2>/dev/null | grep -E "crb|powertools" >/dev/null; then
run_retry $PKG install -y 'dnf-command(config-manager)'
$PKG config-manager --set-enabled crb 2>/dev/null || $PKG config-manager --set-enabled powertools 2>/dev/null || true
fi
fi
fi
elif [[ "$PKG" == "apt-get" ]]; then
export DEBIAN_FRONTEND=noninteractive
rm -f /etc/apt/sources.list.d/45drives.list
apt-get update -qq || true
BASE_APT_PKGS="curl wget gnupg lsb-release ca-certificates"
if [[ "$OS_ID" != "debian" ]]; then BASE_APT_PKGS="software-properties-common $BASE_APT_PKGS"; fi
run_retry apt-get install -y $BASE_APT_PKGS
fi
}
mod_base_tools() {
step "Installing Base System Tools"
if [[ "$PKG" == "dnf" || "$PKG" == "yum" ]]; then
PACKAGES="git curl wget nano neovim zsh util-linux-user bind-utils net-tools openssl policycoreutils-python-utils psmisc PackageKit pcp pcp-conf pcp-libs pcp-selinux"
run_retry $PKG install -y $PACKAGES
elif [[ "$PKG" == "pacman" ]]; then
PACKAGES="git curl wget nano neovim zsh openssl net-tools bind psmisc networkmanager"
run_retry pacman -S --noconfirm $PACKAGES
timeout 30s systemctl enable --now NetworkManager || true
else
PACKAGES="git curl wget nano neovim zsh openssl net-tools dnsutils psmisc packagekit pcp network-manager"
run_retry apt-get install -y $PACKAGES
timeout 30s systemctl enable --now NetworkManager || true
fi
systemctl unmask packagekit 2>/dev/null || true
timeout 30s systemctl start packagekit 2>/dev/null || true
}
mod_network() {
step "Configuring Network & DNS"
if [[ "$PKG" == "apt-get" ]] && command -v netplan >/dev/null 2>&1; then
if ls /etc/netplan/*.yaml >/dev/null 2>&1 && grep -q "addresses:" /etc/netplan/*.yaml; then
log "Static Netplan detected. Skipping wipe to prevent lockout."
else
mkdir -p /etc/netplan
cat > /etc/netplan/01-network-manager-all.yaml <<EOF
network:
version: 2
renderer: NetworkManager
EOF
netplan apply || true
fi
fi
sed -i "/${DOMAIN_FQDN}/d; /${DOMAIN_ALT}/d; /${DC_DNS_IP}/d; /${FILE_SERVER_NAME}/d" /etc/hosts
cat >> /etc/hosts <<EOF
${DC_DNS_IP} ${DOMAIN_FQDN} ${DOMAIN_ALT} ${DOMAIN_SHORT}
${FILE_SERVER_IP} ${FILE_SERVER_NAME}.${DOMAIN_FQDN} ${FILE_SERVER_NAME}.${DOMAIN_ALT} ${FILE_SERVER_NAME}
EOF
if [[ -L /etc/resolv.conf ]]; then rm -f /etc/resolv.conf; fi
echo -e "search ${DOMAIN_FQDN} ${DOMAIN_ALT}\nnameserver ${DC_DNS_IP}" > /etc/resolv.conf
if command -v nmcli &>/dev/null; then
TARGET_IFACE=$(ip -4 -o addr show | grep "172.16." | awk '{print $2}' | head -n1)
if [[ -n "$TARGET_IFACE" ]]; then
CONN=$(nmcli -t -f NAME,DEVICE con show --active | grep ":${TARGET_IFACE}" | cut -d: -f1 | head -n1)
if [[ -n "$CONN" ]]; then
nmcli con mod "$CONN" ipv4.dns "$DC_DNS_IP" ipv4.dns-search "${DOMAIN_FQDN},${DOMAIN_ALT}" ipv4.ignore-auto-dns yes
timeout 15s nmcli con up "$CONN" >/dev/null 2>&1
fi
fi
fi
echo -e "net.ipv6.conf.all.disable_ipv6 = 1\nnet.ipv6.conf.default.disable_ipv6 = 1" > /etc/sysctl.d/90-disable-ipv6.conf
sysctl --system &>/dev/null || true
if command -v systemctl &>/dev/null; then
if [[ "$PKG" == "apt-get" ]]; then run_retry apt-get install -y chrony; CHRONY_CONF="/etc/chrony/chrony.conf"
elif [[ "$PKG" == "pacman" ]]; then run_retry pacman -S --noconfirm chrony; CHRONY_CONF="/etc/chrony.conf"
else run_retry $PKG install -y chrony; CHRONY_CONF="/etc/chrony.conf"; fi
if [[ -f "$CHRONY_CONF" ]]; then
sed -i '/server/d; /pool/d' "$CHRONY_CONF" 2>/dev/null || true
echo "server ${NTP_SERVER} iburst" >> "$CHRONY_CONF"
fi
timeout 30s systemctl restart chronyd 2>/dev/null || timeout 30s systemctl restart chrony || true
fi
}
mod_firewall() {
step "Configuring Firewalld (Defense in Depth)"
if [[ "$PKG" == "apt-get" ]]; then
run_retry apt-get install -y firewalld
systemctl disable ufw --now 2>/dev/null || true
elif [[ "$PKG" == "pacman" ]]; then
run_retry pacman -S --noconfirm firewalld
else
run_retry $PKG install -y firewalld
fi
systemctl enable --now firewalld
firewall-cmd --permanent --zone=trusted --add-source=172.17.0.0/16
firewall-cmd --permanent --zone=trusted --add-source=172.18.0.0/16
firewall-cmd --permanent --zone=trusted --add-source=172.19.0.0/16
firewall-cmd --permanent --zone=trusted --add-source=172.20.0.0/16
firewall-cmd --permanent --zone=trusted --add-source=192.168.250.0/24
firewall-cmd --permanent --add-service=http
firewall-cmd --permanent --add-service=https
firewall-cmd --permanent --remove-service=ssh
firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="10.21.0.0/21" service name="ssh" accept'
firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="172.16.121.0/24" service name="ssh" accept'
firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="172.16.21.0/24" service name="ssh" accept'
firewall-cmd --reload
}
mod_resize_home() {
step "LVM Home Resizer"
if ! command -v lvs &>/dev/null; then return; fi
if ! mountpoint -q /home; then return; fi
HOME_DEV=$(findmnt -n -o SOURCE /home)
if [[ "$HOME_DEV" != *"/mapper/"* ]]; then return; fi
LV_NAME=$(lvs --noheadings -o lv_name "$HOME_DEV" | tr -d ' ')
VG_NAME=$(lvs --noheadings -o vg_name "$HOME_DEV" | tr -d ' ')
LV_PATH="/dev/$VG_NAME/$LV_NAME"
ROOT_LV_PATH="/dev/$VG_NAME/root"
MAPPER_PATH="/dev/mapper/${VG_NAME}-${LV_NAME}"
CURRENT_SIZE=$(lvs --noheadings -o lv_size --units g "$LV_PATH" 2>/dev/null | tr -d 'g ' || lvs --noheadings -o L_SIZE --units g "$LV_PATH" | tr -d 'g ')
if [[ ${CURRENT_SIZE%.*} -le 9 ]]; then return; fi
tar czf /tmp/home_backup.tar.gz -C /home .
fuser -km /home || true
timeout 30s umount /home || timeout 15s umount -l /home || true
lvremove -y "$LV_PATH"
lvcreate -L "$HOME_TARGET_SIZE" -n "$LV_NAME" "$VG_NAME" -y
mkfs.ext4 "$LV_PATH"
sed -i '/\/home/d' /etc/fstab
echo "$MAPPER_PATH /home ext4 defaults 0 0" >> /etc/fstab
systemctl daemon-reload || true
timeout 30s mount /home || true
tar xzf /tmp/home_backup.tar.gz -C /home
if command -v restorecon &>/dev/null; then restorecon -R /home; fi
lvextend -l +100%FREE "$ROOT_LV_PATH"
xfs_growfs / || resize2fs "$ROOT_LV_PATH" || true
rm -f /tmp/home_backup.tar.gz
}
###############################################################################
# 6. DIAGNOSTICS (READ-ONLY)
###############################################################################
_chk() {
local LABEL="$1"; shift
if "$@" &>/dev/null; then
echo -e " ${GREEN}[PASS]${NC} ${LABEL}"
else
echo -e " ${RED}[FAIL]${NC} ${LABEL}"
fi
}
mod_doctor() {
step "System Doctor (read-only diagnostics)"
echo -e "\n${YELLOW}-- Network & DNS --${NC}"
_chk "resolv.conf points at DC (${DC_DNS_IP})" grep -q "${DC_DNS_IP}" /etc/resolv.conf
_chk "DNS resolves ${DOMAIN_FQDN}" timeout 5s getent hosts "${DOMAIN_FQDN}"
_chk "DC reachable (ping ${DC_DNS_IP})" timeout 5s ping -c1 -W2 "${DC_DNS_IP}"
_chk "File server reachable (ping ${FILE_SERVER_IP})" timeout 5s ping -c1 -W2 "${FILE_SERVER_IP}"
_chk "IPv6 disabled" bash -c "sysctl -n net.ipv6.conf.all.disable_ipv6 | grep -q 1 2>/dev/null || sysctl net.ipv6.conf.all.disable_ipv6 2>/dev/null | grep -q '= 1'"
echo -e "\n${YELLOW}-- Time --${NC}"
_chk "Timezone is ${TARGET_TIMEZONE}" bash -c "timedatectl show -p Timezone --value | grep -qx '${TARGET_TIMEZONE}' 2>/dev/null || timedatectl | grep -q '${TARGET_TIMEZONE}'"
if command -v chronyc &>/dev/null; then
_chk "chrony synchronized" bash -c "chronyc tracking 2>/dev/null | grep -q 'Leap status.*Normal'"
fi
echo -e "\n${YELLOW}-- Proxy Path --${NC}"
_chk "Proxy TCP reachable (${PROXY_URL})" timeout 5s bash -c "exec 3<>/dev/tcp/$(echo "$PROXY_URL" | awk -F/ '{print $3}' | cut -d: -f1)/$(echo "$PROXY_URL" | awk -F: '{print $NF}')"
_chk "External fetch via proxy (flathub summary.idx)" timeout 20s curl -s -x "${PROXY_URL}" -o /dev/null -w '%{http_code}' https://dl.flathub.org/repo/summary.idx
_chk "/etc/environment has proxy vars" grep -q "^http_proxy=" /etc/environment
_chk "/etc/profile.d/proxy.sh present & non-empty" test -s /etc/profile.d/proxy.sh
_chk "fwupd proxy drop-in present" test -f /etc/systemd/system/fwupd.service.d/http-proxy.conf
_chk "packagekit proxy drop-in present" test -f /etc/systemd/system/packagekit.service.d/http-proxy.conf
if [[ -f /var/lib/flatpak/repo/config ]]; then
_chk "flathub remote URL correct (dl.flathub.org/repo)" awk '/^\[remote "flathub"\]/{f=1;next} /^\[/{f=0} f && $0=="url=https://dl.flathub.org/repo/"{found=1} END{exit !found}' /var/lib/flatpak/repo/config
_chk "flathub ostree proxy pinned in repo config" awk '/^\[remote "flathub"\]/{f=1;next} /^\[/{f=0} f && /^proxy=/{found=1} END{exit !found}' /var/lib/flatpak/repo/config
fi
echo -e "\n${YELLOW}-- Trust & Identity --${NC}"
_chk "GORTT root cert in trust store" bash -c "ls /etc/pki/ca-trust/source/anchors/${TARGET_CERT_NAME}.pem /usr/local/share/ca-certificates/${TARGET_CERT_NAME}.crt /etc/ca-certificates/trust-source/anchors/${TARGET_CERT_NAME}.crt 2>/dev/null | grep -q ."
if command -v realm &>/dev/null; then
_chk "Domain joined (${DOMAIN_FQDN})" bash -c "timeout 10s realm list 2>/dev/null | grep -q '${DOMAIN_FQDN}'"
fi
_chk "sssd active" systemctl is-active --quiet sssd
_chk "sshd active" bash -c "systemctl is-active --quiet sshd || systemctl is-active --quiet ssh"
echo -e "\n${YELLOW}-- Services --${NC}"
_chk "firewalld active" systemctl is-active --quiet firewalld
_chk "fail2ban active" systemctl is-active --quiet fail2ban
if command -v docker &>/dev/null; then
_chk "docker active" systemctl is-active --quiet docker
_chk "docker daemon proxy configured" bash -c "docker info 2>/dev/null | grep -qi 'HTTP Proxy'"
fi
if command -v flatpak &>/dev/null; then
_chk "flathub remote configured" bash -c "flatpak remotes 2>/dev/null | grep -q flathub"
fi
_chk "packagekit active" systemctl is-active --quiet packagekit
echo -e "\n${YELLOW}-- Known Delay Sources --${NC}"
_chk "systemd-networkd-wait-online masked" bash -c "systemctl is-enabled systemd-networkd-wait-online.service 2>/dev/null | grep -q masked"
_chk "NetworkManager-wait-online masked" bash -c "systemctl is-enabled NetworkManager-wait-online.service 2>/dev/null | grep -q masked"
if command -v sqlite3 &>/dev/null && [ -f /var/lib/PackageKit/transactions.db ]; then
STALE=$(sqlite3 /var/lib/PackageKit/transactions.db "SELECT COUNT(*) FROM proxy WHERE proxy_http IS NULL OR proxy_http = '';" 2>/dev/null || echo "?")
if [[ "$STALE" != "0" && "$STALE" != "?" ]]; then
warn "PackageKit has ${STALE} stale (empty) proxy entries — run --gs-fix to scrub."
else
echo -e " ${GREEN}[PASS]${NC} PackageKit proxy table clean"
fi
fi
echo ""
log "Doctor complete. FAILs above are your troubleshooting starting points."
}
###############################################################################
# 7. CLI ROUTER
###############################################################################
show_help() {
echo "Usage: $0 [OPTION]"
echo "Version: ${SCRIPT_VERSION}"
echo "Supported: RHEL/CentOS/Alma/Rocky/Fedora (dnf/yum), Ubuntu/Debian/Zorin (apt), Arch (pacman)."
echo ""
echo "Core Deployment:"
echo " --basics Proxy, Certs, Repos, Network, Firewalld, AD, Cleanup. (Servers)"
echo " --full Everything (Basics + GUI + Docker + Web/DB + Flatpak/Tools + Shell + Cockpit). (Workstations)"
echo ""
echo "Modular Execution:"
echo " --docker Install and configure Docker Engine with Proxy/Subnets."
echo " --flatpak Configure Flatpak, Flathub (ostree proxy pin), and GUI App Centers."
echo " --gs-fix Fix GNOME Software slowness (fwupd/PackageKit/ostree/appstream)."
echo " --ad-join Run the SSSD and Realmd AD Join sequence."
echo " --certs Mount CIFS, fetch root cert, update CA trust."
echo " --gui-proxy Configure dconf (GNOME/Cinnamon), KDE, and Firefox Proxies."
echo " --proxy-tool Install the 'toggle-proxy' dynamic CLI tool."
echo " --desktop-tools Install Fastfetch, GNOME Tweaks, Flatseal, ExtensionManager."
echo " --shell Starship prompt + zsh/fish/bash integration, plugins, fonts."
echo " --web-stack Install PHP, Nginx, Node, and Java."
echo " --db-stack Install MariaDB and PostgreSQL."
echo " --tools Install zsh, fish, neovim, git, nano, lazydocker."
echo " --resize-home Shrink LVM /home to ${HOME_TARGET_SIZE} (Backup/Restore)."
echo ""
echo "Diagnostics:"
echo " --doctor Read-only health check: DNS, proxy path, certs, AD, services."
echo ""
}
if [[ $# -eq 0 ]]; then show_help; exit 0; fi
if [[ $EUID -ne 0 ]]; then
echo -e "${RED}This script must be run as root (sudo $0 $*).${NC}"
exit 1
fi
# Initialize Global Logging
touch "$LOG_FILE" || true
chmod 600 "$LOG_FILE" || true
# Strip ANSI codes for the file copy, keeping colors active on the terminal
exec > >(tee >(sed -u 's/\x1B\[[0-9;]*m//g' >> "$LOG_FILE")) 2>&1
cat > /etc/logrotate.d/m21-setup <<EOF
${LOG_FILE} {
size 5M
rotate 4
compress
missingok
notifempty
}
EOF
echo -e "\n=== INVOCATION: $0 $* ===" >> "$LOG_FILE"
detect_and_fix_os
init_header
while [[ "$#" -gt 0 ]]; do
case $1 in
--basics) mod_proxy; mod_clock_fix; mod_certs; mod_base_repos; mod_base_tools; mod_network; mod_firewall; mod_domain_users; mod_cleanup ;;
--full) mod_proxy; mod_gui_proxy; mod_proxy_toggle; mod_clock_fix; mod_certs; mod_base_repos; mod_base_tools; mod_flatpak; mod_desktop_tools; mod_shell; mod_gs_fix; mod_network; mod_firewall; mod_domain_users; mod_docker; mod_web_stack; mod_db_stack; mod_cockpit; mod_cleanup ;;
--docker) mod_proxy; mod_docker ;;
--flatpak) mod_proxy; mod_flatpak ;;
--desktop-tools) mod_proxy; mod_desktop_tools; mod_gs_fix ;;
--gs-fix) mod_proxy; mod_gs_fix ;;
--ad-join) mod_domain_users ;;
--certs) mod_certs ;;
--gui-proxy) mod_gui_proxy ;;
--proxy-tool) mod_proxy_toggle ;;
--shell) mod_proxy; mod_shell ;;
--web-stack) mod_proxy; mod_web_stack ;;
--db-stack) mod_proxy; mod_db_stack ;;
--tools) mod_proxy; mod_base_tools; mod_lazydocker ;;
--resize-home) mod_resize_home ;;
--doctor) mod_doctor ;;
*) echo "Unknown option: $1"; show_help; exit 1 ;;
esac
shift
done
echo -e "\n${GREEN}[$(date +'%H:%M:%S')] === Setup Complete (${SCRIPT_VERSION}) ===${NC}"
M21 AD ssh key setup
#!/usr/bin/env bash
#
# M21 AD SSH Key Setup — self-service
#
# Generates an SSH keypair (if you don't already have one) and publishes the
# public half to your own AD "info" (Notes) attribute. Every M21 Linux host
# that's been through domainjoin.sh reads that field via sss_ssh_authorizedkeys
# to decide which key(s) are allowed to log in as you — publish it once here
# and it works on every host, no per-server key distribution needed.
#
# Run this AS YOURSELF, on your own machine. It needs your AD password once,
# to bind and write your own AD object — nothing else is touched, and your
# password is never written to disk or visible to other users on the machine
# while it runs.
#
set -euo pipefail
DOMAIN_FQDN="m21.gov.local"
BASE_DN="DC=m21,DC=gov,DC=local"
# Same DC list as domainjoin.sh, same reason: some of these have a history of
# accepting a TCP connection and never answering an actual LDAP query, so a
# ping/telnet-style check isn't good enough — probe with a real query.
DC_LIST="172.40.132.67 172.40.132.66 172.42.132.66 172.16.21.161"
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[0;33m'; NC='\033[0m'
info() { echo -e "${GREEN}[*]${NC} $1"; }
warn() { echo -e "${YELLOW}[!]${NC} $1"; }
error() { echo -e "${RED}[x]${NC} $1" >&2; }
# LDAP wraps long attribute values across multiple lines (RFC 2849
# line-folding — continuation lines start with a single space). A plain
# `grep '^info:'`-style extraction silently truncates anything long enough to
# wrap, which most SSH public keys are. This unfolds properly regardless of
# where the line breaks land. Tested against real DC output.
extract_ldif_value() {
awk -v attr="^$1:" '
$0 ~ attr { sub(attr" *", ""); val=$0; capturing=1; next }
/^ / && capturing { sub(/^ /, ""); val = val $0; next }
capturing { print val; capturing=0; exit }
END { if (capturing) print val }
'
}
echo "=== M21 AD SSH Key Setup ==="
echo
# --- dependencies ------------------------------------------------------
MISSING=()
command -v ldapsearch &>/dev/null || MISSING+=(openldap-clients)
command -v ldapmodify &>/dev/null || MISSING+=(openldap-clients)
command -v ssh-keygen &>/dev/null || MISSING+=(openssh-clients)
if [[ ${#MISSING[@]} -gt 0 ]]; then
warn "Missing: ${MISSING[*]} — attempting to install (needs sudo)."
if command -v dnf &>/dev/null; then
sudo dnf install -y "${MISSING[@]}"
elif command -v yum &>/dev/null; then
sudo yum install -y "${MISSING[@]}"
elif command -v apt-get &>/dev/null; then
sudo apt-get install -y ldap-utils openssh-client
else
error "Don't know this package manager. Install ${MISSING[*]} yourself and re-run."
exit 1
fi
fi
# --- who ---------------------------------------------------------------
DEFAULT_USER="$(whoami)"
read -rp "AD username [${DEFAULT_USER}]: " AD_USER
AD_USER="${AD_USER:-$DEFAULT_USER}"
# --- find a DC that actually answers ------------------------------------
info "Locating a domain controller..."
DC=""
for CANDIDATE in $DC_LIST; do
if timeout 5s ldapsearch -x -LLL -H "ldap://${CANDIDATE}" \
-s base -b "" defaultNamingContext &>/dev/null; then
DC="$CANDIDATE"
break
fi
done
if [[ -z "$DC" ]]; then
error "No domain controller answered an LDAP query."
error "Check you're on the office network or VPN, then try again."
exit 1
fi
info "Using domain controller: ${DC}"
# --- key generation ------------------------------------------------------
KEY_PATH="${HOME}/.ssh/id_ed25519"
mkdir -p "${HOME}/.ssh"
chmod 700 "${HOME}/.ssh"
GENERATE=true
if [[ -f "$KEY_PATH" ]]; then
read -rp "A key already exists at ${KEY_PATH}. Reuse it instead of generating a new one? [Y/n] " REUSE
REUSE="${REUSE:-Y}"
[[ "$REUSE" =~ ^[Yy] ]] && GENERATE=false
fi
if [[ "$GENERATE" == true ]]; then
ssh-keygen -t ed25519 -C "${AD_USER}@$(hostname -s 2>/dev/null || hostname)" -f "$KEY_PATH"
fi
chmod 600 "$KEY_PATH"
chmod 644 "${KEY_PATH}.pub" 2>/dev/null || true
NEW_PUBKEY="$(cat "${KEY_PATH}.pub")"
# --- bind, find DN, read current value ------------------------------------
echo
read -rsp "AD password for ${AD_USER}: " AD_PASS
echo
echo
# -y reads the password from a file descriptor instead of argv, so it never
# appears in `ps` output for other users on this machine. Process
# substitution gives each call its own private, unlinked fd.
ldap_bind_args=(-x -H "ldap://${DC}" -D "${AD_USER}@${DOMAIN_FQDN}")
USER_DN=$(ldapsearch "${ldap_bind_args[@]}" -y <(printf '%s' "$AD_PASS") \
-b "$BASE_DN" "(sAMAccountName=${AD_USER})" dn 2>/dev/null \
| extract_ldif_value "dn")
if [[ -z "$USER_DN" ]]; then
error "Could not find your AD object — wrong username, wrong password, or"
error "no domain controller reachable. Nothing was changed."
unset AD_PASS
exit 1
fi
CURRENT_INFO=$(ldapsearch "${ldap_bind_args[@]}" -y <(printf '%s' "$AD_PASS") \
-b "$USER_DN" -s base info 2>/dev/null \
| extract_ldif_value "info")
echo "Your AD object: ${USER_DN}"
if [[ -n "$CURRENT_INFO" ]]; then
echo "Current 'info' (Notes) field:"
echo " ${CURRENT_INFO}"
if [[ "$CURRENT_INFO" != ssh-* ]]; then
echo
warn "That doesn't look like an SSH key. If it's genuine notes,"
warn "continuing will PERMANENTLY OVERWRITE them."
fi
else
echo "Current 'info' field is empty."
fi
echo
read -rp "Publish your new key there now? This replaces whatever's currently in it. [y/N] " CONFIRM
if [[ ! "$CONFIRM" =~ ^[Yy] ]]; then
echo "Cancelled. Your key was generated locally but NOT published to AD."
unset AD_PASS
exit 0
fi
if ldapmodify "${ldap_bind_args[@]}" -y <(printf '%s' "$AD_PASS") <<EOF
dn: ${USER_DN}
changetype: modify
replace: info
info: ${NEW_PUBKEY}
EOF
then
echo
info "Done. Your key is published to AD."
info "It can take a few minutes to reach every host. If login doesn't work"
info "immediately on a specific server, ask IT to run 'sss_cache -E' there."
else
error "The write to AD failed. Your key was generated locally but not published."
error "Most likely cause: your account doesn't have write access to its own"
error "'info' attribute here — ask IT to set it for you instead (they have a"
error "one-line ldapmodify command for exactly this)."
fi
unset AD_PASS
DISABLE WAYLAND FOR SUPPORT MESH COMPATIBILITY
| 1 | From the initial boot after installation on the login screen |
| 2 | Select your account and go to the bottom right to click the gear icon |
| 3 | Select Gnome on Xorg |
| 4 | Input password to proceed with login |
Open Terminal
sudo -i
nano /etc/gdm/custom.conf
| 1 | Go to the line #WaylandEnable=false and Delete the hashtag '#' |
| 2 | To exit: CTRL + 'X' |
| 3 | Select 'Y' for yes |
| 4 | To save: 'enter' key |
sudo dnf update -y
sudo reboot
******************************COMPLETED*******************************
CHANGE COMPUTER NAME
| 1 |
Open Terminal PC Name example: MYDNS-IT-C12-L.M21.GOV.LOCAL |
| 2 | sudo hostnamectl set-hostname mydns-it-c12-l.m21.gov.local |
******************************COMPLETED*******************************
TO JOIN THE DOMAIN
Open Terminal
sudo nano /etc/environment
Add the following line to the file:
http_proxy="http://172.40.4.14:8080/"
https_proxy="http://172.40.4.14:8080/"
ftp_proxy="http://172.40.4.14:8080/"
no_proxy=127.0.0.1,localhost,.localdomain,172.30.0.0/20,172.26.21.0/24
HTTP_PROXY="http://172.40.4.14:8080/"
HTTPS_PROXY="http://172.40.4.14:8080/"
FTP_PROXY="http://172.40.4.14:8080/"
NO_PROXY=127.0.0.1,localhost,.localdomain,172.30.0.0/20,172.26.21.0/24
| 1 | To exit: CTRL + 'X' |
| 2 | Select 'Y' for yes |
| 3 |
To save: 'enter' key |
| 4 |
Log out and back in again |
sudo nano /etc/dnf/dnf.conf
Add the following line to the file:
fastestmirror=1
| 1 | To exit: CTRL + 'X' |
| 2 | Select 'Y' for yes |
| 3 |
To save: 'enter' key |
On Fedora
sudo dnf -y install epel-release && sudo dnf -y install realmd sssd oddjob oddjob-mkhomedir adcli samba-common-tools authselect nano curl wget htop btop net-tools git zip unzip tar freeipa-client tmux
On Ubuntu
sudo apt -y install realmd sssd sssd-tools libnss-sss libpam-sss adcli samba-common-bin oddjob oddjob-mkhomedir packagekit nano curl wget htop btop net-tools git zip unzip tar freeipa-client tmux
Fix DNS
sudo unlink /etc/resolv.conf
sudo nano /etc/resolv.conf
Input the IP Address and the Domain Name into file
search m21.gov.local
nameserver 172.16.21.161
| 1 | To exit: CTRL + 'X' |
| 2 | Select 'Y' for yes |
| 3 | To save: 'enter' key |
sudo nano /etc/hosts
Input the following lines into file
172.16.21.161 m21.gov.local M21.GOV.LOCAL
172.16.21.16 mydns-0ic16.m21.gov.local mydns-0ic16
| 1 | To exit: CTRL + 'X' |
| 2 | Select 'Y' for yes |
| 3 | To save: 'enter' key |
sudo realm discover M21.GOV.LOCAL
ping -c 4 M21.GOV.LOCAL
| To stop ping: CTRL + 'C' |
sudo realm join -U ent_username@M21.GOV.LOCAL m21.gov.local -v
Input Ent Account Password
To ensure that it was successful run the realm join code again and you should see "Already joined to this domain"
******************************COMPLETED*******************************
GROUP POLICY CONFLICT RESOLVE (to login without wifi)
Open Terminal
sudo nano /etc/sssd/sssd.conf
Input at the end of the file
ad_gpo_access_control = permissive
Your "/etc/sssd/sssd.conf" should look like this. Make all necessary changes or copy and paste this into the file replacing everything. Can use CTRL + K to cut entire lines until the file is empty.
[sssd]
domains = m21.gov.local
config_file_version = 2
services = nss, pam
[nss]
homedir_substring = /home
[domain/m21.gov.local]
default_shell = /bin/bash
krb5_store_password_if_offline = True
cache_credentials = True
krb5_realm = M21.GOV.LOCAL
realmd_tags = manages-system joined-with-adcli
id_provider = ad
fallback_homedir = /home/%u
ad_domain = m21.gov.local
use_fully_qualified_names = False
ldap_id_mapping = True
access_provider = ad
ad_gpo_access_control = permissive
| 1 | To exit: CTRL + 'X' |
| 2 | Select 'Y' for yes |
| 3 | To save: 'enter' key |
On Fedora
sudo authselect select sssd with-mkhomedir
sudo systemctl restart sssd
On Ubuntu
sudo pam-auth-update --enable mkhomedir
sudo systemctl restart sssd
On CentOS 7
sudo authconfig --enablesssdauth --enablesssd --enablemkhomedir --updateall
sudo systemctl restart sssd
******************************COMPLETED*******************************
TO MAKE AD ACCOUNT A SUDOER
Open Terminal
sudo nano /etc/sudoers.d/domain_admins
| 1 |
Input line : firstname.lastname ALL=(ALL) ALL |
| 2 |
To allow all ICT Staff: %ICT\ Staff\ SG\ M21 ALL=(ALL:ALL) ALL |
|
cn=mydns ict staff sg,ou=security groups_m21,ou=mydns,dc=m21,dc=gov,dc=local |
|
| 3 | To exit: CTRL + 'X' |
| 4 | Select 'Y' for yes |
| 5 | To save: 'enter' key |
******************************COMPLETED*******************************
| 1 | Launch the Files app -> OTHER LOCATIONS -> Bottom of window to enter address |
| 2 | Input: smb://172.16.21.16/ |
| 3 | Toggle on REGISTERED USER |
| 4 | Input: YOUR DOMAIN ACCOUNT USERNAME and PASSWORD |
| 5 | Domain: M21.GOV.LOCAL or 172.16.21.161 |
******************************COMPLETED*******************************
TO ADD PRINTER
Open Terminal
HP Printers
dnf search hplip
sudo dnf install hplip hplip-gui -y
hp-setup
hp-setup ‘printer IP Address’
| 1 | Select detected printer |
| 2 | Follow next prompt until the end |
XEROX Printers
Open Terminal
wget http://download.support.xerox.com/pub/drivers/CQ8580/drivers/linux/pt_BR/XeroxOfficev5Pkg-Linuxx86_64-5.20.661.4684.rpm
sudo dnf -y localinstall XeroxOfficev5Pkg-Linuxx86_64-5.20.661.4684.rpm
NOTE: DO NOT PRINT A TEST PAGE!! Print a regular text document to test
******************************COMPLETED*******************************
TO REPLACE FEDORA LOGO
Download Image and rename as: MYDNS-Logo
| 1 | Go to EXTENSION MANAGER -> SYSTEM EXTENSIONS -> BACKGROUND LOGO |
| 2 | Click on the gear icon to get the background settings |
| 3 |
Go to LOGO -> Filename to attach the MYDNS-Logo.png file -> Filename (dark) to attach the MYDNS-Logo.png file |
| 4 | Scroll down to OPTIONS -> Toggle on Show for all backgrounds |
******************************COMPLETED*******************************
Browse to 172.16.21.16>fileserver2>General>IT FILES>prx and copy the GORTT.pem file to a folder on the local machine.
Adding Certificate File to Local Machine (Ubuntu)
Browse to 172.16.21.16>fileserver2>General>IT FILES>prx and copy the GORTT.pem file to a folder on the local machine.
sudo apt-get install -y ca-certificates
openssl x509 -in GORTT.pem -out GORTT.crt
- Move the ceritficate file to the proper location with the following command:
sudo mv GORTT.crt /usr/local/share/ca-certificates - Update trusted certificates with the following command:
sudo update-ca-certificates
HELPFUL APPS
| 1 |
Extension Manager
flatpak install flathub com.mattjakeman.ExtensionManager |
| 2 | GNOME Tweaks ( sudo dnf install gnome-tweaks ) |
| 3 |
OnlyOffice https://download.onlyoffice.com/install/desktop/editors/linux/onlyoffice-desktopeditors.x86_64.rpm sudo dnf -y localinstall onlyoffice-desktopeditors.x86_64.rpm |
| 4 |
Element
flatpak install flathub im.riot.Riot |
| 5 |
Google Chome (Fedora) wget https://dl.google.com/linux/direct/google-chrome-stable_current_x86_64.rpm sudo dnf -y localinstall google-chrome-stable_current_x86_64.rpm |
| 6 |
Google Chrome (Ubuntu) sudo apt install curl software-properties-common apt-transport-https ca-certificates -y curl -fSsL https://dl.google.com/linux/linux_signing_key.pub | gpg --dearmor | sudo tee /usr/share/keyrings/google-chrome.gpg > /dev/null echo deb [arch=amd64 signed-by=/usr/share/keyrings/google-chrome.gpg] http://dl.google.com/linux/chrome/deb/ stable main | sudo tee /etc/apt/sources.list.d/google-chrome.list sudo apt update sudo apt -y install google-chrome-stable |
HELPFUL EXTENSIONS
| 1 | Dash to Dock - Displays a dynamic centered Taskbar |
| 2 | Dash to Panel - Displays screen width static Taskbar |
| 3 | Vitals - displays the PC health at the top right |
| 4 | Desktop icons NG (Ding) - display anything saved to desktop |
| 5 | Clipboard History - enables clipboard history tool |
******************************COMPLETED*******************************

No comments to display
No comments to display