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

Add SSH certificate information to auth webhook #37

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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