Conversation
PR Review: SSH Agent Proxy FeatureThis is an excellent implementation of SSH agent proxying with host-based filtering. The code quality is high, security considerations are well thought out, and the integration is clean. Here's my detailed feedback: ✅ StrengthsArchitecture & Design
Security
Code Quality
Documentation
🔍 Issues & Recommendations1. Potential Resource Leak (Medium Priority)Location: internal/run/manager.go:440-446 When connecting to the upstream SSH agent, if a subsequent error occurs, the agent connection may not be closed: upstreamAgent, err := sshagent.ConnectAgent(upstreamSocket)
if err != nil {
if proxyServer != nil {
_ = proxyServer.Stop(context.Background())
}
return nil, fmt.Errorf("connecting to SSH agent: %w", err)
}
// If error occurs after this, upstreamAgent.Close() may not be calledRecommendation: Add cleanup for upstreamAgent in all error paths after it's created, or use a defer with error handling: defer func() {
if err != nil && upstreamAgent != nil {
_ = upstreamAgent.Close()
}
}()2. Race Condition in Audit Callback (Low Priority)Location: internal/sshagent/proxy.go:55-62 The // Option 1: Pass audit func to NewProxy
func NewProxy(upstream AgentClient, auditFunc AuditFunc) *Proxy
// Option 2: Use sync.Once for the first call3. Missing Error Check (Low Priority)Location: internal/run/manager.go:477 _, tcpPort, _ := parseHostPort(tcpAddr)The error from Recommendation: Check the error and handle appropriately: _, tcpPort, err := parseHostPort(tcpAddr)
if err != nil {
// cleanup and return error
}4. Test Permissions Check is Weak (Low Priority)Location: internal/sshagent/server_test.go:76-81 if mode&0077 != 0 {
t.Logf("Socket permissions: %o (note: some systems allow different permissions)", mode)
}This test logs a message but doesn't fail. The comment in server.go:88 states permissions should be 0666, but the test expects 0600 or similar. There's a mismatch between intention and validation. Recommendation: Either:
5. Incomplete Host Parsing Error Handling (Low Priority)Location: cmd/moat/cli/grant_ssh.go:157-171 The 6. Socket Directory Cleanup (Low Priority)Location: internal/run/manager.go:494-502 The socket directory is created but only cleaned up on immediate error. If the run fails later, the directory may be left behind. Consider cleanup in Destroy(). 📝 Minor SuggestionsCode Style
Testing
Documentation
🎯 Performance Considerations
🔒 Security Assessment✅ Security Done Right
|
Code Review: SSH Agent Proxy ImplementationI've reviewed this PR and found it to be a well-architected implementation of SSH agent forwarding with host-based filtering. Here's my detailed feedback: ✅ StrengthsSecurity Model
Code Quality
Architecture
🔍 Issues & Suggestions1. Race Condition in Host Tracking (Medium Priority)The // Thread 1: SetCurrentHost("github.com")
// Thread 2: SetCurrentHost("gitlab.com")
// Thread 1: Sign() - might use wrong host!Concern: The PR description mentions host tracking via an SSH wrapper, but I don't see Recommendation:
2. Missing Host Tracking Implementation (High Priority)# This search should show how SetCurrentHost is called:
$ grep -r "SetCurrentHost" internal/
# Only found in tests and the proxy itselfThe planning docs mention an
Questions:
3. Socket Permissions Security (Low Priority)server.go:88 sets socket permissions to 0666 (world-readable/writable): if err := os.Chmod(s.socketPath, 0666); err != nil {Analysis: The comment correctly notes security is maintained by:
Suggestion: Consider adding a reference to the proxy security model doc or expanding the comment to note this is required for cross-UID access. 4. TCP Server Binding (Low - Informational)For Docker mode, the TCP server binds to Question: On Linux, does Docker Desktop also require TCP mode, or does it use Unix sockets? Might be worth documenting the runtime detection logic. 5. Error Handling in socat Bridge (Low Priority)The moat-init.sh script starts socat in the background but doesn't validate it stays running: socat UNIX-LISTEN:/run/moat/ssh/agent.sock,fork TCP:"$MOAT_SSH_TCP_ADDR" &If socat fails after the health check loop, SSH operations will fail silently. Consider:
6. Fingerprint Parsing (Low Priority)grant_ssh.go:166 decodes base64 from authorized_keys format: keyBlob, err := base64.StdEncoding.DecodeString(parts[1])SSH public keys can use 7. Audit Data Consistency (Low Priority)The 📝 DocumentationStrengths:
Suggestions:
🧪 TestingCoverage: Good unit test coverage for core logic (filtering, signing, protocols) Missing:
Performance Considerations
Final VerdictOverall Assessment: This is a solid implementation with good security fundamentals. The architecture is sound and integrates well with moat's existing systems. Blocking Issues:
Recommended Next Steps:
The PR already has owner approval, so these are suggestions for follow-up improvements rather than blocking concerns. Code Quality: ⭐⭐⭐⭐ (4/5) |
Adds secure SSH agent forwarding to containers with per-host key access
control. Users grant access via `moat grant ssh --host github.com` which
maps a specific SSH key to a host. At runtime, the proxy only allows
signing requests for authorized host/key combinations.
Key features:
- Host-based key filtering (keys only work for granted hosts)
- Automatic container setup when SSH grants are present
- TCP bridge for Docker on macOS (Unix sockets via socat in container)
- Direct Unix socket mounting for Linux/Apple containers
- Audit logging for all SSH agent operations
Usage:
moat grant ssh --host github.com # Grant access (one-time)
moat run ./agent # SSH just works if grants present
The agent.yaml only needs:
grants:
- ssh:github.com
PR Review: SSH Agent Proxy with Host-Based FilteringOverviewThis is a well-architected feature that adds SSH agent forwarding with security-conscious host filtering. The implementation follows moat's security model and integrates cleanly with existing credential and audit systems. StrengthsArchitecture & Design
Code Quality
Issues & Concerns1. Socket Permissions Too Permissive (Security)Location: internal/sshagent/server.go:88 Setting socket permissions to 0666 (world read/write) is overly permissive. While comments explain security relies on directory isolation, this violates defense-in-depth. Any process discovering the socket path can connect. Recommendation: Use 0600 or 0660 instead. Investigate ACLs or group permissions for cross-user access. 2. Race Condition in Host Tracking (Bug)Location: internal/sshagent/proxy.go:103 currentHost is set by external wrapper but validated during Sign(). No guarantee host is set before signing. Could cause incorrect validation, wrong audit logs, or unexpected failures. Recommendation: Pass host as parameter to Sign() instead of shared state, or document that proxy is not safe for concurrent use. 3. TCP Server Binding UnclearLocation: internal/run/manager.go:462 TCP server binds to 127.0.0.1, but comments suggest Apple containers need 0.0.0.0 (like HTTP proxy). Configuration seems inconsistent. Recommendation: Document how Docker containers access TCP server. Consider token auth like HTTP proxy. 4. Missing Input Validation (Security)Location: cmd/moat/cli/grant_ssh.go:44 No validation that host is valid hostname. Could accept empty strings, special characters, IPs. Recommendation: Add hostname validation. 5. Incomplete Error Handling (Bug)Location: internal/sshagent/server.go:119-124 Accept errors silently ignored and retried indefinitely. File descriptor exhaustion won't be surfaced. Recommendation: Log errors or implement backoff. 6. Socat Bridge Lacks Health Check (Reliability)Location: internal/deps/scripts/moat-init.sh:14 Socat started in background with no health check or retry logic. Could fail silently. Recommendation: Add socket validation after creation loop. 7. Test Assertion MissingLocation: internal/sshagent/server_test.go:78-80 Test logs but doesn't assert. Name implies validation but doesn't actually test. Recommendation: Make proper assertion, rename as informational, or remove. Security AssessmentStrong Controls
Areas for Improvement
Test CoverageExcellent overall:
Missing:
RecommendationsMust Fix Before Merge
Should Fix
Nice to Have
SummaryThis is high-quality work with sound architecture and good security practices. However, socket permissions and race conditions should be addressed before merging. The implementation shows strong understanding of moat's architecture and follows project conventions well. Recommendation: Request changes for must-fix issues, then approve. |
keys
How it works
The proxy connects to the user's local SSH agent and exposes a filtered socket to the container. Only keys mapped to granted hosts are visible, and sign requests are validated against the grant before forwarding.
Runtime modes:
Changes