Macie & Secrets Manager
Find sensitive data; rotate secrets by identity.
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.
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.
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.
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"}
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
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.