Skip to content

Object lock for documents and backups: research

As of: 2026-09-28. [stale] marks sources older than about 12 months. AWS doc pages are undated (“live doc”). For: RUNBOOK §1.1 (bucket lock before the first upload) and progress doc §3 (governance vs compliance). It follows 2026-09-28-malaysia-hosting-research.md, which recommends S3 in ap-southeast-5. Status: research and a proposed configuration. Nothing here has been run against a real bucket.


1. Decision this supports: compliance mode, 8 years

Section titled “1. Decision this supports: compliance mode, 8 years”
  • Compliance: nobody, not even the AWS root user, can delete or overwrite a locked version, change its mode or shorten its period (docs). AWS names only one way out: delete the whole AWS account.
  • Governance: anyone with s3:BypassGovernanceRetention can remove the lock. A leaked admin key could erase records, which is exactly the threat the lock exists for.
  • Legal hold has no expiry and can be removed by anyone with s3:PutObjectLegalHold. It is not a substitute.

Compliance fits a 7-year statutory record. RUNBOOK §1.1 says governance and should be changed to compliance when this is accepted.

Because compliance can’t be undone, every setting is tried first in governance mode with a 1-day retention (§3, step 2).

  • Object Lock in ap-southeast-5 is implied, not stated. AWS says “all Regions” at launch (2018 [stale]) and for the 2026-09 variable-retention feature (2026). The dry run proves it.

  • Prices per GB-month in ap-southeast-5 (official Price List API, 2026-09):

    Class USD / GB-month
    Standard 0.0225
    Standard-IA 0.01242
    Glacier Instant Retrieval 0.0045
    Glacier Deep Archive 0.0018
  • The lock is kept when lifecycle moves an object to another class (docs). Old backups can sit in Deep Archive. Restoring from there takes hours, so recent backups stay warm.

  • Versioning is required, and once the lock is on, versioning can’t be suspended (docs). S3 can now add a lock to an existing bucket. MinIO community builds may not; only the commercial AIStor docs say so.

  • New in 2026-09: variable retention with event holds. It could express “7 years from the end of the year of assessment” directly, instead of a flat 8 years from upload. Worth a look once the basic setup works.

  • A delete that names a version returns 403.
  • A plain delete returns 200 and adds a delete marker. The locked version survives, and the marker is not protected. The app never deletes (there is no RemoveObject in the code), so this only matters for tools such as backup expiry.

Upload integrity: the app’s thumbnail upload will be rejected

