2026-08-02 · 15 min read
AWS Security Done Right: The Services and Habits That Cut Risk Multiple-Fold
The AWS services and habits that reduce risk the most, Secrets Manager with rotation, SSM (no SSH keys), least-privilege IAM, CodeConnections, KMS, comprehensive logging (CloudTrail + VPC Flow Logs + S3 access logs), and the guardrails, with runnable Terraform.

AWS Security Done Right: The Services and Habits That Cut Risk Multiple-Fold
Most AWS breaches don't come from clever attackers, they come from boring, avoidable mistakes: a long-lived access key in a repo, an IAM policy with "*" on everything, a secret hard-coded in an environment variable, encryption nobody actually controls. The good news is that a handful of AWS services and habits eliminate most of that risk, and they're not hard to adopt. This is the set I reach for on every account, with the reasoning behind each, and I've published runnable Terraform for all of it in aws-secure-foundations so you can see exactly how it fits together.
Three principles run through everything below: no long-lived credentials, least privilege always, and secrets live in a secret store. Keep those in mind and most of AWS security follows.
1. Secrets Manager: stop hard-coding secrets
The single most common credential leak is a secret committed to a repo or baked into a container image. AWS Secrets Manager fixes this by giving secrets one encrypted, access-controlled, auditable home, and the ability to rotate them without redeploying.
How to use it the right way:
- Store the secret in Secrets Manager; your app fetches it at runtime by ARN using its IAM role. The value never touches your build.
- Scope readers with least privilege:
secretsmanager:GetSecretValueon that one secret's ARN, never*. - Turn on rotation for database credentials, Secrets Manager can rotate RDS creds for you.
Why it's worth it: every access is logged in CloudTrail, the value is encrypted with KMS, and revoking access is one policy change. Compare that to a password in a .env file that lives forever and leaks silently.
2. Least-privilege IAM roles: shrink the blast radius
If a credential leaks, the only thing that limits the damage is how narrow its permissions were. Least privilege means specific actions on specific resource ARNs: so a compromised role can touch one bucket, not your whole account.
How to use it the right way:
- Start from zero permissions and add only what the workload demonstrably needs.
- Prefer resource-level ARNs over
*. Most S3, DynamoDB, SQS, and Secrets Manager actions support them. - One role per workload, don't share a giant role across services.
- Use short-lived role credentials (instance profiles, IRSA, task roles), never static access keys.
Why it matters: the difference between "attacker read one bucket" and "attacker owned the account" is usually just policy scope. I've seen both outcomes; the only variable was whether someone bothered to scope the role.
3. CodeConnections: CI/CD to GitHub with no stored token
Wiring AWS CI/CD to GitHub used to mean storing a long-lived Personal Access Token in your pipeline. Tokens over-scope, leak, and rarely get rotated. CodeConnections (formerly CodeStar Connections) replaces that with a GitHub App that AWS manages, minting short-lived tokens on demand. You store nothing.
How to use it the right way:
- Create the connection, authorize it once in the console (an OAuth handshake, it can't be done in Terraform by design), then reference it by ARN in CodePipeline/CodeBuild.
- Scope pipeline roles to
UseConnectionon that specific connection ARN. - Install the GitHub App on only the repos the pipeline needs.
Why it matters: it removes an entire class of credential from your system. There's no token to leak because there's no token.
4. Customer-managed KMS keys: make decryption a second lock
Turning on encryption at rest is easy, but AWS-managed keys give you no say over who can decrypt. A customer-managed key (CMK) with a tight key policy makes decryption an independent, auditable control: even a role with s3:GetObject can't read the data unless the key policy also allows it to use the key.
How to use it the right way:
- Create a CMK with automatic rotation enabled.
- Keep key administration and key usage as separate permissions, workloads get
Encrypt/Decrypt/GenerateDataKey, notkms:*. - Enable S3 bucket keys to avoid a KMS call per object (and a surprising bill).
Why it matters: for anything sensitive, defense in depth means two locks (IAM and the key policy), not one.
5. The account guardrails: turn these on everywhere
Five services quietly do most of the heavy lifting, and several compliance regimes effectively require them. They're the "always on" list:
- GuardDuty: continuous threat detection from your logs, no agents.
- Security Hub: aggregates findings and scores you against standards (CIS, AWS FSBP).
- AWS Config: records what every resource looked like over time (inventory + compliance history).
- IAM Access Analyzer: flags any resource shared outside your account. It's how you catch the overly broad policy you missed in principle #2.
- CloudTrail: the tamper-evident audit log of every API call. If you enable one thing, enable this, multi-region, with log-file validation.
Why it matters: detection and an audit trail are cheap relative to a breach. Without CloudTrail you can't even answer "what happened?" after an incident. The security-baseline example turns all five on in one terraform apply.
6. Systems Manager (SSM): no SSH keys, no bastion, no pain
SSH keys are credentials. Credentials leak. AWS Systems Manager replaces them with a browser-based (or CLI) session that authenticates through IAM, no key pairs, no open port 22, no bastion host to patch.
How to use it the right way:
- Session Manager: interactive shell on any EC2/ECS instance with the SSM agent installed (it's pre-installed on Amazon Linux / AL2023). Access is controlled by IAM:
ssm:StartSessionon the instance ARN. Sessions are logged to CloudWatch Logs or S3 automatically. - Parameter Store: store non-secret config (feature flags, endpoints, ARNs) with free standard-tier parameters. For secrets, use Secrets Manager; for everything else, Parameter Store avoids hard-coding.
- Run Command: push a script or an Ansible playbook across a fleet of instances without SSH. Results are streamed to CloudWatch Logs. Useful for emergency patches or log collection.
- Patch Manager: automated, schedule-based patching with configurable maintenance windows and compliance reporting.
Why it matters: every SSH key you eliminate is one fewer credential that can leak. Every bastion you remove is one fewer server to patch and one fewer network path an attacker can traverse. SSM gives you the same operational access with an audit trail, IAM-controlled, and no exposed ports.
# Terraform: IAM policy for Session Manager (minimum viable)
data "aws_iam_policy_document" "ssm_session" {
statement {
actions = ["ssm:StartSession"]
resources = ["arn:aws:ec2:*:*:instance/*"]
condition {
test = "StringEquals"
variable = "ssm:resourceTag/Environment"
values = ["production"]
}
}
}
7. Secrets rotation: make stolen secrets expire automatically
Secrets Manager supports automatic rotation so even if a credential leaks, it's only valid for hours or days, not forever. For RDS databases, AWS provides built-in Lambda rotators; for custom secrets, you write a short Lambda that generates a new value and updates the consumer.
How rotation works under the hood:
- Secrets Manager invokes your rotation Lambda on a schedule (e.g. every 30 days).
- The Lambda creates a new credential version (AWSPENDING), tests it, then promotes it to AWSCURRENT.
- The old value remains briefly (as AWSPREVIOUS) for graceful failover.
- Your app always fetches AWSCURRENT, it sees the new credential with zero downtime.
How to set it up:
- For RDS/Aurora: enable rotation in the console or Terraform (
rotation_rulesblock + the AWS-providedSecretsManagerRDSPostgreSQLRotationSingleUserLambda). No custom code needed. - For custom secrets: write a Lambda implementing the four rotation steps (
createSecret,setSecret,testSecret,finishSecret). AWS publishes templates for most patterns. - Set the rotation interval tight enough that a leaked credential expires before it's exploited (30 days is common; 7 days for high-sensitivity).
Why it matters: rotation turns a stolen secret from a permanent key into a time-bomb that goes dead on its own. Combined with CloudTrail alerts on unexpected GetSecretValue calls, you catch and contain breaches faster.
# Terraform: Secrets Manager with automatic RDS rotation
resource "aws_secretsmanager_secret_rotation" "db" {
secret_id = aws_secretsmanager_secret.db_creds.id
rotation_lambda_arn = aws_lambda_function.rotator.arn
rotation_rules {
automatically_after_days = 30
}
}
8. Log everything: the visibility that makes the rest useful
Security services are only as good as the data flowing into them. "Log everything" means: every API call, every network flow, every data access, retained long enough to investigate. Without it, you're blind, you can't detect, you can't respond, and you can't prove what happened.
The logging stack:
- CloudTrail (management + data events): the spine. Enable multi-region, organization trail with log-file validation. Store in a dedicated log-archive account if multi-account.
- VPC Flow Logs: capture every accepted/rejected network packet across ENIs, subnets, or VPCs. Sent to CloudWatch Logs or S3. Critical for detecting port scans, lateral movement, or unexpected egress.
- S3 access logs: server access logging on sensitive buckets (separate from CloudTrail data events). Shows who accessed what, from where.
- CloudWatch Logs + retention policies: application logs, Lambda logs, SSM session logs. Set explicit retention (e.g. 90 days for app logs, 365 for security) or costs grow silently.
- Centralized logging: in a multi-account org, push all CloudTrail + VPC Flow Logs to a log-archive account with immutable S3 (Object Lock) so an attacker who owns one account can't delete the evidence.
Why it matters for Cloud Operations (and compliance): the entire security-response workflow: alert → investigate → contain → prove: depends on having the logs. GuardDuty reads CloudTrail + VPC Flow Logs; Security Hub aggregates findings; but without the raw data retained, you lose both detection and forensics.
# Terraform: VPC Flow Logs → CloudWatch Logs (all traffic)
resource "aws_flow_log" "main" {
vpc_id = aws_vpc.main.id
traffic_type = "ALL"
log_destination_type = "cloud-watch-logs"
log_destination = aws_cloudwatch_log_group.flow.arn
iam_role_arn = aws_iam_role.flow_log.arn
}
resource "aws_cloudwatch_log_group" "flow" {
name = "/vpc/flow-logs/${aws_vpc.main.id}"
retention_in_days = 90
}
The habits that tighten it further
The services help, but a few habits matter just as much:
- Block public access on S3 by default: set the account-level and bucket-level public access blocks.
- No root usage; MFA everywhere. The root account is for break-glass only.
- Scan before you ship: secret scanning and IaC scanning in CI catch mistakes before they reach the account (I keep a DevSecOps starter kit for exactly this).
- Everything as code, reviewed. Terraform in a pull request means security review happens before the change is live, and you have a history of who changed what.
Where to start
If you inherit an account tomorrow, do these in order: turn on CloudTrail and GuardDuty (visibility first), enable the S3 public access block account-wide, move any static access keys to roles, and pull hard-coded secrets into Secrets Manager. That sequence removes the biggest risks fast.
All of it, Secrets Manager, least-privilege IAM, CodeConnections, KMS, and the baseline, is in aws-secure-foundations as small, CI-validated Terraform examples that each explain the why. Clone it, read the READMEs, and adapt it to your account.
Security on AWS isn't about doing a hundred things. It's about doing these few things consistently, and never shipping the boring mistakes.
I design and operate secure cloud infrastructure and help teams get their AWS security right as a consultant and trainer. Reach out, or explore more on the blog.