Last Updated: 2026-02-06
nbs-ssh is designed for development, testing, and AI-assisted debugging - not for production deployment in adversarial network conditions.
nbs-ssh exhibits a fundamentally different security philosophy from OpenSSH. Where OpenSSH minimises information disclosure, nbs-ssh maximises diagnostic richness for AI-inspectable workflows. This is an intentional design choice, not a deficiency.
| Aspect | OpenSSH | nbs-ssh |
|---|---|---|
| Error verbosity | Minimal | Verbose (for diagnostics) |
| Default security | Strict | Permissive |
| Attack surface | Minimal dependencies | Python + AsyncSSH + transitive |
| Memory handling | Explicit scrubbing | GC-dependent (with SecureString mitigation) |
| Host verification | Required | Optional |
If you require OpenSSH-level security, use OpenSSH.
This document responds to the security audit in Issue #4. Each finding is categorised as:
- FIXED - Implemented in the codebase
- ACCEPTED - Intentional design decision, documented here
- MITIGATED - Partially addressed with documented limitations
- FUTURE - Planned for future implementation
Status: FIXED
The library now includes comprehensive host key verification with OpenSSH-compatible learning.
CLI Behaviour (FIXED): The CLI now uses --strict-host-key-checking=ask by default, matching OpenSSH behaviour:
- Unknown hosts prompt for user confirmation before adding to
~/.ssh/known_hosts - Changed keys are rejected with a clear fingerprint comparison warning
- Use
--no-host-checkor--strict-host-key-checking=noto explicitly disable verification
# Default: prompts for unknown hosts (secure, matches OpenSSH)
nbs-ssh user@host command
# Explicit disable (for testing)
nbs-ssh --no-host-check user@host command
# Accept new hosts automatically, reject changed
nbs-ssh --strict-host-key-checking=accept-new user@host command
# Strict mode for scripts (reject unknown hosts)
nbs-ssh --strict-host-key-checking=yes user@host commandLibrary Behaviour: When using SSHConnection directly, use host_key_policy:
from nbs_ssh import SSHConnection, HostKeyPolicy
# STRICT: Reject unknown hosts (for scripts)
async with SSHConnection(host, host_key_policy=HostKeyPolicy.STRICT) as conn:
...
# ASK: Prompt for unknown (requires callback)
def on_unknown(host, port, key):
# Display fingerprint, prompt user
return user_approved
async with SSHConnection(
host,
host_key_policy=HostKeyPolicy.ASK,
on_unknown_host_key=on_unknown
) as conn:
...
# ACCEPT_NEW: Accept unknown, reject changed
async with SSHConnection(host, host_key_policy=HostKeyPolicy.ACCEPT_NEW) as conn:
...
# INSECURE: Accept all (testing only)
async with SSHConnection(host, host_key_policy=HostKeyPolicy.INSECURE) as conn:
...Note: Using known_hosts=None without host_key_policy still disables verification for backward compatibility.
Status: ACCEPTED
Dynamic SOCKS forwarding provides no authentication.
Rationale: OpenSSH also lacks built-in SOCKS authentication. The mitigation is binding to localhost only, which nbs-ssh also does by default.
Recommendation: Only use dynamic forwarding on trusted networks. Do not expose SOCKS ports to untrusted clients.
Status: MITIGATED
Python strings are immutable and cannot be reliably cleared from memory.
Mitigation implemented:
SecureStringclass stores secrets in ctypes-controlled memoryeradicate()method overwrites with cryptographically secure random bytesstr()/bytes()return<hidden>, preventing accidental loggingreveal()required for explicit access- CLI calls
eradicate()in finally block
Limitation: When reveal() is called to pass secrets to asyncssh, the returned Python string is in Python's managed memory. This is fundamental - asyncssh requires Python strings. SecureString prevents accidental leakage and provides explicit eradication of the controlled copy.
Usage:
from nbs_ssh import SecureString
password = SecureString(getpass.getpass())
try:
async with SSHConnection(host, auth=create_password_auth(password)):
...
finally:
password.eradicate()Status: ACCEPTED
Error messages include full paths for diagnostic purposes.
Rationale: nbs-ssh's primary purpose is AI-inspectable diagnostics. Detailed error messages enable:
- Automated troubleshooting
- Clear debugging information
- Evidence bundles for support
Recommendation: Do not expose nbs-ssh error messages to untrusted users. Log them internally only.
Status: ACCEPTED
nbs-ssh relies on AsyncSSH defaults.
Rationale: AsyncSSH maintains modern secure defaults. Explicit restrictions would require tracking OpenSSH's evolving recommendations and could break legitimate use cases.
Recommendation: Pin asyncssh>=2.14.2 (see HIGH-8).
Status: FIXED
Remote forwarding now defaults to binding on localhost only, matching OpenSSH's GatewayPorts=no behaviour. To bind to all interfaces, explicitly pass remote_host="" or remote_host="0.0.0.0".
# Default: binds to localhost only (secure)
handle = await manager.forward_remote(8080, "localhost", 3000)
# Explicit all-interface binding (use with caution)
handle = await manager.forward_remote(8080, "localhost", 3000, remote_host="")Status: ACCEPTED
No equivalent to OpenSSH's AllowTcpForwarding, PermitOpen, PermitListen.
Rationale: nbs-ssh is a client library, not a server. Forwarding restrictions are a server-side concern. The SSH server you connect to enforces its own policies.
Status: FIXED
Hostnames are now validated in SSHConnection.__init__:
- RFC 952/1123 compliant
- Max 253 characters total, labels max 63 characters
- Alphanumeric + hyphens only (no leading/trailing hyphens)
- Shell metacharacters, newlines, null bytes rejected
- Normalised to lowercase
from nbs_ssh import validate_hostname
# Explicit validation (also called automatically in SSHConnection)
hostname = validate_hostname("Example.COM") # Returns "example.com"Status: ACCEPTED
SSHSupervisor stores passwords for reconnection.
Rationale: This is intentional - the supervisor's purpose is maintaining persistent connections with automatic reconnection. Without stored credentials, reconnection would be impossible.
Recommendation: Use SSH agent authentication with SSHSupervisor instead of passwords. The agent handles credential lifecycle.
Status: ACCEPTED
Auth failure messages reveal which methods were tried.
Rationale: Essential for debugging authentication issues. OpenSSH's opaque "Permission denied" requires -v flags to diagnose; nbs-ssh provides this information by default.
Status: ACCEPTED
Different auth failures may produce distinguishable errors.
Rationale: See HIGH-6. Diagnostic value outweighs enumeration risk in development/testing contexts.
Status: FIXED
Action: Pin minimum to asyncssh>=2.14.2 in pyproject.toml to exclude CVE-2023-46445, CVE-2023-46446, CVE-2023-48795.
Status: ACCEPTED
Millisecond-precision timing in event logs.
Rationale: Timing information is essential for performance analysis and debugging. JSONL events are opt-in (--events flag or event_collector parameter).
Recommendation: Do not expose JSONL event logs to untrusted parties.
| ID | Issue | Status | Notes |
|---|---|---|---|
| MED-1 | AuthConfig __repr__ exposes secrets |
FIXED | to_dict() excludes secrets; __repr__ should be added |
| MED-2 | No username validation | FIXED | validate_username() - POSIX-style, max 32 chars |
| MED-3 | Path traversal | ACCEPTED | Key paths come from user/config, not untrusted input |
| MED-4 | Regex pattern injection (ReDoS) | ACCEPTED | Automation patterns from user code, not untrusted input |
| MED-5 | Incomplete evidence redaction | FIXED | Comprehensive patterns for passwords, PINs, tokens, API keys, auth headers |
| MED-6 | Transcript captures passwords | ACCEPTED | Automation is for controlled scripts, not untrusted input |
| MED-7 | No renegotiation config | ACCEPTED | AsyncSSH handles renegotiation |
| MED-8 | No port validation | FIXED | validate_port() - Range 1-65535 |
| MED-9 | No resource exhaustion protection | ACCEPTED | OS/SSH server limits apply |
| MED-10 | Intent replay | ACCEPTED | Fresh connection per session |
The following are known testing limitations:
| Gap | Reason | Impact |
|---|---|---|
| FIXED - Comprehensive tests added | ||
| Malformed protocol data | Mock server is well-behaved | Parser robustness untested |
| Fuzzing | Not implemented | Unknown edge cases |
| Rekeying attacks | Not implemented | Session security untested |
| Terrapin attack | AsyncSSH handles; not tested | CVE-2023-48795 mitigation untested |
Mitigation: We rely on AsyncSSH's protocol implementation, which has its own test suite and security track record.
nbs-ssh has a larger attack surface than OpenSSH due to the Python dependency chain:
| Component | OpenSSH | nbs-ssh |
|---|---|---|
| Lines of dependency code | ~100k | ~1M+ |
| Cryptographic implementations | 1 (system OpenSSL) | 2 (asyncssh + bundled) |
| Supply chain attack surface | OS package manager | PyPI |
| Protocol implementation | Reference (30+ years) | Third-party |
Mitigation: Pin dependencies, use pip-audit or similar tools, consider vendoring critical dependencies.
| Method | Status |
|---|---|
| Password | ✅ Supported |
| Public key (files) | ✅ Supported |
| SSH Agent | ✅ Supported |
| GSSAPI/Kerberos | ✅ Supported |
| Keyboard-interactive | ✅ Supported |
| Certificate-based | ✅ Supported |
| PKCS#11/Smart cards | ✅ Supported |
| FIDO2/U2F (sk-* keys) | ✅ Supported |
| Method | Reason |
|---|---|
| Host-based | Security concerns: relies on trusting entire machines, creates transitive trust vulnerabilities, superseded by certificates |
If you must use nbs-ssh in a security-sensitive context:
- Always specify
known_hosts- Never useknown_hosts=None - Use SSH agent - Avoid password storage; let the agent manage credentials
- Pin dependencies - Use
pip freezeand audit regularly - Don't expose events/errors - Keep JSONL logs and error messages internal
- Validate inputs - Validate hostnames, usernames, ports before passing to nbs-ssh
- Use SecureString - Wrap secrets and call
eradicate()when done - Consider OpenSSH - For adversarial networks, use the reference implementation
See SECURITY.md for responsible disclosure procedures.