CoursesAWS security engineeringMacie & Secrets Manager

Macie & Secrets Manager

Find sensitive data; rotate secrets by identity.

Advanced30 min · lesson 9 of 15

A public S3 bucket full of customer support exports is the kind of thing a stranger finds before you do. S3 is Amazon's file storage, and a bucket is one container inside it. The worse part is you often don't know the bucket is there, and you definitely don't know that one CSV (a plain spreadsheet file) inside it has a column of social-security numbers. You can't lock a drawer you've forgotten you own. Amazon Macie exists to find those drawers. AWS Secrets Manager exists so the keys to your databases stop living in config files where they never change and anyone can copy them.

These are two failures with the same root cause: sensitive things sitting where nobody governs them. Macie finds sensitive data at rest, meaning data sitting in storage rather than moving across the network, and tells you which of it is exposed. Secrets Manager takes the credentials your apps need and turns each one into a managed resource. Now the password has a clear owner, and every read of it lands in an audit log. On top of that, it can change itself on a schedule.

An auditor who opens every drawer

Think of an overnight auditor who walks the building, opens every drawer, flags any page with a social-security number on it, and tapes a warning to any drawer left unlocked or sitting by an open window. That's Macie. It's a managed service that reads the objects (the individual files) in your S3 buckets and checks their contents against data identifiers, which are just patterns that describe what sensitive data looks like. The built-in ones (Macie calls them managed data identifiers) already know the shape of a US social-security number, a credit-card number, a passport number, an AWS secret key, and dozens more. You can add your own with a regular expression, a pattern-matching rule, to catch something like the format of your internal customer IDs. You point Macie at a set of buckets as a classification job, and it reports back.

