Es gäbe so viele passende Überschriften für diesen Artikel, Threat Modeling, Linux Hardening, Serverbetrieb in der Cloud u. v. m. Hier soll es um einen Teil der Maßnahmen gehen, die ich privat wie auch geschäftlich anwende.
Es gibt in den Weiten des Internets unzählige von diesen »Hardening Guides« oder auch CIS Benchmarks / STIG’s. Diese haben alle eins gemeinsam: Sie bieten eine Gesamtübersicht, sind aber kein Threat Modeling. Manches ergibt Sinn, manches eben nicht.
Das ist kein Artikel mit einer Copy&Paste Anleitung, sondern soll zum Nachdenken anregen. Jeder sollte für sich selbst überlegen, welches Threat Model er hat und welche Maßnahmen zu ihm passen bzw. er für erforderlich erachtet. Auch auf offensichtliche Dinge wie PermitRootLogin prohibit-password
werde ich nicht eingehen. Anleitungen hierfür gibt es zuhauf im Internet, das sind die absoluten Grundlagen, hier soll es etwas in die Tiefe gehen.
Disclaimer: Dieser Artikel ist klar als Meinung klassifiziert, da er auch Meinung enthält. Es wäre schön wenn in der Kommentarsektion kein Krieg ausbricht, jede Infrastruktur ist verschieden, so auch die (Sicherheits-) Bedürfnisse jedes Einzelnen.
Unsinniges
Als Einstiegsbeispiel eins, das relativ schnell abgefrühstückt ist: Im CIS Benchmark die Auditing-Tipps. Die Infrastruktur gehört keiner Behörde, ich brauch’s nicht. Nicht jede Änderung muss haarklein nachvollziehbar sein. Eine Doku tut’s auch ganz gut. Ganz zur Not kann der erfahrene Admin auch etwas mit dem Output von cat ~/.bash_history
anfangen. Ein Teil den ich überspringe.
Nächstes Beispiel mit Streitpotential: Das ändern des SSH Ports von 22 auf wasauchimmer. Dabei scheiden sich die Geister. Ich gehöre zur Fraktion das ist Hokus-Pokus, aber warum:
- Bringt das Ändern der Standardports zumindest unter Enterprise Linux einen nicht unerheblichen Aufwand mit sich, dank SELinux
- Kommen Bot-automatisierte Bruteforce-Attacken auf allen möglichen, nicht nur den standardisierten, Ports herein
- Das Verschleiern des SSH Ports bringt nur bedingt etwas, nach zwei Tagen ist es öffentlich bei Shodan und Censys einsehbar, da der Port gefunden wurde.
Das einzige Argument dafür ist in meinem Setup auch eins dagegen: Die Logs bleiben sauberer, da die ganz dummen »Port 22-Ausprobier-Bots« ausgesperrt sind. So kann fail2ban aber nicht mehr so viele Spammer-IP’s sammeln – für mich eher ein Nachteil. Durch das strenge Einschränken auf ed25519-sk, bleibt es bei allen auch immer nur beim Verbindungsversuch, ohne Hardware-Key kein Eingang. Als zweite SSH-Absicherungsmaßnahme ist nur ein Login mit Key UND Password zugelassen.
LUKS
Das raten sie alle: Verschlüssle deine Daten! An und für sich gut, doch der Weg den LUKS im Standard geht, begrüße ich nicht. Vor einer Betriebssysteminstallation werden die Partitionen mittels einer gParted Live ISO angepasst und die Platte mit anderen Einstellungen manuell verschlüsselt. Mehr zu meinem Problem mit der Abwärtskompatibilität findet ihr hier.
Das Erstellen von LUKS geschieht in einem Live-System mit: cryptsetup -s 512 -h sha512 -c aes-xts-plain64 -i 20000 --type luks2 --sector-size 4096 --use-urandom --verify-passphrase luksFormat /dev/$DISK
. Streitbar wäre hier die Iteration time, diese wäre völlig übertrieben. Für mich absolut irrelevant, die Hardware gibt es her, in diesem Fall hilft viel wirklich viel. Es ist ein Luxusproblem, ob es nun 3 oder 10 Sekunden zum Entschlüsseln braucht. Jeder Neustart ist eine Downtime, meist geplant. Dann plant man eben eine Minute mehr ein.
Hier geht Sicherheit auf Kosten des Komforts, Neustarts sind nicht so ohne Weiteres möglich. Bei jedem Neustart muss die VNC Konsole des Hosters verfügbar sein, um die Passphrase einzugeben. Es ist nicht mit einem reboot now
getan, es braucht immer ein Computing-Device mit Webbrowser zum “nacharbeiten”.
Cloud und Hypervisor
Google hatte mal das Motto “Don’t be evil.”. Evil unterstelle ich grundsätzlich jeder Firma, alles und jeder möchte unsere Daten oder Geld, mein ewiger Grundsatz “Haben ist besser als brauchen, Vorsorge ist besser als Nachsorge”. Viele Hosting Provider raten einem dazu einen Guest Agent (open-vm-tools
für VMware, qemu-guest-agent
für KVM) zu installieren. Viele folgen dieser Empfehlung blind. Doch was ist dieses Guest-Agent Zeugs eigentlich?
VM’s sind erstmal »getrennt« vom Hypervisor. In manchen Situationen ist es sinnig, dass die VM dem Host etwas mitteilen kann oder auch umgekehrt. Unter anderem dafür sind diese Guest Agents. Doch was haben diese Agents noch alles für Möglichkeiten? Ich kann vom Hypervisor Befehle in der VM ausführen, generell gebe ich dem Host und seinen Admins weitreichende Rechte in meiner VM. Soweit zur einfachen Erklärung. Zumindest der qemu-guest-agent
bietet die Möglichkeit, bestimmte Funktionen zu blacklisten und somit zu sperren. Das passiert im Gast selbst, das kann das Wirtssystem nicht beeinflussen oder verhindern. Nach viel trial and error ist das bei mir: BLACKLIST_RPC=guest-sync-delimited,guest-sync,guest-ping,guest-get-time,guest-set-time,guest-info,guest-shutdown,guest-file-write,guest-get-disks,guest-get-fsinfo,guest-set-user-password,guest-get-memory-blocks,guest-set-memory-blocks,guest-exec,guest-get-devices,guest-ssh-get-authorized-keys,guest-ssh-add-authorized-keys,guest-ssh-remove-authorized-keys,guest-file-open,guest-file-close,guest-file-read,guest-file-seek,guest-file-flush,guest-exec-status
Wer sich das durchgelesen hat, wird nun verstehen was der Host potentiell tun könnte. An dieser Stelle eine Warnung: Das geht mit wesentlichen Abstrichen einher! Es ist z.B. keine Live-Migration der VM mehr möglich. Ob es das Wert ist, muss jeder für sich selbst entscheiden, mein persönliches Sicherheitsbedürfnis überwiegt hier der Uptime. Auch manch cloud-init Script oder die Root-Passwort-vergessen Funktion des Hosters, wird so nicht mehr funktionieren. Eure Testumgebung freut sich auf rege Nutzung.
Zwischenfazit LUKS und Guest-Agent
Die Kombination aus beiden o.g. Maßnahmen ist für mein persönliches Sicherheitsbedürfnis absolut erforderlich, um etwas »in der Cloud« oder auf fremden Hosts zu betreiben. Die Daten sind zum einen vollständig verschlüsselt, zum anderen kann der Host mir nichts tun. Es muss nicht sein, dass der Host oder sein Admin böswillig irgendetwas tun, Fehler durch menschliches Versagen anderer möchte ich nicht auf meinen Systemen.
Jeder muss am Ende des Tages selbst entscheiden ob er seinem Anbieter vertraut – bzw. welchen er wählt. Etwas nachzuhelfen kann ja nicht schaden. Ebenfalls muss jedem bewusst sein, dass auch diese Maßnahmen keinen 100%igen Schutz bieten. Gegen gezielte Angriffe mit Zugriff auf bzw. über den Host ist so einiges möglich. Wenn jemand wirklich etwas wissen möchte, sollte er es nicht all zu leicht haben.
Webserver oder Applikationsserver
Es liegt in der Natur eines Webservers das er in irgendeiner Form erreichbar ist, sei es im Internet oder im LAN. Sobald ein System erreichbar ist, ist es ein potentielles Angriffsziel, einen Webserver air-gapped zu betreiben ergibt überhaupt keinen Sinn. Beruflich platziere ich die Webserver in einer DMZ. Sie bekommen keinen Zugriff nach innen, keine einzige Firewallregel erlaubt einen Zugriff. Sollte ein sich in der DMZ befindender Server Zugriff auf irgendeine interne Datenbank benötigen, Error by Design. Nicht alles programmiert man selbst, sondern muss sich an die Vorgaben des Softwareherstellers halten, schon klar. So auch bei einem unserer Firmensysteme. Dafür wurde ein Weg drumherum gefunden:
Von DMZ ins LAN ist verboten – LAN in DMZ aber nicht. Der interne DB-Server pusht ein Teil seiner Tables alle 10 Minuten per Script in den Webserver, der zugleich auch Datenbankserver für sich selbst ist. Bei uns ist es nicht zwingend erforderlich, die Daten in Echtzeit extern verfügbar zu haben. So müssen keine Löcher in die Firewall geschlagen werden. DSGVO-konform ist es auch noch, es werden nur bestimmte Tabellen gepusht – es kann gesteuert werden, was das Haus verlässt, in diesem Fall keine persönlichen Daten. Man mag sich nicht ausmalen, wenn der Webserver gekapert wird und Vollzugriff, oder auch nur Read-only, auf die internen Systeme hat…
Bei LinuxNews sieht es ganz anders aus. Im Grunde ist es eine Hoster-VM von vielen, ohne Hardware-Firewall davor. Der Server ist auf sich alleine gestellt – keine DMZ notwendig. Selbst wenn der Host gekapert wird, ist der Rest meiner Infrastruktur sicher – es werden keine Änderungen von extern erreichbaren Systemen angenommen, z.B. Passwortänderung zurück ans RedHat IDM. Wenn’s soweit kommt, jo wird neu installiert. Was möchte er hier? Die WordPress Datenbank mit unseren Artikeln? Die Artikelbilder? Nimm und lass mich in Ruh’, geh wo anders spielen.
Anders sieht es bei meiner privaten Nextcloud aus. Auch die ist eine Hoster-VM von vielen. Die Angriffsfläche ist eine deutlich andere. Grundlegend, ja ist ein Webserver, einzelne Punkte überschneiden sich. Die Nextcloud ist an ein RedHat IDM und KeyCloak SSO angebunden. Viel “tragischer” ist die Tatsache, dass darin auch meine KeePassXC Datenbank liegt und durch die Gegend gesynct wird. Zwei nicht zu unterschätzende Punkte, beide könnten meinen “Root of Trust” infiltrieren. Mehrere Maßnahmen sorgen dafür, dass das nicht passiert. Änderungen von der Nextcloud-VM an das IDM zurück funktionieren nicht – nicht mal eine einfache Passwortänderung. Diese muss auf einem separaten Self-Service Portal durchgeführt werden. Das wird auf einem Host hinter HW-Firewall ausgeführt, IP’s für alle Länder außer Deutschland gesperrt. Das reduziert die Wahrscheinlichkeit für dieses Szenario deutlich. Ein Timer in der Firewall erledigt den Rest, Port 443 ist nicht dauerhaft offen.
Als zweites kommt eine Eigenentwicklung zum Tragen: Loggt sich ein spezieller User NICHT alle $zeit auf dem Server erfolgreich per SSH ein, führt der Server ein cryptsetup luksErase
aus und fährt herunter. Bei LuksErase ist der Name Programm, der LUKS Header wird zerstört, kein Entschlüsseln mehr möglich, nicht mal mit der richtigen Passphrase. Header Backups bewahre ich nicht auf. Das produziert einen nicht unerheblichen Aufwand sich immer auf allen Systemen “sonder-einloggen” zu müssen – wie oben Erwähnt geht Sicherheit immer zu Lasten des Komfort. Im Falle von Krankheit ohne PC Zugang (äußerst unwahrscheinlich) übernimmt dies eine Vertrauensperson automatisch, sobald wir nicht mehr täglich in Kontakt stehen. Das auch nur so lange, bis ich die Radischen von untern anschaue, das sonder-einloggen hört auf, der Server geht mit den Daten unter.
Wer nun denkt, das kenn ich doch irgendwoher, dann kann ich das bestätigen. Unter Kali Linux gibt es ein Paket namens, cryptsetup-nuke-password
, das in die initramfs gegossen wird. Das zerstört den LUKS-Header bei Eingabe eines zweiten, zuvor definierten, Kennworts. Ich mag eben den Weg über Bordmittel/Scripts, die Idee kam durch dieses Paket (unter Enterprise Linux nicht verfügbar). Das Endergebnis ist ähnlich, der Weg zum Ziel ein anderer.
NGINX
SQL Injections auf große WordPress-Websites sind keine Seltenheit, auch wir sind keine Ausnahme. Ein Beispiel:
Blocked for SQL Injection in query string: profiletab = posts UNION ALL SELECT NULL,NULL,NULL-- ZalI
Das ist hier bald täglich hereingekommen. Es nervt einfach nur, dafür kann das NGINX filtern:
## Block SQL injections
set $block_sql_injections 0;
if ($query_string ~ "union.*select.*\(") {
set $block_sql_injections 1;
}
if ($query_string ~ "union.*all.*select.*") {
set $block_sql_injections 1;
}
if ($query_string ~ "concat.*\(") {
set $block_sql_injections 1;
}
if ($block_sql_injections = 1) {
return 403;
}
## Block file injections
set $block_file_injections 0;
if ($query_string ~ "[a-zA-Z0-9_]=http://") {
set $block_file_injections 1;
}
if ($query_string ~ "[a-zA-Z0-9_]=(\.\.//?)+") {
set $block_file_injections 1;
}
if ($query_string ~ "[a-zA-Z0-9_]=/([a-z0-9_.]//?)+") {
set $block_file_injections 1;
}
if ($block_file_injections = 1) {
return 403;
}
## Block common exploits
set $block_common_exploits 0;
if ($query_string ~ "(<|%3C).*script.*(>|%3E)") {
set $block_common_exploits 1;
}
if ($query_string ~ "GLOBALS(=|\[|\%[0-9A-Z]{0,2})") {
set $block_common_exploits 1;
}
if ($query_string ~ "_REQUEST(=|\[|\%[0-9A-Z]{0,2})") {
set $block_common_exploits 1;
}
if ($query_string ~ "proc/self/environ") {
set $block_common_exploits 1;
}
if ($query_string ~ "mosConfig_[a-zA-Z_]{1,21}(=|\%3D)") {
set $block_common_exploits 1;
}
if ($query_string ~ "base64_(en|de)code\(.*\)") {
set $block_common_exploits 1;
}
if ($block_common_exploits = 1) {
return 403;
}
## Block spam
set $block_spam 0;
if ($query_string ~ "\b(ultram|unicauca|valium|viagra|vicodin|xanax|ypxaieo)\b") {
set $block_spam 1;
}
if ($query_string ~ "\b(erections|hoodia|huronriveracres|impotence|levitra|libido)\b") {
set $block_spam 1;
}
if ($query_string ~ "\b(ambien|blue\spill|cialis|cocaine|ejaculation|erectile)\b") {
set $block_spam 1;
}
if ($query_string ~ "\b(lipitor|phentermin|pro[sz]ac|sandyauer|tramadol|troyhamby)\b") {
set $block_spam 1;
}
if ($block_spam = 1) {
return 403;
}
Funktioniert wie eine Fußmatte, hält den groben Schmutz draußen. Gegen gezielte Angriffe mag das möglicherweise nicht helfen, das ist aber auch nicht das Schutzziel. Für dumme Bot’s reichts, zumal das WordPress Table Prefix bei uns verändert ist. Bis das mal erraten ist…
PHP
Bei den php-fpm Pools ist es relativ einfach gehalten. Jede Seite hat ihren eigenen Pool. Zusätzlich wurde in php die blacklist_functions
aktiviert:
exec,passthru,shell_exec,system,proc_open,popen,curl_exec,curl_multi_exec,parse_ini_file,show_source
Die Auflistung kann sicherlich noch verfeinert werden, für den alltäglichen Betrieb reicht es.
nftables
Firewalls sind bei jeglicher Art von Server unabdingbar. Sei es on-Host oder als Hardware-Appliance davor. Bei on-Host-Firewalls setze ich auf reines nftables, selbst firewalld wird deinstalliert. Einen schicken Wrapper der nicht alle Funktionen bietet, brauche ich nicht. Eine nftables.conf könnte z.B. so aussehen:
#!/usr/local/sbin/nft -f
flush ruleset
include "/etc/nftables/*.nft"
table inet nftables_svc {
set allowed_protocols {
type inet_proto
elements = { icmp, icmpv6 }
}
set allowed_interfaces {
type ifname
elements = { "lo" }
}
set allowed_tcp_dports {
type inet_service
elements = { ssh, ftp, http, https }
}
set honeypot_tcp_dports {
type inet_service
elements = { smtp, msrdp, mssql }
}
define icmpv6_allowed_types = {
destination-unreachable, packet-too-big, time-exceeded, echo-request, parameter-problem, echo-reply, nd-router-solicit, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert, ind-neighbor-solicit, ind-neighbor-advert
}
define icmp_allowed_types = {
destination-unreachable, time-exceeded, parameter-problem, router-solicitation, router-advertisement
}
counter input_icmpv4_ping {}
counter input_traceroute {}
chain input {
type filter hook input priority 0; policy drop;
ct state invalid drop
iif != lo ip daddr 127.0.0.1/8 drop
iif != lo ip6 daddr ::1/128 drop
ct state established,related accept
ip protocol icmp icmp type $icmp_allowed_types accept
ip6 nexthdr icmpv6 icmpv6 type $icmpv6_allowed_types accept
ip protocol icmp icmp type { echo-reply, echo-request } limit rate 250/second counter name input_icmpv4_ping accept
udp dport 33434-33524 limit rate 50/second counter name input_traceroute accept
iifname @allowed_interfaces accept
tcp dport @allowed_tcp_dports accept
ip protocol tcp reject with tcp reset
ip6 nexthdr tcp reject with tcp reset
}
chain forward {
type filter hook forward priority 0; policy accept;
reject with icmpx type host-unreachable
}
chain output {
type filter hook output priority 0; policy accept;
oifname @allowed_interfaces accept
}
}
fail2ban
fail2ban ist unerlässlich, zumindest für Server die keine Hardware Firewall davor haben. Neben SSH kann es verschiedenste Dienste schützen wie WordPress, Bookstack, Mattermost uvm.
Auf dem Host selbst
Da hilft nur noch sudo, sonst nichts. Wer es schafft, trotz aller Maßnahmen, den Host zu infiltrieren, Hut ab – du hast es verdient Spaß zu haben. Etwas DROP DATABASE
hier, etwas rm -rf --no-preserve-root /
da – dann kommt das Backup.
Threat-Modelling im allgemeinen
Ein Thema in dem man Wochen versenken kann. Für den Anfang gebe ich mal eine Linksammlung an die Hand:
Am Ende des Tages sollte man es mit seinen Systemen so halten, wie bei einem alkoholischen Abend mit den Jungs/Mädels/Diversen: Nur Goethe war Dichter 😆