<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ilia Dubovskii</title>
    <description>The latest articles on DEV Community by Ilia Dubovskii (@beamreachcloud).</description>
    <link>https://dev.to/beamreachcloud</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4203490%2Fc5396c48-e83c-4b2d-8007-0f1cbdc71140.jpg</url>
      <title>DEV Community: Ilia Dubovskii</title>
      <link>https://dev.to/beamreachcloud</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/beamreachcloud"/>
    <language>en</language>
    <item>
      <title>AI Root Cause Analysis for Incidents: What It Can and Can't Do</title>
      <dc:creator>Ilia Dubovskii</dc:creator>
      <pubDate>Fri, 23 Oct 2026 22:40:02 +0000</pubDate>
      <link>https://dev.to/beamreachcloud/ai-root-cause-analysis-for-incidents-what-it-can-and-cant-do-381l</link>
      <guid>https://dev.to/beamreachcloud/ai-root-cause-analysis-for-incidents-what-it-can-and-cant-do-381l</guid>
      <description>&lt;p&gt;An AI agent that investigates an alert can save the on-call engineer the worst part of the first half hour: opening ten dashboards, scrolling the deploy log and asking in the channel who changed what. It can't be trusted to decide what's true, or to change production on a hunch. This guide covers where the line falls in practice: what an AI investigation should collect in the first minutes, how it should rank causes, why it should propose rather than act, how to confirm a fix actually worked, how to get post-mortems out of it that people trust, and how to measure the result without fooling yourself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The short version&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AI is good at the &lt;em&gt;investigation&lt;/em&gt;: gathering evidence from many sources at once and correlating it with recent changes. It is weak at &lt;em&gt;judgement&lt;/em&gt;, novel failures and incidents with more than one cause.&lt;/li&gt;
&lt;li&gt;Every hypothesis should come with a confidence, the evidence behind it, and a list of what was checked and ruled out.&lt;/li&gt;
&lt;li&gt;Give the agent read-only credentials. It proposes; a human approves. Only actions that are verifiable, reversible and contained should ever run on their own, one at a time.&lt;/li&gt;
&lt;li&gt;An incident is over when the metric that triggered it is back to normal, not when someone ran a command. The agent should watch that metric and say if the fix didn't work.&lt;/li&gt;
&lt;li&gt;Build post-mortem drafts from timestamped evidence, mark what's inferred, keep them blameless, and have a human merge them.&lt;/li&gt;
&lt;li&gt;Mean time to restore (MTTR) is a noisy number. Look at distributions and per-incident detail, not a single average.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  1. What AI root cause analysis can and can't do
&lt;/h2&gt;

&lt;p&gt;Incident response has two kinds of work in it. The first is &lt;strong&gt;investigation&lt;/strong&gt;: find out what's affected, what changed, which signals moved and when. It's mechanical, it's parallel, and it's where the first twenty minutes usually go. The second is &lt;strong&gt;judgement&lt;/strong&gt;: decide what's actually going on, whether to roll back, who to wake up, what to tell customers. An AI agent with access to your telemetry can do most of the first and should do very little of the second.&lt;/p&gt;

&lt;p&gt;What it does well:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Breadth and speed.&lt;/strong&gt; It can query metrics, logs, traces, the deploy history and the feature flag audit log at once, where a person opens them one tab at a time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Correlation with change.&lt;/strong&gt; Most incidents follow a change: a deploy, a flag, a config edit, a migration, a scaling event. Lining those up against the moment a metric moved is exactly the kind of tedious cross-referencing a model does reliably.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recognising known patterns.&lt;/strong&gt; Connection pool exhaustion, a missing index, an expired certificate, a dependency that's down. If it has happened before, in your systems or in general, the agent will often name it quickly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Writing it down.&lt;/strong&gt; A clear summary in the channel, and later a first draft of the timeline, saves real time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Where it struggles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Novel failures.&lt;/strong&gt; Without a recognisable pattern or an obvious recent change, a model tends to produce a plausible story rather than say "I don't know".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anchoring.&lt;/strong&gt; Models latch onto the first hypothesis that fits and stop looking, which is a real risk for incidents with more than one contributing cause.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing context.&lt;/strong&gt; It doesn't know that the marketing campaign started at 9:00, or that the payment provider emailed a maintenance notice, unless that information is somewhere it can read.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trade-offs.&lt;/strong&gt; Whether to roll back a release that also contains a security fix is a business decision, not a pattern match.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This isn't only a cautious vendor's opinion. In August 2025, ClickHouse &lt;a href="https://clickhouse.com/blog/llm-observability-challenge" rel="noopener noreferrer"&gt;tested four frontier models&lt;/a&gt; on anomalies injected into the OpenTelemetry demo application, with access to the observability data through an MCP server. Their conclusion was blunt: "Autonomous RCA is not there yet." Models improve quickly, so treat any single benchmark as a snapshot. The design conclusion still holds: let the agent find and organise the evidence, and let a human decide.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. What to collect in the first minutes
&lt;/h2&gt;

&lt;p&gt;The quality of an AI investigation is set by what it can see. Before you worry about the model, make sure the agent can reach these, with read-only access, and that it collects them for every alert:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The alert and the real start time.&lt;/strong&gt; The alert fires when a threshold is crossed, which is often several minutes after things started going wrong. The agent should find the onset in the metric itself and use that, minus a margin, as the start of every other search window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What the service depends on, and what depends on it.&lt;/strong&gt; Take this from APM traces and your service catalog, not a hand-drawn diagram. It tells the agent which databases, queues and downstream services to check, and who else is affected.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Changes before the onset.&lt;/strong&gt; Deploys, feature flag changes (with who and what percentage), config and secret changes, infrastructure-as-code applies, database migrations, scheduled jobs, autoscaling events. This list finds the cause more often than any anomaly detector.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dependency health.&lt;/strong&gt; Database CPU, connections and slow queries; queue depth; cache hit rate; error rates and latency of the services called; third-party status pages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;New log signatures.&lt;/strong&gt; Not "all errors", but error messages that are new or sharply more frequent than in the baseline window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Traces of slow or failing requests.&lt;/strong&gt; Where the time actually goes. This often separates the cause from the symptom.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Similar past incidents.&lt;/strong&gt; Previous incidents for the same service or with the same signature, and how they were resolved.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Owners and on-call.&lt;/strong&gt; Who owns each service involved, so the agent can suggest who to page.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The output of this step isn't an answer. It's a set of facts, each with a timestamp and a link back to its source, which is what makes the next two steps checkable.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Ranking causes: confidence, evidence and "ruled out"
&lt;/h2&gt;

&lt;p&gt;An investigation that returns one confident sentence is dangerous, because a wrong answer looks exactly like a right one. A useful first evaluation has a structure that lets the on-call engineer check it in a minute:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ranked hypotheses&lt;/strong&gt;, each with a plain confidence level (high, medium, low) rather than a falsely precise percentage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evidence for each one&lt;/strong&gt;, as specific facts with links: "flag rolled from 10% to 100% at 09:41", "database CPU 31% to 97% at 09:43", "93% of slow spans wait on this call". If a hypothesis has no evidence the engineer can click, it shouldn't be ranked first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What was ruled out, and why.&lt;/strong&gt; "No deploys in six hours. Payment provider latency normal. Network metrics at baseline." This is the most underrated part. It stops the team spending fifteen minutes on the reflexive "was it the deploy?", and it shows what the agent actually checked, so a gap is visible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cause versus symptom.&lt;/strong&gt; An exhausted connection pool is usually a symptom of slow queries, not a cause. The agent should say which it thinks each item is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What would change its mind.&lt;/strong&gt; For example: "if CPU doesn't fall within five minutes of turning the flag off, this hypothesis is wrong".&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Treat correlation with suspicion, including when the agent finds it. A change that happened shortly before the onset is a strong lead, not proof. The proof comes later, when you undo the change and the metric recovers.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Read-only by default: propose, don't act
&lt;/h2&gt;

&lt;p&gt;The asymmetry decides this. If the agent's hypothesis is wrong, you lose a few minutes reading it. If the agent acts on a wrong hypothesis, it can turn one incident into two: a rollback that removes a fix, a restart that drops in-flight work, a scale-up that hides the real problem and runs up the bill. The vendors in this space mostly agree. Rootly says every change its AI SRE proposes "requires explicit human sign-off before execution". incident.io says the only change its Investigations product can make "is a pull request you review and merge yourself". PagerDuty describes its SRE Agent as performing "approved remediation".&lt;/p&gt;

&lt;p&gt;In practice that means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Read-only credentials&lt;/strong&gt;, scoped to observability data, the deploy history and the flag audit log. Not a production admin role "for convenience".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remediation as a proposal&lt;/strong&gt;: a specific action, its expected effect, how to undo it, and a button. A separate, narrowly permissioned path carries out the approved action, and logs who approved it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code changes as pull requests&lt;/strong&gt;, which go through the same review as any other change.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's a narrow exception. Some actions are safe enough to run without waiting, if they pass three tests: the outcome is &lt;strong&gt;verifiable&lt;/strong&gt; (you can tell from a metric whether it worked), the action is &lt;strong&gt;reversible&lt;/strong&gt; (one step puts things back), and the blast radius is &lt;strong&gt;contained&lt;/strong&gt; (it touches one component). Turning off a feature flag that was changed ten minutes ago can pass. Failing over a database doesn't. Even then, grant autonomy one action at a time, after the team has watched the agent propose that exact action correctly many times. Our &lt;a href="https://beamreach.ai/docs/autonomous-devops.html" rel="noopener noreferrer"&gt;autonomous DevOps framework&lt;/a&gt; covers this test and the gate that enforces it in more detail.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Confirming recovery on the metric that fired
&lt;/h2&gt;

&lt;p&gt;"Flag turned off" isn't the end of an incident. The end is when the metric that started it is back to normal and stays there. This step is where many incident processes, human or automated, go soft: the action is taken, the channel goes quiet, and nobody checks whether the error rate really came down or just moved somewhere else.&lt;/p&gt;

&lt;p&gt;An AI investigation is well placed to close this loop, because it already knows which metric fired and what normal looks like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Define "restored" before acting.&lt;/strong&gt; For example: the error rate below the alert threshold, and close to its baseline, for ten consecutive minutes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch the cause signals as well as the symptom.&lt;/strong&gt; If the hypothesis was a slow query, database CPU should fall first, then latency, then errors. If errors drop but CPU doesn't, something else is going on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set a deadline and say so.&lt;/strong&gt; "If checkout isn't back within ten minutes, I'll report that and reopen the other hypotheses." A fix that didn't work is evidence too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Record the times.&lt;/strong&gt; Onset, alert, first evaluation, action, recovery. These become the post-mortem timeline and the honest version of your MTTR.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is also what makes the agent improve over time. Each incident ends with a check of whether its top hypothesis was right, which is the number to watch when you decide how much to trust it.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Post-mortems you can trust
&lt;/h2&gt;

&lt;p&gt;AI-drafted post-mortems are now a standard feature: incident.io, Rootly, FireHydrant and PagerDuty all offer some form of them. The draft is the easy part. Making it trustworthy takes a few rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Build the timeline from evidence, not from chat.&lt;/strong&gt; Timestamps should come from the metric, the deploy log and the flag audit trail. Chat messages are useful for what people decided and why, but they record when someone noticed something, not when it happened.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separate what was observed from what was inferred.&lt;/strong&gt; "CPU reached 97% at 09:43" is a fact. "The flag caused the slow query" is a conclusion, confirmed or not by what happened when the flag was turned off. Write it that way.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep it blameless.&lt;/strong&gt; Google's SRE book puts it plainly: a blameless postmortem must "focus on identifying the contributing causes of the incident without indicting any individual or team". An AI draft can quietly break this by writing "Maria rolled the flag to 100%" under root cause. The change belongs in the timeline. The cause is in the system: a query without an index, and no check of database impact before a flag reaches all traffic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Action items are suggestions.&lt;/strong&gt; Mark them as "for the team to confirm", with suggested owners. Only people should commit people to work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review it like code.&lt;/strong&gt; Opening the draft as a pull request in the repository where your post-mortems live gives you review, history and an approver by default. incident.io's own docs call its AI draft "a starting point, not a finished document", which is the right framing whatever tool writes it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The quiet benefit is coverage. Small incidents rarely get a write-up because nobody has the time. When the draft costs nothing, more of them get reviewed, and the patterns in small incidents are often what prevents the big ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Measuring MTTR honestly
&lt;/h2&gt;

&lt;p&gt;Mean time to restore is the obvious metric for an incident agent, and it's the metric we build around, because it's what the business feels. It's also easy to misuse. Two credible sources are worth reading before you put it on a slide:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Google's report &lt;a href="https://sre.google/resources/practices-and-processes/incident-metrics-in-sre/" rel="noopener noreferrer"&gt;&lt;em&gt;Incident Metrics in SRE&lt;/em&gt;&lt;/a&gt; (Štěpán Davidovič, 2021) uses simulation to show that statistics like MTTR are "poorly suited for decision making or trend analysis in the context of production incidents".&lt;/li&gt;
&lt;li&gt;Courtney Nash's work on the VOID, an open database of public incident reports, argues &lt;a href="https://www.usenix.org/conference/srecon22americas/presentation/nash" rel="noopener noreferrer"&gt;"how unreliable MTTR can be"&lt;/a&gt; (SREcon22 Americas). Incident durations are heavily skewed, so a mean says little about a typical incident.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That doesn't mean you can't measure. It means measuring carefully:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use the time to restore for each incident&lt;/strong&gt;, and look at the distribution (median and a high percentile), not a monthly mean.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Define the start as the onset in the metric&lt;/strong&gt;, not the alert, and the end as the confirmed recovery from section 5. Otherwise improving the alert threshold looks like improving response.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure the parts the agent controls&lt;/strong&gt;: time from alert to first evaluation, and how often its top-ranked cause was right. Those are directly attributable. A change in overall MTTR over a few months isn't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compare like with like.&lt;/strong&gt; A quarter with one long outage will swamp everything else. Read the individual incidents.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't let the number drive behaviour.&lt;/strong&gt; If people start closing incidents early to keep MTTR down, the metric has stopped measuring anything.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  8. Buy or build?
&lt;/h2&gt;

