Transparent SSH jump host with auditing.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

192 lines
5.2 KiB

package stronghold
import (
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"hash/fnv"
"io"
"net"
"os"
"path/filepath"
"github.com/sirupsen/logrus"
"golang.org/x/crypto/ssh"
"git.maze.io/maze/stronghold/authority"
)
const (
DefaultAddr = ":22"
tag = "server"
maxAuthRetries = 16
version = "SSH-2.0-StrongHold"
)
// Server receives connections from SSH clients.
type Server struct {
// KeyboardInteractiveHandler is a callback for doing keyboard-interactive authentication.
KeyboardInteractiveHandler func(conn ssh.ConnMetadata, client ssh.KeyboardInteractiveChallenge) (*ssh.Permissions, error)
// PasswordHandler is a callback for doing password authentication.
PasswordHandler func(conn ssh.ConnMetadata, password []byte) (*ssh.Permissions, error)
// PublicKeyHandler is a callback for doing public key authentication.
PublicKeyHandler func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error)
// Issuer is a callback for proxied SSH connections.
CertificateAuthority *authority.CertificateAuthority
// SessionHandler is a callback for SSH shell sessions.
SessionHandler func(conn ssh.ConnMetadata, channel ssh.Channel, pty *PTY, windowChanges <-chan *WindowChange) error
recordDir string
config *ssh.ServerConfig
closed chan struct{}
}
// New server
func New(recordDir string, hostKeyFiles ...string) (*Server, error) {
if len(hostKeyFiles) == 0 {
return nil, errors.New("server: at least one key file must be specified")
}
if !filepath.IsAbs(recordDir) {
var err error
if recordDir, err = filepath.Abs(recordDir); err != nil {
return nil, err
}
}
if err := os.MkdirAll(recordDir, 0700); err != nil && !os.IsExist(err) {
return nil, err
}
server := &Server{
config: &ssh.ServerConfig{
NoClientAuth: true,
MaxAuthTries: maxAuthRetries,
ServerVersion: version,
},
closed: make(chan struct{}),
}
for _, keyFile := range hostKeyFiles {
key, err := loadPrivateKeyFile(keyFile)
if err != nil {
return nil, fmt.Errorf("server: error loading private key from %s: %w", keyFile, err)
}
server.config.AddHostKey(key)
}
return server, nil
}
// ListenAndServe starts the SSH server on the given address.
func (server *Server) ListenAndServe(addr string) error {
l, err := net.Listen("tcp", addr)
if err != nil {
return err
}
if server.KeyboardInteractiveHandler != nil {
server.config.NoClientAuth = false
server.config.KeyboardInteractiveCallback = server.KeyboardInteractiveHandler
}
if server.PasswordHandler != nil {
server.config.NoClientAuth = false
server.config.PasswordCallback = server.PasswordHandler
}
if server.PublicKeyHandler != nil {
server.config.NoClientAuth = false
server.config.PublicKeyCallback = server.PublicKeyHandler
}
for {
var conn net.Conn
if conn, err = l.Accept(); err != nil {
logrus.WithError(err).Error("error accepting client")
continue
}
go server.handleConn(conn)
}
}
func (server *Server) handleConn(netConn net.Conn) {
defer netConn.Close()
log := logrus.WithFields(logrus.Fields{
"tag": tag,
"src": netConn.RemoteAddr().String(),
})
log.Info("new client connection")
defer log.Info("client connection closed")
sshConn, newChannels, requests, err := ssh.NewServerConn(netConn, server.config)
if err != nil {
log.WithError(err).Error("handshakeAsServer failed")
return
}
client := newClient(netConn, sshConn, server.recordDir)
client.CertificateAuthority = server.CertificateAuthority
client.SessionHandler = server.SessionHandler
go client.handleRequests(requests)
if err = client.handleNewChannels(newChannels); err != nil {
if err != io.EOF {
log.WithError(err).Error("fatal error")
}
}
<-client.closed
}
func fingerprint(key ssh.PublicKey) string {
h := sha256.New()
_, _ = h.Write(key.Marshal())
return "SHA256:" + base64.RawStdEncoding.EncodeToString(h.Sum(nil))
}
func sessionID(metadata ssh.ConnMetadata) string {
h := fnv.New64()
_, _ = h.Write(metadata.SessionID())
return hex.EncodeToString(h.Sum(nil))
}
// PermitAllPasswords is a password handler that accepts all credentials.
func PermitAllPasswords(metadata ssh.ConnMetadata, _ []byte) (*ssh.Permissions, error) {
log := logrus.WithFields(logrus.Fields{
"session": sessionID(metadata),
"src": metadata.RemoteAddr().String(),
"user": metadata.User(),
})
log.Info("accepting password")
return new(ssh.Permissions), nil
}
// PermitAllPublicKeys is a public key handler that accepts all public keys.
func PermitAllPublicKeys(metadata ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
log := logrus.WithFields(logrus.Fields{
"session": sessionID(metadata),
"src": metadata.RemoteAddr().String(),
"user": metadata.User(),
})
log.WithFields(logrus.Fields{
"fingerprint": fingerprint(key),
"key_type": key.Type(),
}).Info("accepting public key")
return new(ssh.Permissions), nil
}
// PermitAllHostKeys logs and accepts all server keys.
func PermitAllHostKeys(hostname string, remote net.Addr, key ssh.PublicKey) error {
log := logrus.WithFields(logrus.Fields{
"dst": remote.String(),
"hostname": hostname,
"fingerprint": fingerprint(key),
"key_type": key.Type(),
})
log.Info("accepting host key")
return nil
}