peteris.rocks

Unattended installation of WordPress on Ubuntu Server

Unattended installation of WordPress on Ubuntu Server 16.04

Last updated on

We are going to set up WordPress and its dependencies on a fresh installation of Ubuntu Server 16.04.

You can optionally convert your WordPress installation to a network.

Just copy & paste these commands in the terminal. Or create a script. It should take less than 5 minutes if you have a decent internet connection.

This tutorial was written for WordPress 4.5.2 and last tested with WordPress 4.6.

Variables

Change WP_DOMAIN to your site domain. You should also choose a strong password WP_ADMIN_PASSWORD for the admin user.

If you want to, you can customize the other passwords. But you don't have to. Apart from WP_ADMIN_PASSWORD, no one will be able to use these passwords unless they have access to the server.

WP_DOMAIN="wordpress.peteris.rocks"
WP_ADMIN_USERNAME="admin"
WP_ADMIN_PASSWORD="admin"
WP_ADMIN_EMAIL="[email protected]"
WP_DB_NAME="wordpress"
WP_DB_USERNAME="wordpress"
WP_DB_PASSWORD="wordpress"
WP_PATH="/var/www/wordpress"
MYSQL_ROOT_PASSWORD="root"

Don't care about database passwords? No problem.

sudo apt install pwgen
WP_DB_PASSWORD="$(pwgen -1 -s 64)"
MYSQL_ROOT_PASSWORD="$(pwgen -1 -s 64)"

Install software

We are going to install nginx, PHP and MySQL.

By default, mysql-server is going to ask for the root password and we automate that with debconf-set-selections.

echo "mysql-server-5.7 mysql-server/root_password password $MYSQL_ROOT_PASSWORD" | sudo debconf-set-selections
echo "mysql-server-5.7 mysql-server/root_password_again password $MYSQL_ROOT_PASSWORD" | sudo debconf-set-selections
sudo apt install -y nginx php php-mysql php-curl php-gd mysql-server

Configure MySQL

We are going to create a user and a database for WordPress. This database user will have full access to that database.

mysql -u root -p$MYSQL_ROOT_PASSWORD <<EOF
CREATE USER '$WP_DB_USERNAME'@'localhost' IDENTIFIED BY '$WP_DB_PASSWORD';
CREATE DATABASE $WP_DB_NAME;
GRANT ALL ON $WP_DB_NAME.* TO '$WP_DB_USERNAME'@'localhost';
EOF

Ignore the warning that it's insecure to use passwords in the command line.

Configure nginx

We are going to stick to the convention used by Ubuntu and create a new configuration file for the website at /etc/nginx/sites-available/domain.com and symlink it as /etc/nginx/sites-enabled/domain.com.

sudo mkdir -p $WP_PATH/public $WP_PATH/logs
sudo tee /etc/nginx/sites-available/$WP_DOMAIN <<EOF
server {
  listen 80;
  server_name $WP_DOMAIN www.$WP_DOMAIN;

  root $WP_PATH/public;
  index index.php;

  access_log $WP_PATH/logs/access.log;
  error_log $WP_PATH/logs/error.log;

  location / {
    try_files \$uri \$uri/ /index.php?\$args;
  }

  location ~ \.php\$ {
    include snippets/fastcgi-php.conf;
    fastcgi_pass unix:/run/php/php7.0-fpm.sock;
  }
}
EOF
sudo ln -s /etc/nginx/sites-available/$WP_DOMAIN /etc/nginx/sites-enabled/$WP_DOMAIN
sudo systemctl restart nginx

SSL

This is optional.

Install LetsEncrypt.

sudo apt install -y letsencrypt

Get a certificate for $WP_DOMAIN and www.$WP_DOMAIN.

You can also add m.$WP_DOMAIN if you wish.

sudo mkdir -p $WP_PATH
sudo letsencrypt certonly -n --agree-tos --webroot -w $WP_PATH -d $WP_DOMAIN -d www.$WP_DOMAIN -m $WP_ADMIN_EMAIL
sudo openssl dhparam -out /etc/letsencrypt/live/$WP_DOMAIN/dhparam.pem 2048

Change our nginx configuration. This will give you an A+ on the SSL Server Test.

sudo tee /etc/nginx/sites-available/$WP_DOMAIN <<EOF
server {
  listen 80;
  server_name $WP_DOMAIN www.$WP_DOMAIN;
  return 301 https://\$server_name\$request_uri;
}

