Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions packages/gateway-v2/test_connection_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"github.com/jackc/pgx/v5/stdlib"
Comment thread
x032205 marked this conversation as resolved.
mssql "github.com/microsoft/go-mssqldb"
"github.com/microsoft/go-mssqldb/msdsn"
"github.com/smallnest/resp3"
"go.mongodb.org/mongo-driver/v2/mongo"
"go.mongodb.org/mongo-driver/v2/mongo/options"
)
Expand All @@ -49,6 +50,7 @@ var connectionTestMux = sync.OnceValue(func() *http.ServeMux {
const (
testConnModeSQL = "sql"
testConnModeMongoDB = "mongodb"
testConnModeRedis = "redis"
testConnModeLDAP = "ldap"
testConnModeKubernetes = "kubernetes"
testConnModeSSH = "ssh"
Expand Down Expand Up @@ -81,6 +83,14 @@ type mongoTestParams struct {
SslCertificate string `json:"sslCertificate"`
}

type redisTestParams struct {
Username string `json:"username"`
Password string `json:"password"`
SslEnabled bool `json:"sslEnabled"`
SslRejectUnauthorized *bool `json:"sslRejectUnauthorized"`
SslCertificate string `json:"sslCertificate"`
}

type ldapTestParams struct {
Username string `json:"username"`
Password string `json:"password"`
Expand Down Expand Up @@ -232,6 +242,92 @@ func doMongoConnectionTest(ctx context.Context, host string, port int, params mo
return client.Ping(ctx, nil)
}

// doRedisConnectionTest authenticates against the target Redis and PINGs it
func doRedisConnectionTest(ctx context.Context, host string, port int, params redisTestParams) error {
timeout := testConnDefaultTimeout
if deadline, ok := ctx.Deadline(); ok {
timeout = time.Until(deadline)
}

addr := net.JoinHostPort(host, strconv.Itoa(port))
dialer := &net.Dialer{Timeout: timeout}

var conn net.Conn
if params.SslEnabled {
tlsConfig, err := buildTestTLSConfig(host, params.SslCertificate, params.SslRejectUnauthorized)
if err != nil {
return err
}
if conn, err = tls.DialWithDialer(dialer, "tcp", addr, tlsConfig); err != nil {
return err
}
} else {
var err error
if conn, err = dialer.DialContext(ctx, "tcp", addr); err != nil {
return err
}
}
defer conn.Close()

if deadline, ok := ctx.Deadline(); ok {
_ = conn.SetDeadline(deadline)
}

writer := resp3.NewWriter(conn)
reader := bufio.NewReader(io.LimitReader(conn, maxRedisTestReplyBytes))

// only authenticate when password is supplied
if params.Password != "" {
var err error
if params.Username != "" && params.Username != "default" {
err = writer.WriteCommand("AUTH", params.Username, params.Password)
} else {
err = writer.WriteCommand("AUTH", params.Password)
Comment thread
x032205 marked this conversation as resolved.
}
if err != nil {
return err
Comment thread
x032205 marked this conversation as resolved.
}
reply, err := readRedisReplyLine(reader)
if err != nil {
return err
}
if reply != "+OK" {
return fmt.Errorf("redis authentication failed: %s", redisReplyErrorText(reply))
}
}

if err := writer.WriteCommand("PING"); err != nil {
return err
}
reply, err := readRedisReplyLine(reader)
if err != nil {
return err
}
if len(reply) > 0 && (reply[0] == '-' || reply[0] == '!') {
return fmt.Errorf("redis PING failed: %s", redisReplyErrorText(reply))
}
return nil
}

const maxRedisTestReplyBytes = 64 * 1024

// readRedisReplyLine reads a single RESP reply line (bounded by the LimitReader) and trims the CRLF
func readRedisReplyLine(r *bufio.Reader) (string, error) {
line, err := r.ReadString('\n')
if err != nil {
return "", err
}
return strings.TrimRight(line, "\r\n"), nil
}

// redisReplyErrorText strips a RESP error prefix ('-' or '!') for use in an error message
func redisReplyErrorText(line string) string {
if len(line) > 0 && (line[0] == '-' || line[0] == '!') {
return strings.TrimSpace(line[1:])
}
return strings.TrimSpace(line)
}

// doLdapConnectionTest binds to the target directory with the supplied credentials (the Windows AD auth check)
func doLdapConnectionTest(ctx context.Context, host string, port int, params ldapTestParams) error {
timeout := testConnDefaultTimeout
Expand Down Expand Up @@ -451,6 +547,12 @@ func handleTestConnection(w http.ResponseWriter, r *http.Request) {
return
}
op = func() error { return doMongoConnectionTest(ctx, target.host, target.port, params) }
case testConnModeRedis:
var params redisTestParams
if !decode(&params) {
return
}
op = func() error { return doRedisConnectionTest(ctx, target.host, target.port, params) }
case testConnModeLDAP:
var params ldapTestParams
if !decode(&params) {
Expand Down
12 changes: 9 additions & 3 deletions packages/pam/handlers/redis/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,15 @@ func (p *RedisProxy) HandleConnection(ctx context.Context, clientConn net.Conn)
defer func(selfToClientRedisConn *RedisConn) { _ = selfToClientRedisConn.Close() }(selfToClientRedisConn)

// Only authenticate if credentials are provided
if p.config.InjectUsername != "" && p.config.InjectPassword != "" {
if err := selfToClientRedisConn.Writer().WriteCommand("AUTH", p.config.InjectUsername, p.config.InjectPassword); err != nil {
return err
if p.config.InjectPassword != "" {
var writeErr error
if p.config.InjectUsername != "" && p.config.InjectUsername != "default" {
writeErr = selfToClientRedisConn.Writer().WriteCommand("AUTH", p.config.InjectUsername, p.config.InjectPassword)
} else {
writeErr = selfToClientRedisConn.Writer().WriteCommand("AUTH", p.config.InjectPassword)
Comment thread
x032205 marked this conversation as resolved.
}
if writeErr != nil {
return writeErr
}
if err := selfToClientRedisConn.Writer().Flush(); err != nil {
return err
Expand Down
8 changes: 7 additions & 1 deletion packages/pam/handlers/redis/relay_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,15 @@ func (h *RelayHandler) Handle(ctx context.Context) error {
}
switch value.Type {
case resp3.TypeArray:
if len(value.Elems) == 0 {
if err := h.clientToSelfConn.WriteValue(&resp3.Value{Type: resp3.TypeSimpleError, Err: "ERR empty command"}, true); err != nil {
return err
}
continue
}
cmd := value.Elems[0]
if cmd.Type != resp3.TypeBlobString {
return fmt.Errorf("expected SimpleString, got %s", cmd.Type)
return fmt.Errorf("expected blob string command, got %c", cmd.Type)
}
cmdStr := strings.ToLower(value.Elems[0].Str)
switch cmdStr {
Expand Down
99 changes: 98 additions & 1 deletion packages/pam/local/access.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ func StartPAMAccess(accessToken, path, reason, durationStr, targetHost string, p
case AccountTypeSSH:
startSSHAccess(httpClient, &pamResponse, displayPath, durationStr, port)
case AccountTypeRedis:
util.PrintErrorMessageAndExit("Redis access not yet supported in the new PAM model")
startRedisProxy(httpClient, &pamResponse, displayPath, durationStr, port)
Comment thread
x032205 marked this conversation as resolved.
Comment thread
x032205 marked this conversation as resolved.
case AccountTypeKubernetes:
startKubernetesProxy(httpClient, &pamResponse, displayPath, durationStr, port)
case AccountTypeAwsIam:
Expand Down Expand Up @@ -381,6 +381,103 @@ func startDatabaseProxy(httpClient *resty.Client, response *api.PAMAccessRespons
proxy.Run()
}

func startRedisProxy(httpClient *resty.Client, response *api.PAMAccessResponse, path, durationStr string, port int) {
duration, err := time.ParseDuration(durationStr)
if err != nil {
util.HandleError(err, "Failed to parse duration")
return
}

ctx, cancel := context.WithCancel(context.Background())

proxy := &RedisProxyServer{
BaseProxyServer: BaseProxyServer{
httpClient: httpClient,
relayHost: response.RelayHost,
relayClientCert: response.RelayClientCertificate,
relayClientKey: response.RelayClientPrivateKey,
relayServerCertChain: response.RelayServerCertificateChain,
gatewayClientCert: response.GatewayClientCertificate,
gatewayClientKey: response.GatewayClientPrivateKey,
gatewayServerCertChain: response.GatewayServerCertificateChain,
sessionExpiry: time.Now().Add(duration),
sessionId: response.SessionId,
resourceType: response.AccountType,
ctx: ctx,
cancel: cancel,
shutdownCh: make(chan struct{}),
},
}

if err := proxy.ValidateResourceTypeSupported(); err != nil {
util.HandleError(err, "Gateway version outdated")
return
}

if err := proxy.Start(port); err != nil {
util.HandleError(err, "Failed to start proxy server")
return
}

folder, account := parsePath(path)
log.Info().Msgf("Redis proxy server listening on port %d", proxy.port)
printRedisSessionInfo(folder, account, duration, response.Metadata["username"], proxy.port)

sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
go func() {
sig := <-sigChan
log.Info().Msgf("Received signal %v, initiating graceful shutdown...", sig)
proxy.gracefulShutdown()
}()

proxy.Run()
}

func printRedisSessionInfo(folder, account string, duration time.Duration, username string, port int) {
Comment thread
x032205 marked this conversation as resolved.
fmt.Printf("\n")
fmt.Printf("**********************************************************************\n")
fmt.Printf(" Redis Proxy Session Started! \n")
fmt.Printf("**********************************************************************\n")
fmt.Printf("\n")
if folder != "" {
fmt.Printf(" Folder: %s\n", folder)
}
fmt.Printf(" Account: %s\n", account)
fmt.Printf(" Duration: %s\n", duration.String())
fmt.Printf("\n")
fmt.Printf("----------------------------------------------------------------------\n")
fmt.Printf(" Connection Details \n")
fmt.Printf("----------------------------------------------------------------------\n")
fmt.Printf("\n")
fmt.Printf(" Host: 127.0.0.1\n")
fmt.Printf(" Port: %d\n", port)
if username != "" {
fmt.Printf(" Username: %s\n", username)
}
fmt.Printf(" Password: (not required)\n")
Comment thread
x032205 marked this conversation as resolved.
fmt.Printf("\n")
fmt.Printf("----------------------------------------------------------------------\n")
fmt.Printf(" How to Connect \n")
fmt.Printf("----------------------------------------------------------------------\n")
fmt.Printf("\n")
fmt.Printf(" Use your preferred Redis client to connect to 127.0.0.1:%d.\n", port)
fmt.Printf(" No password is needed.\n")
fmt.Printf("\n")
fmt.Printf(" Example:\n")
util.PrintfStderr(" $ redis-cli -h 127.0.0.1 -p %d\n", port)
fmt.Printf("\n")
fmt.Printf(" Connection string:\n")
if username != "" {
util.PrintfStderr(" redis://%s@127.0.0.1:%d\n", username, port)
} else {
util.PrintfStderr(" redis://127.0.0.1:%d\n", port)
}
fmt.Printf("\n")
fmt.Printf("**********************************************************************\n")
fmt.Printf("\n")
}

func startRDPProxy(httpClient *resty.Client, response *api.PAMAccessResponse, path, durationStr string, port int) {
duration, err := time.ParseDuration(durationStr)
if err != nil {
Expand Down
Loading
Loading