The smallest possible AWS account monitoring kit. Two CloudFormation stacks, ~30 EventBridge rules, an SNS topic. You get an email (or Slack / Teams via AWS Chatbot) the moment something interesting happens in your account. Free or near-free, no agents, no extra accounts, no SaaS.
AWS gives you excellent logging (CloudTrail, VPC Flow Logs, Config) but very few alerts. GuardDuty covers a narrow slice (threat intel, anomaly detection on a few services). It does not tell you when:
- The root account just logged in.
- Someone disabled CloudTrail, deleted a KMS key, or stopped your GuardDuty detector.
- A new IAM user was created, or
AdministratorAccesswas attached to a role. - A security group was opened on port 22 to
0.0.0.0/0. - A snapshot or AMI was shared with another AWS account.
- A flood of
AccessDeniederrors is happening (enumeration, misconfigured workload, compromised credential probing). - Your own monitoring stack just got deleted.
ASSK is the minimum so that "I would notice within minutes if something obviously bad happens" is true for your AWS account. It complements GuardDuty. It is not a SIEM, a SOAR, or a full detection engineering practice.
- Edit the variables at the top of the
Makefile(AlarmRecipient,Project,LocalAWSRegion,CTLogGroupName). - Deploy:
make deploymake deploy does three things, in order:
- Hardens the account (S3 Block Public Access, EBS encryption, public AMI / snapshot block, IMDSv2 default, SSM document public-sharing block) across every enabled region.
- Deploys both CloudFormation stacks (
-localin your region,-globalinus-east-1). - Enables CloudFormation stack termination protection on both stacks.
make tear-down disables termination protection and deletes both stacks.
Always-on detections (CloudWatch alarms and EventBridge rules)
- Root user activity
- CloudTrail tampering (
StopLogging,DeleteTrail,UpdateTrail) - AWS Health Dashboard events
- IAM user changes (
Create,Delete,Update,CreateAccessKey,CreateLoginProfile,UpdateLoginProfile, ...) - IAM admin escalation (
Attach*PolicywithAdministratorAccess) - MFA changes (
CreateVirtualMFADevice,DeactivateMFADevice,DeleteVirtualMFADevice, ...) - AccessDenied / UnauthorizedOperation burst (alarm, threshold configurable, see Parameters)
- Console login failures (alarm)
- EBS snapshot exfiltration (
ModifySnapshotAttribute,SharedSnapshotCopyInitiated,SharedSnapshotVolumeCreated) - AMI exfiltration (
ModifyImageAttribute) sts:GetCallerIdentity(flippable, see "may want to turn OFF" below)- IMDSv1
RunInstances - CloudShell exfiltration (
GetFileDownloadUrls) - KMS key tampering (
DisableKey,ScheduleKeyDeletion,DeleteAlias,DisableKeyRotation,PutKeyPolicy) - Security group ingress / egress changes
- AWS Config tampering (
StopConfigurationRecorder,DeleteConfigurationRecorder,DeleteConfigRule,DeleteEvaluationResults) ec2:GetPasswordData(flippable, see "may want to turn OFF" below)secretsmanager:BatchGetSecretValue(flippable, see "may want to turn OFF" below)- Route53 Resolver query-log deletion (
DeleteResolverQueryLogConfig) - VPC Flow Logs (
DeleteFlowLogs,ModifyFlowLogs) - Security group admin-port exposure IPv4 (22 / 3389 from
0.0.0.0/0) - Security group admin-port exposure IPv6 (22 / 3389 from
::/0) - IAM Roles Anywhere (
CreateProfile,CreateTrustAnchor) - STS
GetFederationToken - GuardDuty tampering (
DeleteDetector,UpdateDetector,DeletePublishingDestination,StopMonitoringMembers,CreateFilterwithARCHIVE) - Self-protection (
DeleteStack,UpdateTerminationProtectionon${Project}-*stacks, see caveat below) - Public Lambda function URL (
CreateFunctionUrlConfig/UpdateFunctionUrlConfigwithAuthType: NONE) - S3 bucket replication configuration (
PutBucketReplication) - CloudWatch Logs bulk export (
CreateExportTask) - RDS instance made internet-facing (
CreateDBInstance/ModifyDBInstancewithPubliclyAccessible: true) - EC2 Client VPN endpoint creation (
CreateClientVpnEndpoint)
Opt-in detections (default off)
Off by default because they are noisy in IaC-heavy environments or only meaningful in specific account / region contexts.
| Detection | Parameter | Enable when |
|---|---|---|
S3 bucket policy / ACL / PublicAccessBlock changes |
EnableS3PolicyDetection |
You want to be alerted on any S3 access-surface change. |
| Lambda creation / code update / permission grants | EnableLambdaDetection |
You do not deploy Lambdas frequently from IaC. |
| IAM Identity Center (SSO) permission sets and account assignments | EnableSSODetection |
Only in the account and region of your SSO tenant. |
| VPC peering, routes, IGW | EnableNetworkInfrastructureDetection |
You do not modify network infrastructure often. |
IAM UpdateAssumeRolePolicy (trust policy changes) |
EnableIamTrustPolicyDetection |
You want to catch a common backdoor mechanism. |
AWS Organizations tampering (DetachPolicy, DeletePolicy, DisablePolicyType, LeaveOrganization, RemoveAccountFromOrganization) |
EnableOrganizationsDetection |
Only in the Organizations management account. |
AWS Organizations membership changes (AccountJoinedOrganization, AccountDepartedOrganization — incl. the async Cleaned finalization) |
EnableOrganizationsMembershipDetection |
Only in the Organizations management account. Very low volume; Cleaned can arrive ~90 days after a CloseAccount. |
IAM enumeration / reconnaissance (GetAccountAuthorizationDetails, GenerateCredentialReport, GenerateOrganizationsAccessReport) |
EnableIamEnumerationDetection |
You want to catch IAM enumeration and your account does not run CSPM/IAM tooling that calls these routinely. |
Existing detections you may want to turn OFF
For backward compatibility these stay on, but they are alert-fatigue cannons in active accounts. Flip the parameter to "false" if your inbox is drowning.
| Detection | Parameter | Why it is noisy |
|---|---|---|
sts:GetCallerIdentity |
EnableGetCallerIdentityDetection |
Called on every AWS SDK init, every terraform plan, every CI step. |
secretsmanager:BatchGetSecretValue |
EnableSecretsBatchGetDetection |
Called by ECS / EKS / Lambda when batch-loading secrets at startup. |
ec2:GetPasswordData |
EnableEc2PasswordDataDetection |
Called by SSM and admin tooling on every Windows admin password fetch. |
Account-level hardening applied by make deploy
- S3 Block Public Access (account-scoped, single call).
- EBS default encryption, in every enabled region.
- Block public AMI sharing, in every enabled region. (announcement)
- Block public snapshot sharing, in every enabled region. (blog post)
- IMDSv2 required by default for new EC2 instances, in every enabled region. (announcement)
- Block public sharing of SSM documents, in every enabled region. (docs) SSM documents can embed scripts, parameters, and internal hostnames; AWS leaves public sharing enabled by default, so this closes the gap for free.
The region loop uses aws ec2 describe-regions with opt-in-status in (opt-in-not-required, opted-in), so no manual region list is needed. Per-call CLI timeouts (--cli-connect-timeout 10 --cli-read-timeout 15) prevent a slow region from stalling the loop. Use SkipRegions="us-west-1 me-south-1" in the Makefile to permanently skip a region.
Core
| Parameter | Description |
|---|---|
AlarmRecipient |
Email address that receives alerts. |
Project |
Stack-name prefix (also matched by the self-protection rule). Default aws-security-survival-kit. |
LocalAWSRegion |
Region where the CloudTrail CloudWatch Logs LogGroup lives. Metric-filter alarms (AccessDenied, Failed Console Login, IMDSv1) are evaluated here. EventBridge rules in cfn-local.yml are deployed here. |
CTLogGroupName |
CloudTrail CloudWatch Logs LogGroup name. Required. |
Tuning
| Parameter | Default | Description |
|---|---|---|
AccessDeniedThreshold |
25 |
Threshold for the AccessDenied alarm. Evaluated as Period=3600s, EvaluationPeriods=2, Statistic=Sum, so it fires when this many events or more occur in each of two consecutive 1-hour windows. Raised from the original 1 to absorb legitimate noise (SCP blocks, eventual-consistency lookups, idempotent retries). |
Optional SNS encryption at rest
| Parameter | Default | Description |
|---|---|---|
EnableSnsEncryption |
"false" |
When "true", both SNS topics are encrypted at rest with a dedicated customer-managed KMS key (CMK). |
TrustedAccountIds |
"" |
Comma-separated list of AWS account IDs allowed to use the CMK for cross-account publish. Leave empty for single-account. |
Cost note: enabling encryption creates two CMKs (one per stack, regional and us-east-1). Expect roughly $2/month at rest plus per-call GenerateDataKey / Decrypt charges that scale with alert volume. The CMK key policy grants kms:Decrypt and kms:GenerateDataKey* to events.amazonaws.com, cloudwatch.amazonaws.com, and chatbot.amazonaws.com, so AWS Chatbot integration keeps working when encryption is enabled.
The self-protection rule alerts on DeleteStack and UpdateTerminationProtection against any stack matching ${Project}-* (matches both literal stack names and ARNs via prefix + wildcard).
UpdateTerminationProtection reliably fires (nothing is being deleted yet). DeleteStack is best-effort: the rule's SNS target lives in the same stack being deleted, so EventBridge and SNS are torn down concurrently with the rule trying to fire. For airtight self-protection use an SCP Deny, a watchdog stack in a separate account, or both.
The alert above is detective — it tells you after the fact. To make ASSK genuinely tamper-resistant, pair it with preventive AWS Organizations Service Control Policies that deny anyone from deleting, disabling, or detaching the kit's own resources and the secure-by-default settings it applies.
ASSK does not deploy these SCPs itself: SCPs require AWS Organizations and must be attached from the management account, which is out of scope for a per-account kit (see AGENT.md). They are documented here so you can apply them with your existing landing-zone / Org tooling.
Before you apply them, read these caveats:
- SCPs only constrain member accounts in an Organization. The management account is never restricted by SCPs — keep ASSK alerting on it and protect it separately.
- The
aws:PrincipalARNexclusions below are your break-glass / deploy escape hatch, somake deployand emergency operators are not locked out. Replace the placeholder role ARNs:<ACCOUNT_ID>— the account (or use*to apply org-wide via an OU).arn:aws:iam::*:role/OrgAdminBreakGlass— your break-glass role.arn:aws:iam::*:role/assk-deployer— the principal that runsmake deploy(it must stay excluded, otherwise the next deploy / hardening pass is denied).
- Close the wildcard bypass: because the exclusion matches a role name across accounts, an attacker with
iam:CreateRolecould mint a role with that name to escape theDeny. Either use exact ARNs (no*account), or add a companion SCP that deniesiam:CreateRole/iam:CreateUserfor the protected names. Prefer paths/permission boundaries you control. - This is
Deny-by-policy: validate in a non-production OU first. Adjustaws-security-survival-kitif you changed theProjectprefix, and<CT_LOG_GROUP_NAME>to matchCTLogGroupName.
SCP 1 — protect the ASSK resources (stacks, EventBridge rules, SNS topics, alarms, metric filters):
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ProtectAsskStacks",
"Effect": "Deny",
"Action": ["cloudformation:DeleteStack", "cloudformation:UpdateTerminationProtection"],
"Resource": "arn:aws:cloudformation:*:<ACCOUNT_ID>:stack/aws-security-survival-kit-*/*",
"Condition": { "ArnNotLike": { "aws:PrincipalARN": [
"arn:aws:iam::*:role/OrgAdminBreakGlass",
"arn:aws:iam::*:role/assk-deployer"
] } }
},
{
"Sid": "ProtectAsskEventBridgeRules",
"Effect": "Deny",
"Action": ["events:DeleteRule", "events:DisableRule", "events:RemoveTargets", "events:PutRule", "events:PutTargets"],
"Resource": "arn:aws:events:*:<ACCOUNT_ID>:rule/aws-security-survival-kit-*",
"Condition": { "ArnNotLike": { "aws:PrincipalARN": [
"arn:aws:iam::*:role/OrgAdminBreakGlass",
"arn:aws:iam::*:role/assk-deployer"
] } }
},
{
"Sid": "ProtectAsskSnsTopics",
"Effect": "Deny",
"Action": ["sns:DeleteTopic", "sns:SetTopicAttributes", "sns:AddPermission", "sns:RemovePermission", "sns:Unsubscribe"],
"Resource": "arn:aws:sns:*:<ACCOUNT_ID>:aws-security-survival-kit-alarm-topic-*",
"Condition": { "ArnNotLike": { "aws:PrincipalARN": [
"arn:aws:iam::*:role/OrgAdminBreakGlass",
"arn:aws:iam::*:role/assk-deployer"
] } }
},
{
"Sid": "ProtectAsskAlarms",
"Effect": "Deny",
"Action": ["cloudwatch:DeleteAlarms", "cloudwatch:DisableAlarmActions", "cloudwatch:SetAlarmState"],
"Resource": "arn:aws:cloudwatch:*:<ACCOUNT_ID>:alarm:*[aws-security-survival-kit]*",
"Condition": { "ArnNotLike": { "aws:PrincipalARN": [
"arn:aws:iam::*:role/OrgAdminBreakGlass",
"arn:aws:iam::*:role/assk-deployer"
] } }
},
{
"Sid": "ProtectAsskMetricFilters",
"Effect": "Deny",
"Action": ["logs:DeleteMetricFilter"],
"Resource": "arn:aws:logs:*:<ACCOUNT_ID>:log-group:<CT_LOG_GROUP_NAME>:*",
"Condition": { "ArnNotLike": { "aws:PrincipalARN": [
"arn:aws:iam::*:role/OrgAdminBreakGlass",
"arn:aws:iam::*:role/assk-deployer"
] } }
}
]
}SCP 2 — lock the secure-by-default settings applied by make deploy (S3 BPA, EBS encryption, AMI / snapshot public-sharing block, SSM document public sharing):
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "LockAccountHardening",
"Effect": "Deny",
"Action": [
"s3:PutAccountPublicAccessBlock",
"ec2:DisableEbsEncryptionByDefault",
"ec2:DisableImageBlockPublicAccess",
"ec2:DisableSnapshotBlockPublicAccess"
],
"Resource": "*",
"Condition": { "ArnNotLike": { "aws:PrincipalARN": [
"arn:aws:iam::*:role/OrgAdminBreakGlass",
"arn:aws:iam::*:role/assk-deployer"
] } }
},
{
"Sid": "LockSsmDocumentSharingSetting",
"Effect": "Deny",
"Action": ["ssm:UpdateServiceSetting", "ssm:ResetServiceSetting"],
"Resource": "arn:aws:ssm:*:<ACCOUNT_ID>:servicesetting/ssm/documents/console/public-sharing-permission",
"Condition": { "ArnNotLike": { "aws:PrincipalARN": [
"arn:aws:iam::*:role/OrgAdminBreakGlass",
"arn:aws:iam::*:role/assk-deployer"
] } }
}
]
}Optional companions (add to SCP 2 if they fit your environment): ec2:ModifyInstanceMetadataDefaults to freeze the IMDSv2 default; kms:ScheduleKeyDeletion / kms:DisableKey / kms:DisableKeyRotation / kms:PutKeyPolicy scoped to the optional SNS CMKs; and cloudtrail:StopLogging / cloudtrail:DeleteTrail / cloudtrail:UpdateTrail + logs:DeleteLogGroup on <CT_LOG_GROUP_NAME> to protect the CloudTrail data source ASSK depends on.
Each SCP must stay under the 5,120-character limit. If you hit it, split the statements across multiple policies, or drop the per-statement
Conditionblocks in favor of a single Organization-wide deny SCP plus a narrowly-scopedAllowfor break-glass.
The alerting topic's resource policy intentionally does not grant sns:Subscribe to in-account principals via Principal: "*". A compromised role with sns:Subscribe only via topic policy could otherwise add an attacker email / HTTPS endpoint and silently exfiltrate every security alert. Subscribing still works for principals that hold sns:Subscribe via IAM (the normal admin flow).
Alerts are delivered by email through the SNS topic, as clean plain-text messages (no JSON, no emoji). Each InputTransformer renders a readable, scannable body:
[ASSK] Security alert: StopLogging
Event: StopLogging
Account: 123456789012
Region: eu-west-1
Time: 2026-06-26T14:32:10Z
Source IP: 203.0.113.10
Principal: AROAEXAMPLE123:session
Identity: my-admin-role
Event ID: a1b2c3d4-5678-90ab-cdef-EXAMPLE
Rules that carry extra context add it between Identity and Event ID (for example KeyId, BucketName, GroupId, StackName). The same plain-text body is what AWS Chatbot renders into Slack / Teams.
Two delivery caveats are inherent to SNS email and apply regardless of formatting: the body is plain text only (no HTML, bold, or links), and the email subject line stays the generic "AWS Notification Message" because EventBridge cannot set the SNS subject. Customizing either would require SES or a Lambda, both of which are deliberately out of scope.
Set up AWS Chatbot to get notified directly on Slack or Microsoft Teams.
ASSK ships two CloudWatch dashboards for at-a-glance visibility on suspicious activity:
AWS-Security-Survival-Kit-Dashboard-<region>(Local stack) — metric-filter alarm graphs (Access Denied, failed console logins, IMDSv1 launches), plus EventBridge rule-invocation graphs grouped by category (defense evasion / detection tampering, data exfiltration, network exposure, identity / reconnaissance), plus CloudWatch Logs Insights tables for fast triage (latest Access Denied events, recent IMDSv1 launches, CloudTrail changes).AWS-Security-Survival-Kit-Dashboard-Global(Global stack,us-east-1) — an all-rules overview plus rule-invocation graphs for root & privileged identity, IAM users & MFA, account & org tampering, and AWS Health.
Every detection shipped by the kit appears on a dashboard. Rule-invocation widgets read the AWS/Events Invocations metric per rule, so a flat line at zero means the detection is armed and nothing matched (the metric only materializes after a rule first fires). Opt-in detections (Enable*Detection) only report data once enabled.
Cost note: AWS includes 3 dashboards in the always-free tier; ASSK uses 2, so dashboards stay free. The Logs Insights table widgets run a query on each load/refresh (billed per GB scanned) — the queries use tight limits to keep this negligible. Adding more dashboards (beyond the 3 free) costs ~$3/dashboard/month.