Hetzner Cloud: WordPress per nginx und mariadb mit Ubuntu 18.04 LTS

Hetzner bietet recht flotte und günstige Cloud Server (ab 2,96€ / Monat) an, die sich gut für eine kleine WordPress Seite eignen.

Diese hier läuft auf dem kleinsten, die Einrichtung geht recht schnell, mehr als einen passenden SSH-Key muss man nicht auswählen und nach kurzer Wartezeit steht der CX11 zur Verfügung.

Erste Schritte auf dem Server

  • Aktualisierung & Absicherung made simple: ssh, firewall ufw und fail2ban
  • Installation der notwendigen Pakete
  • Konfiguration von nginx und mariadb
  • Installation und Konfiguration von WordPress

Allgemeine Grundlagen

Der Umgang mit putty / ssh sowie grundlegende Kenntnis der Kommandozeile wird vorausgesetzt. Weiterhin benötigt man einen SSH Key, dieser kann mit puttygen (Windows) oder

ssh-keygen -t ed25519

unter beispielsweise Ubuntu erstellt werden. Zum Anzeigen des erstellten Keys kann beispielsweise

cat ~/.ssh/id_ed25519.pub

genutzt werden.
Dieser wird dann beim Einrichten des Servers in der Hetzner Cloud Console am unteren Ende eingefügt.

Absicherung mit ufw…

Zunächst muss kein Port außer ssh (22) sowie http(80) und https(443) erreichbar sein. Die Konfiguration geht mit ufw leicht von der Hand und gehört zum Standartumfang. Vorher wird noch die Aktualität des Systems sichergestellt. Port 22 wird zunächst erlaubt, erst danach die Firewall aktiviert(!)

apt update && apt upgrade -y
ufw allow ssh
ufw enable

Command may disrupt existing ssh connections. Proceed with operation (y|n)? y
Firewall is active and enabled on system startup

Der root Login wird von yes (per passwort und key bzw. weiteren Möglichkeiten) auf key-only gesetzt, was bruteforce Angriffe deutlich erschwert.

nano /etc/ssh/sshd_config

Die Zeile PermitRootLogin yes durch PermitRootLogin without-password ersetzen.

[...]
#LoginGraceTime 2m
PermitRootLogin without-password
#StrictModes yes
[...]
systemctl restart ssh

…und fail2ban

Als nächstes folgt das Hilfsmittel Fail2Ban.
Der Daemon überwacht die Logdateien von ssh und sperrt automatisch lästige Bots und andere, die ständig einen Login versuchen, aus. Da wir uns über Key anmelden ist ein „vertippen“ ausgeschlossen also werden die Hürden recht hoch gesetzt. Gegen die gerne auftretenden „langsamen“ Bots hilft der recidive Filter.

apt install fail2ban
cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
nano /etc/fail2ban/jail.local

Ich bevorzuge höhere bantime und findtime als die Standarteinstellung (10m):

# "bantime" is the number of seconds that a host is banned.
bantime = 36h
# A host is banned if it has generated "maxretry" during the last "findtime"
# seconds.
findtime = 12h

Abschnitt [sshd] suchen und wie folgt ändern:

[sshd]
mode = aggressive
enabled = true
port = ssh
logpath = %(sshd_log)s
backend = %(sshd_backend)s 

Abschnitt [recidive] suchen und wie folgt ändern:

[recidive]
enabled=true
logpath  = /var/log/fail2ban.log
banaction = %(banaction_allports)s
bantime  = 2w
findtime = 2d

Fail2ban Server neustarten:

systemctl restart fail2ban

Installation nginx, mariadb und wichtige Hilfsmittel

apt -y install nginx mariadb-client mariadb-server php-fpm certbot python-certbot-nginx php-mysql

Ab jetzt kennt UFW auch die Regel Nginx Full:

ufw allow 'nginx full'

Mariadb ist in der Standartkonfiguration noch nicht gesichert, das wird jetzt nachgeholt:

mysql_secure_installation

Faustregel: Alles mit „y“ beantworten und ein langes Zufallspasswort für root setzen.

NOTE: RUNNING ALL PARTS OF THIS SCRIPT IS RECOMMENDED FOR ALL MariaDB
      SERVERS IN PRODUCTION USE!  PLEASE READ EACH STEP CAREFULLY!

