diff --git a/internal/k8s/client.go b/internal/k8s/client.go index bc5fe91b..769e984b 100644 --- a/internal/k8s/client.go +++ b/internal/k8s/client.go @@ -1,3 +1,5 @@ +// Package k8s wraps a Kubernetes API client with convenience methods for +// Lagoon services. package k8s import ( diff --git a/internal/k8s/namespacedetails.go b/internal/k8s/namespacedetails.go index c82fc83c..1f8c8234 100644 --- a/internal/k8s/namespacedetails.go +++ b/internal/k8s/namespacedetails.go @@ -9,8 +9,10 @@ import ( ) const ( - projectIDLabel = "lagoon.sh/projectId" - environmentIDLabel = "lagoon.sh/environmentId" + environmentIDLabel = "lagoon.sh/environmentId" + environmentNameLabel = "lagoon.sh/environment" + projectIDLabel = "lagoon.sh/projectId" + projectNameLabel = "lagoon.sh/project" ) func intFromLabel(labels map[string]string, label string) (int, error) { @@ -22,22 +24,33 @@ func intFromLabel(labels map[string]string, label string) (int, error) { return strconv.Atoi(value) } -// NamespaceDetails gets the details for a Lagoon namespace. -// It performs some sanity checks to validate that the namespace is actually a -// Lagoon namespace. -func (c *Client) NamespaceDetails(ctx context.Context, name string) (int, int, error) { - var pid, eid int +// NamespaceDetails gets the environment ID, project ID, and project name from +// the labels on a Lagoon environment namespace for a Lagoon namespace. If one +// of the expected labels is missing or cannot be parsed, it will return an +// error. +func (c *Client) NamespaceDetails(ctx context.Context, name string) ( + int, int, string, string, error) { + var eid, pid int + var ename, pname string + var ok bool ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() ns, err := c.clientset.CoreV1().Namespaces().Get(ctx, name, metav1.GetOptions{}) if err != nil { - return 0, 0, fmt.Errorf("couldn't get namespace: %v", err) + return 0, 0, "", "", fmt.Errorf("couldn't get namespace: %v", err) + } + if eid, err = intFromLabel(ns.Labels, environmentIDLabel); err != nil { + return 0, 0, "", "", fmt.Errorf("couldn't get environment ID from label: %v", err) } if pid, err = intFromLabel(ns.Labels, projectIDLabel); err != nil { - return 0, 0, fmt.Errorf("couldn't get project ID from label: %v", err) + return 0, 0, "", "", fmt.Errorf("couldn't get project ID from label: %v", err) } - if eid, err = intFromLabel(ns.Labels, environmentIDLabel); err != nil { - return 0, 0, fmt.Errorf("couldn't get environment ID from label: %v", err) + if ename, ok = ns.Labels[environmentNameLabel]; !ok { + return 0, 0, "", "", fmt.Errorf("missing environment name label %v", + environmentNameLabel) + } + if pname, ok = ns.Labels[projectNameLabel]; !ok { + return 0, 0, "", "", fmt.Errorf("missing project name label %v", projectNameLabel) } - return pid, eid, nil + return eid, pid, ename, pname, nil } diff --git a/internal/sshportalapi/server.go b/internal/sshportalapi/server.go index 1f37fcff..ae0fd5f3 100644 --- a/internal/sshportalapi/server.go +++ b/internal/sshportalapi/server.go @@ -1,3 +1,5 @@ +// Package sshportalapi implements the lagoon-core component of the ssh-portal +// service. package sshportalapi import ( diff --git a/internal/sshportalapi/sshportal.go b/internal/sshportalapi/sshportal.go index c820049e..77d49d2d 100644 --- a/internal/sshportalapi/sshportal.go +++ b/internal/sshportalapi/sshportal.go @@ -57,7 +57,7 @@ func sshportal(ctx context.Context, log *zap.Logger, c *nats.EncodedConn, if err = c.Publish(replySubject, false); err != nil { log.Error("couldn't publish reply", zap.Any("query", query), - zap.Bool("reply value", false), + zap.Bool("reply", false), zap.Error(err)) } return @@ -77,7 +77,7 @@ func sshportal(ctx context.Context, log *zap.Logger, c *nats.EncodedConn, if err = c.Publish(replySubject, false); err != nil { log.Error("couldn't publish reply", zap.Any("query", query), - zap.Bool("reply value", false), + zap.Bool("reply", false), zap.Error(err)) } return @@ -91,8 +91,8 @@ func sshportal(ctx context.Context, log *zap.Logger, c *nats.EncodedConn, if err = c.Publish(replySubject, false); err != nil { log.Error("couldn't publish reply", zap.Any("query", query), - zap.Bool("reply value", false), - zap.String("user UUID", user.UUID.String()), + zap.Bool("reply", false), + zap.String("userUUID", user.UUID.String()), zap.Error(err)) } return @@ -107,23 +107,35 @@ func sshportal(ctx context.Context, log *zap.Logger, c *nats.EncodedConn, if err != nil { log.Error("couldn't query user roles and groups", zap.Any("query", query), - zap.String("user UUID", user.UUID.String()), + zap.String("userUUID", user.UUID.String()), zap.Error(err)) return } log.Debug("keycloak query response", - zap.Strings("realm roles", realmRoles), - zap.Strings("user groups", userGroups), - zap.Any("group project IDs", groupProjectIDs), - zap.String("user UUID", user.UUID.String())) + zap.Strings("realmRoles", realmRoles), + zap.Strings("userGroups", userGroups), + zap.Any("groupProjectIDs", groupProjectIDs), + zap.String("userUUID", user.UUID.String())) // calculate permission ok := permission.UserCanSSHToEnvironment(ctx, env, realmRoles, userGroups, groupProjectIDs) + if ok { + log.Info("validated SSH access", + zap.Int("environmentID", env.ID), + zap.Int("projectID", env.ProjectID), + zap.String("SSHFingerprint", query.SSHFingerprint), + zap.String("environmentName", env.Name), + zap.String("namespace", query.NamespaceName), + zap.String("projectName", env.ProjectName), + zap.String("sessionID", query.SessionID), + zap.String("userUUID", user.UUID.String()), + ) + } if err = c.Publish(replySubject, ok); err != nil { log.Error("couldn't publish reply", zap.Any("query", query), - zap.Bool("reply value", ok), - zap.String("user UUID", user.UUID.String()), + zap.Bool("reply", ok), + zap.String("userUUID", user.UUID.String()), zap.Error(err)) } } diff --git a/internal/sshserver/authhandler.go b/internal/sshserver/authhandler.go index 78ec459c..abc8cc2d 100644 --- a/internal/sshserver/authhandler.go +++ b/internal/sshserver/authhandler.go @@ -13,6 +13,16 @@ import ( gossh "golang.org/x/crypto/ssh" ) +type ctxKey int + +const ( + environmentIDKey ctxKey = iota + environmentNameKey + projectIDKey + projectNameKey + sshFingerprint +) + var ( natsTimeout = 8 * time.Second ) @@ -38,15 +48,15 @@ func pubKeyAuth(log *zap.Logger, nc *nats.EncodedConn, pubKey, err := gossh.ParsePublicKey(key.Marshal()) if err != nil { log.Warn("couldn't parse SSH public key", - zap.String("session-id", ctx.SessionID()), + zap.String("sessionID", ctx.SessionID()), zap.Error(err)) return false } // get Lagoon labels from namespace if available - pid, eid, err := c.NamespaceDetails(ctx, ctx.User()) + eid, pid, ename, pname, err := c.NamespaceDetails(ctx, ctx.User()) if err != nil { log.Debug("couldn't get namespace details", - zap.String("session-id", ctx.SessionID()), + zap.String("sessionID", ctx.SessionID()), zap.String("namespace", ctx.User()), zap.Error(err)) return false } @@ -65,15 +75,20 @@ func pubKeyAuth(log *zap.Logger, nc *nats.EncodedConn, natsTimeout) if err != nil { log.Warn("couldn't make NATS request", - zap.String("session-id", ctx.SessionID()), + zap.String("sessionID", ctx.SessionID()), zap.Error(err)) return false } // handle response if response { authSuccessTotal.Inc() + ctx.SetValue(environmentIDKey, eid) + ctx.SetValue(environmentNameKey, ename) + ctx.SetValue(projectIDKey, pid) + ctx.SetValue(projectNameKey, pname) + ctx.SetValue(sshFingerprint, fingerprint) log.Debug("authentication successful", - zap.String("session-id", ctx.SessionID()), + zap.String("sessionID", ctx.SessionID()), zap.String("fingerprint", fingerprint), zap.String("namespace", ctx.User())) return true diff --git a/internal/sshserver/serve.go b/internal/sshserver/serve.go index 6e7fce3f..23cda81b 100644 --- a/internal/sshserver/serve.go +++ b/internal/sshserver/serve.go @@ -1,3 +1,4 @@ +// Package sshserver is the SSH server component of the Lagoon ssh-portal. package sshserver import ( diff --git a/internal/sshserver/sessionhandler.go b/internal/sshserver/sessionhandler.go index 4659578c..3eb595e0 100644 --- a/internal/sshserver/sessionhandler.go +++ b/internal/sshserver/sessionhandler.go @@ -35,8 +35,8 @@ func sessionHandler(log *zap.Logger, c *k8s.Client, sftp bool) ssh.Handler { } // start the command log.Debug("starting command exec", - zap.String("session-id", sid), - zap.Strings("raw command", s.Command()), + zap.String("sessionID", sid), + zap.Strings("rawCommand", s.Command()), zap.String("subsystem", s.Subsystem()), ) // parse the command line arguments to extract any service or container args @@ -48,13 +48,13 @@ func sessionHandler(log *zap.Logger, c *k8s.Client, sftp bool) ssh.Handler { if err := k8s.ValidateLabelValue(service); err != nil { log.Debug("invalid service name", zap.String("service", service), - zap.String("session-id", sid), + zap.String("sessionID", sid), zap.Error(err)) _, err = fmt.Fprintf(s.Stderr(), "invalid service name %s. SID: %s\r\n", service, sid) if err != nil { log.Debug("couldn't write to session stream", - zap.String("session-id", sid), + zap.String("sessionID", sid), zap.Error(err)) } return @@ -62,13 +62,13 @@ func sessionHandler(log *zap.Logger, c *k8s.Client, sftp bool) ssh.Handler { if err := k8s.ValidateLabelValue(container); err != nil { log.Debug("invalid container name", zap.String("container", container), - zap.String("session-id", sid), + zap.String("sessionID", sid), zap.Error(err)) _, err = fmt.Fprintf(s.Stderr(), "invalid container name %s. SID: %s\r\n", container, sid) if err != nil { log.Debug("couldn't write to session stream", - zap.String("session-id", sid), + zap.String("sessionID", sid), zap.Error(err)) } return @@ -78,42 +78,69 @@ func sessionHandler(log *zap.Logger, c *k8s.Client, sftp bool) ssh.Handler { if err != nil { log.Debug("couldn't find deployment for service", zap.String("service", service), - zap.String("session-id", sid), + zap.String("sessionID", sid), zap.Error(err)) _, err = fmt.Fprintf(s.Stderr(), "unknown service %s. SID: %s\r\n", service, sid) if err != nil { log.Debug("couldn't write to session stream", - zap.String("session-id", sid), + zap.String("sessionID", sid), zap.Error(err)) } return } // check if a pty was requested _, _, pty := s.Pty() - log.Info("executing command", - zap.String("namespace", s.User()), - zap.String("deployment", deployment), + // extract info passed through the context by the authhandler + ctx := s.Context() + eid, ok := ctx.Value(environmentIDKey).(int) + if !ok { + log.Warn("couldn't extract environment ID from session context") + } + ename, ok := ctx.Value(environmentNameKey).(string) + if !ok { + log.Warn("couldn't extract environment name from session context") + } + pid, ok := ctx.Value(projectIDKey).(int) + if !ok { + log.Warn("couldn't extract project ID from session context") + } + pname, ok := ctx.Value(projectNameKey).(string) + if !ok { + log.Warn("couldn't extract project name from session context") + } + fingerprint, ok := ctx.Value(sshFingerprint).(string) + if !ok { + log.Warn("couldn't extract SSH key fingerprint from session context") + } + log.Info("executing SSH command", + zap.Bool("pty", pty), + zap.Int("environmentID", eid), + zap.Int("projectID", pid), + zap.String("SSHFingerprint", fingerprint), zap.String("container", container), + zap.String("deployment", deployment), + zap.String("environmentName", ename), + zap.String("namespace", s.User()), + zap.String("projectName", pname), + zap.String("sessionID", sid), zap.Strings("command", cmd), - zap.Bool("pty", pty), - zap.String("session-id", sid), ) err = c.Exec(s.Context(), s.User(), deployment, container, cmd, s, s.Stderr(), pty) if err != nil { log.Warn("couldn't execute command", - zap.String("session-id", sid), + zap.String("sessionID", sid), zap.Error(err)) _, err = fmt.Fprintf(s.Stderr(), "error executing command. SID: %s\r\n", sid) if err != nil { log.Warn("couldn't send error to client", - zap.String("session-id", sid), + zap.String("sessionID", sid), zap.Error(err)) } } log.Debug("finished command exec", - zap.String("session-id", sid)) + zap.String("sessionID", sid)) } }