summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorStan Hu <stanhu@gmail.com>2022-06-12 00:30:20 -0700
committerStan Hu <stanhu@gmail.com>2023-03-08 10:19:38 -0800
commit0bad7a428e8ba0bbde3d9657eb31e6eef1eca9fa (patch)
tree0b0bc29324f382ce540ae9c0a2e3522e0ef665af
parent1461d9ed1283f6dda015e3c26189b70c95d022c2 (diff)
downloadgitlab-shell-sh-ssh-certificates.tar.gz
gitlab-sshd: Add support for signed user certificatessh-ssh-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.
-rw-r--r--cmd/gitlab-shell/command/command.go14
-rw-r--r--internal/config/config.go1
-rw-r--r--internal/sshd/server_config.go109
-rw-r--r--internal/sshd/session.go3
-rw-r--r--internal/sshd/sshd.go1
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(),
}