Agent forwarding lends your keys to every host you pass through, so anyone with root on a bastion can sign in as you elsewhere while your session is open. Engineers reach for ForwardAgent because it makes multi-hop access painless, and the private key never leaves the laptop, which feels safe. The OpenSSH manual is blunt that it is not: a user who can bypass file permissions on the remote host can use the forwarded agent socket, even though they cannot copy the key. ProxyJump solves the same reachability problem by tunnelling an end-to-end session through the jump host instead. This guide compares the two threat models, explains when forwarding is still justified and how to constrain it, and sets out bastion and client configuration for teams who own SSH access. Server hardening in general is covered in the OpenSSH Server Hardening Guide; this page stays on the multi-hop decision.
Key takeaways
- Forwarding exposes key use, not key material. The ssh_config manual states that an attacker on the remote host cannot obtain key material from the agent but can perform operations on the keys, which is all they need to authenticate onward (ATT&CK T1563.001 SSH Hijacking).
- ProxyJump keeps authentication end to end. The client opens a TCP forwarding through the jump host and authenticates directly to the target, so the jump host relays encrypted traffic and never holds a usable signing channel.
- Use ProxyJump by default, forwarding by exception. The ssh manual itself suggests a jump host as a safer alternative to -A. Forwarding remains valid where the remote host must itself authenticate onward, such as a git pull on a build box.
- Constrain any key you forward. Destination constraints (ssh-add -h, OpenSSH 8.9 and later), confirm-on-use (-c), short lifetimes (-t) and FIDO keys with user presence each narrow the window, but none stops a malicious hop from using what you forwarded to it.
- Design the bastion as a relay, not a shell. Allow local TCP forwarding to an allowlisted set of destinations, refuse agent forwarding and interactive shells, and log every channel, so a compromised bastion yields as little as possible.
How each option works
Agent forwarding (ForwardAgent, ssh -A)
Your local ssh-agent holds decrypted private keys and signs authentication challenges on request. With forwarding on, sshd on the first hop creates a Unix-domain socket on that host and sets SSH_AUTH_SOCK in your session. When you run ssh from the first hop to a second host, the second host's challenge travels back over your existing connection to the agent on your laptop, which signs it. The key never crosses the network, which the ssh-agent manual confirms: passphrases and private keys never go over the network.
The weakness is the socket on the intermediate host. The ssh-agent manual describes the agent socket as accessible only to the current user, "but is easily abused by root or another instance of the same user". Anyone who can open that socket while your session is live can ask your agent to sign for any host your keys open. Since OpenSSH 10.1 (released 6 October 2025), forwarded sockets are created under ~/.ssh/agent rather than /tmp, which reduces casual discovery but does nothing against root.
ProxyJump (ssh -J)
ProxyJump tells the client to make an SSH connection to the jump host first, then establish a TCP forwarding from there to the ultimate target. The client then runs a second, complete SSH handshake through that tunnel: host key verification of the target, key exchange and user authentication all happen between your laptop and the target. Multiple hops can be chained with commas. The jump host sees that you opened a channel to a given host and port and passes encrypted bytes; it has no agent socket to abuse and cannot read or alter the inner session.
Side by side
| Property | Agent forwarding | ProxyJump |
|---|---|---|
| Where the target authenticates you | On the intermediate host, which relays the signing request back to your agent | Directly from your client, through a tunnel |
| What a root user on the hop gains | Use of every loaded key for as long as your session lasts | Knowledge of the destination host and port, plus ability to drop or delay traffic |
| Target host key verified by | The intermediate host's known_hosts | Your own known_hosts |
| Server-side setting needed on the hop | AllowAgentForwarding yes (the default) | AllowTcpForwarding yes or local, with PermitOpen to restrict destinations |
| Works when the hop must act for you | Yes: git, scp or ssh run on the hop can use your keys | No: the hop gets no credentials |
| Interactive shell on the hop | Required | Not required; the bastion can refuse shells |
| ATT&CK relevance | T1563.001 SSH Hijacking via the agent socket | T1021.004 SSH lateral movement only with credentials stolen elsewhere |
The threat model in detail
MITRE ATT&CK T1563.001 describes adversaries hijacking existing SSH sessions to move laterally, either by compromising the agent or by having access to the agent's socket, and notes that with root access the hijack is likely trivial. Three scenarios cover most real exposure.
- Compromised shared bastion: an intruder with root on a jump host used by the whole operations team sees a fresh agent socket every time an engineer connects with -A. Each one is a window into that engineer's reach: production databases, other bastions, code hosting. The intruder never needs to crack a passphrase.
- Malicious or over-privileged administrator on the hop: the same mechanics apply to an insider with sudo on the intermediate host. Separation of duties between bastion administrators and production owners collapses the moment forwarding is common.
- Forwarding by accident: a wildcard
Host *block withForwardAgent yesforwards your agent to every server you touch, including third-party and customer hosts you have no reason to trust.
ProxyJump changes what a compromised hop can do. It can refuse to forward, redirect your connection to an impostor (which your own known_hosts check will flag), or record traffic metadata. It cannot sign anything as you. The residual risk moves to the client: your laptop must hold the keys and the known_hosts entries, which is why ProxyJump pairs naturally with a Privileged Access Workstation for high-value targets.
Constraining agent forwarding when you must use it
Some workflows genuinely need the remote host to authenticate onward: pulling a private repository on a build host, running rsync between two servers, or an orchestration host that fans out to many nodes. Before allowing forwarding, consider whether a dedicated, scoped machine credential on that host would be cleaner (see the SSH Key Management Guide). Where forwarding stays, layer the following controls.
| Control | How it is set | What it stops | What it does not stop |
|---|---|---|---|
| Destination constraints | ssh-add -h with host, user@host or hop>user@host entries; OpenSSH 8.9 or later on agent, client and servers | Use of the key for any destination or path not listed; the agent refuses it | A malicious hop using the key for a destination you did list through that hop |
| Confirm on use | ssh-add -c, or AddKeysToAgent confirm in ssh_config; needs ssh-askpass | Silent use: each signature raises a prompt on your desktop | Approval fatigue; the prompt shows no destination or path, so a well-timed request is easy to approve |
| Short lifetime | ssh-add -t or ssh-agent -t | Keys lingering in the agent all day; without -t the default lifetime is forever | Abuse within the lifetime |
| FIDO key with user presence | ed25519-sk or ecdsa-sk key requiring touch, optionally PIN | Unattended signing: every signature needs a physical touch | A touch you give for your own login being used by a request you did not intend |
| Separate agent per trust zone | IdentityAgent pointing to a dedicated agent socket for that Host block, with IdentitiesOnly | Exposure of unrelated keys (production, personal, code signing) to a lower-trust hop | Abuse of the keys in that zone's agent |
The OpenSSH project's own write-up on agent restriction is candid about the limits. Destination constraints rely on protocol extensions in ssh-agent, ssh-add, ssh and sshd on most participating hosts, map host names to host keys through your known_hosts at the time you add the key, and are immutable once loaded. If an agent is forwarded to an attacker-controlled host, that host can still use the keys permitted there. Confirmation prompts are described as seldom used and somewhat easy to phish, and FIDO user presence as offering a little more defence while being similarly phishable. Treat these as damage limitation, not a fix. FIDO details, including the agent's default refusal to sign web authentication requests, are in the Security Keys for SSH guide.
Designing the bastion for ProxyJump
A bastion that only relays connections is far less valuable to an intruder than one where engineers log in, keep shells open and forward agents. The sshd_config manual warns that disabling agent, TCP or StreamLocal forwarding "does not improve security unless users are also denied shell access", and the inverse holds for a relay bastion: the protection comes from combining no shells with tightly scoped forwarding.
- Give relay users no shell: put jump users in a dedicated group and, in a
Match Groupblock, refuse interactive sessions (for example a ForceCommand that exits), setPermitTTY noandX11Forwarding no. - Allow only outbound local forwarding:
AllowTcpForwarding local,AllowAgentForwarding no,AllowStreamLocalForwarding noandPermitListen noneso the bastion cannot be used for reverse tunnels. - Allowlist destinations:
PermitOpenwith the host:port pairs (or address ranges per Match block) that the group may reach. By default all forwarding requests are permitted, so leaving this unset turns the bastion into an open relay for anyone with a valid key. - Remember first-match ordering: when a keyword appears in several satisfied Match blocks, only the first instance applies, so place the most restrictive blocks first and test with
sshd -Tusing connection parameters. - Keep administration separate: bastion administrators connect through a different path and group with shells, MFA and session recording; relay users never share it. The Securing the jump host and bastion itself page covers OS hardening, patching and monitoring of the bastion host.
- Prefer certificates for scale: SSH user certificates with short validity and principals let the bastion and targets trust one CA rather than thousands of authorized_keys entries, and certificate host keys make destination constraints easier to maintain. See the SSH Protocol and Certificate Authority Guide.
A target host behind a relay bastion should also accept connections only from the bastion's addresses (a firewall rule or a from= option on keys) and set DisableForwarding yes unless a Match block grants an owned exception.
Client configuration that makes the safe path the easy path
Most forwarding happens because it is in someone's ~/.ssh/config, not because anyone decided it was needed. Ship a managed client configuration that sets a secure default and makes ProxyJump a one-word habit.
- Global default off: a final
Host *block withForwardAgent no. Because ssh_config uses the first value obtained for each parameter, place specific Host blocks above it. - Per-zone jump hosts:
Host *.prod.example.internalwithProxyJump bastion-prod.example.internal, so engineers type the target name and the tunnel follows. ProxyJump and ProxyCommand compete, and whichever is specified first wins, so remove legacy ProxyCommand lines that callnc. - Named exceptions only: where forwarding is approved, enable it only in the Host block for that host, with IdentityAgent pointing at a zone-specific agent loaded with ssh-add -c -t and -h constraints.
- Pin identities:
IdentitiesOnly yeswith an explicit IdentityFile per zone stops the client offering every loaded key to every server. - Agent hygiene: AddKeysToAgent confirm for interactive keys, a default lifetime on the agent, and ssh-add -D or a locked agent (ssh-add -x) when stepping away.
- Verify effective settings: ssh -G with the target name prints the resolved configuration, including ForwardAgent and ProxyJump, which is the fastest way to audit what a given connection will actually do.
How to implement: a phased migration
- Measure current use: on bastions and intermediate hosts, count sessions that request agent forwarding (sshd logs channel requests at LogLevel VERBOSE) and collect client configs from managed endpoints to find
ForwardAgent yesin wildcard blocks. - Classify each use: pure reachability (replace with ProxyJump), remote action needed (replace with a scoped machine credential or keep as a constrained exception), or unknown (contact the owner).
- Prepare the bastion: stand up the relay Match block with PermitOpen in parallel with existing access; do not remove shells yet.
- Pilot ring: move one team to a managed client config with ProxyJump and ForwardAgent no for two weeks, tracking failed connections and help requests.
- Enforce on the bastion: set AllowAgentForwarding no and remove shells for relay users. Roll back by reverting the Match block, which is a single configuration change and an sshd reload; keep the previous file and validate with sshd -t before every reload.
- Constrain the exceptions: for each approved forwarding case, document owner, hosts and expiry, and require destination constraints and confirmation.
- Close the loop: extend DisableForwarding yes to targets and review exceptions quarterly.
Decision guidance
| Situation | Recommended approach | Reason |
|---|---|---|
| Reaching a private host through a bastion | ProxyJump | No credential exposure on the hop |
| Git pull or rsync run on a remote build or staging host | Scoped deploy key or certificate on that host; constrained forwarding only as a stopgap | The remote action should not borrow a human's full key set |
| Fleet orchestration host fanning out to many nodes | Machine identity with short-lived certificates issued to the orchestrator | Forwarding a human agent to a busy shared host is the highest-exposure pattern |
| Hop you do not control (customer, partner, vendor) | ProxyJump, never forwarding | You cannot assess who has root there |
| Hosts on old OpenSSH without ProxyJump or destination-constraint support | ProxyCommand with ssh -W through the hop; upgrade plan | -W gives the same end-to-end tunnel; constraints need modern versions on every participating host |
| Single-hop access with no intermediate host | Neither: connect directly with ForwardAgent no | Forwarding into the destination only adds exposure |
Small versus large organisations: a small team with one bastion can get most of the benefit in a day by switching to ProxyJump and turning off agent forwarding on the bastion. Large estates need the managed client configuration, a certificate authority, PermitOpen sets per zone and an exceptions register, because forwarding tends to reappear through personal configs.
Detection and measurement
sshd logs to the AUTH facility; forward it centrally, since local logs on a compromised bastion cannot be trusted. The Linux SSH and Authentication Log Detection Guide covers parsing. Signals specific to this topic:
- Agent forwarding requests on hosts that should refuse them, and any successful forwarding on relay bastions after enforcement.
- Non-owner access to agent sockets on intermediate hosts: auditd watches on agent socket directories and process activity by root touching other users' SSH_AUTH_SOCK paths map to T1563.001.
- Logins on targets from the bastion address with keys whose owner has no open bastion session, or outside that owner's working pattern.
- Forwarding to destinations outside PermitOpen (refused channel opens), which can indicate reconnaissance through the bastion.
| Metric | Target |
|---|---|
| Share of bastion sessions requesting agent forwarding | Zero after enforcement |
| Managed endpoints with ForwardAgent yes in a wildcard Host block | Zero |
| Approved forwarding exceptions with owner, expiry and destination constraints | 100 percent |
| Relay bastion users with an interactive shell | Zero |
| Bastions with PermitOpen set for every relay group | 100 percent |
Common failure patterns
- Wildcard forwarding in personal configs:
Host *withForwardAgent yescopied from a blog years ago, forwarding to every server including third-party ones. - Disabling agent forwarding but leaving shells: users with a shell can relay the socket with other tools, which is exactly what the sshd_config warning describes; the bastion setting is cosmetic without shell restrictions.
- ProxyJump through an open relay: no PermitOpen, so any valid key holder can reach any internal port via the bastion.
- Trusting confirm prompts: approving every askpass popup without reading it, when the popup cannot show the destination anyway.
- One agent for everything: production, code signing and personal keys loaded into the same agent that gets forwarded to a staging box.
- Stale known_hosts with destination constraints: constraints resolve host names through known_hosts at load time, so wrong or missing entries either break access or bind the constraint to the wrong key.
Framework mapping
| Control objective | NIST CSF 2.0 | NIST SP 800-53 Rev 5 | ISO/IEC 27001:2022 Annex A | ATT&CK |
|---|---|---|---|---|
| Restrict how remote administrative access is routed | PR.AA-05 | AC-17, SC-7 | 8.20 | T1021.004 |
| Limit credential exposure on intermediate hosts | PR.AA-01 | IA-5, AC-6 | 8.2, 8.5 | T1563.001 |
| Authenticate users end to end | PR.AA-03 | IA-2 | 8.5 | T1563.001 |
| Control flows through the bastion (PermitOpen) | PR.AA-05 | AC-4 | 8.20 | T1021.004 |
| Managed client and server SSH configuration | PR.PS-01 | AC-17 | 8.9 | T1552.004 |
| Log and monitor forwarding and socket access | DE.CM-03 | AU-2 | 8.15, 8.16 | T1563.001 |
Where identity fits
Agent forwarding is an identity problem as much as a network one: a human's credentials end up acting on a host the human does not control, and every automated task built on forwarding is a machine workload borrowing a person's identity. Replacing those tasks with scoped, short-lived machine credentials gives each workload its own auditable identity and removes the reason to forward. The SSH Keys vs SSH Certificates guide covers the credential choice.
Practitioner checklist
- Set ForwardAgent no as the managed client default and use ProxyJump for every bastion path.
- Configure relay bastions with no shells, AllowTcpForwarding local, AllowAgentForwarding no, PermitListen none and an explicit PermitOpen per group.
- Replace automation that relies on a forwarded human agent with scoped machine keys or certificates.
- For each remaining forwarding exception, record owner and expiry and require ssh-add -h, -c and -t.
- Run a separate agent per trust zone with IdentityAgent and IdentitiesOnly.
- Use FIDO keys with user presence for administrative access.
- Restrict targets to accept SSH only from bastion addresses and set DisableForwarding yes by default.
- Forward sshd logs centrally and alert on agent forwarding requests and non-owner agent socket access.
- Audit effective client settings with ssh -G and server settings with sshd -T after each change.
Standards and references
- OpenBSD manual: ssh_config(5)
- OpenBSD manual: ssh(1)
- OpenBSD manual: sshd_config(5)
- OpenBSD manual: ssh-agent(1)
- OpenBSD manual: ssh-add(1)
- OpenSSH: SSH agent restriction
- OpenSSH: Release notes
- MITRE ATT&CK: T1563.001 SSH Hijacking
Related NHI Mgmt Group resources: OpenSSH Server Hardening Guide · Securing the jump host and bastion itself · Security Keys for SSH · If your jump host and bastion is compromised · Privileged Access Workstations vs Jump Servers