Unabhängiges, privates Bürgerportal keine staatliche Behörde

DIESES PORTAL IST NOCH IM AUFBAU UND NICHT FÜR DIE ÖFFENTLICHKEIT BESTIMMT ZUM AKTUELLEN ZEITPUNKT!

Sperrliste

Auffällige IP-Adressen, zum Abrufen für Ihre eigene Firewall.

Worum es geht

Wer eine Webseite betreibt, sieht dieselben Angreifer wie alle anderen: dieselben Scanner, dieselben Anmeldeversuche in Serie, dieselben Formularspammer. Bisher sperrt jeder sie für sich allein und fängt wieder von vorn an.

Diese Liste sammelt, was bei uns und bei anderen auffällt. Eine Adresse kommt darauf, wenn 3 unabhängige Melder sie nennen oder wenn unsere eigene Schutzschicht sie gesperrt hat. Mehrfache Meldungen desselben Melders zählen einmal.

Abrufen

Kein Schlüssel nötig. Die Liste wird fünf Minuten zwischengespeichert; öfter als alle paar Minuten abzurufen bringt nichts.

Die ganze Liste

curl -s https://meinebrd.global/api/sperrliste/v1/liste.txt

Eine Adresse je Zeile, Kommentarzeilen beginnen mit #. Genau so, dass die üblichen Werkzeuge es ohne Umweg einlesen.

Eine einzelne Adresse nachschlagen

curl -s "https://meinebrd.global/api/sperrliste/v1/pruefen?ip=203.0.113.10"
{
  "ok": true,
  "ip": "203.0.113.10",
  "gesperrt": true,
  "meldungen": 4,
  "punkte": 11,
  "grund": "Anmeldeversuche in Serie (3), Abtasten nach Lücken (1)",
  "quelle": "meldung",
  "seit": "2026-09-14 08:21:03"
}

Dazu gibt es https://meinebrd.global/api/sperrliste/v1/liste.json mit Begründung je Eintrag und https://meinebrd.global/api/sperrliste/v1/zahlen für den Stand.

Einbauen

Linux: Firewall mit nftables

Ein Satz Adressen, den die Firewall kennt, und ein Auftrag, der ihn stündlich auffrischt.

# Einmalig: Satz und Regel anlegen
nft add table inet filter
nft add set inet filter sperrliste '{ type ipv4_addr; flags interval; }'
nft add rule inet filter input ip saddr @sperrliste drop

# /usr/local/bin/sperrliste-holen
#!/bin/bash
set -euo pipefail
LISTE=$(mktemp)
curl -fsS --max-time 60 https://meinebrd.global/api/sperrliste/v1/liste.txt \
  | grep -vE '^#|^$' > "$LISTE"

# Erst prüfen, dann tauschen: Eine leere Antwort darf den Satz nicht leeren.
test -s "$LISTE"

nft flush set inet filter sperrliste
while read -r ip; do nft add element inet filter sperrliste "{ $ip }"; done < "$LISTE"
rm -f "$LISTE"
# Stündlich, mit etwas Versatz damit nicht alle zur vollen Stunde anfragen
17 * * * * root /usr/local/bin/sperrliste-holen

Linux: ipset und iptables

ipset create sperrliste hash:ip -exist
iptables -I INPUT -m set --match-set sperrliste src -j DROP

curl -fsS https://meinebrd.global/api/sperrliste/v1/liste.txt | grep -vE '^#|^$' | while read -r ip; do
  ipset add sperrliste "$ip" -exist
done

fail2ban

Als zusätzliche Quelle neben den eigenen Regeln.

# /etc/fail2ban/action.d/sperrliste.conf ist nicht nötig:
# Die Liste kommt fertig, sie muss nur eingespielt werden.
# /etc/cron.hourly/sperrliste
#!/bin/sh
curl -fsS https://meinebrd.global/api/sperrliste/v1/liste.txt | grep -vE '^#|^$' | while read -r ip; do
  fail2ban-client set sshd banip "$ip" > /dev/null 2>&1
