15 Commits (714f19d4d43f743499b36044141a29ae7cef6213)
| Author | SHA1 | Message | Date |
|---|---|---|---|
|
|
d7d116e9a5 |
Jede Mail faehrt wieder ueber ihren eigenen Mailer
Mail::to(...) loest den Standard-Mailer auf, und dessen queue() schreibt danach `$view->mailer($this->name)`. Damit war der im Konstruktor gewaehlte Mailer (cp_mail_<typ>) durch 'smtp' ersetzt, bevor der Auftrag ueberhaupt gebaut war. Mit dem Mailer fiel alles weg, was an ihm haengt: MailboxTransport, die Postfachsuche samt passendem Absender — und der Notschalter aus App\Support\MailDelivery, der genau dort sitzt. Eine abgeschaltete Zustellung schaltete deshalb nichts ab, und die Mails meldeten sich mit den Zugangsdaten aus der Konfiguration an, waehrend der Absender aus der Mail kam: „553 Sender address rejected". Das lief hier im Minutentakt. Zwei Stellen hatten es schon richtig — MaintenanceNotifier und MailPreview, beide mit der Erklaerung an der Zeile. Neun andere nicht. Alle beginnen jetzt mit Mail::mailer($mail->mailer), sodass das Ueberschreiben denselben Wert zurueckschreibt. Die Attrappen in drei Tests zielten auf to() und trafen damit nicht mehr den Einstieg; drei von ihnen bestanden ohnehin nur zufaellig, weil ein Aufruf auf null ebenfalls warf. Sie zielen jetzt auf mailer(). Erzwungen durch tests/Feature/MailDispatchTest.php: Mail::to( ist in app/ verboten, und ein Gegentest haelt das Framework-Verhalten fest, damit die Regel fallen darf, wenn Laravel sie eines Tages unnoetig macht. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
c78866e360 |
fix(security): Aufheben einer Sperre sagt die Wahrheit und wird nachgeholt
tests / pest (push) Waiting to run
Details
tests / assets (push) Waiting to run
Details
tests / release (push) Blocked by required conditions
Details
Mein eigener Befund "die Zeile erneuert sich nach dem Aufheben nicht" war falsch — eine Pruefung ueber den ganzen Weg (Modal schickt das Ereignis, Seite faengt es) zeigt, dass die Zeile sehr wohl in den Verlauf wandert. Dabei fiel der echte Fehler auf, der daneben lag: HostFirewall::release() schluckt jeden Fehlschlag und meldet ihn nur ins Log. Beide Aufrufer verwarfen den Rueckgabewert und meldeten in jedem Fall "Sperre aufgehoben." War der Host im Moment des Aufhebens nicht erreichbar, stand der Datensatz auf aufgehoben und die Regel noch drin: Portal und Konsole zeigten "Aufgehoben", waehrend die Adresse weiter ausgesperrt blieb — bis zum Ablauf der urspruenglichen Sperrzeit, ohne dass es jemand sagen konnte. - release() gibt zurueck, ob die Firewall schon nachgezogen hat; alle drei Stellen (Portal, Host-Ansicht, Kunden-Ansicht) sagen es, wenn nicht. - releaseMany() als Gegenstueck zu blockMany(): eine SSH-Sitzung statt einer je Adresse. - ScanForIntrusions gleicht jetzt in BEIDE Richtungen ab. Bisher trug er nur ein; nichts nahm je einen haengengebliebenen Eintrag wieder heraus. Eine Adresse, die eine ANDERE aktive Sperre desselben Hosts noch traegt, bleibt stehen. - Jeder Loeschbefehl traegt `2>/dev/null || true`: nft scheitert am Loeschen eines Elements, das es nicht gibt, und weg ist genau das Ziel. Ohne das meldete der Abgleich bei jedem Lauf einen Fehlschlag. 8 neue Pruefungen, Suite 2625 gruen. |
|
|
|
f40b762413 |
Zwei Empfaengerkreise, und eine Wegwahl, die auch das Konto verschiebt
Fix-Welle nach dem Gesamt-Review, Punkte 4 und 5. Beide sitzen in denselben
Dateien, deshalb ein Commit.
DIE HOST-MELDUNG WAR WORTWOERTLICH DIE KUNDENMAIL (Punkt 4).
notifyHostManagers() verschickte dieselbe SecurityBlockMail. Deren Text ist fuer
den Kunden geschrieben ("bei Ihrer Cloud", "…koennen Sie die Sperre auf der
Sicherheitsuebersicht aufheben"), der Knopf zeigte auf portal.security — wo ein
Betreiber kein Konto hat und Host-Sperren nie erscheinen —, welcher Host
betroffen ist stand nirgends, und die Anrede blieb leer, weil $block->instance
null ist.
Die Mail kann jetzt beide Faelle. Unterscheidungsmerkmal in der Ansicht ist
$hostName: gesetzt heisst Host-Sperre, null heisst Instanz-Sperre. Ein zweites
Flag daneben waere eine zweite Wahrheit.
- Betreff nennt den Host.
- Eigener Textblock (mail_heading_host, mail_preheader_host, mail_intro_host mit
"fehlgeschlagene SSH-Anmeldungen am Host :host"), eigene Tabellenzeile
field_host, eigener Hinweis mail_note_host.
- Der Knopf zeigt auf admin.hosts.show, also auf die Host-Detailseite der
Konsole, wo die Host-Sperren mit ihrem Aufheben-Knopf stehen.
- Keine Anrede im Host-Fall — sie war vorher leer, und der Empfaengerkreis ist
ohnehin mehrere Personen.
- Der Fehlalarm-Satz ist ein anderer und sagt, was ein Betreiber wissen will:
bestehende Verbindungen bleiben unberuehrt, die Regel steht unter
"established,related accept".
R19: die zwei Kommentare, die fuer Host-Sperren Unwahres behaupteten, sind
richtiggestellt — der Kopfkommentar von SecurityBlockMail und der
MailCatalogue-Eintrag. Beide beschreiben jetzt zwei Empfaengerkreise. Ein
Kommentar mit falscher Begruendung haelt den Naechsten vom Nachsehen ab.
DIE WEGWAHL VERSCHOB DEN ABSENDER, NICHT DAS KONTO (Punkt 5).
SecurityBlockMail nagelte mailer('cp_system') fest und holte das Postfach ueber
die Wegwahl. MailboxTransport loeste weiter ueber den ZWECK auf und
authentifizierte mit dessen Zugangsdaten. Legte der Betreiber security-block auf
info@ — der ausdrueckliche Anlass fuer Abschnitt 3a der Spec —, ging die Mail
mit From: info@ durch die SMTP-Sitzung von no-reply@. Viele Server weisen das ab
("553 5.7.1 Sender address rejected: not owned by user"), und dann verschwindet
ausgerechnet die Mail, die er gerade umgelegt hat. Betraf ALLE Mailarten.
Die Wegwahl bestimmt jetzt auch den Mailer. Die zweitbeste Loesung — die Auswahl
in der Konsole auf Postfaecher zu begrenzen, deren Konto benutzt wird — war
nicht noetig.
- config/mail.php erzeugt AUS MailCatalogue::all() je Mailart einen Mailer
cp_mail_<mailart> mit transport, purpose und mail_key. Erzeugt statt
aufgelistet, weil der Katalog ausdruecklich die EINE Liste ist; eine zweite
hier waere die, die beim siebzehnten Mail vergessen wird. Genauso statisch wie
die fuenf Zweck-Mailer: ein reines Array, keine Datenbank, auch unter
config:cache unveraendert.
- MailboxTransport nimmt den mail_key entgegen und loest damit ueber DIESELBE
MailRoute::purposeOrMailbox() auf, die auch der Umschlag fragt. Ohne
Schluessel entscheidet wie bisher allein der Zweck — jeder Mailer, der keinen
hat, verhaelt sich unveraendert.
- Jede Mailart mit Schluessel benennt ihren Mailer ueber
MailCatalogue::mailer(). Dessen Rueckfall auf cp_<zweck> fuer einen
unbekannten Schluessel ist nicht Zierde: DunningNoticeMail baut seinen
Schluessel aus der Mahnstufe, und ohne den Rueckfall waere eine Stufe
ausserhalb 0-3 ein "Mailer [...] is not defined" und damit ein Totalausfall
statt eines falschen Absenders.
- Betrifft alle Mailarten: 13 Mailables, die CloudReady-Notification und die
cloud-ready-Vorschau. ContactRequestMail bleibt auf cp_support — es hat keinen
Schluessel im Katalog, weil es keine eigene Mailart ist.
MailSenderOwnershipTest ist mitgezogen und dabei STRENGER geworden. Die alte
Fassung verglich "From-Zweck == Mailer-Zweck"; jetzt prueft sie, dass der
Katalog dem Zweck zustimmt, den der Umschlag verlangt. Katalog-Drift war bis
hierher nur als Warnung im Docblock von MailCatalogue festgehalten und durch
nichts erzwungen. Ein Literal, das selbst kein Schluessel ist, wird als PRAEFIX
behandelt (der Mahnlauf) — dann muessen alle Schluessel dahinter zustimmen, und
es muss mindestens einen geben.
MailPreviewTest fragte den Zweck bisher, indem es 'cp_' vom Mailer-Namen
abschnitt. Das war eine Zusicherung ueber das Namensschema, nicht ueber die
Zusage; sie liest den Zweck jetzt aus der Konfiguration des Mailers selbst.
tests/Feature/Mail/SenderAddressTest.php gehoert inhaltlich zur parallel
laufenden Sitzung. Vier Zusicherungen darin pruefen genau den Mailer-Namen, den
dieser Commit aendert, also sind sie mitgezogen — die Datei war dabei sauber,
es steckt keine fremde unfertige Arbeit in diesem Commit.
Committet mit ausdruecklicher Dateiangabe am Zeilenende, weil eine parallele
Sitzung an derselben Ablage arbeitet und der Index fremde Arbeit enthalten kann.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
|
|
552be46881 |
Was aus dem Gast kommt, ist keine Adresse, bis es geprueft ist
Fix-Welle nach dem Gesamt-Review, Punkte 1, 2, 6, 7, 8, 9 und die Haelfte von 3,
die den stillen Misserfolg sichtbar macht.
BEFEHLSEINSCHLEUSUNG ALS ROOT (Punkt 1, Blocker). `remoteAddr` ist eine
beliebige Zeichenkette aus einer Datei IM GAST. `BlockAddress::isExempt()`
prueft nur Zugehoerigkeit — bei Unsinn liefert IpUtils schlicht false, und es
ging weiter. `HostFirewall::block()` schob die Zeichenkette per sprintf in ein
nft-Kommando, und PhpseclibRemoteShell::run() reicht das an /bin/sh ALS ROOT auf
dem Proxmox-Host. Ein Semikolon darin war ein zweiter Befehl.
Dicht an BEIDEN Enden, nicht an einem:
- Im Leser jede Adresse durch filter_var(FILTER_VALIDATE_IP), alles andere
verworfen — im Nextcloud-Zweig, der gar kein Gegenstueck hatte, UND im
SSH-Zweig. Dessen preg_match schliesst ein Semikolon zwar aus, laesst aber
"1.2.3" und "...." durch, und der Riegel darf nicht davon abhaengen, dass
jemand beim naechsten Aendern des Musters daran denkt.
- In HostFirewall derselbe Riegel noch einmal, vor jedem sprintf: false zurueck,
nichts ausgefuehrt, nicht einmal verbunden. Ein Dienst, der eine Wurzel-Shell
fuettert, darf sich nicht darauf verlassen, dass sein Aufrufer sauber war.
NUR OEFFENTLICHE ADRESSEN IM INSTANZ-ZWEIG (Punkt 2, Blocker). TRUSTED_PROXIES
steht in der .env des Gastes nie, waehrend Traefik auf dem Host sitzt: was
Nextcloud protokolliert, ist entweder die Bruecken-adresse des Hosts (dann
zaehlen ALLE Kunden unter einer Adresse und ein echter Angreifer nie) oder ein
X-Forwarded-For, das der Client SELBST waehlt (dann macht sich ein Angreifer mit
10.66.0.1 dauerhaft ausnahmefaehig und sperrt mit einer fremden Adresse gezielt
Unbeteiligte aus). Beide Schadensfaelle sind privat oder reserviert, ein echter
Angreifer ist es nicht — also FILTER_FLAG_NO_PRIV_RANGE|NO_RES_RANGE, aber nur
hier. Im Host-Zweig stammt die Adresse aus dem SSH-Journal und ist echt.
Absichtlich ZWEI getrennte Pruefungen mit zwei getrennten Kommentaren an
derselben Stelle, nicht ein zusammengefasster Aufruf: das eine ist ein Riegel
gegen Einschleusung, das andere eine Vorsichtsmassnahme. Verschmolzen haette die
naechste Person beim Entfernen der Vorsicht den Riegel mitgenommen. Der Kommentar
sagt auch, was die Vorsicht abloest — die Vertrauenskette zum Gast sauber setzen
und BEWEISEN.
EINE VERBINDUNG JE HOST STATT EINE JE SPERRE (Punkt 7). reapplyActiveBlocks()
rief block() je aktiver Sperre, und jede baute ihre eigene SSH-Sitzung auf: bei
dreissig Sperren dreissig Handshakes pro Minute, seriell, auf dem einzigen
Provisionierungs-Arbeiter — also genau WAEHREND eines Angriffs auf demselben
Arbeiter, der die bezahlte Kundenbereitstellung faehrt. Neu ist blockMany(), das
mehrere Elemente in einen nft-Aufruf legt; block() ruft es mit einem einzigen
Eintrag und behaelt seine Signatur. Hoechstens zwei Befehle, weil v4 und v6 in
getrennten Mengen leben, aber nur eine Verbindung.
Die Zusicherung "Restlaufzeit statt Ursprungsdauer" ist dabei ausdruecklich
verteidigt: blockMany() nimmt die Sekunden JE ADRESSE entgegen. Eine gemeinsame
Dauer fuers Buendel waere bequemer gewesen und haette genau das verwaessert,
wofuer es einen eigenen Test gibt. Der bestehende Restlaufzeit-Test laeuft
unveraendert, und der neue Buendel-Test prueft drei verschiedene Restlaufzeiten
in einem Aufruf.
Dabei aufgefallen: dieselbe Adresse kann an zwei Subjekten desselben Hosts
gesperrt sein (Host und Instanz, oder zwei Instanzen), und in der nftables-Menge
ist das EIN Element. Beim Buendeln gewinnt die laengere Restlaufzeit — die
kuerzere zuletzt einzutragen wuerde die laengere Sperre verkuerzen.
EINE SPERRE, DIE NUR IN DER DATENBANK STEHT, DARF NICHT SCHWEIGEN (Punkt 3,
erste Haelfte). Auf einem Host, der noch das Regelwerk ohne die Mengen traegt,
scheitert nft add element bei JEDEM Versuch, und beide Aufrufer verwarfen den
Rueckgabewert: die Sperre stand in Datenbank, Portal, Konsole und in der Mail an
den Kunden als aktiv und in der Firewall nie. Der report() sitzt deshalb in
HostFirewall::apply() selbst, nicht an den Aufrufern — an der Stelle, an der der
Misserfolg anfaellt, kann ihn kein dritter Aufrufer wieder vergessen. Wie man
so einen Host nachzieht, kommt im naechsten Commit.
DER RING UM scanHosts() (Punkt 6). Die Nachbarmethode hat ihn seit Fix-Runde 2,
diese ging leer aus. FailedLoginReader::fromHost() faengt seine eigene
SSH-Ausnahme zwar ab, aber alles danach nicht: SecurityBlock::create(),
Settings::set(), Spaties PermissionDoesNotExist, $host->save(). Wirft eines
davon, bricht der ganze Lauf ab — und vor allem laeuft reapplyActiveBlocks()
nicht, jede Minute erneut.
DIE AUSNAHME FUER DEN EIGENEN SERVER GRIFF NUR BEI EINER IP (Punkt 8).
Str::beforeLast(':') lieferte bei "vpn.clupilot.com:51820" einen Hostnamen, und
IpUtils::checkIp() sagt dazu immer false — in der Form, die .env.example
dokumentiert, war die eigene Adresse still NICHT ausgenommen, der Server konnte
sich also selbst aussperren. Bei IPv6 blieben zusaetzlich die Klammern stehen.
hostPartOf() kennt jetzt die drei Formen, die wirklich vorkommen, ein Name wird
aufgeloest, und ALLE Ergebnisse werden ausgenommen (nur die erste zu nehmen
hiesse, sich bei einem Wechsel im Round-Robin doch wieder auszusperren).
Schlaegt die Aufloesung fehl, faellt der Eintrag weg wie bei leerem Endpunkt.
App\Support\DnsLookup als eigene Klasse, damit ein Test sie im Container
ersetzen kann: ein Test, der fuer seine Zusicherung echtes DNS braeuchte,
prueft das Netz des Rechners, auf dem er gerade laeuft, nicht die Regel. Sie
holt A UND AAAA — gethostbynamel() allein kennt nur A, ein reiner
AAAA-Endpunkt waere sonst still nicht ausgenommen gewesen, derselbe Fehler eine
Adressfamilie weiter. Die Liste wird je BlockAddress-Instanz einmal aufgeloest
und gemerkt; ein Lauf holt sich den Dienst einmal und fragt damit jede
gesichtete Adresse.
DER ERSTE LAUF LIEST NICHT MEHR JEDES PROTOKOLL VON VORN (Punkt 9). Ist der
gespeicherte Versatz 0, wird stat -c %s als Startwert genommen und gar nichts
gelesen. Gesperrt wuerde dabei ohnehin nichts, weil das Zehn-Minuten-Fenster
jede aeltere Zeile wegwirft — es waere eine grosse Uebertragung durch den
Gastagenten, alle Instanzen im selben Auftrag, fuer ein garantiert leeres
Ergebnis. Bewusst getrennt vom Rotationsfall: dort ist die Datei KLEINER als der
Versatz und muss von vorn gelesen werden, weil das Neue am Anfang steht. Der
Code unterscheidet dafuer jetzt $stored (was in der Spalte steht) von $offset
(ab wo gelesen wird).
Der Fixture-Helfer aktiveInstanz() setzt deshalb security_log_offset auf 1: 0
heisst ab jetzt woertlich "noch nie gelesen", und die Tests dieser Datei
beschreiben den Normalfall einer Instanz, die schon einmal gelesen wurde.
Nebenbei: SecurityConsoleTests erster Fall hing 135 Sekunden am
TCP-Verbindungsaufbau, weil das Aufheben einer Sperre HostFirewall::release()
ruft und kein Fake gebunden war — mehr als der gesamte uebrige Ordner zusammen.
Ein beforeEach behebt das; am Verhalten aendert sich nichts.
Committet mit ausdruecklicher Dateiangabe am Zeilenende, weil eine parallele
Sitzung an derselben Ablage arbeitet und der Index fremde Arbeit enthalten kann.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
|
|
51a97019a8 |
Sperren sehen und aufheben: Portalseite fuer den Inhaber, Abschnitte in der Konsole
Aufgabe 6 des Fruehwarnsystems. Der Inhaber sieht im Portal die Sperren SEINER Instanzen und hebt sie dort auf; der Betreiber sieht in der Konsole alle, an der Kunden- und an der Host-Detailseite, und auf der Uebersicht steht ein Hinweis, solange irgendwo eine Sperre aktiv ist. Aufgehoben wird ueber ein Bestaetigungs-Modal (R23), und das Modal mutiert nichts: es wirft ein Ereignis, das die Seite auffaengt und an ihre eigene Methode weiterreicht. Die Berechtigungspruefung bleibt damit an der einen Stelle, an der sie schon stand — noetig, weil ein Modal ohne die Middleware der Seite erreichbar ist (R20). Dazu die Berechtigung `instances.manage`, nach dem Muster der bestehenden `instances.restart`-Migration; Abrechnung und Read-only bleiben unberuehrt. ACHTUNG, was hier sonst noch drinsteckt und NICHT zu dieser Aufgabe gehoert: rund 150 Zeilen zum Versandtakt — das Merkmal `RidesALane`, vierzehn Mailables und `MailLaneRoutingTest`. Die stammen aus einer PARALLEL laufenden Sitzung an einem anderen Feature. Wie das hineingeriet: der Implementierer dieser Aufgabe brach vor dem Commit ab und liess seine fertige Arbeit ungespeichert im Baum. Ich habe sie dateigenau mit `git add <dateien>` vorgemerkt, um nichts Fremdes mitzunehmen — und dabei uebersehen, dass `git add` nur HINZUFUEGT: die andere Sitzung hatte ihre Arbeit bereits vorgemerkt, und `git commit` schreibt den ganzen Index, nicht nur das zuletzt Hinzugefuegte. Richtig waere `git commit -- <dateien>` gewesen. Nichts ist verloren, und die volle Suite ist auf diesem Stand gruen (2571). Aber diese Botschaft soll nicht behaupten, sie beschreibe alles, was hier steht. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
412d67955a |
Fix-Runde 2: Drossel-Marke erst nach Erfolg, Kommentar richtiggestellt
Die Marke stand bisher VOR dem Versandversuch - scheiterte das Einreihen, schwieg das Subjekt fuer den Rest der Stunde, obwohl null statt einer Mail angekommen war. Jetzt steht sie in notifyInstanceOwner() erst nach dem erfolgreichen queue() im try-Block, in notifyHostManagers() erst nach mindestens einem geglueckten Einreihen (Merker ueber die Schleife, da ein einzelnes abgelehntes Postfach weder die uebrigen Betreiber noch die Marke fuer alle blockieren darf). Kein Sturm-Risiko: Sperren entstehen ohnehin nur ab der Zehner-Schwelle, nicht bei jedem Fehlversuch. Die Kommentare behaupteten außerdem, das try/catch finge Zustellungsfehler ab - tatsaechlich faengt es nur, was beim EINREIHEN schiefgeht (synchron, vor der Warteschlange); ein Zustellungsfehler passiert spaeter im Warteschlangen-Arbeiter und steht in dessen Protokoll. Beide Docblocks richtiggestellt. Neuer Testfall haengt einen Wrapper vor die gefakte Mail-Fassade, dessen erster to()-Aufruf wirft und ab dem zweiten an die echte Fake-Instanz durchreicht - MailFake::queue() selbst kann einen Fehlschlag nicht simulieren, weil es den Mailable nur ablegt und dabei nie wirft. Als Gegenprobe testweise auf den alten Code zurueckgesetzt: Testfall lief rot mit der erwarteten Meldung, Datei danach byte-identisch wiederhergestellt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
17a07d1a68 |
Fix-Runde 1: Host-Sperren gehen an jeden Betreiber mit hosts.manage
Koordinator-Entscheidung statt neuer Einstellung: der Empfaengerkreis ist per Definition richtig, weil es genau die Menschen sind, die eine Host-Sperre in der Konsole ueberhaupt aufheben duerfen, und er pflegt sich bei jedem Rollenwechsel von selbst mit. notifyHostManagers() nutzt Spaties eigenen Operator::permission()-Scope (dieselbe Pruefung wie authorize() an anderer Stelle, nur als Mengenabfrage) - kein Empfaenger heisst keine Mail, kein Fehler, keine Drossel-Markierung; jede Adresse einzeln in ihrem eigenen try/catch, damit ein abgelehntes Postfach nicht die uebrigen Betreiber um ihre Meldung bringt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
2d979052d0 |
Benachrichtigung ueber eine gesperrte Adresse, hoechstens eine je Stunde
SecurityBlockMail geht bei einer Instanz-Sperre an die Kontoadresse des Inhabers, aus dem SYSTEM-Postfach wie NewDeviceSignInMail. Die Drossel sitzt in BlockAddress::notifyInstanceOwner() ueber Settings (kein neues Feld fuer etwas, das nach einer Stunde niemanden mehr interessiert) und wird VOR dem Versandversuch gesetzt. Ein Throwable beim Verschicken wird gemeldet und verschluckt: die Sperre steht schon, bevor ueberhaupt versucht wird zu verschicken, und ein kaputtes Postfach darf sie nicht rueckgaengig machen. Host-Sperren verschicken bewusst noch keine Mail: kein Muster im Repo, wie eine Betreiber-Meldung ihren Empfaenger findet (siehe Bericht). Route 'portal.security' minimal angelegt (Aufgabe 6 baut die echte Seite) - auf einem eigenen Pfad, weil sie sich mit der oeffentlichen /security-Seite sonst lautlos gegenseitig ueberschreiben, sobald Portal und Website ohne eigene Domain laufen (RouteCollection indiziert ueber Methode+Domain+URI, nicht ueber den Namen). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
ec1f13807e |
Fix-Runde 2: ein werfender Gast reisst den Lauf nicht mehr mit
guestExec() wirft bei einer echten HTTP-Antwort (->throw()), nicht nur bei einem sauberen Fehlercode — ein abgeschalteter Gast oder ein noch nicht gestarteter Agent liess das ungefangen durch scanInstances() nach oben laufen und beendete handle(), bevor scanHosts() und vor allem reapplyActiveBlocks() dran waren. Eine unbeteiligte Sperre wurde dadurch jede Minute erneut nicht wiedereingetragen. FailedLoginReader::fromInstance() faengt Throwable jetzt genauso wie fromHost() es schon tat; ScanForIntrusions::scanInstances() umschliesst zusaetzlich jede Instanz einzeln, nach dem Muster von PingHosts. Dazu ein Testfall mit einem werfenden und einem lauten Gast nebeneinander, und Carbon::setTestNow() jetzt in finally, damit ein abgebrochener Testfall die Uhr nicht fuer die Folgetests eingefroren laesst. |
|
|
|
118f0203c2 |
Fix-Runde 1: der Zaehlstand haelt jetzt ueber Laeufe hinweg
ScanForIntrusions hielt bisher nur den Zuwachs eines einzelnen Laufs gegen die Schwelle — bei einem Lauf pro Minute wurde aus "10 in 10 Minuten" faktisch "10 in einer Minute", und der geduldige Angreifer mit wenigen Versuchen je Minute lief nie darueber. Ein Zaehlstand je Subjekt und Adresse ueber Laravels RateLimiter (cache-gestuetzt, 600s, wie OperatorLogin es fuer Anmeldeversuche schon vormacht) addiert jeden Lauf auf den bestehenden Stand und wird nach dem Sperren zurueckgesetzt. |
|
|
|
d3407ff613 |
Melder: gescheiterte Anmeldungen lesen, zaehlen, sperren
FailedLoginReader liest Nextclouds Protokoll ueber den Gastagenten (mit Byte-Versatz und Rotationserkennung) und die SSH-Anmeldungen eines Hosts ueber journalctl. ScanForIntrusions bringt beides mit BlockAddress zusammen, jede Minute auf der provisioning-Warteschlange, und traegt am Ende jede noch gueltige Sperre mit ihrer RESTLAUFZEIT erneut in die Firewall ein — der Fall, der einen Neustart des Hosts uebersteht. NextcloudOcc bekommt einen zweiten Baustein (exec()) fuer Gastbefehle jenseits von occ, ohne die Ein-Ort-Regel fuer "docker compose exec" zu verletzen. |
|
|
|
09cb8aea5c |
Fix-Runde 1: der WG-Endpunkt-Zweig der Ausnahmeliste bekommt einen Test
Die drei hart verdrahteten Adressen waren belegt, der vierte Eintrag — die eigene öffentliche Adresse aus CLUPILOT_WG_ENDPOINT — lief in der Testumgebung nie durch, weil die Variable dort leer ist. Setzt die Einstellung, prüft die Ausnahme UND die Gegenprobe am Nachbarn in derselben Zeile, damit der Test nicht bloß beweist, dass gar nichts mehr gesperrt wird. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
fc3ff3cb28 |
Gesperrte Adressen: Datensatz, Verdopplung und die Liste, die nie gesperrt wird
BlockAddress trifft die Sperr-Entscheidung: Ausnahmeliste (Verwaltungsnetz, Loopback, eigene öffentliche Adresse — hart verdrahtet), laufende Sperre nicht doppelt, Verdopplung binnen 24h bis zur 24h-Obergrenze. Der Datensatz entsteht unabhängig vom Rückgabewert von HostFirewall::block() — eine Sperre nur in der Datenbank ist sichtbar und wird nachgeholt (Aufgabe 4), eine Ausnahme dort würde den Zeitplan-Auftrag mitreißen. Migration bringt security_blocks und im selben Zug die zwei Cursor, die Aufgabe 4 braucht: instances.security_log_offset, hosts.security_log_seen_at. |
|
|
|
8f630c5093 |
Fix-Runde 1: strukturelle Pruefung statt Zaehler, und der nicht erreichbare Host bekommt einen Test
substr_count('flags timeout') lief ueber den ganzen Text inklusive
Kommentare und belegte nur "die Phrase kommt zweimal vor", nicht "beide
set-Bloecke tragen die Ablaufzeit". Ersetzt durch je einen strukturellen
Ausdruck pro Menge; der Originalkommentar aus dem Auftragszettel kann
damit wieder wortgenau stehen. Dazu zwei neue Tests mit failConnect, die
belegen, dass block()/release() bei einem nicht erreichbaren Host false
liefern statt zu werfen - der Pfad, auf dem das Wiedereintragen in
Aufgabe 4 aufbaut.
|
|
|
|
77a4c3d990 |
Sperrliste in der Host-Firewall, unter der Regel fuer bestehende Verbindungen
Zwei nftables-Mengen (clupilot_blocked/clupilot_blocked6, beide mit flags timeout) im erzeugten Regelwerk, die Drop-Regel dafuer sitzt absichtlich unter ct state established,related accept — wer drin ist, bleibt drin, gesperrt wird nur, was neu anklopft. HostFirewall::block()/ release() tragen eine Adresse mit Ablaufzeit ein bzw. nehmen sie heraus, ueber die WireGuard-Adresse des Hosts, und geben false statt zu werfen, wenn der Host nicht erreichbar ist, damit eine spaetere Wiedereintrage- Aufgabe die Sperre einfach nochmal versuchen kann. |