Object lock
Object lock is write-once-read-many (WORM) protection: a locked object version cannot be deleted or overwritten until its retention expires or its legal hold is cleared. It is how you meet compliance and legal-hold requirements where data must be provably immutable for a window of time.
Object lock has two independent controls:
- Retention has a mode and a retain-until date. Until that date, the version is protected.
- Legal hold is a separate
ON/OFFflag with no expiry. While it is on, the version is protected regardless of retention.
A version is deletable only when no retention window is active and no legal hold is on.
Retention modes
| Mode | Behavior |
|---|---|
GOVERNANCE | Protects the version; a sufficiently privileged caller could bypass it on AWS. Lockwell does not implement that bypass (see below). |
COMPLIANCE | Immutable until the retain-until date. No one can shorten or remove it, including the account root. |
For both modes, an active retention deadline can be extended to a later date but never shortened. The server rejects a write that would move the deadline earlier or weaken the mode.
Enable object lock at bucket creation
Object lock can only be turned on when the bucket is created, and a bucket with object lock has versioning enabled (lock requires versioning). On the native client, pass objectLock: true (Node) or set ObjectLockEnabled (Go) on the create-bucket input. A bucket created without object lock cannot have it added later.
await native.createBucket("vault", { objectLock: true, versioning: true });_, err := native.CreateBucket(ctx, lockwellnative.CreateBucketInput{
Name: "vault",
Versioning: "enabled",
ObjectLockEnabled: true,
})import com.lockwell.sdk.nativeapi.NativeTypes.CreateBucketOptions;
nativeClient.createBucket("vault", new CreateBucketOptions("enabled", true));For Java ERP integrations, StorageProfiles.fiscalArchiveBucket() returns this same versioning + Object Lock create shape, and StorageProfiles.fiscalArchiveWrite(...) pairs a no-overwrite, checksummed, idempotent write with an explicit COMPLIANCE retention spec. The ERP still supplies the retain-until date.
The S3 client enables object lock at create and takes a default retention rule (a mode and a number of days). New objects inherit that default unless you set per-object retention on the write:
// The Node S3 client enables object lock by passing the lock config at create:
await s3.createBucket("vault");
// then set a default retention rule and per-object retention via PutObject (below).err := s3.CreateBucket(ctx, "vault",
lockwellsdk.WithObjectLockEnabled(lockwellsdk.ObjectLockCompliance, 365))// createBucket(name, defaultMode, defaultDays):
s3.createBucket("vault", "COMPLIANCE", 365);Set and read retention and legal hold
The native client has dedicated set/get methods for retention and legal hold, applied after the object exists. They enforce the same WORM gate as the S3 path. Retention takes a mode (GOVERNANCE or COMPLIANCE) and an RFC3339 retain-until date; legal hold is a separate flag.
await native.setObjectRetention("vault", "ledger.json", {
mode: "COMPLIANCE",
retainUntil: "2030-01-01T00:00:00Z",
});
await native.setObjectLegalHold("vault", "ledger.json", true);
const r = await native.getObjectRetention("vault", "ledger.json"); // { mode, retainUntil }
const held = await native.getObjectLegalHold("vault", "ledger.json"); // boolean_, err := native.SetObjectRetention(ctx, "vault", "ledger.json", "COMPLIANCE", "2030-01-01T00:00:00Z")
_, err = native.SetObjectLegalHold(ctx, "vault", "ledger.json", "ON")
r, err := native.GetObjectRetention(ctx, "vault", "ledger.json")
fmt.Println(r.Mode, r.RetainUntil)
hold, err := native.GetObjectLegalHold(ctx, "vault", "ledger.json")
fmt.Println(hold.Status) // "ON" or "OFF"nativeClient.setObjectRetention("vault", "ledger.json", "COMPLIANCE", "2030-01-01T00:00:00Z");
nativeClient.setObjectLegalHold("vault", "ledger.json", "ON");
var r = nativeClient.getObjectRetention("vault", "ledger.json"); // mode() + retainUntil()
var hold = nativeClient.getObjectLegalHold("vault", "ledger.json"); // status()The Go native SetObjectLegalHold / GetObjectLegalHold use the string "ON" / "OFF"; the Node native client takes and returns a boolean; the Java native client takes and returns the string status. Pass a versionId option (Node) to target a specific version; without it the current version is used.
S3 clients can set a version's retention and legal hold at write time, as options on PutObject. Mode and retain-until must be supplied together, and the bucket must have Object Lock enabled. The Go S3 client also supports post-write updates with typed SetObjectRetention / SetObjectLegalHold methods (and Put... aliases named for the S3 operations); pass WithVersionID to mutate a non-current version.
await s3.putObject("vault", "ledger.json", body, {
objectLockMode: "COMPLIANCE",
objectLockRetainUntil: "2030-01-01T00:00:00Z", // RFC3339
objectLockLegalHold: true,
});until := time.Date(2030, 1, 1, 0, 0, 0, 0, time.UTC)
_, err := s3.PutObject(ctx, "vault", "ledger.json", body,
lockwellsdk.WithObjectLockRetention(lockwellsdk.ObjectLockCompliance, until),
lockwellsdk.WithObjectLockLegalHold(true))var opts = new PutOptions()
.objectLock("COMPLIANCE", "2030-01-01T00:00:00Z") // mode + RFC3339 retain-until
.legalHold(true);
s3.putObject("vault", "ledger.json", body, opts);The Go S3 client uses an ObjectRetention with a mode enum and time.Time for a post-write retention update; legal hold takes a boolean. Both mutation methods emit the standard S3 XML request shape and reject an invalid mode, zero date, or non-future retention date before transport. GetObjectRetention and GetObjectLegalHold read the current lock state. All four Go methods target a version through WithVersionID; without it, they use the current version. A version with no retention reports a not-found error from GetObjectRetention.
const r = await s3.getObjectRetention("vault", "ledger.json");
console.log(r.mode, r.retainUntilDate); // "COMPLIANCE" "2030-01-01T00:00:00Z"
const held = await s3.getObjectLegalHold("vault", "ledger.json"); // booleanuntil := time.Date(2030, 1, 1, 0, 0, 0, 0, time.UTC)
err := s3.SetObjectRetention(ctx, "vault", "ledger.json", lockwellsdk.ObjectRetention{
Mode: lockwellsdk.ObjectLockCompliance,
RetainUntilDate: until,
}, lockwellsdk.WithVersionID(versionID))
err = s3.SetObjectLegalHold(ctx, "vault", "ledger.json", true,
lockwellsdk.WithVersionID(versionID))
r, err := s3.GetObjectRetention(ctx, "vault", "ledger.json")
if lockwellsdk.IsNotFound(err) {
// no retention set on this version
}
fmt.Println(r.Mode, r.RetainUntilDate)
held, err := s3.GetObjectLegalHold(ctx, "vault", "ledger.json") // boolvar r = s3.getObjectRetention("vault", "ledger.json"); // mode() + retainUntilDate()
boolean held = s3.getObjectLegalHold("vault", "ledger.json");What protection blocks
While a version is under active retention or a legal hold:
- A delete of that version is refused. S3 returns
403 AccessDenied; the native API returns412withretention_blockedorlegal_hold_blocked. In a versioned bucket, deleting the key without a version id still writes a delete marker (the protected version is untouched and recoverable). - An overwrite that would replace the protected version's data is refused.
- Shortening the retain-until date or weakening the mode is refused; extending the date is allowed.
A legal hold has no expiry. It stays until you explicitly set it OFF (a boolean false on the Node native client), even after retention expires.
No governance bypass
On AWS, a caller with the s3:BypassGovernanceRetention permission can delete a GOVERNANCE-mode object early by sending a bypass header. Lockwell does not implement that bypass. A GOVERNANCE retention is enforced the same way as COMPLIANCE until its date passes, on both the S3 and native paths. There is no header, scope, or admin call that removes an active retention early. This is a deliberate non-goal that keeps the WORM guarantee honest.
The governance bypass is a deliberate non-goal. GOVERNANCE and COMPLIANCE are equally immutable here, so do
not rely on object lock for a deletable-by-an-operator escape hatch. :::
If you need a deletable-by-an-operator escape hatch, do not rely on object lock for it. Object lock means the data is immutable for the window you set.
Related
- Versioning for the version history object lock protects.
- Deleting objects for how a delete behaves when a version is protected.
- Errors and retries for the surface-specific error a blocked delete returns.
- S3 operations reference for the full operation matrix.