OnCallReady

Lesson 35.27 · AWS I: CLI, IAM, S3 & KMS · 16 min read

Versioning, lifecycle rules and encryption at rest

In plain words

Versioning is like a notebook where you never erase: every time you change a page you write a new copy and keep the old one underneath. "Deleting" just puts a sticky note on top saying "gone". Peel off the note and the page is back.

Lifecycle rules are a cleaning schedule: move pages nobody reads to the cheap cellar after a month, throw away old copies after a year. Encryption is the lock on the notebook, and you choose who holds the key: the shop (SSE-S3) or your own safe (SSE-KMS).

Versioning, lifecycle and encryption at rest

Three bucket settings decide whether you can undo a mistake, what the bucket costs in a year, and who can read the bytes on disk. Versioning keeps every overwrite and delete recoverable. Lifecycle rules move and expire data on a schedule. Default encryption picks the key every new object is encrypted with. All three are set once and forgotten - until the day someone runs aws s3 rm --recursive on the wrong prefix.

Need to know: a bucket is unversioned, Enabled or Suspended - once enabled, it can never go back to unversioned. With versioning, a delete adds a delete marker (the data stays, as a noncurrent version) and an overwrite keeps the old version; you restore by removing the marker or copying an old version back. Lifecycle rules transition (STANDARD_IA after 30 days at the earliest) and expire objects - and noncurrent versions, or a versioned bucket grows forever. Every new object is encrypted, SSE-S3 by default; SSE-KMS adds a KMS key whose policy must also allow you.

Versioning

$ cd ~/oncall-lab/labs/aws
$ aws s3 mb s3://try-config-111122223333
make_bucket: try-config-111122223333
$ aws s3api get-bucket-versioning --bucket try-config-111122223333
$ aws s3api put-bucket-versioning --bucket try-config-111122223333 --versioning-configuration Status=Enabled
$ aws s3api get-bucket-versioning --bucket try-config-111122223333
{
    "Status": "Enabled"
}
$ aws s3 cp try/config-v1.yaml s3://try-config-111122223333/app/config.yaml
upload: try/config-v1.yaml to s3://try-config-111122223333/app/config.yaml
$ aws s3 cp try/config-v2.yaml s3://try-config-111122223333/app/config.yaml
upload: try/config-v2.yaml to s3://try-config-111122223333/app/config.yaml
$ aws s3api list-object-versions --bucket try-config-111122223333 --prefix app/config.yaml --query 'Versions[].[VersionId,IsLatest,Size,LastModified]' --output text
iXx3aMi2m.r60IMYiCvSbgG.YaC3rxnW	True	29	2026-09-22T20:00:04+00:00
Ths0w9hF0n4IjDZW3IzLFnbmJABR7t8o	False	28	2026-09-22T20:00:04+00:00

Note the first get-bucket-versioning: an unversioned bucket returns nothing at all (an empty response prints nothing), not "Disabled". Every version has its own VersionId; reading without one gets the latest. Now delete it - the way aws s3 rm always deletes:

$ aws s3 rm s3://try-config-111122223333/app/config.yaml
delete: s3://try-config-111122223333/app/config.yaml
$ aws s3 ls s3://try-config-111122223333/app/
$ aws s3api get-object --bucket try-config-111122223333 --key app/config.yaml /tmp/c.yaml
aws: [ERROR]: An error occurred (NoSuchKey) when calling the GetObject operation: The specified key does not exist.
$ aws s3api list-object-versions --bucket try-config-111122223333 --prefix app/ --query '{versions: Versions[].[VersionId,IsLatest], markers: DeleteMarkers[].[VersionId,IsLatest]}'
{
    "versions": [
        [
            "iXx3aMi2m.r60IMYiCvSbgG.YaC3rxnW",
            false
        ],
        [
            "Ths0w9hF0n4IjDZW3IzLFnbmJABR7t8o",
            false
        ]
    ],
    "markers": [
        [
            ".zGYGVz.M6tiG01Ey3K1i0QsBjhjB4ei",
            true
        ]
    ]
}

The object "is gone" - ls shows nothing, GET says NoSuchKey - but the bytes are still there: the delete only added a delete marker as the latest version. Two ways back:

