148 lines
5.8 KiB
Go
148 lines
5.8 KiB
Go
// This file is part of the happyDomain (R) project.
|
|
// Copyright (c) 2020-2026 happyDomain
|
|
// Authors: Pierre-Olivier Mercier, et al.
|
|
//
|
|
// This program is offered under a commercial and under the AGPL license.
|
|
// For commercial licensing, contact us at <contact@happydomain.org>.
|
|
//
|
|
// For AGPL licensing:
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
// Package checker implements an SSH server security checker for
|
|
// happyDomain. It probes each SSH endpoint associated with an
|
|
// abstract.Server service and produces a structured report covering
|
|
// reachability, banner/version posture, algorithm negotiation
|
|
// (KEX/HostKey/Cipher/MAC/Compression), authentication method exposure
|
|
// and SSHFP host-key fingerprint validation.
|
|
package checker
|
|
|
|
import "time"
|
|
|
|
// ObservationKeySSH is the observation key this checker writes.
|
|
const ObservationKeySSH = "ssh"
|
|
|
|
// Option ids on CheckerOptions.
|
|
const (
|
|
OptionService = "service"
|
|
OptionDomainName = "domain_name"
|
|
OptionPorts = "ports"
|
|
OptionProbeTimeoutMs = "probeTimeoutMs"
|
|
OptionIncludeAuthProbe = "includeAuthProbe"
|
|
)
|
|
|
|
// Defaults.
|
|
const (
|
|
DefaultSSHPort = 22
|
|
DefaultProbeTimeoutMs = 10000
|
|
MaxConcurrentProbes = 16
|
|
)
|
|
|
|
// Severity levels used in Issue.Severity.
|
|
const (
|
|
SeverityCrit = "crit"
|
|
SeverityWarn = "warn"
|
|
SeverityInfo = "info"
|
|
SeverityOK = "ok"
|
|
)
|
|
|
|
// SSHData is the full collected payload written under ObservationKeySSH.
|
|
type SSHData struct {
|
|
Domain string `json:"domain,omitempty"`
|
|
Endpoints []SSHProbe `json:"endpoints"`
|
|
SSHFP SSHFPSummary `json:"sshfp"`
|
|
CollectedAt time.Time `json:"collected_at"`
|
|
}
|
|
|
|
// SSHFPSummary captures the SSHFP records declared for the service and
|
|
// whether a usable chain (DNSSEC) is available.
|
|
type SSHFPSummary struct {
|
|
Records []SSHFPRecord `json:"records,omitempty"`
|
|
// Present indicates whether the service carries at least one SSHFP RR.
|
|
Present bool `json:"present"`
|
|
}
|
|
|
|
// SSHFPRecord is a single SSHFP record as declared in the zone.
|
|
type SSHFPRecord struct {
|
|
Algorithm uint8 `json:"algorithm"` // 1=RSA, 2=DSA, 3=ECDSA, 4=Ed25519
|
|
Type uint8 `json:"type"` // 1=SHA-1, 2=SHA-256
|
|
Fingerprint string `json:"fingerprint"` // hex, lowercase
|
|
}
|
|
|
|
// SSHProbe is the outcome of probing a single SSH endpoint.
|
|
type SSHProbe struct {
|
|
Host string `json:"host"`
|
|
Port uint16 `json:"port"`
|
|
Address string `json:"address"`
|
|
IP string `json:"ip,omitempty"`
|
|
IsIPv6 bool `json:"ipv6,omitempty"`
|
|
TCPConnected bool `json:"tcp_connected"`
|
|
|
|
// Banner is the SSH protocol banner (e.g. "SSH-2.0-OpenSSH_9.3p1").
|
|
Banner string `json:"banner,omitempty"`
|
|
SoftVer string `json:"software_version,omitempty"`
|
|
ProtoVer string `json:"protocol_version,omitempty"`
|
|
Vendor string `json:"vendor,omitempty"`
|
|
ElapsedMS int64 `json:"elapsed_ms,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
|
|
// Algorithms negotiated by the server.
|
|
KEX []string `json:"kex_algorithms,omitempty"`
|
|
HostKey []string `json:"host_key_algorithms,omitempty"`
|
|
CiphersC2S []string `json:"ciphers_c2s,omitempty"`
|
|
CiphersS2C []string `json:"ciphers_s2c,omitempty"`
|
|
MACsC2S []string `json:"macs_c2s,omitempty"`
|
|
MACsS2C []string `json:"macs_s2c,omitempty"`
|
|
CompC2S []string `json:"compression_c2s,omitempty"`
|
|
CompS2C []string `json:"compression_s2c,omitempty"`
|
|
|
|
// Host keys observed during KEX. Multiple entries can appear if the
|
|
// server advertises several host-key types and we probe each in a
|
|
// second pass.
|
|
HostKeys []HostKeyInfo `json:"host_keys,omitempty"`
|
|
|
|
// Authentication methods advertised for a dummy "none" auth attempt.
|
|
AuthMethods []string `json:"auth_methods,omitempty"`
|
|
PasswordAuth bool `json:"password_auth,omitempty"`
|
|
KeyboardInteractive bool `json:"keyboard_interactive,omitempty"`
|
|
PublicKeyAuth bool `json:"public_key_auth,omitempty"`
|
|
AuthProbeAttempted bool `json:"auth_probe_attempted,omitempty"`
|
|
|
|
// Stage is the furthest probe stage the connection reached. One of
|
|
// "dial", "banner", "banner_write", "kexinit_read", "kexinit_parse",
|
|
// "kexinit_ok", "handshake_ok". Empty means the dial failed before
|
|
// even being attempted.
|
|
Stage string `json:"stage,omitempty"`
|
|
}
|
|
|
|
// HostKeyInfo captures an observed host key and its computed fingerprints.
|
|
type HostKeyInfo struct {
|
|
Type string `json:"type"` // e.g. "ssh-ed25519"
|
|
Bits int `json:"bits,omitempty"` // key size (bits)
|
|
SHA256 string `json:"sha256"` // hex fingerprint (lowercase, no colons)
|
|
SHA1 string `json:"sha1"` // hex fingerprint (lowercase, no colons)
|
|
SSHFPAlgo uint8 `json:"sshfp_algorithm"` // the SSHFP algorithm number matching this key type
|
|
SSHFPMatchSHA256 bool `json:"sshfp_match_sha256"`
|
|
SSHFPMatchSHA1 bool `json:"sshfp_match_sha1"`
|
|
}
|
|
|
|
// Issue is a single SSH finding surfaced to consumers.
|
|
type Issue struct {
|
|
Code string `json:"code"`
|
|
Severity string `json:"severity"`
|
|
Message string `json:"message,omitempty"`
|
|
Fix string `json:"fix,omitempty"`
|
|
// Endpoint is the "host:port" this issue applies to (empty for
|
|
// service-level issues such as missing SSHFP).
|
|
Endpoint string `json:"endpoint,omitempty"`
|
|
}
|