fix: harden maintenance operations

This commit is contained in:
proxyctl coordinator
2026-07-17 03:50:18 +08:00
parent d4c76e4b3c
commit b28ebca2d7
9 changed files with 273 additions and 83 deletions
+45 -49
View File
@@ -2,16 +2,15 @@
package oplog
import (
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"
"time"
"proxyctl/internal/app"
"proxyctl/internal/securefs"
"golang.org/x/sys/unix"
)
type Logger struct {
@@ -34,18 +33,6 @@ func (l *Logger) Write(entry app.LogEntry) error {
if l.maxBytes <= 0 {
return nil
}
directory := filepath.Dir(l.path)
if err := os.MkdirAll(directory, 0o700); err != nil {
return fmt.Errorf("create log directory: %w", err)
}
if err := os.Chmod(directory, 0o700); err != nil {
return fmt.Errorf("secure log directory: %w", err)
}
if l.owned && (os.Geteuid() != l.uid || os.Getegid() != l.gid) {
if err := os.Chown(directory, l.uid, l.gid); err != nil {
return fmt.Errorf("set log directory owner: %w", err)
}
}
detail := strings.NewReplacer("\n", " ", "\r", " ").Replace(app.RedactLogDetail(entry.Detail))
if entry.Time.IsZero() {
entry.Time = time.Now()
@@ -58,56 +45,65 @@ func (l *Logger) Write(entry app.LogEntry) error {
}
line = string(truncated)
}
if info, err := os.Stat(l.path); err == nil && info.Size()+int64(len(line)) > l.maxBytes {
if err := l.rotate(); err != nil {
store := l.store()
current, err := readOptional(store, l.path)
if err != nil {
return fmt.Errorf("read operation log securely: %w", err)
}
if int64(len(current)+len(line)) > l.maxBytes {
if err := l.rotate(store, current); err != nil {
return err
}
} else if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("inspect operation log: %w", err)
current = nil
}
file, err := securefs.OpenNoFollow(l.path, unix.O_CREAT|unix.O_APPEND|unix.O_WRONLY|unix.O_NONBLOCK, 0o600)
if err != nil {
return fmt.Errorf("open operation log: %w", err)
}
defer file.Close()
fileInfo, err := file.Stat()
if err != nil {
return fmt.Errorf("inspect operation log: %w", err)
}
if !fileInfo.Mode().IsRegular() {
return fmt.Errorf("operation log is not a regular file")
}
if err := file.Chmod(0o600); err != nil {
return fmt.Errorf("secure operation log: %w", err)
}
if l.owned && (os.Geteuid() != l.uid || os.Getegid() != l.gid) {
if err := file.Chown(l.uid, l.gid); err != nil {
return fmt.Errorf("set operation log owner: %w", err)
}
}
if _, err := file.WriteString(line); err != nil {
return fmt.Errorf("write operation log: %w", err)
current = append(current, line...)
if err := store.AtomicWrite(l.path, current, 0o600); err != nil {
return fmt.Errorf("write operation log atomically: %w", err)
}
return nil
}
func (l *Logger) store() securefs.Store {
store := securefs.Store{TrustedRoot: securefs.ExistingTrustedRoot(l.path)}
if l.owned {
store.Owner = &securefs.Owner{UID: l.uid, GID: l.gid}
}
return store
}
func field(value string) string {
return strings.NewReplacer(" ", "_", "\n", "_", "\r", "_").Replace(value)
}
func (l *Logger) rotate() error {
func (l *Logger) rotate(store securefs.Store, current []byte) error {
if l.rotations <= 0 {
return os.Remove(l.path)
return nil
}
_ = os.Remove(fmt.Sprintf("%s.%d", l.path, l.rotations))
for i := l.rotations - 1; i >= 1; i-- {
oldPath, newPath := fmt.Sprintf("%s.%d", l.path, i), fmt.Sprintf("%s.%d", l.path, i+1)
if err := os.Rename(oldPath, newPath); err != nil && !os.IsNotExist(err) {
for i := l.rotations; i >= 2; i-- {
previous := fmt.Sprintf("%s.%d", l.path, i-1)
data, err := readOptional(store, previous)
if err != nil {
return fmt.Errorf("read rotated operation log: %w", err)
}
if data == nil {
continue
}
if err := store.AtomicWrite(fmt.Sprintf("%s.%d", l.path, i), data, 0o600); err != nil {
return fmt.Errorf("rotate operation log: %w", err)
}
}
if err := os.Rename(l.path, l.path+".1"); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("rotate operation log: %w", err)
if current != nil {
if err := store.AtomicWrite(l.path+".1", current, 0o600); err != nil {
return fmt.Errorf("rotate operation log: %w", err)
}
}
return nil
}
func readOptional(store securefs.Store, path string) ([]byte, error) {
data, err := store.ReadFile(path)
if errors.Is(err, fs.ErrNotExist) || errors.Is(err, os.ErrNotExist) {
return nil, nil
}
return data, err
}
+22
View File
@@ -11,6 +11,28 @@ import (
"proxyctl/internal/oplog"
)
func TestLoggerRejectsSymlinkedStateAncestor(t *testing.T) {
root := t.TempDir()
victim := filepath.Join(root, "victim")
if err := os.Mkdir(victim, 0o700); err != nil {
t.Fatal(err)
}
if err := os.Symlink(victim, filepath.Join(root, "state")); err != nil {
t.Fatal(err)
}
logger := oplog.New(filepath.Join(root, "state", "proxyctl", "operations.log"), 1024, 1)
if err := logger.Write(app.LogEntry{Operation: "reset", Result: "failure"}); err == nil {
t.Fatal("logger followed a symlinked state ancestor")
}
entries, err := os.ReadDir(victim)
if err != nil {
t.Fatal(err)
}
if len(entries) != 0 {
t.Fatalf("victim changed: %v", entries)
}
}
func TestLoggerRotatesAtBoundAndNeverStoresAuthenticatedURL(t *testing.T) {
path := filepath.Join(t.TempDir(), "state", "proxyctl.log")
logger := oplog.New(path, 180, 2)