Skip to content

[Feat]: Design proposal for tracing daemon-backed build workflows #767

Description

@stupendoussuperpowers

First-pass implementation can be seen here for reference: https://github.com/stupendoussuperpowers/go-witness/tree/docker-filetrace/attestation/workloadrun

Describe the solution you'd like:

Witness currently traces command execution primarily through the process tree started by command-run, with file activity represented through openedfiles and network activity represented through network-trace. That model works when the command being attested performs all relevant work in its own descendant processes. It breaks down for common artifact generation workflows that delegate work to daemons, service managers, containers, or sandbox runtimes. Examples for this include docker build that creates cgroups and mounts filesystems through dockerd and containerd, or snap install which mounts a filesystem through snapd.

This creates gaps for SBOMit and similar consumers that want Witness attestations for common artifact generation flows. For these use cases, the attestation must cover the effective workload, not only the CLI process that initiated it.

The proposed solution is the workload-run (final name TBD) attestor. It observes Linux workload boundaries such as cgroups and mounts while the command-run command is executing, captures low-level workload activity from those boundaries, then lets provider plug-ins interpret and enrich the raw facts into useful runtime-specific attestation data.

Proposed solution:

This is an initial proposal for the workload-run attestor. A reference implementation is linked above, but the final attestation shape, process-coordination model, and even the name for the attestor still warrant some discussions. Any help/insights are appreciated!

workload-run has two cooperating pieces:

  1. A "collector" that observes common Linux workload primitives.
  2. Workflow-specific metadata Providers that decide which observed primitives matter and how to interpret them.

Briefly, the lifecycle of tracing is:

  1. eBPF-backed collectors begin watching for newly created cgroups, mount activity, and file/process events once the command-run execution window starts.
  2. Newly observed cgroups are passed through all enabled providers. A cgroup is traced if at least one provider accepts it.
  3. Activity from accepted cgroups is recorded as raw workload data. After tracing stops, each provider's Enrich function can group, add, or drop data based on the collected system traces.

We make the following assumptions about the system and the build processes in general:

  • Newly created workload boundaries during the traced command are treated as potentially relevant to that command. This might lead to the attestation containing broad host-level events or even parallel builds. These are expected to be pruned out by individual Providers (or the lack thereof).
  • Only newly observed cgroups and mounts inside the tracing window are considered by default. Existing long-lived daemon state is not attributed unless a provider later adds explicit correlation for it.

The reason for a collector/provider split is that most runtimes use the same small set of Linux primitives, while the meaning of those primitives is runtime-specific. The collector can therefore stay focused on cgroups, mounts, file opens, process metadata, and eventually network activity. Providers can stay focused on questions like "is this Docker?", "is this snap?", and "do these raw cgroups belong to the same higher-level workload?".

This allows for a few key benefits such as allowing for reuse of existing eBPF codebases avoiding implementation drift, and allowing for a more convenient way to both add new Providers or update existing ones without needing huge diffs. For instance, a cleverer Docker Provider may be able to differentiate between two parallel docker build commands, and under the existing architecture it would not be required to touch anything other than the DockerProvider struct itself.

Providers

Providers define runtime-specific policy and enrichment:

type Provider interface {
	Name() string
	CgroupFilter(cgroup string) bool
	FileFilter(file string) bool
	MountFilter(mount Mount) bool
	Enrich(ctx context.Context, workloads []Workload) ([]Workload, error)
}

The filter methods run during collection. They are used to accept or reject newly observed cgroups, opened files, and mounts. Enrich runs after collection and can label workloads, merge related cgroups, attach runtime metadata, or drop records that are no longer useful.

The reference implementation provides a minimal Docker provider. It accepts Docker and containerd-looking cgroup paths, keeps file-open activity from those cgroups, and labels matching workloads as docker or containerd during enrichment. It does not yet attempt deeper BuildKit or image-layer correlation.

A new provider such as one for snap would follow the same model: recognize snapd-created cgroups, snap mount patterns, squashfs paths, or snapd state, then enrich the raw workload into a snap-specific record.

Attestation Schema

The proposed attestation type is:

https://witness.dev/attestations/workload-run/v0.1

The current schema is:

type WorkloadRun struct {
	Providers  []string   `json:"providers,omitempty"`
	Collectors []string   `json:"collectors,omitempty"`
	Scope      Scope      `json:"scope,omitempty"`
	Workloads  []Workload `json:"workloads,omitempty"`
}

type Scope struct {
	StartedAt time.Time `json:"started_at,omitempty"`
	EndedAt   time.Time `json:"ended_at,omitempty"`
	Trigger   Trigger   `json:"trigger,omitempty"`
}

type Trigger struct {
	Attestor  string `json:"attestor,omitempty"`
	ProcessID int    `json:"processid,omitempty"`
}

type Workload struct {
	ID          string                          `json:"id,omitempty"`
	Kind        string                          `json:"kind,omitempty"`
	Runtime     string                          `json:"runtime,omitempty"`
	Name        string                          `json:"name,omitempty"`
	Image       string                          `json:"image,omitempty"`
	Cgroups     []Cgroup                        `json:"cgroups,omitempty"`
	Mounts      []Mount                         `json:"mounts,omitempty"`
	Processes   []ProcessInfo                   `json:"processes,omitempty"`
	OpenedFiles map[string]cryptoutil.DigestSet `json:"openedfiles,omitempty"`
	Metadata    map[string]string               `json:"metadata,omitempty"`
}

providers and collectors describe how the attestation was produced. scope describes when tracing was active and which command-run process triggered it. workloads contains the raw and enriched workload records.

This is a rough shape for the attestation, as the first obvious improvement we need to decide where to record network traces. They could either be a top level entry similar to OpenedFiles or both of these can be turned into a nested entry of Cgroups

The workload fields are intentionally a mix of generic collection data and provider-enriched data. cgroups, mounts, processes, and openedfiles come from the collector. runtime, name, image, and metadata are mainly provider-owned fields. There is room to add fields as providers become more precise, for example runtime IDs, build step IDs, network activity, layer IDs, package names, or stronger provenance links to the initiating command.

Example Use

witness run -a command-run -a workload-run --attestor-workload-run-provider docker -- docker build .

Testing, Documentation and Follow-up work required:

  • Add Linux integration coverage for Docker/containerd cgroup detection and opened file attribution.
  • Provide documentation around Linux/eBPF requirements, experimental status, provider authoring, and known over-attribution cases.

CC: @Vyom-Yadav @SantiagoTorres @jkjell

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    featureNew feature (larger than enhancement)

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions