As we reevaluate how to best support and maintain Staging Ref in the future, we encourage development teams using this environment to highlight their use cases in the following issue: https://gitlab.com/gitlab-com/gl-infra/software-delivery/framework/software-delivery-framework-issue-tracker/-/issues/36.

Skip to content
Snippets Groups Projects
Unverified Commit 0bad7a42 authored by Stan Hu's avatar Stan Hu
Browse files

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.
parent 1461d9ed
No related branches found
No related tags found
No related merge requests found
Loading
Loading
@@ -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}
Loading
Loading
Loading
Loading
@@ -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"`
Loading
Loading
Loading
Loading
@@ -4,6 +4,7 @@ import (
"context"
"encoding/base64"
"fmt"
"io/ioutil"
"os"
"strconv"
"time"
Loading
Loading
@@ -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
}
Loading
Loading
@@ -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 {
Loading
Loading
@@ -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) {
Loading
Loading
@@ -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 {
Loading
Loading
@@ -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",
Loading
Loading
Loading
Loading
@@ -28,6 +28,7 @@ type session struct {
channel ssh.Channel
gitlabKeyId string
gitlabKrb5Principal string
gitlabUsername string
remoteAddr string
// State managed by the session
Loading
Loading
@@ -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)
}
Loading
Loading
Loading
Loading
@@ -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(),
}
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment