This is a lab in the Security Specialty hands-on track. The reference posts argue
the decisions; this stands one up. Domain 2 asks you to detect and respond to
security events, and few events deserve a faster response than the account root
signing in to the console. Root can do anything, and nothing in IAM can stop it,
so it should be locked away behind hardware MFA and used almost never. When it is
used, security wants to know within seconds. Here we build the log group, the
metric, the alarm, and the SNS topic, and you wire the part that carries the
decision, since that is where the thinking lives. The full lab is in
lab-scs-03-root-login-alarm.zip.
Before your first lab, do the one-time, once-per-account setup: run the zip’s
preflight.sh to confirm your account is ready, then deploy the lab reaper,
a standing backstop that auto-deletes any lab you forget to tear down after 24
hours. Every lab tags its stack for the reaper on deploy.
The scenario
Someone signs in to the AWS console as the account root. Not an IAM user, not an
assumed role, but the root identity itself. CloudTrail does its job: it records
the ConsoleLogin, and the record lands in a log group with userIdentity.type
set to Root. The evidence is written down within seconds of the login. And then
nothing happens, because nothing is watching the log group for that shape of
event. The login is real, the log line is real, and no one is paged.
That gap is the whole problem. Having the data is not the same as detecting the event. The answer is the CIS AWS Foundations Benchmark’s root-usage monitor, a pairing you will see again and again across its monitoring recommendations: a metric filter that recognises the interesting event in the trail, a custom metric that counts it, and an alarm that fires on the count. For root usage there is no burst threshold to clear, because a single root console login is already worth waking someone. One matching event pages.
What you’re given
CloudFormation builds a log group with short retention standing in for the
CloudTrail log group, a metric filter that should count root sign-ins into a
custom metric (RootAccountUsageCount in the AcmeSecurity namespace), the
RootAccountUsageAlarm that watches that metric with a threshold of one, and the
SNS topic it pages. What is missing is one thing: the filter pattern that decides
which log events count as a root console login. As shipped, the filter carries a
placeholder that is valid syntax (so the stack deploys) but selects on a
userIdentity.type value no CloudTrail record contains, so the metric never
moves and the alarm never fires. Complete, the filter looks like this:
RootUsageMetricFilter:
Type: AWS::Logs::MetricFilter
Properties:
LogGroupName: !Ref TrailLogGroup
FilterPattern: '{ ($.userIdentity.type = "Root") && ($.userIdentity.invokedBy NOT EXISTS) && ($.eventType != "AwsServiceEvent") }'
MetricTransformations:
- MetricNamespace: AcmeSecurity
MetricName: RootAccountUsageCount
MetricValue: '1'
DefaultValue: 0
Deploying the stack as shipped succeeds, but it behaves wrong. The placeholder
pattern means RootAccountUsageCount never leaves zero, so a root login sails
straight past the alarm.
Your task
Close the gap in src/template.yaml by writing the filter pattern. A CloudWatch
Logs metric filter can select on structured JSON, which is exactly what a
CloudTrail record is. The selector is wrapped in { }, addresses fields by path,
and joins its terms with &&. Three terms pin down a root console login. First,
$.userIdentity.type = "Root" says the actor is the account root itself, not an
IAM user or a role. Second, $.userIdentity.invokedBy NOT EXISTS says a human
signed in at the console, rather than an AWS service acting on root’s behalf,
which is the case that adds the invokedBy field. Third,
$.eventType != "AwsServiceEvent" excludes events AWS itself raises. Put together
they are the CIS pattern for root usage.
Leave the metric transformations alone. The DefaultValue: 0 is doing quiet
work, keeping the metric reporting zero through the calm periods that are the
normal state, so the alarm settles to OK instead of stalling in
INSUFFICIENT_DATA waiting for its first data point.
Run it
./scripts/deploy.sh # deploys src/template.yaml
./scripts/test.sh # writes CloudTrail-shaped events and watches the alarm
./scripts/teardown.sh # deletes everything
test.sh never launches compute, which keeps it cheap. It writes CloudTrail-shaped
log events with put-log-events and polls describe-alarms. First it writes an
ordinary IAM-user ConsoleLogin and confirms the alarm ignores it, because an
everyday login is not a page; then it writes a root ConsoleLogin, shaped like a
real CloudTrail record with userIdentity.type of Root, no invokedBy, and an
eventType of AwsConsoleSignIn, and confirms the alarm fires. Metric filters
and alarms take a minute or three to react, so the “still waiting” lines are
normal. With the pattern in place it prints:
t+90s alarm=ALARM
PASS: the ordinary login was ignored and the root console login paged.
If it fails
- The alarm fired on the ordinary IAM-user login. The pattern is matching too
broadly. It should select
$.userIdentity.type = "Root", not anyConsoleLogin. - The alarm never fired, even after the root login. The
FilterPatternis not matching the root sign-in, soRootAccountUsageCountnever moves. Check that the pattern is wrapped in{ }, that each field path starts with$., and thatNOT EXISTSsits on$.userIdentity.invokedBy. - The alarm sits in
INSUFFICIENT_DATA. The metric filter is producing no data. Confirm the metric transformation keeps itsDefaultValue: 0.
Reveal the solution
SRC=solution ./scripts/deploy.sh && ./scripts/test.sh
What you just learned
- CloudWatch Logs metric filters read structured JSON, not just plain text. A
pattern wrapped in
{ }addresses fields by path, compares with=and!=, tests presence withNOT EXISTS, and joins terms with&&. That syntax is what turns a CloudTrail log group into an alarm source. - This is one alarm from the CIS AWS Foundations Benchmark’s monitoring set, which pairs a metric filter with an alarm for events like root usage, console logins without MFA, IAM policy changes, and unauthorised API calls. The shape is always the same: filter the trail, count the metric, alarm on the count.
- Root usage gets a threshold of one. A noisy application metric needs a burst threshold so a single blip does not page, but a root console login is a red flag on its own, because root should be locked behind hardware MFA and used almost never.
- A metric filter with
DefaultValue: 0keeps its metric reporting through quiet periods, so the alarm resolves to OK instead of stalling inINSUFFICIENT_DATAwaiting for a first data point.
Next
The rest of the hands-on lab tracks are listed in the labs README.