server {
  listen 443 ssl http2;
  server_name $WP_DOMAIN www.$WP_DOMAIN;

  root $WP_PATH/public;
  index index.php;

  access_log $WP_PATH/logs/access.log;
  error_log $WP_PATH/logs/error.log;

  ssl_certificate           /etc/letsencrypt/live/$WP_DOMAIN/fullchain.pem;
  ssl_certificate_key       /etc/letsencrypt/live/$WP_DOMAIN/privkey.pem;
  ssl_trusted_certificate   /etc/letsencrypt/live/$WP_DOMAIN/chain.pem;
  ssl_dhparam               /etc/letsencrypt/live/$WP_DOMAIN/dhparam.pem;

  ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
  ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS';
  ssl_prefer_server_ciphers on;
  ssl_session_timeout 1d;
  ssl_session_cache shared:SSL:50m;
  ssl_session_tickets off;
  ssl_stapling on;
  ssl_stapling_verify on;
  resolver 8.8.4.4 8.8.8.8 valid=300s;
  resolver_timeout 10s;

  add_header Strict-Transport-Security max-age=15552000;

  location / {
    try_files \$uri \$uri/ /index.php?\$args;
  }

  location ~ \.php\$ {
    include snippets/fastcgi-php.conf;
    fastcgi_pass unix:/run/php/php7.0-fpm.sock;
  }
}
EOF
sudo systemctl restart nginx

Make sure that the certificates never expire by periodically renewing them.

sudo tee /etc/cron.daily/letsencrypt <<EOF
letsencrypt renew --agree-tos && systemctl restart nginx
EOF
sudo chmod +x /etc/cron.daily/letsencrypt

Install WordPress

Next, we fetch the latest version of the WordPress source code and unarchive it into $WP_PATH.

sudo rm -rf $WP_PATH/public/ # !!!
sudo mkdir -p $WP_PATH/public/
sudo chown -R $USER $WP_PATH/public/
cd $WP_PATH/public/

wget https://wordpress.org/latest.tar.gz
tar xf latest.tar.gz --strip-components=1
rm latest.tar.gz

mv wp-config-sample.php wp-config.php
sed -i s/database_name_here/$WP_DB_NAME/ wp-config.php
sed -i s/username_here/$WP_DB_USERNAME/ wp-config.php
sed -i s/password_here/$WP_DB_PASSWORD/ wp-config.php
echo "define('FS_METHOD', 'direct');" >> wp-config.php

sudo chown -R www-data:www-data $WP_PATH/public/

Finally, let's perform the final step of the installation which is to choose a username and password for the admin user.

curl "http://$WP_DOMAIN/wp-admin/install.php?step=2" \
  --data-urlencode "weblog_title=$WP_DOMAIN"\
  --data-urlencode "user_name=$WP_ADMIN_USERNAME" \
  --data-urlencode "admin_email=$WP_ADMIN_EMAIL" \
  --data-urlencode "admin_password=$WP_ADMIN_PASSWORD" \
  --data-urlencode "admin_password2=$WP_ADMIN_PASSWORD" \
  --data-urlencode "pw_weak=1"  

Or just go to http://$WP_DOMAIN/ and do it manually.

That's it. Your site should be up and running. Just go to http://$WP_DOMAIN.

You can login with $WP_ADMIN_USERNAME and $WP_ADMIN_PASSWORD at http://$WP_DOMAIN/wp-login.php.

WordPress Network

If you plan on running two blogs on one WordPress installation, you can do that by creating a network.

This will work if you didn't install or enable any plugins after the previous step.

# enable multi sites
sudo sed -i "/Happy blogging. \*\//a define('WP_ALLOW_MULTISITE', true);" $WP_PATH/wp-config.php

# log into the admin dashboard
curl "http://$WP_DOMAIN/wp-login.php" \
  -c /tmp/wp-cookies.txt \
  --data-urlencode "log=$WP_ADMIN_USERNAME" \
  --data-urlencode "pwd=$WP_ADMIN_PASSWORD"

# get the csrf token
curl -s "http://$WP_DOMAIN/wp-admin/network.php" -b /tmp/wp-cookies.txt \
  | grep -Eo '_wpnonce" value="\w+"' | cut -d '"' -f 3 > /tmp/wp-nonce.txt

# convert wordpress to multi site wordpress
curl "http://$WP_DOMAIN/wp-admin/network.php" \
  -b /tmp/wp-cookies.txt \
  --data-urlencode "sitename=$WP_DOMAIN network" \
  --data-urlencode "email=$WP_ADMIN_EMAIL" \
  --data-urlencode "subdomain_install=1" \
  --data-urlencode "_wpnonce=$(cat /tmp/wp-nonce.txt)"

rm -f /tmp/wp-cookies.txt /tmp/wp-nonce.txt