&lt;p&gt;For most teams, buying is the right first move. These are the products we'd look at first, described from their own sites (checked October 2026):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Datadog Bits AI SRE&lt;/strong&gt; (called Bits Investigation in Datadog's current docs) investigates alerts as they fire, forms hypotheses from Datadog telemetry and suggests code fixes. The natural choice if Datadog already holds most of your data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;incident.io&lt;/strong&gt; has an AI SRE that investigates when an incident is declared, plus AI post-mortem drafts. Its only change to your systems is a pull request you merge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rootly&lt;/strong&gt; AI SRE ranks evidence-backed hypotheses with confidence scores and suggests fixes, with human sign-off on every change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PagerDuty&lt;/strong&gt; offers an SRE Agent (triage, diagnosis, approved remediation) and a Scribe Agent that captures incident conversations for post-incident reviews.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FireHydrant&lt;/strong&gt; offers AI incident summaries and AI-assisted retrospectives. Freshworks has announced it is acquiring FireHydrant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resolve.ai&lt;/strong&gt; positions itself as an AI SRE that goes on call and mitigates incidents, closer to the autonomous end of the range.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A custom system makes sense in narrower cases: your evidence lives across tools that no single vendor reads well; your runbooks and approval rules are specific and need to be enforced exactly; telemetry can't leave your environment, so the agent must run in your own cloud; or you want the full loop described here (investigate, propose, approve, verify on the metric, draft the post-mortem) tuned to one metric you care about.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How accurate is AI root cause analysis?
&lt;/h3&gt;

&lt;p&gt;It depends heavily on your data and the kind of incident. For failures that follow a recent change or match a known pattern, an agent with good access to telemetry often names the likely cause quickly. For novel or multi-cause incidents it is much weaker: in ClickHouse's August 2025 test of four frontier models, the conclusion was that autonomous root cause analysis is not there yet. Measure it yourself by recording, for every incident, whether the agent's top-ranked cause turned out to be right.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should an AI incident agent have write access to production?
&lt;/h3&gt;

&lt;p&gt;No, not by default. Give it read-only credentials to observability data, deploy history and the feature flag audit log, and have it propose remediation that a human approves. The only exceptions worth considering are actions that are verifiable, reversible and contained, such as turning off a feature flag that was just changed, granted one action at a time after the team has seen the agent propose it correctly many times.&lt;/p&gt;

&lt;h3&gt;
  
  
  What data does an AI incident investigation need?
&lt;/h3&gt;

&lt;p&gt;The alert and the metric behind it, service dependencies from APM traces or a service catalog, every change before the onset (deploys, feature flags, config, infrastructure applies, migrations), dependency health, new log signatures, traces of failing requests, similar past incidents, and service owners. Missing any of these creates blind spots, and the agent should say which sources it could not check.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can AI write the post-mortem?
&lt;/h3&gt;

&lt;p&gt;It can write a good first draft, and most incident management tools now offer this. Build the timeline from timestamped evidence rather than chat, separate what was observed from what was inferred, keep the draft blameless, and present action items as suggestions. A human should review and merge it, for example as a pull request.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is MTTR a good metric for AI incident response?
&lt;/h3&gt;

&lt;p&gt;It is the metric the business feels, but a mean is misleading because incident durations are heavily skewed. Google's report Incident Metrics in SRE and Courtney Nash's work on the VOID both show how unreliable MTTR can be for trend analysis. Track time to restore per incident, look at the median and a high percentile, and also measure what the agent directly controls: time to first evaluation and how often its top cause was right.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should we buy an AI SRE tool or build our own?
&lt;/h3&gt;

&lt;p&gt;Buy first if your telemetry mostly lives in one platform and a vendor's workflow fits your on-call process: Datadog, incident.io, Rootly, PagerDuty, FireHydrant and Resolve.ai all offer AI investigation or post-mortem features. Build, or have one built, when your evidence is spread across tools no single vendor reads well, when telemetry can't leave your environment, or when you need your own runbooks and approval rules enforced exactly.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written by Ilia Dubovskii, founder of Beamreach. Vendor descriptions were checked against each vendor's own site on 10 October 2026. Products in this space change monthly, so if something here no longer matches, the vendor's site wins. Tell us and we'll fix this page.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Related: &lt;a href="https://beamreach.ai/docs/autonomous-devops.html" rel="noopener noreferrer"&gt;Autonomous DevOps&lt;/a&gt; · &lt;a href="https://beamreach.ai/docs/cloud.html" rel="noopener noreferrer"&gt;All guides for cloud teams&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://beamreach.ai/docs/ai-root-cause-analysis-incidents.html" rel="noopener noreferrer"&gt;https://beamreach.ai/docs/ai-root-cause-analysis-incidents.html&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The verifiable, reversible, contained test from section 4 is the core of my talk &lt;em&gt;Autonomous DevOps: Engineering Systems That Don't Need Our Attention&lt;/em&gt; at KCD Porto x DevOpsDays Portugal (19–20 November 2026). Talk page: &lt;a href="https://beamreach.ai/talks/autonomous-devops.html" rel="noopener noreferrer"&gt;https://beamreach.ai/talks/autonomous-devops.html&lt;/a&gt;&lt;/p&gt;

</description>
      <category>sre</category>
      <category>ai</category>
      <category>devops</category>
      <category>observability</category>
    </item>
    <item>
      <title>Autonomous DevOps: A Framework for Deciding What AI Agents May Do Alone</title>
      <dc:creator>Ilia Dubovskii</dc:creator>
      <pubDate>Fri, 23 Oct 2026 22:39:40 +0000</pubDate>
      <link>https://dev.to/beamreachcloud/autonomous-devops-a-framework-for-deciding-what-ai-agents-may-do-alone-daf</link>
      <guid>https://dev.to/beamreachcloud/autonomous-devops-a-framework-for-deciding-what-ai-agents-may-do-alone-daf</guid>
      <description>&lt;p&gt;&lt;strong&gt;Autonomous DevOps&lt;/strong&gt; is an operating model in which agents continuously observe cloud and pipeline telemetry, decide what needs changing, and act inside declared boundaries without a human approving each instance, while the system stays observable, auditable and reversible. Humans set the boundary, not the instance.&lt;/p&gt;

&lt;p&gt;The short version of this guide: autonomy is not a property of the agent. It is a property of the operation. An operation can run without human attention when it is &lt;strong&gt;verifiable&lt;/strong&gt;, &lt;strong&gt;reversible&lt;/strong&gt; and &lt;strong&gt;contained&lt;/strong&gt;, and those three things can be scored from facts about your infrastructure rather than guessed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is autonomous DevOps?
&lt;/h2&gt;

&lt;p&gt;For two decades DevOps has been about automation: CI/CD pipelines, Infrastructure as Code, observability platforms, deployment systems. All of it moved &lt;em&gt;execution&lt;/em&gt; off the human. None of it moved &lt;em&gt;attention&lt;/em&gt; off the human. Someone still reviews the dashboards, investigates the alerts, decides whether an infrastructure change is safe, maintains the honeypots and keeps the Terraform aligned with reality. That after-deploy work is what we call &lt;a href="https://beamreach.ai/blog/continuous-maintenance.html" rel="noopener noreferrer"&gt;Continuous Maintenance&lt;/a&gt;: the stage CI/CD never automated.&lt;/p&gt;

&lt;p&gt;The distinction that matters is between automation and autonomy:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Automation&lt;/th&gt;
&lt;th&gt;Autonomy&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Who decides &lt;em&gt;that&lt;/em&gt; it happens&lt;/td&gt;
&lt;td&gt;A human&lt;/td&gt;
&lt;td&gt;The system, inside declared boundaries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Who decides &lt;em&gt;when&lt;/em&gt;
&lt;/td&gt;
&lt;td&gt;A human, or a schedule a human set&lt;/td&gt;
&lt;td&gt;The system, from what it observes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Human attention needed&lt;/td&gt;
&lt;td&gt;To trigger it and to check it&lt;/td&gt;
&lt;td&gt;To set and revise the boundary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What a human approves&lt;/td&gt;
&lt;td&gt;Each instance&lt;/td&gt;
&lt;td&gt;A class of operations&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So &lt;strong&gt;autonomous DevOps&lt;/strong&gt; is not "an AI agent with production credentials". It is a way of engineering operational systems so that routine decisions no longer need our limited focus, while humans remain responsible for strategy, architecture and exceptions. Autonomy does not mean giving AI unrestricted control. It means identifying the operations that can be trusted to run alone, and building the machinery that enforces that decision.&lt;/p&gt;

&lt;p&gt;Large language models matter here for one reason: they can assemble a fix for situations nobody wrote a rule for. That widens what an agent &lt;em&gt;can&lt;/em&gt; do far beyond a scripted runbook. It also means the set of possible actions can no longer be listed in advance, so it can't be made safe by narrowing it to a few typed commands. It has to be made safe by deciding, case by case, whether each operation qualifies. That decision is the subject of this guide.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attention is the scarce resource
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Opinion.&lt;/strong&gt; I think attention, not compute and not headcount, is now the binding constraint on running cloud-native systems. Here's the argument:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Automation scaled execution. Execution is now close to free.&lt;/li&gt;
&lt;li&gt;Every automation we added created something new that needs watching: a pipeline, a dashboard, an alert rule, a Terraform state, a posture report.&lt;/li&gt;
&lt;li&gt;So the number of decisions per engineer per week went up, not down. I call that &lt;strong&gt;attention debt&lt;/strong&gt;: the accumulated volume of decisions a system needs from humans per unit of time. Every new alert rule and dashboard adds to it, and it compounds quietly.&lt;/li&gt;
&lt;li&gt;Attention doesn't scale the way compute does. It's serial, it breaks under interruption, and it degrades with volume. Alert fatigue is what attention bankruptcy looks like.&lt;/li&gt;
&lt;li&gt;So the next gain isn't "execute more". It's "decide less".&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Two things from my own estate made this concrete for me. My IaC coverage was roughly half, which meant half the infrastructure was invisible to the tool that was supposed to manage it, and nobody was assigned to notice. And the posture scanners I run (Prowler, Trivy, Gitleaks, Checkov) produce findings faster than any person can triage them. The scan was never the bottleneck. Reading the output was.&lt;/p&gt;

&lt;p&gt;AI coding tools make this worse before they make it better. They generate more infrastructure change, faster, and review capacity stays flat. In my view AI is both a cause of the review backlog and the only realistic response to it, which is exactly why the boundary has to be engineered rather than assumed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Autonomy is a property of the operation: verifiable, reversible, contained
&lt;/h2&gt;

&lt;p&gt;Most discussion of AI agents in DevOps asks whether the agent is good enough. That's the wrong unit. The same agent can be perfectly safe planting a decoy credential and dangerous resizing a production database. What changes isn't the agent. It's the operation.&lt;/p&gt;

&lt;p&gt;An operation can run without human attention when it has three properties:&lt;/p&gt;

&lt;h3&gt;
  
  
  Verifiable
&lt;/h3&gt;

&lt;p&gt;The system can prove, mechanically and without a human reading it, that the operation achieved its intent. Not "did it run" but "did it do the right thing". Example: after an import, &lt;code&gt;terraform plan&lt;/code&gt; shows no changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reversible
&lt;/h3&gt;

&lt;p&gt;There is a known, cheap, bounded path back to the prior state. Example: reverting a PR that added Terraform &lt;code&gt;import&lt;/code&gt; blocks changes state, not infrastructure. Deleting a database has no such path.&lt;/p&gt;

&lt;h3&gt;
  
  
  Contained
&lt;/h3&gt;

&lt;p&gt;The maximum damage if the operation is wrong is known in advance and small. This is blast radius. Example: a decoy credential has no dependents; nothing in production reads it.&lt;/p&gt;

&lt;p&gt;These aren't a slogan. Each one can be measured from facts you already have: how many resources an operation touches and of what types, whether they're tagged production, whether the change class is additive or destructive, what the live metrics say about load right now, whether compliance tags are attached. Once you score operations instead of agents, a different set of questions becomes answerable:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;Verifiable&lt;/th&gt;
&lt;th&gt;Reversible&lt;/th&gt;
&lt;th&gt;Contained&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Plant or remove a canary credential via Terraform PR&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Import an unmanaged resource into Terraform&lt;/td&gt;
&lt;td&gt;High (plan is a verifier)&lt;/td&gt;
&lt;td&gt;Very high&lt;/td&gt;
&lt;td&gt;High (no runtime effect)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Route a low-risk finding to an auto-queue&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Downsize a production instance&lt;/td&gt;
&lt;td&gt;Medium (savings are estimates)&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Low to medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delete a resource with data on it&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This also reframes the useful engineering work. It isn't "build a smarter agent". It's building the machinery that decides which operations qualify, and making that decision auditable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The autonomy gate and the dial
&lt;/h2&gt;

&lt;p&gt;In a real system the framework isn't a slide. It's a scoring function and a threshold. Here is how it works in Beamreach Compass, described in prose.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Score the operation.&lt;/strong&gt; Every proposed operation gets a risk score built from five weighted components: &lt;em&gt;blast radius&lt;/em&gt;, &lt;em&gt;data loss&lt;/em&gt;, &lt;em&gt;reversibility&lt;/em&gt;, &lt;em&gt;operational load&lt;/em&gt; and &lt;em&gt;compliance&lt;/em&gt;. Each component is computed from facts about the infrastructure (resource counts and types, environment tags, the change class, live CloudWatch metrics, compliance tags), not from the model's opinion of itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Score the urgency&lt;/strong&gt; separately, because "risky" and "time-sensitive" are different questions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Apply the gate.&lt;/strong&gt; High-risk operations always require human approval, and so do moderately risky ones when they're also urgent. An operation may run unattended only if autopilot is enabled, it didn't trip the approval rule, and both its risk and urgency are at or below the ceilings the team configured.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Turn the dial.&lt;/strong&gt; The team expresses how much autonomy it grants with one number: the maximum risk level allowed to run unattended. Everything else is derived. Unattended execution can be switched off entirely. When it's on, the default ceiling only admits the lowest-risk operations.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Raising the dial is an explicit configuration change made by a person, saved in the workspace config. It's not something the agent negotiates or adjusts for itself. Lowering it takes effect the moment trust is lost.&lt;/p&gt;

&lt;p&gt;The useful mental model: &lt;strong&gt;the framework is a threshold you turn up as trust is earned, and turn down the moment it isn't.&lt;/strong&gt; That's what separates delegated autonomy from an agent that's just running unattended.&lt;/p&gt;

&lt;h2&gt;
  
  
  Containing the model itself
&lt;/h2&gt;

&lt;p&gt;The gate decides &lt;em&gt;whether&lt;/em&gt; an operation may proceed unattended. A second, independent layer decides &lt;em&gt;what the model can physically do&lt;/em&gt;, and that layer doesn't depend on the model behaving. In Compass the LLM runtime works under a restricted execution contract:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Boundary&lt;/th&gt;
&lt;th&gt;Mechanism&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Filesystem&lt;/td&gt;
&lt;td&gt;Working directory pinned to a checked-out repository; no host access&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tools&lt;/td&gt;
&lt;td&gt;Explicit allow-list of file and shell tools, plus optional scoped lookups&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud&lt;/td&gt;
&lt;td&gt;Read-only lookups. There is no write path to live infrastructure.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Termination&lt;/td&gt;
&lt;td&gt;Hard turn limit, bounded repair attempts, and a loop limit with a defined fallback. Loops end by construction.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output&lt;/td&gt;
&lt;td&gt;A git diff, delivered as a pull request. There is no other way out.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Intent&lt;/td&gt;
&lt;td&gt;System prompt: work only inside the repository, don't perform live infrastructure mutations, stop and explain if unsure&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The last row is the weakest guardrail and the first five are the real ones. A prompt is a preference. A missing IAM permission is a boundary. Never rely on the model to respect a rule you could have enforced structurally.&lt;/p&gt;

&lt;h3&gt;
  
  
  Security challenges of autonomous DevOps agents
&lt;/h3&gt;

&lt;p&gt;This is where most of the security risk lives, and it's worth listing plainly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Inherited credentials.&lt;/strong&gt; An agent running on an engineer's session has the engineer's permissions. That's a loophole, not an operating model. Give agents their own narrow identity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prompt injection through data.&lt;/strong&gt; Agents read findings, logs, tickets and repository content, and any of it can carry instructions. Containment matters more than filtering: an injected instruction that can only produce a diff in a PR is a review problem, not an incident.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unbounded loops and spend.&lt;/strong&gt; Agents don't get tired, so a stuck task can burn tokens indefinitely. Turn and loop limits are guardrails, not tuning knobs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Silent failure.&lt;/strong&gt; Some operations fail by omission: a suppressed alert that should have fired. These need the most careful scoring (see the noise example below).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prompt-only rules.&lt;/strong&gt; Any boundary that exists only in a system prompt should be treated as absent.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The design goal is to make the failure mode boring. If the model does something nobody anticipated, the worst case should be a bad diff in a PR that nobody merges. For the organisational side of this (trust, control, cost) see &lt;a href="https://beamreach.ai/docs/obstacles-for-autonomous-software.html" rel="noopener noreferrer"&gt;obstacles for autonomous software&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trust Engineering, and how teams actually adopt autonomy
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Trust Engineering&lt;/strong&gt; is the discipline of making an autonomous system's boundaries explicit, measurable and inspectable, so a human can extend or withdraw autonomy as a deliberate configuration change rather than an act of faith. The scoring function, the gate, the dial and the containment layer are all Trust Engineering. So is being able to answer, after the fact, why a given operation was allowed to run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Opinion.&lt;/strong&gt; My bet is that most teams will adopt autonomy &lt;strong&gt;per operation class&lt;/strong&gt;, not per team and not per tool. Nobody will "turn on the AI". They'll let canary placement run alone first, then Terraform imports, then low-risk finding triage, and keep rightsizing and deletions behind approval for a long time. The adoption curve is a risk threshold being raised one notch at a time, each notch justified by a track record on the notch below.&lt;/p&gt;

&lt;p&gt;In practice that looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Observe.&lt;/strong&gt; Let the agent find, score and propose. Every change goes through a PR a human merges. You're calibrating your trust in the scores, not the agent's prose.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick one operation class&lt;/strong&gt; that scores high on all three properties and enable unattended runs for it alone, at the lowest ceiling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch the outcomes, not the volume.&lt;/strong&gt; Did anything need reverting? Did anything get wrongly suppressed?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Raise the ceiling one step,&lt;/strong&gt; or don't. Lower it immediately when something surprises you.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Opinion.&lt;/strong&gt; Two more hypotheses I hold and want challenged. First, autonomy's real value is that it makes expertise &lt;em&gt;modular, distributable and actionable&lt;/em&gt;: a version of "good" for a given operation that you can subscribe to and inspect, instead of rebuilding it in every team. Second, the moat isn't the agent. Agents are becoming a commodity. The assembled context is not: a queryable map of the estate where every fact carries provenance, confidence and freshness. Without that map, none of the scores above could be computed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Opinion.&lt;/strong&gt; I also think self-hosting multiplies trust rather than being a feature: when the runtime is inside your boundary, "what is it doing with my data" stops being part of the conversation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four worked examples
&lt;/h2&gt;

&lt;p&gt;Each example follows the same pattern: what the operation is, how it scores on the three properties, whether it earns autonomy, and what actually exists. Using the same shape every time is the point. It's what makes this a framework rather than a set of demos.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Honeypot and canary management
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Operation:&lt;/strong&gt; decide from the infrastructure map where a canary belongs, plant it, and later decommission it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scores:&lt;/strong&gt; verifiable is high (the asset exists in state or it doesn't; an alert fires or it doesn't). Reversible is high (decommissioning is a PR that deletes the resource). Contained is high (a fake secret has no dependents).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verdict:&lt;/strong&gt; the best autonomy candidate in the system, and the one people find most surprising, because "security" and "unattended" sound contradictory. You shouldn't trust the agent with security assets. You can trust that this particular operation is contained.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What exists:&lt;/strong&gt; map-driven placement ranking, Terraform generation, plant and decommission PRs, and alert ingestion authenticated through IAM, so there's no shared secret to leak. Scheduled rotation of canaries doesn't exist yet. More on &lt;a href="https://beamreach.ai/honeypot-security.html" rel="noopener noreferrer"&gt;cloud honeypots and canary credentials&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. IaC coverage and drift
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Operation:&lt;/strong&gt; discover resources that exist in AWS but not in Terraform, generate &lt;code&gt;import&lt;/code&gt; blocks and HCL, and open a PR.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scores:&lt;/strong&gt; verifiable is high, because &lt;code&gt;terraform plan&lt;/code&gt; is a mechanical verifier. Reversible is very high, because an import changes state, not infrastructure. Contained is high: no runtime effect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verdict:&lt;/strong&gt; earns autonomy for discovery and proposal. Merging stays human, because the diff encodes a decision about who owns the resource and where it lives in the codebase.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What exists:&lt;/strong&gt; discovery, coverage and drift calculation, HCL generation and import PRs, implemented end to end. More on &lt;a href="https://beamreach.ai/iac-coverage.html" rel="noopener noreferrer"&gt;IaC coverage and Terraform drift detection&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Infrastructure rightsizing (the one that fails)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Operation:&lt;/strong&gt; find waste, attribute it to a Terraform module and owner, propose the change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scores:&lt;/strong&gt; verifiable is medium (savings are estimated, not proven, until the bill arrives). Reversible is medium (you can scale back up, but not instantly and not without impact). Contained is low to medium (it touches live, load-bearing resources).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verdict:&lt;/strong&gt; rightsizing should &lt;em&gt;not&lt;/em&gt; be autonomous, and saying so is what makes the framework credible. The score rules it out. That's the framework working, not failing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What exists:&lt;/strong&gt; FinOps findings attributed to a Terraform module and owner, proposed as PRs. The reversibility component scores downsizes as harder to undo than additive changes, which pushes them toward human approval. More on &lt;a href="https://beamreach.ai/finops.html" rel="noopener noreferrer"&gt;AWS cost optimization with Compass&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Observability noise suppression
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Operation:&lt;/strong&gt; score incoming findings for risk and urgency, auto-queue the trivial ones, escalate the rest.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scores:&lt;/strong&gt; reversible is high (suppression is a routing decision, not a mutation). Contained is high (no infrastructure is touched). Verifiable is only medium: you find out you suppressed something wrongly by missing it, which is a poor feedback loop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verdict:&lt;/strong&gt; earns autonomy, with the caveat that its failure mode is silent. This is the example to be most careful with.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What exists:&lt;/strong&gt; the triage scoring and auto-queue path described in section 4.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's still hard: verifiability
&lt;/h2&gt;

&lt;p&gt;The framework says verifiable, reversible and contained. The scoring function in Compass today measures containment and reversibility. &lt;strong&gt;It doesn't score verifiability at all.&lt;/strong&gt; I'd rather say that here than have someone find it in the code.&lt;/p&gt;

&lt;p&gt;Today, verification is after the fact and human: someone reviews the PR. For an operation to be genuinely unattended, the system needs a mechanical answer to "did this achieve its intent?" That could be a re-scan confirming the finding is gone, a plan diff that matches the proposal, or a canary that fires on demand. The IaC example shows it's solvable for some operation classes, because &lt;code&gt;terraform plan&lt;/code&gt; already is that verifier. For others, like rightsizing and alert suppression, it's an open problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Opinion.&lt;/strong&gt; I think verifiability will turn out to be harder than reversibility or containment, and it's where the field is weakest right now, mine included. Everyone builds governance first because buyers ask for approval gates. Almost nobody asks for verification.&lt;/p&gt;

&lt;p&gt;Other open problems I don't have good answers to yet:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Are the weights right?&lt;/strong&gt; I don't know. They're calibrated on my own estate and they're configuration, not doctrine. They need testing against other people's environments.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auditability of the gate itself.&lt;/strong&gt; The action is a reviewable PR, but the reasoning behind each gate decision (the risk breakdown and thresholds at that moment) should be as inspectable as the action. That's work in progress.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost.&lt;/strong&gt; Turn and loop limits bound token spend, but budget-aware planning isn't solved.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is autonomous DevOps?
&lt;/h3&gt;

&lt;p&gt;Autonomous DevOps is an operating model in which agents continuously observe cloud and pipeline telemetry, decide what needs changing, and act inside declared boundaries without a human approving each instance, while staying observable, auditable and reversible. Humans set the boundary, not the instance. In practice it means deciding, per operation, whether that operation is verifiable, reversible and contained enough to run without human attention.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's the difference between autonomous DevOps and autonomous CloudOps?
&lt;/h3&gt;

&lt;p&gt;Autonomous DevOps is the general operating model and the framework for deciding which operations an agent may run unattended. Autonomous CloudOps (autonomous cloud operations) is that model applied to the cloud infrastructure plane: cost, Terraform drift, IaC coverage, security posture and compliance findings. The &lt;a href="https://beamreach.ai/docs/autonomous-cloudops.html" rel="noopener noreferrer"&gt;autonomous cloud operations guide&lt;/a&gt; covers those workloads.&lt;/p&gt;

&lt;h3&gt;
  
  
  Are autonomous DevOps agents safe?
&lt;/h3&gt;

&lt;p&gt;An agent is not safe or unsafe in general; an operation is. An agent is safe to run unattended on operations whose damage is small and known in advance, that have a cheap path back, and whose result can be checked mechanically. Everything else should go through human approval. Safety also depends on containing the model itself: no write path to live infrastructure, allow-listed tools, hard turn limits, and output limited to a reviewable pull request.&lt;/p&gt;

&lt;h3&gt;
  
  
  What are the security challenges of autonomous DevOps agents?
&lt;/h3&gt;

&lt;p&gt;The main ones are: agents inheriting a human operator's broad credentials; prompt injection through findings, logs or repository content the agent reads; unbounded loops and token spend; silent failure modes such as wrongly suppressed alerts; and relying on instructions in a prompt instead of structural limits. The mitigation is to enforce boundaries structurally (missing permissions, no live-mutation path, allow-listed tools, turn limits, diff-only output) and to treat the prompt as the weakest guardrail, not the main one.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do agentic DevOps tools improve deployment rollback?
&lt;/h3&gt;

&lt;p&gt;They do not make rollback magic. What helps is designing reversibility in before the agent acts: if every change is a Terraform or Git diff delivered as a pull request, rollback is a revert of a known commit. A useful agentic tool also scores reversibility before acting, so deletes and downsizes, which are harder to undo, are pushed toward human approval, while additive or state-only changes can qualify to run unattended.&lt;/p&gt;

&lt;h3&gt;
  
  
  What are AI guardrails for DevOps?
&lt;/h3&gt;

&lt;p&gt;AI guardrails for DevOps are the limits that decide whether an agent may act and what it can physically do. Two independent layers matter: a decision layer that scores each operation for risk from infrastructure facts and compares it to a threshold the team sets, and a containment layer around the model runtime, such as a pinned repository checkout, allow-listed tools, no write access to live infrastructure, turn limits and pull-request-only output.&lt;/p&gt;

&lt;h3&gt;
  
  
  Will autonomous DevOps replace SREs?
&lt;/h3&gt;

&lt;p&gt;No. It moves SREs from approving routine instances to setting boundaries: deciding which operation classes qualify for autonomy, tuning the risk threshold, handling exceptions, and owning architecture. The judgement does not disappear; it moves up a level and gets applied once per class of operation instead of once per change.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I apply this framework without Beamreach Compass?
&lt;/h3&gt;

&lt;p&gt;Yes. The framework is a way of scoring operations, not a product feature. Any team can classify its operations by verifiability, reversibility and containment, compute a risk level from infrastructure facts, and gate unattended execution on a threshold that a human controls. If you want it engineered into your own environment, that is what our &lt;a href="https://beamreach.ai/ai-consulting.html" rel="noopener noreferrer"&gt;custom autonomous systems engineering&lt;/a&gt; does; no Compass required.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://beamreach.ai/docs/autonomous-cloudops.html" rel="noopener noreferrer"&gt;Autonomous cloud operations (CloudOps) guide: cost, drift, posture and compliance&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://beamreach.ai/ai-devops.html" rel="noopener noreferrer"&gt;AI in DevOps: where AI agents help, and where they don't&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://beamreach.ai/docs/obstacles-for-autonomous-software.html" rel="noopener noreferrer"&gt;Obstacles for autonomous software: trust, control and cost&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://beamreach.ai/iac-coverage.html" rel="noopener noreferrer"&gt;IaC coverage and Terraform drift detection&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://beamreach.ai/honeypot-security.html" rel="noopener noreferrer"&gt;Cloud honeypots and canary credentials&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://beamreach.ai/docs/cloud.html" rel="noopener noreferrer"&gt;All guides for cloud teams&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://beamreach.ai/docs/autonomous-devops.html" rel="noopener noreferrer"&gt;https://beamreach.ai/docs/autonomous-devops.html&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I'm presenting &lt;em&gt;Autonomous DevOps: Engineering Systems That Don't Need Our Attention&lt;/em&gt; at KCD Porto x DevOpsDays Portugal (19–20 November 2026). Talk page: &lt;a href="https://beamreach.ai/talks/autonomous-devops.html" rel="noopener noreferrer"&gt;https://beamreach.ai/talks/autonomous-devops.html&lt;/a&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>ai</category>
      <category>sre</category>
      <category>security</category>
    </item>
    <item>
      <title>How to Reduce Alert Fatigue in Prometheus and Alertmanager: Measure, Cut, Verify</title>
      <dc:creator>Ilia Dubovskii</dc:creator>
      <pubDate>Fri, 23 Oct 2026 22:38:45 +0000</pubDate>
      <link>https://dev.to/beamreachcloud/how-to-reduce-alert-fatigue-in-prometheus-and-alertmanager-measure-cut-verify-41bg</link>
      <guid>https://dev.to/beamreachcloud/how-to-reduce-alert-fatigue-in-prometheus-and-alertmanager-measure-cut-verify-41bg</guid>
      <description>&lt;p&gt;Most advice on alert fatigue stops at "make alerts actionable". This guide is the working version for teams on Prometheus and Alertmanager: how to count the alerts that actually reach a person, which config changes cut them (grouping, inhibition, routing, &lt;code&gt;for:&lt;/code&gt; durations), which runbooks are safe to automate, how to run a weekly noise review, and how to check afterwards that you didn't throw away the alert that mattered. Every config snippet was checked against the Prometheus and Alertmanager documentation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The short version&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Track one number: &lt;strong&gt;alerts that reach a human per week&lt;/strong&gt;, broken down by alert rule. Label each page with what happened next.&lt;/li&gt;
&lt;li&gt;Group by the labels that identify one problem (&lt;code&gt;group_by&lt;/code&gt;), and inhibit symptom alerts while their cause is firing (&lt;code&gt;inhibit_rules&lt;/code&gt; with a correct &lt;code&gt;equal&lt;/code&gt; list).&lt;/li&gt;
&lt;li&gt;Page only for &lt;code&gt;severity="page"&lt;/code&gt;. Everything else goes to a ticket queue or a channel, never to a phone.&lt;/li&gt;
&lt;li&gt;Stop flapping with &lt;code&gt;for:&lt;/code&gt; and &lt;code&gt;keep_firing_for:&lt;/code&gt;, and test rule changes with &lt;code&gt;promtool test rules&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Automate a runbook only when its result can be checked, undone and kept small.&lt;/li&gt;
&lt;li&gt;Review the noisiest rules weekly and change them by pull request. After every change, check that real incidents would still have paged.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  1. Measure the noise before you touch a rule
&lt;/h2&gt;

&lt;p&gt;"We have too many alerts" is a feeling. To fix it you need a number that goes down when things improve and doesn't go down when you simply break alerting. The one that works: &lt;strong&gt;alerts that reach a human per week&lt;/strong&gt;. That means pages, phone calls and messages that someone is expected to look at, not everything Prometheus evaluates.&lt;/p&gt;

&lt;p&gt;There are three places to get it, from easiest to most complete:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Your paging tool.&lt;/strong&gt; PagerDuty, Opsgenie and Grafana IRM all keep a history of incidents or alert groups you can pull from the UI or the API. Count them per week and group them by alert name.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alertmanager's own metrics.&lt;/strong&gt; &lt;code&gt;alertmanager_notifications_total&lt;/code&gt; counts notifications sent, labelled by &lt;code&gt;integration&lt;/code&gt; (for example &lt;code&gt;pagerduty&lt;/code&gt; or &lt;code&gt;slack&lt;/code&gt;). &lt;code&gt;sum by (integration) (increase(alertmanager_notifications_total[7d]))&lt;/code&gt; gives a weekly total per channel. It counts notifications, not alert groups, so repeats and resolved messages are included. Use it for the trend, not as an exact count.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A webhook log.&lt;/strong&gt; Add a &lt;code&gt;webhook_configs&lt;/code&gt; receiver that writes every notification payload to a file or table. The payload includes each alert's labels, &lt;code&gt;startsAt&lt;/code&gt; and &lt;code&gt;fingerprint&lt;/code&gt;, so you can count per rule and per firing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To see how much time each rule spends firing, Prometheus already has the data. It writes a synthetic &lt;code&gt;ALERTS&lt;/code&gt; series with &lt;code&gt;alertstate="firing"&lt;/code&gt; at every rule evaluation, so the number of samples is the time spent firing divided by the evaluation interval:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Samples spent firing per rule over the last week.
# Multiply by your rule evaluation interval to get firing time.
topk(20, sum by (alertname) (count_over_time(ALERTS{alertstate="firing"}[7d])))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then add the part no tool gives you: &lt;strong&gt;what happened after each page&lt;/strong&gt;. For a few weeks, have whoever is on call tag every page with one of four outcomes: &lt;em&gt;acted&lt;/em&gt; (did something that changed the system), &lt;em&gt;looked, nothing to do&lt;/em&gt;, &lt;em&gt;duplicate&lt;/em&gt; (another page already covered it) or &lt;em&gt;already known&lt;/em&gt;. A rule's &lt;strong&gt;actionability rate&lt;/strong&gt; is acted divided by total pages. It's a rough measure, and that's fine. A rule that paged 40 times last month with 2 actions is obvious without decimals.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Sort every paging rule: page, ticket, automate or delete
&lt;/h2&gt;

&lt;p&gt;The Google SRE book's &lt;a href="https://sre.google/sre-book/monitoring-distributed-systems/" rel="noopener noreferrer"&gt;chapter on monitoring&lt;/a&gt; has the best short checklist for this. For each rule that can page, ask whether it detects a condition that is urgent, actionable and user-visible that nothing else detects; whether you'll ever ignore it because it's benign; whether the action could wait until morning or be safely automated; and whether other people are being paged for the same issue. The chapter puts it in one line: "Every page should be actionable."&lt;/p&gt;

&lt;p&gt;The answers put each rule into one of four buckets:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Bucket&lt;/th&gt;
&lt;th&gt;Test&lt;/th&gt;
&lt;th&gt;Where it goes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Page&lt;/td&gt;
&lt;td&gt;Users are affected or soon will be, and a person must act now&lt;/td&gt;
&lt;td&gt;On-call, with a runbook link&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ticket&lt;/td&gt;
&lt;td&gt;Needs fixing, but tomorrow is fine (a certificate expiring in 14 days, a disk at 70%)&lt;/td&gt;
&lt;td&gt;Ticket queue, deduplicated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Automate&lt;/td&gt;
&lt;td&gt;The response is always the same, and its result can be checked&lt;/td&gt;
&lt;td&gt;A runbook that runs and verifies, paging only if it fails&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delete&lt;/td&gt;
&lt;td&gt;Nobody has acted on it in months, or another alert always fires with it&lt;/td&gt;
&lt;td&gt;Removed, after the check in section 9&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Prefer paging on symptoms users feel (error rate, latency, failed checkouts) over causes (CPU, memory, a single pod restarting). Cause alerts are useful as context and as tickets. As pages, they're how one bad deploy turns into 30 notifications.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Group alerts that describe one problem
&lt;/h2&gt;

&lt;p&gt;Alertmanager batches alerts with the same values for the labels in &lt;code&gt;group_by&lt;/code&gt; into one notification. Many configs group too finely, or use &lt;code&gt;group_by: ['...']&lt;/code&gt;, which the docs say "effectively disables aggregation entirely". Pick the labels that identify one problem from the responder's point of view, usually cluster, namespace or service, plus the alert name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;route&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;receiver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ticket-queue&lt;/span&gt;          &lt;span class="c1"&gt;# anything not matched below never pages&lt;/span&gt;
  &lt;span class="na"&gt;group_by&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;cluster&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;alertname&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;group_wait&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;30s&lt;/span&gt;                 &lt;span class="c1"&gt;# wait for the rest of the group before the first notification&lt;/span&gt;
  &lt;span class="na"&gt;group_interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5m&lt;/span&gt;              &lt;span class="c1"&gt;# how often to send updates about a group that changed&lt;/span&gt;
  &lt;span class="na"&gt;repeat_interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;4h&lt;/span&gt;             &lt;span class="c1"&gt;# re-send an unchanged, still-firing group&lt;/span&gt;
  &lt;span class="na"&gt;routes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;receiver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;oncall-pager&lt;/span&gt;
      &lt;span class="na"&gt;matchers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;severity="page"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;receiver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;team-channel&lt;/span&gt;
      &lt;span class="na"&gt;matchers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;severity="info"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three details the docs spell out and teams tend to miss:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;group_wait&lt;/code&gt; also decides whether inhibition works.&lt;/strong&gt; If it's too short, the first notification goes out before the alert that should inhibit it has arrived. 30 seconds (the default) is a reasonable floor when you rely on inhibition.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;repeat_interval&lt;/code&gt; is rounded up to a multiple of &lt;code&gt;group_interval&lt;/code&gt;&lt;/strong&gt;, so set it as one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Child routes inherit&lt;/strong&gt; &lt;code&gt;group_by&lt;/code&gt;, &lt;code&gt;group_wait&lt;/code&gt;, &lt;code&gt;group_interval&lt;/code&gt; and &lt;code&gt;repeat_interval&lt;/code&gt; from the parent unless they set their own. A team route can group by &lt;code&gt;service&lt;/code&gt; while the root groups by &lt;code&gt;cluster&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Grafana Alerting works the same way: notification policies have a Group by option and the same three timers, and by default group by &lt;code&gt;alertname&lt;/code&gt; and &lt;code&gt;grafana_folder&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Inhibit symptoms while the cause is firing
&lt;/h2&gt;

&lt;p&gt;Grouping merges alerts that look alike. Inhibition handles alerts that look different but share a cause: a node runs out of disk, and you also get evictions, crash loops and 5xx errors from everything scheduled on it. An inhibit rule mutes the &lt;em&gt;target&lt;/em&gt; alerts while a &lt;em&gt;source&lt;/em&gt; alert is firing, as long as the labels in &lt;code&gt;equal&lt;/code&gt; match:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;inhibit_rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# A critical alert hides the warning version of the same alert.&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;source_matchers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;severity="critical"&lt;/span&gt;
    &lt;span class="na"&gt;target_matchers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;severity="warning"&lt;/span&gt;
    &lt;span class="na"&gt;equal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;alertname&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;cluster&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

  &lt;span class="c1"&gt;# Disk pressure on a node hides pod symptoms on that same node.&lt;/span&gt;
  &lt;span class="c1"&gt;# Alert names are examples: use the names your rules actually produce.&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;source_matchers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;alertname="NodeDiskPressure"&lt;/span&gt;
    &lt;span class="na"&gt;target_matchers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;alertname=~"KubePodEvicted|KubePodCrashLooping"&lt;/span&gt;
    &lt;span class="na"&gt;equal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;cluster&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;node&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second rule is where most inhibition goes wrong, so check two things.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Both alerts must carry every label in &lt;code&gt;equal&lt;/code&gt;.&lt;/strong&gt; Many pod-level alerts don't have a &lt;code&gt;node&lt;/code&gt; label, because the metric they're built on doesn't. If the source has &lt;code&gt;node="ip-10-2-14-7"&lt;/code&gt; and the target has no &lt;code&gt;node&lt;/code&gt; at all, the values differ and nothing is inhibited. The fix is to add the label in the rule's expression (for example by joining with &lt;code&gt;kube_pod_info&lt;/code&gt;), not to drop &lt;code&gt;node&lt;/code&gt; from &lt;code&gt;equal&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Missing labels count as equal.&lt;/strong&gt; The Alertmanager docs say: "if all the label names listed in &lt;code&gt;equal&lt;/code&gt; are missing from both the source and target alerts, the inhibition rule will apply." An &lt;code&gt;equal&lt;/code&gt; list with a typo in a label name can therefore mute every matching target in every cluster. Keep the source and target matchers narrow, and test the rule (section 9).&lt;/p&gt;

