Skip to content

Commit

Permalink
upstream: Add a facility to sshd(8) to penalise particular
Browse files Browse the repository at this point in the history
problematic client behaviours, controlled by two new sshd_config(5) options:
PerSourcePenalties and PerSourcePenaltyExemptList.

When PerSourcePenalties are enabled, sshd(8) will monitor the exit
status of its child pre-auth session processes. Through the exit
status, it can observe situations where the session did not
authenticate as expected. These conditions include when the client
repeatedly attempted authentication unsucessfully (possibly indicating
an attack against one or more accounts, e.g. password guessing), or
when client behaviour caused sshd to crash (possibly indicating
attempts to exploit sshd).

When such a condition is observed, sshd will record a penalty of some
duration (e.g. 30 seconds) against the client's address. If this time
is above a minimum threshold specified by the PerSourcePenalties, then
connections from the client address will be refused (along with any
others in the same PerSourceNetBlockSize CIDR range).

Repeated offenses by the same client address will accrue greater
penalties, up to a configurable maximum. A PerSourcePenaltyExemptList
option allows certain address ranges to be exempt from all penalties.

We hope these options will make it significantly more difficult for
attackers to find accounts with weak/guessable passwords or exploit
bugs in sshd(8) itself.

PerSourcePenalties is off by default, but we expect to enable it
automatically in the near future.

much feedback markus@ and others, ok markus@

OpenBSD-Commit-ID: 89ded70eccb2b4926ef0366a4d58a693de366cca
  • Loading branch information
djmdjm committed Jun 6, 2024
1 parent 916b0b6 commit 81c1099
Show file tree
Hide file tree
Showing 12 changed files with 982 additions and 93 deletions.
6 changes: 3 additions & 3 deletions .depend

Large diffs are not rendered by default.

18 changes: 17 additions & 1 deletion misc.c
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* $OpenBSD: misc.c,v 1.195 2024/05/17 06:11:17 deraadt Exp $ */
/* $OpenBSD: misc.c,v 1.196 2024/06/06 17:15:25 djm Exp $ */
/*
* Copyright (c) 2000 Markus Friedl. All rights reserved.
* Copyright (c) 2005-2020 Damien Miller. All rights reserved.
Expand Down Expand Up @@ -3100,3 +3100,19 @@ lib_contains_symbol(const char *path, const char *s)
return ret;
#endif /* HAVE_NLIST_H */
}

int
signal_is_crash(int sig)
{
switch (sig) {
case SIGSEGV:
case SIGBUS:
case SIGTRAP:
case SIGSYS:
case SIGFPE:
case SIGILL:
case SIGABRT:
return 1;
}
return 0;
}
3 changes: 2 additions & 1 deletion misc.h
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* $OpenBSD: misc.h,v 1.108 2024/05/17 00:30:24 djm Exp $ */
/* $OpenBSD: misc.h,v 1.109 2024/06/06 17:15:25 djm Exp $ */

/*
* Author: Tatu Ylonen <ylo@cs.hut.fi>
Expand Down Expand Up @@ -252,6 +252,7 @@ void notify_complete(struct notifier_ctx *, const char *, ...)

typedef void (*sshsig_t)(int);
sshsig_t ssh_signal(int, sshsig_t);
int signal_is_crash(int);

/* On OpenBSD time_t is int64_t which is long long. */
/* #define SSH_TIME_T_MAX LLONG_MAX */
Expand Down
8 changes: 7 additions & 1 deletion monitor.c
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* $OpenBSD: monitor.c,v 1.239 2024/05/17 06:42:04 jsg Exp $ */
/* $OpenBSD: monitor.c,v 1.240 2024/06/06 17:15:25 djm Exp $ */
/*
* Copyright 2002 Niels Provos <provos@citi.umich.edu>
* Copyright 2002 Markus Friedl <markus@openbsd.org>
Expand Down Expand Up @@ -161,6 +161,7 @@ static char *auth_submethod = NULL;
static u_int session_id2_len = 0;
static u_char *session_id2 = NULL;
static pid_t monitor_child_pid;
int auth_attempted = 0;

struct mon_table {
enum monitor_reqtype type;
Expand Down Expand Up @@ -296,6 +297,10 @@ monitor_child_preauth(struct ssh *ssh, struct monitor *pmonitor)
authenticated = (monitor_read(ssh, pmonitor,
mon_dispatch, &ent) == 1);

/* Record that auth was attempted to set exit status later */
if ((ent->flags & MON_AUTH) != 0)
auth_attempted = 1;

