Ghidul de mai jos strânge la un loc tot ce e nevoie ca să oprești valul de conturi spam de pe o instanță WriteFreely publică. Nu e o soluție teoretică, sunt exact pașii de urmat, cu greșelile tipice și corecturile lor. Presupune un server Linux cu Docker instalat și Caddy ca reverse proxy, fie ca serviciu systemd, fie tot în container - ambele variante sunt acoperite mai jos.
O instanță WriteFreely cu înregistrare deschisă (open_registration = true) atrage devreme sau târziu conturi create automat, folosite pentru spam SEO - articole cu linkuri către pariuri online, produse dubioase sau ghiduri generice fără nicio legătură cu comunitatea. WriteFreely nu are captcha nativ și nici verificare de email obligatorie, deci bariera trebuie pusă din afara aplicației.
open_registration = false - rezolvă spam-ul, dar taie și posibilitatea ca oameni reali să-și facă cont fără să ceară o invitație manuală. Ghidul de mai jos e pentru cazul în care vrei să păstrezi înregistrarea deschisă.
Anubis e un program mic care stă în fața aplicației și cere navigatorului să rezolve un test de tip proof-of-work înainte să lase cererea să treacă mai departe. Un navigator normal rezolvă testul automat, în fundal, în una-două secunde. Un script simplu, care doar trimite cereri HTTP fără să execute JavaScript, nu poate trece deloc.
Cea mai frecventă greșeală aici - presupui numele rutei de înregistrare dintr-o sursă neverificată, în loc să te uiți direct în codul aplicației. Rezultatul e o barieră care nu protejează nimic, fiindcă botul folosește altă rută decât cea presupusă.
Pentru WriteFreely, uitându-te direct în routes.go din codul sursă oficial, există exact două rute care creează un cont nou:
POST /auth/signup - formularul web, cel completat de un vizitator obișnuitPOST /api/auth/signup - rută programatică, folosită de aplicații externe; există doar dacă open_registration = true
Dacă reverse proxy-ul (Caddy, în acest exemplu) rulează ca serviciu systemd direct pe server, nu ca și container, cel mai simplu e ca Anubis să asculte tot pe 127.0.0.1, la fel ca aplicația pe care o protejează.
mkdir -p /opt/services/anubis cd /opt/services/anubis
docker-compose.yml:
services: anubis: image: ghcr.io/techarohq/anubis:latest container_name: anubis-writefreely restart: unless-stopped network_mode: host environment: BIND: "127.0.0.1:8923" DIFFICULTY: "4" METRICS_BIND: "127.0.0.1:9099" TARGET: "http://127.0.0.1:8080" POLICY_FNAME: "/data/cfg/botPolicy.yaml" COOKIE_DOMAIN: "exemplu.ro" COOKIE_EXPIRATION_TIME: "1h" SERVE_ROBOTS_TXT: "true" volumes: - "./botPolicy.yaml:/data/cfg/botPolicy.yaml:ro"
Ajustează TARGET la portul real pe care ascultă aplicația ta, și COOKIE_DOMAIN la domeniul tău.
COOKIE_EXPIRATION_TIME: „1h“) limitează cât timp poate fi refolosit un cookie obținut o singură dată.
Aici pui doar rutele reale identificate la pasul 3, cu dificultate crescută specific pe ele, și lași restul situl fără nicio barieră:
bots: - name: protect-registration expression: any: - 'path.startsWith("/auth/signup")' - 'path.startsWith("/api/auth/signup")' action: CHALLENGE challenge: algorithm: fast difficulty: 5 - name: allow-everything-else expression: "true" action: ALLOW status_codes: CHALLENGE: 200 DENY: 403
| Dificultate | Timp aproximativ de rezolvare | Observație |
|---|---|---|
| 4 (implicit) | sub 1 secundă | prea ușor, orice script trece dacă rulează JavaScript |
| 5 | câteva secunde | echilibru bun pentru o rută vizitată rar |
| 6-7 | 15-30 secunde | riscă să descurajeze și utilizatori reali |
cd /opt/services/anubis docker compose up -d
Găsești blocul din Caddyfile pentru domeniul tău și schimbi ținta de la aplicație direct, la portul pe care ascultă Anubis:
signup.exemplu.ro {
reverse_proxy localhost:8923
}
systemctl reload caddy
curl -I -X POST https://signup.exemplu.ro/auth/signup curl -I -X POST https://signup.exemplu.ro/api/auth/signup
Ambele ar trebui să răspundă cu status 200, tip text/html, și cookie-uri de la Anubis (techaro.lol-anubis-*). Dacă vezi în schimb un răspuns direct de la aplicație (de exemplu un 302 cu redirect), înseamnă că ruta respectivă tot ocolește Anubis.
Merită activat din start, nu doar când apare o problemă - fără el, nu ai cum să vezi exact ce cale a folosit un bot care a reușit să treacă.
signup.exemplu.ro {
log {
output file /var/log/caddy/access.log
format json
}
reverse_proxy localhost:8923
}
mkdir -p /var/log/caddy chown caddy:caddy /var/log/caddy systemctl reload caddy
Dacă apare un cont nou nedorit, primul loc de verificat e acest jurnal, filtrat pe intervalul orar respectiv:
grep "AAAA-LL-ZZ HH:" /var/log/caddy/access.log | grep -E "signup|auth"
Anubis verifică dacă un client poate rezolva un puzzle. Nu limitează câte cereri trimite același client într-un interval de timp, odată ce a rezolvat puzzle-ul o dată. Pentru asta ai nevoie de un strat separat.
Dacă serverul nu are deja ufw sau nftables active, cea mai simplă variantă e direct în iptables, cu modulul recent:
iptables -I INPUT -p tcp --dport 443 -m conntrack --ctstate NEW -m recent --name https_limit --set iptables -I INPUT -p tcp --dport 443 -m conntrack --ctstate NEW -m recent --name https_limit --update --seconds 60 --hitcount 20 -j DROP
Asta limitează la maxim 20 de conexiuni noi pe minut, per adresă IP, către portul 443. E o limită generoasă pentru un vizitator normal, dar taie clar un bot care insistă constant.
Testezi imediat că situl tot răspunde normal:
curl -I https://exemplu.ro/
Faci regulile permanente, altfel dispar la restart:
apt install -y iptables-persistent netfilter-persistent save
Dacă instanța a acumulat deja conturi spam înainte să pui bariera, curățarea se face direct în baza de date, cu backup obligatoriu înainte de orice ștergere.
Pentru WriteFreely cu SQLite:
cp /cale/catre/writefreely.db /cale/catre/backup-$(date +%Y%m%d-%H%M).db cd /cale/catre/writefreely docker compose stop
Ștergi tot ce nu e contul tău de admin, în ordinea corectă (postări, apoi bloguri, apoi useri, ca să nu rămână rânduri orfane):
BEGIN TRANSACTION; DELETE FROM posts WHERE owner_id IN (SELECT id FROM users WHERE username != 'contul-tau-de-admin'); DELETE FROM collections WHERE owner_id IN (SELECT id FROM users WHERE username != 'contul-tau-de-admin'); DELETE FROM users WHERE username != 'contul-tau-de-admin'; COMMIT;
docker compose up -d
Trei straturi, fiecare acoperind ce ratează celălalt: