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-09-18" {
description "Added max-auth-attempts and password-based authentication lockout configuration.";
}

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,19 @@ module libnetconf2-netconf-server {
description
"Represents the maximum amount of seconds an authentication can go on for.";
}

leaf max-auth-attempts {
type uint16;
default 0;
description
"Maximum number of failed authentication attempts allowed within a single SSH session,
after which the session is disconnected. Every rejected authentication request counts,
including the initial 'none' request most clients send and every public key a client
offers that the server does not accept, so this must be set high enough not to
disconnect legitimate clients.

The value 0 means no limit.";
}
}

grouping ssh-server-banner-grouping {
Expand Down Expand Up @@ -461,6 +478,58 @@ module libnetconf2-netconf-server {
}
}

container ssh-password-lockout {
presence
"Enables locking a user out of password-based authentication after repeated failures.";

description
"Temporarily locks a user out of password-based authentication after too many consecutive
failures. Applies to all SSH endpoints, both listening and Call Home.

Password-based authentication is the 'password' method and the 'keyboard-interactive'
method with 'use-system-auth'.

While locked out, even the correct password is rejected. A successful password-based
authentication resets the count.

Public key authentication is not affected.";

reference
"3GPP TS 33.117: Catalogue of general security assurance requirements,
section 4.2.3.4.5 Policy regarding consecutive failed login attempts and
section 4.2.3.4.3.3 Protection against brute force and dictionary attacks";

leaf max-consecutive-failures {
type uint16 {
range "1..max";
}
default 5;
description
"Number of consecutive failed password-based authentications of a user after which the
user is locked out.";
}

leaf duration {
type uint16 {
range "1..max";
}
default 300;
units "seconds";
description
"How long a user stays locked out.";
}

leaf reset-interval {
type uint16 {
range "1..max";
}
default 900;
units "seconds";
description
"The count of consecutive failures of a user is reset if no failure occurs for this long.";
}
}

leaf-list ignored-hello-module {
type string;

Expand Down
61 changes: 61 additions & 0 deletions src/server_config.c
Original file line number Diff line number Diff line change
Expand Up @@ -1665,6 +1665,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 @@ -1716,6 +1725,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 per session (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 @@ -5343,6 +5358,43 @@ config_ignored_hello_module(const struct lyd_node *node, enum nc_operation paren
return 0;
}

#ifdef NC_ENABLED_SSH_TLS

static int
config_ssh_password_lockout(const struct lyd_node *node, enum nc_operation parent_op, struct nc_server_config *config)
{
enum nc_operation op;
struct lyd_node *n;

NC_NODE_GET_OP(node, parent_op, &op);

if (op == NC_OP_DELETE) {
/* the container is gone, so the lockout is off again; max_fails of 0 disables it */
config->authlock.max_fails = 0;
config->authlock.duration = 0;
config->authlock.reset_interval = 0;
return 0;
}

/* default values always present */
nc_lyd_find_child_optional(node, "max-consecutive-failures", &n);
if (n) {
config->authlock.max_fails = strtoul(lyd_get_value(n), NULL, 10);
}
nc_lyd_find_child_optional(node, "duration", &n);
if (n) {
config->authlock.duration = strtoul(lyd_get_value(n), NULL, 10);
}
nc_lyd_find_child_optional(node, "reset-interval", &n);
if (n) {
config->authlock.reset_interval = strtoul(lyd_get_value(n), NULL, 10);
}

return 0;
}

#endif /* NC_ENABLED_SSH_TLS */

static int
config_ln2_netconf_server(const struct lyd_node *node, enum nc_operation parent_op,
struct nc_server_config *config)
Expand All @@ -5361,6 +5413,12 @@ config_ln2_netconf_server(const struct lyd_node *node, enum nc_operation parent_
if (n) {
NC_CHECK_RET(config_cert_exp_notif_intervals(n, op, config));
}

/* config ssh-password-lockout */
nc_lyd_find_child_optional(node, "ssh-password-lockout", &n);
if (n) {
NC_CHECK_RET(config_ssh_password_lockout(n, op, config));
}
#endif /* NC_ENABLED_SSH_TLS */

/* config all ignored-hello-modules */
Expand Down Expand Up @@ -5537,6 +5595,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 Expand Up @@ -6047,6 +6106,8 @@ nc_server_config_dup(const struct nc_server_config *src, struct nc_server_config
dst->cert_exp_notif_intervals[i] = src->cert_exp_notif_intervals[i];
LYA_INCREMENT(dst->cert_exp_notif_intervals);
}

dst->authlock = src->authlock;
#endif /* NC_ENABLED_SSH_TLS */

cleanup:
Expand Down
39 changes: 39 additions & 0 deletions src/session_p.h
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,16 @@ struct nc_hostkey {
};
};

/**
* @brief Password-based authentication lockout policy of the server, shared by all the SSH endpoints.
*/
struct nc_authlock_opts {
uint16_t max_fails; /**< consecutive failed password-based authentications that lock a user out,
0 if the lockout is disabled */
uint16_t duration; /**< how long a user stays locked out, seconds */
uint16_t reset_interval; /**< the count of a user is reset if no failure occurs for this long, seconds */
};

/**
* @brief Server options for configuring the SSH transport protocol.
*/
Expand All @@ -428,6 +438,9 @@ struct nc_server_ssh_opts {
char *banner; /**< SSH banner message, sent before authentication. */

uint16_t auth_timeout; /**< Authentication timeout. */

uint16_t max_auth_attempts; /**< Failed authentication attempts allowed within a single session,
0 for no limit. */
};

/**
Expand Down Expand Up @@ -828,6 +841,8 @@ struct nc_server_config {
struct nc_cert_exp_time anchor; /**< Lower bound of the given interval. */
struct nc_cert_exp_time period; /**< Period of the given interval. */
} *cert_exp_notif_intervals; /**< Certificate expiration notification intervals (sized-array, see libyang docs). */

struct nc_authlock_opts authlock; /**< SSH password-based authentication lockout policy. */
#endif /* NC_ENABLED_SSH_TLS */
};

