Description
These OpenSSH server options limit resources used per client and detect dead connections:
ClientAliveInterval(1 to {{ ssh_client_alive_interval }} seconds) andClientAliveCountMax(1 to {{ ssh_client_alive_count_max }}): the server checks that the client still answers, and closes the session afterClientAliveCountMaxunanswered checks;MaxStartupsat most10:30:60: beyond 10 unauthenticated connections, new ones are refused with a probability of 30 %, rising to 100 % at 60;MaxSessionsat most {{ ssh_max_sessions }}: shells, commands and subsystems multiplexed on one connection.
OpenSSH's own default for ClientAliveInterval is 0 (no check), and Ubuntu keeps it, cloud
images included. Idle shell sessions are closed by TMOUT (CB-UBU-AUTH-020), not by these options, since
a live client always answers keepalive checks. The fix only changes the options that are out of
bounds, in /etc/ssh/sshd_config.d/00-cirsec.conf.
Rationale
Sessions whose client has gone away (laptop closed, network cut) keep a shell open on the server; closing them limits what a later session hijack can reach. Connection limits keep a brute force tool or a flood of half-open connections from making the server unreachable for administrators.
Impact
Sessions of clients that stop answering for more than {{ ssh_client_alive_interval }} ×
{{ ssh_client_alive_count_max }} seconds are closed. Automation that opens many parallel
connections or multiplexed sessions (Ansible pipelining, ControlMaster) may hit the limits;
raise them through tailoring if needed.
Applies when
Detected: when: OpenSSH server is installed A scanner reports the control not applicable when one is false.
Setting
| Location | /etc/ssh/sshd_config.d/00-cirsec.conf |
| Default | ClientAliveInterval 0, ClientAliveCountMax 3, MaxStartups 10:30:100, MaxSessions 10 |
| Recommended | ClientAliveInterval 1-{{ ssh_client_alive_interval }}, ClientAliveCountMax 1-{{ ssh_client_alive_count_max }}, MaxStartups <= 10:30:60, MaxSessions <= {{ ssh_max_sessions }} |
Parameters
| Parameter | Core | Server | Server hardened | Desktop | Desktop hardened | Bounds |
|---|---|---|---|---|---|---|
ssh_client_alive_intervalinteger, seconds. Maximum interval between two keepalive checks of the client. | 300 | 300 | 300 | 300 | 300 | 1 to 900 |
ssh_client_alive_count_maxinteger. Maximum number of unanswered keepalive checks before the session is closed. | 3 | 3 | 3 | 3 | 3 | 1 to 10 |
ssh_max_sessionsinteger. Maximum sessions per SSH connection. | 10 | 10 | 10 | 10 | 10 | 1 to 10 |
The value used is your override (tailoring file), else the profile's, else the one of the profile it extends, else the default. Bounds limit what an override may set.
Audit
Run sshd -T | grep -E '^(clientaliveinterval|clientalivecountmax|maxstartups|maxsessions) '.
clientaliveinterval must be between 1 and {{ ssh_client_alive_interval }},
clientalivecountmax between 1 and {{ ssh_client_alive_count_max }}, each field of
maxstartups at most 10:30:60, and maxsessions at most {{ ssh_max_sessions }}.
Automated: every check must pass. Scanners report each check by its id.
client-alive-interval-set
ClientAliveInterval is not 0 (keepalive off).
key: clientaliveinterval
operator: '>='
value: 1
client-alive-interval
ClientAliveInterval upper bound.
key: clientaliveinterval
operator: <=
value: '{{ ssh_client_alive_interval }}'
client-alive-count-set
ClientAliveCountMax is not 0.
key: clientalivecountmax
operator: '>='
value: 1
client-alive-count
ClientAliveCountMax upper bound.
key: clientalivecountmax
operator: <=
value: '{{ ssh_client_alive_count_max }}'
max-startups
MaxStartups at most 10:30:60.
key: maxstartups
operator: match
value: ^([1-9]|10):([1-9]|[12][0-9]|30):([1-9]|[1-5][0-9]|60)$
max-sessions
MaxSessions upper bound.
key: maxsessions
operator: <=
value: '{{ ssh_max_sessions }}'
Remediation
Add to /etc/ssh/sshd_config.d/00-cirsec.conf:
ClientAliveInterval {{ ssh_client_alive_interval }}
ClientAliveCountMax {{ ssh_client_alive_count_max }}
MaxStartups 10:30:60
MaxSessions {{ ssh_max_sessions }}
Check with sshd -t, then run systemctl reload ssh (on 24.04 and later, when ssh.service
is not running yet, the next connection starts it with the new settings).
| Restart | service: ssh |
| Disruption | low |
| Lockout risk | yes: can cut off administrative access. Off by default in generated scripts and playbooks, opt in through a tailoring file. |
Rollback
Remove the lines from /etc/ssh/sshd_config.d/00-cirsec.conf and run systemctl reload ssh.
limits
# CB-UBU-NET-017: bring SSH keepalive and connection limits within bounds, changing only what is out of bounds.
PATH="$PATH:/usr/sbin"
# sshd -T and -t need /run/sshd, which only exists once ssh.service has run (ssh.socket on 24.04 and later).
[ -d /run/sshd ] || mkdir -p -m 0755 /run/sshd
f=/etc/ssh/sshd_config.d/00-cirsec.conf
grep -Eqi '^[[:space:]]*Include[[:space:]]+/etc/ssh/sshd_config.d/\*\.conf' /etc/ssh/sshd_config || {
echo '/etc/ssh/sshd_config does not include sshd_config.d/*.conf' >&2
exit 1
}
cfg=$(sshd -T 2>/dev/null) || { echo 'sshd -T failed' >&2; exit 1; }
get() { printf '%s\n' "$cfg" | awk -v k="$1" '$1 == k { print $2; exit }'; }
# in_range VALUE MAX: VALUE is an integer between 1 and MAX.
in_range() { case $1 in '' | *[!0-9]*) return 1 ;; esac; [ "$1" -ge 1 ] && [ "$1" -le "$2" ]; }
changes=
in_range "$(get clientaliveinterval)" "$CB_SSH_CLIENT_ALIVE_INTERVAL" || changes="$changes clientaliveinterval=$CB_SSH_CLIENT_ALIVE_INTERVAL"
in_range "$(get clientalivecountmax)" "$CB_SSH_CLIENT_ALIVE_COUNT_MAX" || changes="$changes clientalivecountmax=$CB_SSH_CLIENT_ALIVE_COUNT_MAX"
in_range "$(get maxsessions)" "$CB_SSH_MAX_SESSIONS" || changes="$changes maxsessions=$CB_SSH_MAX_SESSIONS"
get maxstartups | grep -Eq '^([1-9]|10):([1-9]|[12][0-9]|30):([1-9]|[1-5][0-9]|60)$' || changes="$changes maxstartups=10:30:60"
[ -n "$changes" ] || exit 0
mkdir -p /etc/ssh/sshd_config.d
cb_backup "$f"
touch "$f"
chmod 600 "$f"
for c in $changes; do
k=${c%%=*}
sed -i "/^[[:space:]]*${k}[[:space:]]/Id" "$f"
printf '%s %s\n' "$k" "${c#*=}" >>"$f"
done
if ! sshd -t; then
cb_restore "$f"
echo 'sshd -t rejected the change, reverted' >&2
exit 1
fi
Handwritten code reads parameters from CB_<NAME> environment variables. Each release turns these fixes into remediate.sh and an Ansible role per profile: dry run by default, a fix runs only when its check fails.
References
- sshd_config(5), Ubuntu manual pages Canonical, vendor-doc, accessed 2026-10-19
- NIST IR 7966 - Security of Interactive and Automated Access Management Using Secure Shell (SSH) NIST, guide, accessed 2026-10-19
Mappings
| Framework | ID | Relation |
|---|---|---|
| NIST SP 800-53 Rev. 55.1.1 | SC-10 | partial |
| NIST SP 800-53 Rev. 55.1.1 | SC-5 | related |
How this control relates to the other framework's: equivalent, stricter, looser, partial or related.
Threats mitigated
Related controls
- related
CB-UBU-AUTH-020Close idle shell sessions automatically
People and lifecycle
| Authors |
|
| Verified | 2026-10-19 by Sn0wAlice on 20.04, Proxmox VM, Ubuntu 20.04 cloud image, kernel 5.4.0-216-generic, BIOS boot. ClientAliveInterval 0 and MaxStartups 10:30:100 reported; fix.sh sets 300 and 10:30:60; SSH by key of a non-root user accepted on localhost and port 22 reachable from the lab host after the fix and after reboot. |
| Verified | 2026-10-19 by Sn0wAlice on 22.04, Proxmox VM, Ubuntu 22.04 cloud image, kernel 5.15.0-198-generic, BIOS boot. ClientAliveInterval 0 and MaxStartups 10:30:100 reported; fix.sh sets 300 and 10:30:60; SSH by key of a non-root user accepted on localhost and port 22 reachable from the lab host after the fix and after reboot. |
| Verified | 2026-10-19 by Sn0wAlice on 24.04, Proxmox VM, Ubuntu 24.04 cloud image, kernel 6.8.0-147-generic, BIOS boot. ClientAliveInterval 0 and MaxStartups 10:30:100 reported; fix.sh sets 300 and 10:30:60; SSH by key of a non-root user accepted on localhost and port 22 reachable from the lab host after the fix and after reboot. |
| Verified | 2026-10-19 by Sn0wAlice on 26.04, Proxmox VM, Ubuntu 26.04 cloud image, kernel 7.0.0-38-generic, BIOS boot. ClientAliveInterval 0 and MaxStartups 10:30:100 reported; fix.sh sets 300 and 10:30:60; SSH by key of a non-root user accepted on localhost and port 22 reachable from the lab host after the fix and after reboot. |
| Introduced in | v0.1.0 |
| Created, updated | 2026-10-19, 2026-10-19 |
| Translations | fr |