Insight

When an agent misbehaves, the system pulls its badge Gdy agent źle się zachowuje, system odbiera mu identyfikator

Least privilege caps how much damage a compromised agent can do — it doesn't stop it from trying, over and over, between audits. So I added a small sentinel that reads the cloud's own audit log, spots a burst of refusals from one identity, and revokes that agent at the identity provider. Najmniejsze uprawnienia ograniczają, ile szkód skompromitowany agent może zrobić — nie powstrzymują go przed próbowaniem, w kółko, między audytami. Dodałem więc małego strażnika, który czyta log audytowy chmury, wychwytuje serię odmów od jednej tożsamości i odbiera temu agentowi dostęp u dostawcy tożsamości.

The signal

A burst of permission denied from a single identity — five or more in fifteen minutes. The signature of an agent hammering things it isn't allowed to touch.

The sentinel

A keyless watcher with read-only access to the audit dataset. One SQL query to detect; one Okta API call to revoke. It observes — it never writes the data plane.

The result

The offending agent's Okta app is deactivated. It cannot get a new token, and the one in its hand dies within minutes.

This is the third step in a small lab I built to learn about AI agent identity and access. The story so far: each agent got its own ID badge from Okta with no static keys, and IAM held it to least privilege (the first article). Then that identity learned to carry a second fact — who it is acting for — so every action was tagged "acting for Maya" (the second).

Least privilege is good, but it has a limit. It caps how much damage a compromised or prompt-injected agent can do; it does not stop it from trying, over and over, in the weeks between audits. I wanted the system to notice a misbehaving agent and contain it, quickly.

This step only works because of the last one — detection needs something to attribute the bad behaviour to.

The idea: a sentinel that reads the audit log

From the very first article, every action lands in Cloud Audit Logs and is exported to BigQuery. So I added a small sentinel with one job: read that audit log for a burst of "permission denied" from a single identity — a compromised, injected, or simply broken agent — and then revoke that agent's identity at Okta.

Two design choices I care about:

  • It reads what IAM actually denied — not the agent's own account of itself. You cannot trust a compromised agent to report that it's compromised; you can trust the cloud's record of what it refused.
  • Detection is keyless. The sentinel is just another federated identity (Okta → Workload Identity Federation), with read-only access to the audit dataset. It observes; it never writes the data plane.

The response — deactivating the agent's Okta app — uses an Okta admin token. That is a privileged management credential, held by the sentinel, distinct from any agent's credential. I'm calling it out plainly rather than hiding it.

Under the hood

The sentinel is deliberately small: a couple hundred lines of Python and a page of Terraform. Two commands drive it — make watch detects and prints (a dry run), make respond detects and revokes. Building it was three steps, in dependency order:

  1. Give the watcher its own identity first. Before any detection code existed, Terraform created a sentinel app in Okta and an sa-sentinel service account in GCP, joined through the same Workload Identity Federation pool the agents use. The watcher plays by the same rules as the watched: no keys, least privilege — it can run BigQuery queries and read the audit dataset, and that is all it can do.
  2. Detection is one SQL query. The audit sink already stores every data-access event with the caller's identity attached. The query groups those events by identity, keeps only the ones IAM refused (status.code = 7, PERMISSION_DENIED), and flags any identity with five or more denials in the last fifteen minutes. Both numbers are tunable — and both are honest thresholds, not machine learning.
  3. Response is one API call per flagged identity. A small config map says which Okta app belongs to which service account; the sentinel calls Okta's app-deactivate endpoint on the match. Two guardrails are baked in: an identity not in the map is skipped and reported, never guessed at — and the sentinel's own app sits on a protect list, so it can never revoke itself.

One development choice paid off immediately. All the decision logic — the query builder, the flagging rule, the identity→app mapping — is pure functions with no side effects, and the three bits of I/O (fetch a token, run the query, call Okta) are isolated at the edges. So the whole brain of the sentinel was unit-tested with zero credentials, in CI, before it ever touched real infrastructure.

Trust the record, not the actor. Ask the cloud what it denied; don't ask the possibly-compromised agent how it's doing.

The test: a compromised agent, caught and cut off

I simulated a compromised reader-agent hammering writes it has no permission for. Each attempt is denied by IAM and logged. Then I run the sentinel. First, detect:

================================================================================
Agent Identity Lab — sentinel: PERMISSION_DENIED bursts (>= 5 in 15 min)
================================================================================
PRINCIPAL                                            DENIALS
--------------------------------------------------------------------------------
sa-reader-agent@ai-agent-identity-lab...              6   (last 2026-07-10 15:05 UTC)
================================================================================
dry run: detected only. Re-run with --respond to revoke.

The sentinel flagged reader-agent: six denials, over the threshold. Now respond — and check whether the revoked agent can still get in:

responding — deactivating Okta apps at the IdP:
  REVOKED sa-reader-agent  →  deactivated Okta app "reader-agent"
  (no new tokens; any existing token dies within its ~5-min lifetime)

# verify: can reader-agent still get a token?
reader-agent token DENIED — Okta 401 invalid_client. The badge is gone.

That is the whole loop, on real infrastructure: a burst of denials → detected from the audit log → the agent's identity deactivated at the identity provider → the agent can no longer get a token. And because the lab uses short-lived tokens, even a token already in hand dies within minutes. Revocation and its effect are close together.

BigQuery audit query showing sa-reader-agent rows with status_code 7
The audit sink in BigQuery — sa-reader-agent rows with status_code = 7 (PERMISSION_DENIED). This is the raw evidence the sentinel keys on.
BigQuery console with sa-sentinel running the detection query
The sentinel itself, keyless: sa-sentinel running the detection queries against the audit dataset, read-only. It observes what IAM denied — it never trusts the agent.
Okta Applications list showing reader-agent as inactive
After make respond: the reader-agent app is now Inactive — five active, one inactive. Its badge is revoked at the identity provider.
Okta System Log showing a Deactivate application event
The same event in Okta's System Log — a "Deactivate application" on reader-agent. Note the actor is the admin user: the sentinel holds that admin token to do the revoke. The honest caveat, visible right here.

Being honest about the limits

  • Detection is not instant. The audit logs land in BigQuery seconds to about a minute after the action. I state that rather than pretend it's real-time.
  • It's a threshold, not clever anomaly detection. "Many denials, fast" is a crude but honest signal; behavioural detection is a whole other project.
  • Revocation is all-or-nothing. It pulls the whole identity — a blunt hammer, which is exactly the thread to the next step.
  • The response uses an admin token. A keyless, cloud-side variant is possible; I left it as an open thread.

You could build this on a completely different stack

Nothing about the pattern is Okta- or Google-specific. Strip away the vendor names and the loop has four roles, and every major stack can play each one:

  • An identity provider that owns the non-human identities (here: Okta). Microsoft Entra ID workload identities, Auth0, or Ping do the same job — Keycloak if you want it open-source and self-hosted, SPIFFE/SPIRE if you want the standards-track answer to workload identity.
  • A cloud that enforces least privilege and records what it denied (here: GCP IAM + Cloud Audit Logs). On AWS that's IAM plus CloudTrail; on Azure, RBAC plus the activity logs. The keyless federation step exists everywhere too: AWS has OIDC identity providers with AssumeRoleWithWebIdentity, Azure has its own workload identity federation.
  • Somewhere queryable to watch the trail (here: a BigQuery sink). On AWS you'd query CloudTrail with Athena, or have EventBridge rules fire on the denial events directly; on Azure, Log Analytics with KQL. Or skip the DIY entirely and use a SIEM — Splunk, Elastic, Google SecOps, or Microsoft Sentinel (yes, the name collision is real; theirs is the grown-up version of my couple hundred lines).
  • Something with authority to revoke at the identity provider (here: a Python script holding an Okta admin token). In production this is where SOAR playbooks live — Microsoft Sentinel playbooks and Logic Apps, EventBridge plus Lambda, or dedicated tools like Tines and Torq.

The load-bearing ideas travel unchanged: deny by default, trust the audit record over the actor, revoke at the source of identity, keep tokens short-lived. The vendor names are just the casting.

What I learned

  • Trust the record, not the actor. Ask the cloud what it denied; don't ask the possibly-compromised agent how it's doing.
  • Revoke at the source of identity. Deactivating the app at Okta stops new tokens everywhere at once; short lifetimes close the gap.
  • Least privilege and detection are partners. One limits the blast radius; the other shortens how long it lasts.
  • Attribution is the precondition. None of this works until every action carries an identity — which is exactly what the previous two steps built.

And the thread onward: this revoke is all-or-nothing — it nukes the whole agent. The next step is access that bends to context, so I could deny one bad action, or one sensitive data class, without pulling the agent's entire badge. That's the next phase.

This is still a private learning lab, not a product. If you work on non-human identity, detection and response, or agent security, I'd genuinely like your critique — including the sharp kind.

—A Skynarc learning project on non-human identity.

Sygnał

Seria odmów dostępu od jednej tożsamości — pięć lub więcej w piętnaście minut. Sygnatura agenta, który wali w rzeczy, do których nie ma prawa.

Strażnik

Bezkluczowy obserwator z dostępem tylko do odczytu zbioru audytowego. Jedno zapytanie SQL, żeby wykryć; jedno wywołanie API Okty, żeby odebrać. Obserwuje — nigdy nie zapisuje warstwy danych.

Wynik

Aplikacja winnego agenta w Okcie zostaje zdezaktywowana. Nie zdobędzie nowego tokenu, a ten, który ma w ręku, umiera w ciągu minut.

To trzeci krok w małym laboratorium, które zbudowałem, żeby nauczyć się o tożsamości i dostępie agentów AI. Historia dotychczas: każdy agent dostał własny identyfikator z Okty, bez statycznych kluczy, a IAM ograniczał go do najmniejszych uprawnień (pierwszy artykuł). Potem ta tożsamość nauczyła się nieść drugi fakt — w czyim imieniu działa — więc każda akcja była oznaczona „działa w imieniu Mai" (drugi artykuł).

Najmniejsze uprawnienia są dobre, ale mają granicę. Ograniczają, ile szkód skompromitowany albo zmanipulowany (prompt injection) agent może zrobić; nie powstrzymują go przed próbowaniem, w kółko, przez tygodnie między audytami. Chciałem, żeby system zauważył źle zachowującego się agenta i szybko go powstrzymał.

Ten krok działa tylko dzięki poprzedniemu — wykrywanie potrzebuje czegoś, czemu można przypisać złe zachowanie.

Pomysł: strażnik, który czyta log audytowy

Od pierwszego artykułu każda akcja trafia do Cloud Audit Logs i jest eksportowana do BigQuery. Dodałem więc małego strażnika (sentinel) z jednym zadaniem: czytać ten log audytowy w poszukiwaniu serii „odmów dostępu" od jednej tożsamości — skompromitowanego, zmanipulowanego albo po prostu zepsutego agenta — a potem odebrać tożsamość tego agenta w Okcie.

Dwie decyzje projektowe, na których mi zależy:

  • Czyta to, czego IAM faktycznie odmówił — nie własną relację agenta o sobie. Nie można ufać skompromitowanemu agentowi, że zgłosi, że jest skompromitowany; można ufać zapisowi chmury o tym, czego odmówiła.
  • Wykrywanie jest bezkluczowe. Strażnik to po prostu kolejna federowana tożsamość (Okta → Workload Identity Federation), z dostępem tylko do odczytu zbioru audytowego. Obserwuje; nigdy nie zapisuje warstwy danych.

Reakcja — dezaktywacja aplikacji agenta w Okcie — używa tokenu administratora Okty. To uprzywilejowane poświadczenie zarządcze, trzymane przez strażnika, odrębne od poświadczeń agentów. Mówię o tym wprost, zamiast to ukrywać.

Pod maską

