Summary
Objects under a package registry's .quilt/packages/ namespace are content-addressed: the key is the hash of the manifest, so a given key should only ever be written once and never change. Today nothing at the storage layer enforces that — a PutObject to an existing .quilt/packages/<hash> key overwrites it. Making manifest writes create-only (via S3 conditional writes / If-None-Match: *) would turn the immutability that content-addressing assumes into something the storage layer guarantees.
Motivation
Any workflow that grants PutObject on .quilt/packages/* — required today for a principal to push a package to a bucket — also grants the ability to replace other revisions' manifests, because IAM can't distinguish create from overwrite on a prefix. Combined with the client not verifying browsed manifest bytes against the requested hash (filed separately), this is a manifest-poisoning path. Create-only writes close it at the source: an existing content-addressed key becomes physically unwritable.
Proposal
When writing a manifest object to .quilt/packages/<hash>, send If-None-Match: * (S3 has supported conditional writes for new-object creation since 2024). Behavior:
- New hash → key absent → write succeeds.
- Existing hash → precondition fails (
412) → treat as success/no-op, since by content-addressing the bytes are already identical (a colliding hash with different bytes is the thing we want to reject).
This is purely additive: re-pushing an identical revision is idempotent, and a genuine new revision has a new hash and thus a new key. Only in-place manifest replacement — which should never be legitimate — is blocked.
Optional companion: document a bucket-policy stanza denying PutObject on .quilt/packages/* without s3:if-none-match, so administrators can enforce create-only for writers whose client predates the change. That policy is only safe once the client reliably sends the header, so the client change should land first.
Scope / open questions
- Confirm all manifest-write paths (
push, build+push, the CLI, any lambda-side registry writers) route through one place where the header can be added.
- Named-package pointers (
.quilt/named_packages/<name>/<timestamp>, latest) are not content-addressed and must stay mutable — this applies only to .quilt/packages/.
- Non-S3 / local registries: no-op or best-effort.
Related
Defense-in-depth companion to the browse-time manifest verification issue; and context in quiltdata/pearls, whose Scene-isolation model would prefer to grant workers a .quilt/packages/* write that is create-only by construction rather than rely on every writer being trusted not to overwrite.
🤖 Generated with Claude Code
Summary
Objects under a package registry's
.quilt/packages/namespace are content-addressed: the key is the hash of the manifest, so a given key should only ever be written once and never change. Today nothing at the storage layer enforces that — aPutObjectto an existing.quilt/packages/<hash>key overwrites it. Making manifest writes create-only (via S3 conditional writes /If-None-Match: *) would turn the immutability that content-addressing assumes into something the storage layer guarantees.Motivation
Any workflow that grants
PutObjecton.quilt/packages/*— required today for a principal to push a package to a bucket — also grants the ability to replace other revisions' manifests, because IAM can't distinguish create from overwrite on a prefix. Combined with the client not verifying browsed manifest bytes against the requested hash (filed separately), this is a manifest-poisoning path. Create-only writes close it at the source: an existing content-addressed key becomes physically unwritable.Proposal
When writing a manifest object to
.quilt/packages/<hash>, sendIf-None-Match: *(S3 has supported conditional writes for new-object creation since 2024). Behavior:412) → treat as success/no-op, since by content-addressing the bytes are already identical (a colliding hash with different bytes is the thing we want to reject).This is purely additive: re-pushing an identical revision is idempotent, and a genuine new revision has a new hash and thus a new key. Only in-place manifest replacement — which should never be legitimate — is blocked.
Optional companion: document a bucket-policy stanza denying
PutObjecton.quilt/packages/*withouts3:if-none-match, so administrators can enforce create-only for writers whose client predates the change. That policy is only safe once the client reliably sends the header, so the client change should land first.Scope / open questions
push,build+push, the CLI, any lambda-side registry writers) route through one place where the header can be added..quilt/named_packages/<name>/<timestamp>,latest) are not content-addressed and must stay mutable — this applies only to.quilt/packages/.Related
Defense-in-depth companion to the browse-time manifest verification issue; and context in quiltdata/pearls, whose Scene-isolation model would prefer to grant workers a
.quilt/packages/*write that is create-only by construction rather than rely on every writer being trusted not to overwrite.🤖 Generated with Claude Code