From 0a3123964d22eecfb8ce7c994f09955fd85802c5 Mon Sep 17 00:00:00 2001 From: Caio Rocha de Oliveira Date: Sun, 3 May 2026 15:57:01 -0300 Subject: [PATCH 1/3] feat(hcvault): support environment variable for vault token file Signed-off-by: Caio Rocha de Oliveira --- hcvault/keysource.go | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/hcvault/keysource.go b/hcvault/keysource.go index 67706e71e3..c75bc648de 100644 --- a/hcvault/keysource.go +++ b/hcvault/keysource.go @@ -40,6 +40,9 @@ var ( // defaultTokenFile is the name of the file in the user's home directory // where a Vault token is expected to be stored. defaultTokenFile = ".vault-token" + // SopsVaultTokenFileEnv can be set as an environment variable pointing to a + // vault token file. + SopsVaultTokenFileEnv = "SOPS_VAULT_TOKEN_FILE" ) // Token used for authenticating towards a Vault server. @@ -364,12 +367,28 @@ func userVaultToken() (string, error) { if err != nil { return "", fmt.Errorf("error getting user's home directory: %w", err) } - tokenPath := filepath.Join(homePath, defaultTokenFile) + + var tokenPath string + + if tokenPathEnv, ok := os.LookupEnv(SopsVaultTokenFileEnv); ok { + if filepath.IsAbs(tokenPathEnv) { + tokenPath = tokenPathEnv + } else { + tokenPath = filepath.Join(homePath, tokenPathEnv) + } + + } else { + tokenPath = filepath.Join(homePath, defaultTokenFile) + } f, err := os.Open(tokenPath) if err != nil { if errors.Is(err, os.ErrNotExist) { - return "", nil + if tokenPathEnv, ok := os.LookupEnv(SopsVaultTokenFileEnv); ok { + return "", fmt.Errorf("token file specified in %s environment variable does not exist: %s", SopsVaultTokenFileEnv, tokenPathEnv) + } else { + return "", nil + } } return "", err } From a3c306084cd9cd6ed8a9416044a188994f09543d Mon Sep 17 00:00:00 2001 From: Caio Rocha de Oliveira Date: Sun, 3 May 2026 16:16:04 -0300 Subject: [PATCH 2/3] fix(hcvault): bad defaults, better docstring, new names Signed-off-by: Caio Rocha de Oliveira --- hcvault/keysource.go | 34 +++++++++++++++------------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/hcvault/keysource.go b/hcvault/keysource.go index c75bc648de..ca2f3faf5a 100644 --- a/hcvault/keysource.go +++ b/hcvault/keysource.go @@ -359,36 +359,32 @@ func vaultClient(address, token string, hc *http.Client) (*api.Client, error) { return client, nil } -// userVaultToken returns the token from `$HOME/.vault-token` if the file -// exists. It returns an error if the file exists but cannot be read from. -// If the file does not exist, it returns an empty string. +// userVaultToken attempts to read the Vault token from the file specified in +// the SOPS_VAULT_TOKEN_FILE environment variable, or from the default location in +// $HOME/.vault-token if the environment variable is not set. It returns the +// token, or an error if the file cannot be read. func userVaultToken() (string, error) { - homePath, err := homedir.Dir() - if err != nil { - return "", fmt.Errorf("error getting user's home directory: %w", err) - } - var tokenPath string + isTokenEnvSet := false - if tokenPathEnv, ok := os.LookupEnv(SopsVaultTokenFileEnv); ok { - if filepath.IsAbs(tokenPathEnv) { - tokenPath = tokenPathEnv - } else { - tokenPath = filepath.Join(homePath, tokenPathEnv) - } - + if tokenPathEnv, ok := os.LookupEnv(SopsVaultTokenFileEnv); ok && tokenPathEnv != "" { + tokenPath = tokenPathEnv + isTokenEnvSet = true } else { + homePath, err := homedir.Dir() + if err != nil { + return "", fmt.Errorf("error getting user's home directory: %w", err) + } tokenPath = filepath.Join(homePath, defaultTokenFile) } f, err := os.Open(tokenPath) if err != nil { if errors.Is(err, os.ErrNotExist) { - if tokenPathEnv, ok := os.LookupEnv(SopsVaultTokenFileEnv); ok { - return "", fmt.Errorf("token file specified in %s environment variable does not exist: %s", SopsVaultTokenFileEnv, tokenPathEnv) - } else { - return "", nil + if isTokenEnvSet { + return "", fmt.Errorf("token file specified in %s environment variable does not exist: %s", SopsVaultTokenFileEnv, tokenPath) } + return "", nil } return "", err } From 2f966e22471e5b8906d4271da564d1485f6cc80c Mon Sep 17 00:00:00 2001 From: Caio Rocha de Oliveira Date: Sun, 3 May 2026 17:12:03 -0300 Subject: [PATCH 3/3] fix(hcvault): stream safe reading for multiple stream keys Signed-off-by: Caio Rocha de Oliveira --- cmd/sops/main.go | 2 ++ hcvault/keysource.go | 53 +++++++++++++++++++++++++++++++++++--------- 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/cmd/sops/main.go b/cmd/sops/main.go index 330c3bc8eb..55c1165d7f 100644 --- a/cmd/sops/main.go +++ b/cmd/sops/main.go @@ -75,6 +75,8 @@ func warnMoreThanOnePositionalArgument(c *cli.Context) { } func main() { + defer hcvault.ClearFileStreamCache() + cli.VersionPrinter = version.PrintVersion app := cli.NewApp() diff --git a/hcvault/keysource.go b/hcvault/keysource.go index ca2f3faf5a..be5ddc67a3 100644 --- a/hcvault/keysource.go +++ b/hcvault/keysource.go @@ -1,12 +1,10 @@ package hcvault import ( - "bytes" "context" "encoding/base64" "errors" "fmt" - "io" "net/http" "net/url" "os" @@ -14,6 +12,7 @@ import ( "path/filepath" "regexp" "strings" + "sync" "time" "github.com/hashicorp/vault/api" @@ -43,6 +42,9 @@ var ( // SopsVaultTokenFileEnv can be set as an environment variable pointing to a // vault token file. SopsVaultTokenFileEnv = "SOPS_VAULT_TOKEN_FILE" + // vaultTokenStreamCache is a cache for vault token file streams, to avoid + // EOF errors when multiple vault keys attempt to read the same ephemeral token. + vaultTokenStreamCache sync.Map ) // Token used for authenticating towards a Vault server. @@ -359,6 +361,41 @@ func vaultClient(address, token string, hc *http.Client) (*api.Client, error) { return client, nil } +// readTokenStreamSafe reads a file from the given path. +// If it is a stream, it reads the content and caches it in memory. +func readTokenStreamSafe(path string) ([]byte, error) { + fileInfo, err := os.Stat(path) + isStream := err == nil && (fileInfo.Mode()&os.ModeNamedPipe != 0 || fileInfo.Mode()&os.ModeCharDevice != 0) + + if isStream { + if cached, ok := vaultTokenStreamCache.Load(path); ok { + return cached.([]byte), nil + } + } + + b, err := os.ReadFile(path) + if err == nil && isStream { + vaultTokenStreamCache.Store(path, b) + } + return b, err +} + +// ClearFileStreamCache clears the cache for vault token file streams +// zeroing out the byte slices before deleting them from the cache to prevent +// sensitive data from lingering in memory. +func ClearFileStreamCache() { + vaultTokenStreamCache.Range(func(key, value any) bool { + if byte, ok := value.([]byte); ok { + for i := range byte { + byte[i] = 0 + } + } + vaultTokenStreamCache.Delete(key) + return true + }) + +} + // userVaultToken attempts to read the Vault token from the file specified in // the SOPS_VAULT_TOKEN_FILE environment variable, or from the default location in // $HOME/.vault-token if the environment variable is not set. It returns the @@ -378,23 +415,17 @@ func userVaultToken() (string, error) { tokenPath = filepath.Join(homePath, defaultTokenFile) } - f, err := os.Open(tokenPath) + b, err := readTokenStreamSafe(tokenPath) if err != nil { if errors.Is(err, os.ErrNotExist) { if isTokenEnvSet { - return "", fmt.Errorf("token file specified in %s environment variable does not exist: %s", SopsVaultTokenFileEnv, tokenPath) + return "", fmt.Errorf("token file specified in %s does not exist: %w", SopsVaultTokenFileEnv, err) } return "", nil } return "", err } - defer f.Close() - - buf := bytes.NewBuffer(nil) - if _, err := io.Copy(buf, f); err != nil { - return "", err - } - return strings.TrimSpace(buf.String()), nil + return strings.TrimSpace(string(b)), nil } // engineAndKeyFromPath returns the engine path and key name from the full