packagesshimport("context"// #nosec G501"crypto/md5""crypto/rand""crypto/rsa""crypto/x509""encoding/hex""encoding/pem""fmt""log/slog""net""os""path/filepath""sync""time""honeypot/internal/database""honeypot/internal/honeypot""honeypot/internal/logger""honeypot/internal/types""honeypot/internal/utils""golang.org/x/crypto/ssh")const(HoneypotType=types.HoneypotTypeSSHHoneypotLabel="SSH"DefaultHostKeyFile="ssh_key"DefaultConnectionTimeout=3*time.MinuteDefaultRSAKeySize=3072DefaultKeyFilePermissions=0600DefaultDirPermissions=0700DefaultPubKeyPermissions=0644ServerVersion="SSH-2.0-OpenSSH_9.6p1 Ubuntu-3ubuntu13.12"ShutdownTimeout=3*time.Second)// Config holds the configuration for the SSH honeypot.typeConfigstruct{ListenAddrstringPorts[]uint16HostKeyFilestring// Path to the host key file}// sshHoneypot implements the honeypot.Honeypot interface.typesshHoneypotstruct{configConfiglogger*slog.Logger}// New creates a new SSH honeypot instance.funcNew(cfgConfig)honeypot.Honeypot{// Register client_version field for top-N tracking// This is done during creation so fields are registered before log restorationlogger.RegisterTopNField("ssh","client_version")return&sshHoneypot{config:cfg,}}// Name returns the name of this honeypot.func(h*sshHoneypot)Name()types.HoneypotType{returnHoneypotType}// Label returns the label of this honeypot.func(h*sshHoneypot)Label()string{returnHoneypotLabel}// Start starts the SSH honeypot server.func(h*sshHoneypot)Start(ctxcontext.Context,l*slog.Logger)error{h.logger=lsshConfig,err:=h.createServerConfig()iferr!=nil{returnerr}varwgsync.WaitGroup// Start SSH servers on all configured portsfor_,port:=rangeh.config.Ports{ifport>0{wg.Add(1)gofunc(puint16){deferwg.Done()h.startSSHServer(ctx,p,sshConfig)}(port)}}wg.Wait()logger.LogInfo(h.logger,HoneypotType,"honeypot shutdown complete",nil)returnnil}// startSSHServer starts an SSH server on the specified port.func(h*sshHoneypot)startSSHServer(ctxcontext.Context,portuint16,sshConfig*ssh.ServerConfig){listenAddr:=utils.BuildAddress(h.config.ListenAddr,port)listener,err:=net.Listen("tcp",listenAddr)iferr!=nil{logger.LogError(h.logger,HoneypotType,"listen_failed",err,[]any{"addr",listenAddr,})return}deferlistener.Close()logger.LogInfo(h.logger,HoneypotType,"honeypot listening",[]any{"port",port,})h.setupGracefulShutdown(ctx,listener)h.acceptConnections(ctx,listener,sshConfig)logger.LogInfo(h.logger,HoneypotType,"honeypot shutdown complete",[]any{"port",port,})}// setupGracefulShutdown handles graceful shutdown on context cancellation.func(h*sshHoneypot)setupGracefulShutdown(ctxcontext.Context,listenernet.Listener){gofunc(){<-ctx.Done()iferr:=listener.Close();err!=nil{logger.LogError(h.logger,HoneypotType,"listener_close_error",err,nil)}}()}// acceptConnections accepts incoming connections and handles them.func(h*sshHoneypot)acceptConnections(ctxcontext.Context,listenernet.Listener,cfg*ssh.ServerConfig){for{conn,err:=listener.Accept()iferr!=nil{ifctx.Err()!=nil{break}logger.LogError(h.logger,HoneypotType,"accept_failed",err,nil)continue}goh.handleConn(ctx,conn,cfg)}}// createServerConfig creates and configures the SSH server config.func(h*sshHoneypot)createServerConfig()(*ssh.ServerConfig,error){cfg:=&ssh.ServerConfig{ServerVersion:ServerVersion,MaxAuthTries:0,PasswordCallback:h.passwordAuthCallback,PublicKeyCallback:h.publicKeyAuthCallback,}keyFile:=h.getHostKeyFile()hostKey,err:=h.loadOrGenerateHostKey(keyFile)iferr!=nil{logger.LogError(h.logger,HoneypotType,"load_or_generate_host_key_failed",err,[]any{"key_file",keyFile,})returnnil,err}cfg.AddHostKey(hostKey)returncfg,nil}// getHostKeyFile returns the host key file path, using default if not specified.func(h*sshHoneypot)getHostKeyFile()string{ifh.config.HostKeyFile!=""{returnh.config.HostKeyFile}returnDefaultHostKeyFile}// passwordAuthCallback handles password authentication attempts.func(h*sshHoneypot)passwordAuthCallback(cssh.ConnMetadata,pass[]byte)(*ssh.Permissions,error){h.logAuthAttempt(c,"password",map[string]interface{}{"username":c.User(),"password":string(pass),"client_version":string(c.ClientVersion()),})returnnil,fmt.Errorf("access denied")}// publicKeyAuthCallback handles public key authentication attempts.func(h*sshHoneypot)publicKeyAuthCallback(cssh.ConnMetadata,keyssh.PublicKey)(*ssh.Permissions,error){h.logAuthAttempt(c,"public-key",map[string]interface{}{"username":c.User(),"public_key_fingerprint":fingerprint(key),"client_version":string(c.ClientVersion()),})returnnil,fmt.Errorf("access denied")}// handleConn handles a new SSH connection.func(h*sshHoneypot)handleConn(ctxcontext.Context,connnet.Conn,cfg*ssh.ServerConfig){deferconn.Close()conn.SetDeadline(time.Now().Add(DefaultConnectionTimeout))sshConn,chans,reqs,err:=ssh.NewServerConn(conn,cfg)iferr!=nil{return}defersshConn.Close()h.handleSSHRequests(ctx,reqs)h.handleSSHChannels(ctx,chans)}// handleSSHRequests discards all SSH global requests.func(h*sshHoneypot)handleSSHRequests(ctxcontext.Context,reqs<-chan*ssh.Request){gossh.DiscardRequests(reqs)}// handleSSHChannels rejects all SSH channel requests.func(h*sshHoneypot)handleSSHChannels(ctxcontext.Context,chans<-chanssh.NewChannel){forch:=rangechans{ch.Reject(ssh.Prohibited,"access denied")}}// logAuthAttempt logs an authentication attempt with the provided fields.func(h*sshHoneypot)logAuthAttempt(cssh.ConnMetadata,authMethodstring,fieldsmap[string]interface{}){remoteHost,remotePort:=utils.SplitAddr(c.RemoteAddr().String(),h.logger)_,dstPort:=utils.SplitAddr(c.LocalAddr().String(),h.logger)iffields==nil{fields=make(map[string]interface{})}fields["auth_method"]=authMethodfields["supported_algorithms"]=supportedAlgorithms()logger.LogEvent(h.logger,types.LogEvent{Type:HoneypotType,Event:types.EventAuthAttempt,RemoteAddr:remoteHost,RemotePort:remotePort,DstPort:dstPort,Fields:fields,})}// loadOrGenerateHostKey loads an existing host key from the specified file,// or generates a new one and saves it if the file doesn't exist.func(h*sshHoneypot)loadOrGenerateHostKey(keyFilestring)(ssh.Signer,error){if_,err:=os.Stat(keyFile);err==nil{returnh.loadExistingHostKey(keyFile)}returnh.generateAndSaveHostKey(keyFile)}// loadExistingHostKey loads an existing host key from file.func(h*sshHoneypot)loadExistingHostKey(keyFilestring)(ssh.Signer,error){keyBytes,err:=os.ReadFile(keyFile)iferr!=nil{returnnil,fmt.Errorf("failed to read key file: %w",err)}signer,err:=ssh.ParsePrivateKey(keyBytes)iferr!=nil{returnnil,fmt.Errorf("failed to parse private key: %w",err)}logger.LogInfo(h.logger,HoneypotType,"loaded existing host key",[]any{"key_file",keyFile,})returnsigner,nil}// generateAndSaveHostKey generates a new RSA key pair and saves it to disk.func(h*sshHoneypot)generateAndSaveHostKey(keyFilestring)(ssh.Signer,error){key,err:=rsa.GenerateKey(rand.Reader,DefaultRSAKeySize)iferr!=nil{returnnil,fmt.Errorf("failed to generate key: %w",err)}iferr:=h.ensureKeyFileDirectory(keyFile);err!=nil{returnnil,err}iferr:=h.savePrivateKey(keyFile,key);err!=nil{returnnil,err}h.savePublicKey(keyFile,&key.PublicKey)signer,err:=ssh.NewSignerFromKey(key)iferr!=nil{returnnil,fmt.Errorf("failed to create signer: %w",err)}logger.LogInfo(h.logger,HoneypotType,"generated new host key",[]any{"key_file",keyFile,})returnsigner,nil}// ensureKeyFileDirectory creates the directory for the key file if needed.func(h*sshHoneypot)ensureKeyFileDirectory(keyFilestring)error{ifdir:=filepath.Dir(keyFile);dir!="."&&dir!=""{iferr:=os.MkdirAll(dir,DefaultDirPermissions);err!=nil{returnfmt.Errorf("failed to create directory for key file: %w",err)}}returnnil}// savePrivateKey saves the private key to disk in PEM format.func(h*sshHoneypot)savePrivateKey(keyFilestring,key*rsa.PrivateKey)error{privateKeyBytes:=x509.MarshalPKCS1PrivateKey(key)privateKeyPEM:=pem.EncodeToMemory(&pem.Block{Type:"RSA PRIVATE KEY",Bytes:privateKeyBytes,})iferr:=os.WriteFile(keyFile,privateKeyPEM,DefaultKeyFilePermissions);err!=nil{returnfmt.Errorf("failed to write key file: %w",err)}returnnil}// savePublicKey saves the public key to disk (optional, errors are logged but don't fail).func(h*sshHoneypot)savePublicKey(keyFilestring,pubKey*rsa.PublicKey){sshPubKey,err:=ssh.NewPublicKey(pubKey)iferr!=nil{return}pubKeyFile:=keyFile+".pub"pubKeyBytes:=ssh.MarshalAuthorizedKey(sshPubKey)iferr:=os.WriteFile(pubKeyFile,pubKeyBytes,DefaultPubKeyPermissions);err!=nil{// Log warning but don't fail - public key is optionallogger.LogError(h.logger,HoneypotType,"failed to save public key",err,[]any{"pub_key_file",pubKeyFile,})}}// fingerprint calculates the MD5 fingerprint of an SSH public key.funcfingerprint(kssh.PublicKey)string{// #nosec G401h:=md5.Sum(k.Marshal())returnhex.EncodeToString(h[:])}// supportedAlgorithms returns a string describing the supported SSH algorithms.funcsupportedAlgorithms()string{return"kex:curve25519-sha256; ciphers:chacha20-poly1305@openssh.com; macs:hmac-sha2-256"}func(h*sshHoneypot)GetScores(db*database.Database,intervalstring)honeypot.ScoreMap{// auth_attempt scores are handled in the honeypot.authAttemptScores functionreturnhoneypot.ScoreMap{}}func(h*sshHoneypot)Ports()[]uint16{returnh.config.Ports}