Amazon S3 Object Lock: Immutable Artifact Buckets Coding Agents Cannot Overwrite

0 views

Your coding agent uploaded tenant-42/patch.diff at 14:02, then a second tool turn “fixed” it at 14:07 and overwrote the evidence you needed for a customer dispute. Or worse: a prompt-injected agent called DeleteObject on last week’s eval traces. S3 Conditional Writes stop accidental clobber races with If-None-Match. S3 Object Lock stops authorized overwrite and delete for a retention period — WORM storage for agent artifacts, audit packs, and model-eval baselines. Pair with Macie for secret scanning and versioning always on.

⚡ TL;DR: Enable Object Lock (requires versioning) on artifact / audit buckets at create time. Prefer Governance mode for most agent artifacts; Compliance mode for regulated audit packs. Set default retention days; agents PutObject only — never BypassGovernanceRetention in task roles. Related: Conditional Writes, Macie, SCPs, Budgets.

Conditional writes vs Object Lock (do not conflate)

Mechanism Protects against Agent can still…
If-None-Match: * Two writers racing the same new key Overwrite after success; DeleteObject
Versioning alone Silent data loss (old versions kept) Delete markers; lifecycle purge
Object Lock Governance Overwrite/delete until retention ends (bypass with special perm) New versions if allowed by lock settings
Object Lock Compliance Overwrite/delete until retention ends (no user bypass) Only wait out retention

Use conditional writes for idempotent tool uploads. Use Object Lock for immutability SLAs (audit, legal hold, ransomware resistance). Many teams want both on the same bucket.

Enable Object Lock on artifact buckets

Object Lock must be enabled at bucket creation (or on a new bucket you migrate into). Versioning is mandatory.

bash
# ✅ create artifact bucket with Object Lock enabled
aws s3api create-bucket \
  --bucket coding-agent-artifacts-prod \
  --object-lock-enabled-for-bucket \
  --region us-east-1

aws s3api put-object-lock-configuration \
  --bucket coding-agent-artifacts-prod \
  --object-lock-configuration '{
    "ObjectLockEnabled": "Enabled",
    "Rule": {
      "DefaultRetention": {
        "Mode": "GOVERNANCE",
        "Days": 30
      }
    }
  }'
typescript
// ✅ CDK: Object Lock + default governance retention for agent artifacts
const bucket = new s3.Bucket(this, "AgentArtifacts", {
  bucketName: "coding-agent-artifacts-prod",
  versioned: true,
  objectLockEnabled: true,
  encryption: s3.BucketEncryption.S3_MANAGED,
  blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
  enforceSSL: true,
});
// Set default retention via CfnBucket ObjectLockConfiguration or custom resource

❌ Enabling Lock on a bucket that already holds mutable scratch pads without a migration plan: agents that expected overwrite semantics will start failing PutObject in confusing ways — split scratch vs immutable-audit prefixes/buckets.

Agent IAM: write once, never bypass

json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AgentPutOnly",
      "Effect": "Allow",
      "Action": ["s3:PutObject", "s3:GetObject", "s3:ListBucket"],
      "Resource": [
        "arn:aws:s3:::coding-agent-artifacts-prod",
        "arn:aws:s3:::coding-agent-artifacts-prod/tenants/${aws:PrincipalTag/tenant_id}/*"
      ]
    },
    {
      "Sid": "DenyLockBypassAndDeletes",
      "Effect": "Deny",
      "Action": [
        "s3:BypassGovernanceRetention",
        "s3:DeleteObject",
        "s3:DeleteObjectVersion",
        "s3:PutObjectRetention",
        "s3:PutObjectLegalHold"
      ],
      "Resource": "arn:aws:s3:::coding-agent-artifacts-prod/*"
    }
  ]
}
python
# ✅ agent upload with explicit retention headers (optional per-object)
import boto3
from datetime import datetime, timedelta, timezone

s3 = boto3.client("s3")

def put_immutable_artifact(bucket: str, key: str, body: bytes, days: int = 30):
    retain_until = datetime.now(timezone.utc) + timedelta(days=days)
    # ❌ Do not grant the task role s3:BypassGovernanceRetention "just in case"
    return s3.put_object(
        Bucket=bucket,
        Key=key,
        Body=body,
        ContentType="application/octet-stream",
        ObjectLockMode="GOVERNANCE",
        ObjectLockRetainUntilDate=retain_until,
        # Optional: combine with conditional create for race safety
        # IfNoneMatch="*",  # when using conditional writes API shape
    )

