From 487e5d0bbad975feb2640c7e4869547ceb59bad0 Mon Sep 17 00:00:00 2001 From: saif <11242541+saifsmailbox98@users.noreply.github.com> Date: Sat, 27 Jun 2026 02:03:34 +0530 Subject: [PATCH 1/3] feat(pam): add AWS IAM CLI access Write temporary STS credentials to ~/.aws/credentials under a named profile (infisical-pam//). Credentials are cleaned up on Ctrl+C or session expiry. Follows the same pattern as Kubernetes kubeconfig management. --- packages/pam/local/access.go | 2 +- packages/pam/local/aws-iam-access.go | 171 +++++++++++++++++++++++++++ 2 files changed, 172 insertions(+), 1 deletion(-) create mode 100644 packages/pam/local/aws-iam-access.go diff --git a/packages/pam/local/access.go b/packages/pam/local/access.go index 41284164..b3437a3d 100644 --- a/packages/pam/local/access.go +++ b/packages/pam/local/access.go @@ -90,7 +90,7 @@ func StartPAMAccess(accessToken, path, reason, durationStr string, port int) { case AccountTypeKubernetes: startKubernetesProxy(httpClient, &pamResponse, displayPath, durationStr, port) case AccountTypeAwsIam: - util.PrintErrorMessageAndExit("AWS IAM access not yet supported in the new PAM model") + startAWSAccess(httpClient, &pamResponse, displayPath, durationStr, port) case AccountTypeWindows: startRDPProxy(httpClient, &pamResponse, displayPath, durationStr, port) case AccountTypeActiveDirectory: diff --git a/packages/pam/local/aws-iam-access.go b/packages/pam/local/aws-iam-access.go new file mode 100644 index 00000000..c310f9ac --- /dev/null +++ b/packages/pam/local/aws-iam-access.go @@ -0,0 +1,171 @@ +package pam + +import ( + "fmt" + "os" + "os/signal" + "path/filepath" + "syscall" + "time" + + "github.com/go-resty/resty/v2" + "github.com/rs/zerolog/log" + "gopkg.in/ini.v1" + + "github.com/Infisical/infisical-merge/packages/api" + "github.com/Infisical/infisical-merge/packages/util" +) + +func startAWSAccess(_ *resty.Client, response *api.PAMAccessResponse, path, _ string, _ int) { + expiresAtStr := response.Metadata["expiresAt"] + accessKeyId := response.Metadata["accessKeyId"] + secretAccessKey := response.Metadata["secretAccessKey"] + sessionToken := response.Metadata["sessionToken"] + + if accessKeyId == "" || secretAccessKey == "" || sessionToken == "" || expiresAtStr == "" { + util.PrintErrorMessageAndExit("Backend did not return AWS IAM credentials in session metadata") + return + } + + expiresAt, err := time.Parse(time.RFC3339, expiresAtStr) + if err != nil { + util.PrintErrorMessageAndExit(fmt.Sprintf("Failed to parse credential expiry time: %v", err)) + return + } + + folder, account := parsePath(path) + profileName := fmt.Sprintf("infisical-pam/%s/%s", folder, account) + + credFilePath := awsCredentialsFilePath() + createdFile := false + + dir := filepath.Dir(credFilePath) + if err := os.MkdirAll(dir, 0o700); err != nil { + util.PrintErrorMessageAndExit(fmt.Sprintf("Failed to create directory %s: %v", dir, err)) + return + } + + if _, statErr := os.Stat(credFilePath); os.IsNotExist(statErr) { + createdFile = true + } + + cfg, err := ini.LooseLoad(credFilePath) + if err != nil { + util.PrintErrorMessageAndExit(fmt.Sprintf("Failed to load AWS credentials file: %v", err)) + return + } + + section, err := cfg.NewSection(profileName) + if err != nil { + util.PrintErrorMessageAndExit(fmt.Sprintf("Failed to create AWS credentials profile: %v", err)) + return + } + section.Key("aws_access_key_id").SetValue(accessKeyId) + section.Key("aws_secret_access_key").SetValue(secretAccessKey) + section.Key("aws_session_token").SetValue(sessionToken) + + if err := cfg.SaveTo(credFilePath); err != nil { + util.PrintErrorMessageAndExit(fmt.Sprintf("Failed to write AWS credentials file: %v", err)) + return + } + + if createdFile { + _ = os.Chmod(credFilePath, 0o600) + } + + log.Info().Str("profile", profileName).Str("file", credFilePath).Msg("AWS credentials written") + + remaining := time.Until(expiresAt) + printAWSSessionInfo(folder, account, remaining, profileName, expiresAt) + + cleanup := func() { + removeAWSProfile(credFilePath, profileName, createdFile) + } + + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + + select { + case sig := <-sigChan: + log.Info().Msgf("Received signal %v, cleaning up...", sig) + cleanup() + case <-time.After(remaining): + fmt.Printf("\n AWS session expired. Cleaning up credentials...\n\n") + cleanup() + } +} + +func removeAWSProfile(credFilePath, profileName string, createdFile bool) { + cfg, err := ini.LooseLoad(credFilePath) + if err != nil { + log.Error().Err(err).Msg("Failed to load AWS credentials file for cleanup") + return + } + + cfg.DeleteSection(profileName) + + // If we created the file and it's now empty (only DEFAULT section with no keys), remove it + if createdFile && len(cfg.Sections()) <= 1 && len(cfg.Section("DEFAULT").Keys()) == 0 { + if removeErr := os.Remove(credFilePath); removeErr != nil { + log.Error().Err(removeErr).Msg("Failed to remove AWS credentials file") + } else { + log.Info().Str("file", credFilePath).Msg("Removed AWS credentials file (created by this session)") + } + return + } + + if err := cfg.SaveTo(credFilePath); err != nil { + log.Error().Err(err).Msg("Failed to save AWS credentials file after cleanup") + return + } + + log.Info().Str("profile", profileName).Msg("Removed AWS credentials profile") +} + +func awsCredentialsFilePath() string { + if envPath := os.Getenv("AWS_SHARED_CREDENTIALS_FILE"); envPath != "" { + return envPath + } + home, err := os.UserHomeDir() + if err != nil { + return filepath.Join(".", ".aws", "credentials") + } + return filepath.Join(home, ".aws", "credentials") +} + +func printAWSSessionInfo(folder, account string, duration time.Duration, profileName string, expiresAt time.Time) { + fmt.Printf("\n") + fmt.Printf("**********************************************************************\n") + fmt.Printf(" AWS IAM 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.Round(time.Second).String()) + fmt.Printf(" Expires: %s\n", expiresAt.Local().Format("2006-01-02 15:04:05 MST")) + fmt.Printf("\n") + fmt.Printf("----------------------------------------------------------------------\n") + fmt.Printf(" Connection Details \n") + fmt.Printf("----------------------------------------------------------------------\n") + fmt.Printf("\n") + fmt.Printf(" AWS credentials written to: %s\n", awsCredentialsFilePath()) + fmt.Printf(" Profile name: %s\n", profileName) + fmt.Printf("\n") + fmt.Printf("----------------------------------------------------------------------\n") + fmt.Printf(" How to Connect \n") + fmt.Printf("----------------------------------------------------------------------\n") + fmt.Printf("\n") + fmt.Printf(" Use the AWS CLI with the profile:\n") + util.PrintfStderr(" $ aws s3 ls --profile \"%s\"\n", profileName) + fmt.Printf("\n") + fmt.Printf(" Or set the AWS_PROFILE environment variable:\n") + util.PrintfStderr(" $ export AWS_PROFILE=\"%s\"\n", profileName) + util.PrintfStderr(" $ aws sts get-caller-identity\n") + fmt.Printf("\n") + fmt.Printf(" Press Ctrl+C to stop and remove the credentials profile.\n") + fmt.Printf("\n") + fmt.Printf("**********************************************************************\n") + fmt.Printf("\n") +} From cf989d948be3b60269a4157738aa9ec48d0ff026 Mon Sep 17 00:00:00 2001 From: saif <11242541+saifsmailbox98@users.noreply.github.com> Date: Sat, 27 Jun 2026 03:16:22 +0530 Subject: [PATCH 2/3] fix(pam): handle stale AWS credentials profile from previous crash Use cfg.Section() instead of cfg.NewSection() so that a leftover profile from a killed session is silently overwritten instead of causing a fatal error. Also always chmod credentials file to 0600 after write (not just when creating it), and guard against negative remaining duration from clock skew. --- packages/pam/local/aws-iam-access.go | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/pam/local/aws-iam-access.go b/packages/pam/local/aws-iam-access.go index c310f9ac..425ab2ee 100644 --- a/packages/pam/local/aws-iam-access.go +++ b/packages/pam/local/aws-iam-access.go @@ -55,11 +55,7 @@ func startAWSAccess(_ *resty.Client, response *api.PAMAccessResponse, path, _ st return } - section, err := cfg.NewSection(profileName) - if err != nil { - util.PrintErrorMessageAndExit(fmt.Sprintf("Failed to create AWS credentials profile: %v", err)) - return - } + section := cfg.Section(profileName) section.Key("aws_access_key_id").SetValue(accessKeyId) section.Key("aws_secret_access_key").SetValue(secretAccessKey) section.Key("aws_session_token").SetValue(sessionToken) @@ -69,13 +65,15 @@ func startAWSAccess(_ *resty.Client, response *api.PAMAccessResponse, path, _ st return } - if createdFile { - _ = os.Chmod(credFilePath, 0o600) - } + _ = os.Chmod(credFilePath, 0o600) log.Info().Str("profile", profileName).Str("file", credFilePath).Msg("AWS credentials written") remaining := time.Until(expiresAt) + if remaining <= 0 { + util.PrintErrorMessageAndExit("AWS credentials returned by the backend are already expired") + return + } printAWSSessionInfo(folder, account, remaining, profileName, expiresAt) cleanup := func() { From 7b9ba1a52b152343ee300062a6f773c554f31637 Mon Sep 17 00:00:00 2001 From: saif <11242541+saifsmailbox98@users.noreply.github.com> Date: Sat, 27 Jun 2026 04:26:42 +0530 Subject: [PATCH 3/3] fix(pam): check credential expiry before writing to disk Move the expiry guard before the file write so expired credentials are never written to ~/.aws/credentials. --- packages/pam/local/aws-iam-access.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/pam/local/aws-iam-access.go b/packages/pam/local/aws-iam-access.go index 425ab2ee..0cc4a289 100644 --- a/packages/pam/local/aws-iam-access.go +++ b/packages/pam/local/aws-iam-access.go @@ -33,6 +33,12 @@ func startAWSAccess(_ *resty.Client, response *api.PAMAccessResponse, path, _ st return } + remaining := time.Until(expiresAt) + if remaining <= 0 { + util.PrintErrorMessageAndExit("AWS credentials returned by the backend are already expired") + return + } + folder, account := parsePath(path) profileName := fmt.Sprintf("infisical-pam/%s/%s", folder, account) @@ -69,11 +75,6 @@ func startAWSAccess(_ *resty.Client, response *api.PAMAccessResponse, path, _ st log.Info().Str("profile", profileName).Str("file", credFilePath).Msg("AWS credentials written") - remaining := time.Until(expiresAt) - if remaining <= 0 { - util.PrintErrorMessageAndExit("AWS credentials returned by the backend are already expired") - return - } printAWSSessionInfo(folder, account, remaining, profileName, expiresAt) cleanup := func() {