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
This commit is contained in:
John Maguire 2024-04-30 10:50:17 -04:00 committed by GitHub
parent 9cd944d320
commit f31bab5f1a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 74 additions and 25 deletions

View File

@ -181,12 +181,15 @@ punchy:
# A file containing the ssh host private key to use # 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 # A decent way to generate one: ssh-keygen -t ed25519 -f ssh_host_ed25519_key -N "" < /dev/null
#host_key: ./ssh_host_ed25519_key #host_key: ./ssh_host_ed25519_key
# A file containing a list of authorized public keys # Authorized users and their public keys
#authorized_users: #authorized_users:
#- user: steeeeve #- user: steeeeve
# keys can be an array of strings or single string # keys can be an array of strings or single string
#keys: #keys:
#- "ssh public key string" #- "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. # EXPERIMENTAL: relay support for networks that can't establish direct connections.
relay: relay:

13
ssh.go
View File

@ -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) 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") rawKeys := c.Get("sshd.authorized_users")
keys, ok := rawKeys.([]interface{}) keys, ok := rawKeys.([]interface{})
if ok { if ok {

View File

@ -1,6 +1,7 @@
package sshd package sshd
import ( import (
"bytes"
"errors" "errors"
"fmt" "fmt"
"net" "net"
@ -15,8 +16,11 @@ type SSHServer struct {
config *ssh.ServerConfig config *ssh.ServerConfig
l *logrus.Entry l *logrus.Entry
certChecker *ssh.CertChecker
// Map of user -> authorized keys // Map of user -> authorized keys
trustedKeys map[string]map[string]bool trustedKeys map[string]map[string]bool
trustedCAs []ssh.PublicKey
// List of available commands // List of available commands
helpCommand *Command helpCommand *Command
@ -31,6 +35,7 @@ type SSHServer struct {
// NewSSHServer creates a new ssh server rigged with default commands and prepares to listen // NewSSHServer creates a new ssh server rigged with default commands and prepares to listen
func NewSSHServer(l *logrus.Entry) (*SSHServer, error) { func NewSSHServer(l *logrus.Entry) (*SSHServer, error) {
s := &SSHServer{ s := &SSHServer{
trustedKeys: make(map[string]map[string]bool), trustedKeys: make(map[string]map[string]bool),
l: l, l: l,
@ -38,8 +43,43 @@ func NewSSHServer(l *logrus.Entry) (*SSHServer, error) {
conns: make(map[int]*session), 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{ s.config = &ssh.ServerConfig{
PublicKeyCallback: s.matchPubKey, PublicKeyCallback: cc.Authenticate,
//TODO: AuthLogCallback: s.authAttempt, //TODO: AuthLogCallback: s.authAttempt,
//TODO: version string //TODO: version string
ServerVersion: fmt.Sprintf("SSH-2.0-Nebula???"), ServerVersion: fmt.Sprintf("SSH-2.0-Nebula???"),
@ -66,10 +106,26 @@ func (s *SSHServer) SetHostKey(hostPrivateKey []byte) error {
return nil return nil
} }
func (s *SSHServer) ClearTrustedCAs() {
s.trustedCAs = []ssh.PublicKey{}
}
func (s *SSHServer) ClearAuthorizedKeys() { func (s *SSHServer) ClearAuthorizedKeys() {
s.trustedKeys = make(map[string]map[string]bool) 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 // AddAuthorizedKey adds an ssh public key for a user
func (s *SSHServer) AddAuthorizedKey(user, pubKey string) error { func (s *SSHServer) AddAuthorizedKey(user, pubKey string) error {
pk, _, _, _, err := ssh.ParseAuthorizedKey([]byte(pubKey)) pk, _, _, _, err := ssh.ParseAuthorizedKey([]byte(pubKey))
@ -178,26 +234,3 @@ func (s *SSHServer) closeSessions() {
} }
s.connsLock.Unlock() 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
}