done

nginx

# Erzeugt /etc/nginx/sperrliste.conf, danach nginx -s reload
curl -fsS https://meinebrd.global/api/sperrliste/v1/liste.txt \
  | grep -vE '^#|^$' \
  | sed 's/^/deny /; s/$/;/' > /etc/nginx/sperrliste.conf

# In der Serverkonfiguration:
#   include /etc/nginx/sperrliste.conf;

Apache

curl -fsS https://meinebrd.global/api/sperrliste/v1/liste.txt \
  | grep -vE '^#|^$' \
  | sed 's/^/  Require not ip /' > /etc/apache2/sperrliste.conf

# In der Konfiguration:
# <RequireAll>
#   Require all granted
#   Include /etc/apache2/sperrliste.conf
# </RequireAll>

In der eigenen Anwendung

PHP

<?php
/**
 * Nachschlagen, ob eine Adresse auf der Sperrliste steht.
 *
 * Das Ergebnis wird zwischengespeichert: Bei jedem Seitenaufruf zu
 * fragen macht die eigene Seite so langsam wie die fremde, und wenn
 * die fremde ausfaellt, faellt die eigene mit aus.
 */
function sperrlisteFragt(string $ip): bool
{
    $datei = sys_get_temp_dir() . '/sperrliste_' . md5($ip) . '.json';

    if (is_readable($datei) && time() - filemtime($datei) < 3600) {
        $stand = json_decode(file_get_contents($datei), true);
        return !empty($stand['gesperrt']);
    }

    $ctx = stream_context_create(['http' => ['timeout' => 3]]);
    $roh = @file_get_contents(
        'https://meinebrd.global/api/sperrliste/v1/pruefen?ip=' . urlencode($ip), false, $ctx);

    // Im Zweifel durchlassen. Eine Sperrliste, die bei Stoerung die
    // eigene Seite dichtmacht, ist schlimmer als der Angreifer.
    if ($roh === false) {
        return false;
    }

    file_put_contents($datei, $roh);
    $stand = json_decode($roh, true);

    return !empty($stand['gesperrt']);
}

if (sperrlisteFragt($_SERVER['REMOTE_ADDR'])) {
    http_response_code(403);
    exit('Zugriff nicht moeglich.');
}

Java

import java.net.URI;
import java.net.http.*;
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class Sperrliste {

    private static final String BASIS = "https://meinebrd.global/api/sperrliste/v1";
    private static final HttpClient CLIENT = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(3))
            .build();

    // Gemerkte Antworten, damit nicht jeder Aufruf ueber die Leitung geht.
    private static final Map<String, Eintrag> GEMERKT = new ConcurrentHashMap<>();

    private record Eintrag(boolean gesperrt, long bis) {}

    public static boolean gesperrt(String ip) {
        Eintrag e = GEMERKT.get(ip);
        if (e != null && e.bis() > System.currentTimeMillis()) {
            return e.gesperrt();
        }

        try {
            HttpRequest anfrage = HttpRequest.newBuilder()
                    .uri(URI.create(BASIS + "/pruefen?ip=" + ip))
                    .timeout(Duration.ofSeconds(3))
                    .GET()
                    .build();

            String rumpf = CLIENT.send(anfrage, HttpResponse.BodyHandlers.ofString()).body();
            boolean treffer = rumpf.contains("\"gesperrt\":true");

            GEMERKT.put(ip, new Eintrag(treffer, System.currentTimeMillis() + 3_600_000));
            return treffer;

        } catch (Exception ex) {
            // Im Zweifel durchlassen: Eine Stoerung dort darf hier
            // niemanden aussperren.
            return false;
        }
    }
}

Python

import time
import urllib.request
import urllib.parse
import json

BASIS = "https://meinebrd.global/api/sperrliste/v1"
_gemerkt: dict[str, tuple[bool, float]] = {}


def gesperrt(ip: str) -> bool:
    """Steht die Adresse auf der Sperrliste?

    Gemerkt fuer eine Stunde. Bei Stoerung wird durchgelassen: Eine
    Sperrliste, die bei Ausfall die eigene Seite dichtmacht, ist
    schlimmer als der Angreifer.
    """
    stand = _gemerkt.get(ip)
    if stand and stand[1] > time.time():
        return stand[0]

    try:
        ziel = f"{BASIS}/pruefen?ip={urllib.parse.quote(ip)}"
        with urllib.request.urlopen(ziel, timeout=3) as antwort:
            daten = json.load(antwort)
        treffer = bool(daten.get("gesperrt"))
    except Exception:
        return False

    _gemerkt[ip] = (treffer, time.time() + 3600)
    return treffer

Node.js, als Express-Zwischenschicht

const BASIS = 'https://meinebrd.global/api/sperrliste/v1';
const gemerkt = new Map();

async function gesperrt(ip) {
  const stand = gemerkt.get(ip);
  if (stand && stand.bis > Date.now()) return stand.treffer;

  try {
    const antwort = await fetch(`${BASIS}/pruefen?ip=${encodeURIComponent(ip)}`,
      { signal: AbortSignal.timeout(3000) });
    const daten = await antwort.json();

    gemerkt.set(ip, { treffer: Boolean(daten.gesperrt), bis: Date.now() + 3600000 });
    return Boolean(daten.gesperrt);
  } catch {
    // Im Zweifel durchlassen.
    return false;
  }
}

app.use(async (req, res, next) => {
  if (await gesperrt(req.ip)) return res.status(403).send('Zugriff nicht möglich.');
  return next();
});
Zwei Dinge, die in allen Beispielen gleich sind. Die Antwort wird zwischengespeichert, sonst wird Ihre Seite so langsam wie unsere. Und bei einer Störung wird durchgelassen: Eine Sperrliste, die bei Ausfall die eigene Seite dichtmacht, richtet mehr Schaden an als der Angreifer.

Melden

Melden geht nur mit einem Schlüssel, und den gibt es auf Antrag. Eine Liste, in die jeder ungefragt eintragen kann, ist wertlos: Ein einziger Verärgerter würde genügen, um eine fremde Adresse öffentlich zu brandmarken.

Deshalb prüfen wir jeden Antrag von Hand, und deshalb kommt eine Adresse erst auf die Liste, wenn 3 unabhängige Melder sie nennen.

curl -s -X POST https://meinebrd.global/api/sperrliste/v1/melden \
  -H "Authorization: Bearer sl_IHR_SCHLUESSEL" \
  -H "Content-Type: application/json" \
  -d '{
        "ip": "203.0.113.10",
        "grund": "anmeldeversuche",
        "notiz": "40 Versuche auf /wp-login.php in zwei Minuten"
      }'

Der Schlüssel gehört in den Kopf der Anfrage und nie in den Adressteil: Was dort steht, landet im Protokoll jedes Servers dazwischen.

Mögliche Gründe

Schlüssel beantragen

Wir sehen uns jeden Antrag an. Sagen Sie kurz, wofür Sie melden wollen; daran entscheidet sich die Freigabe. Den Schlüssel bekommen Sie danach per E-Mail, und zwar genau einmal.

Dorthin geht der Schlüssel. Sonst nirgendwohin.

Mindestens ein vollständiger Satz. Ein leeres Feld lehnen wir ab, ohne nachzufragen.

Zu Unrecht auf der Liste?

Das kommt vor. Hinter einer Adresse kann ein Anbieter stehen, hinter dem tausend unbeteiligte Anschlüsse hängen, oder jemand hat einen befallenen Rechner wieder sauber. Schreiben Sie uns mit der Adresse, dann sehen wir nach und nehmen sie herunter. Freigegebene Adressen kommen auch durch neue Meldungen nicht wieder darauf.

Zu jedem Eintrag halten wir fest, warum er dasteht, und geben das auf Nachfrage heraus. Eine IP-Adresse kann ein personenbezogenes Datum sein; Einzelheiten stehen in der Datenschutzerklärung.