Strażnik jest celowo mały: kilkaset linii Pythona i jedna strona Terraforma. Sterują nim dwie komendy — make watch wykrywa i wypisuje (na sucho), make respond wykrywa i odbiera. Budowa to były trzy kroki, w kolejności zależności:

  1. Najpierw daj strażnikowi własną tożsamość. Zanim powstała jakakolwiek linia kodu wykrywającego, Terraform utworzył aplikację sentinel w Okcie i konto serwisowe sa-sentinel w GCP, połączone przez tę samą pulę Workload Identity Federation, której używają agenci. Obserwujący gra według tych samych reguł co obserwowani: żadnych kluczy, najmniejsze uprawnienia — może uruchamiać zapytania BigQuery i czytać zbiór audytowy, i to wszystko.
  2. Wykrywanie to jedno zapytanie SQL. Sink audytowy już przechowuje każde zdarzenie dostępu do danych z dołączoną tożsamością wywołującego. Zapytanie grupuje te zdarzenia po tożsamości, zostawia tylko te, których IAM odmówił (status.code = 7, PERMISSION_DENIED), i oznacza każdą tożsamość z pięcioma lub więcej odmowami w ostatnich piętnastu minutach. Obie liczby są konfigurowalne — i obie to szczere progi, nie uczenie maszynowe.
  3. Reakcja to jedno wywołanie API na oznaczoną tożsamość. Mała mapa konfiguracyjna mówi, która aplikacja Okty należy do którego konta serwisowego; strażnik wywołuje endpoint dezaktywacji aplikacji na dopasowaniu. Wbudowane są dwa zabezpieczenia: tożsamość, której nie ma w mapie, jest pomijana i raportowana, nigdy zgadywana — a własna aplikacja strażnika jest na liście chronionych, więc nigdy nie może odebrać dostępu sam sobie.

Jedna decyzja deweloperska opłaciła się natychmiast. Cała logika decyzyjna — budowanie zapytania, reguła oznaczania, mapowanie tożsamość→aplikacja — to czyste funkcje bez efektów ubocznych, a trzy kawałki I/O (pobierz token, uruchom zapytanie, wywołaj Oktę) są odizolowane na brzegach. Dzięki temu cały mózg strażnika został przetestowany jednostkowo bez żadnych poświadczeń, w CI, zanim kiedykolwiek dotknął prawdziwej infrastruktury.

Ufaj zapisowi, nie aktorowi. Zapytaj chmurę, czego odmówiła; nie pytaj (być może skompromitowanego) agenta, jak się miewa.

Test: skompromitowany agent, złapany i odcięty

Zasymulowałem skompromitowanego reader-agenta walącego w zapisy, do których nie ma uprawnień. Każda próba jest odrzucana przez IAM i logowana. Potem uruchamiam strażnika. Najpierw wykrycie:

================================================================================
Agent Identity Lab — sentinel: PERMISSION_DENIED bursts (>= 5 in 15 min)
================================================================================
PRINCIPAL                                            DENIALS
--------------------------------------------------------------------------------
sa-reader-agent@ai-agent-identity-lab...              6   (last 2026-07-10 15:05 UTC)
================================================================================
dry run: detected only. Re-run with --respond to revoke.

Strażnik oznaczył reader-agenta: sześć odmów, powyżej progu. Teraz reakcja — i sprawdzenie, czy odebrany agent nadal może wejść:

responding — deactivating Okta apps at the IdP:
  REVOKED sa-reader-agent  →  deactivated Okta app "reader-agent"
  (no new tokens; any existing token dies within its ~5-min lifetime)

# verify: can reader-agent still get a token?
reader-agent token DENIED — Okta 401 invalid_client. The badge is gone.

To cały cykl, na prawdziwej infrastrukturze: seria odmów → wykryta z logu audytowego → tożsamość agenta zdezaktywowana u dostawcy tożsamości → agent nie może już zdobyć tokenu. A ponieważ laboratorium używa krótkożyciowych tokenów, nawet token już posiadany umiera w ciągu minut. Odebranie i jego skutek są blisko siebie.

Zapytanie audytowe BigQuery — wiersze sa-reader-agent ze status_code 7
Sink audytowy w BigQuery — wiersze sa-reader-agent ze status_code = 7 (PERMISSION_DENIED). To surowy dowód, na którym opiera się strażnik.
Konsola BigQuery — sa-sentinel uruchamiający zapytanie wykrywające
Sam strażnik, bezkluczowo: sa-sentinel uruchamiający zapytania wykrywające na zbiorze audytowym, tylko do odczytu. Obserwuje, czego odmówił IAM — nigdy nie ufa agentowi.
Lista aplikacji Okta — reader-agent jako nieaktywna
Po make respond: aplikacja reader-agent jest teraz Inactive — pięć aktywnych, jedna nieaktywna. Jej identyfikator został odebrany u dostawcy tożsamości.
System Log Okty — zdarzenie Deactivate application
To samo zdarzenie w System Log Okty — „Deactivate application" na reader-agent. Zwróć uwagę, że aktorem jest użytkownik-administrator: strażnik trzyma ten token administratora, żeby wykonać odebranie. Szczere zastrzeżenie, widoczne tutaj.

