
{"id":57,"date":"2025-05-30T22:40:51","date_gmt":"2025-05-30T21:40:51","guid":{"rendered":"http:\/\/blog.mkcloudai.com\/?p=57"},"modified":"2025-05-30T22:40:51","modified_gmt":"2025-05-30T21:40:51","slug":"production-ready-lightsail-launch-script-idempotent-secure-and-fully-automated-for-wordpress-on-apache-with-backups-ssl-monitoring-and-more","status":"publish","type":"post","link":"https:\/\/blog.mkcloudai.com\/?p=57","title":{"rendered":"Production-ready Lightsail launch script (idempotent, secure, and fully automated for WordPress on Apache with backups, SSL, monitoring, and more)."},"content":{"rendered":"\n<h3 class=\"wp-block-heading\">\u2705 <code>lightsail-wordpress-setup.sh<\/code><\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>#!\/bin\/bash<br>set -euo pipefail<br>LOG_FILE=\"\/var\/log\/wordpress-install.log\"<br>exec > >(tee -a \"$LOG_FILE\") 2>&amp;1<br># --------------------------<br># 1. Basic Updates &amp; Tools<br># --------------------------<br>echo \"[+] Updating system...\"<br>apt update -y &amp;&amp; apt upgrade -y<br>apt install -y curl wget unzip software-properties-common gnupg2 ca-certificates lsb-release ufw<br># --------------------------<br># 2. Apache, PHP, and MariaDB<br># --------------------------<br>echo \"[+] Installing Apache, PHP, and MariaDB...\"<br>if ! command -v apache2 > \/dev\/null; then<br>  apt install -y apache2<br>  systemctl enable apache2 &amp;&amp; systemctl start apache2<br>fi<br>apt install -y php php-mysql libapache2-mod-php php-cli php-curl php-xml php-mbstring php-zip php-gd php-soap<br><br>if ! command -v mysql > \/dev\/null; then<br>  apt install -y mariadb-server<br>  systemctl enable mariadb &amp;&amp; systemctl start mariadb<br>fi<br># --------------------------<br># 3. Database Setup<br># --------------------------<br>DB_NAME=\"wordpress\"<br>DB_USER=\"wpuser\"<br>DB_PASS=\"StrongPass123\"<br>echo \"[+] Setting up database...\"<br>mysql -e \"CREATE DATABASE IF NOT EXISTS $DB_NAME;\"<br>mysql -e \"CREATE USER IF NOT EXISTS '$DB_USER'@'localhost' IDENTIFIED BY '$DB_PASS';\"<br>mysql -e \"GRANT ALL PRIVILEGES ON $DB_NAME.* TO '$DB_USER'@'localhost'; FLUSH PRIVILEGES;\"<br># --------------------------<br># 4. WordPress Install (via WP-CLI)<br># --------------------------<br>WP_CLI=\"\/usr\/local\/bin\/wp\"<br>WEB_ROOT=\"\/var\/www\/html\"<br><br>if [ ! -f \"$WP_CLI\" ]; then<br>  echo \"[+] Installing WP-CLI...\"<br>  curl -O https:\/\/raw.githubusercontent.com\/wp-cli\/builds\/gh-pages\/phar\/wp-cli.phar<br>  chmod +x wp-cli.phar &amp;&amp; mv wp-cli.phar \"$WP_CLI\"<br>fi<br><br>if [ ! -f \"$WEB_ROOT\/index.php\" ]; then<br>  echo \"[+] Installing WordPress...\"<br>  cd \/tmp<br>  $WP_CLI core download --path=wordpress<br>  cp -r wordpress\/* $WEB_ROOT\/<br>  cd $WEB_ROOT<br>  $WP_CLI config create --dbname=$DB_NAME --dbuser=$DB_USER --dbpass=$DB_PASS --skip-check<br>  chown -R www-data:www-data $WEB_ROOT<br>  chmod -R 755 $WEB_ROOT<br>fi<br><br># --------------------------<br># 5. SSL via Let's Encrypt (Certbot)<br># --------------------------<br>DOMAIN_NAME=\"yourdomain.com\"  # CHANGE THIS<br>EMAIL=\"you@example.com\"       # CHANGE THIS<br><br>if ! command -v certbot > \/dev\/null; then<br>  echo \"[+] Installing Certbot...\"<br>  apt install -y certbot python3-certbot-apache<br>fi<br><br>echo \"[+] Installing SSL certificate...\"<br>certbot --apache --non-interactive --agree-tos --redirect -m $EMAIL -d $DOMAIN_NAME || true<br><br># --------------------------<br># 6. Fail2Ban Setup<br># --------------------------<br>echo \"[+] Installing Fail2Ban...\"<br>apt install -y fail2ban<br>systemctl enable fail2ban &amp;&amp; systemctl start fail2ban<br><br># --------------------------<br># 7. SSH Hardening<br># --------------------------<br>echo \"[+] Hardening SSH...\"<br>SSH_CONF=\"\/etc\/ssh\/sshd_config\"<br>sed -i 's\/^#Port 22\/Port 2222\/' $SSH_CONF<br>sed -i 's\/^PermitRootLogin yes\/PermitRootLogin no\/' $SSH_CONF<br>systemctl restart sshd<br><br># --------------------------<br># 8. Swap File Setup<br># --------------------------<br>if [ ! -f \/swapfile ]; then<br>  echo \"[+] Creating 1GB swap file...\"<br>  fallocate -l 1G \/swapfile<br>  chmod 600 \/swapfile<br>  mkswap \/swapfile<br>  swapon \/swapfile<br>  echo '\/swapfile none swap sw 0 0' | tee -a \/etc\/fstab<br>fi<br><br># --------------------------<br># 9. Logrotate<br># --------------------------<br>echo \"[+] Configuring logrotate for WordPress...\"<br>cat &lt;&lt;EOF > \/etc\/logrotate.d\/wordpress<br>$WEB_ROOT\/*.log {<br>    daily<br>    missingok<br>    rotate 14<br>    compress<br>    delaycompress<br>    notifempty<br>    create 0640 www-data adm<br>    sharedscripts<br>    postrotate<br>        systemctl reload apache2 > \/dev\/null<br>    endscript<br>}<br>EOF<br><br># --------------------------<br># 10. CloudWatch Agent (optional)<br># --------------------------<br>echo \"[+] Installing AWS CloudWatch Agent...\"<br>curl -O https:\/\/s3.amazonaws.com\/amazoncloudwatch-agent\/ubuntu\/amd64\/latest\/amazon-cloudwatch-agent.deb<br>dpkg -i amazon-cloudwatch-agent.deb<br>rm amazon-cloudwatch-agent.deb<br><br># (Optional) add a CloudWatch agent config JSON here if you want automatic metrics<br><br># --------------------------<br># 11. Backups (Daily via Cron)<br># --------------------------<br>echo \"[+] Setting up backups...\"<br>BACKUP_SCRIPT=\"\/usr\/local\/bin\/wp-backup.sh\"<br><br>cat &lt;&lt;EOF > $BACKUP_SCRIPT<br>#!\/bin\/bash<br>DATE=\\$(date +%F)<br>BACKUP_DIR=\"\/root\/wp-backups\"<br>mkdir -p \\$BACKUP_DIR<br><br># Backup database<br>mysqldump -u $DB_USER -p$DB_PASS $DB_NAME > \\$BACKUP_DIR\/db-\\$DATE.sql<br><br># Backup files<br>tar -czf \\$BACKUP_DIR\/wp-files-\\$DATE.tar.gz $WEB_ROOT<br><br># Keep only last 7 backups<br>find \\$BACKUP_DIR -type f -mtime +7 -delete<br>EOF<br><br>chmod +x $BACKUP_SCRIPT<br>(crontab -l ; echo \"0 3 * * * $BACKUP_SCRIPT\") | crontab -<br><br># --------------------------<br># 12. Create Secure User<br># --------------------------<br>if ! id \"wpadmin\" &amp;>\/dev\/null; then<br>  echo \"[+] Creating wpadmin user...\"<br>  useradd -m -s \/bin\/bash wpadmin<br>  echo \"wpadmin:WPsecure@2025\" | chpasswd<br>  usermod -aG sudo wpadmin<br>fi<br><br># --------------------------<br># Done!<br># --------------------------<br>echo \"[\u2713] WordPress setup complete. Please point your domain ($DOMAIN_NAME) to this instance and test.\"<br><\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">\ud83d\udee0\ufe0f To Use This Script<\/h3>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Replace the following placeholders<\/strong>:\n<ul class=\"wp-block-list\">\n<li><code>yourdomain.com<\/code> \u2192 your actual domain<\/li>\n\n\n\n<li><code>you@example.com<\/code> \u2192 your email for Let&#8217;s Encrypt<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>Paste it in the Lightsail &#8220;launch script&#8221; box<\/strong> when creating your Ubuntu instance.<\/li>\n<\/ol>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">\ud83d\udd12 <strong>1. What happens if you disable root login?<\/strong><\/h3>\n\n\n\n<p>The line in the script:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>sed -i 's\/^PermitRootLogin yes\/PermitRootLogin no\/' \/etc\/ssh\/sshd_config<br><\/code><\/pre>\n\n\n\n<p>will disable direct <code>root<\/code> login over SSH. This is a common security best practice to reduce brute-force attacks on the root account.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">\ud83e\uddd1\u200d\ud83d\udcbb <strong>2. How do you log in if root is blocked?<\/strong><\/h3>\n\n\n\n<p>The script <strong>creates a new admin user<\/strong>:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>useradd -m -s \/bin\/bash wpadmin<br>echo \"wpadmin:WPsecure@2025\" | chpasswd<br>usermod -aG sudo wpadmin<br><\/code><\/pre>\n\n\n\n<p>This means:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>You log in using: <code>ssh wpadmin@your-ip -p 2222<\/code> (since port is changed from 22 \u2192 2222)<\/li>\n\n\n\n<li>The <code>wpadmin<\/code> user <strong>has sudo privileges<\/strong>, so you can run any root command with <code>sudo<\/code>.<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">\ud83d\udee1\ufe0f <strong>3. What SSH hardening changes are made?<\/strong><\/h3>\n\n\n\n<p>These two changes improve SSH security:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Change<\/th><th>Line<\/th><th>Effect<\/th><\/tr><\/thead><tbody><tr><td>\ud83d\udd10 Disable root login<\/td><td><code>PermitRootLogin no<\/code><\/td><td>Prevents attackers from brute-forcing the <code>root<\/code> account<\/td><\/tr><tr><td>\ud83d\udce6 Change default port<\/td><td><code>Port 2222<\/code><\/td><td>Reduces bot attacks on default port <code>22<\/code> (obscurity + security)<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p>The line:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>sed -i 's\/^#Port 22\/Port 2222\/' \/etc\/ssh\/sshd_config<br><\/code><\/pre>\n\n\n\n<p>&#8230;uncomments and changes the SSH port from 22 to 2222.<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p><strong>\u26a0\ufe0f Important:<\/strong> After creating the instance, remember to allow port <code>2222<\/code> in the <strong>Lightsail firewall rules<\/strong>, or you won\u2019t be able to connect.<\/p>\n<\/blockquote>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">1\ufe0f\u20e3 <strong>Add Public Key Authentication Instead of Password<\/strong><\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Create <code>wpadmin<\/code> user <strong>without password<\/strong> (disabled password login for that user).<\/li>\n\n\n\n<li>Copy your SSH public key into <code>\/home\/wpadmin\/.ssh\/authorized_keys<\/code>.<\/li>\n\n\n\n<li>Disable password authentication for SSH entirely (force key-based login).<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">2\ufe0f\u20e3 <strong>Add Fallback Check to Root Login<\/strong><\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>We&#8217;ll keep root login <strong>disabled by default<\/strong> for security.<\/li>\n\n\n\n<li>But in case the <code>wpadmin<\/code> user cannot log in, <strong>root login on port 2222 remains possible via SSH key only<\/strong>.<\/li>\n\n\n\n<li>Password authentication is disabled globally (no password logins at all).<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">3\ufe0f\u20e3 <strong>Write First Boot Instructions to MOTD<\/strong><\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Show clear login instructions on first login via <code>\/etc\/motd<\/code> message.<\/li>\n\n\n\n<li>Include the username, SSH port, and a reminder to add your SSH key.<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p>Add this to the script or replace relevant parts:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>#!\/bin\/bash<br>set -euo pipefail<br># Your public SSH key here (replace with your actual public key)<br>PUB_KEY=\"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQD... your-email@example.com\"<br><br># 1. Create user without password &amp; set up SSH key<br>if ! id \"wpadmin\" &amp;>\/dev\/null; then<br>  echo \"[+] Creating wpadmin user with SSH key auth only...\"<br>  useradd -m -s \/bin\/bash wpadmin<br>  mkdir -p \/home\/wpadmin\/.ssh<br>  echo \"$PUB_KEY\" > \/home\/wpadmin\/.ssh\/authorized_keys<br>  chmod 700 \/home\/wpadmin\/.ssh<br>  chmod 600 \/home\/wpadmin\/.ssh\/authorized_keys<br>  chown -R wpadmin:wpadmin \/home\/wpadmin\/.ssh<br>  usermod -aG sudo wpadmin<br>fi<br><br># 2. SSH Hardening<br><br>SSH_CONF=\"\/etc\/ssh\/sshd_config\"<br><br># Change SSH port to 2222<br>sed -i 's\/^#Port 22\/Port 2222\/' $SSH_CONF<br><br># Disable root password login but allow root key login<br># This config means:<br># - PasswordAuthentication is disabled globally (all users)<br># - PermitRootLogin is set to \"prohibit-password\" to allow root only via SSH keys<br>sed -i 's\/^#PermitRootLogin yes\/PermitRootLogin prohibit-password\/' $SSH_CONF<br>sed -i 's\/^#PasswordAuthentication yes\/PasswordAuthentication no\/' $SSH_CONF<br><br># Restart SSH to apply changes<br>systemctl restart sshd<br><br># 3. Write first-boot instructions to \/etc\/motd<br>cat &lt;&lt;EOF > \/etc\/motd<br>*******************************************************<br>* Welcome to your AWS Lightsail Ubuntu Server!         *<br>*                                                      *<br>* Login using SSH key authentication:                  *<br>*  ssh -i \/path\/to\/private_key -p 2222 wpadmin@&lt;IP>    *<br>*                                                      *<br>* Root login is disabled for password but allowed via  *<br>* key on port 2222 if needed.                           *<br>*                                                      *<br>* Remember to replace \/path\/to\/private_key with your   *<br>* actual SSH private key path.                          *<br>*                                                      *<br>* Enjoy your secure server!                             *<br>*******************************************************<br>EOF<br><\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">\u26a0\ufe0f What You Need to Do Before Running:<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Replace the placeholder <code>PUB_KEY<\/code> string with <strong>your actual SSH public key<\/strong> (from your laptop or wherever you SSH from, e.g., <code>~\/.ssh\/id_rsa.pub<\/code>).<\/li>\n\n\n\n<li>Make sure your SSH private key matches this public key.<\/li>\n\n\n\n<li>Allow inbound TCP port <strong>2222<\/strong> in Lightsail firewall settings.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">\ud83d\udd11 How to Connect After Setup<\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>ssh -i \/path\/to\/private_key -p 2222 wpadmin@&lt;instance-ip><br><\/code><\/pre>\n\n\n\n<p>If something goes wrong with <code>wpadmin<\/code> login, you can also try:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>ssh -i \/path\/to\/private_key -p 2222 root@&lt;instance-ip><br><\/code><\/pre>\n\n\n\n<p>(because root login is allowed with SSH keys, but password login is disabled for root)<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">How to get your public SSH key<\/h3>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h4 class=\"wp-block-heading\">1. <strong>If you already have SSH keys on your local machine<\/strong><\/h4>\n\n\n\n<p><strong>Linux\/macOS:<\/strong><\/p>\n\n\n\n<p>Open a terminal and run:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>cat ~\/.ssh\/id_rsa.pub<br><\/code><\/pre>\n\n\n\n<p>or if you use ed25519 keys:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>cat ~\/.ssh\/id_ed25519.pub<br><\/code><\/pre>\n\n\n\n<p>This will print your public key (starts with <code>ssh-rsa<\/code> or <code>ssh-ed25519<\/code>). Copy the entire output (all one line).<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p><strong>Windows (using PowerShell or WSL):<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>If you use <strong>WSL<\/strong> or <strong>Git Bash<\/strong>, same command as Linux\/macOS.<\/li>\n\n\n\n<li>If you use <strong>PuTTY<\/strong>, you can export the public key from PuTTYgen.<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h4 class=\"wp-block-heading\">2. <strong>If you don\u2019t have SSH keys yet<\/strong><\/h4>\n\n\n\n<p>Generate one with:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>ssh-keygen -t rsa -b 4096 -C \"your-email@example.com\"<br><\/code><\/pre>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Press Enter to accept default file location.<\/li>\n\n\n\n<li>Enter a passphrase if you want (recommended).<\/li>\n\n\n\n<li>Then run:<\/li>\n<\/ul>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>cat ~\/.ssh\/id_rsa.pub<br><\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h4 class=\"wp-block-heading\">3. <strong>Use that key in your script<\/strong><\/h4>\n\n\n\n<p>Example:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>PUB_KEY=\"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCy3XyRyx... your-email@example.com\"<br><\/code><\/pre>\n\n\n\n<p>Paste the full key <strong>in one line<\/strong>, no line breaks.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">1. <strong>Shebang and strict mode<\/strong><\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>#!\/bin\/bash<br>set -euo pipefail<br><\/code><\/pre>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>#!\/bin\/bash<\/code> tells the system to run the script with Bash.<\/li>\n\n\n\n<li><code>set -euo pipefail<\/code>:\n<ul class=\"wp-block-list\">\n<li><code>-e<\/code>: Exit immediately if any command fails.<\/li>\n\n\n\n<li><code>-u<\/code>: Treat unset variables as errors.<\/li>\n\n\n\n<li><code>-o pipefail<\/code>: Fail if any command in a pipeline fails.<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n\n<p>This makes your script safer and more robust.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">2. <strong>User-configurable variables<\/strong><\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>PUB_KEY=\"ssh-rsa AAAAB3N...your-email@example.com\"<br>WP_ADMIN_USER=\"wpadmin\"<br>WP_ADMIN_PASS=\"WPsecure@2025\"<br>SSH_PORT=2222<br>DOMAIN=\"your-domain.com\"<br><\/code><\/pre>\n\n\n\n<p>These are placeholders you <strong>must edit<\/strong> before running the script:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>PUB_KEY<\/code>: Your public SSH key to enable key-based login.<\/li>\n\n\n\n<li><code>WP_ADMIN_USER<\/code>: Name of the admin user created on the system.<\/li>\n\n\n\n<li><code>WP_ADMIN_PASS<\/code>: Password used for MySQL WordPress user (not system user).<\/li>\n\n\n\n<li><code>SSH_PORT<\/code>: Custom SSH port to harden SSH access.<\/li>\n\n\n\n<li><code>DOMAIN<\/code>: Your website domain for SSL certificate.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">3. <strong>Update packages and install dependencies<\/strong><\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>apt update &amp;&amp; apt upgrade -y<br>apt install -y apache2 mysql-server php php-mysql ... certbot fail2ban logrotate awscli unattended-upgrades<br><\/code><\/pre>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Updates system package info.<\/li>\n\n\n\n<li>Installs all required software:\n<ul class=\"wp-block-list\">\n<li>Apache web server<\/li>\n\n\n\n<li>MySQL database server<\/li>\n\n\n\n<li>PHP and extensions needed for WordPress<\/li>\n\n\n\n<li>Certbot for SSL certs<\/li>\n\n\n\n<li>Fail2Ban to protect SSH<\/li>\n\n\n\n<li>Logrotate for logs management<\/li>\n\n\n\n<li>AWS CLI &amp; unattended-upgrades for monitoring and auto updates<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">4. <strong>Swap file creation<\/strong><\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>if ! swapon --show | grep -q 'swapfile'; then<br>  fallocate -l 1G \/swapfile<br>  chmod 600 \/swapfile<br>  mkswap \/swapfile<br>  swapon \/swapfile<br>  echo '\/swapfile none swap sw 0 0' >> \/etc\/fstab<br>fi<br><\/code><\/pre>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Checks if swap exists; if not, creates a 1GB swap file.<\/li>\n\n\n\n<li>Swap helps low-RAM instances avoid memory issues.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">5. <strong>Create admin user with SSH key<\/strong><\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>if ! id \"$WP_ADMIN_USER\" &amp;>\/dev\/null; then<br>  useradd -m -s \/bin\/bash \"$WP_ADMIN_USER\"<br>  mkdir -p \/home\/$WP_ADMIN_USER\/.ssh<br>  echo \"$PUB_KEY\" > \/home\/$WP_ADMIN_USER\/.ssh\/authorized_keys<br>  chmod 700 \/home\/$WP_ADMIN_USER\/.ssh<br>  chmod 600 \/home\/$WP_ADMIN_USER\/.ssh\/authorized_keys<br>  chown -R $WP_ADMIN_USER:$WP_ADMIN_USER \/home\/$WP_ADMIN_USER\/.ssh<br>  usermod -aG sudo $WP_ADMIN_USER<br>fi<br><\/code><\/pre>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Creates a system user (if it doesn&#8217;t exist).<\/li>\n\n\n\n<li>Sets up <code>.ssh\/authorized_keys<\/code> with your public key (no password login).<\/li>\n\n\n\n<li>Adds user to <code>sudo<\/code> group for admin privileges.<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">6. <strong>SSH hardening<\/strong><\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>sed -i \"s\/^#Port 22\/Port $SSH_PORT\/\" \/etc\/ssh\/sshd_config<br>sed -i \"s\/^#PermitRootLogin yes\/PermitRootLogin prohibit-password\/\" \/etc\/ssh\/sshd_config<br>sed -i \"s\/^#PasswordAuthentication yes\/PasswordAuthentication no\/\" \/etc\/ssh\/sshd_config<br>systemctl restart sshd<br><\/code><\/pre>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Changes SSH port from 22 to your custom port.<\/li>\n\n\n\n<li>Disables root login with password (root can still login with keys).<\/li>\n\n\n\n<li>Disables password authentication entirely \u2014 only SSH keys allowed.<\/li>\n\n\n\n<li>Restarts SSH service to apply changes.<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">7. <strong>UFW firewall setup<\/strong><\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>ufw allow $SSH_PORT\/tcp<br>ufw allow 'Apache Full'<br>ufw --force enable<br><\/code><\/pre>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Allows traffic on SSH custom port.<\/li>\n\n\n\n<li>Allows HTTP and HTTPS for Apache.<\/li>\n\n\n\n<li>Enables UFW firewall.<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">8. <strong>MySQL secure installation and database setup<\/strong><\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>mysql_secure_installation &lt;&lt;EOF<br><br>y<br>WPsecure@2025<br>WPsecure@2025<br>y<br>y<br>y<br>y<br>EOF<br><br>mysql -u root -pWPsecure@2025 &lt;&lt;EOF<br>CREATE DATABASE wordpress DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;<br>CREATE USER 'wpuser'@'localhost' IDENTIFIED BY '$WP_ADMIN_PASS';<br>GRANT ALL ON wordpress.* TO 'wpuser'@'localhost';<br>FLUSH PRIVILEGES;<br>EOF<br><\/code><\/pre>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Runs MySQL secure installation with default answers (sets root password, removes anonymous users, etc.).<\/li>\n\n\n\n<li>Creates WordPress database and user with the password.<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">9. <strong>Download and configure WordPress<\/strong><\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>wget https:\/\/wordpress.org\/latest.tar.gz -O \/tmp\/wordpress.tar.gz<br>tar xzf \/tmp\/wordpress.tar.gz -C \/tmp\/<br>rsync -a \/tmp\/wordpress\/ \/var\/www\/html\/<br>chown -R www-data:www-data \/var\/www\/html<br>chmod -R 755 \/var\/www\/html<br><\/code><\/pre>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Downloads WordPress, extracts, and copies to Apache web root.<\/li>\n\n\n\n<li>Sets proper permissions.<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">10. <strong>Create WordPress config file<\/strong><\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>cat > \/var\/www\/html\/wp-config.php &lt;&lt;EOF<br>&lt;?php<br>\/\/ DB connection and security keys here...<br>EOF<br><\/code><\/pre>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Sets up <code>wp-config.php<\/code> with DB credentials and placeholders for secret keys.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Updated snippet to fetch WordPress salts dynamically in your script<\/h3>\n\n\n\n<p>Instead of hardcoding keys like:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">phpCopyEdit<code>define('AUTH_KEY',         'put your unique phrase here');\ndefine('SECURE_AUTH_KEY',  'put your unique phrase here');\n...\n<\/code><\/pre>\n\n\n\n<p>Use this inside your bash script <strong>when creating <code>wp-config.php<\/code><\/strong>:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"><code># Fetch fresh WordPress salts from official API<br>WP_KEYS=$(curl -s https:\/\/api.wordpress.org\/secret-key\/1.1\/salt\/)<br><br>cat > \/var\/www\/html\/wp-config.php &lt;&lt;EOF<br>&lt;?php<br>define('DB_NAME', '${DB_NAME}');<br>define('DB_USER', '${DB_USER}');<br>define('DB_PASSWORD', '${DB_PASSWORD}');<br>define('DB_HOST', 'localhost');<br>define('DB_CHARSET', 'utf8mb4');<br>define('DB_COLLATE', '');<br><br>${WP_KEYS}<br><br>\\$table_prefix = 'wp_';<br>define('WP_DEBUG', false);<br><br>if ( !defined('ABSPATH') )<br>    define('ABSPATH', dirname(__FILE__) . '\/');<br><br>require_once(ABSPATH . 'wp-settings.php');<br>EOF<br><\/code><\/pre>\n\n\n\n<p>This way, every time the script runs, your <code>wp-config.php<\/code> will have <strong>strong unique keys automatically<\/strong>.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">11. <strong>Install SSL with Let&#8217;s Encrypt<\/strong><\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>certbot --apache --non-interactive --agree-tos --redirect -m admin@$DOMAIN -d $DOMAIN<br><\/code><\/pre>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Obtains and installs free SSL cert.<\/li>\n\n\n\n<li>Enables HTTPS and automatic redirect from HTTP.<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">12. <strong>Configure Fail2Ban<\/strong><\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>cat > \/etc\/fail2ban\/jail.local &lt;&lt;EOF<\/code><\/pre>\n\n\n<p>[sshd]<\/p>\n\n\n\n<p>enabled = true port = $SSH_PORT filter = sshd logpath = \/var\/log\/auth.log maxretry = 5 EOF systemctl restart fail2ban<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Protects SSH from brute force attacks by banning after 5 failed attempts.<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">13. <strong>Logrotate for Apache logs<\/strong><\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>cat > \/etc\/logrotate.d\/wordpress &lt;&lt;EOF<br>\/var\/log\/apache2\/*.log {<br>    daily<br>    rotate 14<br>    compress<br>    ...<br>}<br>EOF<br><\/code><\/pre>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Automatically rotates and compresses Apache logs daily, keeping 14 days.<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">14. <strong>Install WP-CLI<\/strong><\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>curl -O https:\/\/raw.githubusercontent.com\/wp-cli\/builds\/gh-pages\/phar\/wp-cli.phar<br>chmod +x wp-cli.phar<br>mv wp-cli.phar \/usr\/local\/bin\/wp<br><\/code><\/pre>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Installs WordPress CLI tool to manage WordPress from the command line.<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">15. <strong>Backup script &amp; cron job<\/strong><\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>cat > \/usr\/local\/bin\/backup_wordpress.sh &lt;&lt;EOF<br>#!\/bin\/bash<br>DATE=\\$(date +'%Y%m%d_%H%M%S')<br>tar czf \/var\/backups\/wordpress\/wp_files_\\$DATE.tar.gz \/var\/www\/html<br>mysqldump -u wpuser -p$WP_ADMIN_PASS wordpress > \/var\/backups\/wordpress\/wp_db_\\$DATE.sql<br>find \/var\/backups\/wordpress -type f -mtime +7 -delete<br>EOF<br>chmod +x \/usr\/local\/bin\/backup_wordpress.sh<br>(crontab -l -u $WP_ADMIN_USER 2>\/dev\/null; echo \"0 2 * * * \/usr\/local\/bin\/backup_wordpress.sh\") | crontab -u $WP_ADMIN_USER -<br><\/code><\/pre>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Creates daily backups of WordPress files and DB at 2 AM.<\/li>\n\n\n\n<li>Keeps backups for 7 days only.<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">16. <strong>CloudWatch Agent installation<\/strong><\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>wget https:\/\/s3.eu-west-2.amazonaws.com\/amazoncloudwatch-agent-eu-west-2\/ubuntu\/amd64\/latest\/amazon-cloudwatch-agent.deb<br>dpkg -i -E .\/amazon-cloudwatch-agent.deb<br>cat > \/opt\/aws\/amazon-cloudwatch-agent\/etc\/amazon-cloudwatch-agent.json &lt;&lt;EOF<br>{<br>  \"metrics\": {<br>    ...<br>  }<br>}<br>EOF<br>amazon-cloudwatch-agent-ctl -a fetch-config -m ec2 -c file:\/opt\/aws\/amazon-cloudwatch-agent\/etc\/amazon-cloudwatch-agent.json -s<br><\/code><\/pre>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Installs AWS CloudWatch Agent to send CPU, memory, disk metrics to CloudWatch.<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">17. <strong>Login instructions in MOTD<\/strong><\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>cat &lt;&lt;EOF > \/etc\/motd<br>*******************************************************<br>* Welcome! SSH to $WP_ADMIN_USER user on port $SSH_PORT *<br>* Use your SSH key for authentication.                 *<br>*******************************************************<br>EOF<br><\/code><\/pre>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Displays helpful login instructions when you SSH in.<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">Summary<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>The script automates <strong>full server setup<\/strong> for a secure, production-ready WordPress with SSL and backups.<\/li>\n\n\n\n<li>It configures SSH to be secure (keys only, custom port, no root password).<\/li>\n\n\n\n<li>It installs and secures MySQL, Apache, WordPress.<\/li>\n\n\n\n<li>It configures Fail2Ban and logrotate.<\/li>\n\n\n\n<li>It sets up monitoring and backups.<\/li>\n<\/ul>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Phase<\/th><th>User to SSH as<\/th><th>Port<\/th><th>Authentication Method<\/th><\/tr><\/thead><tbody><tr><td>Before script runs<\/td><td><code>ubuntu<\/code><\/td><td>22 (default)<\/td><td>SSH key (your Lightsail key)<\/td><\/tr><tr><td>After script completes<\/td><td>New user (e.g., <code>wpadmin<\/code>)<\/td><td>Custom port (e.g., 2222)<\/td><td>SSH key (the key you added in script)<\/td><\/tr><tr><td>Root login<\/td><td>discouraged (disabled password login)<\/td><td>custom port<\/td><td>SSH key (if enabled)<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>\u2705 lightsail-wordpress-setup.sh #!\/bin\/bashset -euo pipefailLOG_FILE=&#8221;\/var\/log\/wordpress-install.log&#8221;exec > >(tee -a &#8220;$LOG_FILE&#8221;) 2>&amp;1# &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8211;# 1. Basic Updates &amp; Tools# &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8211;echo &#8220;[+] Updating system&#8230;&#8221;apt [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"site-sidebar-layout":"default","site-content-layout":"","ast-site-content-layout":"normal-width-container","site-content-style":"boxed","site-sidebar-style":"default","ast-global-header-display":"","ast-banner-title-visibility":"","ast-main-header-display":"","ast-hfb-above-header-display":"","ast-hfb-below-header-display":"","ast-hfb-mobile-header-display":"","site-post-title":"","ast-breadcrumbs-content":"","ast-featured-img":"","footer-sml-layout":"","theme-transparent-header-meta":"","adv-header-id-meta":"","stick-header-meta":"","header-above-stick-meta":"","header-main-stick-meta":"","header-below-stick-meta":"","astra-migrate-meta-layouts":"default","ast-page-background-enabled":"default","ast-page-background-meta":{"desktop":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"ast-content-background-meta":{"desktop":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"footnotes":""},"categories":[5,4,7],"tags":[19,16,17,21,20,18],"class_list":["post-57","post","type-post","status-publish","format-standard","hentry","category-aws","category-linux","category-project","tag-apache","tag-aws-lightsail","tag-linux","tag-shell-scripting","tag-ssh","tag-ubuntu"],"_links":{"self":[{"href":"https:\/\/blog.mkcloudai.com\/index.php?rest_route=\/wp\/v2\/posts\/57","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blog.mkcloudai.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.mkcloudai.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.mkcloudai.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.mkcloudai.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=57"}],"version-history":[{"count":1,"href":"https:\/\/blog.mkcloudai.com\/index.php?rest_route=\/wp\/v2\/posts\/57\/revisions"}],"predecessor-version":[{"id":58,"href":"https:\/\/blog.mkcloudai.com\/index.php?rest_route=\/wp\/v2\/posts\/57\/revisions\/58"}],"wp:attachment":[{"href":"https:\/\/blog.mkcloudai.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=57"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.mkcloudai.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=57"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.mkcloudai.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=57"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}