Compare commits
2 Commits
bce40d182d
...
tp38-oob
| Author | SHA1 | Date | |
|---|---|---|---|
| e87619e89b | |||
| 177ba100eb |
@@ -353,10 +353,76 @@ def job_tp42_dhcp_domain():
|
||||
return [(["crs326-core"], core_set, "TP42-dhcp-domain-bf")]
|
||||
|
||||
|
||||
def job_tp38_oob():
|
||||
"""TP38 (2026-09-15): OOB-MGMT-Port (ether23 untagged VLAN99) + Branch-Input-Firewall.
|
||||
|
||||
Zwei Geraete, zwei config_sets:
|
||||
* branch: ether23 untaggen, VLAN99 um untagged=ether23 erweitern, 8 Input-Regeln
|
||||
(deny-by-default). Die Regeln werden in der Reihenfolge angelegt, in der das
|
||||
Template sie rendert — established zuerst, drop zuletzt — damit kein LOCKOUT:
|
||||
die laufende Nautobot-SSH-Session (10.40.20.30) matcht auf Regel 1 (established),
|
||||
bevor Regel 8 (drop) greift.
|
||||
* core: ether23 untaggen, VLAN99 untagged=ether23, EINE Regel "OOB->core mgmt" mit
|
||||
place-before vor die Log-Regel (sonst landet sie hinter dem finalen drop und
|
||||
waere wirkungslos + non-compliant).
|
||||
|
||||
KEIN LOCKOUT-RISIKO: ether23 ist auf beiden Geraeten frei (running=false); der
|
||||
VLAN99-Umbau aendert nur `untagged`, nicht `tagged`; der Deploy laeuft von Nautobot
|
||||
(10.40.20.30), das in jeder gesetzten Firewall-Freigabe enthalten bleibt.
|
||||
"""
|
||||
bridge_set = (
|
||||
':do { /interface bridge port set [find interface=ether23] pvid=99 '
|
||||
'frame-types=admit-only-untagged-and-priority-tagged comment="TP38-OOB-MGMT" } '
|
||||
'on-error={ :log info "tp38: ether23 set geworfen" }'
|
||||
)
|
||||
vlan_set = (
|
||||
':do { /interface bridge vlan set [find vlan-ids=99] untagged=ether23 } '
|
||||
'on-error={ :log info "tp38: vlan99 untagged geworfen" }'
|
||||
)
|
||||
|
||||
branch_fw = "\n".join([
|
||||
'add action=accept chain=input comment="TP38: established" connection-state=established,related,untracked',
|
||||
'add action=drop chain=input comment="TP38: invalid" connection-state=invalid',
|
||||
'add action=accept chain=input comment="TP38: icmp" protocol=icmp',
|
||||
'add action=accept chain=input comment="TP38: mndp" dst-port=5678 protocol=udp',
|
||||
'add action=accept chain=input comment="TP38: mgmt LAN" dst-port=22,80,8291,8728 protocol=tcp src-address=10.40.0.0/16',
|
||||
'add action=accept chain=input comment="TP38: mgmt mesh" dst-port=22,80,8291,8728 protocol=tcp src-address=100.96.0.0/12',
|
||||
'add action=log chain=input comment="TP38: input log-only" log-prefix=TP38-BRANCH-WOULD-DROP',
|
||||
'add action=drop chain=input comment="TP38: input deny-by-default"',
|
||||
])
|
||||
branch_set = "\n".join([
|
||||
bridge_set,
|
||||
vlan_set,
|
||||
*[f"/ip firewall filter {ln}" for ln in branch_fw.splitlines()],
|
||||
':log info ("tp38: branch pvid=" . [:tostr [/interface bridge port get [find interface=ether23] pvid]]'
|
||||
' . " fw=" . [:tostr [:len [/ip firewall filter find where comment~"TP38"]]] . "/8")',
|
||||
])
|
||||
|
||||
# ⚠️ `add ... place-before=[find ...]` wird ueber Netmiko IGNORIERT (Regel landet am
|
||||
# Ketten-Ende, also HINTER dem finalen drop = wirkungslos; beim Deploy 2026-09-15 so
|
||||
# aufgeschlagen). Deshalb: erst add (appends), dann `move` vor die Log-Regel.
|
||||
core_set = "\n".join([
|
||||
bridge_set,
|
||||
vlan_set,
|
||||
'/ip firewall filter add action=accept chain=input comment="TP38: OOB->core mgmt" '
|
||||
'dst-port=22,80,8291,8728 in-interface=vlan99 protocol=tcp',
|
||||
'/ip firewall filter move [find where comment="TP38: OOB->core mgmt"] '
|
||||
'destination=[find where comment="TP5: input log-only"]',
|
||||
':log info ("tp38: core pvid=" . [:tostr [/interface bridge port get [find interface=ether23] pvid]]'
|
||||
' . " oob=" . [:tostr [:len [/ip firewall filter find where comment="TP38: OOB->core mgmt"]]])',
|
||||
])
|
||||
|
||||
return [
|
||||
(["crs326-branch"], branch_set, "TP38-oob-branch"),
|
||||
(["crs326-core"], core_set, "TP38-oob-core"),
|
||||
]
|
||||
|
||||
|
||||
JOBS = {
|
||||
"drift-cleanup": job_drift_cleanup,
|
||||
"mgmt-acl": job_mgmt_acl,
|
||||
"tp42-dhcp-domain": job_tp42_dhcp_domain,
|
||||
"tp38-oob": job_tp38_oob,
|
||||
}
|
||||
|
||||
|
||||
|
||||
105
nautobot/ingest-tp38-oob.py
Normal file
105
nautobot/ingest-tp38-oob.py
Normal file
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env python3
|
||||
r"""TP38: OOB-Port + Input-Firewall als Daten nach Nautobot schreiben.
|
||||
|
||||
Zwei getrennte Aenderungen, ein Skript:
|
||||
|
||||
1. BRANCH (crs326-branch): `firewall_filter` im local_config_context_data ANLEGEN
|
||||
(8 Input-Regeln, deny-by-default). Der src-Rahmen 10.40.0.0/16 + 100.96.0.0/12
|
||||
spiegelt bewusst das `available-from` der /ip service-Zeilen — kein neues
|
||||
Fenster, nur stateful + deny-by-default + WOULD-DROP-Log.
|
||||
2. CORE (crs326-core): EINE Regel "TP38: OOB->core mgmt" in die bestehende
|
||||
firewall_filter-Liste EINFUEGEN (vor log/drop), damit der OOB-Port (untagged
|
||||
ether23, VLAN99) auch den Core selbst verwalten kann.
|
||||
|
||||
Attributreihenfolge = alphabetisch, exakt wie RouterOS `/export` sie schreibt
|
||||
(gleiche Konvention wie ingest-core-firewall.py).
|
||||
|
||||
python3 nautobot/ingest-tp38-oob.py # Dry-Run
|
||||
python3 nautobot/ingest-tp38-oob.py --apply
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import urllib.request
|
||||
|
||||
NB = os.environ.get("NAUTOBOT_URL", "http://10.40.20.30:8080")
|
||||
|
||||
BRANCH_RULES = [
|
||||
'add action=accept chain=input comment="TP38: established" connection-state=established,related,untracked',
|
||||
'add action=drop chain=input comment="TP38: invalid" connection-state=invalid',
|
||||
'add action=accept chain=input comment="TP38: icmp" protocol=icmp',
|
||||
'add action=accept chain=input comment="TP38: mndp" dst-port=5678 protocol=udp',
|
||||
'add action=accept chain=input comment="TP38: mgmt LAN" dst-port=22,80,8291,8728 protocol=tcp src-address=10.40.0.0/16',
|
||||
'add action=accept chain=input comment="TP38: mgmt mesh" dst-port=22,80,8291,8728 protocol=tcp src-address=100.96.0.0/12',
|
||||
'add action=log chain=input comment="TP38: input log-only" log-prefix=TP38-BRANCH-WOULD-DROP',
|
||||
'add action=drop chain=input comment="TP38: input deny-by-default"',
|
||||
]
|
||||
|
||||
CORE_NEW_RULE = 'add action=accept chain=input comment="TP38: OOB->core mgmt" dst-port=22,80,8291,8728 in-interface=vlan99 protocol=tcp'
|
||||
|
||||
|
||||
def nb(path, method="GET", payload=None):
|
||||
tok = os.environ.get("NAUTOBOT_TOKEN") or sys.exit("NAUTOBOT_TOKEN nicht gesetzt")
|
||||
req = urllib.request.Request(
|
||||
f"{NB}{path}", method=method,
|
||||
data=json.dumps(payload).encode() if payload is not None else None,
|
||||
headers={"Authorization": f"Token {tok}", "Content-Type": "application/json",
|
||||
"Accept": "application/json"})
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
def get_device(name):
|
||||
return nb(f"/api/dcim/devices/?name={name}&depth=1")["results"][0]
|
||||
|
||||
|
||||
def patch_device(dev, ctx, label):
|
||||
dev_id = dev["id"]
|
||||
nb(f"/api/dcim/devices/{dev_id}/", "PATCH", {"local_config_context_data": ctx})
|
||||
print(f" ~ {label} geschrieben.")
|
||||
|
||||
|
||||
def main():
|
||||
apply_ = "--apply" in sys.argv
|
||||
|
||||
# --- BRANCH ---
|
||||
branch = get_device("crs326-branch")
|
||||
bctx = dict(branch.get("local_config_context_data") or {})
|
||||
if bctx.get("firewall_filter") == BRANCH_RULES:
|
||||
print("== branch: firewall_filter unveraendert ==")
|
||||
else:
|
||||
bctx["firewall_filter"] = BRANCH_RULES
|
||||
print(f"== branch: {len(BRANCH_RULES)} Input-Regeln ==")
|
||||
for ln in BRANCH_RULES:
|
||||
print(" " + ln)
|
||||
if apply_:
|
||||
patch_device(branch, bctx, "branch firewall_filter")
|
||||
|
||||
# --- CORE ---
|
||||
core = get_device("crs326-core")
|
||||
cctx = dict(core.get("local_config_context_data") or {})
|
||||
ff = list(cctx.get("firewall_filter") or [])
|
||||
if CORE_NEW_RULE in ff:
|
||||
print("\n== core: Regel schon vorhanden ==")
|
||||
else:
|
||||
# vor der Log-Regel einsortieren (die sitzt direkt vor dem finalen drop).
|
||||
insert_at = len(ff)
|
||||
for i, ln in enumerate(ff):
|
||||
if "log-prefix=TP5-INPUT-WOULD-DROP" in ln:
|
||||
insert_at = i
|
||||
break
|
||||
ff.insert(insert_at, CORE_NEW_RULE)
|
||||
cctx["firewall_filter"] = ff
|
||||
print(f"\n== core: fuege OOB-Regel an Position {insert_at} ein ==")
|
||||
print(" " + CORE_NEW_RULE)
|
||||
if apply_:
|
||||
patch_device(core, cctx, "core firewall_filter")
|
||||
|
||||
if not apply_:
|
||||
print("\nDRY-RUN — mit --apply schreiben. Danach: "
|
||||
"push-gc-template.py + gc-configplan.py --refresh-compliance")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -73,9 +73,11 @@
|
||||
/interface bridge port add bridge=bridge comment=defconf interface=ether14 pvid=30
|
||||
/interface bridge port add bridge=bridge comment=defconf interface=ether15
|
||||
/interface bridge port add bridge=bridge comment=defconf interface=ether16 pvid=100
|
||||
{% for n in range(17, 25) %}
|
||||
{% for n in range(17, 23) %}
|
||||
/interface bridge port add bridge=bridge comment=defconf interface=ether{{ n }}
|
||||
{% endfor %}
|
||||
/interface bridge port add bridge=bridge comment=TP38-OOB-MGMT frame-types=admit-only-untagged-and-priority-tagged interface=ether23 pvid=99
|
||||
/interface bridge port add bridge=bridge comment=defconf interface=ether24
|
||||
/interface bridge port add bridge=bridge comment=defconf interface=sfp-sfpplus1
|
||||
/interface bridge port add bridge=bridge comment=defconf interface=sfp-sfpplus2
|
||||
# ether4 = Wohnzimmer-Sideboard: 2026-08-13 von Trunk auf untagged VLAN40 gezogen (TP21.1/4).
|
||||
@@ -94,7 +96,7 @@
|
||||
/interface bridge vlan add bridge=bridge tagged=ether1,ether3,ether8,ether13,ether15,ether21 untagged=ether2,ether4,ether5 vlan-ids=40
|
||||
/interface bridge vlan add bridge=bridge tagged=ether1,ether3,ether8,ether15,ether21 vlan-ids=50
|
||||
/interface bridge vlan add bridge=bridge tagged=ether1,ether3,ether8,ether15,ether21 vlan-ids=90
|
||||
/interface bridge vlan add bridge=bridge tagged=ether3,ether8,ether15,ether21,bridge vlan-ids=99
|
||||
/interface bridge vlan add bridge=bridge tagged=ether3,ether8,ether15,ether21,bridge untagged=ether23 vlan-ids=99
|
||||
/interface bridge vlan add bridge=bridge tagged=ether1 untagged=ether16 vlan-ids=100
|
||||
|
||||
# [1] INTERFACE-LISTS
|
||||
@@ -422,9 +424,20 @@
|
||||
/system ntp client servers add address=10.40.99.1
|
||||
/ip route add comment=TP21-default dst-address=0.0.0.0/0 gateway=10.40.99.1
|
||||
|
||||
# INPUT-FIREWALL (TP38, 2026-09-15) — deny-by-default fuer die Mgmt-Ebene (10.40.99.2).
|
||||
# Der Branch ist L2-only: ALLE Mgmt-Anfragen kommen auf der einen vlan99-SVI an, egal ob
|
||||
# VLAN10 (ueber den Core geroutet), cf-mesh (100.96.0.0/12) oder der OOB-Port (untagged
|
||||
# ether23, 10.40.99.x). Deshalb matcht die Quelle per src-address, nicht per in-interface.
|
||||
# Der src-Rahmen 10.40.0.0/16 + 100.96.0.0/12 spiegelt bewusst das `available-from` der
|
||||
# /ip service-Zeilen — kein neues Fenster, nur stateful + deny-by-default + WOULD-DROP-Log.
|
||||
{% for line in obj.get_config_context().get("firewall_filter", []) %}
|
||||
/ip firewall filter {{ line }}
|
||||
{% endfor %}
|
||||
|
||||
# L2-FABRIC — wie beim Core ein Literal-Block (Begruendung dort).
|
||||
# ether9-23 tragen pvid=20 OHNE VLAN-Membership (Bootstrap-Rev4-Rest, funktionslos) —
|
||||
# ether9-22 tragen pvid=20 OHNE VLAN-Membership (Bootstrap-Rev4-Rest, funktionslos) —
|
||||
# bewusst so abgebildet, damit eine spaetere Bereinigung als Drift sichtbar wird.
|
||||
# ether23 ist seit TP38 der OOB-MGMT-Port (untagged VLAN99), s. unten.
|
||||
/interface bridge port add bridge=bridge comment=defconf interface=ether1
|
||||
/interface bridge port add bridge=bridge comment=defconf interface=ether2
|
||||
/interface bridge port add bridge=bridge comment=defconf interface=ether3 pvid=10
|
||||
@@ -433,9 +446,10 @@
|
||||
/interface bridge port add bridge=bridge comment=defconf interface=ether6 pvid=40
|
||||
/interface bridge port add bridge=bridge comment=defconf interface=ether7 pvid=20
|
||||
/interface bridge port add bridge=bridge comment=defconf interface=ether8
|
||||
{% for n in range(9, 24) %}
|
||||
{% for n in range(9, 23) %}
|
||||
/interface bridge port add bridge=bridge comment=defconf interface=ether{{ n }} pvid=20
|
||||
{% endfor %}
|
||||
/interface bridge port add bridge=bridge comment=TP38-OOB-MGMT frame-types=admit-only-untagged-and-priority-tagged interface=ether23 pvid=99
|
||||
/interface bridge port add bridge=bridge comment=defconf interface=ether24
|
||||
/interface bridge port add bridge=bridge comment=defconf disabled=yes interface=sfp-sfpplus1
|
||||
/interface bridge port add bridge=bridge comment=defconf disabled=yes interface=sfp-sfpplus2
|
||||
@@ -445,7 +459,7 @@
|
||||
/interface bridge vlan add bridge=bridge comment=MEDIA tagged=ether1,ether24,bridge untagged=ether5,ether6 vlan-ids=40
|
||||
/interface bridge vlan add bridge=bridge comment=UNTRUSTED tagged=ether1,ether24,bridge vlan-ids=50
|
||||
/interface bridge vlan add bridge=bridge comment=GUEST tagged=ether1,ether24,bridge vlan-ids=90
|
||||
/interface bridge vlan add bridge=bridge comment=MGMT tagged=ether1,ether24,bridge vlan-ids=99
|
||||
/interface bridge vlan add bridge=bridge comment=MGMT tagged=ether1,ether24,bridge untagged=ether23 vlan-ids=99
|
||||
{% elif obj.role.name == "Wireless AP" %}
|
||||
{% set ap = obj.get_config_context()["ap"] %}
|
||||
{% set mgmt_host = obj.primary_ip4.host %}
|
||||
|
||||
Reference in New Issue
Block a user