Harden CLI config persistence
This commit is contained in:
+41
-1
@@ -1801,7 +1801,47 @@ func writeConfig(path string, cfg cliConfig) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return os.WriteFile(path, append(raw, '\n'), 0o600)
|
return writeFileAtomic0600(path, append(raw, '\n'))
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeFileAtomic0600(path string, body []byte) error {
|
||||||
|
dir := filepath.Dir(path)
|
||||||
|
tmp, err := os.CreateTemp(dir, "."+filepath.Base(path)+".tmp-")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
tmpName := tmp.Name()
|
||||||
|
removeTmp := true
|
||||||
|
defer func() {
|
||||||
|
if removeTmp {
|
||||||
|
_ = os.Remove(tmpName)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if err := tmp.Chmod(0o600); err != nil {
|
||||||
|
_ = tmp.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
written, err := tmp.Write(body)
|
||||||
|
if err != nil {
|
||||||
|
_ = tmp.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if written != len(body) {
|
||||||
|
_ = tmp.Close()
|
||||||
|
return fmt.Errorf("short atomic write: wrote %d of %d bytes", written, len(body))
|
||||||
|
}
|
||||||
|
if err := tmp.Sync(); err != nil {
|
||||||
|
_ = tmp.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := tmp.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := os.Rename(tmpName, path); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
removeTmp = false
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func redactValue(value any) any {
|
func redactValue(value any) any {
|
||||||
|
|||||||
@@ -156,6 +156,36 @@ func TestConfigSetRedactsSecrets(t *testing.T) {
|
|||||||
if !strings.Contains(string(raw), cliTestAPIKey) {
|
if !strings.Contains(string(raw), cliTestAPIKey) {
|
||||||
t.Fatalf("config did not persist api key")
|
t.Fatalf("config did not persist api key")
|
||||||
}
|
}
|
||||||
|
info, err := os.Stat(config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if info.Mode().Perm() != 0o600 {
|
||||||
|
t.Fatalf("config mode = %o, want 600", info.Mode().Perm())
|
||||||
|
}
|
||||||
|
matches, err := filepath.Glob(filepath.Join(dir, ".config.json.tmp-*"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(matches) != 0 {
|
||||||
|
t.Fatalf("atomic config temp files leaked: %v", matches)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigSetFailsWhenConfigPathIsDirectory(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
env := testEnv(t, map[string]string{
|
||||||
|
"ATTESTO_CONFIG": dir,
|
||||||
|
"ATT_API_KEY": cliTestAPIKey,
|
||||||
|
})
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
code := run([]string{"--json", "config", "set", "--api-key-env", "ATT_API_KEY"}, &stdout, &stderr, env)
|
||||||
|
if code == 0 {
|
||||||
|
t.Fatal("config set must fail when config path is a directory")
|
||||||
|
}
|
||||||
|
if strings.Contains(stdout.String(), cliTestAPIKey) || strings.Contains(stderr.String(), cliTestAPIKey) {
|
||||||
|
t.Fatalf("secret leaked on write failure: stdout=%s stderr=%s", stdout.String(), stderr.String())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLocalVaultSpoolAndStatus(t *testing.T) {
|
func TestLocalVaultSpoolAndStatus(t *testing.T) {
|
||||||
|
|||||||
Reference in New Issue
Block a user