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;
native.createBucket("vault", new CreateBucketOptions("enabled", true));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"native.setObjectRetention("vault", "ledger.json", "COMPLIANCE", "2030-01-01T00:00:00Z");
native.setObjectLegalHold("vault", "ledger.json", "ON");
var r = native.getObjectRetention("vault", "ledger.json"); // mode() + retainUntil()
var hold = native.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.
The S3 client sets a version's retention and legal hold at PUT time, as options on PutObject. There is no separate "put retention" call on the S3 client: the lock state is applied when the object version is written. Mode and retain-until must be supplied together; the server rejects one without the other, and the bucket must have object lock enabled.
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 S3 client reads the current lock state with GetObjectRetention and GetObjectLegalHold. Both target a version through the version-id option; without it, they read 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"); // booleanr, 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 with a 403 (forbidden). 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 403 a blocked delete returns.
- S3 operations reference for the full operation matrix.