SSH Forwarding Features Enabled
Checks effective SSH Match contexts for TCP, agent, tunnel and gateway forwarding.
- Why It Matters
- Forwarding can turn an SSH account into a route to protected services or expose a user's authentication agent.
- How It Detects
- Evaluates sshd -T for root and an unprivileged user from loopback and a representative remote address. Fails when TCP or agent forwarding is enabled (both default to yes), PermitTunnel is yes, point-to-point or ethernet, or GatewayPorts is yes or clientspecified (the defaults for the last two are no). A risky value set inside a Match block in the config files fails as a conditional finding, with the Match condition in the evidence.
- How to Fix
- Set AllowTcpForwarding no, AllowAgentForwarding no, PermitTunnel no and GatewayPorts no globally or in every applicable Match block, then reload sshd.
- How to Verify
Run sshd -T -C user=nobody,host=localhost,addr=192.0.2.1 and confirm each forwarding option is no.- CVSS v3.1 Vector
CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:L/I:L/A:N- Control Category
- Secure Configuration: CIS Controls v8 4; NIST CSF 2.0 PR.PS
- Fix Effort
- Low
- CIS Controls v8 Safeguard
- 4.1
- NIST CSF 2.0 Subcategory
- PR.PS-01
- CWE
- CWE-441
- NIST SP 800-53 Rev. 5
- AC-4, CM-7
- Benchmark Topic
- SSH server configuration: forwarding (topic only, no benchmark text)
- References
- OpenBSD, sshd_config(5) manual page
Source Code
The complete file that defines this check. Run it with openvapt scan --only HOST-SSH-007, or read it offline with openvapt explain HOST-SSH-007.
internal/checks/host/ssh_forwarding.go · Lines 1-121View on GitHub at This Commit
| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "strconv" |
| 6 | "strings" |
| 7 | |
| 8 | "github.com/openvapt/openvapt/internal/check" |
| 9 | "github.com/openvapt/openvapt/internal/checks/collect" |
| 10 | "github.com/openvapt/openvapt/internal/sys" |
| 11 | ) |
| 12 | |
| 13 | func init() { |
| 14 | check.Register(&check.Check{ |
| 15 | ID: "HOST-SSH-007", Title: "SSH Forwarding Features Enabled", Domain: check.DomainHost, Severity: check.Medium, |
| 16 | CVSS: "CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:L/I:L/A:N", |
| 17 | Summary: "Checks effective SSH Match contexts for TCP, agent, tunnel and gateway forwarding.", |
| 18 | Rationale: "Forwarding can turn an SSH account into a route to protected services or expose a user's authentication agent.", |
| 19 | Detection: "Evaluates sshd -T for root and an unprivileged user from loopback and a representative remote address. Fails when TCP or agent forwarding is enabled (both default to yes), PermitTunnel is yes, point-to-point or ethernet, or GatewayPorts is yes or clientspecified (the defaults for the last two are no). A risky value set inside a Match block in the config files fails as a conditional finding, with the Match condition in the evidence.", |
| 20 | Fix: "Set AllowTcpForwarding no, AllowAgentForwarding no, PermitTunnel no and GatewayPorts no globally or in every applicable Match block, then reload sshd.", |
| 21 | Verify: "Run sshd -T -C user=nobody,host=localhost,addr=192.0.2.1 and confirm each forwarding option is no.", |
| 22 | Refs: []check.Ref{refSSHDManual}, Standards: check.Standards{CWE: []string{"CWE-441"}, NIST: []string{"AC-4", "CM-7"}, CISTopic: "SSH server configuration: forwarding"}, |
| 23 | Run: func(ctx context.Context, s sys.System) []check.Result { |
| 24 | cfg, ok := collect.SSHD(ctx, s) |
| 25 | if !ok { |
| 26 | return []check.Result{check.SkipR("sshd", "SSH daemon configuration not found")} |
| 27 | } |
| 28 | // OpenSSH defaults: AllowTcpForwarding yes, AllowAgentForwarding |
| 29 | // yes, PermitTunnel no, GatewayPorts no. |
| 30 | bad := sshForwardingIssues(cfg) |
| 31 | if len(bad) > 0 { |
| 32 | return []check.Result{check.FailR("sshd", "SSH forwarding is enabled in at least one evaluated context", bad...)} |
| 33 | } |
| 34 | if r, bad := sshMatchFail(cfg, "SSH forwarding is enabled", "allowtcpforwarding", "allowagentforwarding", "permittunnel", "gatewayports", "x11forwarding"); bad { |
| 35 | return []check.Result{r} |
| 36 | } |
| 37 | return []check.Result{check.PassR("sshd", "SSH forwarding is disabled in all evaluated contexts")} |
| 38 | }, |
| 39 | }) |
| 40 | check.Register(&check.Check{ |
| 41 | ID: "HOST-SSH-008", Title: "SSH Authentication Controls Too Permissive", Domain: check.DomainHost, Severity: check.Low, |
| 42 | CVSS: "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N", |
| 43 | Summary: "Checks effective SSH authentication method and login grace settings.", |
| 44 | Rationale: "Long login windows and unconstrained single-factor authentication give online guessing and stolen credentials more opportunity.", |
| 45 | Detection: "Evaluates applicable Match contexts and flags LoginGraceTime over 60 seconds, or password or keyboard-interactive login enabled without an AuthenticationMethods list that requires a second method.", |
| 46 | Fix: "Set LoginGraceTime 60 or lower and choose AuthenticationMethods appropriate to the server, such as publickey, then restrict access with AllowUsers or AllowGroups where practical.", |
| 47 | Verify: "Use sshd -T -C for each allowed user and source context and inspect logingracetime, authenticationmethods, allowusers and allowgroups.", |
| 48 | Refs: []check.Ref{refSSHDManual}, Standards: check.Standards{CWE: []string{"CWE-307", "CWE-308"}, NIST: []string{"AC-7", "IA-2"}, CISTopic: "SSH server configuration: authentication restrictions"}, |
| 49 | Run: func(ctx context.Context, s sys.System) []check.Result { |
| 50 | cfg, ok := collect.SSHD(ctx, s) |
| 51 | if !ok { |
| 52 | return []check.Result{check.SkipR("sshd", "SSH daemon configuration not found")} |
| 53 | } |
| 54 | grace := 120 |
| 55 | if v := strings.TrimSuffix(cfg["logingracetime"], "s"); v != "" { |
| 56 | if n, err := strconv.Atoi(v); err == nil { |
| 57 | grace = n |
| 58 | } |
| 59 | } |
| 60 | var bad []string |
| 61 | if grace > 60 { |
| 62 | bad = append(bad, "logingracetime "+strconv.Itoa(grace)) |
| 63 | } |
| 64 | if sshPasswordLike(cfg) && !sshMultiFactor(cfg["authenticationmethods"]) { |
| 65 | bad = append(bad, "password or keyboard-interactive login enabled without a second factor (authenticationmethods "+orDefault(cfg["authenticationmethods"])+")") |
| 66 | } |
| 67 | if len(bad) > 0 { |
| 68 | return []check.Result{check.FailR("sshd", "SSH authentication restrictions are too permissive", bad...)} |
| 69 | } |
| 70 | return []check.Result{check.PassR("sshd", "SSH authentication restrictions are configured")} |
| 71 | }, |
| 72 | }) |
| 73 | } |
| 74 | |
| 75 | // sshForwardingIssues lists forwarding settings that are enabled, applying |
| 76 | // OpenSSH defaults for unset keywords. |
| 77 | func sshForwardingIssues(cfg map[string]string) []string { |
| 78 | var bad []string |
| 79 | for _, k := range []string{"allowtcpforwarding", "allowagentforwarding"} { |
| 80 | switch v := cfg[k]; v { |
| 81 | case "": |
| 82 | bad = append(bad, k+" default yes") |
| 83 | case "yes", "all", "local", "remote": |
| 84 | bad = append(bad, k+" "+v) |
| 85 | } |
| 86 | } |
| 87 | switch v := cfg["permittunnel"]; v { |
| 88 | case "yes", "point-to-point", "ethernet": |
| 89 | bad = append(bad, "permittunnel "+v) |
| 90 | } |
| 91 | switch v := cfg["gatewayports"]; v { |
| 92 | case "yes", "clientspecified": |
| 93 | bad = append(bad, "gatewayports "+v) |
| 94 | } |
| 95 | return bad |
| 96 | } |
| 97 | |
| 98 | // sshPasswordLike reports whether password or keyboard-interactive |
| 99 | // authentication is enabled (PasswordAuthentication defaults to yes). |
| 100 | func sshPasswordLike(cfg map[string]string) bool { |
| 101 | kbd := cfg["kbdinteractiveauthentication"] |
| 102 | if kbd == "" { |
| 103 | kbd = cfg["challengeresponseauthentication"] |
| 104 | } |
| 105 | pw := cfg["passwordauthentication"] |
| 106 | return pw == "" || pw == "yes" || kbd == "yes" |
| 107 | } |
| 108 | |
| 109 | // sshMultiFactor reports whether every AuthenticationMethods list requires at |
| 110 | // least two methods, for example "publickey,keyboard-interactive". |
| 111 | func sshMultiFactor(methods string) bool { |
| 112 | if methods == "" || methods == "any" { |
| 113 | return false |
| 114 | } |
| 115 | for _, list := range strings.Fields(methods) { |
| 116 | if !strings.Contains(list, ",") { |
| 117 | return false |
| 118 | } |
| 119 | } |
| 120 | return true |
| 121 | } |
Also available as plain Markdown for notes, scripts and AI tools. Same facts, same source, no styling.
Cite This Page
If this page helped your work, please cite it. The version and web address are filled in; the access date is today's date in your browser. Every other style, and when credit is required, is on How to Cite OpenVAPT.
OpenVAPT. (2026). HOST-SSH-007 SSH Forwarding Features Enabled (Version 0.1.0-alpha.6). OpenVAPT Check Catalogue. Retrieved October 16, 2026, from https://openvapt.com/checks/HOST-SSH-007.html
In text: (OpenVAPT, 2026). Accessed: the date shown is today's date from your own browser (the site's build date without JavaScript); change it to the day you read the page.
@misc{openvapt_host_ssh_007,
author = {{OpenVAPT}},
title = {{HOST-SSH-007 SSH Forwarding Features Enabled}},
year = {2026},
howpublished = {\url{https://openvapt.com/checks/HOST-SSH-007.html}},
note = {OpenVAPT Check Catalogue, version 0.1.0-alpha.6. Accessed 16 October 2026}
}
Accessed: the date shown is today's date from your own browser (the site's build date without JavaScript); change it to the day you read the page.
Citing OpenVAPT does not mean OpenVAPT endorses, approves or has reviewed your work.