A Macie finding is more than "found something." It names the bucket and the exact object key (the file's full path inside the bucket), says which category matched (personal, financial, or credentials) and counts how many times. Then it records the state of the bucket at the moment it looked: was it public, and was the data sitting there unencrypted or shared outside your account. That last part is the whole point. "This file has 1,240 SSNs" is a note. "This file has 1,240 SSNs, and the bucket is public and unencrypted" is an incident.

start a sensitive-data discovery job
aws macie2 create-classification-job \
--job-type ONE_TIME \
--name "pii-sweep-prod-uploads-2026q3" \
--s3-job-definition '{"bucketDefinitions":[{"accountId":"222222222222","buckets":["prod-customer-uploads","prod-support-attachments"]}]}' \
--managed-data-identifier-selector ALL \
--sampling-percentage 100 \
--region eu-west-1
{
"jobArn": "arn:aws:macie2:eu-west-1:222222222222:classification-job/a1b2c3d4e5f67890abcdef1234567890",
"jobId": "a1b2c3d4e5f67890abcdef1234567890"
}

A ONE_TIME job sweeps once. A SCHEDULED job re-runs on a set cadence and only pays to inspect objects that are new or changed since the last run, which is what you want once things are steady. Sampling lets you check a percentage of the data instead of every byte, and that matters because Macie bills you per gigabyte it inspects. Scanning your whole estate at 100 percent every day adds up to a real bill, so scope the job to the buckets that matter and let the scheduled runs pick up only what changed.

read the finding the job produced
aws macie2 get-findings \
--finding-ids 8f1e2d3c4b5a69780f1e2d3c4b5a6978 \
--region eu-west-1
{
"findings": [
{
"accountId": "222222222222",
"category": "CLASSIFICATION",
"type": "SensitiveData:S3Object/Personal",
"severity": { "description": "High", "score": 3 },
"classificationDetails": {
"jobId": "a1b2c3d4e5f67890abcdef1234567890",
"result": {
"mimeType": "text/csv",
"sizeClassified": 5242880,
"sensitiveData": [
{
"category": "PERSONAL_INFORMATION",
"totalCount": 1240,
"detections": [
{ "type": "USA_SOCIAL_SECURITY_NUMBER", "count": 1240 }
]
}
],
"status": { "code": "COMPLETE" }
}
},
"resourcesAffected": {
"s3Bucket": {
"name": "prod-support-attachments",
"publicAccess": { "effectivePermission": "PUBLIC" },
"defaultServerSideEncryption": { "encryptionType": "NONE" }
},
"s3Object": { "key": "exports/2026-06/tickets.csv", "size": 5242880 }
},
"createdAt": "2026-07-16T09:14:22.187Z"
}
]
}

That's a finding worth waking up for: 1,240 social-security numbers in a bucket that's public and stored with no encryption. You don't read these by hand. Findings flow into Security Hub, AWS's central place for security alerts, and into EventBridge, its event bus, so a rule can catch a High-severity personal-data finding on a public bucket and fire a Lambda function (a small piece of code AWS runs for you) that switches on Block Public Access before anyone outside notices. Across a whole organization, run Macie from a delegated administrator account, so one job definition covers every member account instead of you setting the service up fifty separate times.

A lock that changes its own combination

A secret in Secrets Manager behaves like a padlock whose combination changes itself on a schedule and only hands the new combination to whoever is carrying the right badge. Under the hood, a secret is just a value, a database password or an API key, wrapped in four things a plain config file never gives you. First, fine-grained rules from IAM, AWS's Identity and Access Management system, saying exactly which identities can read it. Second, encryption at rest through KMS, the Key Management Service that scrambles the stored value. Third, versioning, so every past value is kept. Fourth, an entry in CloudTrail, the audit log, on every single read. Apps fetch the value at runtime using their task or instance role, the identity AWS attaches to the running container or server, so the password never sits in the built image, the git repo, or an .env file. Revoke that role and the access is gone with no redeploy. Each secret can carry its own access policy and its own KMS key, so being allowed to read the payments password tells you nothing about the analytics one, and a single stolen role can't reach everything.

Rotation is where it earns its money. You attach a Lambda and a schedule, and the service drives rotation in four steps against a staged copy of the secret. createSecret generates a brand-new password and stores it under the label AWSPENDING. setSecret writes that password onto the database user. testSecret opens a real connection with the new password to prove it works before anything switches over. finishSecret moves the AWSCURRENT label onto the new version and retires the old one. Because get-secret-value returns AWSCURRENT by default, any consumer that re-reads the secret picks up the new password on its own.

Two rotation strategies ship as ready-made Lambda templates, and the choice matters more than it looks. Single-user rotation changes the password on the one database user in place, which leaves a short window where any process still holding the old password gets turned away. Alternating-users rotation keeps two users and flips between them, so the previous credential stays valid through the next cycle and connections that are already open aren't cut off the instant rotation finishes. For a busy production database, alternating users is the calmer default.

turn on rotation, then confirm it
aws secretsmanager rotate-secret \
--secret-id prod/payments/db \
--rotation-lambda-arn arn:aws:lambda:eu-west-1:222222222222:function:SecretsManagerRDSPostgreSQLRotationMultiUser \
--rotation-rules '{"AutomaticallyAfterDays":30,"Duration":"2h"}' \
--rotate-immediately
{
"ARN": "arn:aws:secretsmanager:eu-west-1:222222222222:secret:prod/payments/db-AbCdEf",
"Name": "prod/payments/db",
"VersionId": "b5f8c2a1-3d4e-4f6a-9b8c-2e1d0f9a8b7c"
}
aws secretsmanager describe-secret --secret-id prod/payments/db \
--query '{Rotation:RotationEnabled,Next:NextRotationDate,Lambda:RotationLambdaARN}'
{
"Rotation": true,
"Next": "2026-08-15T02:00:00+01:00",
"Lambda": "arn:aws:lambda:eu-west-1:222222222222:function:SecretsManagerRDSPostgreSQLRotationMultiUser"
}
how the app reads it at runtime, by identity
aws secretsmanager get-secret-value \
--secret-id prod/payments/db \
--version-stage AWSCURRENT \
--query SecretString --output text
{"username":"payments_app","password":"9Kx!pR2$qL7mZ4wv","engine":"postgres","host":"payments.cluster-c9k.eu-west-1.rds.amazonaws.com","port":5432,"dbname":"payments"}
# The caller's role needs secretsmanager:GetSecretValue on THIS arn only,
# plus kms:Decrypt on the secret's key. That read just wrote a CloudTrail event.
echo $?
0
How managed rotation swaps a live credential
1createSecret
Lambda mints a new password, stores it as version label AWSPENDING
2setSecret
writes the new password onto the database user
3testSecret
opens a real connection with it to prove it works
4finishSecret
AWSCURRENT label moves to the new version; old one is retired
get-secret-value returns AWSCURRENT, so a consumer that re-reads after finishSecret gets the new password with no code change. A consumer that caches forever does not.
Rotation without a re-read is just an outage on a timer
If your app reads the secret once at boot and caches it for the life of the process, rotation will eventually retire the password it's still holding, and every connection starts failing at 2am for no obvious reason. Give consumers a short cache with a refresh, and make the overlap window (how long the old and new credential are both accepted) longer than the longest cache anywhere in the fleet. Alternating-users rotation buys you most of that overlap for free. Test rotation in staging before you schedule it in production, because the failure only shows up when the old credential actually goes away.
Quick check
01Your payments service opens new database connections constantly and cannot tolerate auth failures during rotation. Which setup best avoids a blip when the 30-day rotation fires?
Incorrect — Single-user rotation flips the one user's password in place, so any connection still using the old value is rejected during the swap. Speed is not the problem here; the in-place change is.
Correct — Two users alternate, so the credential from the prior cycle keeps working while the new one takes over. In-flight and freshly opened connections don't hit a wall the instant finishSecret runs.
Incorrect — Manual rotation just moves the same swap-window problem onto a human and makes it rarer and more error-prone, not safer. It doesn't remove the moment where the old password stops working.
Incorrect — Replication protects against a regional outage, not against the credential-swap window. Both copies still rotate to the same new password, so the timing gap remains.
02Beyond counting how many social-security numbers or credit-card numbers an object contains, what else does an Amazon Macie finding record, and why does the lesson call that extra detail 'the whole point'?
Incorrect — Macie reports the sensitive data and the bucket's state, not the identity of whoever uploaded the object.
Incorrect — Macie only discovers and reports; remediation is done separately, for example by an EventBridge rule firing a Lambda function.
Incorrect — Macie does not modify your objects; it inspects and classifies their contents.
Correct — the lesson stresses that the bucket-state context is what separates a harmless note from a genuine incident worth waking up for.
03A service reads its database secret from Secrets Manager once at startup and caches the value for the entire life of the process. You then enable 30-day automatic rotation. What does the lesson predict, and what's the right fix?
Correct — a process that caches forever never re-reads AWSCURRENT, so rotation eventually pulls the password out from under it; a refreshing cache plus a long overlap window prevents the 2am failure.
Incorrect — Secrets Manager does not push updates; consumers only pick up the new value by re-reading, which a forever-cache never does.
Incorrect — client-side caching has no effect on the rotation Lambda, which proceeds through finishSecret and retires the old version regardless.
Incorrect — versioning keeps history, but the AWSCURRENT label moves and the old credential stops being accepted on the database after cutover.

Discovery and rotation shrink two of the quietest risks in an account: data you forgot you had, and credentials that never change. What neither one stops is an attacker who already has a foothold moving from the service they landed on to the next one over. That containment is the network's job, and it's where carving up your VPC (your own private network inside AWS) into separate zones comes in.

Try this

Work through “A lock that changes its own combination” 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: rotation without a re-read is just an outage on a timer. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.

Related