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

Commit 625361f

Browse files
authored
Implement support for kerberos authentication (#13)
* 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
1 parent ab477ce commit 625361f

64 files changed

Lines changed: 2253 additions & 112 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

auth/metadata.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package auth
2+
3+
type ConnectionMetadata struct {
4+
// Metadata is a set of key-value pairs that can be returned and
5+
// either consumed by the configuration server or exposed in the
6+
// backend as environment variables.
7+
Metadata map[string]string `json:"metadata,omitempty"`
8+
// Environment is a set of key-value pairs that will be exposed to the
9+
// container as environment variables
10+
Environment map[string]string `json:"environment,omitempty"`
11+
// Files is a key-value pair of files to be placed inside containers.
12+
// The key represents the path to the file while the value is the
13+
// binary content.
14+
Files map[string][]byte `json:"files,omitempty"`
15+
}
16+
17+
// 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)
18+
func (m *ConnectionMetadata) Transmit() *ConnectionMetadata {
19+
if m == nil {
20+
return nil
21+
}
22+
return &ConnectionMetadata{
23+
Metadata: m.Metadata,
24+
}
25+
}
26+
27+
// Merge merges a metadata object into the current one. In case of duplicated keys the one in the new struct take precedence
28+
func (m *ConnectionMetadata) Merge(newmeta *ConnectionMetadata) {
29+
if m == nil {
30+
return
31+
}
32+
if newmeta == nil {
33+
return
34+
}
35+
for k, v := range newmeta.GetMetadata() {
36+
m.GetMetadata()[k] = v
37+
}
38+
for k, v := range newmeta.GetFiles() {
39+
m.GetFiles()[k] = v
40+
}
41+
for k, v := range newmeta.GetEnvironment() {
42+
m.GetEnvironment()[k] = v
43+
}
44+
}
45+
46+
// GetMetadata returns an editable metadata map
47+
func (m *ConnectionMetadata) GetMetadata() map[string]string {
48+
if m == nil {
49+
return nil
50+
}
51+
if m.Metadata == nil {
52+
m.Metadata = make(map[string]string)
53+
}
54+
return m.Metadata
55+
}
56+
57+
// GetFiles returns an editable files map
58+
func (m *ConnectionMetadata) GetFiles() map[string][]byte {
59+
if m == nil {
60+
return nil
61+
}
62+
if m.Files == nil {
63+
m.Files = make(map[string][]byte)
64+
}
65+
return m.Files
66+
}
67+
68+
// GetFiles returns an editable files map
69+
func (m *ConnectionMetadata) GetEnvironment() map[string]string {
70+
if m == nil {
71+
return nil
72+
}
73+
if m.Environment == nil {
74+
m.Environment = make(map[string]string)
75+
}
76+
return m.Environment
77+
}

auth/protocol.go

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,36 @@ type PublicKeyAuthRequest struct {
6060
PublicKey string `json:"publicKey"`
6161
}
6262

63+
// AuthorizationRequest is the authorization request used after some
64+
// authentication methods (e.g. kerberos) to determine whether users are
65+
// allowed to access the service
66+
//
67+
// swagger:model AuthorizationRequest
68+
type AuthorizationRequest struct {
69+
// PrincipalUsername is the authenticated username of the user, this
70+
// username has been verified to be correct and to correspond to the
71+
// user that is connecting.
72+
//
73+
// required: true
74+
PrincipalUsername string `json:"principalUsername"`
75+
// LoginUsername is the username the user wishes to log in as. In
76+
// general, the authorization must check that this matches the
77+
// PrincipalUsername, however in some cases it may be beneficial to let
78+
// some users log in as others (e.g. administrators logging in as
79+
// normal users to debug)
80+
//
81+
// required: true
82+
LoginUsername string `json:"loginUsername"`
83+
// RemoteAddress is the address the user is connecting from
84+
//
85+
// required: true
86+
RemoteAddress string `json:"remoteAddress"`
87+
// ConnectionID is an opaque ID to identify the SSH connection
88+
//
89+
// required: true
90+
ConnectionID string `json:"connectionId"`
91+
}
92+
6393
// ResponseBody is a response to authentication requests.
6494
//
6595
// swagger:model AuthResponseBody
@@ -69,11 +99,13 @@ type ResponseBody struct {
6999
// required: true
70100
Success bool `json:"success"`
71101

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

79111
// Response is the full HTTP authentication response.

auth/webhook/client.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package webhook
33
import (
44
"net"
55

6+
auth2 "github.com/containerssh/libcontainerssh/auth"
67
"github.com/containerssh/libcontainerssh/config"
78
"github.com/containerssh/libcontainerssh/internal/auth"
89
"github.com/containerssh/libcontainerssh/internal/geoip/dummy"
@@ -37,7 +38,7 @@ type AuthenticationContext interface {
3738
// Error returns the error that happened during the authentication.
3839
Error() error
3940
// Metadata returns a set of metadata entries that have been obtained during the authentication.
40-
Metadata() map[string]string
41+
Metadata() *auth2.ConnectionMetadata
4142
}
4243

4344
// NewTestClient creates a new copy of a client usable for testing purposes.

auth/webhook/handler_factory.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package webhook
2+
3+
import (
4+
"net/http"
5+
6+
"github.com/containerssh/libcontainerssh/internal/auth"
7+
"github.com/containerssh/libcontainerssh/log"
8+
)
9+
10+
// NewHandler creates a HTTP handler that forwards calls to the provided h config request handler.
11+
func NewHandler(h AuthRequestHandler, logger log.Logger) http.Handler {
12+
return auth.NewHandler(h, logger)
13+
}

auth/webhook/server_test.go

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"os"
66
"time"
77

8+
"github.com/containerssh/libcontainerssh/auth"
89
"github.com/containerssh/libcontainerssh/auth/webhook"
910
"github.com/containerssh/libcontainerssh/config"
1011
"github.com/containerssh/libcontainerssh/log"
@@ -23,7 +24,7 @@ func (m *myAuthReqHandler) OnPassword(
2324
connectionID string,
2425
) (
2526
success bool,
26-
metadata map[string]string,
27+
metadata *auth.ConnectionMetadata,
2728
err error,
2829
) {
2930
return true, nil, nil
@@ -37,7 +38,21 @@ func (m *myAuthReqHandler) OnPubKey(
3738
connectionID string,
3839
) (
3940
success bool,
40-
metadata map[string]string,
41+
metadata *auth.ConnectionMetadata,
42+
err error,
43+
) {
44+
return true, nil, nil
45+
}
46+
47+
// OnAuthorization will be called after login in non-webhook auth handlers to verify the user is authorized to login
48+
func (m *myAuthReqHandler) OnAuthorization(
49+
principalUsername string,
50+
loginUsername string,
51+
remoteAddress string,
52+
connectionID string,
53+
) (
54+
success bool,
55+
metadata *auth.ConnectionMetadata,
4156
err error,
4257
) {
4358
return true, nil, nil

cmd/containerssh-testauthconfigserver/main.go

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"os/signal"
2525
"syscall"
2626

27+
auth2 "github.com/containerssh/libcontainerssh/auth"
2728
"github.com/containerssh/libcontainerssh/config"
2829
configWebhook "github.com/containerssh/libcontainerssh/config/webhook"
2930
"github.com/containerssh/libcontainerssh/http"
@@ -53,7 +54,7 @@ type authHandler struct {
5354
// "$ref": "#/responses/AuthResponse"
5455
func (a *authHandler) OnPassword(Username string, _ []byte, _ string, _ string) (
5556
bool,
56-
map[string]string,
57+
*auth2.ConnectionMetadata,
5758
error,
5859
) {
5960
if os.Getenv("CONTAINERSSH_ALLOW_ALL") == "1" ||
@@ -81,7 +82,7 @@ func (a *authHandler) OnPassword(Username string, _ []byte, _ string, _ string)
8182
// "$ref": "#/responses/AuthResponse"
8283
func (a *authHandler) OnPubKey(Username string, _ string, _ string, _ string) (
8384
bool,
84-
map[string]string,
85+
*auth2.ConnectionMetadata,
8586
error,
8687
) {
8788
if Username == "foo" || Username == "busybox" {
@@ -90,6 +91,32 @@ func (a *authHandler) OnPubKey(Username string, _ string, _ string, _ string) (
9091
return false, nil, nil
9192
}
9293

94+
// swagger:operation POST /authz Authentication authz
95+
//
96+
// Authorization
97+
//
98+
// ---
99+
// parameters:
100+
// - name: request
101+
// in: body
102+
// description: The authorization request
103+
// required: true
104+
// schema:
105+
// "$ref": "#/definitions/AuthorizationRequest"
106+
// responses:
107+
// "200":
108+
// "$ref": "#/responses/AuthResponse"
109+
func (a *authHandler) OnAuthorization(PrincipalUsername string, _ string,_ string, _ string) (
110+
bool,
111+
*auth2.ConnectionMetadata,
112+
error,
113+
) {
114+
if PrincipalUsername == "foo" || PrincipalUsername == "busybox" {
115+
return true, nil, nil
116+
}
117+
return false, nil, nil
118+
}
119+
93120
type configHandler struct {
94121
}
95122

config/auth.go

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,14 @@ type AuthConfig struct {
1919
// OAuth2 is the configuration for OAuth2 authentication via Keyboard-Interactive.
2020
OAuth2 AuthOAuth2ClientConfig `json:"oauth2" yaml:"oauth2"`
2121

22+
// Kerberos is the configuration for Kerberos authentication via GSSAPI and/or password.
23+
Kerberos AuthKerberosClientConfig `json:"kerberos" yaml:"kerberos"`
24+
25+
// Authz is the authorization configuration. The authorization server
26+
// will receive a webhook after successful user authentication to
27+
// determine whether the specified user has access to the service.
28+
Authz AuthzConfig `json:"authz" yaml:"authz"`
29+
2230
// AuthTimeout is the timeout for the overall authentication call (e.g. verifying a password). If the server
2331
// responds with a non-200 response the call will be retried until this timeout is reached. This timeout
2432
// should be increased to ~180s for OAuth2 login.
@@ -56,22 +64,28 @@ func (c *AuthConfig) Validate() error {
5664
err = c.Webhook.Validate()
5765
case AuthMethodOAuth2:
5866
err = c.OAuth2.Validate()
67+
case AuthMethodKerberos:
68+
err = c.Kerberos.Validate()
5969
default:
6070
return fmt.Errorf("invalid method: %s", c.Method)
6171
}
62-
6372
if err != nil {
6473
return fmt.Errorf("invalid %s client configuration (%w)", c.Method, err)
6574
}
6675

76+
err = c.Authz.Validate()
77+
if err != nil {
78+
return fmt.Errorf("Invalid authz configuration (%w)", err)
79+
}
80+
6781
return nil
6882
}
6983

7084
type AuthMethod string
7185

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

100+
const AuthMethodKerberos AuthMethod = "kerberos"
101+
86102
// AuthWebhookClientConfig is the configuration for webhook authentication.
87103
type AuthWebhookClientConfig struct {
88104
HTTPClientConfiguration `json:",inline" yaml:",inline"`
@@ -293,3 +309,60 @@ func (o *AuthOIDCConfig) Validate() error {
293309
}
294310
return o.HTTPClientConfiguration.Validate()
295311
}
312+
313+
// AuthzConfig is the configuration for the authorization flow
314+
type AuthzConfig struct {
315+
HTTPClientConfiguration `json:",inline" yaml:",inline"`
316+
// Controls whether the authorization flow is enabled. If set to false
317+
// all authenticated users are allowed in the service.
318+
Enable bool `json:"enable" yaml:"enable" default:"false"`
319+
}
320+
321+
func (k *AuthzConfig) Validate() error {
322+
if k.Enable {
323+
return k.HTTPClientConfiguration.Validate()
324+
}
325+
return nil
326+
}
327+
328+
// AuthKerberosClientConfig is the configuration for the Kerberos authentication method.
329+
type AuthKerberosClientConfig struct {
330+
// Keytab is the path to the kerberos keytab. If unset it defaults to
331+
// the default of /etc/krb5.keytab. If this file doesn't exist and
332+
// kerberos authentication is requested ContainerSSH will fail to start
333+
Keytab string `json:"keytab" yaml:"keytab" default:"/etc/krb5.keytab"`
334+
// Acceptor is the name of the keytab entry to authenticate against.
335+
// The value of this field needs to be in the form of `service/name`.
336+
//
337+
// The special value of `host` will authenticate clients only against
338+
// the service `host/hostname` where hostname is the system hostname
339+
// The special value of 'any' will authenticate against all keytab
340+
// entries regardless of name
341+
Acceptor string `json:"acceptor" yaml:"acceptor" default:"any"`
342+
// EnforceUsername specifies whether to check that the username of the
343+
// authenticated user matches the SSH username entered. If set to false
344+
// the authorization server must be responsible for ensuring proper
345+
// access control.
346+
//
347+
// WARNING: If authorization is unset and this is set to false all
348+
// authenticated users can log in to any account!
349+
EnforceUsername bool `json:"enforceUsername" yaml:"enforceUsername" default:"true"`
350+
// CredentialCachePath is the path in which the kerberos credentials
351+
// will be written inside the user containers.
352+
CredentialCachePath string `json:"credentialCachePath" yaml:"credentialCachePath" default:"/tmp/krb5cc"`
353+
// AllowPassword controls whether kerberos-based password
354+
// authentication should be allowed. If set to false only GSSAPI
355+
// authentication will be permitted
356+
AllowPassword bool `json:"allowPassword" yaml:"allowPassword" default:"true"`
357+
// ConfigPath is the path of the kerberos configuration file. This is
358+
// only used for password authentication.
359+
ConfigPath string `json:"configPath" yaml:"configPath" default:"/etc/containerssh/krb5.conf"`
360+
// ClockSkew is the maximum allowed clock skew for kerberos messages,
361+
// any messages older than this will be rejected. This value is also
362+
// used for the replay cache.
363+
ClockSkew time.Duration `json:"clockSkew" yaml:"clockSkew" default:"5m"`
364+
}
365+
366+
func (k *AuthKerberosClientConfig) Validate() error {
367+
return nil
368+
}

config/protocol.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
package config
22

3+
import (
4+
"github.com/containerssh/libcontainerssh/auth"
5+
)
6+
37
// Request is the request object passed from the client to the config server.
48
//
59
// swagger:model Request
@@ -23,7 +27,7 @@ type Request struct {
2327
// Metadata is the metadata received from the authentication server.
2428
//
2529
// required: false
26-
Metadata map[string]string `json:"metadata"`
30+
Metadata *auth.ConnectionMetadata `json:"metadata"`
2731
}
2832

2933
// ResponseBody is the structure representing the JSON HTTP response.

0 commit comments

Comments
 (0)