# update the configuration file
sudo sed -i "/Happy blogging. \*\//a define('MULTISITE', true);" $WP_PATH/public/wp-config.php
sudo sed -i "/Happy blogging. \*\//a define('SUBDOMAIN_INSTALL', true);" $WP_PATH/public/wp-config.php
sudo sed -i "/Happy blogging. \*\//a define('DOMAIN_CURRENT_SITE', '$WP_DOMAIN');" $WP_PATH/public/wp-config.php
sudo sed -i "/Happy blogging. \*\//a define('PATH_CURRENT_SITE', '/');" $WP_PATH/public/wp-config.php
sudo sed -i "/Happy blogging. \*\//a define('SITE_ID_CURRENT_SITE', 1);" $WP_PATH/public/wp-config.php
sudo sed -i "/Happy blogging. \*\//a define('BLOG_ID_CURRENT_SITE', 1);" $WP_PATH/public/wp-config.php
sudo sed -i "/Happy blogging. \*\//a define('COOKIE_DOMAIN', false);" $WP_PATH/public/wp-config.php

What happens here is some dark magic.

We add define('WP_ALLOW_MULTISITE', true) to the wordpress configuration file wp-config.php after the line /* That's all, stop editing! Happy blogging. */.

Then we log in to the admin dashboard and save the cookies so that we can make the next request as the logged in user.

We need to grab the CSRF token from the network setup page. It is used to prevent malicious users from hijacking your site when you are logged into your WordPress admin dashboard in another tab and visit the attackers website. Here I am doing something very dirty (grepping HTML) to accomplish that. And this may not work in the next versions of WordPress.

Next, we perform the upgrade process as if we clicked on a button in the admin dashboard.

Finally, we add some constants to the configuration file that WordPress needs.

Add a new site to the network

So this is not automated yet.

Securing WordPress

Enable automatic updates for core, all plugins and themes. Useful if you don't plan on maintaining this installation. Note that things may break when they're updated.

echo "add_filter( 'allow_dev_auto_core_updates', '__return_false' );" >> wp-config.php
echo "add_filter( 'allow_minor_auto_core_updates', '__return_true' );" >> wp-config.php
echo "add_filter( 'allow_major_auto_core_updates', '__return_true' );" >> wp-config.php
echo "add_filter( 'auto_update_plugin', '__return_true' );" >> wp-config.php
echo "add_filter( 'auto_update_theme', '__return_true' );" >> wp-config.php

You can edit plugin and theme files from the admin dashboard. This will prevent it and may stop some attacks.

echo "define('DISALLOW_FILE_EDIT', true);" >> wp-config.php

Set your own unique keys and salts.

sed -i "s/define('AUTH_KEY',\s*'put your unique phrase here');/define('AUTH_KEY', '`pwgen -1 -s 64`');/" wp-config.php
sed -i "s/define('SECURE_AUTH_KEY',\s*'put your unique phrase here');/define('SECURE_AUTH_KEY', '`pwgen -1 -s 64`');/" wp-config.php
sed -i "s/define('LOGGED_IN_KEY',\s*'put your unique phrase here');/define('LOGGED_IN_KEY', '`pwgen -1 -s 64`');/" wp-config.php
sed -i "s/define('NONCE_KEY',\s*'put your unique phrase here');/define('NONCE_KEY', '`pwgen -1 -s 64`');/" wp-config.php
sed -i "s/define('AUTH_SALT',\s*'put your unique phrase here');/define('AUTH_SALT', '`pwgen -1 -s 64`');/" wp-config.php
sed -i "s/define('SECURE_AUTH_SALT',\s*'put your unique phrase here');/define('SECURE_AUTH_SALT', '`pwgen -1 -s 64`');/" wp-config.php
sed -i "s/define('LOGGED_IN_SALT',\s*'put your unique phrase here');/define('LOGGED_IN_SALT', '`pwgen -1 -s 64`');/" wp-config.php
sed -i "s/define('NONCE_SALT',\s*'put your unique phrase here');/define('NONCE_SALT', '`pwgen -1 -s 64`');/" wp-config.php

Move wp-config.php outside of document root.

sudo mv $WP_PATH/public/wp-config.php $WP_PATH/wp-config.php

Give the WordPress directory more restrictive permissions.

sudo chown -R root:root $WP_PATH
sudo chown -R $USER $WP_PATH/public/
sudo chown -R www-data:www-data $WP_PATH/public/wp-content/

Remove some files.

sudo rm $WP_PATH/public/readme*

Install the following plugins

Uninstall unused plugins and themes.

Restrict access to /wp-admin/.

Extra

Firewall

This will only let HTTP(s) and SSH through.

sudo ufw default deny incoming
sudo ufw allow ssh
sudo ufw allow http
sudo ufw allow https
echo y | sudo ufw enable

Swap

Add some extra memory (1 gigabyte), just in case.

sudo fallocate -l 1G /swapfile
sudo chmod 0600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

Up to date

Don't forget to keep everything up to date.

sudo apt update
sudo apt dist-upgrade -y

TODO

I want to improve this blog post in the future with the following: