审计日志是证据而不是控制手段:写失败不能让请求失败,否则磁盘写满会变成一次远程 拒绝服务——已经执行成功的指令会因为日志追加不上而开始报错,那是坏错了地方。 但它也不能消失。静默丢审计条目是那种"运维以为自己有记录、其实没有"的失败,而这份 日志的全部意义就是事后能回答"谁在什么时候要求了什么"。现在写失败会进 agent 自己的 日志,磁盘写满表现为一行反复出现,而不是一片空白。 顺带把 Close 之后的语义写清楚:Close 会丢掉 sink,此后的写入返回 nil——这是刻意的, 因为一个正在关闭的进程与在途请求赛跑时,为每一个恰好到达的请求报一次失败是噪声 而不是信息。这条差别在 agent 日志里看得见。 测试用真实失败路径(写一个已关闭的文件)而不是填满文件系统,并断言 exec.record 把 它报了出来。 Go 全包、Node 112 项、Swift 101 项通过。
99 lines
3.4 KiB
Go
99 lines
3.4 KiB
Go
package runtime
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// Entry is one line of the audit log.
|
|
//
|
|
// Metadata only, and that is the whole design: the request and response bodies are the actual
|
|
// credentials and SMS contents, and copying them into a second file on the same box adds a place to
|
|
// lose them without adding any investigative value. What is kept is enough to answer "who asked for
|
|
// what, when, and was it allowed" from the relay request id.
|
|
type Entry struct {
|
|
At time.Time `json:"ts"`
|
|
Node string `json:"node,omitempty"`
|
|
Req string `json:"req,omitempty"`
|
|
App string `json:"app,omitempty"`
|
|
Method string `json:"method,omitempty"`
|
|
Path string `json:"path,omitempty"`
|
|
Status int `json:"status"`
|
|
Outcome string `json:"outcome"`
|
|
}
|
|
|
|
// Audit is an append-only JSONL writer. One line per request, flushed as it is written: an audit log
|
|
// that loses its tail on SIGKILL cannot explain the incident that killed the process.
|
|
type Audit struct {
|
|
mu sync.Mutex
|
|
sink *os.File
|
|
enc *json.Encoder
|
|
}
|
|
|
|
// OpenAudit appends to path, creating it 0600 and its parent directories if needed.
|
|
//
|
|
// The file is opened append-only and never rotated in-process. Rotation is logrotate's job: an
|
|
// agent that renames its own log is an agent that can lose an entry during the rename.
|
|
func OpenAudit(path string) (*Audit, error) {
|
|
if path == "" {
|
|
return nil, errors.New("runtime: audit path is empty")
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
return nil, err
|
|
}
|
|
sink, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o600)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &Audit{sink: sink, enc: json.NewEncoder(sink)}, nil
|
|
}
|
|
|
|
// NullAudit returns a writer that keeps nothing. Used by tests and by `msa-agent --audit=-`, because
|
|
// the executor is simpler when it can assume a non-nil audit target.
|
|
func NullAudit() *Audit { return &Audit{} }
|
|
|
|
// Write appends one entry. A write failure is returned to the caller rather than panicking: the
|
|
// audit log is evidence, not a control, and a full disk must not become a remote denial of service.
|
|
//
|
|
// Returning nil after Close is deliberate and is the one case where a dropped entry is not an error:
|
|
// a shutdown path racing an in-flight request would otherwise log a failure for every request that
|
|
// happened to arrive at that moment, which is noise rather than information. The executor logs what it
|
|
// sees either way, so the difference is visible in the agent's log.
|
|
func (a *Audit) Write(entry Entry) error {
|
|
a.mu.Lock()
|
|
defer a.mu.Unlock()
|
|
if a.sink == nil {
|
|
return nil
|
|
}
|
|
if entry.At.IsZero() {
|
|
entry.At = time.Now().UTC()
|
|
}
|
|
entry.At = entry.At.UTC()
|
|
if err := a.enc.Encode(entry); err != nil {
|
|
return fmt.Errorf("runtime: audit write: %w", err)
|
|
}
|
|
return a.sink.Sync()
|
|
}
|
|
|
|
// Close releases the file.
|
|
//
|
|
// A write after Close is dropped, and dropped silently is the wrong word for it: the executor reports
|
|
// every write error it sees, and this returns one, so a request racing a clean shutdown shows up as a
|
|
// warning rather than as a missing line nobody notices. The alternative — failing the request — buys
|
|
// nothing, because the command it describes has already run or been refused.
|
|
func (a *Audit) Close() error {
|
|
a.mu.Lock()
|
|
defer a.mu.Unlock()
|
|
if a.sink == nil {
|
|
return nil
|
|
}
|
|
err := a.sink.Close()
|
|
a.sink = nil
|
|
return err
|
|
}
|