Szczerze o ograniczeniach

  • Wykrywanie nie jest natychmiastowe. Logi audytowe trafiają do BigQuery od sekund do około minuty po akcji. Mówię to, zamiast udawać, że to czas rzeczywisty.
  • To próg, nie sprytne wykrywanie anomalii. „Dużo odmów, szybko" to prymitywny, ale szczery sygnał; wykrywanie behawioralne to zupełnie inny projekt.
  • Odebranie jest „wszystko albo nic". Odbiera całą tożsamość — tępy młot, co jest dokładnie nicią do następnego kroku.
  • Reakcja używa tokenu administratora. Bezkluczowy wariant po stronie chmury jest możliwy; zostawiłem go jako otwarty wątek.

Można to zbudować na zupełnie innym stosie

Nic w tym wzorcu nie jest specyficzne dla Okty ani Google'a. Zdejmij nazwy dostawców, a cykl ma cztery role — i każdy większy stos może zagrać każdą z nich:

  • Dostawca tożsamości, który jest właścicielem tożsamości nieludzkich (tutaj: Okta). Microsoft Entra ID workload identities, Auth0 albo Ping robią tę samą robotę — Keycloak, jeśli chcesz open source i self-hosting, SPIFFE/SPIRE, jeśli chcesz standardową odpowiedź na tożsamość workloadów.
  • Chmura, która egzekwuje najmniejsze uprawnienia i zapisuje, czego odmówiła (tutaj: GCP IAM + Cloud Audit Logs). W AWS to IAM plus CloudTrail; w Azure — RBAC plus logi aktywności. Krok bezkluczowej federacji też istnieje wszędzie: AWS ma dostawców tożsamości OIDC z AssumeRoleWithWebIdentity, Azure ma własną federację tożsamości workloadów.
  • Coś przeszukiwalnego do obserwowania śladu (tutaj: sink w BigQuery). W AWS odpytywałbyś CloudTrail Atheną albo reguły EventBridge odpalałyby się bezpośrednio na zdarzeniach odmowy; w Azure — Log Analytics z KQL. Albo pomiń majsterkowanie i użyj SIEM-a — Splunk, Elastic, Google SecOps albo Microsoft Sentinel (tak, zbieżność nazw jest prawdziwa; ich wersja to dorosła wersja moich kilkuset linii).
  • Coś z uprawnieniem do odbierania u dostawcy tożsamości (tutaj: skrypt w Pythonie z tokenem administratora Okty). W produkcji to miejsce na playbooki SOAR — playbooki Microsoft Sentinel i Logic Apps, EventBridge plus Lambda albo dedykowane narzędzia jak Tines i Torq.

Nośne idee podróżują bez zmian: odmawiaj domyślnie, ufaj zapisowi audytowemu, nie aktorowi, odbieraj u źródła tożsamości, trzymaj tokeny krótkożyciowe. Nazwy dostawców to tylko obsada.

Czego się nauczyłem

  • Ufaj zapisowi, nie aktorowi. Zapytaj chmurę, czego odmówiła; nie pytaj (być może skompromitowanego) agenta, jak się miewa.
  • Odbieraj u źródła tożsamości. Dezaktywacja aplikacji w Okcie zatrzymuje nowe tokeny wszędzie naraz; krótkie czasy życia zamykają lukę.
  • Najmniejsze uprawnienia i wykrywanie to partnerzy. Jedno ogranicza promień rażenia; drugie skraca, jak długo trwa.
  • Atrybucja to warunek konieczny. Nic z tego nie działa, dopóki każda akcja nie niesie tożsamości — a to dokładnie zbudowały dwa poprzednie kroki.

A oto nić dalej: to odebranie jest „wszystko albo nic" — niszczy całego agenta. Następny krok to dostęp, który gnie się pod kontekst, żebym mógł odmówić jednej złej akcji albo jednej wrażliwej klasy danych, bez odbierania całego identyfikatora agenta. To następna faza.

To wciąż prywatne laboratorium do nauki, nie produkt. Jeśli pracujesz z tożsamościami nieludzkimi, wykrywaniem i reakcją albo bezpieczeństwem agentów, naprawdę chciałbym poznać Twoją krytykę — także tę ostrą.

—Projekt edukacyjny Skynarc o tożsamości nieludzkiej.

← Back to Insights