Org trails & immutable logging
One trail to a locked account, tamper-evident.
When an attacker gets into an AWS account, the careful ones go quiet fast. First thing they do is kill the logging. If nothing wrote down what they touched, nobody can prove the break-in happened, and nobody can trace how they got in. So treat your audit trail (the running record of who did what) as the one system you have to assume they'll come for. Two rules fall out of that. It can't sit anywhere they can reach. And it can't be something they can quietly edit later. A good security-camera setup works the same way. The camera watches every door. The recorder lives in a locked room in a separate building, so nobody standing in the lobby can rip the tape out. And every tape gets a tamper-evident seal, so if it ever lands in court, you can show that nobody spliced it.
One trail, recording every account
AWS keeps that record through a service called CloudTrail. Every API call (an API call is just an instruction sent to AWS: create a user, open a firewall rule, read a secret) gets logged as a line saying who made it, from where, and when. The catch with the default setup is that each account keeps its own trail. Own the account and you own the record of your own break-in. An organization trail closes that gap. You define a single trail up in the management account (the account that sits at the top of your org and owns all the others), turn on --is-organization-trail, and it captures activity from every account in the organization, in every region, then drops all of it into one S3 bucket you control. S3 is Amazon's cloud file storage, basically a giant shared folder in the cloud. That bucket lives inside a dedicated log-archive account that runs no workloads and that almost nobody can sign into. The accounts being watched have no route to the place doing the watching. Management events, the calls that change how things are set up, are recorded by default. Data events, the individual reads and writes to things like files in S3 or Lambda functions, cost extra, so you turn those on where the risk is worth the money.
$ aws cloudtrail create-trail --name org-trail \--s3-bucket-name acme-org-audit \--is-organization-trail --is-multi-region-trail \--enable-log-file-validation --kms-key-id alias/cloudtrail-cmk{"Name": "org-trail","S3BucketName": "acme-org-audit","IncludeGlobalServiceEvents": true,"IsMultiRegionTrail": true,"TrailARN": "arn:aws:cloudtrail:eu-west-1:111122223333:trail/org-trail","LogFileValidationEnabled": true,"IsOrganizationTrail": true,"KmsKeyId": "arn:aws:kms:eu-west-1:111122223333:key/8f2b1c0a-4d3e-49aa-9c11-7e6b2f0d5a44"}$ aws cloudtrail start-logging --name org-trail$ aws cloudtrail get-trail-status --name org-trail{"IsLogging": true,"StartLoggingTime": "2026-07-16T09:12:03.882000+00:00","LatestDeliveryTime": "2026-07-16T09:14:52.331000+00:00","LatestDigestDeliveryTime": "2026-07-16T09:13:40.117000+00:00"}
A couple of things have to line up before this works. CloudTrail and AWS Organizations need trusted access switched on between them, and you either create the trail from the management account or hand the job to a delegated administrator account, so you're not signing into the org's most powerful account for routine work. Once it's running, any account that joins the org later gets picked up on its own, with no extra setup per account. That's the whole reason to do it at the org level. Putting everything on one trail has a flip side, though. That single trail feeds detection for every account at once, so if it quietly stops delivering, you go blind across the org in one shot. The LatestDigestDeliveryTime field in the status output is your pulse check, and it's why the digest checks and the StopLogging alarms further down aren't optional extras.
A trail is only as trustworthy as the place its logs land. If an admin (or an attacker holding an admin's stolen login) can delete yesterday's logs, the record proves nothing. Stopping that is the job of S3 Object Lock. It puts every delivered log file under a retention rule that S3 enforces itself, so the file can't be overwritten or removed until the clock runs out. In Compliance mode, not even the account's root user (the all-powerful master login that can normally do anything) can take it early. Compare a filing cabinet that locks with a bank safe-deposit box. Anyone holding the cabinet key can still empty the drawer. The safe-deposit box the bank simply won't open before the agreed date, no matter who's asking or how senior they claim to be. Object Lock is the safe-deposit box. You switch it on when the bucket is created, set a default retention period (say seven years in Compliance mode, to match your evidence rules), then confirm it's on and prove it by trying to delete a log that's already been written.
$ aws s3api create-bucket --bucket acme-org-audit --region eu-west-1 \--create-bucket-configuration LocationConstraint=eu-west-1 \--object-lock-enabled-for-bucket{"Location": "http://acme-org-audit.s3.amazonaws.com/"}$ aws s3api put-object-lock-configuration --bucket acme-org-audit \--object-lock-configuration '{"ObjectLockEnabled":"Enabled","Rule":{"DefaultRetention":{"Mode":"COMPLIANCE","Years":7}}}'$ aws s3api get-object-lock-configuration --bucket acme-org-audit{"ObjectLockConfiguration": {"ObjectLockEnabled": "Enabled","Rule": { "DefaultRetention": { "Mode": "COMPLIANCE", "Years": 7 } }}}$ aws s3api delete-object --bucket acme-org-audit \--key AWSLogs/o-a1b2c3d4e5/111122223333/CloudTrail/eu-west-1/2026/07/16/111122223333_CloudTrail_eu-west-1_20260716T0910Z_9f3c.json.gz \--version-id 3sL4kqtJlcpXroDTDmJVBH40Nrjfkd6oCcHAn error occurred (AccessDenied) when calling the DeleteObject operation:Access Denied because object protected by object lock.
Proving nobody edited the tape
Locking the logs away stops deletion. On its own, though, it doesn't prove the files sitting there are the exact bytes CloudTrail wrote. That's what log file validation is for, the option you switched on earlier with --enable-log-file-validation. Once an hour, CloudTrail writes a digest file: a signed list of every log file it delivered in that window, each one stamped with a cryptographic hash. A hash is a short fingerprint calculated from a file's contents, so change one character in the file and the fingerprint changes too. Every digest also names the digest before it, so the digests link up into a chain. Edit a log file and its fingerprint stops matching the digest. Delete a log file and the digest still lists it, so it shows up as missing. Delete a digest and the next link points at a hole where it used to be. CloudTrail signs each digest with a key that AWS holds and you never see, so an attacker can't forge a clean replacement that passes the check. That signature is what turns a pile of stored logs into evidence. You run validate-logs, it walks the chain, and it tells you in plain numbers whether anything stopped adding up.
$ aws cloudtrail validate-logs \--trail-arn arn:aws:cloudtrail:eu-west-1:111122223333:trail/org-trail \--start-time 2026-07-15T00:00:00Z --end-time 2026-07-16T00:00:00ZValidating log files for trail arn:aws:cloudtrail:eu-west-1:111122223333:trail/org-trail between 2026-07-15T00:00:00Z and 2026-07-16T00:00:00ZResults requested for 2026-07-15T00:00:00Z to 2026-07-16T00:00:00ZResults found for 2026-07-15T00:13:20Z to 2026-07-15T23:47:11Z:24/24 digest files valid576/576 log files valid
Now say someone opened one delivered log file, changed a single line to hide a call, and left everything else untouched. Run the exact same command again. The chain does the rest.
$ aws cloudtrail validate-logs \--trail-arn arn:aws:cloudtrail:eu-west-1:111122223333:trail/org-trail \--start-time 2026-07-15T00:00:00Z --end-time 2026-07-16T00:00:00ZResults found for 2026-07-15T00:13:20Z to 2026-07-15T23:47:11Z:24/24 digest files valid575/576 log files valid, 1/576 log files INVALID (hash value doesn't match)Log file s3://acme-org-audit/AWSLogs/o-a1b2c3d4e5/111122223333/CloudTrail/eu-west-1/2026/07/15/111122223333_CloudTrail_eu-west-1_20260715T1410Z_7b2e.json.gz INVALID: hash value doesn't match
There's one more layer, and it's the loudest of the lot. A competent attacker tries to blind the trail before doing anything noisy, so an alert on the calls that touch logging (StopLogging, DeleteTrail, or a change to the bucket policy on the log sink) is one of the highest-value alarms you can wire up. In a real incident it's often the very first thing that fires, and it's exactly where the response you'll build in the next lesson starts.
Try this
Work through “Proving nobody edited the tape” yourself on a sandbox you can throw away, following the commands above in order. Then break one step deliberately and re-run, so you have seen the failure before it finds you.
Takeaway
The trap worth remembering here: object Lock only works if you turn it on when the bucket is born. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.