Provision a new VPS¶
This guide takes a freshly created cloud server, the kind Hetzner Cloud or
DigitalOcean hands you with only root and your SSH key, to a hardened web
server in three sadame apply runs: an administrator account, a security
baseline (SSH on port 2222 with keys only, a firewall, fail2ban, automatic
updates) and nginx serving a site. Each step is one of the examples, used as
it is, and the output below comes from running exactly these commands
against a fresh Debian 12 server.
What you need¶
- sadame on your machine (see Install) and a checkout of
the repository for the examples, or copies of
examples/06-app-deploy-user,examples/07-hardening-baselineandexamples/02-nginx-static-site; - an SSH key pair; the commands below use
~/.ssh/id_ed25519.pub; - a new server running Debian 12 or Ubuntu 24.04, reachable as
rootwith that key.
1. Create the server¶
Create it with your public key, in the web console or with the provider's
CLI. With Hetzner Cloud's hcloud, after hcloud ssh-key create --name me
--public-key-from-file ~/.ssh/id_ed25519.pub:
hcloud server create --name vps1 --type cx22 --image debian-12 --ssh-key me # types: hcloud server-type list
With DigitalOcean's doctl, using the fingerprint from
doctl compute ssh-key list:
doctl compute droplet create vps1 --image debian-12-x64 --size s-1vcpu-1gb --region fra1 --ssh-keys FINGERPRINT
Note the server's IP address; the commands below write
vps1.example.com in its place. Connect once with ssh root@ADDRESS to
check the key works and to accept the host key.
2. An administrator instead of root¶
06-app-deploy-user creates a user with your key and a sudoers rule.
Given ALL as its only command, the user is an administrator with
passwordless sudo, which sadame needs:
$ sadame apply examples/06-app-deploy-user -h root@vps1.example.com --var "ssh_keys=$(cat ~/.ssh/id_ed25519.pub)" --var sudo_commands=ALL
[vps1.example.com] copying sadame-linux-arm64 to ~/.sadame/bin/dcf637ae7d2f79587cd36cc4fb47eff845444424933647a5e75565917475c21d/
[vps1.example.com] sadame 0.5.1 apply on vps1.example.com (Debian GNU/Linux 12 (bookworm))
[vps1.example.com]
[vps1.example.com] changed package[sudo]
[vps1.example.com] state: absent -> installed
[vps1.example.com] changed user[deploy]
[vps1.example.com] state: absent -> present
[vps1.example.com] shell: /bin/bash
[vps1.example.com] changed directory[/home/deploy/.ssh]
[vps1.example.com] state: absent -> present
[vps1.example.com] mode: 0700
[vps1.example.com] owner: deploy
[vps1.example.com] changed file[/home/deploy/.ssh/authorized_keys]
[vps1.example.com] state: absent -> present
[vps1.example.com] mode: 0600
[vps1.example.com] owner: deploy
[vps1.example.com] --- /dev/null
[vps1.example.com] +++ /home/deploy/.ssh/authorized_keys (desired)
[vps1.example.com] @@ -0,0 +1,2 @@
[vps1.example.com] +# Managed by sadame. Keys added by hand will be removed.
[vps1.example.com] +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFWO3Nymy8E4ghq6ua2q2VpLuhKeGi+Q5Dqe2P1u++2F root@0ca1c9a9292f
[vps1.example.com] changed file[/etc/sudoers.d/deploy]
[vps1.example.com] state: absent -> present
[vps1.example.com] mode: 0440
[vps1.example.com] owner: root
[vps1.example.com] --- /dev/null
[vps1.example.com] +++ /etc/sudoers.d/deploy (desired)
[vps1.example.com] @@ -0,0 +1,2 @@
[vps1.example.com] +# Managed by sadame.
[vps1.example.com] +deploy ALL=(root) NOPASSWD: ALL
[vps1.example.com]
[vps1.example.com] Summary
[vps1.example.com] changed 5 package[sudo]
[vps1.example.com] user[deploy]
[vps1.example.com] directory[/home/deploy/.ssh]
[vps1.example.com] file[/home/deploy/.ssh/authorized_keys]
[vps1.example.com] file[/etc/sudoers.d/deploy]
[vps1.example.com] unchanged 0
[vps1.example.com] failed 0
[vps1.example.com] skipped 0
[vps1.example.com] notified 0
[vps1.example.com]
[vps1.example.com] Finished in 5.60s.
Hosts
vps1.example.com ok
apply on 1 host: 1 ok. Finished in 5.78s.
The first run copied sadame to the server (~/.sadame/bin/); later runs
reuse it.
3. The security baseline¶
From now on, connect as deploy with --sudo. 07-hardening-baseline
moves SSH to port 2222, turns off root and password logins, and lets only
SSH and the ports in open_ports through the firewall:
$ sadame apply examples/07-hardening-baseline -h deploy@vps1.example.com --sudo --var open_ports=80,443
[vps1.example.com] copying sadame-linux-arm64 to ~/.sadame/bin/dcf637ae7d2f79587cd36cc4fb47eff845444424933647a5e75565917475c21d/
[vps1.example.com] sadame 0.5.1 apply on vps1.example.com (Debian GNU/Linux 12 (bookworm))
[vps1.example.com]
[vps1.example.com] unchanged package[openssh-server]
[vps1.example.com] changed package[ufw]
[vps1.example.com] state: absent -> installed
[vps1.example.com] changed package[fail2ban]
[vps1.example.com] state: absent -> installed
[vps1.example.com] changed package[unattended-upgrades]
[vps1.example.com] state: absent -> installed
[vps1.example.com] unchanged service[ssh]
[vps1.example.com] unchanged exec[move ssh.socket to the new port]
[vps1.example.com] changed file[/etc/ssh/sshd_config.d/10-hardening.conf]
[vps1.example.com] state: absent -> present
[vps1.example.com] mode: 0644
[vps1.example.com] --- /dev/null
[vps1.example.com] +++ /etc/ssh/sshd_config.d/10-hardening.conf (desired)
[vps1.example.com] @@ -0,0 +1,7 @@
[vps1.example.com] +# Managed by sadame.
[vps1.example.com] +Port 2222
[vps1.example.com] +PermitRootLogin no
[vps1.example.com] +PasswordAuthentication no
[vps1.example.com] +KbdInteractiveAuthentication no
[vps1.example.com] +X11Forwarding no
[vps1.example.com] +MaxAuthTries 3
[vps1.example.com] changed exec[ufw default deny incoming]
[vps1.example.com] command: ufw default deny incoming
[vps1.example.com] (not_if exited non-zero)
[vps1.example.com] changed exec[ufw allow 2222/tcp]
[vps1.example.com] command: ufw allow 2222/tcp
[vps1.example.com] (not_if exited non-zero)
[vps1.example.com] changed exec[ufw allow 80/tcp]
[vps1.example.com] command: ufw allow 80/tcp
[vps1.example.com] (not_if exited non-zero)
[vps1.example.com] changed exec[ufw allow 443/tcp]
[vps1.example.com] command: ufw allow 443/tcp
[vps1.example.com] (not_if exited non-zero)
[vps1.example.com] changed exec[ufw --force enable]
[vps1.example.com] command: ufw --force enable
[vps1.example.com] (not_if exited non-zero)
[vps1.example.com] changed file[/etc/fail2ban/jail.d/sshd.local]
[vps1.example.com] state: absent -> present
[vps1.example.com] mode: 0644
[vps1.example.com] --- /dev/null
[vps1.example.com] +++ /etc/fail2ban/jail.d/sshd.local (desired)
[vps1.example.com] @@ -0,0 +1,7 @@
[vps1.example.com] +# Managed by sadame.
[vps1.example.com] +[sshd]
[vps1.example.com] +enabled = true
[vps1.example.com] +backend = systemd
[vps1.example.com] +port = 2222
[vps1.example.com] +maxretry = 5
[vps1.example.com] +bantime = 1h
[vps1.example.com] changed service[fail2ban]
[vps1.example.com] state: stopped -> running
[vps1.example.com] unchanged file[/etc/apt/apt.conf.d/20auto-upgrades]
[vps1.example.com] unchanged service[apt-daily-upgrade.timer]
[vps1.example.com] notified service[ssh]: restart (delayed, from file[/etc/ssh/sshd_config.d/10-hardening.conf])
[vps1.example.com] notified exec[move ssh.socket to the new port]: run (delayed, from file[/etc/ssh/sshd_config.d/10-hardening.conf])
[vps1.example.com] notified service[fail2ban]: restart (delayed, from file[/etc/fail2ban/jail.d/sshd.local])
[vps1.example.com]
[vps1.example.com] Summary
[vps1.example.com] changed 11 package[ufw]
[vps1.example.com] package[fail2ban]
[vps1.example.com] package[unattended-upgrades]
[vps1.example.com] file[/etc/ssh/sshd_config.d/10-hardening.conf]
[vps1.example.com] exec[ufw default deny incoming]
[vps1.example.com] exec[ufw allow 2222/tcp]
[vps1.example.com] exec[ufw allow 80/tcp]
[vps1.example.com] exec[ufw allow 443/tcp]
[vps1.example.com] exec[ufw --force enable]
[vps1.example.com] file[/etc/fail2ban/jail.d/sshd.local]
[vps1.example.com] service[fail2ban]
[vps1.example.com] unchanged 5
[vps1.example.com] failed 0
[vps1.example.com] skipped 0
[vps1.example.com] notified 3 service[ssh]: restart
[vps1.example.com] exec[move ssh.socket to the new port]: run
[vps1.example.com] service[fail2ban]: restart
[vps1.example.com]
[vps1.example.com] Finished in 20.58s.
Hosts
vps1.example.com ok
apply on 1 host: 1 ok. Finished in 20.82s.
sshd restarted on the new port at the end of the run; the connection that
ran it was kept, because ufw lets established connections through. On
Ubuntu, where systemd starts sshd from ssh.socket, the recipe also moves
the socket to the new port; restarting sshd alone would leave it on 22,
behind the firewall.
4. The web server¶
From now on, the server is at port 2222:
$ sadame apply examples/02-nginx-static-site -h deploy@vps1.example.com:2222 --sudo --var server_name=vps1.example.com
[vps1.example.com] sadame 0.5.1 apply on vps1.example.com (Debian GNU/Linux 12 (bookworm))
[vps1.example.com]
[vps1.example.com] changed package[nginx]
[vps1.example.com] state: absent -> installed
[vps1.example.com] changed service[nginx]
[vps1.example.com] state: stopped -> running
[vps1.example.com] changed file[/etc/nginx/nginx.conf]
[vps1.example.com] --- /etc/nginx/nginx.conf
[vps1.example.com] +++ /etc/nginx/nginx.conf (desired)
[vps1.example.com] @@ -1,83 +1,17 @@
[vps1.example.com] +# Managed by sadame. Local changes will be overwritten.
[vps1.example.com] user www-data;
[vps1.example.com] worker_processes auto;
[vps1.example.com] pid /run/nginx.pid;
[vps1.example.com] error_log /var/log/nginx/error.log;
[vps1.example.com] -include /etc/nginx/modules-enabled/*.conf;
[vps1.example.com]
[vps1.example.com] events {
[vps1.example.com] - worker_connections 768;
[vps1.example.com] - # multi_accept on;
[vps1.example.com] + worker_connections 1024;
[vps1.example.com] }
[vps1.example.com]
[vps1.example.com] http {
[vps1.example.com] -
[vps1.example.com] - ##
[vps1.example.com] - # Basic Settings
[vps1.example.com] - ##
[vps1.example.com] -
[vps1.example.com] - sendfile on;
[vps1.example.com] - tcp_nopush on;
[vps1.example.com] - types_hash_max_size 2048;
[vps1.example.com] - # server_tokens off;
[vps1.example.com] -
[vps1.example.com] - # server_names_hash_bucket_size 64;
[vps1.example.com] - # server_name_in_redirect off;
[vps1.example.com] -
[vps1.example.com] - include /etc/nginx/mime.types;
[vps1.example.com] - default_type application/octet-stream;
[vps1.example.com] -
[vps1.example.com] - ##
[vps1.example.com] - # SSL Settings
[vps1.example.com] - ##
[vps1.example.com] -
[vps1.example.com] - ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE
[vps1.example.com] - ssl_prefer_server_ciphers on;
[vps1.example.com] -
[vps1.example.com] - ##
[vps1.example.com] - # Logging Settings
[vps1.example.com] - ##
[vps1.example.com] -
[vps1.example.com] - access_log /var/log/nginx/access.log;
[vps1.example.com] -
[vps1.example.com] - ##
[vps1.example.com] - # Gzip Settings
[vps1.example.com] - ##
[vps1.example.com] -
[vps1.example.com] - gzip on;
[vps1.example.com] -
[vps1.example.com] - # gzip_vary on;
[vps1.example.com] - # gzip_proxied any;
[vps1.example.com] - # gzip_comp_level 6;
[vps1.example.com] - # gzip_buffers 16 8k;
[vps1.example.com] - # gzip_http_version 1.1;
[vps1.example.com] - # gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
[vps1.example.com] -
[vps1.example.com] - ##
[vps1.example.com] - # Virtual Host Configs
[vps1.example.com] - ##
[vps1.example.com] -
[vps1.example.com] - include /etc/nginx/conf.d/*.conf;
[vps1.example.com] - include /etc/nginx/sites-enabled/*;
[vps1.example.com] + include /etc/nginx/mime.types;
[vps1.example.com] + default_type application/octet-stream;
[vps1.example.com] + sendfile on;
[vps1.example.com] + access_log /var/log/nginx/access.log;
[vps1.example.com] + include /etc/nginx/conf.d/*.conf;
[vps1.example.com] }
[vps1.example.com] -
[vps1.example.com] -
[vps1.example.com] -#mail {
[vps1.example.com] -# # See sample authentication script at:
[vps1.example.com] -# # http://wiki.nginx.org/ImapAuthenticateWithApachePhpScript
[vps1.example.com] -#
[vps1.example.com] -# # auth_http localhost/auth.php;
[vps1.example.com] -# # pop3_capabilities "TOP" "USER";
[vps1.example.com] -# # imap_capabilities "IMAP4rev1" "UIDPLUS";
[vps1.example.com] -#
[vps1.example.com] -# server {
[vps1.example.com] -# listen localhost:110;
[vps1.example.com] -# protocol pop3;
[vps1.example.com] -# proxy on;
[vps1.example.com] -# }
[vps1.example.com] -#
[vps1.example.com] -# server {
[vps1.example.com] -# listen localhost:143;
[vps1.example.com] -# protocol imap;
[vps1.example.com] -# proxy on;
[vps1.example.com] -# }
[vps1.example.com] -#}
[vps1.example.com] unchanged directory[/etc/nginx/conf.d]
[vps1.example.com] changed file[/etc/nginx/conf.d/static-site.conf]
[vps1.example.com] state: absent -> present
[vps1.example.com] mode: 0644
[vps1.example.com] --- /dev/null
[vps1.example.com] +++ /etc/nginx/conf.d/static-site.conf (desired)
[vps1.example.com] @@ -0,0 +1,7 @@
[vps1.example.com] +# Managed by sadame.
[vps1.example.com] +server {
[vps1.example.com] + listen 80 default_server;
[vps1.example.com] + server_name vps1.example.com;
[vps1.example.com] + root /var/www/static-site;
[vps1.example.com] + index index.html;
[vps1.example.com] +}
[vps1.example.com] changed directory[/var/www/static-site]
[vps1.example.com] state: absent -> present
[vps1.example.com] mode: 0755
[vps1.example.com] changed file[/var/www/static-site/index.html]
[vps1.example.com] state: absent -> present
[vps1.example.com] mode: 0644
[vps1.example.com] --- /dev/null
[vps1.example.com] +++ /var/www/static-site/index.html (desired)
[vps1.example.com] @@ -0,0 +1,11 @@
[vps1.example.com] +<!doctype html>
[vps1.example.com] +<html lang="en">
[vps1.example.com] + <head>
[vps1.example.com] + <meta charset="utf-8">
[vps1.example.com] + <title>It works</title>
[vps1.example.com] + </head>
[vps1.example.com] + <body>
[vps1.example.com] + <h1>It works</h1>
[vps1.example.com] + <p>Served by nginx, configured by sadame.</p>
[vps1.example.com] + </body>
[vps1.example.com] +</html>
[vps1.example.com] notified service[nginx]: reload (delayed, from file[/etc/nginx/nginx.conf])
[vps1.example.com]
[vps1.example.com] Summary
[vps1.example.com] changed 6 package[nginx]
[vps1.example.com] service[nginx]
[vps1.example.com] file[/etc/nginx/nginx.conf]
[vps1.example.com] file[/etc/nginx/conf.d/static-site.conf]
[vps1.example.com] directory[/var/www/static-site]
[vps1.example.com] file[/var/www/static-site/index.html]
[vps1.example.com] unchanged 1
[vps1.example.com] failed 0
[vps1.example.com] skipped 0
[vps1.example.com] notified 1 service[nginx]: reload
[vps1.example.com]
[vps1.example.com] Finished in 3.67s.
Hosts
vps1.example.com ok
apply on 1 host: 1 ok. Finished in 3.88s.
5. Check the result¶
drift on the baseline exits 0: everything is as the recipe says. Root
can no longer log in, and port 22 no longer answers:
$ sadame drift examples/07-hardening-baseline -h deploy@vps1.example.com:2222 --sudo --var open_ports=80,443
[vps1.example.com] sadame 0.5.1 drift on vps1.example.com (Debian GNU/Linux 12 (bookworm))
[vps1.example.com]
[vps1.example.com] No drift: 16 resources match the recipe.
Hosts
vps1.example.com ok
drift on 1 host: 1 ok. Finished in 0.31s.
$ echo $?
0
$ ssh -p 2222 root@vps1.example.com true
root@vps1.example.com: Permission denied (publickey).
$ ssh -p 22 deploy@vps1.example.com true
ssh: connect to host vps1.example.com port 22: Connection timed out
Keep it in an inventory¶
Typing -h deploy@...:2222 --sudo gets old. Put the server in a
hosts.rb, as examples/07-hardening-baseline/hosts-after.rb does, and
use -g:
# After the first apply: SSH listens on vars[:ssh_port] (2222) only.
group :new do
host "vps1.example.com", user: "admin", sudo: true, port: 2222
end
Then Drift in CI checks the server every morning.
Common mistakes¶
- Running the baseline before the administrator account. The baseline turns off root login; without another user with a key and sudo, you are locked out.
- Forgetting
open_ports. The firewall only lets SSH in. A web server needs--var open_ports=80,443(oropen_ports: [80, 443]invars.rb). - Connecting to port 22 after the baseline. Use
HOST:2222,port: 2222inhosts.rb, orPort 2222in~/.ssh/config. - The provider's own firewall. Hetzner and DigitalOcean also have firewalls outside the server; open ports 2222, 80 and 443 there too if you use them.
See also¶
- Remote hosts: SSH options, sudo and troubleshooting.
- Examples:
06,07and02.