Challenge und Szenario: Wir haben in diesem Szenario unantastbare webbasierte Legacy Systeme vorliegen, die nur http auth unterstützen. Diese Sollten 2FA tauglich werden. User-Credentials oder die Dienste selbst können und dürfen nicht angepasst werden. Es muss ein 2FA Proxy dazwischen. Aber Authelia oder Keycloak sind zu komplex. Alles was wir an Werkzeug haben, ist ein Standard Apache2 Webserver mit Standardmodulen. Darauf ist eine KISS basierte Lösung zu realisieren. Wichtige Zusatzanforderung: Die gültigen TOTP Tokens oder deren shared secret sollen auf dem betroffenen Service/Server niemals im Klartext in Dateien liegen, sondern werden nur als Hash von einem zentralen Service bereitgestellt. Der Service darf nur diese Hashes kennen und vergleichen.
## TOTP mit Legacy Systemen, die nur HTTP BASIC AUTH unterstützen
## (Server und Client können nur HTTP BASIC AUTH) - dennoch TOTP in the loop
## Tokens sind dabei aber 8h gültig, also eine lange "Session", obwohl
## ein HTTP AUTH sessionlos ist und im jeden Request die Info trägt.
## TOTP Tokens können dabei jederzeit revoked und gesperrt werden, indem
## man eine einfache Datei löscht.
## Der Trick: wir stellen dem Usernamen die jeweilige TOTP TAN voran,
## schneiden die dann aber nach Prüfung wieder weg
## und geben den restlichen Username weiter. Das tun wir auf der
## Serverseite mit einem standard Apache.
## a2enmod proxy
## a2enmod headers
## a2enmod rewrite
## a2enmod security2
## mkdir /etc/totp
## chown root:root /etc/totp
## chmod u=rwx,g=rwx,o=rx /etc/totp/
## Geprüft wird für den TOTP check also auf der
## Apache Seite nur, ob im System als Dateiname im Verzeichnis
## /etc/totp/ eine Datei existiert, die dem SHA1 Hash der jeweiligen
## kombination auf TOTP PIN und dem usernamen entspricht,
## z.B: ++12345678++oldusername existiert, also auch TAN und altem usernamen
## im Beispielfall also KysxMjM0NTY3OCsrb2xkdXNlcm5hbWU6ZXJic2Vu als base64
## encodierter HTTP Basic auth mit dem Beispiel-Passwort: erbsen.
## Du kannst das testweise so erzeugen
## echo -n ++12345678++oldusername:erbsen | base64
## Unsere TAN muss nummerisch sein, damit nie ein / oder + oder =
## als character im Dateinamen auftaucht, wenn es base64 encodiert wird.
## außerdem manteln wir die 8 stellige TAN mit zwei Plus-Zeichen ++
## um sie bereits im base64 encodierten String zu erkennen, da sie dann
## immer mit "Kys" beginnt und unter der Annahme, dass bisherige usernames
## nie mit einem + begannen.
## Beispiel, um ein Token PIN 12345678 zu aktivieren ist also diese Shell Pipeline
## echo -n ++12345678++oldusername | sha1sum | tr -dc "0-9a-f" | sed "s,^,touch /etc/totp/,g" | sh
## womit die Datei /etc/totp/b48d8430d89c79c4ba0c371d9aac85fe320255e4 angelegt wird.
## Beispiel um veraltete 8h Tokens zu löschen geht via
## find /etc/totp/ -type f -cmin +28800 -delete
## Das Token/TAN-Management wird unabhängig von apache dann in separaten system
## Prozessen bereitgestellt. z.B. auf einem separaten server, die jeweils
## gültige TANs via SSH auf diese Weise hier auf dem apache System erstellt.
## Damit liegt also das shared Secret zur TAN Erzeugung immer nur auf einem
## separaten control server, nicht hier auf dem apache server. Der Löschprozess kann
## jedoch auf dem Apache-System als cronjob laufen. Beides muss sichergestellt und
## gemonitored sein, da ansonsten eine alte TAN dauerhaft gültig bleiben
## würde.
< Location "/totp/">
# nur wenn unser auth mit totp auth ++ beginnt, nicht wenn normaler auth
# damit ist login mit altem aber gültigen username nicht mehr möglich
# sonst wäre damit bypass möglich
Require expr %{HTTP:Authorization} =~ /^Basic\ Kys/
# 1. Den Base64-Teil extrahieren (ohne fehleranfälliges replace)
SetEnvIf Authorization "^Basic\s+(.*)$" CLEAN_B64=$1
# 2. Dekodieren und Zerlegen: %1=Short, %2=Rest, %3=Passwort
# WICHTIG: Das =~ /^(.*)$/ am Ende fängt das Ergebnis der Funktion ab
SetEnvIfExpr "unbase64(reqenv('CLEAN_B64')) =~ /^([^:]{1,12})(.*):(.*)$/" U_SHORT=$1 U_REST=$2 U_PASS=$3
# 3. Den SHA1-Hash berechnen
SetEnvIfExpr "sha1(reqenv('U_SHORT') . reqenv('U_REST')) =~ /^(.*)$/" U_HASH=$1
# 4. Den neuen String (Rest:Passwort) neu kodieren
SetEnvIfExpr "base64(reqenv('U_REST') . ':' . reqenv('U_PASS')) =~ /^(.*)$/" NEW_AUTH_B64=$1
# 5. Debug-Header um mit curl -I zu testen
Header set X-Debug-Clean "%{CLEAN_B64}e"
Header set X-Debug-UShort "%{U_SHORT}e"
Header set X-Debug-URest "%{U_REST}e"
Header set X-Debug-Hash "%{U_HASH}e"
Header set X-Debug-New "%{NEW_AUTH_B64}e"
# 6. TOTP Prüfung
# Wir prüfen, ob die Datei im Ordner /etc/totp/ existiert
# Wenn die Datei NICHT existiert -> 403 Forbidden
# Das '!' negiert die Prüfung (-f = Datei existiert)
RewriteEngine On
RewriteCond "/etc/totp/%{ENV:U_HASH}" !-f
RewriteRule ^ - [F,L]
## 7. Ansonsten mit dem Proxyrequest fortfahren
RequestHeader set Authorization "Basic %{NEW_AUTH_B64}e" env=NEW_AUTH_B64
ProxyPreserveHost On
ProxyAddHeaders On
ProxyPass "https://finalmedia.de/totp_cleaned/"
ProxyPassReverse "https://finalmedia_totp_cleaned/"
< /Location>
SetEnvIf Request_URI ^/totp/ totp
LogFormat "%{%Y%m%d%H%M%S}t %h %u \"%{NEW_AUTH_B64}e\"" totp
CustomLog /var/log/apache2/totp.auth.log totp env=totp
< Location "/totp-cleaned/">
# Auf die cleaned area nur via lokalem loopback erlauben
# der dann die modifizierten (abgeschnittenen) auth header enthält.
# alle anderen requests müssen geblockt werden
# hier nur im Beispiel. Normalerweise ist das Ziel nicht hier
# sondern wieder ein separater webservice.
AuthType Basic
AuthName "Login required"
AuthUserFile /var/data/auth/example/htpasswd
Require ip 127.0.0.1 ::1
Require valid-user
< /Location>
## Um Sicherzustellen, dass man maximal 3 Versuche hat und dann in ein Ratelimit reinläuft
## soll mod security2 genutzt werden.
# Security Regeln
SecRuleEngine On
# Whitelist
SecRule REMOTE_ADDR "@ipMatch 127.0.0.1,::1" "id:99,phase:1,nolog,pass,allow,ctl:ruleEngine=Off"
# IPv4 und IPv6 Gruppe
SecRule REMOTE_ADDR "^([0-9a-fA-F]+:[0-9a-fA-F]+:[0-9a-fA-F]+:[0-9a-fA-F]+):" "id:9005,phase:1,nolog,pass,capture,setenv:IP_GROUP=%{TX.1}::/64"
SecRule REMOTE_ADDR "^(\d+\.\d+\.\d+\.\d+)$" "id:9006,phase:1,nolog,pass,capture,setenv:IP_GROUP=%{TX.1}"
SecAction "id:9000,phase:1,nolog,pass,initcol:ip=%{ENV.IP_GROUP}"
SecRule IP:COMBINED_BLOCKED "@eq 1" "id:9001,phase:1,deny,status:403,msg:'Sperre wegen zu vieler Zugriffsfehler/Strafpunkte'"
# 401 Fehler: +4 Strafpunkte (5 Versuche = 20)
SecRule RESPONSE_STATUS "@eq 401" "id:9002,phase:5,nolog,pass,setvar:ip.score=+4,expirevar:ip.score=180"
# 403 Fehler: +1 Strafpunkt (20 Versuche = 20)
SecRule RESPONSE_STATUS "@eq 403" "id:9008,phase:5,nolog,pass,setvar:ip.score=+1,expirevar:ip.score=180"
# Wenn die Summe der Strafpunkte pro IP >= 20 erreicht
SecRule IP:SCORE "@ge 20" "id:9003,phase:5,nolog,pass,setvar:ip.combined_blocked=1,expirevar:ip.combined_blocked=180"
Ein separater Service, der über das TOTP shared secret verfügt, musst diese via ssh erstellen, wobei er hier ja nur die hashfile anlegen muss.
Hier beispielhaft für den user oldusername
## einmalig
apt-get install oathtool qrencode
## einmalig für den user
openssl rand 32 | base32 > oldusername.sharedsecret
## als cronjob für den user, alle 8 stunden
oathtool -b -s 28800s -d 8 --totp @oldusername.sharedsecret | \
sed "s/^/++/g;s/$/++oldusername/g" | tr -d "\n" | sha1sum | tr -dc "0-9a-f" | \
ssh hier_der_service "sed 's,^,touch /etc/totp/,g' | sh"
## damit der user einen qrcode für seinen authenticator hat
url="otpauth://totp/oldusername:legacyservice?issuer=legacyservice\&digits=8\&algorithm=SHA1\&period=28800\&secret="
cat oldusername.sharedsecret | tr -dc "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567" | sed "s|^|${url}|g" | qrencode -t utf8