Skip to content
Permalink

Comparing changes

This is a direct comparison between two commits made in this repository or its related repositories. View the default comparison for this range or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: uselagoon/lagoon-ssh-portal
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 9f3f78c8ad656bf5aa5490df703b81051b6559db
Choose a base ref
..
head repository: uselagoon/lagoon-ssh-portal
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 39bed8258e999af85f5c944f5b75e6aafe472151
Choose a head ref
29 changes: 29 additions & 0 deletions internal/bus/ssh.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Package bus contains the definitions of the messages passed across NATS.
package bus

import "log/slog"

const (
// SubjectSSHAccessQuery defines the NATS subject for SSH access queries.
SubjectSSHAccessQuery = "lagoon.sshportal.api"
)

// SSHAccessQuery defines the structure of an SSH access query.
type SSHAccessQuery struct {
SSHFingerprint string
NamespaceName string
ProjectID int
EnvironmentID int
SessionID string
}

// LogValue implements the slog.LogValuer interface.
func (q SSHAccessQuery) LogValue() slog.Value {
return slog.GroupValue(
slog.String("sshFingerprint", q.SSHFingerprint),
slog.String("namespaceName", q.NamespaceName),
slog.Int("projectID", q.ProjectID),
slog.Int("environmentID", q.EnvironmentID),
slog.String("sessionID", q.SessionID),
)
}
14 changes: 11 additions & 3 deletions internal/sshportalapi/server.go
Original file line number Diff line number Diff line change
@@ -11,6 +11,7 @@ import (

"github.com/google/uuid"
"github.com/nats-io/nats.go"
"github.com/uselagoon/ssh-portal/internal/bus"
"github.com/uselagoon/ssh-portal/internal/lagoon"
"github.com/uselagoon/ssh-portal/internal/lagoondb"
"github.com/uselagoon/ssh-portal/internal/rbac"
@@ -36,8 +37,15 @@ type KeycloakService interface {
}

// ServeNATS sshportalapi NATS requests.
func ServeNATS(ctx context.Context, stop context.CancelFunc, log *slog.Logger,
p *rbac.Permission, l LagoonDBService, k KeycloakService, natsURL string) error {
func ServeNATS(
ctx context.Context,
stop context.CancelFunc,
log *slog.Logger,
p *rbac.Permission,
l LagoonDBService,
k KeycloakService,
natsURL string,
) error {
// setup synchronisation
wg := sync.WaitGroup{}
wg.Add(1)
@@ -65,7 +73,7 @@ func ServeNATS(ctx context.Context, stop context.CancelFunc, log *slog.Logger,
}
defer nc.Close()
// set up request/response callback for sshportal
_, err = nc.QueueSubscribe(SubjectSSHAccessQuery, queue,
_, err = nc.QueueSubscribe(bus.SubjectSSHAccessQuery, queue,
sshportal(ctx, log, nc, p, l, k))
if err != nil {
return fmt.Errorf("couldn't subscribe to queue: %v", err)
37 changes: 8 additions & 29 deletions internal/sshportalapi/sshportal.go
Original file line number Diff line number Diff line change
@@ -9,37 +9,20 @@ import (
"github.com/nats-io/nats.go"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/uselagoon/ssh-portal/internal/bus"
"github.com/uselagoon/ssh-portal/internal/lagoon"
"github.com/uselagoon/ssh-portal/internal/lagoondb"
"github.com/uselagoon/ssh-portal/internal/rbac"
"go.opentelemetry.io/otel"
)

const (
// SubjectSSHAccessQuery defines the NATS subject for SSH access queries.
SubjectSSHAccessQuery = "lagoon.sshportal.api"
var (
requestsCounter = promauto.NewCounter(prometheus.CounterOpts{
Name: "sshportalapi_requests_total",
Help: "The total number of ssh-portal-api requests received",
})
)

// SSHAccessQuery defines the structure of an SSH access query.
type SSHAccessQuery struct {
SSHFingerprint string
NamespaceName string
ProjectID int
EnvironmentID int
SessionID string
}

// LogValue implements the slog.LogValuer interface.
func (q SSHAccessQuery) LogValue() slog.Value {
return slog.GroupValue(
slog.String("sshFingerprint", q.SSHFingerprint),
slog.String("namespaceName", q.NamespaceName),
slog.Int("projectID", q.ProjectID),
slog.Int("environmentID", q.EnvironmentID),
slog.String("sessionID", q.SessionID),
)
}

func sshportal(
ctx context.Context,
log *slog.Logger,
@@ -48,14 +31,10 @@ func sshportal(
l LagoonDBService,
k KeycloakService,
) nats.Handler {
requestsCounter := promauto.NewCounter(prometheus.CounterOpts{
Name: "sshportalapi_requests_total",
Help: "The total number of ssh-portal-api requests received",
})
return func(_, replySubject string, query *SSHAccessQuery) {
return func(_, replySubject string, query *bus.SSHAccessQuery) {
var realmRoles, userGroups []string
// set up tracing and update metrics
ctx, span := otel.Tracer(pkgName).Start(ctx, SubjectSSHAccessQuery)
ctx, span := otel.Tracer(pkgName).Start(ctx, bus.SubjectSSHAccessQuery)
defer span.End()
requestsCounter.Inc()
log := log.With(slog.Any("query", query))
23 changes: 13 additions & 10 deletions internal/sshserver/authhandler.go
Original file line number Diff line number Diff line change
@@ -8,8 +8,8 @@ import (
"github.com/nats-io/nats.go"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/uselagoon/ssh-portal/internal/bus"
"github.com/uselagoon/ssh-portal/internal/k8s"
"github.com/uselagoon/ssh-portal/internal/sshportalapi"
gossh "golang.org/x/crypto/ssh"
)

@@ -23,22 +23,25 @@ const (
sshFingerprint
)

const (
var (
natsTimeout = 8 * time.Second
)

// pubKeyAuth returns a ssh.PublicKeyHandler which queries the remote
// ssh-portal-api for Lagoon SSH authorization.
func pubKeyAuth(log *slog.Logger, nc *nats.EncodedConn,
c *k8s.Client) ssh.PublicKeyHandler {
authAttemptsTotal := promauto.NewCounter(prometheus.CounterOpts{
var (
authAttemptsTotal = promauto.NewCounter(prometheus.CounterOpts{
Name: "sshportal_authentication_attempts_total",
Help: "The total number of ssh-portal authentication attempts",
})
authSuccessTotal := promauto.NewCounter(prometheus.CounterOpts{
authSuccessTotal = promauto.NewCounter(prometheus.CounterOpts{
Name: "sshportal_authentication_success_total",
Help: "The total number of successful ssh-portal authentications",
})
)

// pubKeyAuth returns a ssh.PublicKeyHandler which queries the remote
// ssh-portal-api for Lagoon SSH authorization.
func pubKeyAuth(log *slog.Logger, nc *nats.EncodedConn,
c *k8s.Client) ssh.PublicKeyHandler {
return func(ctx ssh.Context, key ssh.PublicKey) bool {
authAttemptsTotal.Inc()
log := log.With(slog.String("sessionID", ctx.SessionID()))
@@ -57,7 +60,7 @@ func pubKeyAuth(log *slog.Logger, nc *nats.EncodedConn,
}
// construct ssh access query
fingerprint := gossh.FingerprintSHA256(pubKey)
q := sshportalapi.SSHAccessQuery{
q := bus.SSHAccessQuery{
SSHFingerprint: fingerprint,
NamespaceName: ctx.User(),
ProjectID: pid,
@@ -66,7 +69,7 @@ func pubKeyAuth(log *slog.Logger, nc *nats.EncodedConn,
}
// send query
var ok bool
err = nc.Request(sshportalapi.SubjectSSHAccessQuery, q, &ok, natsTimeout)
err = nc.Request(bus.SubjectSSHAccessQuery, q, &ok, natsTimeout)
if err != nil {
log.Warn("couldn't make NATS request", slog.Any("error", err))
return false
2 changes: 1 addition & 1 deletion internal/sshserver/serve.go
Original file line number Diff line number Diff line change
@@ -70,7 +70,7 @@ func Serve(
log.Warn("couldn't shutdown cleanly", slog.Any("error", err))
}
}()
if err := srv.Serve(l); !errors.Is(ssh.ErrServerClosed, err) {
if err := srv.Serve(l); !errors.Is(err, ssh.ErrServerClosed) {
return err
}
return nil
11 changes: 7 additions & 4 deletions internal/sshserver/sessionhandler.go
Original file line number Diff line number Diff line change
@@ -23,6 +23,13 @@ type K8SAPIService interface {
Logs(context.Context, string, string, string, bool, int64, io.ReadWriter) error
}

var (
sessionTotal = promauto.NewCounter(prometheus.CounterOpts{
Name: "sshportal_sessions_total",
Help: "The total number of ssh-portal sessions started",
})
)

// authCtxValues extracts the context values set by the authhandler.
func authCtxValues(ctx ssh.Context) (int, string, int, string, string, error) {
var ok bool
@@ -84,10 +91,6 @@ func getSSHIntent(sftp bool, cmd []string) []string {
// There is no support for a built-in sftp server.
func sessionHandler(log *slog.Logger, c K8SAPIService,
sftp, logAccessEnabled bool) ssh.Handler {
sessionTotal := promauto.NewCounter(prometheus.CounterOpts{
Name: "sshportal_sessions_total",
Help: "The total number of ssh-portal sessions started",
})
return func(s ssh.Session) {
sessionTotal.Inc()
ctx := s.Context()
13 changes: 8 additions & 5 deletions internal/sshtoken/authhandler.go
Original file line number Diff line number Diff line change
@@ -18,17 +18,20 @@ const (
userUUID ctxKey = iota
)

// pubKeyAuth returns a ssh.PublicKeyHandler which accepts any key which
// matches a user, and the associated user UUID to the ssh context.
func pubKeyAuth(log *slog.Logger, ldb LagoonDBService) ssh.PublicKeyHandler {
authnAttemptsTotal := promauto.NewCounter(prometheus.CounterOpts{
var (
authnAttemptsTotal = promauto.NewCounter(prometheus.CounterOpts{
Name: "sshtoken_authentication_attempts_total",
Help: "The total number of ssh-token authentication attempts",
})
authnSuccessTotal := promauto.NewCounter(prometheus.CounterOpts{
authnSuccessTotal = promauto.NewCounter(prometheus.CounterOpts{
Name: "sshtoken_authentication_success_total",
Help: "The total number of successful ssh-token authentications",
})
)

// pubKeyAuth returns a ssh.PublicKeyHandler which accepts any key which
// matches a user, and the associated user UUID to the ssh context.
func pubKeyAuth(log *slog.Logger, ldb LagoonDBService) ssh.PublicKeyHandler {
return func(ctx ssh.Context, key ssh.PublicKey) bool {
authnAttemptsTotal.Inc()
log := log.With(slog.String("sessionID", ctx.SessionID()))
2 changes: 1 addition & 1 deletion internal/sshtoken/serve.go
Original file line number Diff line number Diff line change
@@ -58,7 +58,7 @@ func Serve(
log.Warn("couldn't shutdown cleanly", slog.Any("error", err))
}
}()
if err := srv.Serve(l); !errors.Is(ssh.ErrServerClosed, err) {
if err := srv.Serve(l); !errors.Is(err, ssh.ErrServerClosed) {
return err
}
return nil
48 changes: 21 additions & 27 deletions internal/sshtoken/sessionhandler.go
Original file line number Diff line number Diff line change
@@ -29,15 +29,25 @@ type KeycloakUserInfoService interface {
UserRolesAndGroups(context.Context, *uuid.UUID) ([]string, []string, error)
}

var (
sessionTotal = promauto.NewCounter(prometheus.CounterOpts{
Name: "sshtoken_sessions_total",
Help: "The total number of ssh-token sessions started",
})
tokensGeneratedTotal = promauto.NewCounter(prometheus.CounterOpts{
Name: "sshtoken_tokens_generated_total",
Help: "The total number of ssh-token user access tokens generated",
})
redirectsTotal = promauto.NewCounter(prometheus.CounterOpts{
Name: "sshtoken_redirects_total",
Help: "The total number of ssh redirect responses served",
})
)

// tokenSession returns a bare access token or full access token response based
// on the user ID
func tokenSession(
s ssh.Session,
log *slog.Logger,
keycloakToken KeycloakTokenService,
uid *uuid.UUID,
tokensGeneratedTotal prometheus.Counter,
) {
func tokenSession(s ssh.Session, log *slog.Logger,
keycloakToken KeycloakTokenService, uid *uuid.UUID) {
// valid commands:
// - grant: returns a full access token response as per
// https://www.rfc-editor.org/rfc/rfc6749#section-4.1.4
@@ -122,7 +132,6 @@ func redirectSession(
keycloakUserInfo KeycloakUserInfoService,
ldb LagoonDBService,
uid *uuid.UUID,
redirectsTotal prometheus.Counter,
) {
ctx := s.Context()
// get the user roles and groups
@@ -241,25 +250,10 @@ func redirectSession(

// sessionHandler returns a ssh.Handler which writes a Lagoon access token to
// the session stream and then closes the connection.
func sessionHandler(
log *slog.Logger,
p *rbac.Permission,
func sessionHandler(log *slog.Logger, p *rbac.Permission,
keycloakToken KeycloakTokenService,
keycloakPermission KeycloakUserInfoService,
ldb LagoonDBService,
) ssh.Handler {
sessionTotal := promauto.NewCounter(prometheus.CounterOpts{
Name: "sshtoken_sessions_total",
Help: "The total number of ssh-token sessions started",
})
tokensGeneratedTotal := promauto.NewCounter(prometheus.CounterOpts{
Name: "sshtoken_tokens_generated_total",
Help: "The total number of ssh-token user access tokens generated",
})
redirectsTotal := promauto.NewCounter(prometheus.CounterOpts{
Name: "sshtoken_redirects_total",
Help: "The total number of ssh redirect responses served",
})
ldb LagoonDBService) ssh.Handler {
return func(s ssh.Session) {
sessionTotal.Inc()
// extract required info from the session context
@@ -278,9 +272,9 @@ func sessionHandler(
}
log = log.With(slog.String("userUUID", uid.String()))
if s.User() == "lagoon" {
tokenSession(s, log, keycloakToken, uid, tokensGeneratedTotal)
tokenSession(s, log, keycloakToken, uid)
} else {
redirectSession(s, log, p, keycloakPermission, ldb, uid, redirectsTotal)
redirectSession(s, log, p, keycloakPermission, ldb, uid)
}
}
}