Skip to content

Commit 0d758b8

Browse files
authored
feat(provider): add ssh provider for protocol-level health checks (#151)
Adds a new SSH provider that performs protocol-level handshakes to capture host key fingerprints for security verification, enabling MITM detection and algorithm compliance checks using CEL expressions.
1 parent 8be743a commit 0d758b8

File tree

8 files changed

+791
-7
lines changed

8 files changed

+791
-7
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ Probes use a compile-time [provider plugin system](pkg/provider) that supports e
1414

1515
* [`system`](pkg/provider/system): Hierarchical grouping of related health checks with status aggregation
1616
* [`satellite`](pkg/provider/satellite): A separate satellite instance of the Platform Health server
17+
* [`ssh`](pkg/provider/ssh): SSH protocol handshake with host key verification
1718
* [`tcp`](pkg/provider/tcp): TCP connectivity checks
1819
* [`tls`](pkg/provider/tls): TLS handshake and certificate verification
1920
* [`http`](pkg/provider/http): HTTP(S) health checks with CEL-based response validation, full REST/GraphQL API support, and TLS details
@@ -256,6 +257,7 @@ Several providers support CEL (Common Expression Language) expressions for custo
256257

257258
- [`http`](pkg/provider/http): HTTP request and response details with JSON parsing for REST/GraphQL API validation
258259
- [`tls`](pkg/provider/tls): TLS connection and certificate details
260+
- [`ssh`](pkg/provider/ssh): SSH host key and connection details
259261
- [`kubernetes`](pkg/provider/kubernetes): Full resource(s), including metadata, spec, status, etc.
260262
- [`helm`](pkg/provider/helm): Release info, chart metadata, values and manifests
261263

cmd/ph/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
_ "github.com/isometry/platform-health/pkg/provider/http"
1414
_ "github.com/isometry/platform-health/pkg/provider/kubernetes"
1515
_ "github.com/isometry/platform-health/pkg/provider/satellite"
16+
_ "github.com/isometry/platform-health/pkg/provider/ssh"
1617
_ "github.com/isometry/platform-health/pkg/provider/system"
1718
_ "github.com/isometry/platform-health/pkg/provider/tcp"
1819
_ "github.com/isometry/platform-health/pkg/provider/tls"

cmd/phs/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
_ "github.com/isometry/platform-health/pkg/provider/http"
1414
_ "github.com/isometry/platform-health/pkg/provider/kubernetes"
1515
_ "github.com/isometry/platform-health/pkg/provider/satellite"
16+
_ "github.com/isometry/platform-health/pkg/provider/ssh"
1617
_ "github.com/isometry/platform-health/pkg/provider/system"
1718
_ "github.com/isometry/platform-health/pkg/provider/tcp"
1819
_ "github.com/isometry/platform-health/pkg/provider/tls"

go.mod

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ require (
1818
github.com/stretchr/testify v1.11.1
1919
github.com/veqryn/slog-context v0.9.0
2020
go.yaml.in/yaml/v3 v3.0.4
21+
golang.org/x/crypto v0.48.0
2122
golang.org/x/sync v0.19.0
2223
golang.org/x/term v0.40.0
2324
google.golang.org/grpc v1.79.1
@@ -135,7 +136,6 @@ require (
135136
github.com/xlab/treeprint v1.2.0 // indirect
136137
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
137138
go.yaml.in/yaml/v2 v2.4.3 // indirect
138-
golang.org/x/crypto v0.48.0 // indirect
139139
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect
140140
golang.org/x/mod v0.33.0 // indirect
141141
golang.org/x/net v0.50.0 // indirect
@@ -144,8 +144,8 @@ require (
144144
golang.org/x/text v0.34.0 // indirect
145145
golang.org/x/time v0.14.0 // indirect
146146
golang.org/x/tools v0.42.0 // indirect
147-
google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d // indirect
148-
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect
147+
google.golang.org/genproto/googleapis/api v0.0.0-20260223185530-2f722ef697dc // indirect
148+
google.golang.org/genproto/googleapis/rpc v0.0.0-20260223185530-2f722ef697dc // indirect
149149
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
150150
gopkg.in/inf.v0 v0.9.1 // indirect
151151
gopkg.in/yaml.v3 v3.0.1 // indirect

go.sum

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -430,10 +430,10 @@ golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
430430
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
431431
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
432432
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
433-
google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d h1:EocjzKLywydp5uZ5tJ79iP6Q0UjDnyiHkGRWxuPBP8s=
434-
google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:48U2I+QQUYhsFrg2SY6r+nJzeOtjey7j//WBESw+qyQ=
435-
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ=
436-
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
433+
google.golang.org/genproto/googleapis/api v0.0.0-20260223185530-2f722ef697dc h1:ULD+ToGXUIU6Pkzr1ARxdyvwfHbelw+agoFDRbLg4TU=
434+
google.golang.org/genproto/googleapis/api v0.0.0-20260223185530-2f722ef697dc/go.mod h1:M5krXqk4GhBKvB596udGL3UyjL4I1+cTbK0orROM9ng=
435+
google.golang.org/genproto/googleapis/rpc v0.0.0-20260223185530-2f722ef697dc h1:51Wupg8spF+5FC6D+iMKbOddFjMckETnNnEiZ+HX37s=
436+
google.golang.org/genproto/googleapis/rpc v0.0.0-20260223185530-2f722ef697dc/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
437437
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
438438
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
439439
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

pkg/provider/ssh/README.md

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
# SSH Provider
2+
3+
The SSH Provider extends the platform-health server to enable monitoring of SSH services. It performs protocol-level handshake(s) to capture host key fingerprints for security verification, without requiring authentication credentials. This enables MITM detection through host key fingerprint pinning and algorithm compliance checks using CEL (Common Expression Language) expressions.
4+
5+
When `algorithms` is specified, the provider polls each algorithm separately, collecting all fingerprints into a map for comprehensive verification.
6+
7+
## Usage
8+
9+
Once the SSH Provider is configured, any query to the platform health server will trigger validation of the configured SSH service(s). The server will attempt to establish SSH connection(s) to each instance, capture the host key(s) during the handshake, and report each instance as "healthy" if the handshake is successful and all CEL checks pass, or "unhealthy" if the connection fails or any check fails.
10+
11+
### Ad-hoc Check
12+
13+
```bash
14+
# Basic SSH check (server chooses algorithm)
15+
ph check ssh --host example.com --port 22
16+
17+
# Check specific algorithm
18+
ph check ssh --host example.com --algorithms ssh-ed25519
19+
20+
# Check multiple algorithms
21+
ph check ssh --host example.com --algorithms ssh-ed25519 --algorithms ecdsa-sha2-nistp256
22+
23+
# Check with CEL expression
24+
ph check ssh --host example.com --check='"ssh-ed25519" in ssh.hostKey'
25+
```
26+
27+
### Context Inspection
28+
29+
Use `ph context` to inspect the available CEL variables before writing expressions:
30+
31+
```bash
32+
# Default (server chooses algorithm)
33+
ph context ssh --host example.com
34+
35+
# Specific algorithm
36+
ph context ssh --host example.com --algorithms ssh-ed25519
37+
38+
# Multiple algorithms
39+
ph context ssh --host example.com --algorithms ssh-ed25519 --algorithms ecdsa-sha2-nistp256
40+
```
41+
42+
## Configuration
43+
44+
The SSH Provider is configured through the platform-health server's configuration file. Each instance is defined with its name as the YAML key under `components`.
45+
46+
- `type` (required): Must be `ssh`.
47+
- `spec`: Provider-specific configuration:
48+
- `host` (required): The hostname or IP address of the SSH service to monitor.
49+
- `port` (default: `22`): The port number of the SSH service to monitor.
50+
- `algorithms` (optional): List of host key algorithms to poll. If not specified, server chooses (single handshake). If specified, one handshake per algorithm.
51+
- `checks`: A list of CEL expressions to validate the SSH connection. Each check has:
52+
- `check` (required): A CEL expression that must evaluate to `true` for the connection to be healthy.
53+
- `message` (optional): Custom error message when the check fails.
54+
55+
### Valid Algorithm Names
56+
57+
- `ssh-ed25519`
58+
- `ecdsa-sha2-nistp256`
59+
- `ecdsa-sha2-nistp384`
60+
- `ecdsa-sha2-nistp521`
61+
- `ssh-rsa`
62+
- `rsa-sha2-256`
63+
- `rsa-sha2-512`
64+
65+
## CEL Check Context
66+
67+
The SSH provider exposes an `ssh` variable containing host key details:
68+
69+
- `ssh.hostKey`: Map of algorithm name to SHA256 fingerprint (map[string]string)
70+
- `ssh.host`: Target hostname used for connection (string)
71+
- `ssh.port`: Target port used for connection (int)
72+
73+
### Example Context Output
74+
75+
Default (server chooses algorithm):
76+
```json
77+
{
78+
"ssh": {
79+
"hostKey": {
80+
"ecdsa-sha2-nistp256": "SHA256:p2QAMXNIC1TJYWeIOttrVc98/R1BUFWu3/LiyKgUfQM"
81+
},
82+
"host": "github.com",
83+
"port": 22
84+
}
85+
}
86+
```
87+
88+
With multiple algorithms specified:
89+
```json
90+
{
91+
"ssh": {
92+
"hostKey": {
93+
"ssh-ed25519": "SHA256:+DiY3wvvV6TuJJhbpZisF/zLDA0zPMSvHdkr4UvCOqU",
94+
"ecdsa-sha2-nistp256": "SHA256:p2QAMXNIC1TJYWeIOttrVc98/R1BUFWu3/LiyKgUfQM"
95+
},
96+
"host": "github.com",
97+
"port": 22
98+
}
99+
}
100+
```
101+
102+
### Example CEL Expressions
103+
104+
```cel
105+
// Check if server supports specific algorithm
106+
"ssh-ed25519" in ssh.hostKey
107+
108+
// Verify known host key fingerprint for specific algorithm (MITM detection)
109+
ssh.hostKey["ssh-ed25519"] == "SHA256:+DiY3wvvV6TuJJhbpZisF/zLDA0zPMSvHdkr4UvCOqU"
110+
111+
// Check at least one modern algorithm is available
112+
"ssh-ed25519" in ssh.hostKey || "ecdsa-sha2-nistp256" in ssh.hostKey
113+
114+
// Ensure RSA is not the only option (when polling multiple)
115+
!("ssh-rsa" in ssh.hostKey) || size(ssh.hostKey) > 1
116+
```
117+
118+
## Examples
119+
120+
### Basic SSH Check
121+
122+
```yaml
123+
components:
124+
bastion:
125+
type: ssh
126+
spec:
127+
host: bastion.example.com
128+
port: 22
129+
```
130+
131+
In this example, the SSH Provider will establish an SSH connection to `bastion.example.com` on port 22 and report the service as "healthy" if the handshake completes successfully. The server chooses which host key algorithm to present.
132+
133+
### Host Key Verification (Single Algorithm)
134+
135+
```yaml
136+
components:
137+
production-ssh:
138+
type: ssh
139+
spec:
140+
host: prod.example.com
141+
port: 22
142+
algorithms:
143+
- ssh-ed25519
144+
checks:
145+
- check: 'ssh.hostKey["ssh-ed25519"] == "SHA256:uNiVztksCsDhcc0u9e8BujQXVUpKZIDTMczCvj3tD2s"'
146+
message: "Host key mismatch - possible MITM attack"
147+
```
148+
149+
This configuration verifies that the SSH server presents the expected ED25519 host key, providing protection against man-in-the-middle attacks.
150+
151+
### Multi-Algorithm Verification
152+
153+
```yaml
154+
components:
155+
secure-bastion:
156+
type: ssh
157+
spec:
158+
host: bastion.example.com
159+
algorithms:
160+
- ssh-ed25519
161+
- ecdsa-sha2-nistp256
162+
checks:
163+
- check: '"ssh-ed25519" in ssh.hostKey'
164+
message: "Server must support ED25519"
165+
- check: 'ssh.hostKey["ssh-ed25519"] == "SHA256:expected..."'
166+
message: "ED25519 key mismatch"
167+
- check: 'ssh.hostKey["ecdsa-sha2-nistp256"] == "SHA256:expected..."'
168+
message: "ECDSA key mismatch"
169+
```
170+
171+
This polls both ED25519 and ECDSA keys and verifies both fingerprints.
172+
173+
### Algorithm Compliance Check
174+
175+
```yaml
176+
components:
177+
modern-ssh:
178+
type: ssh
179+
spec:
180+
host: secure.example.com
181+
algorithms:
182+
- ssh-ed25519
183+
checks:
184+
- check: '"ssh-ed25519" in ssh.hostKey'
185+
message: "ED25519 host key required"
186+
```
187+
188+
This ensures the SSH server supports the modern ED25519 algorithm. The check will fail if the server doesn't support ED25519.

0 commit comments

Comments
 (0)