/* Special handling for multiple required authentications */
if (options.num_auth_methods != 0) {
if (authenticated &&
Expand Down Expand Up @@ -353,6 +358,7 @@ monitor_child_preauth(struct ssh *ssh, struct monitor *pmonitor)
fatal_f("authentication method name unknown");

debug_f("user %s authenticated by privileged process", authctxt->user);
auth_attempted = 0;
ssh->authctxt = NULL;
ssh_packet_set_log_preamble(ssh, "user %s", authctxt->user);

Expand Down
35 changes: 34 additions & 1 deletion monitor_wrap.c
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* $OpenBSD: monitor_wrap.c,v 1.130 2024/05/17 00:30:24 djm Exp $ */
/* $OpenBSD: monitor_wrap.c,v 1.131 2024/06/06 17:15:25 djm Exp $ */
/*
* Copyright 2002 Niels Provos <provos@citi.umich.edu>
* Copyright 2002 Markus Friedl <markus@openbsd.org>
Expand Down Expand Up @@ -29,6 +29,7 @@

#include <sys/types.h>
#include <sys/uio.h>
#include <sys/wait.h>

#include <errno.h>
#include <pwd.h>
Expand Down Expand Up @@ -73,6 +74,7 @@
#include "session.h"
#include "servconf.h"
#include "monitor_wrap.h"
#include "srclimit.h"

#include "ssherr.h"

Expand Down Expand Up @@ -137,6 +139,36 @@ mm_request_send(int sock, enum monitor_reqtype type, struct sshbuf *m)
fatal_f("write: %s", strerror(errno));
}

static void
mm_reap(void)
{
int status = -1;

if (!mm_is_monitor())
return;
while (waitpid(pmonitor->m_pid, &status, 0) == -1) {
if (errno == EINTR)
continue;
pmonitor->m_pid = -1;
fatal_f("waitpid: %s", strerror(errno));
}
if (WIFEXITED(status)) {
if (WEXITSTATUS(status) != 0) {
debug_f("preauth child exited with status %d",
WEXITSTATUS(status));
cleanup_exit(255);
}
} else if (WIFSIGNALED(status)) {
error_f("preauth child terminated by signal %d",
WTERMSIG(status));
cleanup_exit(signal_is_crash(WTERMSIG(status)) ?
EXIT_CHILD_CRASH : 255);
} else {
error_f("preauth child terminated abnormally");
cleanup_exit(EXIT_CHILD_CRASH);
}
}

void
mm_request_receive(int sock, struct sshbuf *m)
{
Expand All @@ -149,6 +181,7 @@ mm_request_receive(int sock, struct sshbuf *m)
if (atomicio(read, sock, buf, sizeof(buf)) != sizeof(buf)) {
if (errno == EPIPE) {
debug3_f("monitor fd closed");
mm_reap();
cleanup_exit(255);
}
fatal_f("read: %s", strerror(errno));
Expand Down
134 changes: 133 additions & 1 deletion servconf.c
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* $OpenBSD: servconf.c,v 1.407 2024/05/17 01:17:40 djm Exp $ */
/* $OpenBSD: servconf.c,v 1.408 2024/06/06 17:15:25 djm Exp $ */
/*
* Copyright (c) 1995 Tatu Ylonen <ylo@cs.hut.fi>, Espoo, Finland
* All rights reserved
Expand Down Expand Up @@ -163,6 +163,16 @@ initialize_server_options(ServerOptions *options)
options->per_source_max_startups = -1;
options->per_source_masklen_ipv4 = -1;
options->per_source_masklen_ipv6 = -1;
options->per_source_penalty_exempt = NULL;
options->per_source_penalty.enabled = -1;
options->per_source_penalty.max_sources = -1;
options->per_source_penalty.overflow_mode = -1;
options->per_source_penalty.penalty_crash = -1;
options->per_source_penalty.penalty_authfail = -1;
options->per_source_penalty.penalty_noauth = -1;
options->per_source_penalty.penalty_grace = -1;
options->per_source_penalty.penalty_max = -1;
options->per_source_penalty.penalty_min = -1;
options->max_authtries = -1;
options->max_sessions = -1;
options->banner = NULL;
Expand Down Expand Up @@ -402,6 +412,24 @@ fill_default_server_options(ServerOptions *options)
options->per_source_masklen_ipv4 = 32;
if (options->per_source_masklen_ipv6 == -1)
options->per_source_masklen_ipv6 = 128;
if (options->per_source_penalty.enabled == -1)
options->per_source_penalty.enabled = 0;
if (options->per_source_penalty.max_sources == -1)
options->per_source_penalty.max_sources = 65536;
if (options->per_source_penalty.overflow_mode == -1)
options->per_source_penalty.overflow_mode = PER_SOURCE_PENALTY_OVERFLOW_PERMISSIVE;
if (options->per_source_penalty.penalty_crash == -1)
options->per_source_penalty.penalty_crash = 90;
if (options->per_source_penalty.penalty_grace == -1)
options->per_source_penalty.penalty_grace = 20;
if (options->per_source_penalty.penalty_authfail == -1)
options->per_source_penalty.penalty_authfail = 5;
if (options->per_source_penalty.penalty_noauth == -1)
options->per_source_penalty.penalty_noauth = 1;
if (options->per_source_penalty.penalty_min == -1)
options->per_source_penalty.penalty_min = 15;
if (options->per_source_penalty.penalty_max == -1)
options->per_source_penalty.penalty_max = 600;
if (options->max_authtries == -1)
options->max_authtries = DEFAULT_AUTH_FAIL_MAX;
if (options->max_sessions == -1)
Expand Down Expand Up @@ -479,6 +507,7 @@ fill_default_server_options(ServerOptions *options)
CLEAR_ON_NONE(options->chroot_directory);
CLEAR_ON_NONE(options->routing_domain);
CLEAR_ON_NONE(options->host_key_agent);
CLEAR_ON_NONE(options->per_source_penalty_exempt);

for (i = 0; i < options->num_host_key_files; i++)
CLEAR_ON_NONE(options->host_key_files[i]);
Expand Down Expand Up @@ -513,6 +542,7 @@ typedef enum {
sBanner, sUseDNS, sHostbasedAuthentication,
sHostbasedUsesNameFromPacketOnly, sHostbasedAcceptedAlgorithms,
sHostKeyAlgorithms, sPerSourceMaxStartups, sPerSourceNetBlockSize,
sPerSourcePenalties, sPerSourcePenaltyExemptList,
sClientAliveInterval, sClientAliveCountMax, sAuthorizedKeysFile,
sGssAuthentication, sGssCleanupCreds, sGssStrictAcceptor,
sAcceptEnv, sSetEnv, sPermitTunnel,
Expand Down Expand Up @@ -645,6 +675,8 @@ static struct {
{ "maxstartups", sMaxStartups, SSHCFG_GLOBAL },
{ "persourcemaxstartups", sPerSourceMaxStartups, SSHCFG_GLOBAL },
{ "persourcenetblocksize", sPerSourceNetBlockSize, SSHCFG_GLOBAL },
{ "persourcepenalties", sPerSourcePenalties, SSHCFG_GLOBAL },
{ "persourcepenaltyexemptlist", sPerSourcePenaltyExemptList, SSHCFG_GLOBAL },
{ "maxauthtries", sMaxAuthTries, SSHCFG_ALL },
{ "maxsessions", sMaxSessions, SSHCFG_ALL },
{ "banner", sBanner, SSHCFG_ALL },
Expand Down Expand Up @@ -1945,6 +1977,89 @@ process_server_config_line_depth(ServerOptions *options, char *line,
options->per_source_max_startups = value;
break;

case sPerSourcePenaltyExemptList:
charptr = &options->per_source_penalty_exempt;
arg = argv_next(&ac, &av);
if (!arg || *arg == '\0')
fatal("%s line %d: missing file name.",
filename, linenum);
if (addr_match_list(NULL, arg) != 0) {
fatal("%s line %d: keyword %s "
"invalid address argument.",
filename, linenum, keyword);
}
if (*activep && *charptr == NULL)
*charptr = xstrdup(arg);
break;

case sPerSourcePenalties:
while ((arg = argv_next(&ac, &av)) != NULL) {
found = 1;
value = -1;
value2 = 0;
p = NULL;
/* Allow no/yes only in first position */
if (strcasecmp(arg, "no") == 0 ||
(value2 = (strcasecmp(arg, "yes") == 0))) {
if (ac > 0) {
fatal("%s line %d: keyword %s \"%s\" "
"argument must appear alone.",
filename, linenum, keyword, arg);
}
if (*activep &&
options->per_source_penalty.enabled == -1)
options->per_source_penalty.enabled = value2;
continue;
} else if (strncmp(arg, "crash:", 6) == 0) {
p = arg + 6;
intptr = &options->per_source_penalty.penalty_crash;
} else if (strncmp(arg, "authfail:", 9) == 0) {
p = arg + 9;
intptr = &options->per_source_penalty.penalty_authfail;
} else if (strncmp(arg, "noauth:", 7) == 0) {
p = arg + 7;
intptr = &options->per_source_penalty.penalty_noauth;
} else if (strncmp(arg, "grace-exceeded:", 15) == 0) {
p = arg + 15;
intptr = &options->per_source_penalty.penalty_grace;
} else if (strncmp(arg, "max:", 4) == 0) {
p = arg + 4;
intptr = &options->per_source_penalty.penalty_max;
} else if (strncmp(arg, "min:", 4) == 0) {
p = arg + 4;
intptr = &options->per_source_penalty.penalty_min;
} else if (strncmp(arg, "max-sources:", 12) == 0) {
intptr = &options->per_source_penalty.max_sources;
if ((errstr = atoi_err(arg+12, &value)) != NULL)
fatal("%s line %d: %s value %s.",
filename, linenum, keyword, errstr);
} else if (strcmp(arg, "overflow:deny-all") == 0) {
intptr = &options->per_source_penalty.overflow_mode;
value = PER_SOURCE_PENALTY_OVERFLOW_DENY_ALL;
} else if (strcmp(arg, "overflow:permissive") == 0) {
intptr = &options->per_source_penalty.overflow_mode;
value = PER_SOURCE_PENALTY_OVERFLOW_PERMISSIVE;
} else {
fatal("%s line %d: unsupported %s keyword %s",
filename, linenum, keyword, arg);
}
/* If no value was parsed above, assume it's a time */
if (value == -1 && (value = convtime(p)) == -1) {
fatal("%s line %d: invalid %s time value.",
filename, linenum, keyword);
}
if (*activep && *intptr == -1) {
*intptr = value;
/* any option implicitly enables penalties */
options->per_source_penalty.enabled = 1;
}
}
if (!found) {
fatal("%s line %d: no %s specified",
filename, linenum, keyword);
}
break;

case sMaxAuthTries:
intptr = &options->max_authtries;
goto parse_int;
Expand Down Expand Up @@ -3082,6 +3197,7 @@ dump_config(ServerOptions *o)
dump_cfg_string(sRDomain, o->routing_domain);
#endif
dump_cfg_string(sSshdSessionPath, o->sshd_session_path);
dump_cfg_string(sPerSourcePenaltyExemptList, o->per_source_penalty_exempt);

/* string arguments requiring a lookup */
dump_cfg_string(sLogLevel, log_level_name(o->log_level));
Expand Down Expand Up @@ -3169,4 +3285,20 @@ dump_config(ServerOptions *o)
if (o->pubkey_auth_options & PUBKEYAUTH_VERIFY_REQUIRED)
printf(" verify-required");
printf("\n");

if (o->per_source_penalty.enabled) {
printf("persourcepenalties crash:%d authfail:%d noauth:%d "
"grace-exceeded:%d max:%d min:%d max-sources:%d "
"overflow:%s\n", o->per_source_penalty.penalty_crash,
o->per_source_penalty.penalty_authfail,
o->per_source_penalty.penalty_noauth,
o->per_source_penalty.penalty_grace,
o->per_source_penalty.penalty_max,
o->per_source_penalty.penalty_min,
o->per_source_penalty.max_sources,
o->per_source_penalty.overflow_mode ==
PER_SOURCE_PENALTY_OVERFLOW_DENY_ALL ?
"deny-all" : "permissive");
} else
printf("persourcepenalties no\n");
}
18 changes: 17 additions & 1 deletion servconf.h
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* $OpenBSD: servconf.h,v 1.163 2024/05/23 23:47:16 jsg Exp $ */
/* $OpenBSD: servconf.h,v 1.164 2024/06/06 17:15:25 djm Exp $ */

/*
* Author: Tatu Ylonen <ylo@cs.hut.fi>
Expand Down Expand Up @@ -65,6 +65,20 @@ struct listenaddr {
struct addrinfo *addrs;
};

#define PER_SOURCE_PENALTY_OVERFLOW_DENY_ALL 1
#define PER_SOURCE_PENALTY_OVERFLOW_PERMISSIVE 2
struct per_source_penalty {
int enabled;
int max_sources;
int overflow_mode;
int penalty_crash;
int penalty_grace;
int penalty_authfail;
int penalty_noauth;
int penalty_max;
int penalty_min;
};

typedef struct {
u_int num_ports;
u_int ports_from_cmdline;
Expand Down Expand Up @@ -172,6 +186,8 @@ typedef struct {
int per_source_max_startups;
int per_source_masklen_ipv4;
int per_source_masklen_ipv6;
char *per_source_penalty_exempt;
struct per_source_penalty per_source_penalty;
int max_authtries;
int max_sessions;
char *banner; /* SSH-2 banner message */
Expand Down
Loading

0 comments on commit 81c1099

Please sign in to comment.