Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Containerfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ FROM golang:${GO_VERSION}-bookworm AS builder
WORKDIR /build
COPY go.mod go.sum ./
COPY cmd/policy-proxy ./cmd/policy-proxy
COPY internal ./internal
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o policy-proxy ./cmd/policy-proxy

# ---
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,10 @@ For a fuller setup guide, including local CLI build, config paths, skill import,

## Command Audit Logging

Command audit logging is opt-in while the custom init image path is experimental. When enabled, the init daemon attaches eBPF exec tracepoints inside the Apple container Linux VM and writes every observed process exec to:
Command audit logging is opt-in while the custom init image path is experimental. When enabled, the init daemon attaches eBPF exec tracepoints inside the Apple container Linux VM and writes observed process exec events to the unified audit log:

```text
~/.local/state/opencode-sandbox/runs/<run-id>/command-events.jsonl
~/.local/state/opencode-sandbox/runs/<run-id>/audit-events.jsonl
```

The log includes full argv by default, so it may contain URLs, prompts, tokens, or other command-line secrets. If a run hangs at Apple container startup and the boot log mentions `/sbin/vminitd`, disable command audit and use practical proxy mode until the init image is rebuilt.
Expand Down
120 changes: 113 additions & 7 deletions cmd/policy-ebpfd/attach.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/binary"
"fmt"
"net"
"sync"

"github.com/cilium/ebpf"
"github.com/cilium/ebpf/asm"
Expand All @@ -16,7 +17,11 @@ type enforcementHandle struct {
settings *ebpf.Map
blockedIPv4 *ebpf.Map
allowedIPv4 *ebpf.Map
events *ebpf.Map
defaultAction uint32
mu sync.Mutex
nextRuleID uint32
rules map[uint32]string
}