In order to log into MariaDB to secure it, we'll need the current
password for the root user.  If you've just installed MariaDB, and
you haven't set the root password yet, the password will be blank,
so you should just press enter here.

Enter current password for root (enter for none):
OK, successfully used password, moving on...

Setting the root password ensures that nobody can log into the MariaDB
root user without the proper authorisation.

Set root password? [Y/n] Y
New password:
Re-enter new password:
Password updated successfully!
Reloading privilege tables..
 ... Success!


By default, a MariaDB installation has an anonymous user, allowing anyone
to log into MariaDB without having to have a user account created for
them.  This is intended only for testing, and to make the installation
go a bit smoother.  You should remove them before moving into a
production environment.

Remove anonymous users? [Y/n] Y
 ... Success!

Normally, root should only be allowed to connect from 'localhost'.  This
ensures that someone cannot guess at the root password from the network.

Disallow root login remotely? [Y/n] Y
 ... Success!

By default, MariaDB comes with a database named 'test' that anyone can
access.  This is also intended only for testing, and should be removed
before moving into a production environment.

Remove test database and access to it? [Y/n] Y
 - Dropping test database...
 ... Success!
 - Removing privileges on test database...
 ... Success!

Reloading the privilege tables will ensure that all changes made so far
will take effect immediately.

Reload privilege tables now? [Y/n] Y
 ... Success!

Cleaning up...

All done!  If you've completed all of the above steps, your MariaDB
installation should now be secure.

Thanks for using MariaDB!

Jetzt wird die Datenbank angelegt:

 mysql -u root -p
create database wordpress;
create user 'press'@'localhost' IDENTIFIED BY '[email protected]';
grant all on wordpress.* to 'press'@'localhost' with grant option;
flush privileges;
exit;

Damit sind Vorbereitungen soweit abgeschlossen und die WordPress Dateien können ins passende Verzeichnis entpackt werden.

P.s. Wer die Zeilen oben alle reinkopiert hat und sich nun fragt wie man das Passwort ändern kann:

set password for 'press'@'localhost' = password('neuespasswort');
flush privileges;
exit;

Installation von WordPress

Los gehts mit dem Download der aktuellen Version, entpacken ins Zielverzeichnis und grundlegende Berechtigungen setzen.

wget https://wordpress.org/latest.tar.gz
tar -zxvf latest.tar.gz
mv wordpress /var/www/
chown -R www-data:www-data /var/www/wordpress
chmod -R 755 $(find /var/www/wordpress -type d)
chmod -R 644 $(find /var/www/wordpress -type f)

Anmerkung: Diese Zugriffsrechte sind eher praktisch als sicher, in meinem Fall läuft jedoch sonst nichts auf dem Server. Weitere Informationen dazu gibts hier.
Update: Das setzen der Zugriffsrechte wurde etwas verbessert.

Nun folgt die Konfiguration von nginx:

nano /etc/nginx/sites-available/wordpress

Folgenden Inhalt einfügen und den Eintrag unter server_name in beiden Serverblöcken ändern:

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name www.meinedomain.tld meinedomain.tld;

    root /var/www/wordpress;
    index index.php;
    access_log off;
    error_log /var/log/nginx/error.log crit;

    location / {
        try_files $uri $uri/ /index.php;
    }

    location ~ \.php$ {
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass unix:/run/php/php7.2-fpm.sock;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }
    location ~* \.(js|css|png|jpg|jpeg|gif|ico)$ {
        expires max;
        log_not_found off;
    }
    location = /robots.txt {
        allow all;
        log_not_found off;
        access_log off;
    }
    location /doc/ {
        alias /usr/share/doc/;
        autoindex on;
        allow 127.0.0.1;
        deny all;
    }

    location ~/\.ht {
        deny all;
    }
}
server {

    listen 80;
    listen [::]:80;
    server_name www.meinedomain.tld meinedomain.tld;
}

Dem aufmerksamen Leser wird auffallen, dass noch ein paar Angaben fehlen: Das erledigt gleich der certbot.

Zunächst wird die Seite mit nachfolgendem Befehl „aktiviert“:

ln -s /etc/nginx/sites-available/wordpress /etc/nginx/sites-enabled/

Mit nginx -t kann die Konfiguration geprüft werden, wirft das keine Fehler folgt das Einrichten des ssl Zertifikats.
Das übernimmt der certbot mit Hilfe von LetsEncrypt:

