Skip to content

Commit bc0b2f6

Browse files
committed
feat: add SSH proxy server support
- Implement complete SSH server with public key and password authentication - Add SSH key management to user database (both File and MongoDB) - Create SSH CLI tools for key management - Add SSH configuration schema and TypeScript types - Integrate SSH server with main proxy lifecycle - Add REST endpoints for SSH key CRUD operations - Include comprehensive test suite and documentation - Support Git operations over SSH with full proxy chain integration
1 parent 10e0cb3 commit bc0b2f6

File tree

22 files changed

+1720
-85
lines changed

22 files changed

+1720
-85
lines changed

SSH.md

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
### SSH Git Proxy Data Flow
2+
3+
1. **Client Connection:**
4+
- An SSH client (e.g., `git` command line) connects to the proxy server's listening port.
5+
- The `ssh2.Server` instance receives the connection.
6+
7+
2. **Authentication:**
8+
- The server requests authentication (`client.on('authentication', ...)`).
9+
- **Public Key Auth:**
10+
- Client sends its public key.
11+
- Proxy formats the key (`keyString = \`${keyType} ${keyData.toString('base64')}\``).
12+
- Proxy queries the `Database` (`db.findUserBySSHKey(keyString)`).
13+
- If a user is found, auth succeeds (`ctx.accept()`). The _public_ key info is temporarily stored (`client.userPrivateKey`).
14+
- **Password Auth:**
15+
- If _no_ public key was offered, the client sends username/password.
16+
- Proxy queries the `Database` (`db.findUser(ctx.username)`).
17+
- If user exists, proxy compares the hash (`bcrypt.compare(ctx.password, user.password)`).
18+
- If valid, auth succeeds (`ctx.accept()`).
19+
- **Failure:** If any auth step fails, the connection is rejected (`ctx.reject()`).
20+
21+
3. **Session Ready & Command Execution:**
22+
- Client signals readiness (`client.on('ready', ...)`).
23+
- Client requests a session (`client.on('session', ...)`).
24+
- Client executes a command (`session.on('exec', ...)`), typically `git-upload-pack` or `git-receive-pack`.
25+
- Proxy extracts the repository path from the command.
26+
27+
4. **Internal Processing (Chain):**
28+
- The proxy constructs a simulated request object (`req`).
29+
- It calls `chain.executeChain(req)` to apply internal rules/checks.
30+
- **Blocked/Error:** If the chain returns an error or blocks the action, an error message is sent directly back to the client (`stream.write(...)`, `stream.end()`), and the flow stops.
31+
32+
5. **Connect to Remote Git Server:**
33+
- If the chain allows, the proxy initiates a _new_ SSH connection (`remoteGitSsh = new Client()`) to the actual remote Git server (e.g., GitHub), using the URL from `config.getProxyUrl()`.
34+
- **Key Selection:**
35+
- It initially intends to use the key from `client.userPrivateKey` (captured during client auth).
36+
- **Crucially:** Since `client.userPrivateKey` only contains the _public_ key details, the proxy cannot use it to authenticate _outbound_.
37+
- It **defaults** to using the **proxy's own private host key** (`config.getSSHConfig().hostKey.privateKeyPath`) for the connection to the remote server.
38+
- **Connection Options:** Sets host, port, username (`git`), timeouts, keepalives, and the selected private key.
39+
40+
6. **Remote Command Execution & Data Piping:**
41+
- Once connected to the remote server (`remoteGitSsh.on('ready', ...)`), the proxy executes the _original_ Git command (`remoteGitSsh.exec(command, ...)`).
42+
- The core proxying begins:
43+
- Data from **Client -> Proxy** (`stream.on('data', ...)`): Forwarded to **Proxy -> Remote** (`remoteStream.write(data)`).
44+
- Data from **Remote -> Proxy** (`remoteStream.on('data', ...)`): Forwarded to **Proxy -> Client** (`stream.write(data)`).
45+
46+
7. **Error Handling & Fallback (Remote Connection):**
47+
- If the initial connection attempt to the remote fails with an authentication error (`remoteGitSsh.on('error', ...)` message includes `All configured authentication methods failed`), _and_ it was attempting to use the (incorrectly identified) client key, it will explicitly **retry** the connection using the **proxy's private key**.
48+
- This retry logic handles the case where the initial key selection might have been ambiguous, ensuring it falls back to the guaranteed working key (the proxy's own).
49+
- If the retry also fails, or if the error was different, the error is sent to the client (`stream.write(err.toString())`, `stream.end()`).
50+
51+
8. **Stream Management & Teardown:**
52+
- Handles `close`, `end`, `error`, and `exit` events for both client (`stream`) and remote (`remoteStream`) streams.
53+
- Manages keepalives and timeouts for both connections.
54+
- When the client finishes sending data (`stream.on('end', ...)`), the proxy closes the connection to the remote server (`remoteGitSsh.end()`) after a brief delay.
55+
56+
### Data Flow Diagram (Sequence)
57+
58+
```mermaid
59+
sequenceDiagram
60+
participant C as Client (Git)
61+
participant P as Proxy Server (SSHServer)
62+
participant DB as Database
63+
participant R as Remote Git Server (e.g., GitHub)
64+
65+
C->>P: SSH Connect
66+
P-->>C: Request Authentication
67+
C->>P: Send Auth (PublicKey / Password)
68+
69+
alt Public Key Auth
70+
P->>DB: Verify Public Key (findUserBySSHKey)
71+
DB-->>P: User Found / Not Found
72+
else Password Auth
73+
P->>DB: Verify User/Password (findUser + bcrypt)
74+
DB-->>P: Valid / Invalid
75+
end
76+
77+
alt Authentication Successful
78+
P-->>C: Authentication Accepted
79+
C->>P: Execute Git Command (e.g., git-upload-pack repo)
80+
81+
P->>P: Execute Internal Chain (Check rules)
82+
alt Chain Blocked/Error
83+
P-->>C: Error Message
84+
Note right of P: End Flow
85+
else Chain Passed
86+
P->>R: SSH Connect (using Proxy's Private Key)
87+
R-->>P: Connection Ready
88+
P->>R: Execute Git Command
89+
90+
loop Data Transfer (Proxying)
91+
C->>P: Git Data Packet (Client Stream)
92+
P->>R: Forward Git Data Packet (Remote Stream)
93+
R->>P: Git Data Packet (Remote Stream)
94+
P->>C: Forward Git Data Packet (Client Stream)
95+
end
96+
97+
C->>P: End Client Stream
98+
P->>R: End Remote Connection (after delay)
99+
P-->>C: End Client Stream
100+
R-->>P: Remote Connection Closed
101+
C->>P: Close Client Connection
102+
end
103+
else Authentication Failed
104+
P-->>C: Authentication Rejected
105+
Note right of P: End Flow
106+
end
107+
108+
```
109+
110+
```
111+
112+
```

config.schema.json

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,39 @@
183183
}
184184
}
185185
}
186+
},
187+
"ssh": {
188+
"description": "SSH proxy server configuration",
189+
"type": "object",
190+
"properties": {
191+
"enabled": {
192+
"type": "boolean",
193+
"description": "Enable SSH proxy server"
194+
},
195+
"port": {
196+
"type": "number",
197+
"description": "Port for SSH proxy server to listen on",
198+
"default": 2222
199+
},
200+
"hostKey": {
201+
"type": "object",
202+
"description": "SSH host key configuration",
203+
"properties": {
204+
"privateKeyPath": {
205+
"type": "string",
206+
"description": "Path to private SSH host key",
207+
"default": "./.ssh/host_key"
208+
},
209+
"publicKeyPath": {
210+
"type": "string",
211+
"description": "Path to public SSH host key",
212+
"default": "./.ssh/host_key.pub"
213+
}
214+
},
215+
"required": ["privateKeyPath", "publicKeyPath"]
216+
}
217+
},
218+
"required": ["enabled"]
186219
}
187220
},
188221
"definitions": {

package-lock.json

Lines changed: 48 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
"react-html-parser": "^2.0.2",
8282
"react-router-dom": "6.30.1",
8383
"simple-git": "^3.28.0",
84+
"ssh2": "^1.16.0",
8485
"uuid": "^11.1.0",
8586
"validator": "^13.15.15",
8687
"yargs": "^17.7.2"

0 commit comments

Comments
 (0)