Hier zeige ich Euch, wir Ihr Wireguard-UI und den Traefik-Crowdsec-Stack nutzt, um Dienste zur Verfügung zu stellen, die extern (Über die offizielle IP) oder über intern (über VPN oder von einer VM) genutzt werden können.
Dienste wie Traefik Dashboard oder Portainer, möchte man ja nicht so gerne “im Internet” zur Verfügung stellen. Hier bietet sich die Unterteilung in “interne” und “externe” Dienste an. Mit extern sind Dienste gemeint, die aus dem Internet erreichbar sind und mit intern welche, die nur über VPN erreichbar sind.
Voraussetzung für diese Installation sind die bereits erfolgte Umsetzung der Artikel:
und
Bei mir habe ich die Rechte der Wireguard-Containers eingeschränkt, so dass dieser keine NET-Capability usw. haben muss, sondern nur normale Container-Rechte. Den Neustart von WireGuard macht ich von Hand (mit wg-quick down und up). Für diese Installation sollte dies aber keinen Unterschied machen.
Um erst einmal einen Überblick zu bekommen, beschreibe ich die IP-Adressen/-Bereiche, abstrakt und dann an einem konkreten Beispiel.
0. Versionshistorie
Datum | Änderungen |
---|---|
03.12.2023 | Erstellung dieser Anleitung |
1.abstrakte Beschreibung
1.1 VPN-Definition
VPN-Bereich zur Einwahl der Clients: 192.168.196.0/24
Interface am Server: wg1
(kann natürlich auch wg0 sein. Bei mir ist wg0 eine Punkt-zu-Punkt-Verbindung der verschiedenen bare metal Server)
IP-Adresse des Servers: 192.168.196.1/24
Beispiel-IP Adresse eines Clients: 192.168.196.2/32
Der Client hat als Einstellung sein eigenes Netzwerk, welches durch durch das VPN geroutet werden. (192.168.196.0/24 – also NICHT 0.0.0.0/0).
Wenn Ihr pihole installiert habt, könnt Ihr “USE-DNS” in der Wireguard-Verbindung anmachen.
1.2 EntryPoints am Traefik:
80: http-extern (=web)
443: https-extern (=websecure)
81: http-intern (-web-intern)
444: https-intern (websecure-intern)
Hier lasse ich die middle-ware traefic-crowdsec-bouncer@file weg, damit ich nicht ausgesperrt werden kann. Zusätzlich verhindere ich das Aussperren durch den traefic-crowdsec-bouncer durch eine iptables-Regel.
Grundsätzliches zum Verständnis des traefik-crowdsec-bouncers / crowdsec-iptables-bouncer / crowdsec.
Crowdsec erhält von der “Zentrale” eine Liste mit -IP-Adressen und crowdsec fügt dieser Liste durch Parsen der Logs einzelne IP-Adressen hinzu. Dieser Vorgang findet sich in den Decisions.
Diese lassen sich so ermitteln:
docker exec cscli decisions list
Jetzt holen sich der traefik-bouncer und der iptables-bouncer diese IP-Adress-Liste ab.
Wenn ich jetzt HTTP-Datenverkehr habe und vom traefik-bouncer geblockt werde, dann erhalte ich ein “Forbidden” in einer sonst leeren Seite
Schwieriger ist es, wenn mich der iptables-bouncer blockt. Dann ist Schweigen im Walde und nichts geht mehr.
Frage: Wie ist der iptables bouncer implementiert?
Es gibt ein IP-Set, welches alle gesperrten IPv4 und Ipv6-Adressen enthält. Diese findet sich in der ersten Regeln in der chain INPUT mit dem Namen crowdsec-blacklists
iptables -L INPUT -vn
2836 168K DROP 0 -- * * 0.0.0.0/0 0.0.0.0/0 match-set crowdsec-blacklists src
Ihr könnt euch auch die CrowdSec Blacklist ausgeben lassen:
ipset list crowdsec-blacklists
zeigt viele IP-Adressen wie: (Der Timeout zeigt, wie lange dieser Eintrag noch gültig ist.)
146.70.181.246 timeout 460415 74.116.129.188 timeout 547570 188.40.137.158 timeout 460415 59.152.58.67 timeout 568330 203.56.183.68 timeout 599933
Will ich mich jetzt vor diese Blockliste “mogeln”, dann hilft folgendes snippet:
In der ersten Zeile ermittle ich die offizielle IP-Adresse des Servers (IP)
Dann kommt meine Source-IP-Adresse von der ich per SSH auf den Server zugreife. (SIP)
Jetzt wird geschaut, ob diese IP-Adresse schon “freigeschaltet ist”
Wenn nicht, dann wird Sie an Position 1 vor die Blacklist eingetragen.
IP=$( curl -4 ifconfig.io 2> /dev/null ) SIP=$( echo $SSH_CLIENT | awk '{ print $1 }' ) iptables -L -vn | grep $SIP | grep $IP > /dev/null if [ "$?" == "0" ] ; then # echo Regel schon da else iptables -I INPUT 1 -s $SIP -d $IP -j ACCEPT
Ich habe das Snippet bei mir an die ~/.bashrc angehängt. Jedesmal, wenn ich mich per ssh verbinde (auch von einem anderen Server) trägt das Snippet die Block-Verhinderungsregel an den Beginn der INPUT chain ein. Also vor das Block-IPSET von crowdsec. Also vor dem IPTABLES-Bouncer bin ich schon mal sicher. Jetzt kommt der traefik-bouncer.
Dieses snippet fügt als Regel 1 in die Chain INPUT folgende Regel ein:
source=IP-Adresse, die sich in der ENV-Variable SSH-Client befindet (meine offizielle Home-IP)
dest=offizielle IP-Adresse des Servers
Die INPUT chain sieht jetzt bspw. so aus:
iptables -L INPUT -vn
Chain INPUT (policy ACCEPT 113K packets, 531M bytes) pkts bytes target prot opt in out source destination 42559 2173K ACCEPT 0 -- * * 45.84.139.252 95.216.9.232 2923 173K DROP 0 -- * * 0.0.0.0/0 0.0.0.0/0 match-set crowdsec-blacklists src
Mit diesem Code kann ich ermitteln, ob ich von crowdsec lokal oder global gesperrt wurde.
ipset list crowdsec-blacklists | grep $SIP if [ "$?" == "0" ] ; then echo ich bin auf der crowdsec-blacklists gelandet! fi docker exec crowdsec cscli decisions | grep $SIP if [ "$?" == "0" ] ; then echo "crowdsec hat mich durch Log-Analyse gesperrt." fi
Hier muss natürlich etwas vernünftiges rein, bspw:
docker exec crowdsec cscli decisions delete $SIP
Jetzt bin ich also davor geschützt, dass mich crowdsec blockt, kann mich also dran machen, die Firewall-Regeln zu erstellen. Im Prinzip “biege” ich den Port 80 auf 81 und 443 auf 444 um.
1.3 Einträge in die IPTABLES-Firewall.
iptables -t nat -I PREROUTING 1 -i wg1 -p tcp --dport 80 --to-destination 192.168.196.1:81 -j DNAT iptables -t nat -I PREROUTING 1 -i wg1 -p tcp --dport 443 --to-destination 192.168.196.1:444 -j DNAT
Also gegeben sei die IP-Adresse des Docker / VPN Servers: 192.168.196.1
Was machen wir hier genau?
Wenn etwas über das Interface wg1 kommt (also VPN), dann übersetze den Port 80 auf 81 und 443 auf 444. Das bedeutet aller VPN-Verkehr wird auf den “VPN-Entry-Point” umgeleitet. Es wird auch ausschließlich Verkehr auf die 192.168.196.1 übersetzt. Was anderes kommt in der PREROUTING CHAIN nicht an.
Denkt dran, dass bei euch wahrscheinlich das Interface wg0 heißt.
2 konkrete Umsetzung
In der nachfolgenden Datei werden die entrypoints definiert.
nano /opt/containers/traefik-crowdsec-stack/traefik/traefik.yml
2.1 bisherige Einträge in der static configuration:
Bisher wurden ja in der /opt/containers/traefik-crowdsec-stack/traefik/traefik.yml die entryPoints 80 (web) und 443 (websecure) definiert, so wie deren Middlware.
entryPoints: web: address: ':80' http: redirections: entryPoint: to: websecure scheme: https middlewares: - traefik-crowdsec-bouncer@file websecure: address: ':443' http: middlewares: - traefik-crowdsec-bouncer@file proxyProtocol: trustedIPs: - 10.0.0.0/8 - 172.16.0.0/12 - 192.168.0.0/16 forwardedHeaders: trustedIPs: - 10.0.0.0/8 - 172.16.0.0/12 - 192.168.0.0/16
2.2 neue Einträge:
Als “internen” Einträge kommen jetzt die Ports 81 (web-intern) und 444 (websecure-intern) dazu. Damit sind Entry-Points gemeint, die nur über das VPN erreichbar sein sollen. Natürlich will keiner in den Browser https://url:444 eingeben. Wie wir das realisieren, kommt etwas später.
entryPoints: web-intern: address: ':81' http: redirections: entryPoint: to: websecure-intern scheme: https middlewares: - ip-whitelists@file websecure-intern: address: ':444' http: middlewares: - ip-whitelists@file proxyProtocol: trustedIPs: - 10.0.0.0/8 - 172.16.0.0/12 - 192.168.0.0/16 forwardedHeaders: trustedIPs: - 10.0.0.0/8 - 172.16.0.0/12 - 192.168.0.0/16
2.3 Variante A: Umsetzung mit IPTABLES
Das erste Script findet die Offizielle IP-Adresse ($OFFIP) und die VPN-IP-Adresse ($VPNIP) des Servers heraus. Dann werden auf dem Wireguard-Interface (hier wg0) zwei Regeln eingetragen, die den Port 80 auf 81 und 443 auf 444 übertsetzen.
Will ich also auf https://192.168.196.1 zugreifen greift die folgende Regel:
1. Regel: iptables -t nat (Verändere in der Tabelle nat = network adress translation) -I PREROUTING 1 (in der PREROUTING CHAIN an Position 1 - also vor den ganzen Docker-Einträgen) -i wg0 (auf dem Wireguard-Interface) -p tcp --dport 443 (mit dem TCP-Protokoll auf dem Port 443 - also https://) --to-destination 192.168.196.1:444 (auf die Destination 192.168.196.1 und Port 444) -j DNAT (führe DNAT durch) 2. Regel: iptables -I INPUT 1 (Füge an Position eine Regel für eingehenden Verkehr ein = CHAIN INPUT) -d $OFFIP (Ziel ist die offizielle IP-Adresse) -p tcp --dport 444 (auf den Port 444) -j REJECT (lehne den Zugriff ab!)
Der Zugriff wird aber nur auf das Ziel der offiziellen IP-Adresse abgelehnt. Alle internen Zugriffe würden weiterhin funktionieren.
WGINT=wg0 OFFIP=$( curl ifconfig.io ) VPNIP=$/ ip a | grep $WGINT | grep inet | awk '{ print $2 }' | cut -f1 -d"/" ) iptables -t nat -I PREROUTING 1 -i $WGINT -p tcp --dport 80 --to-destination $VPNIP:81 -j DNAT iptables -t nat -I PREROUTING 1 -i $WGINT -p tcp --dport 443 --to-destination $VPNIP:444 -j DNAT iptables -I INPUT 1 -d $OFFIP -p tcp --dport 81 -j REJECT iptables -I INPUT 1 -d $OFFIP -p tcp --dport 444 -j REJECT
2.4 Variante B: Umsetzung mit IPTABLES und middleware ip-whitelists
Bei der Variante B: sind die ersten beiden Regeln gleich wie in Variante A (NAT auf dem VPN Interface).
WGINT=wg0 VPNIP=$/ ip a | grep $WGINT | grep inet | awk '{ print $2 }' | cut -f1 -d"/" ) iptables -t nat -I PREROUTING 1 -i $WGINT -p tcp --dport 80 --to-destination $VPNIP:81 -j DNAT iptables -t nat -I PREROUTING 1 -i $WGINT -p tcp --dport 443 --to-destination $VPNIP:444 -j DNAT
In der statischen Konfiguraton (traefik.yaml) sieht man eine weitere middleware: – ipwhitelists@file, welche sich bei den EntryPoints 81 und 444 befindet.
Als letztes brauchen wir Einträge zur Definition der middleware ip-whitelists in der dynamic_conf.yml. (Hier unser VPN-Netzwerk)
ip-whitelist: ipwhitelist: sourceRange: - "127.0.0.1/8" - "192.168.196.0/24"
Diese Whitelist definiert die erlaubten IP.Adressen auf einem EntryPoint. Eleganter finde ich aber die label-Variante, aber diese scheint sich mit dem Label traefik.http.routers.wordpress2.middlewares=default@file
nicht zu verstehen.
labels: - "traefik.http.middlewares.ip-whitelist.ipwhitelist.sourcerange=127.0.0.1/32,192.168.196.0/24"
Zuletzt definieren wir die labels am Container:
Hier bei einem Beispiel für einen externen Container bleibt alles gleich:
- "traefik.enable=true" - "traefik.http.routers.wordpress1.entrypoints=websecure" - "traefik.http.routers.wordpress1.rule=Host(`webseite1.euredomain.de`)" - "traefik.http.routers.wordpress1.middlewares=default@file" - "traefik.http.routers.wordpress1.tls=true" - "traefik.http.routers.wordpress1.tls.certresolver=http_resolver" - "traefik.http.routers.wordpress1.service=wordpress1" - "traefik.http.services.wordpress1.loadbalancer.server.port=80" - "traefik.docker.network=proxy"
Bei einem internen Container wird bei middlewares die ip-whitelists@file und der entrypoint websecure-intern eingetragen werden. Dies stellt sicher, dass dieser Dienst nur über VPN erreichbar ist.
- "traefik.enable=true" - "traefik.http.routers.wordpress2.entrypoints=websecure-intern" - "traefik.http.routers.wordpress2.rule=Host(`intranet2.euredomain.de`)" - "traefik.http.routers.wordpress2.middlewares=default@file,ip-whitelist@file" - "traefik.http.routers.wordpress2.tls=true" - "traefik.http.routers.wordpress2.tls.certresolver=http_resolver" - "traefik.http.routers.wordpress2.service=wordpress2" - "traefik.http.services.wordpress2.loadbalancer.server.port=80" - "traefik.docker.network=proxy"
Das sehr schöne an dieser Methode ist. dass wir hier auch intern ein let’s encrypt Zerifikat benutzen können, da dies nur einmal für den externen Zugriff freigeschaltet sein muss, damit sich Traefik das Zertifikat zieht. Da dieses Zertifikat ein Jahr Gültigkeit hat, hat man erst mal Ruhe. Die Container müssen dann zum “Geburtstag” mal vor die Tür.
Wenn das nicht geht kann man immer noch per certbot ein wildcard Zertifikat besorgen und hinterlegen. Dieses lässt sich dann so einbinden:
tls: certResolver: http_resolver domains: - main: dev.domain.ca sans: - "dev.domain.ca" - "*.dev.domain.ca"
Hier wurde das genauer beschrieben. https://goneuland.de/traefik-2-wildcard-lets-encrypt-zertifikate-verwenden/
Den Catch-All Redirect von http auf https auch auf den internen Entrypoints haben wir ja bereits in der Entry-Point-Definition erledigt.
Hier noch ein Bild auf das Traefik-Dashboard mit den neuen Entrypoints: Der Entrypoint 8080 ist das Traefik-Dashboard und der auf 88 ist der Healthcheck vom Crowdsec. 80 und 443 sind die bekannten und 81 und 444 die neu hinzugefügten.
Oben die Einträge direkt am Container. Man achte auf die Benutzung des Entrypoints websecure-internal. Auch braucht man natürlich einen Hostnamen, der auf eine VPN-IP-Adresse zeigt. Also wie hier der Name portainer.internal zeigt im DNS auf die 192.168.196.1. Dort kann ich verständlicherweise nicht auf externe Namen verweisen, weil diese ja auf offizielle IP-Adressen zeigen. Realisiert habe ich das über eine Installation von PIHole, dnsmasq und unbound. Zu PIHole, gibt es diesen Beitrag:
https://goneuland.de/pi-hole-mit-docker-compose-und-traefik-installieren/
Möglich wäre auch die Benutzung von dienst.intern.domain.de. Hier könnte man auch intern ein Zertifikat verwenden, welches offiziell auch sauber signiert ist. Hier muss man mit den Wildcard-Zertifikat aufpassen, weil man kann sich nicht gleichzeitig verschiedene Tiefen der Wildcard verwenden. Also *.intern.domain.de würde gehen, aber *.intern.domain.de und *.domain.de nicht.
Zugegeben die Konfiguration ist durch die Kombination von Firewall-Regeln und Traefik-Konfigurationen etwas unübersichtlich, aber das Ergebnis lohnt sich.
Ich kann mich nach wie vor nicht so wirklich mit Wireguard anfreunden, da es keine Möglichkeit gibt, die Verbindungseinstellungen irgendwie mit einem Passwort zu schützen. Bislang verwende ich einen RPi, auf dem ein OpenVPN-Server läuft, der eine direkte Portfreigabe an der Fritzbox hat. Da muss ich bei jedem Start des VPN von Hand das Passwort des Benutzers eingeben, der die Verbindung herstellen soll. Das scheint im ersten Moment ein wenig lästig, aber man gewöhnt sich dran und mir kommt das dann ein wenig sicherer vor, keine Ahnung, ob das tatsächlich so ist. Mein Gedanke war einfach nur, dass selbst wenn jetzt jemand aus welchen Gründen auch immer mein Laptop in die Finger bekommt, das halt gerade nicht gesperrt ist, dann hat derjenige nicht direkt auch noch SMB-Zugriff auf meine gesamten privaten Dokumente zu Hause, sondern lediglich auf das, was ich synchonisiert habe, was halt mein Unikram, aber eben nicht Finanzdokumente, Wiederherstellungsschlüssel und z.B. das Paperless-Dokumentenverzeichnis ist.
Gibt es irgendeine gute Begründung, warum ich zu Wireguard wechseln sollte unabhängig von dieser Anleitung hier?
Und wenn nicht, wäre es möglich, das hier auch mit OpenVPN zu implementieren, vielleicht nur etwas nerviger?
Ich hab versucht die IP-Tables einzutragen, aber bei mir kommt immer der Fehler das er die Option “–to-destination” nicht kennt. Dr. Google hat mir auch nicht weiterhelfen können, außer das es wohl immer mal wieder diese fehlermeldung auspuckt …
Der Fehler steckt im snippet das -j DNAT muss vor –to-destination
iptables -t nat -I PREROUTING 1 -i wg0 -p tcp –dport 80 -j DNAT –to-destination IP:81
Ansonsten finde ich die Anleitung nicht ganz so gut, wie die anderen Anleitungen. Manchmal wird mir nicht ganz klar, wo ich welches Snippet einfügen muss oder an welcher stelle ich eine .yml erweitere.
Das wäre es schön, für die uebersicht, wenn man einfach die komplette datei aus den verlinkten beiträgen einfliessen lässt .. oder mindestens mal 5 zeilen drüber und drunter 🙂 Das macht sowas übersichtlichter und hilft beim verständnis.
Auch ist mir nicht ganz klar, wo soll ich den die Scirpte bei z.B. 2.3 hinwerfen? Was soll ich damit machen?
Ich habe in der Doku zu Routern gelesen, dass diese auch basierend auf der ClientIP eingerichtet werden können.
Wäre das dann nicht die einfachste Variante um zu sagen, welcher Dienst nur von der lokalen IP (= hinter dem VPN) erreichbar sein soll?
Gute Idee!
Ich habe mein traffik und VPN webgui zwar von außen erreichbar gemacht, aber mit authelia geschützt und authelia wird von crowdsec überwacht.
Das lasse ich erstmal so stehen, ich habe ein gutes Gefühl dabei 🙂
Oder was meint ihr?