certbot --nginx --agree-tos --redirect --hsts --uir --no-eff-email --rsa-key-size 4096 --must-staple -n -m [email protected] -d www.meinedomain.tld -d meinedomain.tld

Die Argumente sind für folgende Einstellungen, mindestens E-Mail und Domains müssen angepasst werden:

    • –nginx – Ändern der oben angelegten Konfiguration (wird über den -d meinedomain.tld erkannt)
    • –agree-tos – Erforderlich, Zustimmung zu den Nutzungsbedingungen
    • –redirect – Alle unverschlüsselten Anfragen (über http) werden auf https umgeleitet
    • –hsts – Strict-Transport-Security Header, Browser wird angewiesen für diese Domain immer SSL Verschlüsselung zu nutzen
    • –uir – Content-Security-Policy: upgrade-insecure-requests Header: Ähnlich wie oben, greift aber auch für möglicherweise extern nachgeladene Ressourcen wie googlefonts oder Bilder von anderen Domains als der eigenen
    • –rsa-key-size 4096 – Größe des RSA Keys, Standart wäre 2048
    • –must-staple – OCSP Stapling
    • –no-eff-email – keine Weitergabe der eigenen Mailadresse an die EFF
    • -n – nicht interaktivere Modus
    • -m [email protected] – Angabe empfohlen: schickt u.a. eine Erinnerung bevor die 90 Tage ablaufen
    • -d www.meinedomain.tld – Angabe der Domain für die ein SSL Zertifikat beantragt wird, Subdomains müssen jeweils extra angegeben werden

Je nach Plugin Stand des nginx-python-installers kann es zu Fehlern kommen, sollte aber dennoch das Zertifikat ablegen und die nginx Konfiguration soweit anpassen. Das ergibt dann ein A+ beim ssltest. (Testdurchführung nach Abschluss aller Arbeiten empfohlen!)

Zum jetzigen Stand erscheint unter der Domain noch die Standartausgabe von Nginx, da weder wir noch der certbot einen reload von nginx ausführen. Das ist auch gut so, denn nun können wir in Ruhe die WordPress Konfiguration anpassen und den cronjob für Renew des Zertifikats setzen:

crontab -e

Dann nachfolgende Zeile einfügen, nachdem bei erstmaliger Verwendung noch der Editor festgelegt wurde mit dem man arbeiten möchte.

0 12 * * * /usr/bin/certbot renew --quiet

Nun gehts zur WordPress Konfiguration:

cp /var/www/wordpress/wp-config-sample.php /var/www/wordpress/wp-config.php
nano /var/www/wordpress/wp-config.php

Die relevanten Einträge, analog zu der oben durchgeführten Datenbankerstellung, sehen wie folgt aus:

define('DB_NAME', 'wordpress');

/** MySQL database username */
define('DB_USER', 'press');

/** MySQL database password */
define('DB_PASSWORD', '[email protected]');

Ein Stück weiter unten müssen noch die ‚unique phrases‘ gesetzt werden, das geht am einfachsten über diesen Link

Sieht dann beispielsweise so aus:
Bitte nicht übernehmen, sondern eigene Werte generieren!

define('AUTH_KEY',         '^X+W;[email protected]!r2/z*gEb$4<Ia{fEvac:Ow!u4t&<v[Rjal1$:xs>xz0L25}o<%-+V!'); define('SECURE_AUTH_KEY', '|]-/(pGGg_hDmuV{DnU.Y~S+,c^dG4OiCex4``%&[email protected]|[?K]RrzwFc!'); define('LOGGED_IN_KEY', '!mR=p[~2p[OfK^S>j[jm9J;|vLJndX|ar4IBv|!=kCCgk7mb;> +?+-:]5=)(pmi');
define('NONCE_KEY',        'tXFwwPPUD8(zB)vCfM|64_(Pg)k6n-}c4S),pynVt3g|@y[LV[_(pq DXUI`TRtN');
define('AUTH_SALT',        'AV,3rfjkfJd^nItaxN0g0`GPt*;,0/c&|W?T5Xm][email protected]/A)qvX-yGG&-dc)CV2gn');
define('SECURE_AUTH_SALT', 'i$++?98FKOg1jt-~z-.~p-LpiuOnh=&G`|BRa4>c: j<J$*[email protected]}vcdT8so'); define('LOGGED_IN_SALT', 'yC)K5-6X;@ts)#fWv2F;3J;q&?-r>-)oj6^c$br*QwImBb2b(;NYUgs&)3s-D~u*');
define('NONCE_SALT',       '!J*Ix/[email protected]^{x,Wk_,b4o(OVHc Qo#/S`s{vZkSca6DCf|S7UeYkK)Iy)~Xj');

Nun also noch die default Konfiguration entfernen und nginx neu laden.

rm /etc/nginx/sites-enabled/default
systemctl reload nginx

Nun kann die Seite unter https://www.meinedomain.tld im Browser geöffnet werden um die WordPress Installation zu beenden.
Aufgrund immer wieder sehr flott aggierender Bots sollte nach dem reload und dem Aufrufen der Webseite keine allzugroße Zeitspanne vergehen, sonst „schnappt“ sich eure Seite jemand anders mit einem eigenen Admin Account bzw. über Code Injection. Keine schöne Sache 🙂
Wenn alles funktioniert sollte man dann noch die wp-config-sample.php löschen:

rm /var/www/wordpress/wp-config-sample.php
chmod 400 /var/www/wordpress/wp-config.php

Wenn ihr dann also euren Benutzernamen, Passwort und E-mail Adresse hinterlegt habt seid ihr schon auf dem Admin Panel und könnt loslegen.
Viel Spass!

Schlusswort

Das Thema WordPress ist zu groß um Absicherung, sinnvolle Plugins und Optimierung für Suchmaschinen und Geschwindigkeit hier zu behandeln.

Vielleicht nur soviel: Denkt an ein Impressum und an eine Seite für den Datenschutz, nach deutschem Recht kann man bei einem Blog (fast) immer davon ausgehen, dass man Beides braucht. Z.B. von e-recht24.de

Mit der oben angegebenen Konfiguration für nginx wird jedenfalls weder IP noch sonstige Merkmale der Besucher gespeichert. Weiterhin sollte man auch unter /etc/nginx/nginx.conf access_log off; setzen und logrotate so einstellen, dass die error.log nach einer kurzen Zeitspanne (z.B. 48h) gelöscht werden. Das sieht dann in /etc/logrotate.d/nginx unterer Abschnitt ab ‚postrotate‘ so aus:

        postrotate
                invoke-rc.d nginx rotate >/dev/null 2>&1
                /usr/bin/find /var/log/nginx/ -name "error.log.*" -type f -mtime +0 -exec rm {} \;
        endscript

Anhang

Vollständiger nginx Vhost

Für die bessere Nachvollziehbarkeit hier noch die komplette vhost Datei mit allen SSL optionen, Cloudflare origin pull für diese Seite.

# HTTPS Server
server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name www.sebastian-fritz.net sebastian-fritz.net;

    root /var/www/wordpress;
    index index.php;
    access_log off;
    error_log /var/log/nginx/error.log crit;

    ssl_client_certificate /etc/nginx/ssl/cloudflare.crt; #ONLY for Cloudflare
    ssl_verify_client on; #ONLY 'on' if cloudflare origin ssl pull activated - else: off (!)
    ssl_certificate /etc/letsencrypt/live/www.sebastian-fritz.net/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/www.sebastian-fritz.net/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains";
    client_max_body_size 20M;

    location / {
        try_files $uri $uri/ /index.php;
    }

    location ~ \.php$ {
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass unix:/run/php/php7.2-fpm.sock;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }
    location ~* \.(js|css|png|jpg|jpeg|gif|ico)$ {
        expires max;
        log_not_found off;
    }
    location = /robots.txt {
        allow all;
        log_not_found off;
        access_log off;
    }
    location /doc/ {
        alias /usr/share/doc/;
        autoindex on;
        allow 127.0.0.1;
        deny all;
    }

    location ~/\.ht {
        deny all;
    }
}
server {
    if ($host = sebastian-fritz.net) {
        return 301 https://$host$request_uri;
    } # managed by Certbot


    if ($host = www.sebastian-fritz.net) {
        return 301 https://$host$request_uri;
    } # managed by Certbot


    listen 80;
    listen [::]:80;
    server_name www.sebastian-fritz.net sebastian-fritz.net;
    return 404; # managed by Certbot
}

Fehler gefunden?

Dann hinterlasst mir einen Kommentar oder schreibts per Mail, danke!

Updates:
– 25.05. Fehler WordPress Installation behoben und Berechtigungen setzen verbessert

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.