Browse Source

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
John Maguire 1 year ago
parent
commit
f31bab5f1a
3 changed files with 74 additions and 25 deletions
  1. 4 1
      examples/config.yml
  2. 13 0
      ssh.go
  3. 57 24
      sshd/server.go

+ 4 - 1
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:

+ 13 - 0
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 {

+ 57 - 24
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
-}