From 8c16e53793f0308a29cd5f260cc2b5091172fc97 Mon Sep 17 00:00:00 2001 From: Andrey Date: Sun, 16 Aug 2026 22:57:07 -0400 Subject: [PATCH 1/3] feat(pam): iam auth for postgres --- go.mod | 7 +- go.sum | 2 + packages/api/model.go | 2 + .../gateway-v2/test_connection_handler.go | 22 ++++- packages/pam/aws_rds_auth.go | 87 +++++++++++++++++++ packages/pam/pam-proxy.go | 19 +++- packages/pam/session/credentials.go | 4 + 7 files changed, 138 insertions(+), 5 deletions(-) create mode 100644 packages/pam/aws_rds_auth.go diff --git a/go.mod b/go.mod index bc4dc53d..124a7634 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,10 @@ require ( github.com/Masterminds/sprig/v3 v3.3.0 github.com/awnumar/memguard v0.23.0 github.com/aws/aws-sdk-go-v2 v1.27.2 + github.com/aws/aws-sdk-go-v2/config v1.27.18 + github.com/aws/aws-sdk-go-v2/credentials v1.17.18 + github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.4.2 + github.com/aws/aws-sdk-go-v2/service/sts v1.28.12 github.com/bradleyjkemp/cupaloy/v2 v2.8.0 github.com/charmbracelet/lipgloss v0.9.1 github.com/creack/pty v1.1.21 @@ -77,8 +81,6 @@ require ( github.com/alessio/shellescape v1.4.1 // indirect github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef // indirect github.com/awnumar/memcall v0.4.0 // indirect - github.com/aws/aws-sdk-go-v2/config v1.27.18 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.17.18 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.5 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.9 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.9 // indirect @@ -87,7 +89,6 @@ require ( github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.11 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.20.11 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.5 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.28.12 // indirect github.com/aws/smithy-go v1.20.2 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/bodgit/ntlmssp v0.0.0-20240506230425-31973bb52d9b // indirect diff --git a/go.sum b/go.sum index 3ba40cf0..8630626f 100644 --- a/go.sum +++ b/go.sum @@ -104,6 +104,8 @@ github.com/aws/aws-sdk-go-v2/credentials v1.17.18 h1:D/ALDWqK4JdY3OFgA2thcPO1c9a github.com/aws/aws-sdk-go-v2/credentials v1.17.18/go.mod h1:JuitCWq+F5QGUrmMPsk945rop6bB57jdscu+Glozdnc= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.5 h1:dDgptDO9dxeFkXy+tEgVkzSClHZje/6JkPW5aZyEvrQ= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.5/go.mod h1:gjvE2KBUgUQhcv89jqxrIxH9GaKs1JbZzWejj/DaHGA= +github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.4.2 h1:TFju6ZoqO3TnX0C42VmYW4TxNcUFfbV/3cnaOxbcc5Y= +github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.4.2/go.mod h1:HLaNMGEhcO6GnJtrozRtluhCVM5/B/ZV5XHQ477uIgA= github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.9 h1:cy8ahBJuhtM8GTTSyOkfy6WVPV1IE+SS5/wfXUYuulw= github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.9/go.mod h1:CZBXGLaJnEZI6EVNcPd7a6B5IC5cA/GkRWtu9fp3S6Y= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.9 h1:A4SYk07ef04+vxZToz9LWvAXl9LW0NClpPpMsi31cz0= diff --git a/packages/api/model.go b/packages/api/model.go index 2406d068..0a923d29 100644 --- a/packages/api/model.go +++ b/packages/api/model.go @@ -1009,6 +1009,8 @@ type PAMSessionCredentials struct { Username string `json:"username"` Password string `json:"password"` AuthMethod string `json:"authMethod,omitempty"` + AwsRegion string `json:"awsRegion,omitempty"` + RoleArn string `json:"roleArn,omitempty"` PrivateKey string `json:"privateKey,omitempty"` Certificate string `json:"certificate,omitempty"` Url string `json:"url,omitempty"` diff --git a/packages/gateway-v2/test_connection_handler.go b/packages/gateway-v2/test_connection_handler.go index 7940425f..16445bfd 100644 --- a/packages/gateway-v2/test_connection_handler.go +++ b/packages/gateway-v2/test_connection_handler.go @@ -28,6 +28,8 @@ import ( "github.com/smallnest/resp3" "go.mongodb.org/mongo-driver/v2/mongo" "go.mongodb.org/mongo-driver/v2/mongo/options" + + "github.com/Infisical/infisical-merge/packages/pam" ) const ( @@ -65,9 +67,12 @@ type testConnectionEnvelope struct { } type sqlTestParams struct { - Dialect string `json:"dialect"` // "postgres" | "mysql" | "mssql" + Dialect string `json:"dialect"` // "postgres" | "mysql" | "mssql" + AuthMethod string `json:"authMethod"` // empty, or unrecognised, means the password below Username string `json:"username"` Password string `json:"password"` + AwsRegion string `json:"awsRegion"` + RoleArn string `json:"roleArn"` Database string `json:"database"` SslEnabled bool `json:"sslEnabled"` SslRejectUnauthorized *bool `json:"sslRejectUnauthorized"` @@ -205,6 +210,21 @@ func openSQLTestDB(host string, port int, params sqlTestParams) (*sql.DB, error) // doSQLConnectionTest authenticates against the target SQL server and runs a trivial query func doSQLConnectionTest(ctx context.Context, host string, port int, params sqlTestParams) error { + if params.AuthMethod == pam.AwsIamAuthMethod { + token, err := pam.BuildRdsAuthToken(ctx, pam.RdsAuthTokenParams{ + Host: host, + Port: port, + Region: params.AwsRegion, + DBUser: params.Username, + RoleArn: params.RoleArn, + SessionName: "infisical-pam-connection-test", + }) + if err != nil { + return err + } + params.Password = token + } + db, err := openSQLTestDB(host, port, params) if err != nil { return err diff --git a/packages/pam/aws_rds_auth.go b/packages/pam/aws_rds_auth.go new file mode 100644 index 00000000..040d642f --- /dev/null +++ b/packages/pam/aws_rds_auth.go @@ -0,0 +1,87 @@ +package pam + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/credentials/stscreds" + "github.com/aws/aws-sdk-go-v2/feature/rds/auth" + "github.com/aws/aws-sdk-go-v2/service/sts" +) + +// AwsIamAuthMethod is the credential auth method the server sends when the gateway, rather than +// Infisical, produces the login token. Any other value means the credential carries its own secret. +const AwsIamAuthMethod = "aws-iam" + +const rdsAuthTokenTimeout = 15 * time.Second + +// Reused across connections: resolving it fetches the gateway's own credentials from pod identity or +// instance metadata, which is a network round trip we should not repeat on every dial. Bounded by the +// number of regions an org's accounts live in. +var regionConfigs sync.Map + +func loadRegionConfig(ctx context.Context, region string) (aws.Config, error) { + if cached, ok := regionConfigs.Load(region); ok { + return cached.(aws.Config), nil + } + + cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(region)) + if err != nil { + return aws.Config{}, err + } + + regionConfigs.Store(region, cfg) + return cfg, nil +} + +type RdsAuthTokenParams struct { + Host string + Port int + Region string + DBUser string + RoleArn string + SessionName string +} + +// BuildRdsAuthToken mints an RDS/Aurora IAM authentication token for a single connection. The token is +// a SigV4 signature over the exact endpoint being dialled, so it is never reusable elsewhere, and it +// is never cached or written to disk. +// +// The gateway assumes the account's role using credentials from its own environment (pod identity, +// instance role, env vars, profile), which keeps the authority to reach a database inside the +// customer's account and scoped to one PAM account. A missing role is an error rather than a fall back +// to the gateway's own identity, which would quietly widen that scope. +func BuildRdsAuthToken(ctx context.Context, params RdsAuthTokenParams) (string, error) { + if params.Region == "" { + return "", fmt.Errorf("no AWS region configured for this account") + } + if params.RoleArn == "" { + return "", fmt.Errorf("no IAM role configured for this account") + } + + ctx, cancel := context.WithTimeout(ctx, rdsAuthTokenTimeout) + defer cancel() + + cfg, err := loadRegionConfig(ctx, params.Region) + if err != nil { + return "", fmt.Errorf("unable to load AWS credentials on the gateway (no pod identity, instance role, or AWS environment variables): %w", err) + } + + credentials := aws.NewCredentialsCache( + stscreds.NewAssumeRoleProvider(sts.NewFromConfig(cfg), params.RoleArn, func(o *stscreds.AssumeRoleOptions) { + o.RoleSessionName = params.SessionName + }), + ) + + endpoint := fmt.Sprintf("%s:%d", params.Host, params.Port) + token, err := auth.BuildAuthToken(ctx, endpoint, params.Region, params.DBUser, credentials) + if err != nil { + return "", fmt.Errorf("failed to generate an AWS IAM authentication token as %s: %w", params.RoleArn, err) + } + + return token, nil +} diff --git a/packages/pam/pam-proxy.go b/packages/pam/pam-proxy.go index 2ff2653a..2c457e0e 100644 --- a/packages/pam/pam-proxy.go +++ b/packages/pam/pam-proxy.go @@ -287,10 +287,27 @@ func HandlePAMProxy(ctx context.Context, conn *tls.Conn, pamConfig *GatewayPAMCo switch pamConfig.ResourceType { case session.ResourceTypePostgres: + injectPassword := credentials.Password + if credentials.AuthMethod == AwsIamAuthMethod { + token, err := BuildRdsAuthToken(ctx, RdsAuthTokenParams{ + Host: credentials.Host, + Port: credentials.Port, + Region: credentials.AwsRegion, + DBUser: credentials.Username, + RoleArn: credentials.RoleArn, + SessionName: fmt.Sprintf("infisical-pam-%s", pamConfig.SessionId), + }) + if err != nil { + log.Error().Err(err).Str("sessionId", pamConfig.SessionId).Msg("Failed to mint AWS IAM authentication token") + return err + } + injectPassword = token + } + proxyConfig := handlers.PostgresProxyConfig{ TargetAddr: fmt.Sprintf("%s:%d", credentials.Host, credentials.Port), InjectUsername: credentials.Username, - InjectPassword: credentials.Password, + InjectPassword: injectPassword, InjectDatabase: credentials.Database, EnableTLS: credentials.SSLEnabled, TLSConfig: tlsConfig, diff --git a/packages/pam/session/credentials.go b/packages/pam/session/credentials.go index c8691353..5eb15176 100644 --- a/packages/pam/session/credentials.go +++ b/packages/pam/session/credentials.go @@ -21,6 +21,8 @@ type PAMCredentials struct { AuthMethod string Username string Password string + AwsRegion string + RoleArn string Database string ConnectionString string // MongoDB: full URI (mongodb[+srv]://...) PrivateKey string @@ -180,6 +182,8 @@ func (cm *CredentialsManager) GetPAMSessionCredentials(sessionId string, expiryT AuthMethod: response.Credentials.AuthMethod, Username: response.Credentials.Username, Password: response.Credentials.Password, + AwsRegion: response.Credentials.AwsRegion, + RoleArn: response.Credentials.RoleArn, Database: response.Credentials.Database, ConnectionString: response.Credentials.ConnectionString, PrivateKey: response.Credentials.PrivateKey, From ac5a4a4bbe8487fac14b6c5482ba0aa3466a3aeb Mon Sep 17 00:00:00 2001 From: Andrey Date: Mon, 17 Aug 2026 10:24:57 -0400 Subject: [PATCH 2/3] move to backend minting --- go.mod | 7 +- go.sum | 2 - packages/api/model.go | 2 - .../gateway-v2/test_connection_handler.go | 22 +---- packages/pam/aws_rds_auth.go | 87 ------------------- packages/pam/pam-proxy.go | 26 ++---- packages/pam/session/credentials.go | 4 - 7 files changed, 12 insertions(+), 138 deletions(-) delete mode 100644 packages/pam/aws_rds_auth.go diff --git a/go.mod b/go.mod index 124a7634..bc4dc53d 100644 --- a/go.mod +++ b/go.mod @@ -8,10 +8,6 @@ require ( github.com/Masterminds/sprig/v3 v3.3.0 github.com/awnumar/memguard v0.23.0 github.com/aws/aws-sdk-go-v2 v1.27.2 - github.com/aws/aws-sdk-go-v2/config v1.27.18 - github.com/aws/aws-sdk-go-v2/credentials v1.17.18 - github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.4.2 - github.com/aws/aws-sdk-go-v2/service/sts v1.28.12 github.com/bradleyjkemp/cupaloy/v2 v2.8.0 github.com/charmbracelet/lipgloss v0.9.1 github.com/creack/pty v1.1.21 @@ -81,6 +77,8 @@ require ( github.com/alessio/shellescape v1.4.1 // indirect github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef // indirect github.com/awnumar/memcall v0.4.0 // indirect + github.com/aws/aws-sdk-go-v2/config v1.27.18 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.17.18 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.5 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.9 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.9 // indirect @@ -89,6 +87,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.11 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.20.11 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.5 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.28.12 // indirect github.com/aws/smithy-go v1.20.2 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/bodgit/ntlmssp v0.0.0-20240506230425-31973bb52d9b // indirect diff --git a/go.sum b/go.sum index 8630626f..3ba40cf0 100644 --- a/go.sum +++ b/go.sum @@ -104,8 +104,6 @@ github.com/aws/aws-sdk-go-v2/credentials v1.17.18 h1:D/ALDWqK4JdY3OFgA2thcPO1c9a github.com/aws/aws-sdk-go-v2/credentials v1.17.18/go.mod h1:JuitCWq+F5QGUrmMPsk945rop6bB57jdscu+Glozdnc= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.5 h1:dDgptDO9dxeFkXy+tEgVkzSClHZje/6JkPW5aZyEvrQ= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.5/go.mod h1:gjvE2KBUgUQhcv89jqxrIxH9GaKs1JbZzWejj/DaHGA= -github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.4.2 h1:TFju6ZoqO3TnX0C42VmYW4TxNcUFfbV/3cnaOxbcc5Y= -github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.4.2/go.mod h1:HLaNMGEhcO6GnJtrozRtluhCVM5/B/ZV5XHQ477uIgA= github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.9 h1:cy8ahBJuhtM8GTTSyOkfy6WVPV1IE+SS5/wfXUYuulw= github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.9/go.mod h1:CZBXGLaJnEZI6EVNcPd7a6B5IC5cA/GkRWtu9fp3S6Y= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.9 h1:A4SYk07ef04+vxZToz9LWvAXl9LW0NClpPpMsi31cz0= diff --git a/packages/api/model.go b/packages/api/model.go index 0a923d29..2406d068 100644 --- a/packages/api/model.go +++ b/packages/api/model.go @@ -1009,8 +1009,6 @@ type PAMSessionCredentials struct { Username string `json:"username"` Password string `json:"password"` AuthMethod string `json:"authMethod,omitempty"` - AwsRegion string `json:"awsRegion,omitempty"` - RoleArn string `json:"roleArn,omitempty"` PrivateKey string `json:"privateKey,omitempty"` Certificate string `json:"certificate,omitempty"` Url string `json:"url,omitempty"` diff --git a/packages/gateway-v2/test_connection_handler.go b/packages/gateway-v2/test_connection_handler.go index 16445bfd..7940425f 100644 --- a/packages/gateway-v2/test_connection_handler.go +++ b/packages/gateway-v2/test_connection_handler.go @@ -28,8 +28,6 @@ import ( "github.com/smallnest/resp3" "go.mongodb.org/mongo-driver/v2/mongo" "go.mongodb.org/mongo-driver/v2/mongo/options" - - "github.com/Infisical/infisical-merge/packages/pam" ) const ( @@ -67,12 +65,9 @@ type testConnectionEnvelope struct { } type sqlTestParams struct { - Dialect string `json:"dialect"` // "postgres" | "mysql" | "mssql" - AuthMethod string `json:"authMethod"` // empty, or unrecognised, means the password below + Dialect string `json:"dialect"` // "postgres" | "mysql" | "mssql" Username string `json:"username"` Password string `json:"password"` - AwsRegion string `json:"awsRegion"` - RoleArn string `json:"roleArn"` Database string `json:"database"` SslEnabled bool `json:"sslEnabled"` SslRejectUnauthorized *bool `json:"sslRejectUnauthorized"` @@ -210,21 +205,6 @@ func openSQLTestDB(host string, port int, params sqlTestParams) (*sql.DB, error) // doSQLConnectionTest authenticates against the target SQL server and runs a trivial query func doSQLConnectionTest(ctx context.Context, host string, port int, params sqlTestParams) error { - if params.AuthMethod == pam.AwsIamAuthMethod { - token, err := pam.BuildRdsAuthToken(ctx, pam.RdsAuthTokenParams{ - Host: host, - Port: port, - Region: params.AwsRegion, - DBUser: params.Username, - RoleArn: params.RoleArn, - SessionName: "infisical-pam-connection-test", - }) - if err != nil { - return err - } - params.Password = token - } - db, err := openSQLTestDB(host, port, params) if err != nil { return err diff --git a/packages/pam/aws_rds_auth.go b/packages/pam/aws_rds_auth.go deleted file mode 100644 index 040d642f..00000000 --- a/packages/pam/aws_rds_auth.go +++ /dev/null @@ -1,87 +0,0 @@ -package pam - -import ( - "context" - "fmt" - "sync" - "time" - - "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/config" - "github.com/aws/aws-sdk-go-v2/credentials/stscreds" - "github.com/aws/aws-sdk-go-v2/feature/rds/auth" - "github.com/aws/aws-sdk-go-v2/service/sts" -) - -// AwsIamAuthMethod is the credential auth method the server sends when the gateway, rather than -// Infisical, produces the login token. Any other value means the credential carries its own secret. -const AwsIamAuthMethod = "aws-iam" - -const rdsAuthTokenTimeout = 15 * time.Second - -// Reused across connections: resolving it fetches the gateway's own credentials from pod identity or -// instance metadata, which is a network round trip we should not repeat on every dial. Bounded by the -// number of regions an org's accounts live in. -var regionConfigs sync.Map - -func loadRegionConfig(ctx context.Context, region string) (aws.Config, error) { - if cached, ok := regionConfigs.Load(region); ok { - return cached.(aws.Config), nil - } - - cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(region)) - if err != nil { - return aws.Config{}, err - } - - regionConfigs.Store(region, cfg) - return cfg, nil -} - -type RdsAuthTokenParams struct { - Host string - Port int - Region string - DBUser string - RoleArn string - SessionName string -} - -// BuildRdsAuthToken mints an RDS/Aurora IAM authentication token for a single connection. The token is -// a SigV4 signature over the exact endpoint being dialled, so it is never reusable elsewhere, and it -// is never cached or written to disk. -// -// The gateway assumes the account's role using credentials from its own environment (pod identity, -// instance role, env vars, profile), which keeps the authority to reach a database inside the -// customer's account and scoped to one PAM account. A missing role is an error rather than a fall back -// to the gateway's own identity, which would quietly widen that scope. -func BuildRdsAuthToken(ctx context.Context, params RdsAuthTokenParams) (string, error) { - if params.Region == "" { - return "", fmt.Errorf("no AWS region configured for this account") - } - if params.RoleArn == "" { - return "", fmt.Errorf("no IAM role configured for this account") - } - - ctx, cancel := context.WithTimeout(ctx, rdsAuthTokenTimeout) - defer cancel() - - cfg, err := loadRegionConfig(ctx, params.Region) - if err != nil { - return "", fmt.Errorf("unable to load AWS credentials on the gateway (no pod identity, instance role, or AWS environment variables): %w", err) - } - - credentials := aws.NewCredentialsCache( - stscreds.NewAssumeRoleProvider(sts.NewFromConfig(cfg), params.RoleArn, func(o *stscreds.AssumeRoleOptions) { - o.RoleSessionName = params.SessionName - }), - ) - - endpoint := fmt.Sprintf("%s:%d", params.Host, params.Port) - token, err := auth.BuildAuthToken(ctx, endpoint, params.Region, params.DBUser, credentials) - if err != nil { - return "", fmt.Errorf("failed to generate an AWS IAM authentication token as %s: %w", params.RoleArn, err) - } - - return token, nil -} diff --git a/packages/pam/pam-proxy.go b/packages/pam/pam-proxy.go index 2c457e0e..d0f72461 100644 --- a/packages/pam/pam-proxy.go +++ b/packages/pam/pam-proxy.go @@ -184,6 +184,13 @@ func HandlePAMProxy(ctx context.Context, conn *tls.Conn, pamConfig *GatewayPAMCo credentialExpiryTime = cloudTokenMaxLifetime } } + // An RDS IAM login's password is a 15-minute token, so a connection opened late in a long session + // must not reuse the one cached at session start. + if pamConfig.ResourceType == session.ResourceTypePostgres { + if rdsTokenMaxLifetime := time.Now().Add(10 * time.Minute); rdsTokenMaxLifetime.Before(credentialExpiryTime) { + credentialExpiryTime = rdsTokenMaxLifetime + } + } credentials, err := pamConfig.CredentialsManager.GetPAMSessionCredentials(pamConfig.SessionId, credentialExpiryTime) if err != nil { @@ -287,27 +294,10 @@ func HandlePAMProxy(ctx context.Context, conn *tls.Conn, pamConfig *GatewayPAMCo switch pamConfig.ResourceType { case session.ResourceTypePostgres: - injectPassword := credentials.Password - if credentials.AuthMethod == AwsIamAuthMethod { - token, err := BuildRdsAuthToken(ctx, RdsAuthTokenParams{ - Host: credentials.Host, - Port: credentials.Port, - Region: credentials.AwsRegion, - DBUser: credentials.Username, - RoleArn: credentials.RoleArn, - SessionName: fmt.Sprintf("infisical-pam-%s", pamConfig.SessionId), - }) - if err != nil { - log.Error().Err(err).Str("sessionId", pamConfig.SessionId).Msg("Failed to mint AWS IAM authentication token") - return err - } - injectPassword = token - } - proxyConfig := handlers.PostgresProxyConfig{ TargetAddr: fmt.Sprintf("%s:%d", credentials.Host, credentials.Port), InjectUsername: credentials.Username, - InjectPassword: injectPassword, + InjectPassword: credentials.Password, InjectDatabase: credentials.Database, EnableTLS: credentials.SSLEnabled, TLSConfig: tlsConfig, diff --git a/packages/pam/session/credentials.go b/packages/pam/session/credentials.go index 5eb15176..c8691353 100644 --- a/packages/pam/session/credentials.go +++ b/packages/pam/session/credentials.go @@ -21,8 +21,6 @@ type PAMCredentials struct { AuthMethod string Username string Password string - AwsRegion string - RoleArn string Database string ConnectionString string // MongoDB: full URI (mongodb[+srv]://...) PrivateKey string @@ -182,8 +180,6 @@ func (cm *CredentialsManager) GetPAMSessionCredentials(sessionId string, expiryT AuthMethod: response.Credentials.AuthMethod, Username: response.Credentials.Username, Password: response.Credentials.Password, - AwsRegion: response.Credentials.AwsRegion, - RoleArn: response.Credentials.RoleArn, Database: response.Credentials.Database, ConnectionString: response.Credentials.ConnectionString, PrivateKey: response.Credentials.PrivateKey, From 845247b8ce110c29d2088b8d008321aea9a64f5e Mon Sep 17 00:00:00 2001 From: Andrey Date: Thu, 20 Aug 2026 19:45:56 -0400 Subject: [PATCH 3/3] address review --- packages/pam/pam-proxy.go | 7 ------- packages/pam/session/credentials.go | 13 ++++++++++++- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/pam/pam-proxy.go b/packages/pam/pam-proxy.go index d0f72461..2ff2653a 100644 --- a/packages/pam/pam-proxy.go +++ b/packages/pam/pam-proxy.go @@ -184,13 +184,6 @@ func HandlePAMProxy(ctx context.Context, conn *tls.Conn, pamConfig *GatewayPAMCo credentialExpiryTime = cloudTokenMaxLifetime } } - // An RDS IAM login's password is a 15-minute token, so a connection opened late in a long session - // must not reuse the one cached at session start. - if pamConfig.ResourceType == session.ResourceTypePostgres { - if rdsTokenMaxLifetime := time.Now().Add(10 * time.Minute); rdsTokenMaxLifetime.Before(credentialExpiryTime) { - credentialExpiryTime = rdsTokenMaxLifetime - } - } credentials, err := pamConfig.CredentialsManager.GetPAMSessionCredentials(pamConfig.SessionId, credentialExpiryTime) if err != nil { diff --git a/packages/pam/session/credentials.go b/packages/pam/session/credentials.go index c8691353..b255d9f2 100644 --- a/packages/pam/session/credentials.go +++ b/packages/pam/session/credentials.go @@ -17,6 +17,10 @@ func uploadTokenFilePath(sessionID string) string { return filepath.Join(GetSessionRecordingDir(), "chunks", sessionID+".uploadtoken.enc") } +const AwsIamAuthMethod = "aws-iam" + +const awsIamCredentialTTL = 10 * time.Minute + type PAMCredentials struct { AuthMethod string Username string @@ -203,10 +207,17 @@ func (cm *CredentialsManager) GetPAMSessionCredentials(sessionId string, expiryT PolicyRules: response.PolicyRules, } + cacheExpiry := expiryTime + if credentials.AuthMethod == AwsIamAuthMethod { + if tokenExpiry := time.Now().Add(awsIamCredentialTTL); tokenExpiry.Before(cacheExpiry) { + cacheExpiry = tokenExpiry + } + } + cm.cacheMutex.Lock() cm.credentialsCache[sessionId] = &cachedCredentials{ credentials: credentials, - expiresAt: expiryTime, + expiresAt: cacheExpiry, } cm.cacheMutex.Unlock()