&lt;p&gt;Inhibited alerts aren't deleted. They still show in Prometheus and in the Alertmanager API as suppressed, with the inhibiting alert listed. That matters when you check you lost nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Route non-urgent alerts to tickets, not phones
&lt;/h2&gt;

&lt;p&gt;The cheapest change on this list: make paging opt-in. With the routing tree above, only &lt;code&gt;severity="page"&lt;/code&gt; reaches the on-call receiver. Everything else lands in a ticket queue through a webhook:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;receivers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;oncall-pager&lt;/span&gt;
    &lt;span class="na"&gt;pagerduty_configs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;routing_key_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/etc/alertmanager/secrets/pagerduty-key&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ticket-queue&lt;/span&gt;
    &lt;span class="na"&gt;webhook_configs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://alert-to-ticket.internal.example/hook&lt;/span&gt;
        &lt;span class="na"&gt;send_resolved&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;team-channel&lt;/span&gt;
    &lt;span class="na"&gt;slack_configs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;api_url_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/etc/alertmanager/secrets/slack-webhook&lt;/span&gt;
        &lt;span class="na"&gt;channel&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;#ops-alerts'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The webhook service should create one ticket per alert group (use the payload's &lt;code&gt;groupKey&lt;/code&gt;), comment on it on repeats instead of opening a new one, and close it when the alert resolves. That turns the cert-expiring-in-14-days alert from six pages a week into one ticket.&lt;/p&gt;

&lt;p&gt;Check routing changes without deploying them: &lt;code&gt;amtool check-config alertmanager.yml&lt;/code&gt; validates the file, and &lt;code&gt;amtool config routes test --config.file=alertmanager.yml severity=page service=payments&lt;/code&gt; prints which receiver a given label set would reach.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Tune thresholds with for: and keep_firing_for:
&lt;/h2&gt;

&lt;p&gt;A rule that fires for 40 seconds and resolves on its own is flapping. Two fields fix most of it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;groups&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;payments&lt;/span&gt;
    &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;alert&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PaymentsHighErrorRate&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;sum(rate(http_requests_total{job="payments", code=~"5.."}[5m]))&lt;/span&gt;
            &lt;span class="s"&gt;/ sum(rate(http_requests_total{job="payments"}[5m])) &amp;gt; 0.02&lt;/span&gt;
        &lt;span class="na"&gt;for&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10m&lt;/span&gt;              &lt;span class="c1"&gt;# must be true for 10 minutes before it fires&lt;/span&gt;
        &lt;span class="na"&gt;keep_firing_for&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5m&lt;/span&gt;   &lt;span class="c1"&gt;# stays firing 5 minutes after it stops being true&lt;/span&gt;
        &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;page&lt;/span&gt;
        &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Payments 5xx above 2% for 10 minutes&lt;/span&gt;
          &lt;span class="na"&gt;runbook_url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://runbooks.internal.example/payments-5xx&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;for&lt;/code&gt; keeps the alert in the pending state until the condition has held for that long, so short spikes never notify. &lt;code&gt;keep_firing_for&lt;/code&gt; (Prometheus 2.42 and later) keeps it firing for a while after the condition clears, so a metric that dips below the threshold for one evaluation doesn't produce a resolve and a fresh page. Grafana alert rules have the same idea as a pending period.&lt;/p&gt;

&lt;p&gt;Don't guess the durations. Use the firing-time query from section 1 and your incident history: if real incidents for this rule always lasted longer than 15 minutes and the noise always cleared in under 3, a &lt;code&gt;for&lt;/code&gt; of 5 to 10 minutes costs you little detection time. Write the decision down as a unit test so the next person can see why:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# payments_test.yml, run with: promtool test rules payments_test.yml&lt;/span&gt;
&lt;span class="na"&gt;rule_files&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;payments_rules.yml&lt;/span&gt;
&lt;span class="na"&gt;evaluation_interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1m&lt;/span&gt;
&lt;span class="na"&gt;tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1m&lt;/span&gt;
    &lt;span class="na"&gt;input_series&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# 3-minute spike to 5% errors, then normal: must NOT fire.&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;series&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;http_requests_total{job="payments",&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;code="500"}'&lt;/span&gt;
        &lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0+0x10&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;0+5x3&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;15+0x20'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;series&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;http_requests_total{job="payments",&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;code="200"}'&lt;/span&gt;
        &lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0+100x40'&lt;/span&gt;
    &lt;span class="na"&gt;alert_rule_test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;eval_time&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;20m&lt;/span&gt;
        &lt;span class="na"&gt;alertname&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PaymentsHighErrorRate&lt;/span&gt;
        &lt;span class="na"&gt;exp_alerts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add a second test with a sustained error rate that &lt;em&gt;must&lt;/em&gt; fire. The pair stops anyone loosening the rule until it misses a real outage.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Which runbooks are safe to automate
&lt;/h2&gt;

&lt;p&gt;If the response to an alert is always the same (clear old logs, restart one stuck worker, rotate one node), a person doing it at 2 a.m. adds delay and little judgement. But automatic remediation is only safe when three things are true. We call them &lt;a href="https://beamreach.ai/docs/autonomous-devops.html" rel="noopener noreferrer"&gt;verifiable, reversible and contained&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Verifiable.&lt;/strong&gt; You can tell from a metric whether it worked, ideally the same metric that raised the alert. "Disk on the node below 80% within 5 minutes" is verifiable. "Restarted the service" is not; it only says the command ran.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reversible.&lt;/strong&gt; If it was the wrong move, you can undo it. Restarting a stateless pod is reversible in practice. Deleting data or failing over a primary database usually isn't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contained.&lt;/strong&gt; It acts on a bounded scope with hard limits: one node at a time, at most three runs an hour, never during a deploy, never on more than 10% of a pool.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Wire it so the runbook runs, waits, reads the triggering metric again, and if the metric hasn't recovered, stops and pages a person with what it tried. An automated fix that fails quietly is worse than a page. Log every run where the team will see it, so the weekly review can count automated fixes too: a rule fixed automatically 30 times a week is a root cause waiting for a ticket.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. The weekly noise review
&lt;/h2&gt;

&lt;p&gt;Alert rules decay as the system under them changes, so tuning once doesn't hold. A 30-minute weekly slot with the people who were on call does. A format that works:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The number&lt;/strong&gt; (5 minutes). Alerts that reached a human this week versus last, and how many were acted on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The top five rules by pages&lt;/strong&gt; (20 minutes). For each: how many pages, how many acted on, how long it fired. Decide one of: keep, retune, demote to ticket, automate, delete. Write down why.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Owners&lt;/strong&gt; (5 minutes). Every decision becomes a pull request against the rules repo with a named owner, merged by the following week.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Two rules keep it honest. Changes go through pull requests, never through a silence that someone forgets to remove. And nobody mutes a rule because it's annoying without saying what would catch the problem instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Check you didn't lose signal
&lt;/h2&gt;

&lt;p&gt;You can always reduce alerts by alerting on less. The number from section 1 can't tell you whether you did that, so every change needs a second check.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Replay real incidents.&lt;/strong&gt; Take the last few months of real incidents, especially those found by a page. For each, ask whether the changed rules would still have paged, and how much later. Unit tests make this concrete: feed the incident's metric shape into &lt;code&gt;promtool test rules&lt;/code&gt; and assert the alert still fires.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch suppressed alerts for a week.&lt;/strong&gt; Prometheus's &lt;code&gt;ALERTS&lt;/code&gt; series isn't affected by Alertmanager inhibition or routing. Compare what fired in Prometheus with what was delivered. Any alert that was inhibited while its source alert was &lt;em&gt;not&lt;/em&gt; about the same problem is a bad &lt;code&gt;equal&lt;/code&gt; list.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Count incidents found the wrong way.&lt;/strong&gt; Track incidents first reported by customers, support or another team. If that count rises after a tuning round, you cut too deep, whatever the page count says.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Demote before deleting.&lt;/strong&gt; Move a doubtful rule to &lt;code&gt;severity="ticket"&lt;/code&gt; for a month first. If nobody acts on any of those tickets either, delete it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  10. Where products and agents fit
&lt;/h2&gt;

&lt;p&gt;Everything above works with open-source Prometheus and Alertmanager, and for many teams it's enough. The commercial tools automate parts of it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PagerDuty&lt;/strong&gt; offers Intelligent Alert Grouping (machine-learned grouping of related alerts into one incident) and Auto-Pause Incident Notifications (holding notifications for transient alerts so they can resolve on their own) in its AIOps add-on, or as Signal Intelligence in its PD Reliability Platform plans. PagerDuty Runbook Automation can run diagnostics and remediation from incidents.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Datadog Event Management&lt;/strong&gt; groups related alerts into cases, with Intelligent Correlation (machine learning using service topology) and Pattern-based Correlation (rules you define).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BigPanda&lt;/strong&gt; correlates alerts from many monitoring tools into incidents and is aimed at large enterprise IT operations teams.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Opsgenie&lt;/strong&gt; stopped being sold on 4 June 2025 and its support ends on 5 April 2027. Atlassian moves its alerting and on-call features into Jira Service Management, so plan that migration rather than new Opsgenie tuning.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What none of them do for you is the judgement in sections 2, 7, 8 and 9: deciding which rule is wrong, fixing it in your repo, and proving nothing was lost.&lt;/p&gt;

&lt;h2&gt;
  
  
  11. FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is alert fatigue?
&lt;/h3&gt;

&lt;p&gt;Alert fatigue is what happens when on-call engineers get so many alerts that lead nowhere that they stop treating each one as urgent. Pages get acknowledged without being read, and the alert that matters is missed or answered late. The fix is fewer alerts reaching people, not faster responses to all of them.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I reduce the number of alerts from Alertmanager?
&lt;/h3&gt;

&lt;p&gt;Start by counting pages per alert rule for a few weeks and noting which ones anyone acted on. Then group alerts that describe one problem with group_by, inhibit symptom alerts while their cause is firing with inhibit_rules, route anything non-urgent to a ticket queue instead of the pager, and add for: durations to rules that flap. Review the noisiest rules weekly and change them by pull request.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the difference between grouping and inhibition in Alertmanager?
&lt;/h3&gt;

&lt;p&gt;Grouping merges alerts that have the same values for the labels in group_by into one notification, for example nine crash-looping pods in one namespace. Inhibition mutes alerts of a different kind while a related source alert is firing, for example pod evictions on a node that is out of disk. Grouping reduces repeats; inhibition removes symptoms of a cause you already know about.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why is my Alertmanager inhibit rule not working?
&lt;/h3&gt;

&lt;p&gt;The usual cause is the equal list. Both the source and target alerts must have the same value for every label in it, and if the target alert doesn't carry a label such as node at all, the values differ and nothing is inhibited. A group_wait that's too short is the other cause: the target's notification goes out before the source alert arrives. Check the labels on both alerts in the Alertmanager UI or with amtool.&lt;/p&gt;

&lt;h3&gt;
  
  
  How long should the for: duration be on a Prometheus alert?
&lt;/h3&gt;

&lt;p&gt;Long enough that short spikes which clear on their own never fire, and short enough that a real incident still pages in time. Look at how long this rule's noisy firings lasted compared with real incidents, and pick a value between them. Add keep_firing_for (Prometheus 2.42 and later) if the alert resolves and re-fires when the metric hovers around the threshold.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which runbooks are safe to automate?
&lt;/h3&gt;

&lt;p&gt;Runbooks whose result you can check with a metric, that you can undo, and that act on a small, bounded scope, such as clearing old logs on one node or restarting one stateless worker. The automation should re-read the metric that triggered the alert after it runs and page a person if the metric hasn't recovered. Anything that deletes data or fails over a database should stay with a human.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I know I didn't lose important alerts after tuning?
&lt;/h3&gt;

&lt;p&gt;Replay recent real incidents against the changed rules, ideally as promtool unit tests, and confirm each one would still have paged. Compare what fired in Prometheus with what Alertmanager delivered for a week to catch inhibit rules that hide too much. Track incidents first reported by customers or other teams: if that number rises after a tuning round, you cut too deep.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written by Ilia Dubovskii, founder of Beamreach. Config was checked against the Prometheus and Alertmanager documentation, and product details against each vendor's own site, on 10 October 2026. If something here no longer matches the docs, the docs win. Tell us and we'll fix this page.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Related: &lt;a href="https://beamreach.ai/docs/autonomous-devops.html" rel="noopener noreferrer"&gt;Autonomous DevOps&lt;/a&gt; · &lt;a href="https://beamreach.ai/docs/cloud.html" rel="noopener noreferrer"&gt;All guides for cloud teams&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://beamreach.ai/docs/reduce-alert-fatigue-alertmanager.html" rel="noopener noreferrer"&gt;https://beamreach.ai/docs/reduce-alert-fatigue-alertmanager.html&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The verifiable, reversible, contained test from section 7 is the core of my talk &lt;em&gt;Autonomous DevOps: Engineering Systems That Don't Need Our Attention&lt;/em&gt; at KCD Porto x DevOpsDays Portugal (19–20 November 2026). Talk page: &lt;a href="https://beamreach.ai/talks/autonomous-devops.html" rel="noopener noreferrer"&gt;https://beamreach.ai/talks/autonomous-devops.html&lt;/a&gt;&lt;/p&gt;

