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.";
}
}

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 */
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