func (h *enforcementHandle) Close() error {
Expand Down Expand Up @@ -52,6 +57,12 @@ func (h *enforcementHandle) Close() error {
}
return nil
},
func() error {
if h.events != nil {
return h.events.Close()
}
return nil
},
} {
if err := closeFn(); err != nil && firstErr == nil {
firstErr = err
Expand All @@ -65,7 +76,7 @@ func (h *enforcementHandle) UpdateBlocked(ip net.IP, rule string) error {
if !ok {
return nil
}
value := uint32(1)
value := h.ruleID(rule)
return h.blockedIPv4.Update(key, value, ebpf.UpdateAny)
}

Expand All @@ -74,7 +85,7 @@ func (h *enforcementHandle) UpdateAllowed(ip net.IP, rule string) error {
if !ok {
return nil
}
value := uint32(1)
value := h.ruleID(rule)
return h.allowedIPv4.Update(key, value, ebpf.UpdateAny)
}

Expand All @@ -94,6 +105,26 @@ func (h *enforcementHandle) RemoveAllowed(ip net.IP) error {
return h.allowedIPv4.Delete(key)
}

func (h *enforcementHandle) ruleID(rule string) uint32 {
h.mu.Lock()
defer h.mu.Unlock()
if h.rules == nil {
h.rules = map[uint32]string{}
}
h.nextRuleID++
if h.nextRuleID == 0 {
h.nextRuleID = 1
}
h.rules[h.nextRuleID] = rule
return h.nextRuleID
}

func (h *enforcementHandle) ruleName(id uint32) string {
h.mu.Lock()
defer h.mu.Unlock()
return h.rules[id]
}

// attachCgroupConnect loads an IPv4 cgroup/connect eBPF program and attaches
// it for the daemon lifetime. The program uses exact-IP maps that are updated
// by the resolver. Allowlist wins, then blocklist, then defaultAction.
Expand Down Expand Up @@ -134,6 +165,17 @@ func attachCgroupConnect(cgroupPath string, bundle *PolicyBundle) (*enforcementH
return nil, fmt.Errorf("creating allowed IPv4 map: %w", err)
}

events, err := ebpf.NewMap(&ebpf.MapSpec{
Type: ebpf.PerfEventArray,
Name: "network_events",
})
if err != nil {
settings.Close()
blockedIPv4.Close()
allowedIPv4.Close()
return nil, fmt.Errorf("creating network event map: %w", err)
}

defaultAction := uint32(1)
if bundle.Network.DefaultAction == "deny" || bundle.Network.Mode == "off" {
defaultAction = 0
Expand All @@ -143,6 +185,7 @@ func attachCgroupConnect(cgroupPath string, bundle *PolicyBundle) (*enforcementH
settings.Close()
blockedIPv4.Close()
allowedIPv4.Close()
events.Close()
return nil, fmt.Errorf("initializing settings map: %w", err)
}

Expand All @@ -151,20 +194,30 @@ func attachCgroupConnect(cgroupPath string, bundle *PolicyBundle) (*enforcementH
settings.Close()
blockedIPv4.Close()
allowedIPv4.Close()
events.Close()
return nil, fmt.Errorf("associating settings map: %w", err)
}
if err := insns.AssociateMap("blocked_ipv4", blockedIPv4); err != nil {
settings.Close()
blockedIPv4.Close()
allowedIPv4.Close()
events.Close()
return nil, fmt.Errorf("associating blocked map: %w", err)
}
if err := insns.AssociateMap("allowed_ipv4", allowedIPv4); err != nil {
settings.Close()
blockedIPv4.Close()
allowedIPv4.Close()
events.Close()
return nil, fmt.Errorf("associating allowed map: %w", err)
}
if err := insns.AssociateMap("network_events", events); err != nil {
settings.Close()
blockedIPv4.Close()
allowedIPv4.Close()
events.Close()
return nil, fmt.Errorf("associating network event map: %w", err)
}

progSpec := &ebpf.ProgramSpec{
Name: "cgroup_connect4",
Expand All @@ -178,6 +231,7 @@ func attachCgroupConnect(cgroupPath string, bundle *PolicyBundle) (*enforcementH
settings.Close()
blockedIPv4.Close()
allowedIPv4.Close()
events.Close()
return nil, fmt.Errorf("loading cgroup/connect program: %w", err)
}

Expand All @@ -191,6 +245,7 @@ func attachCgroupConnect(cgroupPath string, bundle *PolicyBundle) (*enforcementH
settings.Close()
blockedIPv4.Close()
allowedIPv4.Close()
events.Close()
return nil, fmt.Errorf("attaching cgroup/connect: %w", err)
}

Expand All @@ -200,40 +255,91 @@ func attachCgroupConnect(cgroupPath string, bundle *PolicyBundle) (*enforcementH
settings: settings,
blockedIPv4: blockedIPv4,
allowedIPv4: allowedIPv4,
events: events,
defaultAction: defaultAction,
rules: map[uint32]string{},
}, nil
}

func cgroupConnect4Instructions() asm.Instructions {
return asm.Instructions{
insns := asm.Instructions{
asm.Mov.Reg(asm.R9, asm.R1),
asm.LoadMem(asm.R6, asm.R1, 4, asm.Word),
asm.StoreMem(asm.RFP, -4, asm.R6, asm.Word),

asm.LoadMapPtr(asm.R1, 0).WithReference("allowed_ipv4"),
asm.Mov.Reg(asm.R2, asm.RFP),
asm.Add.Imm(asm.R2, -4),
asm.FnMapLookupElem.Call(),
asm.JNE.Imm(asm.R0, 0, "allow"),
asm.JEq.Imm(asm.R0, 0, "check_blocked"),
asm.LoadMem(asm.R7, asm.R0, 0, asm.Word),
asm.Mov.Imm(asm.R8, 1),
asm.Ja.Label("allow"),

asm.Mov.Imm(asm.R0, 0).WithSymbol("check_blocked"),
asm.LoadMapPtr(asm.R1, 0).WithReference("blocked_ipv4"),
asm.Mov.Reg(asm.R2, asm.RFP),
asm.Add.Imm(asm.R2, -4),
asm.FnMapLookupElem.Call(),
asm.JNE.Imm(asm.R0, 0, "deny"),
asm.JEq.Imm(asm.R0, 0, "check_default"),
asm.LoadMem(asm.R7, asm.R0, 0, asm.Word),
asm.Mov.Imm(asm.R8, 2),
asm.Ja.Label("deny"),

asm.Mov.Imm(asm.R0, 0).WithSymbol("check_default"),
asm.StoreImm(asm.RFP, -8, 0, asm.Word),
asm.LoadMapPtr(asm.R1, 0).WithReference("settings"),
asm.Mov.Reg(asm.R2, asm.RFP),
asm.Add.Imm(asm.R2, -8),
asm.FnMapLookupElem.Call(),
asm.JEq.Imm(asm.R0, 0, "deny"),
asm.JEq.Imm(asm.R0, 0, "default_deny"),
asm.LoadMem(asm.R0, asm.R0, 0, asm.Word),
asm.JNE.Imm(asm.R0, 0, "allow"),
asm.JEq.Imm(asm.R0, 0, "default_deny"),
asm.Mov.Imm(asm.R7, 0),
asm.Mov.Imm(asm.R8, 3),
asm.Ja.Label("allow"),

asm.Mov.Imm(asm.R7, 0).WithSymbol("default_deny"),
asm.Mov.Imm(asm.R8, 4),
asm.Ja.Label("deny"),

asm.Mov.Imm(asm.R0, 0).WithSymbol("deny"),
}
insns = append(insns, networkEventOutputInstructions(0)...)
insns = append(insns,
asm.Return(),
asm.Mov.Imm(asm.R0, 1).WithSymbol("allow"),
)
insns = append(insns, networkEventOutputInstructions(1)...)
insns = append(insns,
asm.Return(),
)
return insns
}

func networkEventOutputInstructions(decision int32) asm.Instructions {
return asm.Instructions{
asm.StoreMem(asm.RFP, -32, asm.R6, asm.Word),
asm.FnGetCurrentPidTgid.Call(),
asm.RSh.Imm(asm.R0, 32),
asm.StoreMem(asm.RFP, -36, asm.R0, asm.Word),
asm.LoadMem(asm.R0, asm.R9, 20, asm.Word),
asm.StoreMem(asm.RFP, -28, asm.R0, asm.Word),
asm.Mov.Imm(asm.R0, decision),
asm.StoreMem(asm.RFP, -24, asm.R0, asm.Word),
asm.StoreMem(asm.RFP, -20, asm.R8, asm.Word),
asm.StoreMem(asm.RFP, -16, asm.R7, asm.Word),
asm.Mov.Imm(asm.R0, 0),
asm.StoreMem(asm.RFP, -12, asm.R0, asm.Word),
asm.StoreMem(asm.RFP, -8, asm.R0, asm.Word),
asm.Mov.Reg(asm.R1, asm.R9),
asm.LoadMapPtr(asm.R2, 0).WithReference("network_events"),
asm.LoadImm(asm.R3, 0xffffffff, asm.DWord),
asm.Mov.Reg(asm.R4, asm.RFP),
asm.Add.Imm(asm.R4, -36),
asm.Mov.Imm(asm.R5, 28),
asm.FnPerfEventOutput.Call(),
asm.Mov.Imm(asm.R0, decision),
}
}

Expand Down
29 changes: 27 additions & 2 deletions cmd/policy-ebpfd/attach_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"encoding/binary"
"net"
"testing"
)
Expand Down Expand Up @@ -33,12 +34,12 @@ func TestCgroupConnect4InstructionsReferenceMaps(t *testing.T) {
symbols[sym] = true
}
}
for _, want := range []string{"allowed_ipv4", "blocked_ipv4", "settings"} {
for _, want := range []string{"allowed_ipv4", "blocked_ipv4", "settings", "network_events"} {
if !refs[want] {
t.Fatalf("expected map reference %q in instructions", want)
}
}
for _, want := range []string{"allow", "deny"} {
for _, want := range []string{"allow", "deny", "check_blocked", "check_default", "default_deny"} {
if !symbols[want] {
t.Fatalf("expected symbol %q in instructions", want)
}
Expand All @@ -47,3 +48,27 @@ func TestCgroupConnect4InstructionsReferenceMaps(t *testing.T) {
t.Fatalf("expected instruction symbols to resolve: %v", err)
}
}

func TestNetworkEventFromSampleResolvesDecisionAndRule(t *testing.T) {
bundle := &PolicyBundle{RunID: "run-1"}
bundle.Project.Name = "proj"
handle := &enforcementHandle{rules: map[uint32]string{7: "*.example.com"}}
raw := make([]byte, 28)
binary.LittleEndian.PutUint32(raw[0:4], 42)
binary.LittleEndian.PutUint32(raw[4:8], 0xcb00710a)
binary.LittleEndian.PutUint32(raw[8:12], 443)
binary.LittleEndian.PutUint32(raw[12:16], 0)
binary.LittleEndian.PutUint32(raw[16:20], networkReasonBlocklist)
binary.LittleEndian.PutUint32(raw[20:24], 7)

event, err := networkEventFromSample(raw, bundle, handle)
if err != nil {
t.Fatalf("networkEventFromSample failed: %v", err)
}
if event.EventType != "network.connect" || event.Decision != "block" || event.Reason != "blocklist" {
t.Fatalf("unexpected event: %+v", event)
}
if event.DstIP != "203.0.113.10" || event.DstPort != 443 || event.MatchedRule != "*.example.com" {
t.Fatalf("unexpected destination/rule: %+v", event)
}
}
Loading
Loading