</description>
      <category>prometheus</category>
      <category>monitoring</category>
      <category>devops</category>
      <category>sre</category>
    </item>
    <item>
      <title>AWS Canary Credentials: How to Set Up an IAM Honeytoken</title>
      <dc:creator>Ilia Dubovskii</dc:creator>
      <pubDate>Fri, 23 Oct 2026 22:37:52 +0000</pubDate>
      <link>https://dev.to/beamreachcloud/aws-canary-credentials-how-to-set-up-an-iam-honeytoken-4398</link>
      <guid>https://dev.to/beamreachcloud/aws-canary-credentials-how-to-set-up-an-iam-honeytoken-4398</guid>
      <description>&lt;p&gt;A canary credential, or AWS honeytoken, is a real AWS access key that can do nothing and that nobody legitimate will ever use. You plant it where an attacker looks for credentials. When someone uses it, CloudTrail records the call and you get an alert that carries very little doubt, and that points at exactly one place: wherever you planted that key. This guide builds one from scratch in Terraform, explains the AWS details that most write-ups get wrong, and compares the free and commercial alternatives honestly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The short version&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create an IAM user with no console access, an explicit deny-all policy and one access key.&lt;/li&gt;
&lt;li&gt;Alert on any CloudTrail event from that user. If you use EventBridge, the rule needs &lt;code&gt;state = "ENABLED_WITH_ALL_CLOUDTRAIL_MANAGEMENT_EVENTS"&lt;/code&gt; (AWS provider 5.27.0 or later). Without it, you'll miss &lt;code&gt;sts:GetCallerIdentity&lt;/code&gt;, which is usually the attacker's first call.&lt;/li&gt;
&lt;li&gt;EventBridge rules are regional, so cover every enabled region or use a multi-region trail with a CloudWatch Logs metric filter as a backstop.&lt;/li&gt;
&lt;li&gt;Anyone holding the key can learn its AWS account ID without touching your logs. Decide deliberately which account the canary lives in.&lt;/li&gt;
&lt;li&gt;Plant one key per location, under names your own tooling won't load automatically.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  1. What a canary credential is, and why it's the cheapest certainty
&lt;/h2&gt;

&lt;p&gt;A honeypot is any decoy built to be touched only by an intruder. A &lt;strong&gt;honeytoken&lt;/strong&gt; is a decoy piece of data, such as a credential, a document or a database row, rather than a whole decoy system. A &lt;strong&gt;canary token&lt;/strong&gt; is a honeytoken that alerts you when it's used. An &lt;strong&gt;AWS canary credential&lt;/strong&gt; is all three: a real IAM access key (&lt;code&gt;AKIA…&lt;/code&gt;) attached to a user that is explicitly denied everything.&lt;/p&gt;

&lt;p&gt;The point isn't the decoy, it's the quality of the signal. Most alerts measure a &lt;em&gt;symptom&lt;/em&gt;: CPU, egress, error rate, cost. They fire after something has started to go wrong, against a baseline that legitimate work (and attackers) move around in. That's why a human has to triage them, and why automating a response to them is risky. If one alert in a thousand is an attack, an automatic kill switch is wrong 999 times.&lt;/p&gt;

&lt;p&gt;A canary trip measures &lt;em&gt;intent&lt;/em&gt;. Someone found a credential, decided it was worth stealing, and used it. There's no baseline to tune, and because each key lives in exactly one place, the key ID tells you where it was taken from. That's a signal you can act on immediately, including automatically, as long as the action is one you can undo.&lt;/p&gt;

&lt;p&gt;Canaries aren't the only high-confidence signal. A GuardDuty finding for instance credentials used outside AWS, or a Falco rule for a shell spawned in a production container, can be just as trustworthy, and if you trust them you should automate on them too. The rule is to &lt;strong&gt;automate on certainty&lt;/strong&gt;. Canaries are simply the cheapest way to manufacture certainty: no agents, they work on ECS Fargate and Lambda, and one takes an afternoon. They don't replace your alerts. Keep both.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Your options: canarytokens.org, Thinkst Canary, DIY, Compass
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;What you get&lt;/th&gt;
&lt;th&gt;Enough when…&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;canarytokens.org (free, by Thinkst)&lt;/td&gt;
&lt;td&gt;An "AWS API key" token in about a minute. Thinkst owns the AWS account and the detection, and alerts come by email or webhook. Their docs say alerts can take &lt;a href="https://docs.canarytokens.org/guide/aws-keys-token.html" rel="noopener noreferrer"&gt;between 2 and 30 minutes&lt;/a&gt;.&lt;/td&gt;
&lt;td&gt;You want a first canary today and don't mind the key living in a well-known account. TruffleHog now &lt;a href="https://trufflesecurity.com/canaries" rel="noopener noreferrer"&gt;recognises these keys offline&lt;/a&gt;, so a careful attacker using it won't trip them.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Thinkst Canary (commercial)&lt;/td&gt;
&lt;td&gt;A mature product: hardware, virtual and cloud Canaries plus a managed console for tokens, including AWS keys. TruffleHog says it deliberately doesn't fingerprint the paid service's accounts.&lt;/td&gt;
&lt;td&gt;You want canaries across the whole estate (network, Windows, cloud) with a vendor who has done this for years.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DIY with Terraform (this guide)&lt;/td&gt;
&lt;td&gt;A key in an account you choose, alerting through your own CloudTrail, EventBridge and SNS. You own every moving part.&lt;/td&gt;
&lt;td&gt;You have a handful of high-value places to cover and someone who will keep the region coverage and placements up to date.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://compass.beamreach.ai/" rel="noopener noreferrer"&gt;Beamreach Compass&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Suggests placements from a map of your AWS infrastructure, plants them as Terraform PRs in your own repo and account, and routes trips to Slack or PagerDuty. Today only the IAM canary is available. The rest is in development or on the roadmap; see the &lt;a href="https://beamreach.ai/honeypot-security.html#status" rel="noopener noreferrer"&gt;honest status&lt;/a&gt;.&lt;/td&gt;
&lt;td&gt;You want placement and lifecycle managed through code review and wired into an automated response. Disclosure: we make it.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you're unsure, start with one free token today and build the DIY version this week. The rest of this guide is the DIY version.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. How detection works in AWS: the facts that matter
&lt;/h2&gt;