For brand-new keys, prefer also conditional writes so two parallel tool turns do not create dueling versions unintentionally — Lock preserves both versions; your UX may still want single-writer semantics.

Governance vs Compliance for agent workloads

Mode Who can shorten retention Use for
Governance Principals with s3:BypassGovernanceRetention Default agent artifacts, eval traces, patch diffs
Compliance Nobody (root included) until date passes Regulated audit exports, signed customer delivery packs
Legal Hold Independent of retention date Active incident / lawsuit freeze

Start with Governance + deny Bypass on agent roles + allow Bypass only on a break-glass SecOps role protected by SCPs and MFA.

Lifecycle, cost, and Macie

Immutability is not infinite storage:

  • Lifecycle noncurrent version expiration after retention elapses
  • Separate short-lived scratch bucket (no Lock) for intermediate tool files
  • Macie still scans locked objects — findings do not require overwrite; quarantine via copy-out + IAM deny
bash
# ✅ lifecycle: expire noncurrent after retention window + buffer
aws s3api put-bucket-lifecycle-configuration \
  --bucket coding-agent-artifacts-prod \
  --lifecycle-configuration '{
    "Rules": [{
      "ID": "expire-old-versions",
      "Status": "Enabled",
      "NoncurrentVersionExpiration": {"NoncurrentDays": 37},
      "Filter": {"Prefix": "tenants/"}
    }]
  }'

Watch storage with Budgets / Cost Anomaly — a runaway agent writing GB of locked logs will not be deletable for 30 days.

Production checklist

  • [ ] Artifact/audit buckets created with Object Lock + versioning
  • [ ] Default retention set (e.g. 30d Governance); Compliance only where required
  • [ ] Agent task roles: Put/Get/List only; Deny Bypass + Delete*
  • [ ] Break-glass SecOps role for Governance bypass; MFA + SCP
  • [ ] Scratch/mutable data in a different bucket without Lock
  • [ ] Conditional writes on create paths to reduce version spam
  • [ ] Macie enabled on artifact buckets; Security Hub aggregation
  • [ ] Runbook: legal hold for incidents; customer export packs use Compliance

FAQ

Q: Can I add Object Lock to an existing bucket?
A: Not retrospectively in-place for the feature flag — create a new Lock-enabled bucket and replicate/migrate. Plan prefixes before agents go live.

Q: Does Lock replace backups?
A: No. It prevents tampering/deletion in-account for a window. Still replicate critical audit packs cross-account.

Q: What about Overwrite via new version?
A: Object Lock retention applies per object version. Putting a “new” key version does not erase the locked old version — good for forensics, costly if agents rewrite constantly. Design keys as append-only (…/turns/0007.diff).

Key design: append-only paths for multi-turn agents

Agents rewrite the same logical artifact across turns. If you reuse tenants/42/final.patch, Governance Lock will either reject the overwrite or accumulate versions forever. Prefer append-only keys:

tenants/{tenant_id}/tasks/{task_id}/turns/{turn:04d}/patch.diff
tenants/{tenant_id}/tasks/{task_id}/turns/{turn:04d}/test.log
tenants/{tenant_id}/tasks/{task_id}/manifest.json  # points at latest turn

Keep manifest.json in a mutable control bucket (or accept Governance + SecOps bypass for manifests only). Lock the turn payloads. Query history with Athena on S3 over the turn prefix — immutability makes eval reproducibility boring in the best way.

typescript
// ✅ key helper — never clobber turn N
export function artifactKey(tenant: string, task: string, turn: number, file: string) {
  return `tenants/${tenant}/tasks/${task}/turns/${String(turn).padStart(4, "0")}/${file}`;
}

Ransomware / malicious agent deletes then become no-ops on locked versions; recovery is “point manifest at last known good turn,” not “hope backups finished.”

Related reading

S3 Object Lock turns coding-agent artifact buckets from mutable scratch pads into WORM evidence stores. Combine with conditional writes for races, deny Bypass on task roles, and keep scratch elsewhere — so agents can ship patches without rewriting history.

Last updated on September 26, 2026

Deep-dive PDF

Get the expanded guide for this post — extra diagrams-style checklists, failure modes, and a production walkthrough. Free when you subscribe to CheatCoders.

Already subscribed? or open the subscribe page.


Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

Comments

No comments yet. Why don’t you start the discussion?

Leave a comment

No account needed. Name and email are optional.