Section titled “Upload integrity: the app’s thumbnail upload will be rejected”
  • AWS: Content-MD5 or a checksum header “is required for any request to upload an object with a retention period configured using Object Lock” (PutObject).

  • Browser uploads (PresignPut) already sign X-Amz-Checksum-Sha256. That is probably accepted: S3’s own error text, quoted in a third-party report, names “x-amz-checksum-” headers (loki#20088). AWS docs don’t say so directly, so the dry run confirms it.

  • Thumbnail uploads (Put, s3store.go:126) send neither header. Verified in minio-go v7.3.0 source:

    • SendContentMd5 defaults to false.
    • The automatic CRC32C checksum is added only when the client sets TrailingHeaders (api.go:327, api-put-object.go:354), and ours doesn’t.
    • Small objects take the single-part path, which adds only Content-MD5 and only when asked (api-put-object-streaming.go:658).

    Fix: minio.PutObjectOptions{ContentType: contentType, SendContentMd5: true}.

  • Local testing will not catch this. MinIO does not enforce the rule (minio#16480 [stale], still open).

  • Each re-upload creates a new version, and every version is locked for 8 years and billed (docs).
  • Open question: is a thumbnail a business record? If not, either never re-upload an existing thumbnail, or keep thumbnails in a separate bucket without the lock.
  • WAL-G can lock what it writes: S3_RETENTION_MODE=COMPLIANCE, S3_RETENTION_PERIOD in seconds (docs).
  • pgBackRest has no lock setting. It only notes that locking the bucket “may be useful”, and offers repo-target-time to read a versioned repository as it was (guide). A bucket-level default retention does the locking instead.
  • Not verified for either tool:
    • How their expiry behaves against locked objects. From the delete rules above, it would appear to succeed while storage keeps growing until the lock ends.
    • Whether they send the integrity header.
  • Pattern: a separate backup bucket, compliance 8 years, lifecycle to Deep Archive, and old versions expired only after the lock period.
  • Block public access and TLS only (aws:SecureTransport) (best practices).
  • Encryption: SSE-S3, not SSE-KMS. The lock “does not protect against losing access to the encryption keys”. Deleting a KMS key would make locked records unreadable, which is a second way to destroy them (docs).
  • MFA delete: skip. It can’t be used with lifecycle rules, and compliance mode already covers the same threat (docs).
  • Deny s3:PutBucketObjectLockConfiguration in the bucket policy except for a break-glass role.
  • CloudTrail data events on both buckets, written to a third locked bucket.
  • The app’s credentials: s3:PutObject, s3:GetObject and s3:ListBucket only. No retention, bypass or bucket-configuration permissions.
  • Commands: mc mb --with-lock ALIAS/erp-docs, then mc retention set --default COMPLIANCE 8y ALIAS/erp-docs (AIStor docs).
  • The minio/minio repository was archived on 2026-04-25 (“NO LONGER MAINTAINED”). It is still AGPLv3, and the old binaries get no updates (repo). The dev stack’s quay.io/minio/minio:RELEASE.2025-09-07… image still works locally. As a production fallback, MinIO now means the commercial AIStor or an unmaintained build. That strengthens the case for S3.
Terminal window
REGION=ap-southeast-5
DOCS=erp-docs-prod
# 1. Create with Object Lock (turns versioning on)
aws s3api create-bucket --bucket $DOCS --region $REGION \
--create-bucket-configuration LocationConstraint=$REGION \
--object-lock-enabled-for-bucket
# 2. Dry run: GOVERNANCE, 1 day. Exercise the app: browser upload, thumbnail, download.
aws s3api put-object-lock-configuration --bucket $DOCS --object-lock-configuration \
'{"ObjectLockEnabled":"Enabled","Rule":{"DefaultRetention":{"Mode":"GOVERNANCE","Days":1}}}'
# 3. Only after step 2 passes: COMPLIANCE, 8 years. Cannot be undone.
aws s3api put-object-lock-configuration --bucket $DOCS --object-lock-configuration \
'{"ObjectLockEnabled":"Enabled","Rule":{"DefaultRetention":{"Mode":"COMPLIANCE","Years":8}}}'
# 4. Block public access; 5. SSE-S3
aws s3api put-public-access-block --bucket $DOCS --public-access-block-configuration \
BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true
aws s3api put-bucket-encryption --bucket $DOCS --server-side-encryption-configuration \
'{"Rules":[{"ApplyServerSideEncryptionByDefault":{"SSEAlgorithm":"AES256"}}]}'
# 6. Bucket policy: deny non-TLS; deny lock-config and policy changes except a break-glass role
# 7. Same for the backup bucket, plus lifecycle to DEEP_ARCHIVE
# 8. CloudTrail S3 data events → a third locked bucket

4. Code changes this implies (1, 2 and 4 made on ops-groundwork)

Section titled “4. Code changes this implies (1, 2 and 4 made on ops-groundwork)”
# Where Change Needs a decision?
1 s3store.go Put SendContentMd5: true, or AWS rejects thumbnails Done
2 s3store.go EnsureBucket In production, don’t create an unlocked bucket: fail at startup if it is missing (optionally check GetObjectLockConfig). minio-go has MakeBucketOptions.ObjectLocking for dev Done: fails unless OBJECT_STORE_CREATE_BUCKET=true
3 Thumbnail keys Write once, or a separate unlocked bucket Yes: is a thumbnail a record?
4 Tests MinIO can’t catch #1, so a test asserts the header on the request instead Done
  • An AWS page naming Object Lock in ap-southeast-5 (only “all Regions”)
  • Whether x-amz-checksum-sha256 alone satisfies the lock rule (third-party wording only)
  • Any AWS Support process to shorten compliance retention; what happens to locked data after an account is closed
  • Whether community MinIO RELEASE.2025-09-07 can lock an existing bucket
  • Whether quay.io/minio/minio images are still published
  • pgBackRest / WAL-G expiry and integrity headers against locked buckets
  • Minimum storage durations and per-object fees for the cheaper classes in ap-southeast-5
  • The exact LocationConstraint requirement for this region; the policy JSON is untested