diff --git a/modules/libnetconf2-netconf-server@2026-04-17.yang b/modules/libnetconf2-netconf-server@2026-07-16.yang similarity index 97% rename from modules/libnetconf2-netconf-server@2026-04-17.yang rename to modules/libnetconf2-netconf-server@2026-07-16.yang index 4b88a94c..04f93a06 100644 --- a/modules/libnetconf2-netconf-server@2026-04-17.yang +++ b/modules/libnetconf2-netconf-server@2026-07-16.yang @@ -31,6 +31,10 @@ module libnetconf2-netconf-server { prefix tlss; } + revision "2026-07-16" { + description "Added max-auth-attempts leaf to limit SSH authentication attempts."; + } + revision "2026-04-17" { description "Change SSH banner description, reference and string length to reflect its correct purpose."; } @@ -159,6 +163,15 @@ module libnetconf2-netconf-server { description "Represents the maximum amount of seconds an authentication can go on for."; } + + leaf max-auth-attempts { + type uint16 { + range "1..max"; + } + default 3; + description + "Maximum number of failed SSH authentication attempts before disconnecting the client."; + } } grouping ssh-server-banner-grouping { diff --git a/src/server_config.c b/src/server_config.c index b3e10adf..dd1f629c 100644 --- a/src/server_config.c +++ b/src/server_config.c @@ -1483,6 +1483,15 @@ config_ssh_auth_timeout(const struct lyd_node *node, enum nc_operation UNUSED(pa return 0; } +static int +config_ssh_max_auth_attempts(const struct lyd_node *node, enum nc_operation UNUSED(parent_op), + struct nc_server_ssh_opts *ssh) +{ + /* default value always present */ + ssh->max_auth_attempts = strtoul(lyd_get_value(node), NULL, 10); + return 0; +} + static int config_endpt_reference(const struct lyd_node *node, enum nc_operation parent_op, char **endpt_ref) { @@ -1534,6 +1543,12 @@ config_ssh_client_auth(const struct lyd_node *node, enum nc_operation parent_op, NC_CHECK_RET(config_ssh_auth_timeout(n, op, ssh)); } + /* config max auth attempts (augment) */ + nc_lyd_find_child_optional(node, "libnetconf2-netconf-server:max-auth-attempts", &n); + if (n) { + NC_CHECK_RET(config_ssh_max_auth_attempts(n, op, ssh)); + } + /* config endpoint reference (augment) */ nc_lyd_find_child_optional(node, "libnetconf2-netconf-server:endpoint-reference", &n); if (n) { @@ -5618,6 +5633,7 @@ nc_server_config_ssh_dup(const struct nc_server_ssh_opts *src, struct nc_server_ } (*dst)->auth_timeout = src->auth_timeout; + (*dst)->max_auth_attempts = src->max_auth_attempts; cleanup: if (rc) { diff --git a/src/session_p.h b/src/session_p.h index d5482854..a2a7ac3a 100644 --- a/src/session_p.h +++ b/src/session_p.h @@ -386,6 +386,7 @@ struct nc_server_ssh_opts { char *banner; /**< SSH banner message, sent before authentication. */ uint16_t auth_timeout; /**< Authentication timeout. */ + uint16_t max_auth_attempts; /**< Maximum number of failed authentication attempts. */ }; /** @@ -961,6 +962,7 @@ struct nc_session { #ifdef NC_ENABLED_SSH_TLS uint16_t ssh_auth_attempts; /**< number of failed SSH authentication attempts */ + uint16_t ssh_max_auth_attempts; /**< max-auth-attempts of the endpoint the session was accepted on */ void *client_cert; /**< TLS client certificate if used for authentication */ #endif /* NC_ENABLED_SSH_TLS */ } server; diff --git a/src/session_server_ssh.c b/src/session_server_ssh.c index bc55850d..4c1f2635 100644 --- a/src/session_server_ssh.c +++ b/src/session_server_ssh.c @@ -1727,7 +1727,13 @@ nc_server_ssh_auth(struct nc_session *session, struct nc_server_ssh_opts *opts, ++session->opts.server.ssh_auth_attempts; VRB(session, "Failed user \"%s\" authentication attempt (#%d).", session->username, session->opts.server.ssh_auth_attempts); - ssh_message_reply_default(msg); + if (session->opts.server.ssh_max_auth_attempts && + (session->opts.server.ssh_auth_attempts >= session->opts.server.ssh_max_auth_attempts)) { + /* disconnect immediately so client does not prompt again */ + ssh_disconnect(session->ti.libssh.session); + } else { + ssh_message_reply_default(msg); + } } return 0; @@ -2013,6 +2019,9 @@ nc_accept_ssh_session_auth(struct nc_session *session, struct nc_server_ssh_opts DBG(session, "SSH authentication..."); + /* copy so the value stays fixed even if endpoint-reference delegation later uses different opts */ + session->opts.server.ssh_max_auth_attempts = opts->max_auth_attempts; + /* authenticate */ if (opts->auth_timeout) { nc_timeouttime_get(&ts_timeout, opts->auth_timeout * 1000); @@ -2040,14 +2049,31 @@ nc_accept_ssh_session_auth(struct nc_session *session, struct nc_server_ssh_opts /* timeout */ break; } + if (session->opts.server.ssh_max_auth_attempts && + (session->opts.server.ssh_auth_attempts >= session->opts.server.ssh_max_auth_attempts)) { + /* max auth attempts reached */ + break; + } } if (!(session->flags & NC_SESSION_SSH_AUTHENTICATED)) { - /* timeout */ - if (session->username) { - ERR(session, "User \"%s\" failed to authenticate for too long, disconnecting.", session->username); + if (session->opts.server.ssh_max_auth_attempts && + (session->opts.server.ssh_auth_attempts >= session->opts.server.ssh_max_auth_attempts)) { + /* max auth attempts reached */ + if (session->username) { + ERR(session, "User \"%s\" reached the maximum number of failed SSH authentication attempts (%d), disconnecting.", + session->username, session->opts.server.ssh_max_auth_attempts); + } else { + ERR(session, "Reached the maximum number of failed SSH authentication attempts (%d), disconnecting.", + session->opts.server.ssh_max_auth_attempts); + } } else { - ERR(session, "User failed to authenticate for too long, disconnecting."); + /* timeout */ + if (session->username) { + ERR(session, "User \"%s\" failed to authenticate for too long, disconnecting.", session->username); + } else { + ERR(session, "User failed to authenticate for too long, disconnecting."); + } } return 0; }