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

Connecting Securely via DBeaver

1. Local

MySQL 8.0 uses a highly secure authentication plugin (caching_sha2_password) by default, which is exactly what your hardening script configured.

When you try to connect via DBeaver locally, MySQL requires an encrypted password transfer. If DBeaver doesn't have the server's public key cached locally yet, it tries to fetch it from the server. Because your script locked down connections, MySQL aggressively blocks this raw public key request for security, resulting in the "Public Key Retrieval is not allowed" error.

Fortunately, you don't need to change anything on your server to fix this. You just need to flip one toggle inside DBeaver.

How to Fix It in DBeaver

Assuming you already created MySQL connection on DBeaver.

  1. Open DBeaver.
  2. Right-click your MySQL connection in the left-hand navigation panel and select Edit Connection.
  3. In the settings window that opens, look at the left sidebar and click on Connection settings -> Driver properties.
  4. In the main panel, you will see a long, alphabetized list of properties. Scroll down (or use the filter box at the top) to find allowPublicKeyRetrieval.
  5. Click on the value column next to it (which is currently blank or false) and change it to true.
  6. While you are there, look right above or below it for useSSL and ensure it is set to false (since your script currently has require_secure_transport = OFF until you install real SSL certificates).
  7. Click Test Connection at the bottom.

2. Remote

Because your database is locked tightly to 127.0.0.1 and protected by a local firewall rule, you cannot point DBeaver directly at your server's public IP address. Instead, you must bridge into the server securely using an SSH Tunnel.

Connection Settings Matrix

Create a new MySQL connection profile inside DBeaver and complete the following two configuration tabs:

1. Main Connection Settings Tab

Parameter Setting Context
Host localhost Crucial: Do not put your server IP here. Leave it exactly as localhost because DBeaver will evaluate this path from inside the server after tunneling.
Port 3306 Default MySQL communications port.
Database mysql The core administrative system schema.
Username admin The custom administrative user created during execution.
Password •••••••••••• The corresponding admin password.

2. SSH Connection Tab

Check the box labeled "Use SSH Tunnel" and fill out your host environment properties:

Parameter Setting
Connection Method Key Pair (Recommended) or Password
Host/IP Your server's actual public-facing IP address.
Port 22 (Or your custom SSH server port).
Username Your server's SSH login user (e.g., root or ubuntu).
Passphrase / Password Your system account SSH passphrase or password.

Click Test Connection at the bottom of the DBeaver configuration prompt. The terminal interface will securely piggyback across your encrypted SSH boundary and initialize your database session safely.

@imannms000

Copy link
Copy Markdown
Author

// secured for documentation

@imannms000

Copy link
Copy Markdown
Author

// secured for documentation

@imannms000

Copy link
Copy Markdown
Author

// secured for documentation

@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