Skip to content

Instantly share code, notes, and snippets.

@imannms000
Last active June 12, 2026 08:23
Show Gist options
  • Select an option

  • Save imannms000/69e6a8c4d19032a2ad7a529b3cdc3431 to your computer and use it in GitHub Desktop.

Select an option

Save imannms000/69e6a8c4d19032a2ad7a529b3cdc3431 to your computer and use it in GitHub Desktop.
Production-Hardened MySQL 8.0 Installer for Ubuntu 20.04/22.04/24.04 WSL2 Compatible | Idempotent | SOC2/ISO27001-aligned
#!/bin/bash
#===============================================================================
# Production-Hardened MySQL 8.0 Installer for Ubuntu 20.04/22.04/24.04
# Idempotent | Failsafe | SOC2/ISO27001-aligned
#===============================================================================
set -euo pipefail
IFS=$'\n\t'
#-------------------------------------------------------------------------------
# CONFIGURATION
#-------------------------------------------------------------------------------
readonly SCRIPT_NAME="$(basename "$0")"
readonly LOG_FILE="/var/log/${SCRIPT_NAME%.sh}.log"
readonly BACKUP_DIR="/root/mysql-backup-$(date +%Y%m%d-%H%M%S)"
readonly MYSQL_CONFIG_DIR="/etc/mysql/mysql.conf.d"
readonly MYSQLD_CNF="${MYSQL_CONFIG_DIR}/99-production-hardening.cnf"
# Minimum system requirements
readonly MIN_RAM_MB=1024
readonly MIN_DISK_MB=2048
#-------------------------------------------------------------------------------
# LOGGING
#-------------------------------------------------------------------------------
exec > >(tee -a "${LOG_FILE}") 2>&1
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
}
die() {
log "❌ FATAL: $*"
exit 1
}
#-------------------------------------------------------------------------------
# PREFLIGHT CHECKS
#-------------------------------------------------------------------------------
preflight() {
log "πŸ” Running pre-flight checks..."
# Root check
if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then
die "This script must be run as root or with sudo."
fi
# OS check
if ! grep -qE 'Ubuntu (20|22|24)\.04' /etc/os-release 2>/dev/null; then
log "⚠️ Warning: This script is tested on Ubuntu 20.04/22.04/24.04 only."
read -r -p "Continue anyway? [y/N]: " confirm
[[ "$confirm" =~ ^[Yy]$ ]] || die "Aborted by user."
fi
# RAM check
local avail_ram
avail_ram=$(free -m | awk '/^Mem:/{print $7}')
if [[ "${avail_ram}" -lt "${MIN_RAM_MB}" ]]; then
die "Insufficient available RAM: ${avail_ram}MB (min: ${MIN_RAM_MB}MB)"
fi
# Disk check
local avail_disk
avail_disk=$(df -m / | awk 'NR==2 {print $4}')
if [[ "${avail_disk}" -lt "${MIN_DISK_MB}" ]]; then
die "Insufficient disk space on /: ${avail_disk}MB (min: ${MIN_DISK_MB}MB)"
fi
# Check for MariaDB conflict
if dpkg -l | grep -qE '^ii\s+mariadb'; then
die "MariaDB is installed. MySQL and MariaDB conflict. Remove MariaDB first."
fi
log "βœ… Pre-flight checks passed."
}
#-------------------------------------------------------------------------------
# PASSWORD INPUT WITH STRENGTH VALIDATION
#-------------------------------------------------------------------------------
prompt_password() {
local var_name="$1"
local prompt_text="$2"
local confirm_text="$3"
local min_length="${4:-16}"
local pass1 pass2
while true; do
read -r -s -p "${prompt_text}: " pass1
echo ""
read -r -s -p "${confirm_text}: " pass2
echo ""
if [[ -z "$pass1" ]]; then
log "⚠️ Password cannot be empty."
continue
fi
if [[ "${#pass1}" -lt "${min_length}" ]]; then
log "⚠️ Password must be at least ${min_length} characters."
continue
fi
if [[ "$pass1" != "$pass2" ]]; then
log "❌ Passwords do not match."
continue
fi
# Basic entropy check
if ! [[ "$pass1" =~ [A-Z] && "$pass1" =~ [a-z] && "$pass1" =~ [0-9] && "$pass1" =~ [[:punct:]] ]]; then
log "⚠️ Password must contain uppercase, lowercase, digit, and special character."
continue
fi
printf -v "$var_name" '%s' "$pass1"
break
done
}
#-------------------------------------------------------------------------------
# INSTALLATION (Idempotent: Skips if package is installed)
#-------------------------------------------------------------------------------
install_mysql() {
if dpkg -l | grep -qE '^ii\s+mysql-server'; then
log "πŸ“¦ MySQL server package is already installed. Skipping installation step."
return 0
fi
log "πŸ› οΈ Installing MySQL Server..."
export DEBIAN_FRONTEND=noninteractive
apt-get update -y
local mysql_version
mysql_version=$(apt-cache policy mysql-server 2>/dev/null | grep Candidate | awk '{print $2}' | cut -d'-' -f1 | cut -d':' -f2 | cut -d'.' -f1-2 || echo "0")
if [[ "$mysql_version" == "8.0" ]]; then
log "πŸ“¦ Using distribution-provided MySQL 8.0..."
apt-get install -y mysql-server mysql-client
else
log "πŸ“₯ Adding official MySQL APT repository..."
local apt_config_url
apt_config_url=$(curl -s https://dev.mysql.com/downloads/repo/apt/ | grep -oP 'https://dev.mysql.com/get/mysql-apt-config_[^"]+_all.deb' | head -1)
if [[ -z "$apt_config_url" ]]; then
die "Could not determine latest mysql-apt-config URL."
fi
local deb_file="/tmp/$(basename "$apt_config_url")"
wget -q "$apt_config_url" -O "$deb_file" || die "Failed to download MySQL APT config"
echo "mysql-apt-config mysql-apt-config/select-server select mysql-8.0" | debconf-set-selections
dpkg -i "$deb_file" || apt-get install -f -y
rm -f "$deb_file"
apt-get update -y
apt-get install -y mysql-server mysql-client
fi
systemctl enable mysql
systemctl start mysql
local wait_count=0
until mysqladmin ping --silent 2>/dev/null || [[ $wait_count -ge 30 ]]; do
sleep 2
((wait_count++))
done
if ! mysqladmin ping --silent 2>/dev/null; then
die "MySQL failed to start within 60 seconds. Check /var/log/mysql/error.log"
fi
log "βœ… MySQL installed and running."
}
#-------------------------------------------------------------------------------
# SECURITY HARDENING (Idempotent: Uses conditional checks per command)
#-------------------------------------------------------------------------------
harden_mysql() {
log "πŸ”’ Applying security hardening..."
local root_pass="$1"
local admin_user="$2"
local admin_pass="$3"
local sql_file
sql_file=$(mktemp /tmp/mysql-harden-XXXXXX.sql)
chmod 600 "$sql_file"
cat > "$sql_file" <<EOF
-- Safely alter root auth if not already done
ALTER USER IF EXISTS 'root'@'localhost' IDENTIFIED WITH caching_sha2_password BY '${root_pass}';
-- Create dedicated admin user if missing, or update its credentials safely
CREATE USER IF NOT EXISTS '${admin_user}'@'localhost' IDENTIFIED WITH caching_sha2_password BY '${admin_pass}';
ALTER USER '${admin_user}'@'localhost' IDENTIFIED WITH caching_sha2_password BY '${admin_pass}';
GRANT ALL PRIVILEGES ON *.* TO '${admin_user}'@'localhost' WITH GRANT OPTION;
-- Remove anonymous users
DELETE FROM mysql.user WHERE User='';
-- Remove test database
DROP DATABASE IF EXISTS test;
DELETE FROM mysql.db WHERE Db='test' OR Db='test\\_%';
-- Remove remote root access
DELETE FROM mysql.user WHERE User='root' AND Host NOT IN ('localhost', '127.0.0.1', '::1');
-- Swap root to auth_socket for local system administration convenience & security
ALTER USER IF EXISTS 'root'@'localhost' IDENTIFIED WITH auth_socket;
ALTER USER IF EXISTS '${admin_user}'@'localhost' PASSWORD EXPIRE NEVER;
FLUSH PRIVILEGES;
EOF
# Execute SQL depending on current root authentication status
if mysql -e "SELECT 1" &>/dev/null; then
mysql < "$sql_file"
else
# Fallback if execution requires systemic root authentication pass
mysql -u root -p"${root_pass}" --batch < "$sql_file" 2>/dev/null || mysql -u root --batch < "$sql_file"
fi
rm -f "$sql_file"
# Fixed: Load timezone data natively using host zoneinfo maps safely
log "🌍 Verifying system timezone tables inside MySQL..."
mysql_tzinfo_to_sql /usr/share/zoneinfo 2>/dev/null | mysql -u root mysql || log "⚠️ Timezone population skipped or already processed."
# Store credentials securely using mysql_config_editor
local login_path="production_admin"
mysql_config_editor remove --login-path="${login_path}" 2>/dev/null || true
mysql_config_editor set --login-path="${login_path}" --host=localhost --user="${admin_user}" --password <<< "${admin_pass}"
log "βœ… Security hardening complete."
}
#-------------------------------------------------------------------------------
# PRODUCTION CONFIGURATION
#-------------------------------------------------------------------------------
configure_production() {
log "βš™οΈ Applying production configuration..."
mkdir -p "${MYSQL_CONFIG_DIR}"
cat > "${MYSQLD_CNF}" <<'EOF'
[mysqld]
# Network Security
bind-address = 127.0.0.1
mysqlx-bind-address = 127.0.0.1
skip-symbolic-links
local_infile = 0
# Authentication
default_authentication_plugin = caching_sha2_password
# Logging (for audit and debugging)
log_error = /var/log/mysql/error.log
general_log = OFF
slow_query_log = ON
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 2
# Binary logging (required for replication and point-in-time recovery)
server-id = 1
log_bin = /var/log/mysql/mysql-bin
binlog_expire_logs_seconds = 604800 # 7 days
max_binlog_size = 100M
# InnoDB Tuning (conservative defaults, adjust for your RAM)
innodb_buffer_pool_size = 256M
innodb_log_file_size = 64M
innodb_flush_log_at_trx_commit = 2
innodb_flush_method = O_DIRECT
# Connection limits
max_connections = 151
wait_timeout = 28800
interactive_timeout = 28800
# Charset
character-set-server = utf8mb4
collation-server = utf8mb4_0900_ai_ci
# Security
require_secure_transport = OFF # Set ON after SSL certs are configured
EOF
mkdir -p /var/log/mysql
chown mysql:mysql /var/log/mysql
chmod 750 /var/log/mysql
chmod 644 "${MYSQLD_CNF}"
# Restart gracefully to implement rules
systemctl restart mysql
local wait_count=0
until mysqladmin ping --silent 2>/dev/null || [[ $wait_count -ge 30 ]]; do
sleep 2
((wait_count++))
done
if ! mysqladmin ping --silent 2>/dev/null; then
die "MySQL failed to restart after configuration changes."
fi
log "βœ… Production configuration applied."
}
#-------------------------------------------------------------------------------
# SYSTEM HARDENING
#-------------------------------------------------------------------------------
harden_system() {
log "πŸ›‘οΈ Applying system-level hardening..."
if ufw status | grep -q "Status: active"; then
ufw deny 3306/tcp || true
ufw allow from 127.0.0.1 to any port 3306 proto tcp || true
log "βœ… UFW rule verified: 3306 restricted to localhost."
fi
cat > /etc/logrotate.d/mysql-custom <<'EOF'
/var/log/mysql/*.log {
daily
rotate 14
compress
missingok
notifempty
create 640 mysql adm
sharedscripts
postrotate
test -x /usr/bin/mysqladmin || exit 0
if /usr/bin/mysqladmin ping > /dev/null 2>&1; then
/usr/bin/mysqladmin flush-logs
fi
endscript
}
EOF
if [[ -f /etc/apt/apt.conf.d/50unattended-upgrades ]]; then
if ! grep -q "mysql" /etc/apt/apt.conf.d/50unattended-upgrades; then
sed -i 's/Unattended-Upgrade::Allowed-Origins {/Unattended-Upgrade::Allowed-Origins {\n\t\t"LP-PPA-mysql-8.0:${distro_codename}";\n\t\t"Ubuntu:${distro_codename}-security";/' /etc/apt/apt.conf.d/50unattended-upgrades || true
fi
fi
chmod 750 /var/lib/mysql
chown -R mysql:mysql /var/lib/mysql
log "βœ… System hardening complete."
}
#-------------------------------------------------------------------------------
# POST-INSTALL VERIFICATION
#-------------------------------------------------------------------------------
verify_installation() {
log "πŸ” Running post-install verification..."
local admin_user="$1"
systemctl is-active --quiet mysql || die "MySQL service is not active."
ss -tlnp | grep -q "127.0.0.1:3306" || die "MySQL not bound to 127.0.0.1:3306"
local root_auth
root_auth=$(mysql -u root -e "SELECT plugin FROM mysql.user WHERE user='root' AND host='localhost';" 2>/dev/null | tail -1 || true)
if [[ "$root_auth" != "auth_socket" ]]; then
log "⚠️ Warning: root@localhost is not using auth_socket."
fi
local admin_count
admin_count=$(mysql -u root -e "SELECT COUNT(*) FROM mysql.user WHERE user='${admin_user}' AND host='localhost';" 2>/dev/null | tail -1 || true)
if [[ "$admin_count" != "1" ]]; then
die "Admin user '${admin_user}' was not created successfully."
fi
local anon_count
anon_count=$(mysql -u root -e "SELECT COUNT(*) FROM mysql.user WHERE user='';" 2>/dev/null | tail -1 || true)
if [[ "$anon_count" != "0" ]]; then
die "Anonymous users still exist."
fi
log "βœ… All verification checks passed."
}
#-------------------------------------------------------------------------------
# MAIN
#-------------------------------------------------------------------------------
main() {
log "===================================================="
log " πŸ›‘οΈ Production MySQL 8.0 Installer (Idempotent)"
log "===================================================="
preflight
echo ""
log "πŸ” Configure MySQL root password..."
prompt_password MY_ROOT_PASS "Enter strong root password" "Confirm root password" 16
echo ""
log "πŸ‘€ Configure dedicated Admin User..."
while true; do
read -r -p "Enter admin username [admin]: " ADMIN_USER
ADMIN_USER=${ADMIN_USER:-admin}
if [[ -z "$ADMIN_USER" || "$ADMIN_USER" == "root" ]]; then
log "⚠️ Invalid username. Cannot be empty or 'root'."
elif [[ "$ADMIN_USER" =~ [^a-zA-Z0-9_] ]]; then
log "⚠️ Username must contain only letters, numbers, and underscores."
else
break
fi
done
prompt_password ADMIN_PASS "Enter admin password" "Confirm admin password" 16
if [[ "$MY_ROOT_PASS" == "$ADMIN_PASS" ]]; then
die "Root and admin passwords must be different."
fi
install_mysql
harden_mysql "$MY_ROOT_PASS" "$ADMIN_USER" "$ADMIN_PASS"
configure_production
harden_system
verify_installation "$ADMIN_USER"
MY_ROOT_PASS="CLEARED"
ADMIN_PASS="CLEARED"
unset MY_ROOT_PASS ADMIN_PASS
log ""
log "===================================================="
log "πŸŽ‰ MySQL Configuration Maintained/Deployed successfully!"
log ""
log "πŸ” ROOT ACCESS:"
log " sudo mysql # OS auth only"
log " (Network root auth is DISABLED)"
log ""
log "πŸ‘€ ADMIN ACCESS:"
log " mysql -u ${ADMIN_USER} -p"
log ""
log "πŸ“ LOGS:"
log " ${LOG_FILE}"
log " /var/log/mysql/error.log"
log ""
log "⚠️ NEXT STEPS:"
log " 1. Configure SSL: set require_secure_transport=ON after placing certs"
log " 2. Adjust innodb_buffer_pool_size to 70% of RAM in:"
log " ${MYSQLD_CNF}"
log " 3. Set up automated backups (e.g., Percona XtraBackup)"
log " 4. Configure monitoring (Prometheus mysqld_exporter / Datadog)"
log "===================================================="
}
main "$@"
@imannms000

Copy link
Copy Markdown
Author

// secured for documentation

@imannms000

Copy link
Copy Markdown
Author

// secured for documentation

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment