Skip to content

Commit

Permalink
Implement support for kerberos authentication (#13)
Browse files Browse the repository at this point in the history
* Initial kerberos authentication implementation

* Implement credetial delegation

* Implement kerberos-based password authentication

* Log successful GSSAPI auths

* Implement authorization webhook for kerberos

* Document kerberos configuration

* Use agent path from configuration

* Send remote address and connectionId to authz

* Add auth handler constructor

* Add support for logging in as other users

* fixup! Initial kerberos authentication implementation

* fixup! Implement authorization webhook for kerberos

* Improve comments

* Fix swagger operation

* Properly wrap (some) kerberos error messages

* Integrate retry library

* Fix retry attempts check

* Implement authz retrying

* Add kerberos tests

* Update go.mod

* Update gokrb5 to use containerssh fork

* fixup! Add kerberos tests

* Update delegation handling to new API

* fixup! fixup! Implement authorization webhook for kerberos

* Fix AllowLogin

* Fix tests on other modules

* Fix linter warnings

* Address review comments

* Update gokrb5 version to fix credential delegation

* Safeguard the case that delegated credentials are nil

* Change auth metadata to be a struct

* Make kubernetes backend write all files to the pod

* fixup! Update gokrb5 version to fix credential delegation

* Limit metadata transmission according to sensitivity

* fixup! Change auth metadata to be a struct

* fixup! Change auth metadata to be a struct

* fixup! Limit metadata transmission according to sensitivity

* fixup! Make kubernetes backend write all files to the pod

* fixup! Change auth metadata to be a struct

* fixup! fixup! Make kubernetes backend write all files to the pod

* fixup! fixup! Limit metadata transmission according to sensitivity

* Support files in session mode

* Support file writing in docker backend

* Document authorization call

* fixup! Support file writing in docker backend

* fixup! fixup! Support file writing in docker backend

* Add config option for clockskew

* Add option for strict acceptor check

* Make authz available to all authentication backends

* Ensure failed auths get rejected in sshserver

* fixup! Make authz available to all authentication backends

* Remove retry library

* Address review comments

* Address review comments

* Remove sensitivity and add environment customization

* Resolve golangci error

* Address review comments

* Fix lint issues
  • Loading branch information
tsipinakis committed Jan 4, 2022
1 parent ab477ce commit 625361f
Show file tree
Hide file tree
Showing 64 changed files with 2,253 additions and 112 deletions.
77 changes: 77 additions & 0 deletions auth/metadata.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package auth

type ConnectionMetadata struct {
// Metadata is a set of key-value pairs that can be returned and
// either consumed by the configuration server or exposed in the
// backend as environment variables.
Metadata map[string]string `json:"metadata,omitempty"`
// Environment is a set of key-value pairs that will be exposed to the
// container as environment variables
Environment map[string]string `json:"environment,omitempty"`
// Files is a key-value pair of files to be placed inside containers.
// The key represents the path to the file while the value is the
// binary content.
Files map[string][]byte `json:"files,omitempty"`
}

// Transmit returns a copy of the Metadata containing only the metadata map for transmission to external servers (file and environment maps are considered sensitive by default)
func (m *ConnectionMetadata) Transmit() *ConnectionMetadata {
if m == nil {
return nil
}
return &ConnectionMetadata{
Metadata: m.Metadata,
}
}

// Merge merges a metadata object into the current one. In case of duplicated keys the one in the new struct take precedence
func (m *ConnectionMetadata) Merge(newmeta *ConnectionMetadata) {
if m == nil {
return
}
if newmeta == nil {
return
}
for k, v := range newmeta.GetMetadata() {
m.GetMetadata()[k] = v
}
for k, v := range newmeta.GetFiles() {
m.GetFiles()[k] = v
}
for k, v := range newmeta.GetEnvironment() {
m.GetEnvironment()[k] = v
}
}

// GetMetadata returns an editable metadata map
func (m *ConnectionMetadata) GetMetadata() map[string]string {
if m == nil {
return nil
}
if m.Metadata == nil {
m.Metadata = make(map[string]string)
}
return m.Metadata
}

// GetFiles returns an editable files map
func (m *ConnectionMetadata) GetFiles() map[string][]byte {
if m == nil {
return nil
}
if m.Files == nil {
m.Files = make(map[string][]byte)
}
return m.Files
}

// GetFiles returns an editable files map
func (m *ConnectionMetadata) GetEnvironment() map[string]string {
if m == nil {
return nil
}
if m.Environment == nil {
m.Environment = make(map[string]string)
}
return m.Environment
}
36 changes: 34 additions & 2 deletions auth/protocol.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,36 @@ type PublicKeyAuthRequest struct {
PublicKey string `json:"publicKey"`
}

// AuthorizationRequest is the authorization request used after some
// authentication methods (e.g. kerberos) to determine whether users are
// allowed to access the service
//
// swagger:model AuthorizationRequest
type AuthorizationRequest struct {
// PrincipalUsername is the authenticated username of the user, this
// username has been verified to be correct and to correspond to the
// user that is connecting.
//
// required: true
PrincipalUsername string `json:"principalUsername"`
// LoginUsername is the username the user wishes to log in as. In
// general, the authorization must check that this matches the
// PrincipalUsername, however in some cases it may be beneficial to let
// some users log in as others (e.g. administrators logging in as
// normal users to debug)
//
// required: true
LoginUsername string `json:"loginUsername"`
// RemoteAddress is the address the user is connecting from
//
// required: true
RemoteAddress string `json:"remoteAddress"`
// ConnectionID is an opaque ID to identify the SSH connection
//
// required: true
ConnectionID string `json:"connectionId"`
}

// ResponseBody is a response to authentication requests.
//
// swagger:model AuthResponseBody
Expand All @@ -69,11 +99,13 @@ type ResponseBody struct {
// required: true
Success bool `json:"success"`

// Metadata is a set of key-value pairs that can be returned and either consumed by the configuration server or
// Metadata is a set of key-value pairs that can be returned and either
// consumed by the configuration server or
// exposed in the backend as environment variables.
// They can also be used to deploy files in the container
//
// required: false
Metadata map[string]string `json:"metadata,omitempty"`
Metadata *ConnectionMetadata `json:"metadata,omitempty"`
}

// Response is the full HTTP authentication response.
Expand Down
3 changes: 2 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"

auth2 "github.com/containerssh/libcontainerssh/auth"
"github.com/containerssh/libcontainerssh/config"
"github.com/containerssh/libcontainerssh/internal/auth"
"github.com/containerssh/libcontainerssh/internal/geoip/dummy"
Expand Down Expand Up @@ -37,7 +38,7 @@ type AuthenticationContext interface {
// Error returns the error that happened during the authentication.
Error() error
// Metadata returns a set of metadata entries that have been obtained during the authentication.
Metadata() map[string]string
Metadata() *auth2.ConnectionMetadata
}

// NewTestClient creates a new copy of a client usable for testing purposes.
Expand Down
13 changes: 13 additions & 0 deletions auth/webhook/handler_factory.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package webhook

import (
"net/http"

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

// NewHandler creates a HTTP handler that forwards calls to the provided h config request handler.
func NewHandler(h AuthRequestHandler, logger log.Logger) http.Handler {
return auth.NewHandler(h, logger)
}
19 changes: 17 additions & 2 deletions auth/webhook/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"os"
"time"

"github.com/containerssh/libcontainerssh/auth"
"github.com/containerssh/libcontainerssh/auth/webhook"
"github.com/containerssh/libcontainerssh/config"
"github.com/containerssh/libcontainerssh/log"
Expand All @@ -23,7 +24,7 @@ func (m *myAuthReqHandler) OnPassword(
connectionID string,
) (
success bool,
metadata map[string]string,
metadata *auth.ConnectionMetadata,
err error,
) {
return true, nil, nil
Expand All @@ -37,7 +38,21 @@ func (m *myAuthReqHandler) OnPubKey(
connectionID string,
) (
success bool,
metadata map[string]string,
metadata *auth.ConnectionMetadata,
err error,
) {
return true, nil, nil
}

// OnAuthorization will be called after login in non-webhook auth handlers to verify the user is authorized to login
func (m *myAuthReqHandler) OnAuthorization(
principalUsername string,
loginUsername string,
remoteAddress string,
connectionID string,
) (
success bool,
metadata *auth.ConnectionMetadata,
err error,
) {
return true, nil, nil
Expand Down
31 changes: 29 additions & 2 deletions cmd/containerssh-testauthconfigserver/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"os/signal"
"syscall"

auth2 "github.com/containerssh/libcontainerssh/auth"
"github.com/containerssh/libcontainerssh/config"
configWebhook "github.com/containerssh/libcontainerssh/config/webhook"
"github.com/containerssh/libcontainerssh/http"
Expand Down Expand Up @@ -53,7 +54,7 @@ type authHandler struct {
// "$ref": "#/responses/AuthResponse"
func (a *authHandler) OnPassword(Username string, _ []byte, _ string, _ string) (
bool,
map[string]string,
*auth2.ConnectionMetadata,
error,
) {
if os.Getenv("CONTAINERSSH_ALLOW_ALL") == "1" ||
Expand Down Expand Up @@ -81,7 +82,7 @@ func (a *authHandler) OnPassword(Username string, _ []byte, _ string, _ string)
// "$ref": "#/responses/AuthResponse"
func (a *authHandler) OnPubKey(Username string, _ string, _ string, _ string) (
bool,
map[string]string,
*auth2.ConnectionMetadata,
error,
) {
if Username == "foo" || Username == "busybox" {
Expand All @@ -90,6 +91,32 @@ func (a *authHandler) OnPubKey(Username string, _ string, _ string, _ string) (
return false, nil, nil
}

// swagger:operation POST /authz Authentication authz
//
// Authorization
//
// ---
// parameters:
// - name: request
// in: body
// description: The authorization request
// required: true
// schema:
// "$ref": "#/definitions/AuthorizationRequest"
// responses:
// "200":
// "$ref": "#/responses/AuthResponse"
func (a *authHandler) OnAuthorization(PrincipalUsername string, _ string,_ string, _ string) (
bool,
*auth2.ConnectionMetadata,
error,
) {
if PrincipalUsername == "foo" || PrincipalUsername == "busybox" {
return true, nil, nil
}
return false, nil, nil
}

type configHandler struct {
}

Expand Down
77 changes: 75 additions & 2 deletions config/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ type AuthConfig struct {
// OAuth2 is the configuration for OAuth2 authentication via Keyboard-Interactive.
OAuth2 AuthOAuth2ClientConfig `json:"oauth2" yaml:"oauth2"`

// Kerberos is the configuration for Kerberos authentication via GSSAPI and/or password.
Kerberos AuthKerberosClientConfig `json:"kerberos" yaml:"kerberos"`

// Authz is the authorization configuration. The authorization server
// will receive a webhook after successful user authentication to
// determine whether the specified user has access to the service.
Authz AuthzConfig `json:"authz" yaml:"authz"`

// AuthTimeout is the timeout for the overall authentication call (e.g. verifying a password). If the server
// responds with a non-200 response the call will be retried until this timeout is reached. This timeout
// should be increased to ~180s for OAuth2 login.
Expand Down Expand Up @@ -56,22 +64,28 @@ func (c *AuthConfig) Validate() error {
err = c.Webhook.Validate()
case AuthMethodOAuth2:
err = c.OAuth2.Validate()
case AuthMethodKerberos:
err = c.Kerberos.Validate()
default:
return fmt.Errorf("invalid method: %s", c.Method)
}

if err != nil {
return fmt.Errorf("invalid %s client configuration (%w)", c.Method, err)
}

err = c.Authz.Validate()
if err != nil {
return fmt.Errorf("Invalid authz configuration (%w)", err)
}

return nil
}

type AuthMethod string

// Validate checks if the provided method is valid or not.
func (m AuthMethod) Validate() error {
if m == "webhook" || m == "oauth2" {
if m == "webhook" || m == "oauth2" || m == "kerberos" {
return nil
}
return fmt.Errorf("invalid value for method: %s", m)
Expand All @@ -83,6 +97,8 @@ const AuthMethodWebhook AuthMethod = "webhook"
// AuthMethodOAuth2 authenticates by sending the user to a web interface using the keyboard-interactive facility.
const AuthMethodOAuth2 AuthMethod = "oauth2"

const AuthMethodKerberos AuthMethod = "kerberos"

// AuthWebhookClientConfig is the configuration for webhook authentication.
type AuthWebhookClientConfig struct {
HTTPClientConfiguration `json:",inline" yaml:",inline"`
Expand Down Expand Up @@ -293,3 +309,60 @@ func (o *AuthOIDCConfig) Validate() error {
}
return o.HTTPClientConfiguration.Validate()
}

// AuthzConfig is the configuration for the authorization flow
type AuthzConfig struct {
HTTPClientConfiguration `json:",inline" yaml:",inline"`
// Controls whether the authorization flow is enabled. If set to false
// all authenticated users are allowed in the service.
Enable bool `json:"enable" yaml:"enable" default:"false"`
}

func (k *AuthzConfig) Validate() error {
if k.Enable {
return k.HTTPClientConfiguration.Validate()
}
return nil
}

// AuthKerberosClientConfig is the configuration for the Kerberos authentication method.
type AuthKerberosClientConfig struct {
// Keytab is the path to the kerberos keytab. If unset it defaults to
// the default of /etc/krb5.keytab. If this file doesn't exist and
// kerberos authentication is requested ContainerSSH will fail to start
Keytab string `json:"keytab" yaml:"keytab" default:"/etc/krb5.keytab"`
// Acceptor is the name of the keytab entry to authenticate against.
// The value of this field needs to be in the form of `service/name`.
//
// The special value of `host` will authenticate clients only against
// the service `host/hostname` where hostname is the system hostname
// The special value of 'any' will authenticate against all keytab
// entries regardless of name
Acceptor string `json:"acceptor" yaml:"acceptor" default:"any"`
// EnforceUsername specifies whether to check that the username of the
// authenticated user matches the SSH username entered. If set to false
// the authorization server must be responsible for ensuring proper
// access control.
//
// WARNING: If authorization is unset and this is set to false all
// authenticated users can log in to any account!
EnforceUsername bool `json:"enforceUsername" yaml:"enforceUsername" default:"true"`
// CredentialCachePath is the path in which the kerberos credentials
// will be written inside the user containers.
CredentialCachePath string `json:"credentialCachePath" yaml:"credentialCachePath" default:"/tmp/krb5cc"`
// AllowPassword controls whether kerberos-based password
// authentication should be allowed. If set to false only GSSAPI
// authentication will be permitted
AllowPassword bool `json:"allowPassword" yaml:"allowPassword" default:"true"`
// ConfigPath is the path of the kerberos configuration file. This is
// only used for password authentication.
ConfigPath string `json:"configPath" yaml:"configPath" default:"/etc/containerssh/krb5.conf"`
// ClockSkew is the maximum allowed clock skew for kerberos messages,
// any messages older than this will be rejected. This value is also
// used for the replay cache.
ClockSkew time.Duration `json:"clockSkew" yaml:"clockSkew" default:"5m"`
}

func (k *AuthKerberosClientConfig) Validate() error {
return nil
}
6 changes: 5 additions & 1 deletion config/protocol.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package config

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

// Request is the request object passed from the client to the config server.
//
// swagger:model Request
Expand All @@ -23,7 +27,7 @@ type Request struct {
// Metadata is the metadata received from the authentication server.
//
// required: false
Metadata map[string]string `json:"metadata"`
Metadata *auth.ConnectionMetadata `json:"metadata"`
}

// ResponseBody is the structure representing the JSON HTTP response.
Expand Down
Loading

0 comments on commit 625361f

Please sign in to comment.