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.
- Go to
http://$WP_DOMAIN/wp-admin/network/sites.php
- For
Site Address (URL)
add something, anything, for instance,blah
- Then click the link
Edit Site
or go tohttp://$WP_DOMAIN/wp-admin/network/site-info.php?id=2
- Change the
Site Address (URL)
to the real domain - Click
Save Changes
- Run
WP_DOMAIN=anothersite.example.com
- Repeat the
Configuring nginx
step for the new$WP_DOMAIN
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
- Wordfense Security
- WP Security Audit Log
- Security Ninja
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:
- Redirect www to non-www
- Use wp-cli instead of curl
- Install some plugins
- Automatic updates
- Mail server
- Automatic backups
- Ansible script
- Optional slack monitoring
- Rotate nginx logs