Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -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.";
}
Expand Down Expand Up @@ -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.";
}
Comment on lines +167 to +174

@Roytak Roytak Jul 30, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mention in the description that it is per connection. I think that the RFC4252 should be referenced here.

Furthermore, this description should probably mention something along the lines "each failed authentication request, including rejected public keys, counts as one attempt", because some SSH clients try all their configured public keys and hence hitting the 3 might be easy, so just so the users are aware.

}

grouping ssh-server-banner-grouping {
Expand Down
16 changes: 16 additions & 0 deletions src/server_config.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
2 changes: 2 additions & 0 deletions src/session_p.h
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
};

/**
Expand Down Expand Up @@ -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;
Expand Down
36 changes: 31 additions & 5 deletions src/session_server_ssh.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 */

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that both ssh_message_reply_default and ssh_disconnect should be called here, the abrupt disconnect is unnecessary.

ssh_disconnect(session->ti.libssh.session);
} else {
ssh_message_reply_default(msg);
}
}

return 0;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}
Expand Down