This repository has been archived by the owner on Nov 22, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 16
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Refactor file writing out to decouple from Secrets
This makes a new function which handles atomically writing files with a given mode and permissions. I plan to reuse this code in the future, so this is just the first part of seperation The package is called "output" because I plan to move the Output* code there too, but that requires a bit more refactoring to avoid circular dependencies. So that's another PR for later.
- Loading branch information
1 parent
c6d25b7
commit e85be05
Showing
4 changed files
with
149 additions
and
125 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
package output | ||
|
||
import ( | ||
"crypto/rand" | ||
"encoding/hex" | ||
"fmt" | ||
"os" | ||
"path/filepath" | ||
"syscall" | ||
) | ||
|
||
// FileInfo returns the filesystem properties atomicWrite wrote | ||
type FileInfo struct { | ||
Mode os.FileMode | ||
UID uint32 | ||
GID uint32 | ||
} | ||
|
||
// GetFileInfo from an open file | ||
func GetFileInfo(file *os.File) (*FileInfo, error) { | ||
stat, err := file.Stat() | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to stat after writing: %v", err) | ||
} | ||
filemode := stat.Mode() | ||
uid := stat.Sys().(*syscall.Stat_t).Uid | ||
gid := stat.Sys().(*syscall.Stat_t).Gid | ||
|
||
return &FileInfo{filemode, uid, gid}, nil | ||
} | ||
|
||
// WriteFileAtomically creates a temporary file, sets perms, writes content, and renames it to filename | ||
// This sequence ensures the following: | ||
// 1. Nobody can open the file before we set owner/permissions properly | ||
// 2. Nobody observes a partially-overwritten secret file. | ||
func WriteFileAtomically(dir, filename string, mode os.FileMode, chownFiles bool, uid, gid int, enforceFilesystem Filesystem, content []byte) (*FileInfo, error) { | ||
if err := os.MkdirAll(dir, 0775); err != nil { | ||
return nil, fmt.Errorf("making client directory '%s': %v", dir, err) | ||
} | ||
|
||
// We can't use ioutil.TempFile because we want to open 0000. | ||
buf := make([]byte, 32) | ||
_, err := rand.Read(buf) | ||
if err != nil { | ||
return nil, err | ||
} | ||
randSuffix := hex.EncodeToString(buf) | ||
fullPath := filepath.Join(dir, filename) | ||
f, err := os.OpenFile(fullPath+randSuffix, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0000) | ||
// Try to remove the file, in event we early-return with an error. | ||
defer os.Remove(fullPath + randSuffix) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
if chownFiles { | ||
err = f.Chown(uid, gid) | ||
if err != nil { | ||
return nil, err | ||
} | ||
} | ||
|
||
// Always Chmod after the Chown, so we don't expose secret with the wrong owner. | ||
err = f.Chmod(mode) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
if enforceFilesystem != 0 { | ||
good, err := isFilesystem(f, enforceFilesystem) | ||
if err != nil { | ||
return nil, fmt.Errorf("checking filesystem type: %v", err) | ||
} | ||
if !good { | ||
return nil, fmt.Errorf("unexpected filesystem writing %s", filename) | ||
} | ||
} | ||
_, err = f.Write(content) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed writing filesystem content: %v", err) | ||
} | ||
|
||
fileinfo, err := GetFileInfo(f) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to get file mode back from file: %v", err) | ||
} | ||
|
||
// While this is intended for use with tmpfs, you could write secrets to disk. | ||
// We ignore any errors from syncing, as it's not strictly required. | ||
_ = f.Sync() | ||
|
||
// Rename is atomic, so nobody will observe a partially updated secret | ||
err = os.Rename(fullPath+randSuffix, fullPath) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return fileinfo, nil | ||
} | ||
|
||
// The Filesystem identification. On Mac, this is uint32, and int64 on linux | ||
// So both are safe to store as an int64. | ||
// Linux Tmpfs = 0x01021994 | ||
// Get these constants with `stat --file-system --format=%t` | ||
type Filesystem int64 | ||
|
||
func isFilesystem(file *os.File, fs Filesystem) (bool, error) { | ||
var statfs syscall.Statfs_t | ||
err := syscall.Fstatfs(int(file.Fd()), &statfs) | ||
return Filesystem(statfs.Type) == fs, err | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters