feat(ssh): Support authless SSH health check (#956)
* Feature + Test + Documentation: added no-auth ssh health cheack feature, changed documentation to fit new behavior, added ssh test cases. * Refactor: refactored authenticate field to infer from username and password insted of specifying it inside config. * Refactor: removed non used field. * Refactor: changed error, removed spaces. * Refactor: added comments.
This commit is contained in:
parent
0bba77ab2b
commit
fa3e5dcc6e
17
README.md
17
README.md
@ -2058,6 +2058,23 @@ endpoints:
|
|||||||
- "[STATUS] == 0"
|
- "[STATUS] == 0"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
you can also use no authentication to monitor the endpoint by not specifying the username
|
||||||
|
and password fields.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
endpoints:
|
||||||
|
- name: ssh-example
|
||||||
|
url: "ssh://example.com:22" # port is optional. Default is 22.
|
||||||
|
ssh:
|
||||||
|
username: ""
|
||||||
|
password: ""
|
||||||
|
|
||||||
|
interval: 1m
|
||||||
|
conditions:
|
||||||
|
- "[CONNECTED] == true"
|
||||||
|
- "[STATUS] == 0"
|
||||||
|
```
|
||||||
|
|
||||||
The following placeholders are supported for endpoints of type SSH:
|
The following placeholders are supported for endpoints of type SSH:
|
||||||
- `[CONNECTED]` resolves to `true` if the SSH connection was successful, `false` otherwise
|
- `[CONNECTED]` resolves to `true` if the SSH connection was successful, `false` otherwise
|
||||||
- `[STATUS]` resolves the exit code of the command executed on the remote server (e.g. `0` for success)
|
- `[STATUS]` resolves the exit code of the command executed on the remote server (e.g. `0` for success)
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/smtp"
|
"net/smtp"
|
||||||
@ -197,6 +198,34 @@ func CanCreateSSHConnection(address, username, password string, config *Config)
|
|||||||
return true, cli, nil
|
return true, cli, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func CheckSSHBanner(address string, cfg *Config) (bool, int, error) {
|
||||||
|
var port string
|
||||||
|
if strings.Contains(address, ":") {
|
||||||
|
addressAndPort := strings.Split(address, ":")
|
||||||
|
if len(addressAndPort) != 2 {
|
||||||
|
return false, 1, errors.New("invalid address for ssh, format must be ssh://host:port")
|
||||||
|
}
|
||||||
|
address = addressAndPort[0]
|
||||||
|
port = addressAndPort[1]
|
||||||
|
} else {
|
||||||
|
port = "22"
|
||||||
|
}
|
||||||
|
dialer := net.Dialer{}
|
||||||
|
connStr := net.JoinHostPort(address, port)
|
||||||
|
conn, err := dialer.Dial("tcp", connStr)
|
||||||
|
if err != nil {
|
||||||
|
return false, 1, err
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
conn.SetReadDeadline(time.Now().Add(time.Second))
|
||||||
|
buf := make([]byte, 256)
|
||||||
|
_, err = io.ReadAtLeast(conn, buf, 1)
|
||||||
|
if err != nil {
|
||||||
|
return false, 1, err
|
||||||
|
}
|
||||||
|
return true, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
// ExecuteSSHCommand executes a command to an address using the SSH protocol.
|
// ExecuteSSHCommand executes a command to an address using the SSH protocol.
|
||||||
func ExecuteSSHCommand(sshClient *ssh.Client, body string, config *Config) (bool, int, error) {
|
func ExecuteSSHCommand(sshClient *ssh.Client, body string, config *Config) (bool, int, error) {
|
||||||
type Body struct {
|
type Body struct {
|
||||||
|
@ -474,3 +474,38 @@ func TestQueryDNS(t *testing.T) {
|
|||||||
time.Sleep(10 * time.Millisecond)
|
time.Sleep(10 * time.Millisecond)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCheckSSHBanner(t *testing.T) {
|
||||||
|
cfg := &Config{Timeout: 3}
|
||||||
|
|
||||||
|
t.Run("no-auth-ssh", func(t *testing.T) {
|
||||||
|
connected, status, err := CheckSSHBanner("tty.sdf.org", cfg)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Expected: error != nil, got: %v ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if connected == false {
|
||||||
|
t.Errorf("Expected: connected == true, got: %v", connected)
|
||||||
|
}
|
||||||
|
if status != 0 {
|
||||||
|
t.Errorf("Expected: 0, got: %v", status)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid-address", func(t *testing.T) {
|
||||||
|
connected, status, err := CheckSSHBanner("idontplaytheodds.com", cfg)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Expected: error, got: %v ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if connected != false {
|
||||||
|
t.Errorf("Expected: connected == false, got: %v", connected)
|
||||||
|
}
|
||||||
|
if status != 1 {
|
||||||
|
t.Errorf("Expected: 1, got: %v", status)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
@ -363,6 +363,18 @@ func (e *Endpoint) call(result *Result) {
|
|||||||
}
|
}
|
||||||
result.Duration = time.Since(startTime)
|
result.Duration = time.Since(startTime)
|
||||||
} else if endpointType == TypeSSH {
|
} else if endpointType == TypeSSH {
|
||||||
|
// If there's no username/password specified, attempt to validate just the SSH banner
|
||||||
|
if len(e.SSHConfig.Username) == 0 && len(e.SSHConfig.Password) == 0 {
|
||||||
|
result.Connected, result.HTTPStatus, err =
|
||||||
|
client.CheckSSHBanner(strings.TrimPrefix(e.URL, "ssh://"), e.ClientConfig)
|
||||||
|
if err != nil {
|
||||||
|
result.AddError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
result.Success = result.Connected
|
||||||
|
result.Duration = time.Since(startTime)
|
||||||
|
return
|
||||||
|
}
|
||||||
var cli *ssh.Client
|
var cli *ssh.Client
|
||||||
result.Connected, cli, err = client.CanCreateSSHConnection(strings.TrimPrefix(e.URL, "ssh://"), e.SSHConfig.Username, e.SSHConfig.Password, e.ClientConfig)
|
result.Connected, cli, err = client.CanCreateSSHConnection(strings.TrimPrefix(e.URL, "ssh://"), e.SSHConfig.Username, e.SSHConfig.Password, e.ClientConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -19,6 +19,10 @@ type Config struct {
|
|||||||
|
|
||||||
// Validate the SSH configuration
|
// Validate the SSH configuration
|
||||||
func (cfg *Config) Validate() error {
|
func (cfg *Config) Validate() error {
|
||||||
|
// If there's no username and password, this endpoint can still check the SSH banner, so the endpoint is still valid
|
||||||
|
if len(cfg.Username) == 0 && len(cfg.Password) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
if len(cfg.Username) == 0 {
|
if len(cfg.Username) == 0 {
|
||||||
return ErrEndpointWithoutSSHUsername
|
return ErrEndpointWithoutSSHUsername
|
||||||
}
|
}
|
||||||
|
@ -7,10 +7,8 @@ import (
|
|||||||
|
|
||||||
func TestSSH_validate(t *testing.T) {
|
func TestSSH_validate(t *testing.T) {
|
||||||
cfg := &Config{}
|
cfg := &Config{}
|
||||||
if err := cfg.Validate(); err == nil {
|
if err := cfg.Validate(); err != nil {
|
||||||
t.Error("expected an error")
|
t.Error("didn't expect an error")
|
||||||
} else if !errors.Is(err, ErrEndpointWithoutSSHUsername) {
|
|
||||||
t.Errorf("expected error to be '%v', got '%v'", ErrEndpointWithoutSSHUsername, err)
|
|
||||||
}
|
}
|
||||||
cfg.Username = "username"
|
cfg.Username = "username"
|
||||||
if err := cfg.Validate(); err == nil {
|
if err := cfg.Validate(); err == nil {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user