From f31bab5f1a91e9a34695982b03342dd23247012c Mon Sep 17 00:00:00 2001 From: John Maguire Date: Tue, 30 Apr 2024 10:50:17 -0400 Subject: [PATCH] Add support for SSH CAs (#1098) - Accept certs signed by trusted CAs - Username must match the cert principal if set - Any username can be used if cert principal is empty - Don't allow removed pubkeys/CAs to be used after reload --- examples/config.yml | 5 ++- ssh.go | 13 ++++++++ sshd/server.go | 81 +++++++++++++++++++++++++++++++-------------- 3 files changed, 74 insertions(+), 25 deletions(-) diff --git a/examples/config.yml b/examples/config.yml index 9064c23..7886f0e 100644 --- a/examples/config.yml +++ b/examples/config.yml @@ -181,12 +181,15 @@ punchy: # A file containing the ssh host private key to use # A decent way to generate one: ssh-keygen -t ed25519 -f ssh_host_ed25519_key -N "" < /dev/null #host_key: ./ssh_host_ed25519_key - # A file containing a list of authorized public keys + # Authorized users and their public keys #authorized_users: #- user: steeeeve # keys can be an array of strings or single string #keys: #- "ssh public key string" + # Trusted SSH CA public keys. These are the public keys of the CAs that are allowed to sign SSH keys for access. + #trusted_cas: + #- "ssh public key string" # EXPERIMENTAL: relay support for networks that can't establish direct connections. relay: diff --git a/ssh.go b/ssh.go index 0f4f651..f096121 100644 --- a/ssh.go +++ b/ssh.go @@ -115,6 +115,19 @@ func configSSH(l *logrus.Logger, ssh *sshd.SSHServer, c *config.C) (func(), erro return nil, fmt.Errorf("error while adding sshd.host_key: %s", err) } + // Clear existing trusted CAs and authorized keys + ssh.ClearTrustedCAs() + ssh.ClearAuthorizedKeys() + + rawCAs := c.GetStringSlice("sshd.trusted_cas", []string{}) + for _, caAuthorizedKey := range rawCAs { + err := ssh.AddTrustedCA(caAuthorizedKey) + if err != nil { + l.WithError(err).WithField("sshCA", caAuthorizedKey).Warn("SSH CA had an error, ignoring") + continue + } + } + rawKeys := c.Get("sshd.authorized_users") keys, ok := rawKeys.([]interface{}) if ok { diff --git a/sshd/server.go b/sshd/server.go index 4a78fdf..9e8c721 100644 --- a/sshd/server.go +++ b/sshd/server.go @@ -1,6 +1,7 @@ package sshd import ( + "bytes" "errors" "fmt" "net" @@ -15,8 +16,11 @@ type SSHServer struct { config *ssh.ServerConfig l *logrus.Entry + certChecker *ssh.CertChecker + // Map of user -> authorized keys trustedKeys map[string]map[string]bool + trustedCAs []ssh.PublicKey // List of available commands helpCommand *Command @@ -31,6 +35,7 @@ type SSHServer struct { // NewSSHServer creates a new ssh server rigged with default commands and prepares to listen func NewSSHServer(l *logrus.Entry) (*SSHServer, error) { + s := &SSHServer{ trustedKeys: make(map[string]map[string]bool), l: l, @@ -38,8 +43,43 @@ func NewSSHServer(l *logrus.Entry) (*SSHServer, error) { conns: make(map[int]*session), } + cc := ssh.CertChecker{ + IsUserAuthority: func(auth ssh.PublicKey) bool { + for _, ca := range s.trustedCAs { + if bytes.Equal(ca.Marshal(), auth.Marshal()) { + return true + } + } + + return false + }, + UserKeyFallback: func(c ssh.ConnMetadata, pubKey ssh.PublicKey) (*ssh.Permissions, error) { + pk := string(pubKey.Marshal()) + fp := ssh.FingerprintSHA256(pubKey) + + tk, ok := s.trustedKeys[c.User()] + if !ok { + return nil, fmt.Errorf("unknown user %s", c.User()) + } + + _, ok = tk[pk] + if !ok { + return nil, fmt.Errorf("unknown public key for %s (%s)", c.User(), fp) + } + + return &ssh.Permissions{ + // Record the public key used for authentication. + Extensions: map[string]string{ + "fp": fp, + "user": c.User(), + }, + }, nil + + }, + } + s.config = &ssh.ServerConfig{ - PublicKeyCallback: s.matchPubKey, + PublicKeyCallback: cc.Authenticate, //TODO: AuthLogCallback: s.authAttempt, //TODO: version string ServerVersion: fmt.Sprintf("SSH-2.0-Nebula???"), @@ -66,10 +106,26 @@ func (s *SSHServer) SetHostKey(hostPrivateKey []byte) error { return nil } +func (s *SSHServer) ClearTrustedCAs() { + s.trustedCAs = []ssh.PublicKey{} +} + func (s *SSHServer) ClearAuthorizedKeys() { s.trustedKeys = make(map[string]map[string]bool) } +// AddTrustedCA adds a trusted CA for user certificates +func (s *SSHServer) AddTrustedCA(pubKey string) error { + pk, _, _, _, err := ssh.ParseAuthorizedKey([]byte(pubKey)) + if err != nil { + return err + } + + s.trustedCAs = append(s.trustedCAs, pk) + s.l.WithField("sshKey", pubKey).Info("Trusted CA key") + return nil +} + // AddAuthorizedKey adds an ssh public key for a user func (s *SSHServer) AddAuthorizedKey(user, pubKey string) error { pk, _, _, _, err := ssh.ParseAuthorizedKey([]byte(pubKey)) @@ -178,26 +234,3 @@ func (s *SSHServer) closeSessions() { } s.connsLock.Unlock() } - -func (s *SSHServer) matchPubKey(c ssh.ConnMetadata, pubKey ssh.PublicKey) (*ssh.Permissions, error) { - pk := string(pubKey.Marshal()) - fp := ssh.FingerprintSHA256(pubKey) - - tk, ok := s.trustedKeys[c.User()] - if !ok { - return nil, fmt.Errorf("unknown user %s", c.User()) - } - - _, ok = tk[pk] - if !ok { - return nil, fmt.Errorf("unknown public key for %s (%s)", c.User(), fp) - } - - return &ssh.Permissions{ - // Record the public key used for authentication. - Extensions: map[string]string{ - "fp": fp, - "user": c.User(), - }, - }, nil -}