Skip to content

Commit

Permalink
Merge pull request #147 from uselagoon/return-ssh-endpoint
Browse files Browse the repository at this point in the history
Return ssh-portal endpoint on attempted shell access through ssh-token service
  • Loading branch information
smlx authored Dec 7, 2022
2 parents 3fa9f2f + 985a080 commit 303bd25
Show file tree
Hide file tree
Showing 7 changed files with 336 additions and 143 deletions.
46 changes: 28 additions & 18 deletions cmd/ssh-token/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,19 @@ import (

// ServeCmd represents the serve command.
type ServeCmd struct {
APIDBAddress string `kong:"required,env='API_DB_ADDRESS',help='Lagoon API DB Address (host[:port])'"`
APIDBDatabase string `kong:"default='infrastructure',env='API_DB_DATABASE',help='Lagoon API DB Database Name'"`
APIDBPassword string `kong:"required,env='API_DB_PASSWORD',help='Lagoon API DB Password'"`
APIDBUsername string `kong:"default='api',env='API_DB_USERNAME',help='Lagoon API DB Username'"`
KeycloakBaseURL string `kong:"required,env='KEYCLOAK_BASE_URL',help='Keycloak Base URL'"`
KeycloakClientID string `kong:"default='auth-server',env='KEYCLOAK_AUTH_SERVER_CLIENT_ID',help='Keycloak OAuth2 Client ID'"`
KeycloakClientSecret string `kong:"required,env='KEYCLOAK_AUTH_SERVER_CLIENT_SECRET',help='Keycloak OAuth2 Client Secret'"`
SSHServerPort uint `kong:"default='2222',env='SSH_SERVER_PORT',help='Port the SSH server will listen on for SSH client connections'"`
HostKeyECDSA string `kong:"env='HOST_KEY_ECDSA',help='PEM encoded ECDSA host key'"`
HostKeyED25519 string `kong:"env='HOST_KEY_ED25519',help='PEM encoded Ed25519 host key'"`
HostKeyRSA string `kong:"env='HOST_KEY_RSA',help='PEM encoded RSA host key'"`
APIDBAddress string `kong:"required,env='API_DB_ADDRESS',help='Lagoon API DB Address (host[:port])'"`
APIDBDatabase string `kong:"default='infrastructure',env='API_DB_DATABASE',help='Lagoon API DB Database Name'"`
APIDBPassword string `kong:"required,env='API_DB_PASSWORD',help='Lagoon API DB Password'"`
APIDBUsername string `kong:"default='api',env='API_DB_USERNAME',help='Lagoon API DB Username'"`
KeycloakBaseURL string `kong:"required,env='KEYCLOAK_BASE_URL',help='Keycloak Base URL'"`
KeycloakTokenClientID string `kong:"default='auth-server',env='KEYCLOAK_AUTH_SERVER_CLIENT_ID',help='Keycloak auth-server OAuth2 Client ID'"`
KeycloakTokenClientSecret string `kong:"required,env='KEYCLOAK_AUTH_SERVER_CLIENT_SECRET',help='Keycloak auth-server OAuth2 Client Secret'"`
KeycloakPermissionClientID string `kong:"default='service-api',env='KEYCLOAK_SERVICE_API_CLIENT_ID',help='Keycloak service-api OAuth2 Client ID'"`
KeycloakPermissionClientSecret string `kong:"env='KEYCLOAK_SERVICE_API_CLIENT_SECRET',help='Keycloak service-api OAuth2 Client Secret'"`
SSHServerPort uint `kong:"default='2222',env='SSH_SERVER_PORT',help='Port the SSH server will listen on for SSH client connections'"`
HostKeyECDSA string `kong:"env='HOST_KEY_ECDSA',help='PEM encoded ECDSA host key'"`
HostKeyED25519 string `kong:"env='HOST_KEY_ED25519',help='PEM encoded Ed25519 host key'"`
HostKeyRSA string `kong:"env='HOST_KEY_RSA',help='PEM encoded RSA host key'"`
}

// Run the serve command to ssh-portal API requests.
Expand All @@ -48,13 +50,19 @@ func (cmd *ServeCmd) Run(log *zap.Logger) error {
dbConf.User = cmd.APIDBUsername
ldb, err := lagoondb.NewClient(ctx, dbConf.FormatDSN())
if err != nil {
return fmt.Errorf("couldn't init lagoon DBClient: %v", err)
return fmt.Errorf("couldn't init lagoonDB client: %v", err)
}
// init keycloak client
k, err := keycloak.NewClient(ctx, log, cmd.KeycloakBaseURL,
cmd.KeycloakClientID, cmd.KeycloakClientSecret)
// init token / auth-server keycloak client
keycloakToken, err := keycloak.NewClient(ctx, log, cmd.KeycloakBaseURL,
cmd.KeycloakTokenClientID, cmd.KeycloakTokenClientSecret)
if err != nil {
return fmt.Errorf("couldn't init keycloak Client: %v", err)
return fmt.Errorf("couldn't init keycloak token client: %v", err)
}
// init permission / service-api keycloak client
keycloakPermission, err := keycloak.NewClient(ctx, log, cmd.KeycloakBaseURL,
cmd.KeycloakPermissionClientID, cmd.KeycloakPermissionClientSecret)
if err != nil {
return fmt.Errorf("couldn't init keycloak permission client: %v", err)
}
// start listening on TCP port
l, err := net.Listen("tcp", fmt.Sprintf(":%d", cmd.SSHServerPort))
Expand All @@ -63,11 +71,13 @@ func (cmd *ServeCmd) Run(log *zap.Logger) error {
}
// check for persistent host key arguments
var hostkeys [][]byte
for _, hk := range []string{cmd.HostKeyECDSA, cmd.HostKeyED25519, cmd.HostKeyRSA} {
for _, hk := range []string{cmd.HostKeyECDSA, cmd.HostKeyED25519,
cmd.HostKeyRSA} {
if len(hk) > 0 {
hostkeys = append(hostkeys, []byte(hk))
}
}
// start serving SSH token requests
return sshtoken.Serve(ctx, log, l, ldb, k, hostkeys)
return sshtoken.Serve(ctx, log, l, ldb, keycloakToken, keycloakPermission,
hostkeys)
}
31 changes: 30 additions & 1 deletion internal/lagoondb/client.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Package lagoondb provides an interface to the Lagoon API database.
package lagoondb

import (
Expand Down Expand Up @@ -69,7 +70,8 @@ func (c *Client) EnvironmentByNamespaceName(ctx context.Context, name string) (*
project.id AS project_id,
project.name AS project_name
FROM environment JOIN project ON environment.project = project.id
WHERE environment.openshift_project_name = ?`, name)
WHERE environment.openshift_project_name = ?
LIMIT 1`, name)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNoResult
Expand Down Expand Up @@ -99,3 +101,30 @@ func (c *Client) UserBySSHFingerprint(ctx context.Context, fingerprint string) (
}
return &user, nil
}

// SSHEndpointByEnvironmentID returns the SSH host and port of the ssh-portal
// associated with the given environment ID.
func (c *Client) SSHEndpointByEnvironmentID(ctx context.Context,
envID int) (string, string, error) {
// set up tracing
ctx, span := otel.Tracer(pkgName).Start(ctx, "SSHEndpointByEnvironmentID")
defer span.End()
// run query
ssh := struct {
Host string `db:"ssh_host"`
Port string `db:"ssh_port"`
}{}
err := c.db.GetContext(ctx, &ssh, `
SELECT
openshift.ssh_host AS ssh_host,
openshift.ssh_port AS ssh_port
FROM environment JOIN openshift ON environment.openshift = openshift.id
WHERE environment.id = ?`, envID)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return "", "", ErrNoResult
}
return "", "", err
}
return ssh.Host, ssh.Port, nil
}
5 changes: 0 additions & 5 deletions internal/sshportalapi/sshportal.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,11 +111,6 @@ func sshportal(ctx context.Context, log *zap.Logger, c *nats.EncodedConn,
zap.Error(err))
return
}
log.Debug("keycloak query response",
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)
Expand Down
6 changes: 1 addition & 5 deletions internal/sshserver/sessionhandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,7 @@ func sshifyCommand(sftp bool, cmd []string) []string {
func sessionHandler(log *zap.Logger, c *k8s.Client, sftp bool) ssh.Handler {
return func(s ssh.Session) {
sessionTotal.Inc()
sid, ok := s.Context().Value(ssh.ContextKeySessionID).(string)
if !ok {
log.Warn("couldn't get session ID")
return
}
sid := s.Context().SessionID()
// start the command
log.Debug("starting command exec",
zap.String("sessionID", sid),
Expand Down
28 changes: 6 additions & 22 deletions internal/sshtoken/authhandler.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package sshtoken

import (
"context"
"errors"

"github.com/gliderlabs/ssh"
Expand All @@ -18,11 +17,6 @@ const (
userUUID ctxKey = iota
)

// LagoonDBService provides methods for querying the Lagoon API DB.
type LagoonDBService interface {
UserBySSHFingerprint(context.Context, string) (*lagoondb.User, error)
}

var (
authnAttemptsTotal = promauto.NewCounter(prometheus.CounterOpts{
Name: "sshtoken_authentication_attempts_total",
Expand All @@ -32,15 +26,11 @@ var (
Name: "sshtoken_authentication_success_total",
Help: "The total number of successful ssh-token authentications",
})
authnAttemptsNonLagoonUser = promauto.NewCounter(prometheus.CounterOpts{
Name: "sshtoken_authentication_attempts_non_lagoon_user",
Help: "The total number of failed authentication attempts with a user other than lagoon",
})
)

// 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 *zap.Logger, l LagoonDBService) ssh.PublicKeyHandler {
func pubKeyAuth(log *zap.Logger, ldb LagoonDBService) ssh.PublicKeyHandler {
return func(ctx ssh.Context, key ssh.PublicKey) bool {
authnAttemptsTotal.Inc()
// parse SSH public key
Expand All @@ -51,36 +41,30 @@ func pubKeyAuth(log *zap.Logger, l LagoonDBService) ssh.PublicKeyHandler {
zap.Error(err))
return false
}
// validate user string
if ctx.User() != "lagoon" {
authnAttemptsNonLagoonUser.Inc()
log.Debug(`invalid user: only "lagoon" is supported`,
zap.String("sessionID", ctx.SessionID()),
zap.String("user", ctx.User()))
return false
}
// identify Lagoon user by ssh key fingerprint
fingerprint := gossh.FingerprintSHA256(pubKey)
user, err := l.UserBySSHFingerprint(ctx, fingerprint)
user, err := ldb.UserBySSHFingerprint(ctx, fingerprint)
if err != nil {
if errors.Is(err, lagoondb.ErrNoResult) {
log.Debug("unknown SSH Fingerprint",
zap.String("sessionID", ctx.SessionID()))
} else {
log.Warn("couldn't query for user by SSH key fingerprint",
zap.String("sessionID", ctx.SessionID()),
zap.String("fingerprint", fingerprint),
zap.Error(err))
}
return false
}
// The SSH key fingerprint was in the database so authentication was
// The SSH key fingerprint was in the database so "authentication" was
// successful. Inject the user UUID into the context so it can be used in
// the session handler.
authnSuccessTotal.Inc()
ctx.SetValue(userUUID, user.UUID)
log.Info("authentication successful",
zap.String("sessionID", ctx.SessionID()),
zap.String("fingerprint", fingerprint))
zap.String("fingerprint", fingerprint),
zap.String("userID", user.UUID.String()))
return true
}
}
12 changes: 10 additions & 2 deletions internal/sshtoken/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,19 @@ import (
// give an 8 second deadline to shut down cleanly.
const shutdownTimeout = 8 * time.Second

// LagoonDBService provides methods for querying the Lagoon API DB.
type LagoonDBService interface {
EnvironmentByNamespaceName(context.Context, string) (*lagoondb.Environment, error)
UserBySSHFingerprint(context.Context, string) (*lagoondb.User, error)
SSHEndpointByEnvironmentID(context.Context, int) (string, string, error)
}

// Serve contains the main ssh session logic
func Serve(ctx context.Context, log *zap.Logger, l net.Listener,
ldb *lagoondb.Client, k *keycloak.Client, hostKeys [][]byte) error {
ldb *lagoondb.Client, keycloakToken, keycloakPermission *keycloak.Client,
hostKeys [][]byte) error {
srv := ssh.Server{
Handler: sessionHandler(log, k),
Handler: sessionHandler(log, keycloakToken, keycloakPermission, ldb),
PublicKeyHandler: pubKeyAuth(log, ldb),
}
for _, hk := range hostKeys {
Expand Down
Loading

0 comments on commit 303bd25

Please sign in to comment.