[PATCH 4/5] ssh, ssh-agent: show forwarding host in confirmation prompts
Yannik Sembritzki
yannik at sembritzki.org
Wed Oct 21 01:17:15 AEDT 2026
When a key is used through a forwarded agent, the agent knows the host
key of the host the agent was forwarded to, but not its name, so a
confirmation prompt can't tell the user which forwarded session the
request came from.
Add a session-hostname at openssh.com agent extension that ssh(1) sends
immediately before session-bind at openssh.com when it opens an agent
connection for a forwarded channel, carrying the host as given on the
command line. ssh-agent only accepts it once and only before the first
bind on a connection, so the remote host can't replace it: by the time
it can send anything, ssh(1) has already bound the connection. It is
used only for display, escaped like the user name, and never for access
control. With multi-hop forwarding, intermediate ssh(1)s send it over
connections that are already bound; the agent ignores it there.
ssh-agent shows it in prompts for forwarded requests, e.g.
requested from host "jump.example.com" via agent forwarding
for login as "root" to host key ED25519 SHA256:...
Agents that don't support the extension reply with a failure, which
ssh(1) ignores. With an ssh(1) older than 8.9, which doesn't bind
forwarded connections at all, the remote host could supply its own
hostname together with its own bind; this is no worse than the existing
situation where such connections can't be identified as forwarded.
---
PROTOCOL.agent | 22 ++++++++++++++++++
authfd.c | 24 +++++++++++++++++++
authfd.h | 1 +
clientloop.c | 2 ++
ssh-agent.c | 62 +++++++++++++++++++++++++++++++++++++++++++++++---
5 files changed, 108 insertions(+), 3 deletions(-)
diff --git a/PROTOCOL.agent b/PROTOCOL.agent
index 6dc75c05e..ab94a3078 100644
--- a/PROTOCOL.agent
+++ b/PROTOCOL.agent
@@ -104,4 +104,26 @@ A SSH_AGENTC_ADD_SMARTCARD_KEY_CONSTRAINED will return SSH_AGENT_SUCCESS
if any key (plain private or certificate) was successfully loaded, or
SSH_AGENT_FAILURE if no key was loaded.
+4. session-hostname at openssh.com extension
+
+This extension allows a ssh client to tell the agent the name of the
+host that it is about to bind an agent connection to using
+session-bind at openssh.com, so that the agent can show it to the user,
+e.g. when asking for confirmation of a key use.
+
+ byte SSH_AGENTC_EXTENSION (0x1b)
+ string session-hostname at openssh.com
+ string hostname
+
+Where 'hostname' is the destination host as specified by the user.
+
+This message must be sent before the first session-bind at openssh.com
+message on the connection, and may only be sent once. An agent will
+refuse it otherwise. This ensures that, for forwarded agent connections,
+the name was supplied by the ssh client that opened the connection to
+the agent and not by the remote host.
+
+The hostname is informational only. Agents must not use it for access
+control decisions, which should be based on the bound host keys.
+
$OpenBSD: PROTOCOL.agent,v 1.26 2026/06/02 06:18:27 djm Exp $
diff --git a/authfd.c b/authfd.c
index aef809fa6..832cc7363 100644
--- a/authfd.c
+++ b/authfd.c
@@ -745,6 +745,30 @@ ssh_remove_all_identities(int sock, int version)
return r;
}
+/*
+ * Tells the agent the name of the host that the following
+ * ssh_agent_bind_hostkey() is for, for display in confirmation prompts.
+ */
+int
+ssh_agent_bind_hostname(int sock, const char *hostname)
+{
+ struct sshbuf *msg;
+ int r;
+
+ if (hostname == NULL)
+ return SSH_ERR_INVALID_ARGUMENT;
+ if ((msg = sshbuf_new()) == NULL)
+ return SSH_ERR_ALLOC_FAIL;
+ if ((r = sshbuf_put_u8(msg, SSH_AGENTC_EXTENSION)) != 0 ||
+ (r = sshbuf_put_cstring(msg, "session-hostname at openssh.com")) != 0 ||
+ (r = sshbuf_put_cstring(msg, hostname)) != 0)
+ goto out;
+ r = ssh_request_reply_decode(sock, msg);
+ out:
+ sshbuf_free(msg);
+ return r;
+}
+
/* Binds a session ID to a hostkey via the initial KEX signature. */
int
ssh_agent_bind_hostkey(int sock, const struct sshkey *key,
diff --git a/authfd.h b/authfd.h
index b2e07bcf2..779dc2aa5 100644
--- a/authfd.h
+++ b/authfd.h
@@ -66,6 +66,7 @@ int ssh_agent_sign(int sock, const struct sshkey *key,
int ssh_agent_bind_hostkey(int sock, const struct sshkey *key,
const struct sshbuf *session_id, const struct sshbuf *signature,
int forwarding);
+int ssh_agent_bind_hostname(int sock, const char *hostname);
int ssh_agent_query_extensions(int sock, char ***exts);
diff --git a/clientloop.c b/clientloop.c
index 0df129113..e65446a80 100644
--- a/clientloop.c
+++ b/clientloop.c
@@ -1858,6 +1858,8 @@ client_request_agent(struct ssh *ssh, const char *request_type, int rchan)
debug_fr(r, "ssh_get_authentication_socket");
return NULL;
}
+ if ((r = ssh_agent_bind_hostname(sock, options.host_arg)) != 0)
+ debug2_fr(r, "ssh_agent_bind_hostname");
if ((r = ssh_agent_bind_hostkey(sock, ssh->kex->initial_hostkey,
ssh->kex->session_id, ssh->kex->initial_sig, 1)) == 0)
debug_f("bound agent to hostkey");
diff --git a/ssh-agent.c b/ssh-agent.c
index ac3a54a58..cb1419ade 100644
--- a/ssh-agent.c
+++ b/ssh-agent.c
@@ -99,6 +99,8 @@
#define AGENT_MAX_SESSION_IDS 16
/* Maximum size of session ID */
#define AGENT_MAX_SID_LEN 128
+/* Maximum length of hostname sent by session-hostname at openssh.com */
+#define AGENT_MAX_HOSTNAME_LEN 1024
/* Maximum number of destination constraints to accept on a key */
#define AGENT_MAX_DEST_CONSTRAINTS 1024
/* Maximum number of associated certificate constraints to accept on a key */
@@ -129,6 +131,7 @@ typedef struct socket_entry {
size_t nsession_ids;
struct hostkey_sid *session_ids;
int session_bind_attempted;
+ char *hostname;
} SocketEntry;
u_int sockets_alloc = 0;
@@ -219,6 +222,7 @@ close_socket(SocketEntry *e)
sshbuf_free(e->session_ids[i].sid);
}
free(e->session_ids);
+ free(e->hostname);
memset(e, '\0', sizeof(*e));
e->fd = -1;
e->type = AUTH_UNUSED;
@@ -864,6 +868,8 @@ buf_equal(const struct sshbuf *a, const struct sshbuf *b)
* from the requester. The destination host key is only shown if the
* request is for the session most recently bound by the remote ssh(1),
* whose host key signature the agent verified. Returns NULL on error.
+ * The forwarding host name was supplied by the local ssh(1) before it
+ * bound the connection.
*/
static char *
describe_forwarded_request(SocketEntry *e, const char *user,
@@ -893,7 +899,11 @@ describe_forwarded_request(SocketEntry *e, const char *user,
to) < 0)
goto out;
}
- xasprintf(&ret, "requested via agent forwarding\n%s", dest);
+ if (e->hostname == NULL || e->nsession_ids == 0)
+ xasprintf(&ret, "requested via agent forwarding\n%s", dest);
+ else
+ xasprintf(&ret, "requested from host \"%s\" via agent "
+ "forwarding\n%s", e->hostname, dest);
out:
free(fp);
free(to);
@@ -1858,6 +1868,48 @@ process_ext_session_bind(SocketEntry *e)
return r == 0 ? 1 : 0;
}
+/*
+ * Records the name of the host that the connection is about to be bound
+ * to. Only accepted before the first session-bind at openssh.com, so that
+ * the name can only come from the ssh(1) that opened the connection and
+ * not from a remote host using it as a forwarded agent.
+ */
+static int
+process_ext_session_hostname(SocketEntry *e)
+{
+ int r, width = INT_MAX;
+ char *hostname = NULL;
+ size_t len;
+
+ debug2_f("entering");
+ /*
+ * Not an error: with multi-hop forwarding, each intermediate ssh(1)
+ * sends one over a connection that is already bound.
+ */
+ if (e->session_bind_attempted || e->hostname != NULL) {
+ debug_f("ignoring hostname after session bind");
+ return 0;
+ }
+ if ((r = sshbuf_get_cstring(e->request, &hostname, &len)) != 0) {
+ error_fr(r, "parse");
+ return 0;
+ }
+ if (len == 0 || len > AGENT_MAX_HOSTNAME_LEN) {
+ error_f("invalid hostname length %zu", len);
+ free(hostname);
+ return 0;
+ }
+ /* It is only ever displayed, so store it escaped */
+ if (asmprintf(&e->hostname, INT_MAX, &width, "%s", hostname) < 0) {
+ error_f("asmprintf failed");
+ free(hostname);
+ return 0;
+ }
+ free(hostname);
+ debug_f("recorded hostname \"%s\"", e->hostname);
+ return 1;
+}
+
static int
process_ext_query(SocketEntry *e)
{
@@ -1870,7 +1922,8 @@ process_ext_query(SocketEntry *e)
if ((r = sshbuf_put_u8(msg, SSH_AGENT_EXTENSION_RESPONSE)) != 0 ||
(r = sshbuf_put_cstring(msg, "query")) != 0 ||
/* string[] supported extension types */
- (r = sshbuf_put_cstring(msg, "session-bind at openssh.com")) != 0)
+ (r = sshbuf_put_cstring(msg, "session-bind at openssh.com")) != 0 ||
+ (r = sshbuf_put_cstring(msg, "session-hostname at openssh.com")) != 0)
fatal_fr(r, "compose");
if ((r = sshbuf_put_stringb(e->output, msg)) != 0)
fatal_fr(r, "enqueue");
@@ -1893,12 +1946,15 @@ process_extension(SocketEntry *e)
/*
* This function can be called while the agent is locked to allow
- * session binds to be processed for new channels.
+ * session binds (and the hostnames that precede them) to be
+ * processed for new channels.
* Other operations should be refused when locked.
*/
if (strcmp(name, "session-bind at openssh.com") == 0) {
success = process_ext_session_bind(e);
+ } else if (strcmp(name, "session-hostname at openssh.com") == 0) {
+ success = process_ext_session_hostname(e);
} else if (locked) {
debug_f("attempt to use extension \"%s\" while locked", name);
goto generic_fail;
--
2.56.0
More information about the openssh-unix-dev
mailing list