I have been deploying many VPSs for varied use cases and wanted to finally document the steps I take for my reference. The idea is to spin up a VPS, layer adequate security, and allow me easy access to the server from my Home PC (Windows 11) and other devices.
For this project, I want to set up a WordPress website with a free control panel. Been using CloudPanel (taking advantage of the 1-click installation of WordPress and Sites' Management features) and Cloudflare with no issues for a couple of years already. I am familiar with them and they provide what I need for my use case/requirements.
And if these steps can help others navigate their DIY adventures, that would be great too! :)
- Workstation/Client: Windows 11 PC
- Terminal: Powershell 7.x
- 1 x Admin/User (me)
- 1 x main SSH key (mine)
- VPS - 2vCore and 2GB Ram
- Ubuntu 24.04 OS
- DNS management via Cloudflare (+ Origin Certificates issuance)
- Cloudpanel as websites control panel
- CloudPanel Login page under reverse proxy (to achieve clp.domain.com as my panel's login)
- Cloudpanel default stack is LEMP (Nginx)
- Installation of Wordpress (hardened)
Shortlisted providers are all with SG-located datacenter. Going for a minimum of 2GB Ram.
Digital Ocean:
- ~ SGD 11/mth: 1 vCPU | 1GB Ram | 35 GB NVMe SSD
- ~ SGD 22/mth: 1 vCPU | 2GB Ram | 70 GB NVMe SSD
- ~ SGD 23/mth: 2 vCPU | 2 GB Ram | 90 GB NVMe SSD
Hetzner:
- ~ SGD 14/mth: 2 vCPU | 2 GB Ram | 40 GB NVME SSD
OVH SG:
- VLE-2: ~ SGD 8/mth : 2 vCPU | 2 GB Ram | 40 GB NVME SSD
- VLE-4: ~ SGD 16/mth : 4 vCPU | 4 GB Ram | 40 GB
Vultr:
- ~ SGD 8/mth: 1 vCPU | 1 GB Ram | 25 GB NVMe SSD
- ~ SGD 16/mth: 1vCPU | 2GB Ram | 50 GB NVMe SSD
- ~ SGD 25/mth: 2vCPU | 2GB Ram | 60 GB NVMe SSD
For this test deployment, am using Digital Ocean since I have $200 free referral credits to use up within the next 60 days :)
Get your free $200 credit here: https://m.do.co/c/97213212086d\
For the production-ready site, may go for somewhere else or stick to DO, will see..
Allright, Lets go!
This page is where the entire steps are, but for specific sections/parts of the deployment, the shortcuts are:
- Part 1 - SSH Setup for Windows
- Part 2 - VPS Setup & Login
- Part 3 - Hardening: SSH, UFW & Fail2ban
- Part 4 - Cloudflare, SSLs & DNS Setup
- Part 5 - CloudPanel Setup
- Part 6 - Install & Securing Wordpress
- Installing Powershell 7.5.x from Windows Store
- Other methods of installation and guide here: https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell-on-windows?view=powershell-7.5
- Start PowerShell
ssh-keygen -t ed25519 -f C:/Users/{user}/.ssh/sfarhan-key -C ""
Note
- Can add any nickname/comment after -C (eg: -C homepc)
- -C "" will not have any comments in the keys
- Can remove the private keys from local directory after adding key to [ssh-add] setup and store it at Bitwarden for safekeeping
All keys are autosave to: C:/Users/{user}/.ssh folder
Go to System -> Optional Features -> Add OpenSSH
Open PowerShell as Administrator
For [sshd]:
Get-Service -Name sshd | Set-Service -StartupType Automatic
For [ssh-agent]:
Get-Service ssh-agent | Set-Service -StartupType Automatic
These will set the sshd and ssh-agent services to start automatically.
Start-Service sshd
Start-Service ssh-agent
Get-Service ssh-agent
Note
- This setup and configurations for Windows are for [ssh-agent] to securely store the private keys within Windows security context, associated with Windows account
- And to start the [ssh-agent] service each time the computer is rebooted (to start automatically)
- By default the [ssh-agent] service is disabled
ssh-add $env:USERPROFILE\.ssh\sfarhan-key
Note
- The ssh-agent in Windows will now automatically retrieve the local private key and pass it to SSH client.
- If need be, can create new set of keys for each devices (Tablet, Phone) to SSH in.
- Then standby to add those public keys onto the server when ready.
Lets now go over to our VPS Provider side of things..
- For Digital Ocean, Hetzner, Vultr or any other VPS providers, if there is an option to “park” public keys at their console, use the feature
- If not, deploy with root password
For Digital Ocean, the settings are under "My Account" -> "Manage Team Settings" -> "Security" -> "Add SSH Key":
When creating server, now will have the option to add the public key for a password-less entry:
Run Powershell as Administrator and navigate to .ssh folder and create the file:
cd C:/Users/{user}/.ssh
code config
or
New-Item config
# this is main user access
Host dosvr2
Hostname {IP address}
Identityfile C:/Users/{user}/.ssh/sfarhan-key
Note
- Host (which is an alias, sort of shortcut name for Windows/ssh-agent) can be anything
- Hostname is usually IP address or domain
- IdentityFile is the path to the private keys, as we did rename the key to some other name than the default
- Can use Notepad to open and edit the config file from Windows Explorer
- To use the same config file and configure change of SSH port later
Save file.
From Powershell:
ssh root@dosvr2
Lets secure our VPS next.
apt update && apt upgrade -y
apt autoremove && apt autoclean -y
shutdown -r now
dpkg-reconfigure tzdata
Follow on screen instructions to set timezone.
service cron restart
dpkg-reconfigure unattended-upgrades
or can remove it completely:
apt remove unattended-upgrades
Ensure that the 2 public keys are in the /root/.ssh folder
If not..
cd /root
mkdir .ssh
cd .ssh
nano authorized_keys
Then put the relevant public keys in here.
If need to add additional public keys (eg: for Laptop/Tablet/Phone devices entry points), can add them in now.
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
chown -R root:root ~/.ssh
systemctl restart ssh
systemctl restart ssh.service
Note
All users (root and other users) all share the same config in /etc/ssh/sshd_config, but they don't all share the same 'authorized_keys' files. Thus, even when using same set of keys, need a separate authorized_keys file in /root/.ssh/ and for in /home/yournameuser/.ssh/ but in this case contains same set of public keys_
adduser <user>
usermod -aG sudo <user>
mkdir /home/{user}/.ssh
cp /root/.ssh/authorized_keys /home/{user}/.ssh/authorized_keys
chown -R {user}:{user} /home/{user}/.ssh
chmod 700 /home/{user}/.ssh
chmod 600 /home/{user}/.ssh/authorized_keys
Now the new sudo user has the public keys in their own authorized_keys file
systemctl restart ssh
systemctl restart ssh.service
Open NEW Powershell terminal:
ssh sfarhan@dosvr2
Note: The "dosvr2" is what we defined in the Windows side of things /.ssh config file above
Test a few sudo commands to ensure all is okay, then next step is to disable root login, securing SSH access and implement UFW and Fail2ban
- Change SSH port from 22 to another (here I am using 22022)
- Disable RootLogin
- Disable entry points via Passwords (only SSH keys)
sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.backup
sudo vim /etc/ssh/sshd_config
Port 22022
PermitRootLogin no
PubkeyAuthentication yes
PasswordAuthentication no
Save And Close file
sudo systemctl restart ssh
To activate this new config, it is now required to inform systemd about the change:
sudo systemctl daemon-reload
sudo systemctl restart ssh.socket
sudo systemctl restart ssh.service
sudo lsof -i tcp:22022
Important
Before logging off, need to allow the new port in UFW (Firewall) settings and add new port info in Windows SSH config file
Do not close the existing session! and continue next steps
UFW is already pre-installed with Ubuntu 24.04 but disabled.
sudo ufw enable
can proceed if system asks to
sudo ufw status
sudo ufw default allow outgoing
sudo ufw default deny incoming
sudo ufw allow 22022/tcp
sudo ufw allow http/tcp
sudo ufw allow https/tcp
sudo ufw logging off # or sudo vim /etc/ufw/ufw.conf (LOGLEVEL=off).
sudo UFW status
Note
Will need to add the port info during Cloudpanel's installation process, as its default port is 22
Open the config file via Notepad and add the port info. File is at C:/Users/{user}/.ssh/config
# this is main user access
Host dosvr2
Hostname {IP address}
Port 22022 # <-this
Identityfile C:/Users/{user}/.ssh/sfarhan-key
Save file
Important
Before logging off, Start a new terminal to SSH in to test
Log-in server as usual without the need to specify port info:
sudo apt-get update
sudo apt-get install fail2ban -y
sudo /etc/init.d/fail2ban status
sudo touch /etc/fail2ban/jail.local
sudo nano /etc/fail2ban/jail.local
Add:
[DEFAULT]
ignoreip = 127.0.0.1/8
findtime = 3200
bantime = 86400
[sshd]
backend=systemd
enabled = true
port = 22022
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
Note
Every .conf file can be overridden with a file named .local. The .conf file is read first, then .local, with later settings overriding earlier ones. Thus, a .local file doesn't have to include everything in the corresponding .conf file, only those settings that you wish to override. Modifications should take place in the .local and not in the .conf. This avoids merging problems when upgrading.
sudo systemctl restart fail2ban
sudo service fail2ban restart
Note
Install this if fail2ban jock error shows up:
sudo apt install python3-systemd
sudo iptables -S
sudo iptables -L
sudo fail2ban-client status sshd
tail -f /var/log/auth.log
tail -f /var/log/fail2ban.log
- Easy and secured entry from a Windows setup
- and Installation of applications at will
Next Step is to go over to our Cloudflare account for the DNS and SSLs
Go to Domain Registrar and add Cloudflare's given nameservers
Note
CloudPanel's log-in page will be clp.domain.com
Do not proxy it through Cloudflare yet!
SSL/TLS -> Overview -> Configure
SSL/TLS -> Origin Server -> Create
Copy the two sets of keys somewhere. We will need them later. Once copied, click OK.
We have a few choices - CloudPanel, HestiaCP, Webadmin etc..
Here I am documenting the steps I take for using CloudPanel for the WordPress installation
curl -sS https://installer.cloudpanel.io/ce/v2/install.sh -o install.sh && sudo bash install.sh --ssh_port 22022
https://www.cloudpanel.io/docs/v2/getting-started/other/
Since I have a different SSH port than the usual 22, will need to add that SSH part for the installation
apt-get autoremove && apt-get autoclean
Admin Area -> Security
Add: Custom SSH Port - 22022 - ip4: 0.0.0.0/0
Add: Custom SSH Port - 22022 - ip6: ::/0
- Go to Sites and Create a Reverse Proxy
- Enter the Domain Name as clp.domain.com, enter https://127.0.0.1:8443 as Reverse Proxy Url.
- Make sure the https is the url
- User: reverseproxy (this is just to contain the /home docs)
- Once created, go to Manage -> SSL -> and import Cloudflare Origin Certificate for clp.domain.com (to log into the panel)
- The Cloudflare Origin Certificate was from the previous step
- Go back to Cloudflare DNS page and enable Proxy for the clp.domain.com A record
- https://clp.domain.com will now bring to CloudPanel login page with SSL
Last few steps are to install WordPress and layer some basic WP securities
- Site -> Manage -> Security -> Allow traffic from Cloudflare only
- Site -> Manage -> Settings -> Adjust PHP Settings and Add SSH Keys for SSH/SFTP (though CloudPanel already has a decent file manager in-built)
- Can start tweaking any web-related settings where makes sense!
Credentials are as saved earlier in Cloudpanel:
1. Wordfence
If they found a file that needs to hide but is unable to hide at UI level eg:
location ~ \.user\.ini$ {
deny all;
}
2. Limit Login Attempts Reloaded
3. WP fail2ban – Advanced Security
WPf2b comes with three fail2ban filters that we need:
wordpress-hard.conf
wordpress-soft.conf
wordpress-extra.conf
↪️ Explore the plugin's conf files at htdocs and find the appropriate conf file to copy over to server's fail2ban filter.d directory
Via CLI:
sudo cp /home/$User/htdocs/$domain.com/wp-content/plugins/wp-fail2ban/filters.d/wordpress-hard.conf /etc/fail2ban/filter.d/
and also the -soft and the -extra conf files
$User = Admin Username for the website over at CloudPanel
Or, via File Manager at CloudPanel:
open -> copy -> create respective files in /etc/fail2ban/filter.d/ -> paste:
vim /etc/fail2ban/jail.local
[wordpress-hard]
enabled = true
filter = wordpress-hard
logpath = /var/log/auth.log
backend = auto
maxretry = 3
port = http,https
[wordpress-soft]
enabled = true
filter = wordpress-soft
logpath = /var/log/auth.log
backend = auto
maxretry = 3
port = http,https
[wordpress-extra]
enabled = true
filter = wordpress-extra
logpath = /var/log/auth.log
backend = auto
maxretry = 3
port = http,https
Save file.
sudo systemctl restart fail2ban
sudo tail -f /var/log/fail2ban.log
Note
Potentially can add new jails configs for Wordpress / MSQL / PHP / Nginx Bots etc… https://webdock.io/en/docs/how-guides/security-guides/how-configure-fail2ban-common-services
##############
👍 All right, WordPress website is up and running, WP-Admin is secured (relatively), with processes in place for ease of monitoring and tweaking.
Let me know if there are any issues/improvements with the above steps. Happy to check them out. And hopefully, if any is following the steps above, it has been helpful.. :)