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 c60e11cfcb..bf2628f11b 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" @@ -107,6 +106,12 @@ 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" + // 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. @@ -431,30 +436,71 @@ 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. +// 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 +// 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 && 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) } - 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 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