Skip to content
This repository has been archived by the owner on Jan 6, 2025. It is now read-only.

Commit

Permalink
Fixes ContainerSSH/ContainerSSH#331: Add SSH certificate information …
Browse files Browse the repository at this point in the history
…to webhook.
  • Loading branch information
Janos Bonic committed Jan 4, 2022
1 parent c925d96 commit cf545cf
Show file tree
Hide file tree
Showing 28 changed files with 209 additions and 76 deletions.
11 changes: 9 additions & 2 deletions auditlog/message/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@ func (p PayloadAuthPasswordBackendError) Equals(other Payload) bool {

// PayloadAuthPubKey is a payload for a public key based authentication.
type PayloadAuthPubKey struct {
Username string `json:"username" yaml:"username"`
Key string `json:"key" yaml:"key"`
Username string `json:"username" yaml:"username"`
Key string `json:"key" yaml:"key"`
CACert *CACertificate `json:"caCertificate" yaml:"caCertificate"`
}

// Equals compares two PayloadAuthPubKey payloads.
Expand All @@ -46,6 +47,12 @@ func (p PayloadAuthPubKey) Equals(other Payload) bool {
if !ok {
return false
}
if p.CACert == nil && p2.CACert != nil || p.CACert != nil && p.CACert == nil {
return false
}
if p.CACert != nil && !p.CACert.Equals(p2.CACert) {
return false
}
return p.Username == p2.Username && p.Key == p2.Key
}

Expand Down
50 changes: 50 additions & 0 deletions auditlog/message/cacertificate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package message

import "time"

// CACertificate is an SSH certificate presented by a client to verify their key against a CA.
type CACertificate struct {
// PublicKey contains the public key of the CA signing the public key presented in the OpenSSH authorized key
// format.
PublicKey string `json:"key"`
// KeyID contains an identifier for the key.
KeyID string `json:"keyID"`
// ValidPrincipals contains a list of principals for which this CA certificate is valid.
ValidPrincipals []string `json:"validPrincipals"`
// ValidAfter contains the time after which this certificate is valid. This may be empty.
ValidAfter time.Time `json:"validAfter"`
// ValidBefore contains the time when this certificate expires. This may be empty.
ValidBefore time.Time `json:"validBefore"`
}

// Equals compares the CACertificate record.
func (c *CACertificate) Equals(cert *CACertificate) bool {
if c == nil {
return cert == nil
}
if cert == nil {
return false
}
if c.PublicKey != cert.PublicKey {
return false
}
if c.KeyID != cert.KeyID {
return false
}
if len(c.ValidPrincipals) != len(cert.ValidPrincipals) {
return false
}
for _, validPrincipal := range c.ValidPrincipals {
found := false
for _, otherPrincipal := range cert.ValidPrincipals {
if otherPrincipal == validPrincipal {
found = true
break
}
}
if !found {
return false
}
}
return c.ValidAfter == cert.ValidAfter && c.ValidBefore == cert.ValidBefore
}
28 changes: 28 additions & 0 deletions auth/protocol.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package auth

import "time"

// PasswordAuthRequest is an authentication request for password authentication.
//
// swagger:model PasswordAuthRequest
Expand Down Expand Up @@ -58,6 +60,13 @@ type PublicKeyAuthRequest struct {
//
// required: true
PublicKey string `json:"publicKey"`

// CACertificate contains information about the SSH certificate presented by a connecting client. This certificate
// is not an SSL/TLS/x509 certificate and has a much simpler structure. However, this can be used to verify if the
// connecting client belongs to an organization.
//
// required: false
CACertificate *CACertificate `json:"caCertificate,omitempty"`
}

// ResponseBody is a response to authentication requests.
Expand Down Expand Up @@ -85,3 +94,22 @@ type Response struct {
// in: body
ResponseBody
}

// CACertificate contains information about the SSH certificate presented by a connecting client. This certificate
// is not an SSL/TLS/x509 certificate and has a much simpler structure. However, this can be used to verify if the
// connecting client belongs to an organization.
//
// swagger:model CACertificate
type CACertificate struct {
// PublicKey contains the public key of the CA signing the public key presented in the OpenSSH authorized key
// format.
PublicKey string `json:"key"`
// KeyID contains an identifier for the key.
KeyID string `json:"keyID"`
// ValidPrincipals contains a list of principals for which this CA certificate is valid.
ValidPrincipals []string `json:"validPrincipals"`
// ValidAfter contains the time after which this certificate is valid.
ValidAfter time.Time `json:"validAfter"`
// ValidBefore contains the time when this certificate expires.
ValidBefore time.Time `json:"validBefore"`
}
15 changes: 14 additions & 1 deletion auth/webhook/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package webhook
import (
"net"

protocol "github.com/containerssh/libcontainerssh/auth"
"github.com/containerssh/libcontainerssh/config"
"github.com/containerssh/libcontainerssh/internal/auth"
"github.com/containerssh/libcontainerssh/internal/geoip/dummy"
Expand All @@ -22,11 +23,22 @@ type Client interface {

// PubKey authenticates with a public key from the client. It returns a bool if the authentication as successful
// or not. If an error happened while contacting the authentication server it will return an error.
//
// The parameters are as follows:
//
// - username is the username provided by the connecting client.
// - pubKey is the public key offered by the connecting client. The client may offer multiple keys which will be
// presented by calling this function multiple times.
// - connectionID is an opaque random string representing this SSH connection across multiple webhooks and logs.
// - remoteAddr is the IP address of the connecting client.
// - caPubKey is the verified public key of the SSH CA certificate offered by the client. If no CA certificate
// was offered this value is nil.
PubKey(
username string,
pubKey string,
connectionID string,
remoteAddr net.IP,
caPubKey *protocol.CACertificate,
) AuthenticationContext
}

Expand Down Expand Up @@ -79,6 +91,7 @@ func (a authClientWrapper) PubKey(
pubKey string,
connectionID string,
remoteAddr net.IP,
caPubKey *protocol.CACertificate,
) AuthenticationContext {
return a.c.PubKey(username, pubKey, connectionID, remoteAddr)
return a.c.PubKey(username, pubKey, connectionID, remoteAddr, caPubKey)
}
2 changes: 1 addition & 1 deletion internal/auditlog/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ type Connection interface {
OnAuthPasswordBackendError(username string, password []byte, reason string)

// OnAuthPubKey creates an audit log message for an authentication attempt with public key.
OnAuthPubKey(username string, pubKey string)
OnAuthPubKey(username string, pubKey string, caKey *message.CACertificate)
// OnAuthPubKeySuccess creates an audit log message for a successful public key authentication.
OnAuthPubKeySuccess(username string, pubKey string)
// OnAuthPubKeyFailed creates an audit log message for a failed public key authentication.
Expand Down
2 changes: 1 addition & 1 deletion internal/auditlog/logger_empty.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ func (e *empty) OnAuthPasswordFailed(_ string, _ []byte) {}

func (e *empty) OnAuthPasswordBackendError(_ string, _ []byte, _ string) {}

func (e *empty) OnAuthPubKey(_ string, _ string) {}
func (e *empty) OnAuthPubKey(_ string, _ string, _ *message.CACertificate) {}

func (e *empty) OnAuthPubKeySuccess(_ string, _ string) {}

Expand Down
3 changes: 2 additions & 1 deletion internal/auditlog/logger_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,14 +156,15 @@ func (l *loggerConnection) OnAuthPasswordBackendError(username string, password
})
}

func (l *loggerConnection) OnAuthPubKey(username string, pubKey string) {
func (l *loggerConnection) OnAuthPubKey(username string, pubKey string, caCert *message.CACertificate) {
l.log(message.Message{
ConnectionID: l.connectionID,
Timestamp: time.Now().UnixNano(),
MessageType: message.TypeAuthPubKey,
Payload: message.PayloadAuthPubKey{
Username: username,
Key: pubKey,
CACert: caCert,
},
ChannelID: nil,
})
Expand Down
6 changes: 3 additions & 3 deletions internal/auditlog/logger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,11 +254,11 @@ func TestAuth(t *testing.T) {
connection.OnAuthPasswordFailed("foo", []byte("bar"))
connection.OnAuthPassword("foo", []byte("baz"))
connection.OnAuthPasswordSuccess("foo", []byte("baz"))
connection.OnAuthPubKey("foo", "ssh-rsa ASDF")
connection.OnAuthPubKey("foo", "ssh-rsa ASDF", "")
connection.OnAuthPubKeyBackendError("foo", "ssh-rsa ASDF", "no particular reason")
connection.OnAuthPubKey("foo", "ssh-rsa ASDF")
connection.OnAuthPubKey("foo", "ssh-rsa ASDF", "")
connection.OnAuthPubKeyFailed("foo", "ssh-rsa ASDF")
connection.OnAuthPubKey("foo", "ssh-rsa ABCDEF")
connection.OnAuthPubKey("foo", "ssh-rsa ABCDEF", "")
connection.OnAuthPubKeySuccess("foo", "ssh-rsa ABCDEF")
connection.OnHandshakeSuccessful("foo")
connection.OnDisconnect()
Expand Down
27 changes: 16 additions & 11 deletions internal/auditlogintegration/handler_networkconnection.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,17 +83,22 @@ func (n *networkConnectionHandler) OnAuthPassword(
return response, metadata, reason
}

func (n *networkConnectionHandler) OnAuthPubKey(
username string,
pubKey string,
clientVersion string,
) (
response sshserver.AuthResponse,
metadata map[string]string,
reason error,
) {
n.audit.OnAuthPubKey(username, pubKey)
response, metadata, reason = n.backend.OnAuthPubKey(username, pubKey, clientVersion)
func convertCACertificate(caCert *sshserver.CACertificate) *message.CACertificate {
if caCert == nil {
return nil
}
return &message.CACertificate{
PublicKey: caCert.PublicKey,
KeyID: caCert.KeyID,
ValidPrincipals: caCert.ValidPrincipals,
ValidAfter: caCert.ValidAfter,
ValidBefore: caCert.ValidBefore,
}
}

func (n *networkConnectionHandler) OnAuthPubKey(username string, pubKey string, clientVersion string, caCert *sshserver.CACertificate) (response sshserver.AuthResponse, metadata map[string]string, reason error) {
n.audit.OnAuthPubKey(username, pubKey, convertCACertificate(caCert))
response, metadata, reason = n.backend.OnAuthPubKey(username, pubKey, clientVersion, caCert)
switch response {
case sshserver.AuthResponseSuccess:
n.audit.OnAuthPubKeySuccess(username, pubKey)
Expand Down
6 changes: 1 addition & 5 deletions internal/auditlogintegration/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -316,11 +316,7 @@ func (b *backendHandler) OnAuthPassword(username string, _ []byte, _ string) (
return sshserver.AuthResponseFailure, nil, nil
}

func (b *backendHandler) OnAuthPubKey(_ string, _ string, _ string) (
response sshserver.AuthResponse,
metadata map[string]string,
reason error,
) {
func (b *backendHandler) OnAuthPubKey(username string, pubKey string, clientVersion string, caKey string) (response sshserver.AuthResponse, metadata map[string]string, reason error) {
return sshserver.AuthResponseFailure, nil, nil
}

Expand Down
5 changes: 4 additions & 1 deletion internal/auth/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package auth

import (
"net"

"github.com/containerssh/libcontainerssh/auth"
)

// AuthenticationContext holds the results of an authentication.
Expand Down Expand Up @@ -38,6 +40,7 @@ type Client interface {
pubKey string,
connectionID string,
remoteAddr net.IP,
caPubKey *auth.CACertificate,
) AuthenticationContext

// KeyboardInteractive is a method to post a series of questions to the user and receive answers.
Expand Down Expand Up @@ -69,4 +72,4 @@ type KeyboardInteractiveQuestion struct {
type KeyboardInteractiveAnswers struct {
// KeyboardInteractiveQuestion is the original question that was answered.
Answers map[string]string
}
}
8 changes: 2 additions & 6 deletions internal/auth/client_http.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,7 @@ func (client *httpAuthClient) Password(
return client.processAuthWithRetry(username, method, authType, connectionID, url, authRequest, remoteAddr)
}

func (client *httpAuthClient) PubKey(
username string,
pubKey string,
connectionID string,
remoteAddr net.IP,
) AuthenticationContext {
func (client *httpAuthClient) PubKey(username string, pubKey string, connectionID string, remoteAddr net.IP, caPubKey *auth.CACertificate) AuthenticationContext {
if !client.enablePubKey {
err := message.UserMessage(
message.EAuthDisabled,
Expand All @@ -117,6 +112,7 @@ func (client *httpAuthClient) PubKey(
ConnectionID: connectionID,
SessionID: connectionID,
PublicKey: pubKey,
CACertificate: caPubKey,
}
method := "Public key"
authType := "pubkey"
Expand Down
3 changes: 2 additions & 1 deletion internal/auth/client_oauth2.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"net"
"strings"

"github.com/containerssh/libcontainerssh/auth"
"github.com/containerssh/libcontainerssh/log"
"github.com/containerssh/libcontainerssh/message"
)
Expand Down Expand Up @@ -48,7 +49,7 @@ func (o *oauth2Client) Password(_ string, _ []byte, _ string, _ net.IP) Authenti
), nil}
}

func (o *oauth2Client) PubKey(_ string, _ string, _ string, _ net.IP) AuthenticationContext {
func (o *oauth2Client) PubKey(_ string, _ string, _ string, _ net.IP, _ *auth.CACertificate) AuthenticationContext {
return &oauth2Context{false, nil, message.UserMessage(
message.EAuthUnsupported,
"Public key authentication is not available.",
Expand Down
1 change: 1 addition & 0 deletions internal/auth/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ func TestPubKeyDisabled(t *testing.T) {
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDP39LqSomHi4kicGADA3XVQoYxzNkvrBLOqN5AEEP01p0TZ39LXa6FdB4Pmvg8h51c+BNLoxpYrTk4UibMD87OPKYYXrNmLvq0GwjMPYpzoICevAJm+/2sDVlK9sXT93Fkin+tei+Evgf/hQK0xN+HXqP8dz8SWSXeWjBv588eHHCdrV+0FlZLXH+9D18tD4BNPHe9iJLpeeH6gsvQBvArXcIEQVvHIo1cCcsy28ymUFndG55LdOaTCA+pcfHLmRtL8HO2mI2Qc/0HBSc2d1gb3lHAnmdMT82K58OjRp9Tegc5hVuKVE+hkmNjfo3f1mVHsNu6JYLxRngnbJ20QdzuKcPb3pRMty+ggRgEQExvgl1pC3OVcgyc8YX1eXiyhYy0kXT/Jg++AcaIC1Xk/hDfB0T7WxCO0Wwd4KSjKr79tIxM/m4jP2K1Hk4yAnT7mZQ0GjdphLLuDk3yt8R809SPuzkPCXBM0sL6FrqT2GVDNihN2pBh1MyuUt7S8ZXpuW0=",
"asdf",
net.ParseIP("127.0.0.1"),
nil,
)
if authContext.Success() {
t.Fatal("Public key authentication method resulted in successful authentication.")
Expand Down
6 changes: 3 additions & 3 deletions internal/auth/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,15 +99,15 @@ func TestAuth(t *testing.T) {
assert.Equal(t, false, authenticationContext.Success())
assert.Equal(t, float64(1), metricsCollector.GetMetric(auth.MetricNameAuthBackendFailure)[0].Value)

authenticationContext = client.PubKey("foo", "ssh-rsa asdf", "0123456789ABCDEF", net.ParseIP("127.0.0.1"))
authenticationContext = client.PubKey("foo", "ssh-rsa asdf", "0123456789ABCDEF", net.ParseIP("127.0.0.1"), nil)
assert.Equal(t, nil, authenticationContext.Error())
assert.Equal(t, true, authenticationContext.Success())

authenticationContext = client.PubKey("foo", "ssh-rsa asdx", "0123456789ABCDEF", net.ParseIP("127.0.0.1"))
authenticationContext = client.PubKey("foo", "ssh-rsa asdx", "0123456789ABCDEF", net.ParseIP("127.0.0.1"), nil)
assert.Equal(t, nil, authenticationContext.Error())
assert.Equal(t, false, authenticationContext.Success())

authenticationContext = client.PubKey("crash", "ssh-rsa asdx", "0123456789ABCDEF", net.ParseIP("127.0.0.1"))
authenticationContext = client.PubKey("crash", "ssh-rsa asdx", "0123456789ABCDEF", net.ParseIP("127.0.0.1"), nil)
assert.NotEqual(t, nil, authenticationContext.Error())
assert.Equal(t, false, authenticationContext.Success())
})
Expand Down
Loading

0 comments on commit cf545cf

Please sign in to comment.