Guard: watch logs, ban misbehaving addresses in Housecarl
Files and the journal, patterns with one <IP>, bounties within a window, bans in a Housecarl ban list with repeat offenders' bans growing; presets for sshd, jarl, housecarl, steward, bind-xfr, postfix-sasl, dovecot, nginx-auth and housecarl-drops; guard run/check/test/status/bans/ban/unban; a Debian package with a sandboxed unit; an end-to-end test with Housecarl. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
d06bf8d531
47 files changed
+4614
No files matched your search
Executable
+189
@@ -0,0 +1,189 @@
|
||||
#!/bin/bash
|
||||
# End-to-end test of Guard with Housecarl and real traffic: a router
|
||||
# container (Debian 13, nftables 1.1.3) runs housecarld between an
|
||||
# "internet" network and a LAN; a server on the LAN runs Guard over a log
|
||||
# file; a host on the internet misbehaves. Checks that Guard bans it at
|
||||
# the router (one port, for the server alone), that never_ban holds, that
|
||||
# bans wait while Housecarl is down, and that Guard on the router bans
|
||||
# scanners from Housecarl's own log of dropped packets.
|
||||
#
|
||||
# Needs Docker and ../housecarl (its image housecarl-nft, made by its
|
||||
# scripts/e2e.sh if missing). Run from the repository root:
|
||||
#
|
||||
# make e2e or scripts/e2e.sh
|
||||
#
|
||||
# KEEP=1 leaves the containers and networks running.
|
||||
set -u
|
||||
ROOT=$(cd "$(dirname "$0")/.." && pwd)
|
||||
HC=${HOUSECARL_SRC:-$ROOT/../housecarl}
|
||||
P=${E2E_PREFIX:-gde2e}
|
||||
IMAGE=${E2E_IMAGE:-housecarl-nft}
|
||||
WORK=${WORK:-$(mktemp -d "${TMPDIR:-/tmp}/guard-e2e.XXXXXX")}
|
||||
docker info >/dev/null 2>&1 || { echo "Docker isn't reachable (set DOCKER_HOST?)"; exit 2; }
|
||||
docker image inspect "$IMAGE" >/dev/null 2>&1 || { echo "no image $IMAGE: run ../housecarl's make e2e once to make it"; exit 2; }
|
||||
|
||||
(cd "$ROOT" && CGO_ENABLED=0 go build -o "$WORK/guard" ./cmd/guard) || exit 1
|
||||
(cd "$HC" && CGO_ENABLED=0 go build -o "$WORK/housecarld" ./cmd/housecarld && CGO_ENABLED=0 go build -o "$WORK/hc" ./cmd/hc) || exit 1
|
||||
|
||||
cleanup() {
|
||||
if [ "${KEEP:-}" = 1 ]; then echo "left running: containers ${P}-*, networks ${P}-*, files in $WORK"; return; fi
|
||||
docker exec ${P}-router sh -c 'rm -rf /w/data /w/*.log /w/state*' >/dev/null 2>&1
|
||||
docker rm -f $(docker ps -aq --filter "name=^${P}-") >/dev/null 2>&1
|
||||
docker network rm ${P}-wan ${P}-lan >/dev/null 2>&1
|
||||
rm -rf "$WORK"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
docker rm -f $(docker ps -aq --filter "name=^${P}-") >/dev/null 2>&1
|
||||
docker network rm ${P}-wan ${P}-lan >/dev/null 2>&1
|
||||
|
||||
# The internet 198.18.2.0/24, the LAN 10.89.10.0/24; the router has .1 on
|
||||
# the LAN, .2 on the internet; the server is 10.89.10.10.
|
||||
docker network create --subnet 198.18.2.0/24 --gateway 198.18.2.254 ${P}-wan >/dev/null || exit 1
|
||||
docker network create --subnet 10.89.10.0/24 --gateway 10.89.10.254 ${P}-lan >/dev/null || exit 1
|
||||
WAN4=198.18.2.2 LAN4=10.89.10.1 SRV=10.89.10.10 NET=198.18.2.10 NET2=198.18.2.11
|
||||
docker run -d --name ${P}-router --hostname router --cap-add NET_ADMIN --sysctl net.ipv4.ip_forward=1 \
|
||||
--network ${P}-wan --ip $WAN4 -v "$WORK:/w" "$IMAGE" sleep infinity >/dev/null || exit 1
|
||||
docker network connect --ip $LAN4 ${P}-lan ${P}-router || exit 1
|
||||
docker exec ${P}-router sh -c 'ip link set eth0 down && ip link set eth0 name wan && ip link set wan up && ip link set eth1 down && ip link set eth1 name lan && ip link set lan up && ip route replace default via 198.18.2.254' || exit 1
|
||||
host() { # NAME NETWORK IP GATEWAY
|
||||
docker run -d --name ${P}-$1 --cap-add NET_ADMIN --network ${P}-$2 --ip $3 -v "$WORK:/w" "$IMAGE" sleep infinity >/dev/null || exit 1
|
||||
docker exec ${P}-$1 ip route replace default via $4 || exit 1
|
||||
}
|
||||
host srv lan $SRV $LAN4
|
||||
host net wan $NET 198.18.2.254
|
||||
docker exec ${P}-net sh -c "ip addr add $NET2/24 dev eth0 && ip route replace 10.89.10.0/24 via $WAN4"
|
||||
# The server: "ssh" on 22 and "web" on 80.
|
||||
docker exec -d ${P}-srv sh -c 'while true; do echo ssh | nc -N -l 0.0.0.0 22; done'
|
||||
docker exec -d ${P}-srv sh -c 'while true; do echo web | nc -N -l 0.0.0.0 80; done'
|
||||
|
||||
PASS=0; FAIL=0
|
||||
ok() { echo "PASS: $1"; PASS=$((PASS+1)); }
|
||||
bad() { echo "FAIL: $1"; FAIL=$((FAIL+1)); }
|
||||
run() {
|
||||
local desc=$1 want=$2; shift 2
|
||||
OUT=$("$@" 2>&1); local rc=$?
|
||||
if [ "$rc" = "$want" ]; then ok "$desc (rc=$rc)"; else bad "$desc: rc=$rc want $want"; echo "$OUT" | sed 's/^/ /'; fi
|
||||
}
|
||||
contains() { if echo "$OUT" | grep -q -- "$1"; then ok "output has '$1'"; else bad "output lacks '$1'"; echo "$OUT" | sed 's/^/ /'; fi; }
|
||||
not_contains() { if echo "$OUT" | grep -q -- "$1"; then bad "output has '$1'"; echo "$OUT" | sed 's/^/ /'; else ok "output lacks '$1'"; fi; }
|
||||
# eventually DESC CMD...: retries for 15 seconds until CMD succeeds.
|
||||
eventually() {
|
||||
local desc=$1; shift
|
||||
for i in $(seq 1 30); do OUT=$("$@" 2>&1) && { ok "$desc"; return; }; sleep 0.5; done
|
||||
bad "$desc"; echo "$OUT" | sed 's/^/ /'
|
||||
}
|
||||
R() { docker exec ${P}-router "$@"; }
|
||||
H() { docker exec ${P}-router /w/hc "$@"; }
|
||||
connect() { docker exec ${P}-net nc -w 3 ${2:+-s $2} $SRV $1 </dev/null; }
|
||||
fails() { # LINES SOURCE: sshd's failed logins
|
||||
for i in $(seq 1 $1); do echo "$(date '+%b %e %T') srv sshd-session[$i]: Failed password for invalid user admin from $2 port 4$i ssh2"; done >> "$WORK/auth.log"
|
||||
}
|
||||
|
||||
cat > "$WORK/housecarl.yaml" <<'EOF'
|
||||
listen: ["0.0.0.0:8070"]
|
||||
database: /w/data/housecarl.db
|
||||
ruleset: /w/data/ruleset.nft
|
||||
log: {level: debug}
|
||||
stats: {enabled: false}
|
||||
EOF
|
||||
start_hc() { R sh -c "(/w/housecarld -c /w/housecarl.yaml serve >> /w/housecarl.log 2>&1 &); for i in \$(seq 1 30); do [ -S /run/housecarl/housecarl.sock ] && exit 0; sleep 0.3; done; exit 1"; }
|
||||
stop_hc() { R sh -c 'kill $(pidof housecarld)'; sleep 1; }
|
||||
|
||||
# ---- Housecarl: a router with a ban list for the server
|
||||
|
||||
run "housecarld starts" 0 start_hc
|
||||
run "zones" 0 sh -c "docker exec ${P}-router /w/hc zone add wan --interface wan --external && docker exec ${P}-router /w/hc zone add lan --interface lan"
|
||||
run "the API from the LAN" 0 H rule add input --in lan --service tcp/8070
|
||||
run "the internet reaches the server's ssh and web" 0 H rule add forward --in wan --to $SRV --service tcp/22 --service tcp/80
|
||||
run "the LAN goes out" 0 H rule add forward --in lan --out wan
|
||||
run "a ban list for the server" 0 H banlist add srv --scope hosts --host $SRV
|
||||
run "a user for the server's Guard" 0 H user add guard-srv --no-password --grant bans:write:srv
|
||||
R sh -c "/w/hc token create srv --user guard-srv > /w/token 2>/dev/null"
|
||||
run "its token" 0 test -s "$WORK/token"
|
||||
run "ssh from the internet" 0 connect 22
|
||||
contains "^ssh$"
|
||||
|
||||
# ---- Guard on the server, over a log file
|
||||
|
||||
: > "$WORK/auth.log"
|
||||
cat > "$WORK/guard.yaml" <<EOF
|
||||
housecarl: {url: "http://$LAN4:8070", token_file: /w/token, list: srv}
|
||||
never_ban: [$NET2]
|
||||
defaults: {hits: 3, within: 10m, ban: 1h}
|
||||
watch:
|
||||
- name: ssh
|
||||
preset: sshd
|
||||
file: /w/auth.log
|
||||
ban: {protocol: tcp, port: ssh}
|
||||
state: /w/state.json
|
||||
socket: /w/guard.sock
|
||||
log: {level: debug}
|
||||
EOF
|
||||
run "guard check" 0 docker exec ${P}-srv /w/guard -c /w/guard.yaml check
|
||||
contains "ban list srv: ok (scope hosts, IPv6 banned by /64)"
|
||||
docker exec -d ${P}-srv sh -c '/w/guard -c /w/guard.yaml run >> /w/guard.log 2>&1'
|
||||
sleep 2
|
||||
fails 2 $NET
|
||||
sleep 2
|
||||
run "two failures: no ban yet" 0 H bans srv
|
||||
not_contains "$NET"
|
||||
fails 1 $NET
|
||||
eventually "the third: banned at the router" sh -c "docker exec ${P}-router /w/hc bans srv | grep -q '$NET *tcp/22 .*guard-srv *guard/ssh'"
|
||||
run "ssh from it is cut" 1 connect 22
|
||||
run "its web isn't (tcp/22 only)" 0 connect 80
|
||||
contains "^web$"
|
||||
run "guard status" 0 docker exec ${P}-srv /w/guard -c /w/guard.yaml status
|
||||
contains "ssh *file /w/auth.log *3 *3 *1"
|
||||
contains "$NET *srv *tcp/ssh *1h *banned"
|
||||
fails 5 $NET2
|
||||
sleep 3
|
||||
run "never_ban: not banned" 0 H bans srv
|
||||
not_contains "$NET2"
|
||||
run "ssh from it still works" 0 connect 22 $NET2
|
||||
contains "^ssh$"
|
||||
run "guard test over the log" 0 docker exec ${P}-srv /w/guard -c /w/guard.yaml test ssh /w/auth.log
|
||||
contains "would ban 1"
|
||||
contains "never banned: in never_ban $NET2/32"
|
||||
run "guard unban" 0 docker exec ${P}-srv /w/guard -c /w/guard.yaml unban $NET
|
||||
contains "1 bans lifted"
|
||||
run "ssh again" 0 connect 22
|
||||
contains "^ssh$"
|
||||
|
||||
# ---- Housecarl away: the ban waits
|
||||
|
||||
stop_hc
|
||||
fails 3 $NET
|
||||
sleep 3
|
||||
run "the ban waits" 0 docker exec ${P}-srv /w/guard -c /w/guard.yaml status
|
||||
contains "1 bans wait for Housecarl"
|
||||
run "housecarld starts again" 0 start_hc
|
||||
eventually "the ban that waited arrives" sh -c "docker exec ${P}-router /w/hc bans srv | grep -q '$NET *tcp/22'"
|
||||
run "its time grew: a repeat offender" 0 H bans srv
|
||||
contains "$NET *tcp/22 *1h59m\|$NET *tcp/22 *2h"
|
||||
run "ssh from it is cut again" 1 connect 22
|
||||
run "lift it" 0 H unban srv $NET
|
||||
|
||||
# ---- Guard on the router: scans in Housecarl's log
|
||||
|
||||
run "a ban list for the router" 0 H banlist add edge --scope everything
|
||||
run "a logged drop for a scanned port" 0 H rule add input --in wan --service tcp/9999 -a drop --log
|
||||
run "a user for the router's Guard" 0 H user add guard-edge --no-password --grant bans:write:edge --grant log:read
|
||||
R sh -c "/w/hc token create edge --user guard-edge > /w/token-edge 2>/dev/null"
|
||||
cat > "$WORK/guard-edge.yaml" <<EOF
|
||||
housecarl: {socket: /run/housecarl/housecarl.sock, token_file: /w/token-edge, list: edge}
|
||||
watch:
|
||||
- {name: scans, preset: housecarl-drops, hits: 4, within: 1m, ban: {time: 1d}}
|
||||
state: /w/state-edge.json
|
||||
socket: /w/guard-edge.sock
|
||||
EOF
|
||||
run "guard check on the router" 0 R /w/guard -c /w/guard-edge.yaml check
|
||||
contains "Housecarl's log (housecarl-drops): ok"
|
||||
docker exec -d ${P}-router sh -c '/w/guard -c /w/guard-edge.yaml run >> /w/guard-edge.log 2>&1'
|
||||
sleep 2
|
||||
for i in 1 2 3 4; do docker exec ${P}-net nc -w 1 $WAN4 9999 </dev/null >/dev/null 2>&1; done
|
||||
eventually "the scanner is banned from everything" sh -c "docker exec ${P}-router /w/hc bans edge | grep -q '$NET *all .*guard/scans'"
|
||||
run "and can't reach the server at all" 1 connect 80
|
||||
|
||||
echo
|
||||
echo "RESULT: $PASS passed, $FAIL failed"
|
||||
[ $FAIL = 0 ]
|
||||
Executable
+73
@@ -0,0 +1,73 @@
|
||||
#!/bin/sh
|
||||
# Builds Guard's Debian package, dist/guard_<version>_<arch>.deb:
|
||||
#
|
||||
# /usr/bin/guard and its bash completion
|
||||
# /usr/lib/systemd/system/guard.service, /usr/lib/sysusers.d/guard.conf
|
||||
# /usr/share/doc/guard/ (README, CHANGELOG, docs)
|
||||
#
|
||||
# Its scripts (deploy/debian) create the guard user and an example
|
||||
# /etc/guard/guard.yaml on the first install, restart a running guard on
|
||||
# upgrades, and stop it on removal. No file in /etc belongs to the
|
||||
# package, so an upgrade never asks about configuration files.
|
||||
#
|
||||
# Run through 'make deb'; needs only dpkg-deb, no root and no debhelper.
|
||||
set -eu
|
||||
|
||||
: "${DEB_ARCH:?}" "${DEB_VERSION:?}" "${VERSION:?}" "${LDFLAGS:?}"
|
||||
GOFLAGS=${GOFLAGS:-}
|
||||
|
||||
goarm=
|
||||
case "$DEB_ARCH" in
|
||||
amd64) goarch=amd64 ;;
|
||||
arm64) goarch=arm64 ;;
|
||||
armhf) goarch=arm goarm=7 ;;
|
||||
i386) goarch=386 ;;
|
||||
ppc64el) goarch=ppc64le ;;
|
||||
riscv64) goarch=riscv64 ;;
|
||||
*) echo "mkdeb: unsupported architecture $DEB_ARCH" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
if [ -z "${DEB_MAINTAINER:-}" ]; then
|
||||
name=$(git config user.name 2>/dev/null || true)
|
||||
email=$(git config user.email 2>/dev/null || true)
|
||||
DEB_MAINTAINER="${name:-Guard packager} <${email:-root@localhost}>"
|
||||
fi
|
||||
|
||||
work=dist/deb/${DEB_VERSION}_${DEB_ARCH}
|
||||
root=$work/guard
|
||||
rm -rf "$work"
|
||||
rm -f dist/guard_*_"$DEB_ARCH".deb
|
||||
mkdir -p "$root"
|
||||
|
||||
echo "mkdeb: building for linux/$goarch"
|
||||
export GOFLAGS
|
||||
GOOS=linux GOARCH=$goarch GOARM=$goarm CGO_ENABLED=0 go build -ldflags "$LDFLAGS" -o "$work/bin/guard" ./cmd/guard
|
||||
make --no-print-directory install-files DESTDIR="$root" PREFIX=/usr UNITDIR=/usr/lib/systemd/system GUARD_BIN="$work/bin/guard" >/dev/null
|
||||
mkdir -p "$root/DEBIAN"
|
||||
for f in postinst prerm postrm; do
|
||||
install -m 0755 "deploy/debian/$f" "$root/DEBIAN/$f"
|
||||
done
|
||||
|
||||
indent() { sed -e 's/^$/./' -e 's/^/ /' "$1"; }
|
||||
{
|
||||
echo "Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/"
|
||||
echo "Upstream-Name: guard"
|
||||
echo
|
||||
echo "Files: *"
|
||||
echo "Copyright: $(sed -n 's/^Copyright (c) //p' LICENSE)"
|
||||
echo "License: Expat"
|
||||
indent LICENSE
|
||||
} > "$root/usr/share/doc/guard/copyright"
|
||||
rm -f "$root/usr/share/doc/guard/LICENSE"
|
||||
find "$root" -type d -exec chmod 0755 {} +
|
||||
chmod -R go-w "$root"
|
||||
size=$(du -sk --exclude=DEBIAN "$root" | cut -f1)
|
||||
sed -e "s/@VERSION@/$DEB_VERSION/g" -e "s/@ARCH@/$DEB_ARCH/" -e "s/@SIZE@/$size/" \
|
||||
-e "s|@MAINTAINER@|$DEB_MAINTAINER|" deploy/debian/control.in > "$root/DEBIAN/control"
|
||||
(cd "$root" && find . -path ./DEBIAN -prune -o -type f -print | sed 's|^\./||' | sort | xargs md5sum) > "$root/DEBIAN/md5sums"
|
||||
chmod 0644 "$root/DEBIAN/control" "$root/DEBIAN/md5sums"
|
||||
out=dist/guard_${DEB_VERSION}_${DEB_ARCH}.deb
|
||||
dpkg-deb --root-owner-group -Zxz --build "$root" "$out" >/dev/null
|
||||
echo "mkdeb: wrote $out"
|
||||
dpkg-deb --info "$out" | sed -n 's/^ \(Package\|Version\|Architecture\|Installed-Size\|Depends\):/ \1:/p'
|
||||
rm -rf dist/deb
|
||||
Reference in new issue
Block a user