$ MARKER=$(aws s3api list-object-versions --bucket try-config-111122223333 --prefix app/config.yaml --query 'DeleteMarkers[?IsLatest].VersionId' --output text)
$ aws s3api delete-object --bucket try-config-111122223333 --key app/config.yaml --version-id $MARKER
{
    "DeleteMarker": true,
    "VersionId": ".zGYGVz.M6tiG01Ey3K1i0QsBjhjB4ei"
}
$ aws s3 cp s3://try-config-111122223333/app/config.yaml -
replicas: 3
log_level: debug
$ OLD=$(aws s3api list-object-versions --bucket try-config-111122223333 --prefix app/config.yaml --query 'Versions[?!IsLatest] | [0].VersionId' --output text)
$ aws s3api copy-object --bucket try-config-111122223333 --key app/config.yaml --copy-source "try-config-111122223333/app/config.yaml?versionId=$OLD" --query VersionId --output text
qMx7XMUt6iXennITdw7VrANVJEySPreS
$ aws s3 cp s3://try-config-111122223333/app/config.yaml -
replicas: 2
log_level: info
  1. Delete the delete marker (a delete-object with its --version-id): the previous version becomes the latest again.
  2. Copy an old version over the current one (copy-object from key?versionId=...): a new version with the old content - the history is kept, which is what you want for "roll back the config".

A delete-object with a --version-id of real data is the one delete that cannot be undone. That is why the permission is separate (s3:DeleteObjectVersion) and worth denying to everyone but a break-glass role. Suspending versioning stops creating new versions (new writes get the version ID null) but keeps all the old ones. MFA delete can require an MFA code for version deletes and versioning changes (root user only, rarely used); Object Lock makes versions undeletable for a retention period (backups, compliance).

Lifecycle rules

A lifecycle configuration is a list of rules, each with a filter (a prefix, tags, sizes) and actions:

ActionDoes
Transitionsmove current versions to a cheaper class after N days (STANDARD_IA / ONEZONE_IA not before 30 days, GLACIER_IR, GLACIER, DEEP_ARCHIVE)
Expirationdelete current versions after N days (on a versioned bucket: add a delete marker)
NoncurrentVersionTransitions / NoncurrentVersionExpirationthe same for old versions, N days after they became noncurrent (NewerNoncurrentVersions keeps the last few)
AbortIncompleteMultipartUploaddelete the parts of uploads that never finished - invisible, but billed
ExpiredObjectDeleteMarkerremove delete markers that have no versions left behind them
$ cat try/lifecycle.json
{
    "Rules": [
        {
            "ID": "logs-tiering",
            "Status": "Enabled",
            "Filter": {
                "Prefix": "logs/"
            },
            "Transitions": [
                {
                    "Days": 30,
                    "StorageClass": "STANDARD_IA"
                },
                {
                    "Days": 90,
                    "StorageClass": "GLACIER"
                }
            ],
            "Expiration": {
                "Days": 365
            }
        },
        {
            "ID": "old-versions",
            "Status": "Enabled",
            "Filter": {},
            "NoncurrentVersionExpiration": {
                "NoncurrentDays": 30,
                "NewerNoncurrentVersions": 3
            }
        },
        {
            "ID": "abort-uploads",
            "Status": "Enabled",
            "Filter": {},
            "AbortIncompleteMultipartUpload": {
                "DaysAfterInitiation": 7
            }
        }
    ]
}
$ aws s3api put-bucket-lifecycle-configuration --bucket try-config-111122223333 --lifecycle-configuration file://try/lifecycle.json
{
    "TransitionDefaultMinimumObjectSize": "all_storage_classes_128K"
}
$ aws s3api get-bucket-lifecycle-configuration --bucket try-config-111122223333 --query 'Rules[].[ID,Status]' --output text
logs-tiering	Enabled
old-versions	Enabled
abort-uploads	Enabled
$ aws s3api put-bucket-lifecycle-configuration --bucket try-config-111122223333 --lifecycle-configuration '{"Rules":[{"ID":"too-soon","Status":"Enabled","Filter":{"Prefix":"logs/"},"Transitions":[{"Days":7,"StorageClass":"STANDARD_IA"}]}]}'
aws: [ERROR]: An error occurred (InvalidArgument) when calling the PutBucketLifecycleConfiguration operation: 'Days' in Transition action must be greater than or equal to 30 for storageClass 'STANDARD_IA'

Lifecycle runs asynchronously (S3 evaluates the rules about once a day), the whole configuration is replaced on every put (read it, edit it, put it back), and objects smaller than 128 KB are not transitioned by default (TransitionDefaultMinimumObjectSize) - moving them would cost more than it saves. The classic bill surprise: versioning on, no noncurrent-version rule, and a job that rewrites the same 10 GB every night.

Encryption at rest

Every object S3 stores is encrypted. You choose with which key:

