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
Commit 51ea0f50 authored by Marin Hannache's avatar Marin Hannache Committed by Igor Drozdov
Browse files

Add support for the gssapi-with-mic auth method

parent 977a7e80
No related branches found
No related tags found
No related merge requests found
Showing
with 359 additions and 39 deletions
Loading
Loading
@@ -44,6 +44,20 @@ func NewWithKey(gitlabKeyId string, env sshenv.Env, config *config.Config, readW
return nil, disallowedcommand.Error
}
func NewWithKrb5Principal(gitlabKrb5Principal 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.GitlabKrb5Principal = gitlabKrb5Principal
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
@@ -71,6 +71,8 @@ func main() {
cfg.GitalyClient.InitSidechannelRegistry(ctx)
sshd.LoadGSSAPILib(&cfg.Server.GSSAPI)
server, err := sshd.NewServer(cfg)
if err != nil {
log.WithError(err).Fatal("Failed to start GitLab built-in sshd")
Loading
Loading
Loading
Loading
@@ -107,3 +107,11 @@ sshd:
- /run/secrets/ssh-hostkeys/ssh_host_rsa_key-cert.pub
- /run/secrets/ssh-hostkeys/ssh_host_ecdsa_key-cert.pub
- /run/secrets/ssh-hostkeys/ssh_host_ed25519_key-cert.pub
# GSSAPI-related settings
gssapi:
# Enable the gssapi-with-mic authentication method. Defaults to false.
enabled: false
# Keytab path. Defaults to "", system default (usually /etc/krb5.keytab).
keytab: ""
# The Kerberos service name to be used by sshd. Defaults to "", accepts any service name in keytab file.
service_principal_name: ""
Loading
Loading
@@ -8,6 +8,7 @@ require (
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0
github.com/mattn/go-shellwords v1.0.11
github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a
github.com/openshift/gssapi v0.0.0-20161010215902-5fb4217df13b
github.com/otiai10/copy v1.4.2
github.com/pires/go-proxyproto v0.6.2
github.com/prometheus/client_golang v1.13.1
Loading
Loading
Loading
Loading
@@ -26,12 +26,13 @@ var (
)
type Shell struct {
Arguments []string
GitlabUsername string
GitlabKeyId string
SshArgs []string
CommandType CommandType
Env sshenv.Env
Arguments []string
GitlabUsername string
GitlabKeyId string
GitlabKrb5Principal string
SshArgs []string
CommandType CommandType
Env sshenv.Env
}
func (s *Shell) Parse() error {
Loading
Loading
Loading
Loading
@@ -23,6 +23,13 @@ const (
type YamlDuration time.Duration
type GSSAPIConfig struct {
Enabled bool `yaml:"enabled,omitempty"`
Keytab string `yaml:"keytab,omitempty"`
ServicePrincipalName string `yaml:"service_principal_name,omitempty"`
LibPath string
}
type ServerConfig struct {
Listen string `yaml:"listen,omitempty"`
ProxyProtocol bool `yaml:"proxy_protocol,omitempty"`
Loading
Loading
@@ -41,6 +48,7 @@ type ServerConfig struct {
MACs []string `yaml:"macs"`
KexAlgorithms []string `yaml:"kex_algorithms"`
Ciphers []string `yaml:"ciphers"`
GSSAPI GSSAPIConfig `yaml:"gssapi,omitempty"`
}
type HttpSettingsConfig struct {
Loading
Loading
Loading
Loading
@@ -22,13 +22,14 @@ type Client struct {
}
type Request struct {
Action commandargs.CommandType `json:"action"`
Repo string `json:"project"`
Changes string `json:"changes"`
Protocol string `json:"protocol"`
KeyId string `json:"key_id,omitempty"`
Username string `json:"username,omitempty"`
CheckIp string `json:"check_ip,omitempty"`
Action commandargs.CommandType `json:"action"`
Repo string `json:"project"`
Changes string `json:"changes"`
Protocol string `json:"protocol"`
KeyId string `json:"key_id,omitempty"`
Username string `json:"username,omitempty"`
Krb5Principal string `json:"krb5principal,omitempty"`
CheckIp string `json:"check_ip,omitempty"`
}
type Gitaly struct {
Loading
Loading
@@ -81,6 +82,8 @@ func (c *Client) Verify(ctx context.Context, args *commandargs.Shell, action com
if args.GitlabUsername != "" {
request.Username = args.GitlabUsername
} else if args.GitlabKrb5Principal != "" {
request.Krb5Principal = args.GitlabKrb5Principal
} else {
request.KeyId = args.GitlabKeyId
}
Loading
Loading
Loading
Loading
@@ -56,7 +56,7 @@ func buildExpectedResponse(who string) *Response {
func TestSuccessfulResponses(t *testing.T) {
okResponse := testResponse{body: responseBody(t, "allowed.json"), status: http.StatusOK}
client := setup(t,
map[string]testResponse{"first": okResponse},
map[string]testResponse{"first": okResponse, "test@TEST.TEST": okResponse},
map[string]testResponse{"1": okResponse},
)
Loading
Loading
@@ -73,6 +73,10 @@ func TestSuccessfulResponses(t *testing.T) {
desc: "Provide username within the request",
args: &commandargs.Shell{GitlabUsername: "first"},
who: "user-1",
}, {
desc: "Provide krb5principal within the request",
args: &commandargs.Shell{GitlabKrb5Principal: "test@TEST.TEST"},
who: "user-1",
},
}
Loading
Loading
@@ -255,6 +259,10 @@ func setup(t *testing.T, userResponses, keyResponses map[string]testResponse) *C
w.WriteHeader(tr.status)
_, err := w.Write(tr.body)
require.NoError(t, err)
} else if tr, ok := userResponses[requestBody.Krb5Principal]; ok {
w.WriteHeader(tr.status)
_, err := w.Write(tr.body)
require.NoError(t, err)
} else if tr, ok := keyResponses[requestBody.KeyId]; ok {
w.WriteHeader(tr.status)
_, err := w.Write(tr.body)
Loading
Loading
Loading
Loading
@@ -38,6 +38,8 @@ func (c *Client) GetByCommandArgs(ctx context.Context, args *commandargs.Shell)
params.Add("username", args.GitlabUsername)
} else if args.GitlabKeyId != "" {
params.Add("key_id", args.GitlabKeyId)
} else if args.GitlabKrb5Principal != "" {
params.Add("krb5principal", args.GitlabKrb5Principal)
} else {
// There was no 'who' information, this matches the ruby error
// message.
Loading
Loading
Loading
Loading
@@ -38,6 +38,13 @@ func init() {
Name: "Jane Doe",
}
json.NewEncoder(w).Encode(body)
} else if r.URL.Query().Get("krb5principal") == "john-doe@TEST.TEST" {
body := &Response{
UserId: 3,
Username: "john-doe",
Name: "John Doe",
}
json.NewEncoder(w).Encode(body)
} else if r.URL.Query().Get("username") == "broken_message" {
w.WriteHeader(http.StatusForbidden)
body := &client.ErrorResponse{
Loading
Loading
@@ -76,6 +83,16 @@ func TestGetByUsername(t *testing.T) {
require.Equal(t, &Response{UserId: 1, Username: "jane-doe", Name: "Jane Doe"}, result)
}
func TestGetByKrb5Principal(t *testing.T) {
client := setup(t)
params := url.Values{}
params.Add("krb5principal", "john-doe@TEST.TEST")
result, err := client.getResponse(context.Background(), params)
require.NoError(t, err)
require.Equal(t, &Response{UserId: 3, Username: "john-doe", Name: "John Doe"}, result)
}
func TestMissingUser(t *testing.T) {
client := setup(t)
Loading
Loading
package sshd
import (
"fmt"
"github.com/openshift/gssapi"
"gitlab.com/gitlab-org/gitlab-shell/v14/internal/config"
"gitlab.com/gitlab-org/labkit/log"
)
var lib *gssapi.Lib
func LoadGSSAPILib(config *config.GSSAPIConfig) error {
var err error
if config.Enabled {
options := &gssapi.Options{
Krb5Ktname: config.Keytab,
}
if config.LibPath != "" {
options.LibPath = config.LibPath
}
lib, err = gssapi.Load(options)
if err != nil {
log.WithError(err).Error("Unable to load GSSAPI library, gssapi-with-mic is disabled")
config.Enabled = false
}
}
return err
}
type OSGSSAPIServer struct {
Keytab string
ServicePrincipalName string
contextId *gssapi.CtxId
}
func (_ *OSGSSAPIServer) str2name(str string) (*gssapi.Name, error) {
strBuffer, err := lib.MakeBufferString(str)
if err != nil {
return nil, err
}
defer strBuffer.Release()
return strBuffer.Name(lib.GSS_C_NO_OID)
}
func (server *OSGSSAPIServer) AcceptSecContext(
token []byte,
) (
outputToken []byte,
srcName string,
needContinue bool,
err error,
) {
tokenBuffer, err := lib.MakeBufferBytes(token)
if err != nil {
return
}
defer tokenBuffer.Release()
var spn *gssapi.CredId = lib.GSS_C_NO_CREDENTIAL
if server.ServicePrincipalName != "" {
var name *gssapi.Name
name, err = server.str2name(server.ServicePrincipalName)
if err != nil {
return
}
defer name.Release()
var actualMech *gssapi.OIDSet
spn, actualMech, _, err = lib.AcquireCred(name, 0, lib.GSS_C_NO_OID_SET, gssapi.GSS_C_ACCEPT)
if err != nil {
return
}
defer spn.Release()
defer actualMech.Release()
}
ctxOut, srcNameName, _, outputTokenBuffer, _, _, _, err := lib.AcceptSecContext(
server.contextId,
spn,
tokenBuffer,
nil,
)
if err == gssapi.ErrContinueNeeded {
needContinue = true
err = nil
} else if err != nil {
return
}
defer outputTokenBuffer.Release()
defer srcNameName.Release()
outputToken = outputTokenBuffer.Bytes()
server.contextId = ctxOut
return outputToken, srcNameName.String(), needContinue, err
}
func (server *OSGSSAPIServer) VerifyMIC(
micField []byte,
micToken []byte,
) error {
if server.contextId == nil {
return fmt.Errorf("gssapi: uninitialized contextId")
}
micFieldBuffer, err := lib.MakeBufferBytes(micField)
if err != nil {
return err
}
defer micFieldBuffer.Release()
micTokenBuffer, err := lib.MakeBufferBytes(micToken)
if err != nil {
return err
}
defer micTokenBuffer.Release()
_, err = server.contextId.VerifyMIC(micFieldBuffer, micTokenBuffer)
return err
}
func (server *OSGSSAPIServer) DeleteSecContext() error {
if server.contextId == nil {
return nil
}
err := server.contextId.DeleteSecContext()
if err == nil {
server.contextId = nil
}
return err
}
package sshd
import (
"testing"
"github.com/stretchr/testify/require"
"gitlab.com/gitlab-org/gitlab-shell/v14/internal/config"
)
func TestLoadGSSAPILibSucces(t *testing.T) {
config := &config.GSSAPIConfig{Enabled: true}
err := LoadGSSAPILib(config)
require.NotNil(t, lib)
require.Nil(t, err)
require.True(t, config.Enabled)
}
func TestLoadGSSAPILibFailure(t *testing.T) {
config := &config.GSSAPIConfig{Enabled: true, LibPath: "/invalid"}
err := LoadGSSAPILib(config)
require.Nil(t, lib)
require.NotNil(t, err)
require.False(t, config.Enabled)
}
Loading
Loading
@@ -146,6 +146,26 @@ func (s *serverConfig) getAuthKey(ctx context.Context, user string, key ssh.Publ
}
func (s *serverConfig) get(ctx context.Context) *ssh.ServerConfig {
var gssapiWithMICConfig *ssh.GSSAPIWithMICConfig
if s.cfg.Server.GSSAPI.Enabled {
gssapiWithMICConfig = &ssh.GSSAPIWithMICConfig{
AllowLogin: func(conn ssh.ConnMetadata, srcName string) (*ssh.Permissions, error) {
if conn.User() != s.cfg.User {
return nil, fmt.Errorf("unknown user")
}
return &ssh.Permissions{
// Record the Kerberos principal used for authentication.
Extensions: map[string]string{
"krb5principal": srcName,
},
}, nil
},
Server: &OSGSSAPIServer{
ServicePrincipalName: s.cfg.Server.GSSAPI.ServicePrincipalName,
},
}
}
sshCfg := &ssh.ServerConfig{
PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
res, err := s.getAuthKey(ctx, conn.User(), key)
Loading
Loading
@@ -160,7 +180,8 @@ func (s *serverConfig) get(ctx context.Context) *ssh.ServerConfig {
},
}, nil
},
ServerVersion: "SSH-2.0-GitLab-SSHD",
GSSAPIWithMICConfig: gssapiWithMICConfig,
ServerVersion: "SSH-2.0-GitLab-SSHD",
}
if len(s.cfg.Server.MACs) > 0 {
Loading
Loading
Loading
Loading
@@ -171,6 +171,52 @@ func TestCustomAlgorithms(t *testing.T) {
require.Equal(t, customCiphers, sshServerConfig.Ciphers)
}
func TestGSSAPIWithMIC(t *testing.T) {
srvCfg := &serverConfig{
cfg: &config.Config{
Server: config.ServerConfig{
GSSAPI: config.GSSAPIConfig{
Enabled: true,
ServicePrincipalName: "host/test@TEST.TEST",
},
},
},
}
sshServerConfig := srvCfg.get(context.Background())
server := sshServerConfig.GSSAPIWithMICConfig.Server.(*OSGSSAPIServer)
require.NotNil(t, sshServerConfig.GSSAPIWithMICConfig)
require.NotNil(t, sshServerConfig.GSSAPIWithMICConfig.AllowLogin)
require.NotNil(t, server)
require.Equal(t, server.ServicePrincipalName, "host/test@TEST.TEST")
sshServerConfig.SetDefaults()
require.NotNil(t, sshServerConfig.GSSAPIWithMICConfig)
require.NotNil(t, sshServerConfig.GSSAPIWithMICConfig.AllowLogin)
require.NotNil(t, server)
require.Equal(t, server.ServicePrincipalName, "host/test@TEST.TEST")
}
func TestGSSAPIWithMICDisabled(t *testing.T) {
srvCfg := &serverConfig{
cfg: &config.Config{
Server: config.ServerConfig{
GSSAPI: config.GSSAPIConfig{
Enabled: false,
},
},
},
}
sshServerConfig := srvCfg.get(context.Background())
require.Nil(t, sshServerConfig.GSSAPIWithMICConfig)
sshServerConfig.SetDefaults()
require.Nil(t, sshServerConfig.GSSAPIWithMICConfig)
}
func rsaPublicKey(t *testing.T) ssh.PublicKey {
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err)
Loading
Loading
Loading
Loading
@@ -13,6 +13,7 @@ import (
grpcstatus "google.golang.org/grpc/status"
shellCmd "gitlab.com/gitlab-org/gitlab-shell/v14/cmd/gitlab-shell/command"
"gitlab.com/gitlab-org/gitlab-shell/v14/internal/command"
"gitlab.com/gitlab-org/gitlab-shell/v14/internal/command/readwriter"
"gitlab.com/gitlab-org/gitlab-shell/v14/internal/command/shared/disallowedcommand"
"gitlab.com/gitlab-org/gitlab-shell/v14/internal/config"
Loading
Loading
@@ -23,10 +24,11 @@ import (
type session struct {
// State set up by the connection
cfg *config.Config
channel ssh.Channel
gitlabKeyId string
remoteAddr string
cfg *config.Config
channel ssh.Channel
gitlabKeyId string
gitlabKrb5Principal string
remoteAddr string
// State managed by the session
execCmd string
Loading
Loading
@@ -166,7 +168,14 @@ func (s *session) handleShell(ctx context.Context, req *ssh.Request) (uint32, er
ErrOut: s.channel.Stderr(),
}
cmd, err := shellCmd.NewWithKey(s.gitlabKeyId, env, s.cfg, rw)
var cmd command.Command
var err error
if s.gitlabKrb5Principal != "" {
cmd, err = shellCmd.NewWithKrb5Principal(s.gitlabKrb5Principal, env, s.cfg, rw)
} else {
cmd, err = shellCmd.NewWithKey(s.gitlabKeyId, env, s.cfg, rw)
}
if err != nil {
if errors.Is(err, disallowedcommand.Error) {
s.toStderr(ctx, "ERROR: Unknown command: %v\n", s.execCmd)
Loading
Loading
Loading
Loading
@@ -130,21 +130,29 @@ func TestHandleExec(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
out := &bytes.Buffer{}
f := &fakeChannel{stdErr: out}
s := &session{
gitlabKeyId: "root",
channel: f,
cfg: &config.Config{GitlabUrl: url},
sessions := []*session{
{
gitlabKeyId: "root",
cfg: &config.Config{GitlabUrl: url},
},
{
gitlabKrb5Principal: "test@TEST.TEST",
cfg: &config.Config{GitlabUrl: url},
},
}
for _, s := range sessions {
out := &bytes.Buffer{}
f := &fakeChannel{stdErr: out}
r := &ssh.Request{Payload: tc.payload}
s.channel = f
shouldContinue, err := s.handleExec(context.Background(), r)
require.Equal(t, tc.expectedErr, err)
require.Equal(t, false, shouldContinue)
require.Equal(t, tc.sentRequestName, f.sentRequestName)
require.Equal(t, tc.sentRequestPayload, f.sentRequestPayload)
}
r := &ssh.Request{Payload: tc.payload}
shouldContinue, err := s.handleExec(context.Background(), r)
require.Equal(t, tc.expectedErr, err)
require.Equal(t, false, shouldContinue)
require.Equal(t, tc.sentRequestName, f.sentRequestName)
require.Equal(t, tc.sentRequestPayload, f.sentRequestPayload)
})
}
}
Loading
Loading
Loading
Loading
@@ -194,11 +194,12 @@ func (s *Server) handleConn(ctx context.Context, nconn net.Conn) {
conn := newConnection(s.Config, nconn)
conn.handle(ctx, s.serverConfig.get(ctx), func(sconn *ssh.ServerConn, channel ssh.Channel, requests <-chan *ssh.Request) error {
session := &session{
cfg: s.Config,
channel: channel,
gitlabKeyId: sconn.Permissions.Extensions["key-id"],
remoteAddr: remoteAddr,
started: time.Now(),
cfg: s.Config,
channel: channel,
gitlabKeyId: sconn.Permissions.Extensions["key-id"],
gitlabKrb5Principal: sconn.Permissions.Extensions["krb5principal"],
remoteAddr: remoteAddr,
started: time.Now(),
}
return session.handle(ctx, requests)
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