Wie wir Prefix-Policies mit IRRd, Ansible und MikroTik RouterOS als kontrollierten Change gebaut haben
„Das läuft doch seit Jahren. Warum sollten wir daran etwas ändern?“
Diesen Gedanken kann man bei BGP-Prefix-Filtern gut nachvollziehen. Eine einmal sauber aufgebaute Address-List funktioniert im laufenden Betrieb oft über sehr lange Zeiträume – bis sich auf der Gegenseite etwas ändert.
Ein Kunde registriert ein zusätzliches Prefix. Ein Peer ergänzt ein neues Route-Objekt. Ein AS-SET wächst. Oder ein Prefix wird aus dem IRR entfernt.
Die technische Änderung ist meistens klein. Der operative Aufwand dahinter ist es nicht.
Man muss die Quelle prüfen, Prefixe validieren, mehrere Router konsistent aktualisieren, mögliche Auswirkungen auf bestehende BGP-Sessions berücksichtigen und den Vorgang sauber dokumentieren.
Bei einem Router ist das überschaubar. Mit jedem weiteren Kunden, Peering und Edge-Router wird aus einer kleinen Konfigurationsänderung ein Prozessproblem.
Genau diesen Ablauf haben wir deshalb automatisiert.
Das Ergebnis ist eine Ansible-basierte Lösung, die:
- IRR-Daten über die IRRd-GraphQL-API abfragt
- Prefixe technisch und organisatorisch validiert
- RouterOS-Address-Lists auf mehreren MikroTik-Routern abgleicht
- Änderungen kontrolliert ausführt
- bei Bedarf betroffene BGP-Verbindungen neu initialisiert
- pro Lauf genau ein Ticket erzeugt
- und auch bei „keine Änderungen“ einen vollständigen Betriebsnachweis liefert
Der interessante Teil war dabei nicht, ein Prefix auf einen Router zu schreiben.
Der interessante Teil war, den gesamten Prozess so zu bauen, dass er auch bei fehlerhaften Daten, teilweise ausgeführten Änderungen oder einer ausgefallenen BGP-Session nachvollziehbar und sicher bleibt.
Die gezeigten Beispiele stammen aus unserer produktiven RouterOS-7-Infrastruktur. AS-Nummern, Prefixe, Listennamen und weitere Details wurden abstrahiert oder durch Dokumentationsbereiche ersetzt.
Ausgangslage
Wir betreiben mehrere MikroTik-RouterOS-7-Systeme als BGP-Edges.
Auf den Routern werden Address-Lists unter anderem für folgende Aufgaben genutzt:
- Kunden-Prefixe
- bilaterale Peerings
- Prefix-Filter
- input.accept-nlri
- Routing-Policies
- RTBH-Freigaben
- weitere sicherheitsrelevante Filter
Für einen Kunden oder Peer existiert dabei typischerweise eine eigene Address-List, zum Beispiel:
Diese Listen wurden bisher manuell gepflegt.
Das funktioniert, solange Änderungen selten sind. Mit zunehmender Anzahl an Kunden, Peerings und Routern entstehen aber typische Probleme:
- Die Liste auf Router A ist aktuell, auf Router B nicht.
- Ein Prefix ist im IRR registriert, im Filter aber noch nicht enthalten.
- Ein veraltetes Prefix bleibt auf dem Router bestehen.
- Eine Änderung wird durchgeführt, aber nicht dokumentiert.
- Ein Fehler tritt nach dem Schreiben auf, obwohl der eigentliche Eintrag bereits angelegt wurde.
- Ein automatischer Job läuft nicht, aber niemand bemerkt es.
- Eine Address-List ändert sich, wird von input.accept-nlri aber erst nach einem BGP-Neustart berücksichtigt.
Aus einer kleinen Konfigurationsänderung wird so schnell ein Prozessproblem. Und Prozessprobleme fallen im Netzbetrieb meist nicht sofort auf, sondern erst dann, wenn ein Kunde etwas nicht erreicht.
Das Zielbild: ein kontrollierter Change, kein blinder Write
Die Automatisierung sollte nicht einfach IRR-Daten auf Router schreiben.
Sie sollte sich wie ein kontrollierter Change verhalten.
Der gewünschte Ablauf war:
Zusätzlich sollte ein Lauf ohne Änderungen ebenfalls ein Ticket erzeugen.
Das war eine bewusste Entscheidung.
Ein fehlendes Ticket bedeutet dadurch nicht „es gab nichts zu tun“, sondern „der Lauf hat möglicherweise gar nicht stattgefunden“.
Ein Ticket mit:
ist deshalb kein Rauschen, sondern ein technischer Betriebsnachweis.
Die Datenquelle: IRRd per GraphQL
Als Quelle nutzen wir IRRd.
Die Abfrage erfolgt nicht über klassischen WHOIS-Text, sondern über die GraphQL-Schnittstelle.
Das hat mehrere Vorteile:
- strukturierte Antworten
- eindeutige Fehlererkennung
- saubere Trennung zwischen ASN- und AS-SET-Abfragen
- einfache Weiterverarbeitung in Ansible
- keine fehleranfällige Textauswertung
Wir unterstützen zwei Objekttypen:
und
Eine anonymisierte Policy sieht beispielsweise so aus:
Für ein AS-SET entsprechend:
Das Feld:
war dabei besonders hilfreich.
Damit konnten wir AS-SET-Abfragen und größere Datenmengen vollständig testen, ohne die Ergebnisse tatsächlich auf einen Router zu schreiben. Die Policy wird abgefragt und validiert, aber nicht angewendet.
Warum Validierung wichtiger ist als die API-Abfrage
Ein erfolgreicher HTTP-Status bedeutet noch lange nicht, dass ein Ergebnis für den Produktivbetrieb geeignet ist.
Deshalb prüfen wir vor jedem Write unter anderem:
- Ist das Ergebnis überhaupt vorhanden?
- Ist die Anzahl der Prefixe plausibel?
- Handelt es sich ausschließlich um IPv4-CIDRs?
- Ist die Präfixlänge zulässig?
- Ist 0.0.0.0/0 enthalten?
- Sind private Netze enthalten?
- Sind Loopback-, Link-Local- oder Multicast-Bereiche enthalten?
- Überschreitet die Antwort ein definiertes Maximum?
- Ist die Antwort ungewöhnlich klein?
Eine typische Begrenzung sieht so aus:
Damit verhindern wir beispielsweise, dass ein fehlerhaftes IRR-Objekt plötzlich tausende Prefixe in eine Kundenliste schreibt.
Die maximale Prefixanzahl ist keine technische Komfortfunktion.
Sie ist ein Schutzmechanismus gegen fehlerhafte oder unerwartete Quelldaten.
Soll-Ist-Vergleich auf dem Router
Die Router werden über die native RouterOS-API angesprochen.
Zunächst lesen wir die bestehenden Address-List-Einträge:
Danach werden zwei Mengen berechnet:
Ein Beispiel:
Das Ergebnis lautet:
Dadurch bleibt der Prozess idempotent.
Ein zweiter Lauf mit demselben Datenstand verändert nichts mehr.
Genau das konnten wir anschließend im produktiven Test sehen:
Beide Router meldeten:
Das ist das gewünschte Verhalten einer sauberen Infrastrukturautomatisierung.
Additions immer vor Removals
Ein wichtiges Designprinzip war die Reihenfolge.
Neue Prefixe werden immer zuerst hinzugefügt. Erst danach werden veraltete Einträge entfernt.
Damit reduzieren wir das Risiko, bei einem teilweise fehlerhaften Lauf die gültige Erreichbarkeit zu beeinträchtigen.
Der Ablauf lautet:
Und ausdrücklich nicht:
Ein vollständiges Löschen und Neuerstellen wäre einfacher zu programmieren, betrieblich aber deutlich riskanter.
Gerade wenn Address-Lists direkt in Routing-Filtern oder in input.accept-nlri verwendet werden, kann ein kurzer leerer Zustand bereits Auswirkungen haben.
Eine Besonderheit von input.accept-nlri
Eine der Address-Lists wurde auf einem Router als input.accept-nlri verwendet.
Das bedeutet: Die Liste wirkt nicht erst innerhalb einer nachgelagerten Routing-Filterregel, sondern bereits beim Empfang der NLRI.
Eine Änderung an der Address-List wird dadurch nicht zwingend sofort auf eine bestehende BGP-Session angewendet. Die betroffene BGP-Verbindung muss neu initialisiert werden.
Dafür wurde die Policy erweitert:
Damit ist der Neustart nicht pauschal auf allen Routern aktiv.
Er erfolgt nur:
- bei einer tatsächlichen Prefixänderung
- auf dem definierten Router
- für eine passende BGP-Connection
- wenn die Connection administrativ aktiviert ist
Das war wichtig, weil wir keine absichtlich deaktivierte BGP-Connection automatisch wieder einschalten wollten.
Die erste Hürde: BGP-Session ist kein gültiger API-Pfad
Der erste Ansatz war, die aktive Session direkt über:
abzufragen.
In der RouterOS-CLI existiert diese Sicht. Das verwendete Ansible-Modul akzeptierte diesen Pfad für api_info allerdings nicht.
Die Lösung war, nicht die dynamische Session zu verwalten, sondern die zugrunde liegende statische Konfiguration:
Das ist ohnehin der bessere Ort für einen kontrollierten Neustart.
Der Ablauf wurde deshalb geändert zu:
Damit funktioniert der Neustart auch dann, wenn die Session zum Zeitpunkt des Runs bereits down ist. Eine bestehende Session ist dafür nicht erforderlich.
Die zweite Hürde: RouterOS .id und Ansible
RouterOS liefert interne Objekt-IDs mit einem Schlüssel wie:
Naheliegend wäre in Ansible:
Das funktioniert allerdings nicht zuverlässig.
Ansible interpretiert den führenden Punkt als Teil eines Attributpfades und versucht dabei, auf ein leeres Attribut zuzugreifen. Das Ergebnis war:
Die Lösung war der direkte Dictionary-Zugriff:
Die IDs werden deshalb in einer Schleife gesammelt:
Das ist ein kleines Detail. Aber genau solche Details entscheiden darüber, ob eine Automatisierung im Labor oder im Produktionsbetrieb funktioniert.
Fehlerbehandlung mit block und rescue
Ein weiterer wichtiger Punkt war die Behandlung teilweise ausgeführter Runs.
Ein Beispiel aus dem Test:
- Das Prefix wurde erfolgreich auf beide Router geschrieben.
- Der anschließende BGP-Neustart schlug fehl.
- Die Automatisierung durfte jetzt nicht behaupten, es sei nichts passiert.
- Gleichzeitig durfte sie den bereits angelegten Eintrag nicht ignorieren.
Deshalb läuft jede Policy innerhalb eines Ansible-Blocks:
Dadurch kann das Ticket auch bei einem Fehler enthalten:
Das ist wesentlich aussagekräftiger als ein pauschales:
Ein Ticket pro Run
Wir wollten bewusst kein Ticket pro Router und kein Ticket pro Policy. Stattdessen entsteht genau ein Ticket für den gesamten Lauf.
Das Ticket enthält:
- Startzeit
- Endzeit
- IRRd-Status
- verwendete Quellen
- Anzahl der Additions
- Anzahl der Removals
- Ergebnis je Router
- Ergebnis je Policy
- betroffene Address-List
- durchgeführter BGP-Neustart
- Fehlermeldungen
- nicht angewendete Test-Policies
Die Titel werden dynamisch erzeugt.
Ohne Änderungen:
Mit Änderungen:
Bei Fehlern:
Die Ticketerstellung erfolgt über einen HTTP-POST auf einen Webhook.
Wichtig war die Reihenfolge:
So bleibt der Job bei Problemen technisch rot, aber die Dokumentation wird trotzdem erstellt.
Der letzte Task lautet sinngemäß:
Bei einem erfolgreichen Lauf wird dieser Task übersprungen. Bei einem Fehler ist das Ticket bereits vorhanden, bevor Ansible mit einem Fehlercode endet.
Warum auch „keine Änderungen“ ein Ticket erzeugt
Man könnte argumentieren, dass ein No-Change-Run kein Ticket benötigt.
Für uns war die Nachvollziehbarkeit wichtiger.
Ein tägliches Ticket mit:
beweist:
- der Timer wurde ausgelöst
- der Controller war funktionsfähig
- IRRd war erreichbar
- die Policies wurden verarbeitet
- beide Router waren erreichbar
- die API-Anmeldung funktionierte
- der Soll-Ist-Vergleich wurde durchgeführt
- es bestand kein Handlungsbedarf
Ohne dieses Ticket müsste man bei fehlenden Änderungen zusätzlich prüfen, ob der Job überhaupt gelaufen ist.
Die Automatisierung erzeugt damit nicht nur Changes, sondern auch einen täglichen Betriebsnachweis.
Der systemd-Timer
Nach den erfolgreichen manuellen Tests wurde das Playbook über einen systemd-Timer produktiv geschaltet.
Der Service läuft als dedizierter Benutzer:
Zusätzlich nutzen wir flock, um parallele Läufe zu verhindern.
Der Wrapper startet sinngemäß:
Damit kann ein verzögerter Lauf nicht mit dem nächsten Zeitplan kollidieren.
Die nächste Hürde: systemd-Hardening und Ansible-Temp-Dateien
Der Service wurde zunächst mit:
gehärtet.
Das führte beim ersten Start zu:
Ansible benötigt sowohl ein lokales als auch ein „Remote“-Temp-Verzeichnis. Das gilt auch dann, wenn Tasks auf localhost ausgeführt werden.
Die Lösung war:
Zusammen mit:
erhält der Service einen eigenen beschreibbaren Temp-Bereich, während das Home-Verzeichnis weiterhin read-only bleiben kann.
Danach endete der Service sauber mit:
und
Das wirkt auf den ersten Blick ungewöhnlich, ist bei einem erfolgreichen Type=oneshot-Service aber genau richtig. Der Job läuft, beendet sich erfolgreich und bleibt nicht dauerhaft aktiv.
Der finale Ablauf
Heute arbeitet die Automatisierung folgendermaßen:
Bei unverändertem Zustand:
Bei erfolgreichen Änderungen:
Bei einem Fehler:
Was wir bewusst nicht automatisiert haben
Nicht jede Address-List sollte ungeprüft vollständig aus dem IRR verwaltet werden.
Gemeinsam genutzte Listen, in denen statische eigene Prefixe und dynamisch ermittelte Kundenprefixe gemischt sind, benötigen eine andere Logik. Dort wäre ein einfaches Soll-Ist-Verfahren gefährlich.
Für solche Listen braucht es beispielsweise:
- getrennte statische und dynamische Listen
- eine kontrollierte Merge-Logik
- eindeutige Kommentare oder Ownership-Marker
- Schutz vor dem Löschen manuell verwalteter Einträge
Automatisierung sollte nicht bedeuten, möglichst viel automatisch zu verändern.
Sie sollte bedeuten, klar definierte Zuständigkeiten zuverlässig umzusetzen.
Die wichtigsten Punkte zusammengefasst
- Der Router-Write ist der kleinste Teil Ein Prefix auf einen Router zu schreiben, ist trivial. Die eigentliche Arbeit steckt in Datenquelle, Validierung, Fehlerbehandlung und Nachweis.
- Ein erfolgreicher API-Status ist kein Freibrief HTTP 200 sagt nichts über die Qualität der Daten. Erst die Validierung entscheidet, ob ein Ergebnis produktiv verwendet werden darf.
- Eine maximale Prefixanzahl ist ein Schutzmechanismus max_prefixes verhindert, dass ein fehlerhaftes IRR-Objekt tausende Einträge in eine Kundenliste schreibt.
- Additions gehören vor Removals Zuerst hinzufügen, dann entfernen. Das reduziert das Risiko, bei einem Teilfehler gültige Erreichbarkeit zu verlieren.
- input.accept-nlri braucht einen gezielten BGP-Neustart Eine geänderte Address-List wirkt nicht automatisch auf eine bestehende Session. Der Neustart muss kontrolliert und nur dort erfolgen, wo er nötig ist.
- Teilfehler müssen dokumentiert werden block und rescue sorgen dafür, dass ein Ticket auch bei einem Fehler zeigt, was tatsächlich passiert ist – und nicht nur „Automation failed“.
- Auch ein No-Change-Run braucht ein Ticket Ein „keine Änderungen“-Ticket ist der tägliche Beweis, dass der Job wirklich gelaufen ist. Kein Ticket ist ein Alarmsignal, keine Entwarnung.
- Erst das Ticket, dann der rote Exit-Code Bei Fehlern muss die Dokumentation entstehen, bevor der Job abbricht. Sonst fehlt genau bei Problemen der Nachweis.
Fazit
Der eigentliche Router-Write war der kleinste Teil dieser Lösung.
Die entscheidenden Fragen waren:
- Woher kommen die Daten?
- Wie werden sie validiert?
- Wie verhindern wir unplausible Änderungen?
- Was passiert bei einem Teilfehler?
- Wie gehen wir mit einer bereits ausgeführten Addition um?
- Wann muss eine BGP-Verbindung neu gestartet werden?
- Was passiert, wenn die Session bereits down ist?
- Wie dokumentieren wir einen erfolgreichen Leerlauf?
- Wie stellen wir sicher, dass der Job täglich wirklich läuft?
Erst durch diese Punkte wurde aus einem Script eine belastbare Betriebsautomatisierung.
Für uns ist das der Unterschied zwischen:
und:
Der erste Ansatz spart ein paar Befehle.
Der zweite schafft Konsistenz, Nachvollziehbarkeit und einen reproduzierbaren Betrieb.

