diff --git a/auth.h b/auth.h index 0f11458ca..17227cffb 100644 --- a/auth.h +++ b/auth.h @@ -1,4 +1,4 @@ -/* $OpenBSD: auth.h,v 1.110 2026/07/06 07:44:48 djm Exp $ */ +/* $OpenBSD: auth.h,v 1.111 2026/09/16 00:16:52 djm Exp $ */ /* * Copyright (c) 2000 Markus Friedl. All rights reserved. @@ -59,6 +59,8 @@ struct Authctxt { int valid; /* user exists and is allowed to login */ int attempt; int failures; + int pk_ok_failures; + int auth_failure_already_counted; int server_caused_failure; int force_pwchange; char *user; /* username sent by the client */ diff --git a/auth2-pubkey.c b/auth2-pubkey.c index c15b1ba71..97d248795 100644 --- a/auth2-pubkey.c +++ b/auth2-pubkey.c @@ -1,4 +1,4 @@ -/* $OpenBSD: auth2-pubkey.c,v 1.127 2026/07/30 03:37:39 djm Exp $ */ +/* $OpenBSD: auth2-pubkey.c,v 1.128 2026/09/16 00:16:52 djm Exp $ */ /* * Copyright (c) 2000 Markus Friedl. All rights reserved. * Copyright (c) 2010 Damien Miller. All rights reserved. @@ -291,7 +291,19 @@ userauth_pubkey(struct ssh *ssh, const char *method) (r = ssh_packet_write_wait(ssh)) != 0) fatal_fr(r, "send packet"); authctxt->postponed = 1; - } + } else { + /* + * Don't count this as an authentication failure + * unless we have already used up the separate + * max-pk-ok budget (if any). + */ + if (authctxt->pk_ok_failures++ < options.max_pubkey_ok) + authctxt->auth_failure_already_counted = 1; + debug3_f("pubkey test %d of %d%s", + authctxt->pk_ok_failures, options.max_pubkey_ok, + authctxt->auth_failure_already_counted ? + "" : ": treating as authentication failure"); + } } done: if (authenticated == 1 && auth_activate_options(ssh, authopts) != 0) { diff --git a/auth2.c b/auth2.c index 3f353a719..6971b933b 100644 --- a/auth2.c +++ b/auth2.c @@ -1,4 +1,4 @@ -/* $OpenBSD: auth2.c,v 1.174 2026/07/06 07:44:48 djm Exp $ */ +/* $OpenBSD: auth2.c,v 1.175 2026/09/16 00:16:52 djm Exp $ */ /* * Copyright (c) 2000 Markus Friedl. All rights reserved. * @@ -344,6 +344,7 @@ input_userauth_request(int type, uint32_t seq, struct ssh *ssh) auth2_authctxt_reset_info(authctxt); authctxt->postponed = 0; authctxt->server_caused_failure = 0; + authctxt->auth_failure_already_counted = 0; /* try to authenticate user */ m = authmethod_lookup(authctxt, method); @@ -448,6 +449,7 @@ userauth_finish(struct ssh *ssh, int authenticated, const char *packet_method, } else { /* Allow initial try of "none" auth without failure penalty */ if (!partial && !authctxt->server_caused_failure && + !authctxt->auth_failure_already_counted && (authctxt->attempt > 1 || strcmp(method, "none") != 0)) authctxt->failures++; if (authctxt->failures >= options.max_authtries) { diff --git a/servconf.c b/servconf.c index d6f344989..a5bf3479a 100644 --- a/servconf.c +++ b/servconf.c @@ -1,4 +1,4 @@ -/* $OpenBSD: servconf.c,v 1.453 2026/09/16 00:13:58 djm Exp $ */ +/* $OpenBSD: servconf.c,v 1.454 2026/09/16 00:16:52 djm Exp $ */ /* * Copyright (c) 1995 Tatu Ylonen , Espoo, Finland * All rights reserved @@ -159,6 +159,9 @@ initialize_server_options(ServerOptions *options) options->subsystem_name = NULL; \ options->subsystem_command = NULL; \ options->subsystem_args = NULL; +#define init_pubkey_auth_options(options) \ + options->pubkey_auth_options = -1; \ + options->max_pubkey_ok = -1; #define init_timingsecret(options) \ options->timing_secret = 0; @@ -407,6 +410,10 @@ fill_default_server_options(ServerOptions *options) options->sshd_session_path = xstrdup(_PATH_SSHD_SESSION); if (options->sshd_auth_path == NULL) options->sshd_auth_path = xstrdup(_PATH_SSHD_AUTH); + if (options->pubkey_auth_options == -1) { + options->pubkey_auth_options = 0; + options->max_pubkey_ok = DEFAULT_AUTH_FAIL_MAX; + } assemble_algorithms(options); @@ -1403,6 +1410,7 @@ process_server_config_line_depth(ServerOptions *options, char *line, case sPubkeyAuthOptions: intptr = &options->pubkey_auth_options; value = 0; + value2 = -1; while ((arg = argv_next(&ac, &av)) != NULL) { if (strcasecmp(arg, "none") == 0) continue; @@ -1410,14 +1418,24 @@ process_server_config_line_depth(ServerOptions *options, char *line, value |= PUBKEYAUTH_TOUCH_REQUIRED; else if (strcasecmp(arg, "verify-required") == 0) value |= PUBKEYAUTH_VERIFY_REQUIRED; - else { + else if (strncasecmp(arg, "max-pk-ok:", 10) == 0) { + value2 = strtonum(arg + 10, 0, 255, &errstr); + if (errstr != NULL) { + error("%s line %d: bad %s max-pk-ok " + "value: %s", filename, linenum, + keyword, errstr); + goto out; + } + } else { error("%s line %d: unsupported %s option %s", filename, linenum, keyword, arg); goto out; } } - if (*activep && *intptr == -1) - *intptr = value; + if (*activep && options->pubkey_auth_options == -1) { + options->pubkey_auth_options = value; + options->max_pubkey_ok = value2; + } break; #ifdef KRB5 @@ -3060,6 +3078,19 @@ serialise_subsystem(const ServerOptions *options, struct sshbuf *buf) return 0; } +static int +serialise_pubkey_auth_options(const ServerOptions *options, struct sshbuf *buf) +{ + int r; + + if ((r = serialise_s32(buf, options->pubkey_auth_options)) != 0 || + (r = serialise_s32(buf, options->max_pubkey_ok)) != 0) { + error_fr(r, "serialise"); + return r; + } + return 0; +} + static int serialise_timingsecret(const ServerOptions *options, struct sshbuf *buf) { @@ -3594,6 +3625,19 @@ deserialise_subsystem(ServerOptions *options, struct sshbuf *buf) return 0; } +static int +deserialise_pubkey_auth_options(ServerOptions *options, struct sshbuf *buf) +{ + int r; + + if ((r = deserialise_s32(buf, &options->pubkey_auth_options)) != 0 || + (r = deserialise_s32(buf, &options->max_pubkey_ok)) != 0) { + error_fr(r, "deserialise"); + return r; + } + return 0; +} + static int deserialise_timingsecret(ServerOptions *options, struct sshbuf *buf) { @@ -3768,6 +3812,7 @@ free_server_options(ServerOptions *options) #define free_persourcenetblocksize(options) #define free_persourcepenalties(options) #define free_rekeylimit(options) +#define free_pubkey_auth_options(options) #define free_timingsecret(options) SSHD_CONFIG_ENTRIES @@ -3900,6 +3945,15 @@ copy_subsystem(ServerOptions *dst, const ServerOptions *src) dst->num_subsystems = src->num_subsystems; } +static void +copy_pubkey_auth_options(ServerOptions *dst, const ServerOptions *src) +{ + if (src->pubkey_auth_options != -1) { + dst->pubkey_auth_options = src->pubkey_auth_options; + dst->max_pubkey_ok = src->max_pubkey_ok; + } +} + /* * Copy any supported values that are set. * @@ -4374,6 +4428,8 @@ dump_config(ServerOptions *o) printf(" touch-required"); if (o->pubkey_auth_options & PUBKEYAUTH_VERIFY_REQUIRED) printf(" verify-required"); + if (o->max_pubkey_ok != -1) + printf(" max-pk-ok:%d", o->max_pubkey_ok); printf("\n"); if (o->per_source_penalty.enabled) { diff --git a/servconf.h b/servconf.h index a394274e2..089d600ad 100644 --- a/servconf.h +++ b/servconf.h @@ -1,4 +1,4 @@ -/* $OpenBSD: servconf.h,v 1.180 2026/09/16 00:13:58 djm Exp $ */ +/* $OpenBSD: servconf.h,v 1.181 2026/09/16 00:16:52 djm Exp $ */ /* * Author: Tatu Ylonen @@ -161,6 +161,7 @@ SSHCONF_CUSTOM(MaxStartups, maxstartups, SSHCFG_GLOBAL, SSHCFG_COPY_NONE) \ SSHCONF_CUSTOM(PerSourceNetBlockSize, persourcenetblocksize, SSHCFG_GLOBAL, SSHCFG_COPY_NONE) \ SSHCONF_CUSTOM(PerSourcePenalties, persourcepenalties, SSHCFG_GLOBAL, SSHCFG_COPY_NONE) \ SSHCONF_CUSTOM(RekeyLimit, rekeylimit, SSHCFG_ALL, SSHCFG_COPY_MATCH) \ +SSHCONF_CUSTOM(PubkeyAuthOptions, pubkey_auth_options, SSHCFG_ALL, SSHCFG_COPY_MATCH) \ SSHCONF_NONCONF(timingsecret) #define SSHD_CONFIG_ENTRIES_MAIN \ @@ -192,7 +193,6 @@ SSHCONF_INTFLAG(hostbased_uses_name_from_packet_only, HostbasedUsesNameFromPacke SSHCONF_STRING(hostbased_accepted_algos, HostbasedAcceptedAlgorithms, SSHCFG_ALL, SSHCFG_COPY_MATCH) \ SSHCONF_STRING(hostkeyalgorithms, HostKeyAlgorithms, SSHCFG_GLOBAL, SSHCFG_COPY_NONE) \ SSHCONF_STRING(ca_sign_algorithms, CASignatureAlgorithms, SSHCFG_ALL, SSHCFG_COPY_MATCH) \ -SSHCONF_INT(pubkey_auth_options, PubkeyAuthOptions, SSHCFG_ALL, NULL, 0, SSHCFG_COPY_MATCH) \ SSHCONF_INTFLAG(pubkey_authentication, PubkeyAuthentication, SSHCFG_ALL, 1, SSHCFG_COPY_MATCH) \ SSHCONF_STRING(pubkey_accepted_algos, PubkeyAcceptedAlgorithms, SSHCFG_ALL, SSHCFG_COPY_MATCH) \ SSHCONF_INTFLAG(password_authentication, PasswordAuthentication, SSHCFG_ALL, 1, SSHCFG_COPY_MATCH) \ @@ -394,6 +394,9 @@ typedef struct ServerOptions { /* RekeyLimit */ int64_t rekey_limit; int rekey_interval; + /* PubkeyAuthOptions */ + int pubkey_auth_options; + int max_pubkey_ok; /* Passed by config but not keyword for this */ uint64_t timing_secret; } ServerOptions; diff --git a/sshd_config.5 b/sshd_config.5 index 1bb4a3791..0d9f88061 100644 --- a/sshd_config.5 +++ b/sshd_config.5 @@ -33,7 +33,7 @@ .\" (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF .\" THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. .\" -.\" $OpenBSD: sshd_config.5,v 1.404 2026/09/16 00:13:58 djm Exp $ +.\" $OpenBSD: sshd_config.5,v 1.405 2026/09/16 00:16:52 djm Exp $ .Dd $Mdocdate: September 16 2026 $ .Dt SSHD_CONFIG 5 .Os @@ -1782,9 +1782,10 @@ Sets one or more public key authentication options. The supported keywords are: .Cm none (the default; indicating no additional options are enabled), -.Cm touch-required +.Cm touch-required , +.Cm verify-required and -.Cm verify-required . +.Cm max-pk-ok . .Pp The .Cm touch-required @@ -1812,6 +1813,19 @@ Neither the or .Cm verify-required options have any effect for other, non-FIDO, public key types. +.Pp +The +.Cm max-pk-ok +option takes a numeric argument separated by a colon character, e.g. +.Sq max-pk-ok:4 +to specify the number of public key query attempts are permitted +before the attempts are considered failed authentications that count against +.Cm MaxAuthTries . +Public key queries are a SSH protocol mechanism that allow a client to test +whether a server accepts a given public key before an actual authentication +attempt, to avoid the need to enter PINs/key passphrases or touch FIDO +authenticators unnecessarily. +The default is 6. .It Cm PubkeyAuthentication Specifies whether public key authentication is allowed. The default is