From 4a8e00841281a2af7fe541b8694ef2493579ef2e Mon Sep 17 00:00:00 2001 From: Gerrit Date: Wed, 12 Jun 2024 11:20:38 +0200 Subject: [PATCH 01/15] Allow admins to access machines independent of allocation state. --- internal/console/server.go | 130 ++++++++++++++++++------------------- 1 file changed, 62 insertions(+), 68 deletions(-) diff --git a/internal/console/server.go b/internal/console/server.go index 97f53ba..04eace9 100644 --- a/internal/console/server.go +++ b/internal/console/server.go @@ -8,7 +8,6 @@ import ( "io" "log/slog" "os" - "strings" "sync" "time" @@ -37,20 +36,13 @@ func NewServer(log *slog.Logger, spec *Specification, client metalgo.Client) *co } } -func (cs *consoleServer) userClient(token string) (metalgo.Client, error) { - client, err := metalgo.NewDriver(cs.spec.MetalAPIURL, token, "") - if err != nil { - return nil, err - } - return client, nil -} - // Run starts ssh server and listen for console connections. func (cs *consoleServer) Run() error { s := &ssh.Server{ Addr: fmt.Sprintf(":%d", cs.spec.Port), Handler: cs.sessionHandler, - PublicKeyHandler: cs.authHandler, + PublicKeyHandler: cs.publicKeyHandler, + PasswordHandler: cs.passwordHandler, } hostKey, err := loadHostKey() @@ -80,51 +72,6 @@ func (cs *consoleServer) sessionHandler(s ssh.Session) { } m := resp.Payload - // If the machine is a firewall - // check if the ssh session contains the oidc token and the user is member of admin group - // ssh client can pass environment variables, but only environment variables starting with LC_ are passed - // OIDC token must be stored in LC_METAL_STACK_OIDC_TOKEN - if m.Allocation != nil && m.Allocation.Role != nil && *m.Allocation.Role == models.V1MachineAllocationRoleFirewall { - token := "" - for _, env := range s.Environ() { - _, t, found := strings.Cut(env, oidcEnv+"=") - if found { - token = t - break - } - } - if token == "" { - _, _ = io.WriteString(s, fmt.Sprintf("unable to find OIDC token stored in %s env variable which is required for firewall console access\n", oidcEnv)) - cs.exitSession(s) - return - } - uc, err := cs.userClient(token) - if err != nil { - _, _ = io.WriteString(s, "technical error\n") - cs.log.Error("failed to create user client", "error", err) - cs.exitSession(s) - return - } - user, err := uc.User().GetMe(user.NewGetMeParams(), nil) - if err != nil { - _, _ = io.WriteString(s, "given oidc token is invalid\n") - cs.log.Error("failed to fetch user details from oidc token", "machineID", machineID, "error", err) - cs.exitSession(s) - return - } - - isAdmin := false - for _, g := range user.Payload.Groups { - if g == cs.spec.AdminGroupName { - isAdmin = true - } - } - if !isAdmin { - _, _ = io.WriteString(s, fmt.Sprintf("you are not member of required admin group:%s to access this machine console\n", cs.spec.AdminGroupName)) - cs.exitSession(s) - return - } - } mgmtServiceAddress := m.Partition.Mgmtserviceaddress @@ -338,10 +285,24 @@ func (cs *consoleServer) connectToManagementNetwork(mgmtServiceAddress string) ( return tcpConn, nil } -func (cs *consoleServer) authHandler(ctx ssh.Context, publicKey ssh.PublicKey) bool { +func (cs *consoleServer) publicKeyHandler(ctx ssh.Context, publicKey ssh.PublicKey) bool { machineID := ctx.User() + + resp, err := cs.client.Machine().FindMachine(machine.NewFindMachineParams().WithID(machineID), nil) + if err != nil { + cs.log.Error("failed to fetch requested machine", "machineID", machineID, "error", err) + return false + } + + m := resp.Payload + + if m == nil || m.Allocation == nil || m.Allocation.Role == nil || *m.Allocation.Role != models.V1MachineAllocationRoleMachine { + cs.log.Error("only access to machines is allowed", "machineID", machineID) + return false + } + cs.log.Info("authHandler", "publicKey", publicKey) - knownAuthorizedKeys, err := cs.getAuthorizedKeysForMachine(machineID) + knownAuthorizedKeys, err := cs.getAuthorizedKeysForMachine(resp.Payload) if err != nil { cs.log.Error("abort establishment of console session", "machineID", machineID, "error", err) return false @@ -353,21 +314,15 @@ func (cs *consoleServer) authHandler(ctx ssh.Context, publicKey ssh.PublicKey) b return true } } + cs.log.Warn("no matching authorized key found", "machineID", machineID) + return false } -func (cs *consoleServer) getAuthorizedKeysForMachine(machineID string) ([]ssh.PublicKey, error) { - resp, err := cs.client.Machine().FindMachine(machine.NewFindMachineParams().WithID(machineID), nil) - if err != nil { - cs.log.Error("failed to fetch requested machine", "machineID", machineID, "error", err) - return nil, err - } - if resp.Payload == nil || resp.Payload.Allocation == nil { - cs.log.Error("requested machine is nil", "machineID", machineID) - return nil, fmt.Errorf("no machine found with id: %s", machineID) - } - alloc := resp.Payload.Allocation +func (cs *consoleServer) getAuthorizedKeysForMachine(m *models.V1MachineResponse) ([]ssh.PublicKey, error) { + machineID := *m.ID + alloc := m.Allocation cs.createdAts.Store(machineID, alloc.Created.String()) @@ -410,3 +365,42 @@ func loadPublicHostKey() (gossh.PublicKey, error) { pubKey, _, _, _, err := ssh.ParseAuthorizedKey(bb) return pubKey, err } + +func (cs *consoleServer) passwordHandler(ctx ssh.Context, password string) bool { + err := cs.checkIsAdmin(ctx.User(), password) + if err != nil { + cs.log.Error("user is not an admin", "error", err) + return false + } + + return true +} + +func (cs *consoleServer) checkIsAdmin(machineID string, token string) error { + if token == "" { + return fmt.Errorf("unable to find OIDC token stored in %s env variable which is required for machine console access\n", oidcEnv) + } + + metal, err := metalgo.NewDriver(cs.spec.MetalAPIURL, token, "") + if err != nil { + return fmt.Errorf("failed to create metal client: %w", err) + } + + user, err := metal.User().GetMe(user.NewGetMeParams(), nil) + if err != nil { + cs.log.Error("failed to fetch user details from oidc token", "machineID", machineID, "error", err) + return fmt.Errorf("given oidc token is invalid") + } + + isAdmin := false + for _, g := range user.Payload.Groups { + if g == cs.spec.AdminGroupName { + isAdmin = true + } + } + if !isAdmin { + return fmt.Errorf("you are not member of required admin group:%s to access this machine console", cs.spec.AdminGroupName) + } + + return nil +} From 80641743e481dfabd0558b1baee990c7ad051a1c Mon Sep 17 00:00:00 2001 From: Gerrit Date: Wed, 12 Jun 2024 11:39:17 +0200 Subject: [PATCH 02/15] Next attempt. --- internal/console/server.go | 70 ++++++++++++++++++++++++-------------- 1 file changed, 44 insertions(+), 26 deletions(-) diff --git a/internal/console/server.go b/internal/console/server.go index 04eace9..c0e24ce 100644 --- a/internal/console/server.go +++ b/internal/console/server.go @@ -8,6 +8,7 @@ import ( "io" "log/slog" "os" + "strings" "sync" "time" @@ -73,6 +74,28 @@ func (cs *consoleServer) sessionHandler(s ssh.Session) { m := resp.Payload + // If the machine is a firewall or not allocated + // check if the ssh session contains the oidc token and the user is member of admin group + // ssh client can pass environment variables, but only environment variables starting with LC_ are passed + // OIDC token must be stored in LC_METAL_STACK_OIDC_TOKEN + if (m.Allocation == nil) || (m.Allocation != nil && m.Allocation.Role != nil && *m.Allocation.Role == models.V1MachineAllocationRoleFirewall) { + token := "" + for _, env := range s.Environ() { + _, t, found := strings.Cut(env, oidcEnv+"=") + if found { + token = t + break + } + } + + _, err := cs.checkIsAdmin(machineID, token) + if err != nil { + _, _ = io.WriteString(s, err.Error()) + cs.exitSession(s) + return + } + } + mgmtServiceAddress := m.Partition.Mgmtserviceaddress if cs.spec.DevMode() { @@ -288,21 +311,8 @@ func (cs *consoleServer) connectToManagementNetwork(mgmtServiceAddress string) ( func (cs *consoleServer) publicKeyHandler(ctx ssh.Context, publicKey ssh.PublicKey) bool { machineID := ctx.User() - resp, err := cs.client.Machine().FindMachine(machine.NewFindMachineParams().WithID(machineID), nil) - if err != nil { - cs.log.Error("failed to fetch requested machine", "machineID", machineID, "error", err) - return false - } - - m := resp.Payload - - if m == nil || m.Allocation == nil || m.Allocation.Role == nil || *m.Allocation.Role != models.V1MachineAllocationRoleMachine { - cs.log.Error("only access to machines is allowed", "machineID", machineID) - return false - } - cs.log.Info("authHandler", "publicKey", publicKey) - knownAuthorizedKeys, err := cs.getAuthorizedKeysForMachine(resp.Payload) + knownAuthorizedKeys, err := cs.getAuthorizedKeysForMachine(machineID) if err != nil { cs.log.Error("abort establishment of console session", "machineID", machineID, "error", err) return false @@ -320,9 +330,17 @@ func (cs *consoleServer) publicKeyHandler(ctx ssh.Context, publicKey ssh.PublicK return false } -func (cs *consoleServer) getAuthorizedKeysForMachine(m *models.V1MachineResponse) ([]ssh.PublicKey, error) { - machineID := *m.ID - alloc := m.Allocation +func (cs *consoleServer) getAuthorizedKeysForMachine(machineID string) ([]ssh.PublicKey, error) { + resp, err := cs.client.Machine().FindMachine(machine.NewFindMachineParams().WithID(machineID), nil) + if err != nil { + cs.log.Error("failed to fetch requested machine", "machineID", machineID, "error", err) + return nil, err + } + if resp.Payload == nil || resp.Payload.Allocation == nil { + cs.log.Error("requested machine is nil", "machineID", machineID) + return nil, fmt.Errorf("no machine found with id: %s", machineID) + } + alloc := resp.Payload.Allocation cs.createdAts.Store(machineID, alloc.Created.String()) @@ -367,29 +385,29 @@ func loadPublicHostKey() (gossh.PublicKey, error) { } func (cs *consoleServer) passwordHandler(ctx ssh.Context, password string) bool { - err := cs.checkIsAdmin(ctx.User(), password) + isAdmin, err := cs.checkIsAdmin(ctx.User(), password) if err != nil { - cs.log.Error("user is not an admin", "error", err) + cs.log.Error("error evaluating if user is admin", "error", err) return false } - return true + return isAdmin } -func (cs *consoleServer) checkIsAdmin(machineID string, token string) error { +func (cs *consoleServer) checkIsAdmin(machineID string, token string) (bool, error) { if token == "" { - return fmt.Errorf("unable to find OIDC token stored in %s env variable which is required for machine console access\n", oidcEnv) + return false, fmt.Errorf("unable to find OIDC token stored in %s env variable which is required for machine console access\n", oidcEnv) } metal, err := metalgo.NewDriver(cs.spec.MetalAPIURL, token, "") if err != nil { - return fmt.Errorf("failed to create metal client: %w", err) + return false, fmt.Errorf("failed to create metal client: %w", err) } user, err := metal.User().GetMe(user.NewGetMeParams(), nil) if err != nil { cs.log.Error("failed to fetch user details from oidc token", "machineID", machineID, "error", err) - return fmt.Errorf("given oidc token is invalid") + return false, fmt.Errorf("given oidc token is invalid") } isAdmin := false @@ -399,8 +417,8 @@ func (cs *consoleServer) checkIsAdmin(machineID string, token string) error { } } if !isAdmin { - return fmt.Errorf("you are not member of required admin group:%s to access this machine console", cs.spec.AdminGroupName) + return false, fmt.Errorf("you are not member of required admin group:%s to access this machine console", cs.spec.AdminGroupName) } - return nil + return true, nil } From 0de9e0517baf57e695c7c28d9d80c28055697ea3 Mon Sep 17 00:00:00 2001 From: Gerrit Date: Wed, 12 Jun 2024 12:42:02 +0200 Subject: [PATCH 03/15] Debug. --- internal/console/server.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/console/server.go b/internal/console/server.go index c0e24ce..8ccbd07 100644 --- a/internal/console/server.go +++ b/internal/console/server.go @@ -90,7 +90,7 @@ func (cs *consoleServer) sessionHandler(s ssh.Session) { _, err := cs.checkIsAdmin(machineID, token) if err != nil { - _, _ = io.WriteString(s, err.Error()) + _, _ = io.WriteString(s, err.Error()+"\n") cs.exitSession(s) return } @@ -396,7 +396,7 @@ func (cs *consoleServer) passwordHandler(ctx ssh.Context, password string) bool func (cs *consoleServer) checkIsAdmin(machineID string, token string) (bool, error) { if token == "" { - return false, fmt.Errorf("unable to find OIDC token stored in %s env variable which is required for machine console access\n", oidcEnv) + return false, fmt.Errorf("unable to find OIDC token stored in %s env variable which is required for machine console access", oidcEnv) } metal, err := metalgo.NewDriver(cs.spec.MetalAPIURL, token, "") @@ -406,7 +406,7 @@ func (cs *consoleServer) checkIsAdmin(machineID string, token string) (bool, err user, err := metal.User().GetMe(user.NewGetMeParams(), nil) if err != nil { - cs.log.Error("failed to fetch user details from oidc token", "machineID", machineID, "error", err) + cs.log.Error("failed to fetch user details from oidc token", "machineID", machineID, "error", err, "token", token) return false, fmt.Errorf("given oidc token is invalid") } From 4525c165920a7c5ed9b162e70a9e7df8026d69e3 Mon Sep 17 00:00:00 2001 From: Gerrit Date: Wed, 12 Jun 2024 13:00:41 +0200 Subject: [PATCH 04/15] Workaround password auth. --- internal/console/server.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/internal/console/server.go b/internal/console/server.go index 8ccbd07..a570e1e 100644 --- a/internal/console/server.go +++ b/internal/console/server.go @@ -73,6 +73,7 @@ func (cs *consoleServer) sessionHandler(s ssh.Session) { } m := resp.Payload + var isAdmin bool // If the machine is a firewall or not allocated // check if the ssh session contains the oidc token and the user is member of admin group @@ -88,7 +89,7 @@ func (cs *consoleServer) sessionHandler(s ssh.Session) { } } - _, err := cs.checkIsAdmin(machineID, token) + isAdmin, err = cs.checkIsAdmin(machineID, token) if err != nil { _, _ = io.WriteString(s, err.Error()+"\n") cs.exitSession(s) @@ -127,7 +128,9 @@ func (cs *consoleServer) sessionHandler(s ssh.Session) { cs.redirectIO(s, sshSession, done) // check periodically if the session is still allowed. - go cs.terminateIfPublicKeysChanged(s) + if !isAdmin { + go cs.terminateIfPublicKeysChanged(s) + } err = sshSession.Start("bash") if err != nil { From d3e46359ebad71f08d1dd78fa178bdf85b8a87c1 Mon Sep 17 00:00:00 2001 From: Gerrit Date: Wed, 12 Jun 2024 13:32:49 +0200 Subject: [PATCH 05/15] Use public key from session in handler. --- internal/console/server.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/internal/console/server.go b/internal/console/server.go index a570e1e..7e634fb 100644 --- a/internal/console/server.go +++ b/internal/console/server.go @@ -73,13 +73,12 @@ func (cs *consoleServer) sessionHandler(s ssh.Session) { } m := resp.Payload - var isAdmin bool // If the machine is a firewall or not allocated // check if the ssh session contains the oidc token and the user is member of admin group // ssh client can pass environment variables, but only environment variables starting with LC_ are passed // OIDC token must be stored in LC_METAL_STACK_OIDC_TOKEN - if (m.Allocation == nil) || (m.Allocation != nil && m.Allocation.Role != nil && *m.Allocation.Role == models.V1MachineAllocationRoleFirewall) { + if s.PublicKey() == nil || (m.Allocation != nil && m.Allocation.Role != nil && *m.Allocation.Role == models.V1MachineAllocationRoleFirewall) { token := "" for _, env := range s.Environ() { _, t, found := strings.Cut(env, oidcEnv+"=") @@ -89,7 +88,7 @@ func (cs *consoleServer) sessionHandler(s ssh.Session) { } } - isAdmin, err = cs.checkIsAdmin(machineID, token) + _, err = cs.checkIsAdmin(machineID, token) if err != nil { _, _ = io.WriteString(s, err.Error()+"\n") cs.exitSession(s) @@ -128,7 +127,7 @@ func (cs *consoleServer) sessionHandler(s ssh.Session) { cs.redirectIO(s, sshSession, done) // check periodically if the session is still allowed. - if !isAdmin { + if s.PublicKey() != nil { go cs.terminateIfPublicKeysChanged(s) } From 0925cc56d7bec3665acdfb36144462d8ea019bde Mon Sep 17 00:00:00 2001 From: Gerrit Date: Thu, 13 Jun 2024 09:29:31 +0200 Subject: [PATCH 06/15] Hopefully easier to read. --- internal/console/server.go | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/internal/console/server.go b/internal/console/server.go index 7e634fb..2967070 100644 --- a/internal/console/server.go +++ b/internal/console/server.go @@ -74,21 +74,13 @@ func (cs *consoleServer) sessionHandler(s ssh.Session) { m := resp.Payload - // If the machine is a firewall or not allocated - // check if the ssh session contains the oidc token and the user is member of admin group - // ssh client can pass environment variables, but only environment variables starting with LC_ are passed - // OIDC token must be stored in LC_METAL_STACK_OIDC_TOKEN - if s.PublicKey() == nil || (m.Allocation != nil && m.Allocation.Role != nil && *m.Allocation.Role == models.V1MachineAllocationRoleFirewall) { - token := "" - for _, env := range s.Environ() { - _, t, found := strings.Cut(env, oidcEnv+"=") - if found { - token = t - break - } - } - - _, err = cs.checkIsAdmin(machineID, token) + switch { + case m.Allocation != nil && m.Allocation.Role != nil && *m.Allocation.Role != models.V1MachineAllocationRoleMachine, s.PublicKey() == nil: + // If the machine is a not a regular machine, i.e. a firewall, or an admin wants access to an arbitrary machine + // check if the ssh session contains the oidc token and the user is member of admin group + // ssh client can pass environment variables, but only environment variables starting with LC_ are passed + // OIDC token must be stored in LC_METAL_STACK_OIDC_TOKEN + _, err = cs.checkIsAdmin(machineID, oidcTokenFromSessionEnv(s)) if err != nil { _, _ = io.WriteString(s, err.Error()+"\n") cs.exitSession(s) @@ -141,6 +133,17 @@ func (cs *consoleServer) sessionHandler(s ssh.Session) { <-done } +func oidcTokenFromSessionEnv(s ssh.Session) string { + for _, env := range s.Environ() { + _, t, found := strings.Cut(env, oidcEnv+"=") + if found { + return t + } + } + + return "" +} + func (cs *consoleServer) terminateIfPublicKeysChanged(s ssh.Session) { machineID := s.User() createdAt, ok := cs.createdAts.Load(machineID) From a9128c5ddc61d08414b5aff52d2a9b7b84c0293b Mon Sep 17 00:00:00 2001 From: Gerrit Date: Thu, 13 Jun 2024 09:39:22 +0200 Subject: [PATCH 07/15] Comment. --- internal/console/server.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/console/server.go b/internal/console/server.go index 2967070..6373da1 100644 --- a/internal/console/server.go +++ b/internal/console/server.go @@ -119,6 +119,7 @@ func (cs *consoleServer) sessionHandler(s ssh.Session) { cs.redirectIO(s, sshSession, done) // check periodically if the session is still allowed. + // this is only required when public key authorization was used. if s.PublicKey() != nil { go cs.terminateIfPublicKeysChanged(s) } From cdba9aeefebf7cc24c9c93a6d00b9b511c8daa31 Mon Sep 17 00:00:00 2001 From: Gerrit Date: Thu, 13 Jun 2024 10:31:41 +0200 Subject: [PATCH 08/15] Review. --- go.mod | 2 +- internal/console/server.go | 27 ++++++++++++++------------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/go.mod b/go.mod index 2a305fb..7d885df 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/gliderlabs/ssh v0.3.7 github.com/kelseyhightower/envconfig v1.4.0 github.com/metal-stack/metal-go v0.31.2 + github.com/metal-stack/metal-lib v0.16.3 github.com/metal-stack/v v1.0.3 github.com/stretchr/testify v1.9.0 golang.org/x/crypto v0.24.0 @@ -42,7 +43,6 @@ require ( github.com/lestrrat-go/jwx/v2 v2.0.21 // indirect github.com/lestrrat-go/option v1.0.1 // indirect github.com/mailru/easyjson v0.7.7 // indirect - github.com/metal-stack/metal-lib v0.16.3 // indirect github.com/metal-stack/security v0.8.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/oklog/ulid v1.3.1 // indirect diff --git a/internal/console/server.go b/internal/console/server.go index 6373da1..462542c 100644 --- a/internal/console/server.go +++ b/internal/console/server.go @@ -16,6 +16,7 @@ import ( "github.com/metal-stack/metal-go/api/client/machine" "github.com/metal-stack/metal-go/api/client/user" "github.com/metal-stack/metal-go/api/models" + "github.com/metal-stack/metal-lib/pkg/pointer" "github.com/gliderlabs/ssh" gossh "golang.org/x/crypto/ssh" @@ -73,9 +74,9 @@ func (cs *consoleServer) sessionHandler(s ssh.Session) { } m := resp.Payload + role := pointer.SafeDeref(pointer.SafeDeref(m.Allocation).Role) - switch { - case m.Allocation != nil && m.Allocation.Role != nil && *m.Allocation.Role != models.V1MachineAllocationRoleMachine, s.PublicKey() == nil: + if role != models.V1MachineAllocationRoleMachine || s.PublicKey() == nil { // If the machine is a not a regular machine, i.e. a firewall, or an admin wants access to an arbitrary machine // check if the ssh session contains the oidc token and the user is member of admin group // ssh client can pass environment variables, but only environment variables starting with LC_ are passed @@ -134,17 +135,6 @@ func (cs *consoleServer) sessionHandler(s ssh.Session) { <-done } -func oidcTokenFromSessionEnv(s ssh.Session) string { - for _, env := range s.Environ() { - _, t, found := strings.Cut(env, oidcEnv+"=") - if found { - return t - } - } - - return "" -} - func (cs *consoleServer) terminateIfPublicKeysChanged(s ssh.Session) { machineID := s.User() createdAt, ok := cs.createdAts.Load(machineID) @@ -400,6 +390,17 @@ func (cs *consoleServer) passwordHandler(ctx ssh.Context, password string) bool return isAdmin } +func oidcTokenFromSessionEnv(s ssh.Session) string { + for _, env := range s.Environ() { + _, t, found := strings.Cut(env, oidcEnv+"=") + if found { + return t + } + } + + return "" +} + func (cs *consoleServer) checkIsAdmin(machineID string, token string) (bool, error) { if token == "" { return false, fmt.Errorf("unable to find OIDC token stored in %s env variable which is required for machine console access", oidcEnv) From 7ced5d11cc0e1f4fa15303f1b6f76bf77d4d8dbd Mon Sep 17 00:00:00 2001 From: Gerrit Date: Thu, 13 Jun 2024 10:34:00 +0200 Subject: [PATCH 09/15] Log access attempt. --- internal/console/server.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/console/server.go b/internal/console/server.go index 462542c..6c62eb2 100644 --- a/internal/console/server.go +++ b/internal/console/server.go @@ -83,6 +83,7 @@ func (cs *consoleServer) sessionHandler(s ssh.Session) { // OIDC token must be stored in LC_METAL_STACK_OIDC_TOKEN _, err = cs.checkIsAdmin(machineID, oidcTokenFromSessionEnv(s)) if err != nil { + cs.log.Error("prevented admin access to a machine console", "machineID", machineID, "role", role, "error", err, "from", s.RemoteAddr()) _, _ = io.WriteString(s, err.Error()+"\n") cs.exitSession(s) return From 9f0285491ed4de57b0ddeddfca5e3d1054284da7 Mon Sep 17 00:00:00 2001 From: Gerrit Date: Thu, 13 Jun 2024 10:44:35 +0200 Subject: [PATCH 10/15] More logging. --- internal/console/server.go | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/internal/console/server.go b/internal/console/server.go index 6c62eb2..099002f 100644 --- a/internal/console/server.go +++ b/internal/console/server.go @@ -12,6 +12,7 @@ import ( "sync" "time" + "github.com/golang-jwt/jwt/v4" metalgo "github.com/metal-stack/metal-go" "github.com/metal-stack/metal-go/api/client/machine" "github.com/metal-stack/metal-go/api/client/user" @@ -81,13 +82,17 @@ func (cs *consoleServer) sessionHandler(s ssh.Session) { // check if the ssh session contains the oidc token and the user is member of admin group // ssh client can pass environment variables, but only environment variables starting with LC_ are passed // OIDC token must be stored in LC_METAL_STACK_OIDC_TOKEN - _, err = cs.checkIsAdmin(machineID, oidcTokenFromSessionEnv(s)) + _, claims, err := cs.checkIsAdmin(machineID, oidcTokenFromSessionEnv(s)) if err != nil { - cs.log.Error("prevented admin access to a machine console", "machineID", machineID, "role", role, "error", err, "from", s.RemoteAddr()) + cs.log.Error("prevented admin access to a machine console", "machineID", machineID, "role", role, "claims", claims, "from", s.RemoteAddr(), "error", err) _, _ = io.WriteString(s, err.Error()+"\n") cs.exitSession(s) return } + + cs.log.Info("allowed admin access to a machine console", "machineID", machineID, "role", role, "claims", claims, "from", s.RemoteAddr()) + } else { + cs.log.Info("allowed user access to a machine", "machineID", machineID, "role", role, "from", s.RemoteAddr()) } mgmtServiceAddress := m.Partition.Mgmtserviceaddress @@ -382,7 +387,7 @@ func loadPublicHostKey() (gossh.PublicKey, error) { } func (cs *consoleServer) passwordHandler(ctx ssh.Context, password string) bool { - isAdmin, err := cs.checkIsAdmin(ctx.User(), password) + isAdmin, _, err := cs.checkIsAdmin(ctx.User(), password) if err != nil { cs.log.Error("error evaluating if user is admin", "error", err) return false @@ -402,20 +407,26 @@ func oidcTokenFromSessionEnv(s ssh.Session) string { return "" } -func (cs *consoleServer) checkIsAdmin(machineID string, token string) (bool, error) { +func (cs *consoleServer) checkIsAdmin(machineID string, token string) (bool, jwt.Claims, error) { if token == "" { - return false, fmt.Errorf("unable to find OIDC token stored in %s env variable which is required for machine console access", oidcEnv) + return false, nil, fmt.Errorf("unable to find OIDC token stored in %s env variable which is required for machine console access", oidcEnv) + } + + claims := &jwt.MapClaims{} + _, _, err := new(jwt.Parser).ParseUnverified(string(token), claims) + if err != nil { + return false, nil, fmt.Errorf("unable to parse jwt: %w", err) } metal, err := metalgo.NewDriver(cs.spec.MetalAPIURL, token, "") if err != nil { - return false, fmt.Errorf("failed to create metal client: %w", err) + return false, claims, fmt.Errorf("failed to create metal client: %w", err) } user, err := metal.User().GetMe(user.NewGetMeParams(), nil) if err != nil { cs.log.Error("failed to fetch user details from oidc token", "machineID", machineID, "error", err, "token", token) - return false, fmt.Errorf("given oidc token is invalid") + return false, claims, fmt.Errorf("given oidc token is invalid") } isAdmin := false @@ -425,8 +436,8 @@ func (cs *consoleServer) checkIsAdmin(machineID string, token string) (bool, err } } if !isAdmin { - return false, fmt.Errorf("you are not member of required admin group:%s to access this machine console", cs.spec.AdminGroupName) + return false, claims, fmt.Errorf("you are not member of required admin group:%s to access this machine console", cs.spec.AdminGroupName) } - return true, nil + return true, claims, nil } From 8e8358a2ffbcfe2ce71e4c11ce8f6be5b5a164ca Mon Sep 17 00:00:00 2001 From: Gerrit Date: Thu, 13 Jun 2024 10:54:29 +0200 Subject: [PATCH 11/15] Logging. --- internal/console/server.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/internal/console/server.go b/internal/console/server.go index 099002f..d26e05b 100644 --- a/internal/console/server.go +++ b/internal/console/server.go @@ -313,16 +313,17 @@ func (cs *consoleServer) connectToManagementNetwork(mgmtServiceAddress string) ( func (cs *consoleServer) publicKeyHandler(ctx ssh.Context, publicKey ssh.PublicKey) bool { machineID := ctx.User() - cs.log.Info("authHandler", "publicKey", publicKey) + cs.log.Info("authHandler", "publicKey", string(publicKey.Marshal())) + knownAuthorizedKeys, err := cs.getAuthorizedKeysForMachine(machineID) if err != nil { cs.log.Error("abort establishment of console session", "machineID", machineID, "error", err) return false } for _, key := range knownAuthorizedKeys { - cs.log.Info("authHandler", "machineID", machineID, "authorizedKey", key) same := ssh.KeysEqual(publicKey, key) if same { + cs.log.Info("authHandler found matching key for machine access", "machineID", machineID, "authorizedKey", string(key.Marshal())) return true } } From b51e49aa2dc8ac9dfa1dfc6eeac2ae3ce25d0800 Mon Sep 17 00:00:00 2001 From: Gerrit Date: Thu, 13 Jun 2024 10:59:52 +0200 Subject: [PATCH 12/15] Refinements. --- internal/console/server.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/internal/console/server.go b/internal/console/server.go index d26e05b..e469775 100644 --- a/internal/console/server.go +++ b/internal/console/server.go @@ -76,13 +76,15 @@ func (cs *consoleServer) sessionHandler(s ssh.Session) { m := resp.Payload role := pointer.SafeDeref(pointer.SafeDeref(m.Allocation).Role) + isAdmin := false if role != models.V1MachineAllocationRoleMachine || s.PublicKey() == nil { // If the machine is a not a regular machine, i.e. a firewall, or an admin wants access to an arbitrary machine // check if the ssh session contains the oidc token and the user is member of admin group // ssh client can pass environment variables, but only environment variables starting with LC_ are passed // OIDC token must be stored in LC_METAL_STACK_OIDC_TOKEN - _, claims, err := cs.checkIsAdmin(machineID, oidcTokenFromSessionEnv(s)) + var claims jwt.Claims + isAdmin, claims, err = cs.checkIsAdmin(machineID, oidcTokenFromSessionEnv(s)) if err != nil { cs.log.Error("prevented admin access to a machine console", "machineID", machineID, "role", role, "claims", claims, "from", s.RemoteAddr(), "error", err) _, _ = io.WriteString(s, err.Error()+"\n") @@ -125,9 +127,9 @@ func (cs *consoleServer) sessionHandler(s ssh.Session) { cs.redirectIO(s, sshSession, done) - // check periodically if the session is still allowed. - // this is only required when public key authorization was used. - if s.PublicKey() != nil { + if !isAdmin { + // check periodically if the session is still allowed. + // admins don't need to be disconnected from machines go cs.terminateIfPublicKeysChanged(s) } From d38e5d81755e6f0a4621b57a615b49d44fcb4042 Mon Sep 17 00:00:00 2001 From: Gerrit Date: Thu, 13 Jun 2024 11:01:56 +0200 Subject: [PATCH 13/15] Logs. --- internal/console/server.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/console/server.go b/internal/console/server.go index e469775..8e2fb2a 100644 --- a/internal/console/server.go +++ b/internal/console/server.go @@ -315,7 +315,7 @@ func (cs *consoleServer) connectToManagementNetwork(mgmtServiceAddress string) ( func (cs *consoleServer) publicKeyHandler(ctx ssh.Context, publicKey ssh.PublicKey) bool { machineID := ctx.User() - cs.log.Info("authHandler", "publicKey", string(publicKey.Marshal())) + cs.log.Info("authHandler", "publicKey", publicKey) knownAuthorizedKeys, err := cs.getAuthorizedKeysForMachine(machineID) if err != nil { @@ -325,7 +325,7 @@ func (cs *consoleServer) publicKeyHandler(ctx ssh.Context, publicKey ssh.PublicK for _, key := range knownAuthorizedKeys { same := ssh.KeysEqual(publicKey, key) if same { - cs.log.Info("authHandler found matching key for machine access", "machineID", machineID, "authorizedKey", string(key.Marshal())) + cs.log.Info("authHandler found matching key for machine access", "machineID", machineID) return true } } From 2c745679c5002203f5326c8192a030562fdac31c Mon Sep 17 00:00:00 2001 From: Gerrit Date: Thu, 13 Jun 2024 11:02:23 +0200 Subject: [PATCH 14/15] Logs. --- internal/console/server.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/console/server.go b/internal/console/server.go index 8e2fb2a..bc2022b 100644 --- a/internal/console/server.go +++ b/internal/console/server.go @@ -315,7 +315,7 @@ func (cs *consoleServer) connectToManagementNetwork(mgmtServiceAddress string) ( func (cs *consoleServer) publicKeyHandler(ctx ssh.Context, publicKey ssh.PublicKey) bool { machineID := ctx.User() - cs.log.Info("authHandler", "publicKey", publicKey) + cs.log.Info("authHandler evaluating machine access", "machineID", machineID, "publicKey", publicKey) knownAuthorizedKeys, err := cs.getAuthorizedKeysForMachine(machineID) if err != nil { From 3d66930f54b2393da03af07dcaa4e7c1fc02ef7f Mon Sep 17 00:00:00 2001 From: Gerrit Date: Thu, 13 Jun 2024 11:05:08 +0200 Subject: [PATCH 15/15] Logs. --- internal/console/server.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/console/server.go b/internal/console/server.go index bc2022b..98c9a63 100644 --- a/internal/console/server.go +++ b/internal/console/server.go @@ -315,7 +315,7 @@ func (cs *consoleServer) connectToManagementNetwork(mgmtServiceAddress string) ( func (cs *consoleServer) publicKeyHandler(ctx ssh.Context, publicKey ssh.PublicKey) bool { machineID := ctx.User() - cs.log.Info("authHandler evaluating machine access", "machineID", machineID, "publicKey", publicKey) + cs.log.Info("evaluating machine console access with public key access", "machineID", machineID, "publicKey", publicKey) knownAuthorizedKeys, err := cs.getAuthorizedKeysForMachine(machineID) if err != nil { @@ -325,7 +325,7 @@ func (cs *consoleServer) publicKeyHandler(ctx ssh.Context, publicKey ssh.PublicK for _, key := range knownAuthorizedKeys { same := ssh.KeysEqual(publicKey, key) if same { - cs.log.Info("authHandler found matching key for machine access", "machineID", machineID) + cs.log.Info("found matching public key for machine access", "machineID", machineID) return true } }