From 0bad7a428e8ba0bbde3d9657eb31e6eef1eca9fa Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Sun, 12 Jun 2022 00:30:20 -0700 Subject: gitlab-sshd: Add support for signed user certificates We add a `trusted_user_ca_keys` config setting that allows gitlab-sshd to trust any SSH certificate signed by the keys listed in this file. This is equivalent to the `TrustedUserCAKeys` OpenSSH setting. We assume the certificate identity is equivalent to the GitLab username. --- cmd/gitlab-shell/command/command.go | 14 +++++ internal/config/config.go | 1 + internal/sshd/server_config.go | 109 ++++++++++++++++++++++++++++++++---- internal/sshd/session.go | 3 + internal/sshd/sshd.go | 1 + 5 files changed, 117 insertions(+), 11 deletions(-) diff --git a/cmd/gitlab-shell/command/command.go b/cmd/gitlab-shell/command/command.go index b2a0266..260e517 100644 --- a/cmd/gitlab-shell/command/command.go +++ b/cmd/gitlab-shell/command/command.go @@ -58,6 +58,20 @@ func NewWithKrb5Principal(gitlabKrb5Principal string, env sshenv.Env, config *co return nil, disallowedcommand.Error } +func NewWithUsername(gitlabUsername string, env sshenv.Env, config *config.Config, readWriter *readwriter.ReadWriter) (command.Command, error) { + args, err := Parse(nil, env) + if err != nil { + return nil, err + } + + args.GitlabUsername = gitlabUsername + if cmd := Build(args, config, readWriter); cmd != nil { + return cmd, nil + } + + return nil, disallowedcommand.Error +} + func Parse(arguments []string, env sshenv.Env) (*commandargs.Shell, error) { args := &commandargs.Shell{Arguments: arguments, Env: env} diff --git a/internal/config/config.go b/internal/config/config.go index cfee3d0..cd4dc25 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -45,6 +45,7 @@ type ServerConfig struct { LivenessProbe string `yaml:"liveness_probe"` HostKeyFiles []string `yaml:"host_key_files,omitempty"` HostCertFiles []string `yaml:"host_cert_files,omitempty"` + TrustedUserCAKeys string `yaml:"trusted_user_ca_keys,omitempty"` MACs []string `yaml:"macs"` KexAlgorithms []string `yaml:"kex_algorithms"` Ciphers []string `yaml:"ciphers"` diff --git a/internal/sshd/server_config.go b/internal/sshd/server_config.go index 3c1fdbf..394a9c9 100644 --- a/internal/sshd/server_config.go +++ b/internal/sshd/server_config.go @@ -4,6 +4,7 @@ import ( "context" "encoding/base64" "fmt" + "io/ioutil" "os" "strconv" "time" @@ -40,6 +41,7 @@ type serverConfig struct { cfg *config.Config hostKeys []ssh.Signer hostKeyToCertMap map[string]*ssh.Certificate + trustedUserCAKeys map[string]ssh.PublicKey authorizedKeysClient *authorizedkeys.Client } @@ -110,6 +112,33 @@ func parseHostCerts(hostKeys []ssh.Signer, certFiles []string) map[string]*ssh.C return keyToCertMap } +func parseTrustedUserCAKeys(filename string) (map[string]ssh.PublicKey, error) { + keys := make(map[string]ssh.PublicKey) + + if filename == "" { + return keys, nil + } + + keysRaw, err := ioutil.ReadFile(filename) + if err != nil { + log.WithError(err).WithFields(log.Fields{"filename": filename}).Warn("failed to read trusted user keys") + return keys, err + } + + for len(keysRaw) > 0 { + publicKey, _, _, rest, err := ssh.ParseAuthorizedKey(keysRaw) + if err != nil { + log.WithError(err).WithFields(log.Fields{"filename": filename}).Warn("failed to parse trusted user keys") + return keys, err + } + + keys[string(publicKey.Marshal())] = publicKey + keysRaw = rest + } + + return keys, nil +} + func newServerConfig(cfg *config.Config) (*serverConfig, error) { authorizedKeysClient, err := authorizedkeys.NewClient(cfg) if err != nil { @@ -122,8 +151,19 @@ func newServerConfig(cfg *config.Config) (*serverConfig, error) { } hostKeyToCertMap := parseHostCerts(hostKeys, cfg.Server.HostCertFiles) + trustedUserCAKeys, err := parseTrustedUserCAKeys(cfg.Server.TrustedUserCAKeys) + if err != nil { + return nil, fmt.Errorf("failed to load trusted user keys") + } - return &serverConfig{cfg: cfg, authorizedKeysClient: authorizedKeysClient, hostKeys: hostKeys, hostKeyToCertMap: hostKeyToCertMap}, nil + return &serverConfig{ + cfg: cfg, + authorizedKeysClient: authorizedKeysClient, + hostKeys: hostKeys, + hostKeyToCertMap: hostKeyToCertMap, + trustedUserCAKeys: trustedUserCAKeys, + }, + nil } func (s *serverConfig) getAuthKey(ctx context.Context, user string, key ssh.PublicKey) (*authorizedkeys.Response, error) { @@ -145,6 +185,57 @@ func (s *serverConfig) getAuthKey(ctx context.Context, user string, key ssh.Publ return res, nil } +func (s *serverConfig) handleUserKey(ctx context.Context, user string, key ssh.PublicKey) (*ssh.Permissions, error) { + res, err := s.getAuthKey(ctx, user, key) + if err != nil { + return nil, err + } + + return &ssh.Permissions{ + // Record the public key used for authentication. + Extensions: map[string]string{ + "key-id": strconv.FormatInt(res.Id, 10), + }, + }, nil +} + +func (s *serverConfig) validUserCertificate(cert *ssh.Certificate) bool { + if cert.CertType != ssh.UserCert { + return false + } + + publicKey := s.trustedUserCAKeys[string(cert.SignatureKey.Marshal())] + if publicKey == nil { + return false + } + + return true +} + +func (s *serverConfig) handleUserCertificate(user string, cert *ssh.Certificate) (*ssh.Permissions, error) { + logger := log.WithFields(log.Fields{ + "ssh_user": user, + "certificate_identity": cert.KeyId, + "public_key_fingerprint": ssh.FingerprintSHA256(cert.Key), + "signing_ca_fingerprint": ssh.FingerprintSHA256(cert.SignatureKey), + }) + + if !s.validUserCertificate(cert) { + logger.Warn("user certificate not signed by trusted key") + return nil, fmt.Errorf("user certificate not signed by trusted key") + } + + logger.Info("user certificate is valid") + + // The gitlab-shell commands will make an internal API call to /discover + // to look up the username, so unlike the SSH key case we don't need to do it here. + return &ssh.Permissions{ + Extensions: map[string]string{ + "gitlab-username": cert.KeyId, + }, + }, nil +} + func (s *serverConfig) get(ctx context.Context) *ssh.ServerConfig { var gssapiWithMICConfig *ssh.GSSAPIWithMICConfig if s.cfg.Server.GSSAPI.Enabled { @@ -168,17 +259,13 @@ func (s *serverConfig) get(ctx context.Context) *ssh.ServerConfig { } sshCfg := &ssh.ServerConfig{ PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) { - res, err := s.getAuthKey(ctx, conn.User(), key) - if err != nil { - return nil, err - } + cert, ok := key.(*ssh.Certificate) - return &ssh.Permissions{ - // Record the public key used for authentication. - Extensions: map[string]string{ - "key-id": strconv.FormatInt(res.Id, 10), - }, - }, nil + if !ok { + return s.handleUserKey(ctx, conn.User(), key) + } else { + return s.handleUserCertificate(conn.User(), cert) + } }, GSSAPIWithMICConfig: gssapiWithMICConfig, ServerVersion: "SSH-2.0-GitLab-SSHD", diff --git a/internal/sshd/session.go b/internal/sshd/session.go index 3394b2a..3d5fbad 100644 --- a/internal/sshd/session.go +++ b/internal/sshd/session.go @@ -28,6 +28,7 @@ type session struct { channel ssh.Channel gitlabKeyId string gitlabKrb5Principal string + gitlabUsername string remoteAddr string // State managed by the session @@ -173,6 +174,8 @@ func (s *session) handleShell(ctx context.Context, req *ssh.Request) (uint32, er if s.gitlabKrb5Principal != "" { cmd, err = shellCmd.NewWithKrb5Principal(s.gitlabKrb5Principal, env, s.cfg, rw) + } else if s.gitlabUsername != "" { + cmd, err = shellCmd.NewWithUsername(s.gitlabUsername, env, s.cfg, rw) } else { cmd, err = shellCmd.NewWithKey(s.gitlabKeyId, env, s.cfg, rw) } diff --git a/internal/sshd/sshd.go b/internal/sshd/sshd.go index fbb5052..3be5cae 100644 --- a/internal/sshd/sshd.go +++ b/internal/sshd/sshd.go @@ -198,6 +198,7 @@ func (s *Server) handleConn(ctx context.Context, nconn net.Conn) { channel: channel, gitlabKeyId: sconn.Permissions.Extensions["key-id"], gitlabKrb5Principal: sconn.Permissions.Extensions["krb5principal"], + gitlabUsername: sconn.Permissions.Extensions["gitlab-username"], remoteAddr: remoteAddr, started: time.Now(), } -- cgit v1.2.1