agent-substrate / agent-substrate/substrate

Add a SystemInfo volume source to ActorTemplate for delivering per-actor generated files

Ouverte
#802 4 commentaires 0 réactions 1 personne assignée Réclamée par @thompsonmax Voir sur GitHub
area/identity area/security kind/feature prio/P0
Langage dominant
Go
Étoiles
1.8k
Forks
316
Merge moyen
2 j 43 min
PR mergées (30 j)
287

Description

## Summary

Add a "SystemInfo" volume source that delivers substrate-generated, per-actor files into an actor's containers, similar to [Kubernetes projected volumes](https://kubernetes.io/docs/concepts/storage/projected-volumes/) / downwardAPI. The volume is declared in spec.volumes with a list of data sources, each of which writes one or more files at configurable relative path(s), and is bind-mounted read-only wherever a container mounts it.

This replaces the hardcoded /run/ate/actor-id identity mount with an explicit and extensible API: templates opt in, choose the mount location and file layout, and future identity material (JWTs, certificates) can be added as new data sources without inventing new mount conventions.

## Why a volume, not env vars or baked files

Anything living in checkpointed process memory (env vars) or in the container image is frozen into snapshots. When a snapshot seeds a different actor than the one that created it (the template's shared snapshot, or CreateActorRequest.source_snapshot), every actor restored from it would observe the source actor's values if baked into the snapshot. For rotating material (tokens, certificates), a frozen copy goes stale across suspend/resume cycles even within a single actor. The metadata fields themselves are fixed for an actor's lifetime, only the cross-actor case affects them.

SystemInfo volume contents are instead regenerated by atelet on the host on every Run/Restore, whatever the boot source (cold boot, a shared template snapshot, or the actor's own prior snapshot), so the mounted files always carry the resumed actor's own, current values. Workloads should read them fresh rather than caching at startup.

### Preventing leakage of credentials read into process memory

Note that an actor may read this material into process memory, where it will be captured by snapshots that could later seed a different actor. We rely on these mechanisms to prevent Actor credentials provided from this volume type from leaking into other Actors seeded from a snapshot of a previous Actor:

- Short TTLs. Tokens and certificates expire on the order of their renewal interval. A snapshot is at earliest replayed minutes later and typically much older — by the time a captured credential could be extracted or resumed elsewhere, it has usually already expired. The workload re-reads from the SystemInfo volume at time of use, so expiry never breaks the legitimate holder.
- Activation binding. Credentials are minted per activation (one placement of one actor on one worker). Renewal is not a bearer operation: the broker derives the actor from the control plane's current placement of the authenticated worker and never from material the caller presents. A fail-closed UID guard rejects mints that cross an assignment change. A credential (or private key) recovered from a snapshot therefore cannot be exchanged for a fresh one: replaying it from anywhere other than the live activation it was minted for fails authorization, and the restored actor's own new activation mints fresh credentials under a brand-new key.

## Proposed API

```yaml
spec:
volumes:
- name: system-info
systemInfo:
dataSources:
# Part 1 (this PR): own-metadata projection, downwardAPI-style
- actorMetadata:
items:
- field: name # enum: name | atespace | uid
path: actor-name
- field: atespace
path: atespace
- field: uid
path: actor-uid

# Planned (#802 follow-up): minted JWT — mirrors serviceAccountToken
- actorIdentityToken:
path: token
audience: some-verifier.example.com
expirationSeconds: 3600

# Planned (#802 follow-up): x509 identity from the #670 issuance work
- actorIdentityCertificate:
certificatePath: tls/actor.crt # leaf + chain, PEM
keyPath: tls/actor.key # private key, PEM

containers:
- name: main
image: app@sha256:...
volumeMounts:
- name: system-info
mountPath: /run/ate # the actor reads e.g. /run/ate/actor-name.
```

Each dataSources entry sets exactly one source type (CEL-enforced, same pattern as VolumeSource). path is relative to the volume root. Paths must be clean relative paths (no `..`, no absolute paths) and unique across the whole volume. Within actorMetadata, each field may appear at most once, all enforced at admission via CEL.

## Data sources

- **actorMetadata** - projects the actor's identity fields per the resource identity model (https://github.com/agent-substrate/substrate/blob/main/docs/api-style-guide.md#2-resource-naming-and-identity). One file per item: name (unique within an atespace), atespace (with name, forms the actor's full identity and DNS name), and uid (server-generated, distinguishes incarnations of the same name). Values are written raw with no trailing newline. All three are fixed for the actor's lifetime, across suspend/resume/migration.
- **actorIdentityToken** - a token attesting the actor's identity, refreshed by atelet.
- **actorIdentityCertificate** - the x509 identity from the credential-issuance work (#670), delivered as key/cert files.

## Semantics

- Read-only bind mount; contents are generated on the host by atelet, never written by the workload.
- Regenerated on every Run/Restore, before the sandbox starts, regardless of what the actor boots from.
- Never captured into snapshots: volume roots live on the host outside the durable-dir tree and outside the guest filesystem delta, and are wiped and rebuilt each Run/Restore. The authoritative copies do not appear in checkpointed state.
- Atomic updates: files are written with Kubernetes' AtomicWriter (vendored), so a data source refreshed while the actor is running can never be observed half-written.
- Micro-VM support rides the same mechanism durable-dir volumes already use: ateom-microvm serves per-actor host directories into the guest over virtio-fs (one share, one subdirectory per volume, bind-mounted into each declaring container, see cmd/ateom-microvm/durable.go). SystemInfo volumes get the analogous treatment: a read-only virtiofsd share serving the per-actor system-info/ volume roots (cache=auto, since atelet rewrites contents between resumes), per-container read-only binds from the share's per-volume subdirectories, and a system-info analog of the containers' DurableDirVolumeMounts field in the ateom proto. atelet's host-side file population is already runtime-agnostic, and because the volume roots live outside the durable-dir tree, the durable share's snapshot tar cannot ingest them.

## Plan
- [x] Part 1: `actorMetadata` source: API, validation, atelet population, gVisor + micro-VM mounts, e2e
- [ ] Part 2: `actorIdentityToken` source
- [ ] Part 3: `actorIdentityCertificate` source

Guide de contribution

Ouvrir le guide de contribution

Évaluation

Cette issue n'a pas encore été évaluée.

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.