[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