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