&lt;p&gt;The whole design rests on a few AWS behaviours. Each one below is checked against AWS documentation, which is linked.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CloudTrail records calls made with the key, including denied ones.&lt;/strong&gt; A call refused by the deny-all policy is still an authenticated API call. It's logged with &lt;code&gt;errorCode&lt;/code&gt; (for example &lt;code&gt;AccessDenied&lt;/code&gt;), plus &lt;code&gt;sourceIPAddress&lt;/code&gt;, &lt;code&gt;userAgent&lt;/code&gt; and &lt;code&gt;userIdentity.accessKeyId&lt;/code&gt;. (&lt;a href="https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-event-reference-record-contents.html" rel="noopener noreferrer"&gt;CloudTrail record contents&lt;/a&gt;.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;sts:GetCallerIdentity&lt;/code&gt; can't be denied, and it is logged.&lt;/strong&gt; AWS says no permissions are required and that an explicit deny doesn't stop it (&lt;a href="https://docs.aws.amazon.com/STS/latest/APIReference/API_GetCallerIdentity.html" rel="noopener noreferrer"&gt;API reference&lt;/a&gt;). CloudTrail captures all IAM and STS API calls (&lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/cloudtrail-integration.html" rel="noopener noreferrer"&gt;IAM user guide&lt;/a&gt;). This is the "whoami" call that attackers and secret scanners make first. Your canary succeeds at it, which helps it look real, and the call still lands in your logs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EventBridge ignores read-only calls unless you opt in.&lt;/strong&gt; CloudTrail sends "AWS API Call via CloudTrail" events to the default event bus. A rule in the normal &lt;code&gt;ENABLED&lt;/code&gt; state matches write events but not read-only management events such as &lt;code&gt;Get*&lt;/code&gt;, &lt;code&gt;List*&lt;/code&gt;, &lt;code&gt;Describe*&lt;/code&gt; and &lt;code&gt;GetCallerIdentity&lt;/code&gt;. Only a rule in the &lt;code&gt;ENABLED_WITH_ALL_CLOUDTRAIL_MANAGEMENT_EVENTS&lt;/code&gt; state matches those (&lt;a href="https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-service-event-cloudtrail.html" rel="noopener noreferrer"&gt;EventBridge: events via CloudTrail&lt;/a&gt;, &lt;a href="https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-service-event-cloudtrail-management.html" rel="noopener noreferrer"&gt;read-only management events&lt;/a&gt;). In Terraform this is the &lt;code&gt;state&lt;/code&gt; argument of &lt;code&gt;aws_cloudwatch_event_rule&lt;/code&gt;, added in AWS provider &lt;strong&gt;5.27.0&lt;/strong&gt; (it replaces the deprecated &lt;code&gt;is_enabled&lt;/code&gt;). This is the single most common bug in DIY AWS canaries: a rule that only catches the attacker's second, mutating call, if they ever make one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need a trail.&lt;/strong&gt; EventBridge only receives CloudTrail-sourced events if a trail with logging is enabled. Event history alone isn't enough (&lt;a href="https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-service-event-cloudtrail.html" rel="noopener noreferrer"&gt;same page&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Events land in the region that was called.&lt;/strong&gt; EventBridge rules are regional. A call to a regional endpoint such as &lt;code&gt;sts.eu-west-1.amazonaws.com&lt;/code&gt; is logged in eu-west-1. Calls to the global STS endpoint are logged in us-east-1 (&lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/cloudtrail-integration.html" rel="noopener noreferrer"&gt;IAM user guide&lt;/a&gt;). The attacker chooses the region, not you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forwarding read-only events needs the special state twice.&lt;/strong&gt; If you forward events from one region's default bus to another bus, both the forwarding rule and the destination rule need &lt;code&gt;ENABLED_WITH_ALL_CLOUDTRAIL_MANAGEMENT_EVENTS&lt;/code&gt; (&lt;a href="https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-service-event-cloudtrail.html" rel="noopener noreferrer"&gt;same page&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CloudTrail isn't instant.&lt;/strong&gt; AWS says CloudTrail typically delivers events to S3 and CloudWatch Logs within an average of about 5 minutes, and that this isn't guaranteed (&lt;a href="https://docs.aws.amazon.com/awscloudtrail/latest/userguide/send-cloudtrail-events-to-cloudwatch-logs.html" rel="noopener noreferrer"&gt;CloudTrail to CloudWatch Logs&lt;/a&gt;). One vendor's measurement of S3 delivery found an average around 2.5 minutes and a 99th percentile just over 5 (&lt;a href="https://tracebit.com/blog/how-fast-is-cloudtrail-today-investigating-cloudtrail-delays-using-athena" rel="noopener noreferrer"&gt;Tracebit&lt;/a&gt;). AWS doesn't publish a latency figure for the EventBridge path, so measure your own (section 9).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. Step 1: the canary user and key
&lt;/h2&gt;

&lt;p&gt;Use one IAM user per placement. When a key trips, the user name and key ID should map to exactly one location. Name it like your other service users. Anything visible in AWS (name, path, description, tags) can be read by whoever holds a credential with &lt;code&gt;iam:Get*&lt;/code&gt; or &lt;code&gt;iam:List*&lt;/code&gt;, so none of it should say "canary".&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;required_version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&amp;gt;= 1.5"&lt;/span&gt;
  &lt;span class="nx"&gt;required_providers&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;aws&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;source&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hashicorp/aws"&lt;/span&gt;
      &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&amp;gt;= 5.27, &amp;lt; 6.0"&lt;/span&gt; &lt;span class="c1"&gt;# 5.27.0 added aws_cloudwatch_event_rule.state&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Home region. Global-endpoint IAM/STS calls are logged here.&lt;/span&gt;
&lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="s2"&gt;"aws"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;region&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"us-east-1"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_caller_identity"&lt;/span&gt; &lt;span class="s2"&gt;"current"&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="nx"&gt;locals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;account_id&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_caller_identity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;account_id&lt;/span&gt;
  &lt;span class="nx"&gt;canary_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"svc-backup-reporting"&lt;/span&gt; &lt;span class="c1"&gt;# match your real naming style&lt;/span&gt;
  &lt;span class="nx"&gt;home_bus&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"arn:aws:events:us-east-1:${local.account_id}:event-bus/default"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_user"&lt;/span&gt; &lt;span class="s2"&gt;"canary"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;canary_name&lt;/span&gt;
  &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/service/"&lt;/span&gt;
  &lt;span class="c1"&gt;# No aws_iam_user_login_profile: no console password, API key only.&lt;/span&gt;
  &lt;span class="c1"&gt;# No tags or descriptions that give the game away.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_user_policy"&lt;/span&gt; &lt;span class="s2"&gt;"deny_all"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"backup-reporting"&lt;/span&gt;
  &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;canary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;policy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonencode&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;Version&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;
    &lt;span class="nx"&gt;Statement&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="nx"&gt;Effect&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Deny"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Action&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Resource&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_access_key"&lt;/span&gt; &lt;span class="s2"&gt;"canary"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;canary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"canary_access_key_id"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_access_key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;canary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"canary_secret_access_key"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_access_key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;canary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;secret&lt;/span&gt;
  &lt;span class="nx"&gt;sensitive&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two notes. First, the secret ends up in Terraform state in plain text. For a key that can do nothing that's usually acceptable, and anyone who steals it from state trips it. If your policy forbids it, use the &lt;code&gt;pgp_key&lt;/code&gt; argument or create the key outside Terraform. Second, an explicit &lt;code&gt;Deny&lt;/code&gt; wins over any &lt;code&gt;Allow&lt;/code&gt;, so even if someone later attaches a policy to this user by mistake, the key stays useless.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Step 2a: fast alerts with EventBridge, in every region
&lt;/h2&gt;

&lt;p&gt;Match on the user's ARN rather than the key ID. If an attacker exchanges the key for a session token, later calls carry a temporary &lt;code&gt;ASIA…&lt;/code&gt; key ID but the same user ARN. The home-region rule sends matches to an SNS topic. Every other region gets a copy of the rule that forwards matches to the home region's default bus.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"alert_email"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_sns_topic"&lt;/span&gt; &lt;span class="s2"&gt;"alerts"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ops-notifications"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_sns_topic_subscription"&lt;/span&gt; &lt;span class="s2"&gt;"email"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;topic_arn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_sns_topic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alerts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
  &lt;span class="nx"&gt;protocol&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"email"&lt;/span&gt;
  &lt;span class="nx"&gt;endpoint&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alert_email&lt;/span&gt; &lt;span class="c1"&gt;# click the confirmation link AWS sends&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Let EventBridge rules and CloudWatch alarms in this account publish.&lt;/span&gt;
&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_policy_document"&lt;/span&gt; &lt;span class="s2"&gt;"alerts"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;sid&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"EventBridgePublish"&lt;/span&gt;
    &lt;span class="nx"&gt;actions&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"sns:Publish"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_sns_topic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alerts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;principals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Service"&lt;/span&gt;
      &lt;span class="nx"&gt;identifiers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"events.amazonaws.com"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;condition&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;test&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ArnLike"&lt;/span&gt;
      &lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"aws:SourceArn"&lt;/span&gt;
      &lt;span class="nx"&gt;values&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:events:us-east-1:${local.account_id}:rule/*"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;sid&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"CloudWatchAlarmPublish"&lt;/span&gt;
    &lt;span class="nx"&gt;actions&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"sns:Publish"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_sns_topic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alerts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;principals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Service"&lt;/span&gt;
      &lt;span class="nx"&gt;identifiers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"cloudwatch.amazonaws.com"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;condition&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;test&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ArnLike"&lt;/span&gt;
      &lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"aws:SourceArn"&lt;/span&gt;
      &lt;span class="nx"&gt;values&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:cloudwatch:us-east-1:${local.account_id}:alarm:*"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_sns_topic_policy"&lt;/span&gt; &lt;span class="s2"&gt;"alerts"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;arn&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_sns_topic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alerts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
  &lt;span class="nx"&gt;policy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_iam_policy_document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alerts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;locals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;canary_pattern&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonencode&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="s2"&gt;"detail-type"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"AWS API Call via CloudTrail"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;detail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;userIdentity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;arn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_iam_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;canary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Home region rule. It also matches events forwarded from other regions.&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_cloudwatch_event_rule"&lt;/span&gt; &lt;span class="s2"&gt;"canary_home"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;          &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"backup-reporting-activity"&lt;/span&gt;
  &lt;span class="nx"&gt;state&lt;/span&gt;         &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ENABLED_WITH_ALL_CLOUDTRAIL_MANAGEMENT_EVENTS"&lt;/span&gt;
  &lt;span class="nx"&gt;event_pattern&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;canary_pattern&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_cloudwatch_event_target"&lt;/span&gt; &lt;span class="s2"&gt;"canary_home_sns"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;rule&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_cloudwatch_event_rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;canary_home&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;arn&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_sns_topic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alerts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Role EventBridge uses to forward events from other regions to us-east-1.&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role"&lt;/span&gt; &lt;span class="s2"&gt;"events_forwarder"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"events-forwarder"&lt;/span&gt;
  &lt;span class="nx"&gt;assume_role_policy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonencode&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;Version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;
    &lt;span class="nx"&gt;Statement&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
      &lt;span class="nx"&gt;Effect&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
      &lt;span class="nx"&gt;Principal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Service&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"events.amazonaws.com"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nx"&gt;Action&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"sts:AssumeRole"&lt;/span&gt;
    &lt;span class="p"&gt;}]&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role_policy"&lt;/span&gt; &lt;span class="s2"&gt;"events_forwarder"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"put-events-home-bus"&lt;/span&gt;
  &lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;events_forwarder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;policy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonencode&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;Version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;
    &lt;span class="nx"&gt;Statement&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
      &lt;span class="nx"&gt;Effect&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
      &lt;span class="nx"&gt;Action&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"events:PutEvents"&lt;/span&gt;
      &lt;span class="nx"&gt;Resource&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;home_bus&lt;/span&gt;
    &lt;span class="p"&gt;}]&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Repeat this block for EVERY region enabled in the account.&lt;/span&gt;
&lt;span class="c1"&gt;# eu-west-1 shown. List yours with: aws account list-regions --region-opt-status-contains ENABLED ENABLED_BY_DEFAULT&lt;/span&gt;
&lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="s2"&gt;"aws"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;alias&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"eu_west_1"&lt;/span&gt;
  &lt;span class="nx"&gt;region&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"eu-west-1"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_cloudwatch_event_rule"&lt;/span&gt; &lt;span class="s2"&gt;"canary_eu_west_1"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;provider&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;eu_west_1&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;          &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"backup-reporting-activity"&lt;/span&gt;
  &lt;span class="nx"&gt;state&lt;/span&gt;         &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ENABLED_WITH_ALL_CLOUDTRAIL_MANAGEMENT_EVENTS"&lt;/span&gt;
  &lt;span class="nx"&gt;event_pattern&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;canary_pattern&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_cloudwatch_event_target"&lt;/span&gt; &lt;span class="s2"&gt;"canary_eu_west_1"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;eu_west_1&lt;/span&gt;
  &lt;span class="nx"&gt;rule&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_cloudwatch_event_rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;canary_eu_west_1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;arn&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;home_bus&lt;/span&gt;
  &lt;span class="nx"&gt;role_arn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;events_forwarder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wrap the per-region pair in a small module and instantiate it once per region with a provider alias. (AWS provider v6 adds a per-resource &lt;code&gt;region&lt;/code&gt; argument that lets you &lt;code&gt;for_each&lt;/code&gt; over regions instead. This guide sticks to v5.) Email is the simplest target to show. In practice, subscribe your paging tool to the topic, or point the rule's target at an SQS queue or Lambda that posts to Slack.&lt;/p&gt;

&lt;p&gt;EventBridge still needs a trail that is logging (section 3). Most accounts already have one. If yours doesn't, the trail in step 2b covers it. Configure it for both read and write management events. We haven't verified whether a write-only trail still feeds read-only events to EventBridge, so don't rely on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Step 2b: the backstop, a multi-region trail with a metric filter
&lt;/h2&gt;

&lt;p&gt;A multi-region trail sends events from every enabled region to one CloudWatch Logs log group (&lt;a href="https://docs.aws.amazon.com/awscloudtrail/latest/userguide/send-cloudtrail-events-to-cloudwatch-logs.html" rel="noopener noreferrer"&gt;AWS docs&lt;/a&gt;). A metric filter on the canary's ARN, plus an alarm, catches calls in any region, including regions you forget to add rules for. It's slower, because it waits for CloudTrail's delivery to CloudWatch Logs. If you already have a multi-region trail, add the CloudWatch Logs settings to it instead of creating a second one. CloudTrail bills additional copies of management events (&lt;a href="https://aws.amazon.com/cloudtrail/pricing/" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;locals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;trail_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"management-events"&lt;/span&gt;
  &lt;span class="nx"&gt;trail_arn&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"arn:aws:cloudtrail:us-east-1:${local.account_id}:trail/${local.trail_name}"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket"&lt;/span&gt; &lt;span class="s2"&gt;"trail"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bucket_prefix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"audit-logs-"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket_public_access_block"&lt;/span&gt; &lt;span class="s2"&gt;"trail"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt;                  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;trail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;block_public_acls&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;block_public_policy&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;ignore_public_acls&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;restrict_public_buckets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_policy_document"&lt;/span&gt; &lt;span class="s2"&gt;"trail_bucket"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;sid&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AWSCloudTrailAclCheck"&lt;/span&gt;
    &lt;span class="nx"&gt;actions&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"s3:GetBucketAcl"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;trail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;principals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Service"&lt;/span&gt;
      &lt;span class="nx"&gt;identifiers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"cloudtrail.amazonaws.com"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;condition&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;test&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"StringEquals"&lt;/span&gt;
      &lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"aws:SourceArn"&lt;/span&gt;
      &lt;span class="nx"&gt;values&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;trail_arn&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;sid&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AWSCloudTrailWrite"&lt;/span&gt;
    &lt;span class="nx"&gt;actions&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"s3:PutObject"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"${aws_s3_bucket.trail.arn}/AWSLogs/${local.account_id}/*"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;principals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Service"&lt;/span&gt;
      &lt;span class="nx"&gt;identifiers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"cloudtrail.amazonaws.com"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;condition&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;test&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"StringEquals"&lt;/span&gt;
      &lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"s3:x-amz-acl"&lt;/span&gt;
      &lt;span class="nx"&gt;values&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"bucket-owner-full-control"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;condition&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;test&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"StringEquals"&lt;/span&gt;
      &lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"aws:SourceArn"&lt;/span&gt;
      &lt;span class="nx"&gt;values&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;trail_arn&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket_policy"&lt;/span&gt; &lt;span class="s2"&gt;"trail"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;trail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;policy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_iam_policy_document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;trail_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_cloudwatch_log_group"&lt;/span&gt; &lt;span class="s2"&gt;"trail"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;              &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"cloudtrail/management-events"&lt;/span&gt;
  &lt;span class="nx"&gt;retention_in_days&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role"&lt;/span&gt; &lt;span class="s2"&gt;"trail_to_logs"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"cloudtrail-to-cloudwatch-logs"&lt;/span&gt;
  &lt;span class="nx"&gt;assume_role_policy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonencode&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;Version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;
    &lt;span class="nx"&gt;Statement&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
      &lt;span class="nx"&gt;Effect&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
      &lt;span class="nx"&gt;Principal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Service&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"cloudtrail.amazonaws.com"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nx"&gt;Action&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"sts:AssumeRole"&lt;/span&gt;
    &lt;span class="p"&gt;}]&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role_policy"&lt;/span&gt; &lt;span class="s2"&gt;"trail_to_logs"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"write-trail-log-group"&lt;/span&gt;
  &lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;trail_to_logs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;policy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonencode&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;Version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;
    &lt;span class="nx"&gt;Statement&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
      &lt;span class="nx"&gt;Effect&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
      &lt;span class="nx"&gt;Action&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"logs:CreateLogStream"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"logs:PutLogEvents"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="nx"&gt;Resource&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${aws_cloudwatch_log_group.trail.arn}:*"&lt;/span&gt;
    &lt;span class="p"&gt;}]&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_cloudtrail"&lt;/span&gt; &lt;span class="s2"&gt;"main"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;                          &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;trail_name&lt;/span&gt;
  &lt;span class="nx"&gt;s3_bucket_name&lt;/span&gt;                &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;trail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;is_multi_region_trail&lt;/span&gt;         &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;include_global_service_events&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;enable_log_file_validation&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;cloud_watch_logs_group_arn&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${aws_cloudwatch_log_group.trail.arn}:*"&lt;/span&gt;
  &lt;span class="nx"&gt;cloud_watch_logs_role_arn&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;trail_to_logs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;

  &lt;span class="nx"&gt;event_selector&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;read_write_type&lt;/span&gt;           &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"All"&lt;/span&gt; &lt;span class="c1"&gt;# read-only calls like GetCallerIdentity included&lt;/span&gt;
    &lt;span class="nx"&gt;include_management_events&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;depends_on&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_s3_bucket_policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;trail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role_policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;trail_to_logs&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_cloudwatch_log_metric_filter"&lt;/span&gt; &lt;span class="s2"&gt;"canary"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;           &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"backup-reporting-activity"&lt;/span&gt;
  &lt;span class="nx"&gt;log_group_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_cloudwatch_log_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;trail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;pattern&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"{ $.userIdentity.arn = &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;${aws_iam_user.canary.arn}&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; }"&lt;/span&gt;

  &lt;span class="nx"&gt;metric_transformation&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;name&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"BackupReportingActivity"&lt;/span&gt;
    &lt;span class="nx"&gt;namespace&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Ops"&lt;/span&gt;
    &lt;span class="nx"&gt;value&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"1"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_cloudwatch_metric_alarm"&lt;/span&gt; &lt;span class="s2"&gt;"canary"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;alarm_name&lt;/span&gt;          &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"backup-reporting-activity"&lt;/span&gt;
  &lt;span class="nx"&gt;namespace&lt;/span&gt;           &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Ops"&lt;/span&gt;
  &lt;span class="nx"&gt;metric_name&lt;/span&gt;         &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"BackupReportingActivity"&lt;/span&gt;
  &lt;span class="nx"&gt;statistic&lt;/span&gt;           &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Sum"&lt;/span&gt;
  &lt;span class="nx"&gt;period&lt;/span&gt;              &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;
  &lt;span class="nx"&gt;evaluation_periods&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="nx"&gt;threshold&lt;/span&gt;           &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="nx"&gt;comparison_operator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"GreaterThanOrEqualToThreshold"&lt;/span&gt;
  &lt;span class="nx"&gt;treat_missing_data&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"notBreaching"&lt;/span&gt;
  &lt;span class="nx"&gt;alarm_actions&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_sns_topic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alerts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An alarm notifies when it moves into &lt;code&gt;ALARM&lt;/code&gt;, not on every matching event. A second trip while it's still in alarm won't page again, so use the EventBridge path for per-event detail and treat the alarm as the "something happened somewhere" backstop.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;2a: EventBridge rules&lt;/th&gt;
&lt;th&gt;2b: Trail → Logs → metric filter&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Latency&lt;/td&gt;
&lt;td&gt;Doesn't wait for log-file delivery. AWS publishes no figure, so measure it.&lt;/td&gt;
&lt;td&gt;CloudTrail to CloudWatch Logs averages about 5 minutes per AWS (not guaranteed), plus the alarm period.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Region coverage&lt;/td&gt;
&lt;td&gt;Only regions where you deployed a rule&lt;/td&gt;
&lt;td&gt;Every enabled region, through one multi-region trail&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Event detail in the alert&lt;/td&gt;
&lt;td&gt;The full CloudTrail event&lt;/td&gt;
&lt;td&gt;A metric crossing a threshold. Query Logs for detail.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost driver&lt;/td&gt;
&lt;td&gt;Small. Canary events are rare, though cross-region forwarding is billed per event.&lt;/td&gt;
&lt;td&gt;CloudWatch Logs ingests every management event in the account, which can add up in busy accounts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Main failure mode&lt;/td&gt;
&lt;td&gt;A region without a rule, or a rule missing the read-only state&lt;/td&gt;
&lt;td&gt;Slow, and no per-event paging&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you can afford both, run both: 2a for speed, 2b so a missed region can't hide a trip.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Account-ID fingerprinting: which account should own the canary?
&lt;/h2&gt;

&lt;p&gt;Anyone holding an access key ID can find out which AWS account owns it, without ever using the key. &lt;code&gt;sts:GetAccessKeyInfo&lt;/code&gt; returns the account ID for any key ID (&lt;a href="https://docs.aws.amazon.com/STS/latest/APIReference/API_GetAccessKeyInfo.html" rel="noopener noreferrer"&gt;API reference&lt;/a&gt;), and the call is logged only in the &lt;em&gt;caller's&lt;/em&gt; account, not yours (&lt;a href="https://hackingthe.cloud/aws/enumeration/get-account-id-from-keys/" rel="noopener noreferrer"&gt;Hacking the Cloud&lt;/a&gt;). The account ID can also be decoded offline from the key ID itself, a technique published by Aidan Steele and later refined by Tal Be'ery. That's how TruffleHog spots canarytokens.org keys without tripping them.&lt;/p&gt;

&lt;p&gt;So the account a canary lives in is part of the disguise:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;In the same account as the workload.&lt;/strong&gt; The key matches the account ID an attacker sees everywhere else in the environment (ARNs, task metadata), so it looks like it belongs. The cost is a deny-all IAM user in production. It's harmless, but it's one more principal to explain to auditors.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In a dedicated canary account.&lt;/strong&gt; This keeps production clean and makes the canary easy to manage centrally. The cost is that its account ID matches nothing else the attacker has found. Give the account a believable name in your organisation and don't reuse it for anything that would make it look empty.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In a third party's account&lt;/strong&gt; (canarytokens.org). This is the easiest option, and also the easiest for tools to recognise.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's no universally right answer. Pick one deliberately, and write the choice down next to the Terraform.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Where to plant canaries, and where not to
&lt;/h2&gt;

&lt;p&gt;Good places, where an attacker who got in would look:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CI runners:&lt;/strong&gt; a pipeline variable with a plausible but non-default name, such as &lt;code&gt;BACKUP_AWS_ACCESS_KEY_ID&lt;/code&gt; and &lt;code&gt;BACKUP_AWS_SECRET_ACCESS_KEY&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build boxes and bastions:&lt;/strong&gt; a named profile in &lt;code&gt;~/.aws/credentials&lt;/code&gt;, for example &lt;code&gt;[backup-prod]&lt;/code&gt;. Not &lt;code&gt;[default]&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Containers (ECS, Fargate, Kubernetes):&lt;/strong&gt; extra env vars in the task definition, or a decoy &lt;code&gt;.env&lt;/code&gt; file in the image. Malware routinely dumps the environment, and on Fargate this is the canary with no agent at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secrets stores:&lt;/strong&gt; a decoy secret named like your real ones, holding the canary key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Old internal repos, wikis and runbooks:&lt;/strong&gt; places where leaked keys realistically sit for years.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bad places, which cause false trips or outages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Standard variable names in a workload that uses an AWS SDK.&lt;/strong&gt; The SDK default credential chain reads &lt;code&gt;AWS_ACCESS_KEY_ID&lt;/code&gt; before the container or instance role. You'd silently replace the task's real credentials with a deny-all key, break the service, and trip the canary yourself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A &lt;code&gt;[default]&lt;/code&gt; profile&lt;/strong&gt; on any machine where the CLI, Terraform or an SDK runs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anywhere your own secret scanner verifies keys.&lt;/strong&gt; Scanners that validate AWS keys typically call &lt;code&gt;GetCallerIdentity&lt;/code&gt;. Either exclude the canary locations or recognise your scanner's user agent in the alert.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Public repos,&lt;/strong&gt; unless you want to measure the internet. Public credentials are found and tried by bots, and AWS itself may react to keys exposed publicly. You'll get trips that say nothing about your own environment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backups or images you share with vendors,&lt;/strong&gt; where a legitimate third party may reasonably test what they find.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One key per location, always. When it trips, the key is the address.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Testing it safely
&lt;/h2&gt;

&lt;p&gt;Tell whoever is on call first. Run the test from a machine whose IP you know, in a subshell so the credentials don't leak into your session:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt;
  &lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AWS_ACCESS_KEY_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;terraform output &lt;span class="nt"&gt;-raw&lt;/span&gt; canary_access_key_id&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AWS_SECRET_ACCESS_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;terraform output &lt;span class="nt"&gt;-raw&lt;/span&gt; canary_secret_access_key&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;unset &lt;/span&gt;AWS_SESSION_TOKEN AWS_PROFILE
  &lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; +%H:%M:%S

  &lt;span class="c"&gt;# 1. Read-only, can't be denied: succeeds. Proves the read-only state works.&lt;/span&gt;
  aws sts get-caller-identity &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1

  &lt;span class="c"&gt;# 2. Denied call: fails with AccessDenied, and is still logged.&lt;/span&gt;
  aws s3api list-buckets &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1

  &lt;span class="c"&gt;# 3. A regional endpoint in another region: proves multi-region coverage.&lt;/span&gt;
  aws sts get-caller-identity &lt;span class="nt"&gt;--region&lt;/span&gt; eu-west-1 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--endpoint-url&lt;/span&gt; https://sts.eu-west-1.amazonaws.com
&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should get three alerts, or one alarm for path 2b. Note the time each arrives and compare it with the event's &lt;code&gt;eventTime&lt;/code&gt;. That difference is &lt;em&gt;your&lt;/em&gt; latency, which is the only number worth quoting. If test 1 produces nothing, the rule is missing &lt;code&gt;ENABLED_WITH_ALL_CLOUDTRAIL_MANAGEMENT_EVENTS&lt;/code&gt;. If test 3 produces nothing, eu-west-1 has no rule. To confirm CloudTrail saw the calls regardless of alerting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws cloudtrail lookup-events &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--lookup-attributes&lt;/span&gt; &lt;span class="nv"&gt;AttributeKey&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;AccessKeyId,AttributeValue&lt;span class="o"&gt;=&lt;/span&gt;AKIA...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Event history is regional, so run the lookup with &lt;code&gt;--region eu-west-1&lt;/code&gt; as well to see the third call.&lt;/p&gt;

&lt;p&gt;Repeat the test after every change to regions, trails or alert routing, and on a schedule. A canary that silently stopped alerting is worse than none, because you believe you're covered.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. What to do when it trips
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Read the event:&lt;/strong&gt; &lt;code&gt;eventName&lt;/code&gt;, &lt;code&gt;sourceIPAddress&lt;/code&gt;, &lt;code&gt;userAgent&lt;/code&gt;, &lt;code&gt;awsRegion&lt;/code&gt;. Rule out your own test or your own scanner in under a minute. Everything else is real until proven otherwise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Go to the source, not the key.&lt;/strong&gt; The key tells you where it was planted: that runner, that task definition, that laptop. Whatever could read the canary could also read the real credentials stored next to it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contain that source.&lt;/strong&gt; Stop or isolate the workload (on ECS, stopping the task lets the service replace it from a clean image). Revoke active sessions for the role it ran as, and rotate any real secrets it could reach.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Leave the canary key active for now.&lt;/strong&gt; It can't do anything, and every further call it makes shows you what the attacker is trying. Deactivating it immediately also tells them they've been seen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Afterwards, burn it and plant a fresh one.&lt;/strong&gt; A used canary is a known canary.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Steps 2 and 3 are good candidates for automation, because the signal is certain and the action is reversible and contained. Our &lt;a href="https://beamreach.ai/docs/autonomous-devops.html" rel="noopener noreferrer"&gt;autonomous DevOps guide&lt;/a&gt; sets out that test (verifiable, reversible, contained) in more detail.&lt;/p&gt;

&lt;h2&gt;
  
  
  11. Limits and what we couldn't verify
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;EventBridge latency.&lt;/strong&gt; AWS publishes no figure for CloudTrail-to-EventBridge delivery. Measure it as in section 9.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write-only trails.&lt;/strong&gt; We haven't confirmed whether a trail set to write-only management events still feeds read-only events to EventBridge. Use &lt;code&gt;read_write_type = "All"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unlogged APIs.&lt;/strong&gt; Security researchers have repeatedly found AWS API calls that weren't recorded by CloudTrail and could reveal the caller's identity in an error message. AWS has fixed reported cases, but a careful attacker may still validate a key without tripping it. A canary makes detection very likely, not guaranteed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delivery delays.&lt;/strong&gt; CloudTrail occasionally delivers late. Delayed events carry an &lt;code&gt;addendum&lt;/code&gt; with reason &lt;code&gt;DELIVERY_DELAY&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coverage.&lt;/strong&gt; A canary only covers where it's planted. It tells you nothing about an attacker who never looks there. That's why you keep your other alerts.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  12. FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is an AWS canary token?
&lt;/h3&gt;

&lt;p&gt;An AWS canary token is a real AWS access key that exists only to be stolen. It belongs to an IAM user that is denied every action, so it can't do any harm, but any API call made with it is recorded by CloudTrail and can trigger an alert. Because nobody legitimate uses it, an alert is high-confidence by design. Because each key is planted in exactly one place, the key ID also tells you where it was stolen from.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is an AWS honeytoken the same as a canary credential?
&lt;/h3&gt;

&lt;p&gt;In practice, yes. A honeytoken is any decoy piece of data, and a canary credential is a honeytoken in the form of a credential, wired to alert when used. An AWS honeypot is the broader term and can also mean decoy servers, buckets or databases.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does sts:GetCallerIdentity trip an IAM canary?
&lt;/h3&gt;

&lt;p&gt;It's logged by CloudTrail, and it succeeds even with a deny-all policy because it needs no permissions. Whether it trips your alert depends on the alerting. An EventBridge rule only matches it if the rule's state is ENABLED_WITH_ALL_CLOUDTRAIL_MANAGEMENT_EVENTS (Terraform: the state argument of aws_cloudwatch_event_rule, AWS provider 5.27.0 or later), in the region where the call was made. A multi-region trail feeding a CloudWatch Logs metric filter catches it in any region, more slowly.&lt;/p&gt;

&lt;h3&gt;
  
  
  How quickly will I get the alert?
&lt;/h3&gt;

&lt;p&gt;It depends on the path, and you should measure your own. AWS says CloudTrail typically delivers events to S3 and CloudWatch Logs within an average of about 5 minutes, without a guarantee. canarytokens.org documents 2 to 30 minutes for its AWS key tokens. AWS doesn't publish a latency figure for CloudTrail events reaching EventBridge, so time your own test calls against each event's eventTime.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can a canary key be used to do damage?
&lt;/h3&gt;

&lt;p&gt;Not if it's built as described. The user has no console password and an explicit Deny on every action, and an explicit deny overrides any allow added later. The one thing it can do is call sts:GetCallerIdentity, which reveals the user's ARN and account ID. Choose names and the owning account with that in mind.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should I use canarytokens.org or build my own?
&lt;/h3&gt;

&lt;p&gt;Use canarytokens.org if you want a canary in the next five minutes. It's free and run by Thinkst. Build your own if you need the key to live in an account you control, because the free tokens sit in known Thinkst accounts that secret scanners such as TruffleHog recognise offline. Build your own too if you want alerts in your own pipeline. Thinkst Canary, the commercial product, and Beamreach Compass are the managed options.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I detect leaked AWS keys that aren't canaries?
&lt;/h3&gt;

&lt;p&gt;Enable GuardDuty, which flags credentials used from unusual places, including EC2 instance credentials used outside AWS. Run secret scanning on your repositories and CI logs, review IAM access key last-used data and remove unused keys, and move workloads to short-lived role credentials. Canaries complement this. They catch the act of looking for credentials in places you choose, even when the real keys are never touched.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written by Ilia Dubovskii, founder of Beamreach. AWS behaviours were checked against AWS documentation on 4 October 2026. AWS changes, so if something here no longer matches the docs, the docs win. Tell us and we'll fix this page.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Related: &lt;a href="https://beamreach.ai/honeypot-security.html" rel="noopener noreferrer"&gt;AWS honeypots and canary credentials in Compass&lt;/a&gt; · &lt;a href="https://beamreach.ai/docs/autonomous-devops.html" rel="noopener noreferrer"&gt;Autonomous DevOps&lt;/a&gt; · &lt;a href="https://beamreach.ai/docs/cloud.html" rel="noopener noreferrer"&gt;All guides for cloud teams&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://beamreach.ai/docs/aws-canary-credentials.html" rel="noopener noreferrer"&gt;https://beamreach.ai/docs/aws-canary-credentials.html&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I'm giving a five-minute Ignite on this idea, &lt;em&gt;One Fake Password Is Worth More Than 10,000 Alerts&lt;/em&gt;, at DevOpsDays Barcelona on 14 November 2026. Talk page: &lt;a href="https://beamreach.ai/talks/one-fake-password.html" rel="noopener noreferrer"&gt;https://beamreach.ai/talks/one-fake-password.html&lt;/a&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>security</category>
      <category>terraform</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
