Skip to content

Commit

Permalink
Add support for SSH CAs (#1098)
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
johnmaguire authored Apr 30, 2024
1 parent 9cd944d commit f31bab5
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 25 deletions.
5 changes: 4 additions & 1 deletion examples/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
13 changes: 13 additions & 0 deletions ssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
81 changes: 57 additions & 24 deletions sshd/server.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package sshd

import (
"bytes"
"errors"
"fmt"
"net"
Expand All @@ -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
Expand All @@ -31,15 +35,51 @@ 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,
commands: radix.New(),
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???"),
Expand All @@ -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))
Expand Down Expand Up @@ -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
}

0 comments on commit f31bab5

Please sign in to comment.