diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d9d367..0cf5027 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 0.9.4: Passing through OnShutdown, Keyboard Interactive. + +This release fixes a bug where the OnShutdown hook would not be passed through to the backends and adds Keyboard-Interactive authentication support. + ## 0.9.3: Bumping version This release bumps the version to work aroung Go caching. diff --git a/go.sum b/go.sum index c5cbea1..b50d91c 100644 --- a/go.sum +++ b/go.sum @@ -161,6 +161,7 @@ golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191224085550-c709ea063b76/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201112073958-5cba982894dd h1:5CtCZbICpIOFdgO940moixOPjc0178IU44m4EjOO5IY= golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -210,6 +211,7 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/handler_factory.go b/handler_factory.go index 94aeb00..5592d10 100644 --- a/handler_factory.go +++ b/handler_factory.go @@ -6,7 +6,6 @@ import ( ) // NewHandler creates a new audit logging handler that logs all events as configured, and passes request to a provided backend. -//goland:noinspection GoUnusedExportedFunction func NewHandler(backend sshserver.Handler, logger auditlog.Logger) sshserver.Handler { return &handler{ backend: backend, diff --git a/handler_networkconnection.go b/handler_networkconnection.go index 9ec0e16..923e0d5 100644 --- a/handler_networkconnection.go +++ b/handler_networkconnection.go @@ -1,17 +1,64 @@ package auditlogintegration import ( + "context" + "github.com/containerssh/auditlog" + "github.com/containerssh/auditlog/message" "github.com/containerssh/sshserver" ) type networkConnectionHandler struct { - sshserver.AbstractNetworkConnectionHandler - backend sshserver.NetworkConnectionHandler audit auditlog.Connection } +func (n *networkConnectionHandler) OnAuthKeyboardInteractive( + user string, + challenge func( + instruction string, + questions sshserver.KeyboardInteractiveQuestions, + ) (answers sshserver.KeyboardInteractiveAnswers, err error), +) (response sshserver.AuthResponse, reason error) { + return n.backend.OnAuthKeyboardInteractive( + user, + func( + instruction string, + questions sshserver.KeyboardInteractiveQuestions, + ) (answers sshserver.KeyboardInteractiveAnswers, err error) { + var auditQuestions []message.KeyboardInteractiveQuestion + for _, q := range questions { + auditQuestions = append(auditQuestions, message.KeyboardInteractiveQuestion{ + Question: q.Question, + Echo: q.EchoResponse, + }) + } + n.audit.OnAuthKeyboardInteractiveChallenge(user, instruction, auditQuestions) + answers, err = challenge(instruction, questions) + if err != nil { + return answers, err + } + var auditAnswers []message.KeyboardInteractiveAnswer + for _, q := range auditQuestions { + a, err := answers.GetByQuestionText(q.Question) + if err != nil { + return answers, err + } + auditAnswers = append(auditAnswers, message.KeyboardInteractiveAnswer{ + Question: q.Question, + Answer: a, + }) + } + n.audit.OnAuthKeyboardInteractiveAnswer(user, auditAnswers) + return answers, err + }, + ) +} + +func (n *networkConnectionHandler) OnShutdown(shutdownContext context.Context) { + n.backend.OnShutdown(shutdownContext) +} + func (n *networkConnectionHandler) OnAuthPassword( username string, password []byte, diff --git a/handler_sessionchannel.go b/handler_sessionchannel.go index 5080b3d..6490061 100644 --- a/handler_sessionchannel.go +++ b/handler_sessionchannel.go @@ -1,18 +1,27 @@ package auditlogintegration import ( + "context" + "github.com/containerssh/auditlog" "github.com/containerssh/sshserver" ) type sessionChannelHandler struct { - sshserver.AbstractSessionChannelHandler - backend sshserver.SessionChannelHandler audit auditlog.Channel session sshserver.SessionChannel } +func (s *sessionChannelHandler) OnClose() { + s.audit.OnClose() + s.backend.OnClose() +} + +func (s *sessionChannelHandler) OnShutdown(shutdownContext context.Context) { + s.backend.OnShutdown(shutdownContext) +} + func (s *sessionChannelHandler) OnUnsupportedChannelRequest(requestID uint64, requestType string, payload []byte) { s.backend.OnUnsupportedChannelRequest(requestID, requestType, payload) s.audit.OnRequestUnknown(requestID, requestType, payload) diff --git a/handler_sshconnection.go b/handler_sshconnection.go index 1f7d1be..a97f57b 100644 --- a/handler_sshconnection.go +++ b/handler_sshconnection.go @@ -1,6 +1,7 @@ package auditlogintegration import ( + "context" "io" "github.com/containerssh/auditlog" @@ -9,12 +10,14 @@ import ( ) type sshConnectionHandler struct { - sshserver.AbstractSSHConnectionHandler - backend sshserver.SSHConnectionHandler audit auditlog.Connection } +func (s *sshConnectionHandler) OnShutdown(shutdownContext context.Context) { + s.backend.OnShutdown(shutdownContext) +} + func (s *sshConnectionHandler) OnUnsupportedGlobalRequest(requestID uint64, requestType string, payload []byte) { //todo audit payload s.audit.OnGlobalRequestUnknown(requestType) @@ -111,9 +114,6 @@ func (s *sessionProxy) CloseWrite() error { } func (s *sessionProxy) Close() error { - if s.audit == nil { - panic("BUG: close requested before channel is open") - } - s.audit.OnClose() + // Audit logging is done via the session channel hook. return s.backend.Close() } diff --git a/integration_test.go b/integration_test.go index 1488986..3306dd1 100644 --- a/integration_test.go +++ b/integration_test.go @@ -22,6 +22,59 @@ import ( "github.com/containerssh/auditlogintegration" ) +func TestKeyboardInteractiveAuthentication(t *testing.T) { + logger := log.GetTestLogger(t) + + dir, err := ioutil.TempDir("temp", "testcase") + assert.NoError(t, err) + defer func() { + _ = os.RemoveAll(dir) + }() + + geoipLookup, err := geoip.New( + geoip.Config{ + Provider: "dummy", + }, + ) + assert.NoError(t, err) + + auditLogHandler, err := auditlogintegration.New( + auditlog.Config{ + Enable: true, + Format: auditlog.FormatBinary, + Storage: auditlog.StorageFile, + File: file.Config{ + Directory: dir, + }, + }, + &backendHandler{}, + geoipLookup, + logger, + ) + assert.NoError(t, err) + + user := sshserver.NewTestUser("test") + user.AddKeyboardInteractiveChallengeResponse("Challenge", "Response") + + srv := sshserver.NewTestServer(auditLogHandler, logger) + srv.Start() + client := sshserver.NewTestClient(srv.GetListen(), srv.GetHostKey(), user, logger) + connection := client.MustConnect() + _ = connection.Close() + srv.Stop(10 * time.Second) + + messages, errors, done := getStoredMessages(t, dir, logger) + if done { + return + } + assert.Empty(t, errors) + assert.Equal(t, message.TypeConnect, messages[0].MessageType) + assert.Equal(t, message.TypeAuthKeyboardInteractiveChallenge, messages[1].MessageType) + assert.Equal(t, message.TypeAuthKeyboardInteractiveAnswer, messages[2].MessageType) + assert.Equal(t, message.TypeHandshakeSuccessful, messages[3].MessageType) + assert.Equal(t, message.TypeDisconnect, messages[4].MessageType) +} + func TestConnectMessages(t *testing.T) { logger := log.GetTestLogger(t) @@ -85,6 +138,23 @@ func createTestServer(t *testing.T, dir string, logger log.Logger) (sshserver.Te } func checkStoredAuditMessages(t *testing.T, dir string, logger log.Logger) { + messages, errors, done := getStoredMessages(t, dir, logger) + if done { + return + } + assert.Empty(t, errors) + assert.NotEmpty(t, messages) + assert.Equal(t, message.TypeConnect, messages[0].MessageType) + assert.Equal(t, message.TypeAuthPassword, messages[1].MessageType) + assert.Equal(t, message.TypeAuthPasswordSuccessful, messages[2].MessageType) + assert.Equal(t, message.TypeHandshakeSuccessful, messages[3].MessageType) + assert.Equal(t, message.TypeNewChannelSuccessful, messages[4].MessageType) + assert.Equal(t, message.TypeChannelRequestShell, messages[5].MessageType) + assert.Equal(t, message.TypeExit, messages[6].MessageType) + assert.Equal(t, message.TypeDisconnect, messages[7].MessageType) +} + +func getStoredMessages(t *testing.T, dir string, logger log.Logger) ([]message.Message, []error, bool) { storage, err := file.NewStorage( file.Config{ Directory: dir, @@ -102,7 +172,7 @@ func checkStoredAuditMessages(t *testing.T, dir string, logger log.Logger) { } assert.NotNil(t, logReader) if logReader == nil { - return + return nil, nil, true } decoder := binary.NewDecoder() @@ -124,16 +194,7 @@ loop: errors = append(errors, err) } } - assert.Empty(t, errors) - assert.NotEmpty(t, messages) - assert.Equal(t, message.TypeConnect, messages[0].MessageType) - assert.Equal(t, message.TypeAuthPassword, messages[1].MessageType) - assert.Equal(t, message.TypeAuthPasswordSuccessful, messages[2].MessageType) - assert.Equal(t, message.TypeHandshakeSuccessful, messages[3].MessageType) - assert.Equal(t, message.TypeNewChannelSuccessful, messages[4].MessageType) - assert.Equal(t, message.TypeChannelRequestShell, messages[5].MessageType) - assert.Equal(t, message.TypeExit, messages[6].MessageType) - assert.Equal(t, message.TypeDisconnect, messages[7].MessageType) + return messages, errors, false } type backendHandler struct { @@ -142,12 +203,28 @@ type backendHandler struct { func (b *backendHandler) OnAuthKeyboardInteractive( _ string, - _ func( + challenge func( instruction string, questions sshserver.KeyboardInteractiveQuestions, ) (answers sshserver.KeyboardInteractiveAnswers, err error), ) (response sshserver.AuthResponse, reason error) { - return sshserver.AuthResponseUnavailable, fmt.Errorf("not implemented") + answers, err := challenge( + "Test", + sshserver.KeyboardInteractiveQuestions{{ + Question: "Challenge", + EchoResponse: true, + }}, + ) + if err != nil { + return + } + answerText, err := answers.GetByQuestionText("Challenge") + if err == nil { + if answerText != "Response" { + return sshserver.AuthResponseUnavailable, fmt.Errorf("invalid response") + } + } + return sshserver.AuthResponseSuccess, err } func (b *backendHandler) OnClose() {