Ubiquity UDM - Geräte sperren
Client-Quarantäne auf dem UniFi Gateway
Die Client-Quarantäne dient dazu, einen verdächtigen oder kompromittierten Netzwerk-Client im Ernstfall innerhalb weniger Sekunden über SSH zu sperren. Der Client wird anhand seiner MAC-Adresse identifiziert, sodass ein Wechsel der IPv4-Adresse die Sperre nicht umgeht.
Der Hauptgrund für diese Lösung ist, dass unsere Client-Netzwerke teilweise Zugriff auf viele andere Netzwerkzonen besitzen. Im Sicherheitsvorfall wäre es zu langsam und fehleranfällig, für jede einzelne Zone manuell eine Firewallregel anzulegen. Stattdessen wird die Quarantäne direkt vor den regulären UniFi-Firewallregeln eingehängt.
Dabei werden IPv4, IPv6 und über das Gateway laufender Netzwerkverkehr des betroffenen Clients blockiert. Bestehende Verbindungen werden zusätzlich aus Conntrack entfernt.
Wichtig: Verkehr zwischen zwei Geräten innerhalb desselben VLANs kann unter Umständen direkt über einen Switch oder Access Point laufen und das Gateway gar nicht erreichen. Bei einem besonders kritischen Sicherheitsvorfall sollte daher zusätzlich der Switch-Port deaktiviert oder der WLAN-Client isoliert werden.
Hinweis: Die hier verwendeten Regeln werden direkt per SSH verwaltet und liegen außerhalb der normalen UniFi-Konfiguration. Nach Firmware-Updates, Neustarts oder einer Neu-Provisionierung sollte der Status der Quarantäne geprüft und gegebenenfalls mit quarantine-client apply erneut angewendet werden.
Installation
Installation
Die Installation erfolgt als root direkt per SSH auf dem UniFi Gateway. Das Skript und seine Konfiguration werden unter /persistent/client-quarantine gespeichert.
Der folgende Block installiert das komplette Quarantäne-Werkzeug:
install -d -m 700 -o root -g root /persistent/client-quarantine
cat > /persistent/client-quarantine/quarantine-client <<'QUARANTINE_SCRIPT'
#!/bin/bash
set -Eeuo pipefail
PATH='/usr/sbin:/usr/bin:/sbin:/bin'
umask 077
readonly BASE_DIR='/persistent/client-quarantine'
readonly STATE_FILE="${BASE_DIR}/blocked-macs"
readonly LOCK_FILE="${BASE_DIR}/lock"
readonly IPT4_CHAIN='QRTN_MAC_V4'
readonly IPT6_CHAIN='QRTN_MAC_V6'
readonly EBT_BR_CHAIN='QRTN_MAC_BR'
readonly EBT_IN_CHAIN='QRTN_MAC_IN'
readonly EBT_FWD_CHAIN='QRTN_MAC_FWD'
readonly EBT_OUT_CHAIN='QRTN_MAC_OUT'
die() {
printf 'Fehler: %s\n' "$*" >&2
exit 1
}
log_message() {
logger -t client-quarantine -- "$*" 2>/dev/null || true
printf '%s\n' "$*"
}
require_root() {
[ "${EUID}" -eq 0 ] ||
die 'Dieses Werkzeug muss als root ausgeführt werden.'
}
require_commands() {
local command_name
for command_name in \
awk sort mktemp install ip \
iptables ip6tables ebtables \
conntrack logger flock
do
command -v "${command_name}" >/dev/null 2>&1 ||
die "Benötigter Befehl fehlt: ${command_name}"
done
}
prepare_storage() {
install -d -m 700 -o root -g root "${BASE_DIR}"
touch "${STATE_FILE}" "${LOCK_FILE}"
chown root:root "${STATE_FILE}" "${LOCK_FILE}"
chmod 600 "${STATE_FILE}" "${LOCK_FILE}"
}
normalize_mac() {
local mac="${1,,}"
[[ "${mac}" =~ ^([0-9a-f]{2}:){5}[0-9a-f]{2}$ ]] ||
die 'Ungültige MAC-Adresse. Erwartet wird aa:bb:cc:dd:ee:ff.'
[ "${mac}" != '00:00:00:00:00:00' ] ||
die 'Die Null-MAC ist nicht zulässig.'
[ "${mac}" != 'ff:ff:ff:ff:ff:ff' ] ||
die 'Die Broadcast-MAC ist nicht zulässig.'
local first_octet="${mac%%:*}"
(( (16#${first_octet} & 1) == 0 )) ||
die 'Multicast-MAC-Adressen sind nicht zulässig.'
printf '%s\n' "${mac}"
}
reject_gateway_mac() {
local target="$1"
local address_file
local local_mac
for address_file in /sys/class/net/*/address; do
[ -r "${address_file}" ] || continue
IFS= read -r local_mac < "${address_file}" || true
local_mac="${local_mac,,}"
[ "${target}" != "${local_mac}" ] ||
die "Abbruch: ${target} gehört zu einer Gateway-Schnittstelle."
done
}
get_ssh_peer_mac() {
[ -n "${SSH_CONNECTION:-}" ] || return 0
local peer_ip="${SSH_CONNECTION%% *}"
ip neigh show to "${peer_ip}" 2>/dev/null |
awk '
{
for (i = 1; i <= NF; i++) {
if ($i == "lladdr") {
print tolower($(i + 1))
exit
}
}
}
'
}
reject_current_ssh_peer() {
local target="$1"
local peer_mac
peer_mac="$(get_ssh_peer_mac || true)"
if [ -n "${peer_mac}" ] && [ "${target}" = "${peer_mac}" ]; then
die 'Diese MAC gehört zum aktuellen SSH-Gegenüber. Verwende add --force nur bei bewusstem Verbindungsabbruch.'
fi
}
read_macs() {
awk '
/^[[:space:]]*$/ {
next
}
{
print tolower($1)
}
' "${STATE_FILE}" |
sort -u
}
add_to_state() {
local mac="$1"
local temporary_file
temporary_file="$(mktemp "${BASE_DIR}/blocked-macs.XXXXXX")"
{
read_macs
printf '%s\n' "${mac}"
} |
sort -u > "${temporary_file}"
chown root:root "${temporary_file}"
chmod 600 "${temporary_file}"
mv -f "${temporary_file}" "${STATE_FILE}"
}
remove_from_state() {
local mac="$1"
local temporary_file
temporary_file="$(mktemp "${BASE_DIR}/blocked-macs.XXXXXX")"
awk -v remove_mac="${mac}" '
NF && tolower($1) != remove_mac {
print tolower($1)
}
' "${STATE_FILE}" |
sort -u > "${temporary_file}"
chown root:root "${temporary_file}"
chmod 600 "${temporary_file}"
mv -f "${temporary_file}" "${STATE_FILE}"
}
prepare_iptables_chain() {
local binary="$1"
local custom_chain="$2"
"${binary}" -w 5 -t mangle -N "${custom_chain}" 2>/dev/null ||
true
"${binary}" -w 5 -t mangle -F "${custom_chain}"
while "${binary}" \
-w 5 \
-t mangle \
-C PREROUTING \
-j "${custom_chain}" \
2>/dev/null
do
"${binary}" \
-w 5 \
-t mangle \
-D PREROUTING \
-j "${custom_chain}"
done
"${binary}" \
-w 5 \
-t mangle \
-I PREROUTING 1 \
-j "${custom_chain}"
}
prepare_ebtables_chain() {
local table="$1"
local builtin_chain="$2"
local custom_chain="$3"
ebtables \
-t "${table}" \
-N "${custom_chain}" \
2>/dev/null ||
true
ebtables \
-t "${table}" \
-F "${custom_chain}"
while ebtables \
-t "${table}" \
-D "${builtin_chain}" \
-j "${custom_chain}" \
2>/dev/null
do
:
done
ebtables \
-t "${table}" \
-I "${builtin_chain}" 1 \
-j "${custom_chain}"
}
apply_rules() {
local mac
prepare_iptables_chain iptables "${IPT4_CHAIN}"
prepare_iptables_chain ip6tables "${IPT6_CHAIN}"
prepare_ebtables_chain \
broute \
BROUTING \
"${EBT_BR_CHAIN}"
prepare_ebtables_chain \
filter \
INPUT \
"${EBT_IN_CHAIN}"
prepare_ebtables_chain \
filter \
FORWARD \
"${EBT_FWD_CHAIN}"
prepare_ebtables_chain \
filter \
OUTPUT \
"${EBT_OUT_CHAIN}"
while IFS= read -r mac; do
[ -n "${mac}" ] || continue
iptables \
-w 5 \
-t mangle \
-A "${IPT4_CHAIN}" \
-m mac \
--mac-source "${mac}" \
-j DROP
ip6tables \
-w 5 \
-t mangle \
-A "${IPT6_CHAIN}" \
-m mac \
--mac-source "${mac}" \
-j DROP
ebtables \
-t broute \
-A "${EBT_BR_CHAIN}" \
-s "${mac}" \
-j RETURN
ebtables \
-t filter \
-A "${EBT_IN_CHAIN}" \
-s "${mac}" \
-j DROP
ebtables \
-t filter \
-A "${EBT_FWD_CHAIN}" \
-s "${mac}" \
-j DROP
ebtables \
-t filter \
-A "${EBT_FWD_CHAIN}" \
-d "${mac}" \
-j DROP
ebtables \
-t filter \
-A "${EBT_OUT_CHAIN}" \
-d "${mac}" \
-j DROP
done < <(read_macs)
}
known_addresses_for_mac() {
local family="$1"
local mac="$2"
ip "-${family}" -o neigh show 2>/dev/null |
awk -v mac="${mac}" '
{
for (i = 1; i <= NF; i++) {
if ($i == "lladdr" && tolower($(i + 1)) == mac) {
print $1
break
}
}
}
' |
sort -u
}
clear_conntrack_for_mac() {
local mac="$1"
local address
while IFS= read -r address; do
[ -n "${address}" ] || continue
conntrack \
-D \
-f ipv4 \
-s "${address}" \
>/dev/null 2>&1 ||
true
conntrack \
-D \
-f ipv4 \
-d "${address}" \
>/dev/null 2>&1 ||
true
done < <(known_addresses_for_mac 4 "${mac}")
while IFS= read -r address; do
[ -n "${address}" ] || continue
conntrack \
-D \
-f ipv6 \
-s "${address}" \
>/dev/null 2>&1 ||
true
conntrack \
-D \
-f ipv6 \
-d "${address}" \
>/dev/null 2>&1 ||
true
done < <(known_addresses_for_mac 6 "${mac}")
}
command_add() {
local force=0
local mac
if [ "${1:-}" = '--force' ]; then
force=1
shift
fi
[ "$#" -eq 1 ] ||
die 'Verwendung: quarantine-client add [--force] MAC'
mac="$(normalize_mac "$1")"
reject_gateway_mac "${mac}"
if [ "${force}" -eq 0 ]; then
reject_current_ssh_peer "${mac}"
fi
add_to_state "${mac}"
apply_rules
clear_conntrack_for_mac "${mac}"
log_message "Client ${mac} wurde quarantänisiert."
}
command_remove() {
local mac
[ "$#" -eq 1 ] ||
die 'Verwendung: quarantine-client remove MAC'
mac="$(normalize_mac "$1")"
remove_from_state "${mac}"
apply_rules
log_message "Quarantäne für ${mac} wurde aufgehoben."
}
command_apply() {
[ "$#" -eq 0 ] ||
die 'Verwendung: quarantine-client apply'
apply_rules
log_message 'Gespeicherte Quarantäne-Regeln wurden angewendet.'
}
command_list() {
[ "$#" -eq 0 ] ||
die 'Verwendung: quarantine-client list'
if [ ! -s "${STATE_FILE}" ]; then
printf 'Keine Clients in Quarantäne.\n'
return
fi
read_macs
}
command_status() {
[ "$#" -eq 0 ] ||
die 'Verwendung: quarantine-client status'
printf 'Gespeicherte MAC-Adressen:\n'
command_list
printf '\nIPv4-Hook:\n'
iptables -w 5 -t mangle -S PREROUTING |
awk -v chain="${IPT4_CHAIN}" '$0 ~ chain'
printf '\nIPv6-Hook:\n'
ip6tables -w 5 -t mangle -S PREROUTING |
awk -v chain="${IPT6_CHAIN}" '$0 ~ chain'
printf '\nBridge-Hooks:\n'
ebtables -t broute -L BROUTING 2>/dev/null |
awk -v chain="${EBT_BR_CHAIN}" '$0 ~ chain'
ebtables -t filter -L INPUT 2>/dev/null |
awk -v chain="${EBT_IN_CHAIN}" '$0 ~ chain'
ebtables -t filter -L FORWARD 2>/dev/null |
awk -v chain="${EBT_FWD_CHAIN}" '$0 ~ chain'
ebtables -t filter -L OUTPUT 2>/dev/null |
awk -v chain="${EBT_OUT_CHAIN}" '$0 ~ chain'
}
command_self_test() {
[ "$#" -eq 0 ] ||
die 'Verwendung: quarantine-client self-test'
normalize_mac '02:00:00:00:00:01' >/dev/null
printf 'Selbsttest erfolgreich. Es wurde kein Client gesperrt.\n'
}
show_usage() {
cat <<'USAGE'
Verwendung:
quarantine-client add [--force] MAC
quarantine-client remove MAC
quarantine-client list
quarantine-client apply
quarantine-client status
quarantine-client self-test
USAGE
}
main() {
require_root
require_commands
prepare_storage
exec 9>"${LOCK_FILE}"
flock -x 9
local action="${1:-}"
if [ "$#" -gt 0 ]; then
shift
fi
case "${action}" in
add)
command_add "$@"
;;
remove)
command_remove "$@"
;;
apply)
command_apply "$@"
;;
list)
command_list "$@"
;;
status)
command_status "$@"
;;
self-test)
command_self_test "$@"
;;
help|-h|--help|'')
show_usage
;;
*)
die "Unbekannte Aktion: ${action}"
;;
esac
}
main "$@"
QUARANTINE_SCRIPT
chown root:root /persistent/client-quarantine/quarantine-client
chmod 700 /persistent/client-quarantine/quarantine-client
ln -sfn \
/persistent/client-quarantine/quarantine-client \
/usr/local/sbin/quarantine-clientSyntax prüfen
bash -n /persistent/client-quarantine/quarantine-client && echo "Syntax OK"Erwartete Ausgabe:
Syntax OKSelbsttest durchführen
quarantine-client self-test
quarantine-client listErwartete Ausgabe:
Selbsttest erfolgreich. Es wurde kein Client gesperrt.
Keine Clients in Quarantäne.Damit ist die Installation abgeschlossen. Der Selbsttest selbst legt keine Firewall-Sperre für einen Client an.
Nutzung
Nutzung
MAC-Adresse anhand einer IP-Adresse ermitteln
Wenn die aktuelle IP-Adresse des verdächtigen Clients bekannt ist, kann die dazugehörige MAC-Adresse über die Neighbor-Tabelle ermittelt werden:
ip neigh show to 10.15.37.80Beispielausgabe:
10.15.37.80 dev br40 lladdr bc:24:11:34:bb:e1 REACHABLEIn diesem Beispiel lautet die MAC-Adresse des Clients:
bc:24:11:34:bb:e1Client sofort quarantänisieren
quarantine-client add bc:24:11:34:bb:e1Erwartete Ausgabe:
Client bc:24:11:34:bb:e1 wurde quarantänisiert.Die MAC-Adresse wird dabei dauerhaft in der Quarantäne-Liste gespeichert. Die Firewallregeln werden unmittelbar neu aufgebaut und bekannte bestehende IPv4- und IPv6-Verbindungen des Clients werden aus Conntrack gelöscht.
Schutz vor versehentlichem Aussperren
Das Skript versucht zu erkennen, ob die angegebene MAC-Adresse zum Rechner gehört, über den die aktuelle SSH-Verbindung aufgebaut wurde. In diesem Fall wird die Sperre standardmäßig abgebrochen.
Beispiel:
Fehler: Diese MAC gehört zum aktuellen SSH-Gegenüber. Verwende add --force nur bei bewusstem Verbindungsabbruch.Soll der aktuell verwendete SSH-Client tatsächlich absichtlich gesperrt werden, kann die Schutzfunktion mit --force übergangen werden:
quarantine-client add --force bc:24:11:34:bb:e1Achtung: Ist dies tatsächlich der aktuelle Administrationsrechner, kann dadurch die SSH-Verbindung sofort abbrechen.
Quarantänisierte Clients anzeigen
quarantine-client listBeispielausgabe:
bc:24:11:34:bb:e1Wenn kein Client quarantänisiert ist:
Keine Clients in Quarantäne.Status der Firewall-Hooks anzeigen
quarantine-client statusDer Befehl zeigt die gespeicherten MAC-Adressen sowie die aktuell installierten IPv4-, IPv6- und Bridge-Hooks an.
Gespeicherte Quarantäne erneut anwenden
Wenn UniFi die Firewall neu aufgebaut hat oder das Gateway neu gestartet wurde, können alle gespeicherten Quarantäne-Einträge erneut in die laufende Firewall übernommen werden:
quarantine-client applyErwartete Ausgabe:
Gespeicherte Quarantäne-Regeln wurden angewendet.Quarantäne eines Clients aufheben
quarantine-client remove bc:24:11:34:bb:e1Erwartete Ausgabe:
Quarantäne für bc:24:11:34:bb:e1 wurde aufgehoben.Danach kontrollieren
quarantine-client listWenn die MAC-Adresse nicht mehr in der Ausgabe vorhanden ist, wurde die Quarantäne aufgehoben.
Kurzreferenz für den Ernstfall
MAC zu einer bekannten IP ermitteln:
ip neigh show to IP-ADRESSEClient sperren:
quarantine-client add MAC-ADRESSEGesperrte Clients anzeigen:
quarantine-client listClient wieder freigeben:
quarantine-client remove MAC-ADRESSEQuarantäne-System vollständig entfernen
Vollständige Deinstallation
Bei der Deinstallation sollten zuerst alle gespeicherten Clients aus der Quarantäne entfernt werden.
Aktuelle Quarantäne-Liste anzeigen
quarantine-client listJede dort aufgeführte MAC-Adresse mit folgendem Befehl entfernen:
quarantine-client remove bc:24:11:34:bb:e1Danach sollte die Liste leer sein:
Keine Clients in Quarantäne.Firewall-Hooks und Quarantäne-Ketten entfernen
Mit folgendem Block werden die von diesem Werkzeug angelegten IPv4-, IPv6- und Bridge-Firewallketten vollständig aus der laufenden Firewall entfernt:
for spec in \
"iptables QRTN_MAC_V4" \
"ip6tables QRTN_MAC_V6"
do
set -- $spec
binary="$1"
chain="$2"
while "$binary" \
-w 5 \
-t mangle \
-C PREROUTING \
-j "$chain" \
2>/dev/null
do
"$binary" \
-w 5 \
-t mangle \
-D PREROUTING \
-j "$chain"
done
"$binary" \
-w 5 \
-t mangle \
-F "$chain" \
2>/dev/null || true
"$binary" \
-w 5 \
-t mangle \
-X "$chain" \
2>/dev/null || true
done
for spec in \
"broute BROUTING QRTN_MAC_BR" \
"filter INPUT QRTN_MAC_IN" \
"filter FORWARD QRTN_MAC_FWD" \
"filter OUTPUT QRTN_MAC_OUT"
do
set -- $spec
table="$1"
builtin_chain="$2"
custom_chain="$3"
while ebtables \
-t "$table" \
-D "$builtin_chain" \
-j "$custom_chain" \
2>/dev/null
do
:
done
ebtables \
-t "$table" \
-F "$custom_chain" \
2>/dev/null || true
ebtables \
-t "$table" \
-X "$custom_chain" \
2>/dev/null || true
doneProgrammdateien entfernen
rm -f /usr/local/sbin/quarantine-client
rm -rf /persistent/client-quarantineKontrolle
Der folgende Befehl sollte anschließend keine Quarantäne-Ketten mehr anzeigen:
{
iptables -t mangle -S 2>/dev/null
ip6tables -t mangle -S 2>/dev/null
ebtables -t broute -L 2>/dev/null
ebtables -t filter -L 2>/dev/null
} | awk '/QRTN_MAC_/ { print }'Wenn keine Ausgabe erscheint, sind die Quarantäne-Firewallregeln vollständig entfernt.
Zusätzlich sollte der Befehl nicht mehr vorhanden sein:
command -v quarantine-client || echo "quarantine-client wurde entfernt."Erwartete Ausgabe:
quarantine-client wurde entfernt.Wichtige Grenzen der Quarantäne
Die Quarantäne greift auf dem UniFi Gateway und kann damit insbesondere Verkehr zur Firewall selbst, zwischen gerouteten Netzwerken und zum Internet unterbinden.
Befinden sich zwei Geräte jedoch im selben VLAN und wird deren Verkehr direkt über einen externen Switch oder Access Point vermittelt, muss dieser Datenverkehr das Gateway möglicherweise überhaupt nicht passieren. Eine Gateway-Firewall kann solchen Verkehr folglich nicht zuverlässig stoppen.
Bei einem bestätigten Angriff oder einer akuten Kompromittierung sollte deshalb zusätzlich eine physische beziehungsweise Layer-2-Isolation erfolgen, beispielsweise durch das Deaktivieren des betreffenden Switch-Ports oder das Trennen des WLAN-Clients.
Notfall-Zusammenfassung
1. MAC-Adresse feststellen:
ip neigh show to IP-ADRESSE
2. Client sofort sperren:
quarantine-client add MAC-ADRESSE
3. Sperre kontrollieren:
quarantine-client list
quarantine-client status
4. Client wieder freigeben:
quarantine-client remove MAC-ADRESSE
No comments to display
No comments to display