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 - v64cv65c
#!/usr/bin/env bash
#
# MASTER INFRASTRUCTURE SETUP
# Version: v64v65
#
# v64v65 Changelog:
# - FIX:NEW: krb5-libsDMZ shipspeer-to-peer atrust. stockmod_firewall /etc/krb5.confnow withtrusts default_realmDMZ_TRUSTED_CIDRS
# commented out. adcli authenticates fine (that step doesn't need it),
# butdefault: the separateDMZ GSSAPI/SASLsubnet, library172.16.121.0/24) callwholesale thatin performs the actual
# LDAP bind does its own profile-based realm lookup and fails with
# "Configuration file does not specify default realm" if the system
# krb5.conf has none set — even though --domain-realm was passed
# explicitly on the adcli command line. Every never-before-joined host
# hits this identically; confirmed on servicedesk.m21.gov.local
# 2026-08-04 against the unmodified package template. mod_domain_users
# now sets default_realm before attempting the join, only if it'firewalld's
# genuinelytrusted absent.zone on every host — no per-port rules, no per-peer IP
# tailoring. These VMs talk to each other constantly for DB access and
# cross-service calls, entirely internal, no external exposure; a shared
# trusted subnet matches that shape and means adding a new DMZ VM never
# requires a firewall change on any existing host. Scoped to internal
# peers only — the public zone (http/https, the ssh rich rules) still
# governs everything arriving from outside the DMZ.
#
###############################################################################
# 1. CONFIGURATION
###############################################################################
SCRIPT_VERSION="v64"v65"
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
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}"
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*******************************
