checker-ssh/checker/types.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"`
}