OptionKeyNotes
SSE-S3 (AES256)keys owned and managed by S3the default for every new object since January 2023; no extra permissions
SSE-KMS (aws:kms)a KMS key: the AWS managed aws/s3, or a customer managed oneaccess to the key is checked on every read and write; KMS calls are logged in CloudTrail and billed
DSSE-KMS (aws:kms:dsse)KMS, two layers of encryptionfor rules that demand dual-layer encryption
SSE-Ca key you send with every requestS3 never stores it; lose it, lose the data. Buckets can block it (BlockedEncryptionTypes)
Client-sideencrypted before uploadS3 sees only ciphertext
$ aws s3api get-bucket-encryption --bucket try-config-111122223333
{
    "ServerSideEncryptionConfiguration": {
        "Rules": [
            {
                "ApplyServerSideEncryptionByDefault": {
                    "SSEAlgorithm": "AES256"
                },
                "BucketKeyEnabled": false
            }
        ]
    }
}
$ aws s3api head-object --bucket try-config-111122223333 --key app/config.yaml --query ServerSideEncryption --output text
AES256
$ aws s3api put-bucket-encryption --bucket try-config-111122223333 --server-side-encryption-configuration '{"Rules":[{"ApplyServerSideEncryptionByDefault":{"SSEAlgorithm":"aws:kms"},"BucketKeyEnabled":true}]}'
$ aws s3 cp try/config-v2.yaml s3://try-config-111122223333/app/new.yaml
upload: try/config-v2.yaml to s3://try-config-111122223333/app/new.yaml
$ aws s3api head-object --bucket try-config-111122223333 --key app/new.yaml --query '[ServerSideEncryption,SSEKMSKeyId,BucketKeyEnabled]' --output text
aws:kms	arn:aws:kms:eu-central-1:111122223333:key/29385a4d-39e5-413b-a949-5d8a63badf81	True
$ aws s3api head-object --bucket try-config-111122223333 --key app/config.yaml --query ServerSideEncryption --output text
AES256
$ aws kms list-aliases --query "Aliases[?AliasName=='alias/aws/s3'].TargetKeyId" --output text
29385a4d-39e5-413b-a949-5d8a63badf81

In an interview: "Someone deleted objects from a bucket - can you get them back?" - "Only if versioning was on (or there is a backup or replication). With versioning, a normal delete just adds a delete marker: remove the marker, or copy the previous version back. Deletes that name a version ID are permanent, which is why s3:DeleteObjectVersion should be restricted and why Object Lock exists for backups."

You can now: turn versioning on and explain delete markers, recover a deleted or overwritten object two ways, write lifecycle rules for current and noncurrent versions (and know the 30-day and 128 KB rules), and choose between SSE-S3, SSE-KMS, DSSE-KMS and SSE-C - knowing that a new default does not touch old objects.

Why it helps

"Someone deleted the files" and "why is the S3 bill so high" are two of the most common S3 questions, and both are answered in this lesson. Versioning makes mistakes recoverable, lifecycle rules keep that safety from becoming a bill, and default encryption with the right key decides who can read the data at all.

Getting these right before the incident matters: versioning cannot protect objects deleted before it was enabled, and changing the default encryption does not re-encrypt what is already stored.

Commands in this lesson

cd aws cat

FAQ

Can I turn versioning off again?

No. Once enabled, a bucket can only be Enabled or Suspended. Suspended stops creating new versions (new writes get the version ID null) but keeps every existing version. That is why the lab enables versioning on purpose and then adds a lifecycle rule for old versions instead of trying to switch it off.

How do I recover a deleted object?

With versioning, a normal delete adds a delete marker as the current version. list-object-versions shows it; deleting the marker by its version ID makes the previous version current again. To undo an overwrite, copy the older version over the current one. A delete that names a version ID is permanent.

What do lifecycle transitions cost?

Each transition is a request with a price, and the colder classes have a minimum storage duration (30 days for STANDARD_IA, 90 for GLACIER) and minimum billable object size, so moving many tiny objects can cost more than it saves. Transitions from STANDARD to STANDARD_IA need the object to be at least 30 days old.

What is the difference between SSE-S3 and SSE-KMS?

SSE-S3 (AES256) uses keys S3 manages entirely; anyone with s3:GetObject can read. SSE-KMS uses a KMS key, so readers also need kms:Decrypt on that key and every use is logged in CloudTrail; a bucket key reduces the number of KMS calls and their cost. SSE-C uses a key you send with each request.

Does changing the default encryption re-encrypt existing objects?

No. The bucket default only applies to new writes that do not specify their own encryption. Existing objects keep the encryption they were written with until they are copied over themselves (copy-object with the new settings) or rewritten, for example with S3 Batch Operations for large buckets.

In an interview Junior

Someone deleted objects from a bucket. Can you get them back?

Only if versioning was enabled before the delete (or there is a backup or replication). With versioning, a normal delete adds a delete marker: aws s3api list-object-versions shows it, and deleting the marker by its version ID brings the previous version back; an overwrite is undone by copying the older version over the current one. A delete that names a version ID is permanent, which is why s3:DeleteObjectVersion should be tightly restricted and why Object Lock exists for backups. A lifecycle rule then expires noncurrent versions so the safety net does not grow forever.

Also asked: What are S3 lifecycle rules used for? · What is the difference between SSE-S3 and SSE-KMS? · What is a delete marker?

Practise this lesson in the terminal Free, in your browser - a real Ubuntu terminal to try it in, with missions that check your work.