Expand Down Expand Up @@ -947,6 +962,22 @@ struct nc_server_opts {
pthread_mutex_t lock; /**< Certificate expiration notification thread's data and cond lock. */
pthread_cond_t cond; /**< Condition for the certificate expiration notification thread. */
} cert_exp_notif;

/* ACCESS locked - authlock lock - leaf lock, never acquire another lock while holding it */
pthread_mutex_t authlock_lock; /**< Lock for the password-based authentication lockout tally. */

/**
* @brief Failed password-based authentication tally of a single user.
*
* Shared by all the SSH endpoints, an entry is created on the first failure of a user. Only users
* known to the server ever get one, so the tally is bounded by the number of users.
*/
struct nc_authlock_entry {
char *username; /**< User the tally belongs to. */
uint32_t fails; /**< Consecutive failed password-based authentications. */
time_t last_fail; /**< When the last one was. */
time_t locked_until; /**< No password-based authentication before this, 0 if not locked out. */
} *authlock; /**< Password-based authentication lockout tally (sized-array, see libyang docs). */
#endif /* NC_ENABLED_SSH_TLS */

/**
Expand Down Expand Up @@ -1162,6 +1193,7 @@ struct nc_session {
ATOMIC_T *ch_thread_running;

uint16_t ssh_auth_attempts; /**< number of failed SSH authentication attempts */

void *client_cert; /**< TLS client certificate if used for authentication */
#endif /* NC_ENABLED_SSH_TLS */
} server;
Expand Down Expand Up @@ -1738,6 +1770,13 @@ void nc_server_ch_thread_names_free(char **names);
*/
int nc_server_ch_threads_destroy(void);

/**
* @brief Free the password-based authentication lockout tally.
*
* Must not be called before every thread that may authenticate a client has been joined.
*/
void nc_server_ssh_authlock_free(void);

/**
* @brief Stop a dispatched Call Home client thread, if such thread was dispatched for the given client.
*
Expand Down
7 changes: 7 additions & 0 deletions src/session_server.c
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ struct nc_server_opts server_opts = {
.binds_lock = PTHREAD_MUTEX_INITIALIZER,
.opts_lock = PTHREAD_RWLOCK_INITIALIZER,
.ch_threads_lock = PTHREAD_MUTEX_INITIALIZER,
#ifdef NC_ENABLED_SSH_TLS
.authlock_lock = PTHREAD_MUTEX_INITIALIZER,
#endif /* NC_ENABLED_SSH_TLS */
};

static nc_rpc_clb global_rpc_clb = NULL;
Expand Down Expand Up @@ -1696,6 +1699,10 @@ nc_server_destroy(void)
nc_server_config_release(config);

#ifdef NC_ENABLED_SSH_TLS
/* free the password-based authentication lockout tally; only safe here, once the Call Home and
* accept threads that authenticate clients have been joined */
nc_server_ssh_authlock_free();

/* close the TLS keylog file */
if (server_opts.tls_keylog_file) {
fclose(server_opts.tls_keylog_file);
Expand Down
Loading
Loading