feat: secure Windows client spool state
This commit is contained in:
@@ -50,7 +50,7 @@ func writeIdentityAtomically(path string, data []byte) (result error) {
|
|||||||
_ = os.Remove(temporaryPath)
|
_ = os.Remove(temporaryPath)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
if err := temporary.Chmod(0o600); err != nil {
|
if err := securePrivateFile(temporaryPath); err != nil {
|
||||||
_ = temporary.Close()
|
_ = temporary.Close()
|
||||||
return fmt.Errorf("secure client identity temporary file: %w", err)
|
return fmt.Errorf("secure client identity temporary file: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package spool
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ensurePrivateFile(path string) error {
|
||||||
|
file, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_RDWR, 0o600)
|
||||||
|
if err == nil {
|
||||||
|
return file.Close()
|
||||||
|
}
|
||||||
|
if !errors.Is(err, os.ErrExist) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
info, err := os.Lstat(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 {
|
||||||
|
return fmt.Errorf("%w: %s is not a regular file", ErrUnsafeDataDirectory, path)
|
||||||
|
}
|
||||||
|
if info.Mode().Perm()&0o077 != 0 {
|
||||||
|
return fmt.Errorf("%w: %s permissions %04o expose private state", ErrUnsafeDataDirectory, path, info.Mode().Perm())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensurePrivateDirectory(path string) error {
|
||||||
|
info, err := os.Lstat(path)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
if err := os.MkdirAll(path, 0o700); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
info, err = os.Lstat(path)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if info.Mode()&os.ModeSymlink != 0 || !info.IsDir() {
|
||||||
|
return fmt.Errorf("%w: %s is not a real directory", ErrUnsafeDataDirectory, path)
|
||||||
|
}
|
||||||
|
if info.Mode().Perm()&0o077 != 0 {
|
||||||
|
return fmt.Errorf("%w: %s permissions %04o expose private state", ErrUnsafeDataDirectory, path, info.Mode().Perm())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func securePrivateFile(path string) error { return os.Chmod(path, 0o600) }
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package spool
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
const privateStateSDDL = "D:P(A;;FA;;;SY)(A;;FA;;;BA)"
|
||||||
|
|
||||||
|
// Windows service state is owned by LocalSystem. The DACL is protected against
|
||||||
|
// parent inheritance and grants full access only to SYSTEM and Administrators;
|
||||||
|
// unprivileged execution contexts access it solely through the service.
|
||||||
|
func ensurePrivateFile(path string) error {
|
||||||
|
file, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_RDWR, 0o600)
|
||||||
|
if err == nil {
|
||||||
|
err = file.Close()
|
||||||
|
} else if !errors.Is(err, os.ErrExist) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
info, err := os.Lstat(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 || hasReparsePoint(path) {
|
||||||
|
return fmt.Errorf("%w: %s is not a regular private file", ErrUnsafeDataDirectory, path)
|
||||||
|
}
|
||||||
|
return applyPrivateStateACL(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensurePrivateDirectory(path string) error {
|
||||||
|
info, err := os.Lstat(path)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
if err := os.MkdirAll(path, 0o700); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
info, err = os.Lstat(path)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 || hasReparsePoint(path) {
|
||||||
|
return fmt.Errorf("%w: %s is not a real private directory", ErrUnsafeDataDirectory, path)
|
||||||
|
}
|
||||||
|
return applyPrivateStateACL(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func securePrivateFile(path string) error { return applyPrivateStateACL(path) }
|
||||||
|
|
||||||
|
func hasReparsePoint(path string) bool {
|
||||||
|
value, err := windows.UTF16PtrFromString(path)
|
||||||
|
if err != nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
attributes, err := windows.GetFileAttributes(value)
|
||||||
|
return err != nil || attributes&windows.FILE_ATTRIBUTE_REPARSE_POINT != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyPrivateStateACL(path string) error {
|
||||||
|
descriptor, err := windows.SecurityDescriptorFromString(privateStateSDDL)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dacl, _, err := descriptor.DACL()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return windows.SetNamedSecurityInfo(path, windows.SE_FILE_OBJECT, windows.DACL_SECURITY_INFORMATION|windows.PROTECTED_DACL_SECURITY_INFORMATION, nil, nil, dacl, nil)
|
||||||
|
}
|
||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -145,45 +144,4 @@ func (store *Store) Close() error {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func ensurePrivateFile(path string) error {
|
|
||||||
file, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_RDWR, 0o600)
|
|
||||||
if err == nil {
|
|
||||||
return file.Close()
|
|
||||||
}
|
|
||||||
if !errors.Is(err, os.ErrExist) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
info, err := os.Lstat(path)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 {
|
|
||||||
return fmt.Errorf("%w: %s is not a regular file", ErrUnsafeDataDirectory, path)
|
|
||||||
}
|
|
||||||
if info.Mode().Perm()&0o077 != 0 {
|
|
||||||
return fmt.Errorf("%w: %s permissions %04o expose private state", ErrUnsafeDataDirectory, path, info.Mode().Perm())
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ensurePrivateDirectory(path string) error {
|
|
||||||
info, err := os.Lstat(path)
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
if err := os.MkdirAll(path, 0o700); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
info, err = os.Lstat(path)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if info.Mode()&os.ModeSymlink != 0 || !info.IsDir() {
|
|
||||||
return fmt.Errorf("%w: %s is not a real directory", ErrUnsafeDataDirectory, path)
|
|
||||||
}
|
|
||||||
if info.Mode().Perm()&0o077 != 0 {
|
|
||||||
return fmt.Errorf("%w: %s permissions %04o expose private state", ErrUnsafeDataDirectory, path, info.Mode().Perm())
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func immutableDigest(payload []byte) [32]byte { return sha256.Sum256(payload) }
|
func immutableDigest(payload []byte) [32]byte { return sha256.Sum256(payload) }
|
||||||
|
|||||||
Reference in New Issue
Block a user