fix: harden read-only target diagnostics

This commit is contained in:
proxyctl coordinator
2026-07-17 10:42:48 +08:00
parent 0fbaa71e37
commit a5360028ca
8 changed files with 385 additions and 33 deletions
+57 -8
View File
@@ -68,15 +68,24 @@ func (ExecRunner) Run(ctx context.Context, name string, args ...string) ([]byte,
return output, nil
}
type ProcessAuthority interface {
EffectiveUID() int
}
type processAuthority struct{}
func (processAuthority) EffectiveUID() int { return os.Geteuid() }
type Adapter struct {
configuration Configuration
files FileSystem
runner CommandRunner
authority ProcessAuthority
}
var _ target.Diagnosable = (*Adapter)(nil)
func New(configuration Configuration, files FileSystem, runner CommandRunner) *Adapter {
func New(configuration Configuration, files FileSystem, runner CommandRunner, authorities ...ProcessAuthority) *Adapter {
if configuration.DaemonConfigPath == "" {
configuration.DaemonConfigPath = "/etc/docker/daemon.json"
}
@@ -90,7 +99,11 @@ func New(configuration Configuration, files FileSystem, runner CommandRunner) *A
if runner == nil {
runner = ExecRunner{}
}
return &Adapter{configuration: configuration, files: files, runner: runner}
authority := ProcessAuthority(processAuthority{})
if len(authorities) != 0 && authorities[0] != nil {
authority = authorities[0]
}
return &Adapter{configuration: configuration, files: files, runner: runner, authority: authority}
}
func (a *Adapter) ID() string { return "docker-daemon" }
@@ -119,9 +132,9 @@ func (a *Adapter) DiagnosticChecks(ctx context.Context, _ *profile.Profile) []ta
}
schemaCheck := target.Check{Name: "configuration schema", OK: true, Evidence: "Docker daemon configuration and recovery metadata are absent or valid JSON"}
if data, err := a.files.ReadFile(a.configuration.DaemonConfigPath); err == nil {
if _, schemaErr := mergeConfig(data, profile.Profile{}); schemaErr != nil {
if schemaErr := validateConfigSchema(data); schemaErr != nil {
schemaCheck.OK = false
schemaCheck.Evidence = profile.RedactText("Docker daemon configuration cannot be safely merged: " + schemaErr.Error())
schemaCheck.Evidence = profile.RedactText("Docker daemon configuration schema is invalid: " + schemaErr.Error())
}
} else if !errors.Is(err, fs.ErrNotExist) {
schemaCheck.OK = false
@@ -133,10 +146,17 @@ func (a *Adapter) DiagnosticChecks(ctx context.Context, _ *profile.Profile) []ta
schemaCheck.Evidence = profile.RedactText("Docker daemon recovery metadata is invalid or unreadable: " + err.Error())
}
}
serviceAuthority := target.Check{Name: "service control authority", OK: a.authority.EffectiveUID() == 0}
if serviceAuthority.OK {
serviceAuthority.Evidence = "effective process authority can control the system Docker service"
} else {
serviceAuthority.Evidence = "effective process authority cannot reload or restart the system Docker service; run proxyctl explicitly with sufficient authority"
}
return []target.Check{
commandCheck,
schemaCheck,
target.ReadOnlyWriteAuthority(a.configuration.DaemonConfigPath, a.configuration.RecoveryPath),
serviceAuthority,
}
}
@@ -366,16 +386,45 @@ func (a *Adapter) writeRecovery(value recovery) error {
return a.files.AtomicWrite(a.configuration.RecoveryPath, append(data, '\n'), 0o600)
}
func mergeConfig(data []byte, p profile.Profile) ([]byte, error) {
func validateConfigSchema(data []byte) error {
if len(strings.TrimSpace(string(data))) == 0 {
return errors.New("configuration must be a JSON object")
}
var document map[string]any
if err := json.Unmarshal(data, &document); err != nil {
return err
}
if document == nil {
return errors.New("configuration must be a JSON object")
}
existing, present := document["proxies"]
if !present {
return nil
}
proxies, ok := existing.(map[string]any)
if !ok {
return errors.New("the existing proxies field is not an object")
}
for _, key := range []string{"http-proxy", "https-proxy", "no-proxy"} {
if value, present := proxies[key]; present {
if _, ok := value.(string); !ok {
return fmt.Errorf("the existing proxies.%s field is not a string", key)
}
}
}
return nil
}
func mergeConfig(data []byte, p profile.Profile) ([]byte, error) {
if len(strings.TrimSpace(string(data))) == 0 {
data = []byte("{}")
}
if err := json.Unmarshal(data, &document); err != nil {
if err := validateConfigSchema(data); err != nil {
return nil, err
}
if document == nil {
document = map[string]any{}
var document map[string]any
if err := json.Unmarshal(data, &document); err != nil {
return nil, err
}
proxies := map[string]any{}
if existing, ok := document["proxies"].(map[string]any); ok {