Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 21 additions & 7 deletions root-ubuntu.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4464,16 +4464,26 @@ FAIL2BAN_MAXRETRY="${FAIL2BAN_MAXRETRY:-5}"
# boxes you administer from here.
FAIL2BAN_IGNOREIP="${FAIL2BAN_IGNOREIP:-}"

# Addresses connected to sshd right now: the operator running this script.
# Banning them is a lockout, so they go in ignoreip.
# Addresses logged in over SSH right now: the operator running this script.
# Banning them is a lockout, so they go in ignoreip. "Connected" is not enough:
# a brute-forcer mid-attempt holds an established connection too, and would be
# exempted for good. So a peer counts only if sshd also logged a successful
# login from it (auth.log, or the journal on a box without one).
_fail2ban_ssh_peers() {
local peer
local peer accepted
[[ -n "${SSH_CLIENT:-}" ]] && printf '%s\n' "${SSH_CLIENT%% *}"
command -v ss >/dev/null 2>&1 || return 0
if [[ -f /var/log/auth.log ]]; then
accepted="$(tail -n 20000 /var/log/auth.log 2>/dev/null)"
else
accepted="$(journalctl -q --no-pager --since -2d -t sshd -t sshd-session 2>/dev/null | tail -n 20000)"
fi
accepted="$(printf '%s\n' "$accepted" | sed -n 's/.*Accepted [^ ]* for [^ ]* from \([^ ]*\) port.*/\1/p' | sort -u)"
[[ -n "$accepted" ]] || return 0
ss -tnH state established "( sport = :${SSH_PORT:-22} )" 2>/dev/null \
| awk '{print $4}' | while read -r peer; do
peer="${peer%:*}"; peer="${peer#[}"; peer="${peer%]}"
[[ -n "$peer" ]] && printf '%s\n' "$peer"
[[ -n "$peer" ]] && grep -qxF -- "$peer" <<<"$accepted" && printf '%s\n' "$peer"
done
return 0
}
Expand Down Expand Up @@ -4538,9 +4548,13 @@ EOF
fi

# A jail that failed to load leaves the service running and nothing banned.
if ! fail2ban-client status sshd >/dev/null 2>&1; then
warn "fail2ban is running but the sshd jail is not loaded: fail2ban-client status sshd"
fi
# The socket takes a moment after a restart, so wait before calling it.
local i
for i in 1 2 3 4 5 6 7 8 9 10; do
fail2ban-client status sshd >/dev/null 2>&1 && return 0
sleep "${FAIL2BAN_WAIT:-1}"
done
warn "fail2ban is running but the sshd jail is not loaded: fail2ban-client status sshd"
}

# ---------------------------------------------------- networkd-dispatcher ---
Expand Down
27 changes: 23 additions & 4 deletions test/root-ubuntu.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1446,20 +1446,39 @@ describe('configure_fail2ban', () => {
expect(conf).toMatch(/\[sshd\]\nenabled = true\nport = 22\nbackend = auto/);
});

it('never bans loopback, the tailnet, the admin list or the operator connected now', () => {
it('never bans loopback, the tailnet, the admin list or the operator logged in now', () => {
const dir = fakeRoot();
writeFileSync(join(dir, 'var/log/auth.log'), '');
writeFileSync(
join(dir, 'var/log/auth.log'),
'2026-09-25T12:31:52+00:00 box sshd-session[1]: Accepted publickey for anthony from 198.51.100.7 port 40000 ssh2: ED25519 SHA256:x\n' +
'2026-09-25T12:32:00+00:00 box sshd-session[2]: Accepted keyboard-interactive/pam for ops from 198.51.100.8 port 40001 ssh2\n',
);
run(
dir,
"FAIL2BAN_IGNOREIP='67.205.189.229 10.0.0.0/8' SSH_CLIENT='203.0.113.5 51234 22' " +
"FAKE_SS=$'0 0 23.95.228.174:22 198.51.100.7:40000\\n0 0 23.95.228.174:22 203.0.113.5:51234\\n'",
"FAKE_SS=$'0 0 23.95.228.174:22 198.51.100.7:40000\\n0 0 23.95.228.174:22 198.51.100.8:40001\\n0 0 23.95.228.174:22 203.0.113.5:51234\\n'",
);
const line = /^ignoreip = (.*)$/m.exec(jail(dir))?.[1].split(' ') ?? [];
expect(line).toEqual([
'127.0.0.1/8', '::1', '100.64.0.0/10', '67.205.189.229', '10.0.0.0/8', '203.0.113.5', '198.51.100.7',
'127.0.0.1/8', '::1', '100.64.0.0/10', '67.205.189.229', '10.0.0.0/8',
'203.0.113.5', '198.51.100.7', '198.51.100.8',
]);
});

it('does not exempt a peer that is connected but never logged in', () => {
// A brute-forcer mid-attempt holds an established connection too.
const dir = fakeRoot();
writeFileSync(
join(dir, 'var/log/auth.log'),
'2026-09-25T12:31:52+00:00 box sshd-session[1]: Failed password for root from 192.0.2.99 port 5 ssh2\n' +
'2026-09-25T12:31:53+00:00 box sshd-session[2]: Accepted publickey for anthony from 198.51.100.7 port 40000 ssh2\n',
);
run(dir, "FAKE_SS=$'0 0 23.95.228.174:22 192.0.2.99:5\\n0 0 23.95.228.174:22 198.51.100.7:40000\\n'");
const ignore = /^ignoreip = (.*)$/m.exec(jail(dir))?.[1] ?? '';
expect(ignore).toContain('198.51.100.7');
expect(ignore).not.toContain('192.0.2.99');
});

it('reads the journal on a box with no auth.log', () => {
const dir = fakeRoot();
run(dir);
Expand Down
Loading