Here’s a finding that looks clean and isn’t:
Service principal
payments-reconciler— Owner: jane.doe. ✅ Governed.
Jane left the company eight months ago. Her account is disabled. The service principal she “owns” has admin-class permissions, a static secret nobody has rotated, and no human who would notice if it started behaving strangely. On the dashboard, it’s green — it has an owner. In reality, it’s exactly the dangling, unowned, over-privileged identity that “Improper Offboarding” (the #1 OWASP Non-Human Identity risk) is about.
I ran into this while using my own open-source NHI scanner against a real tenant, and it exposed something bigger than one bug. Non-human identity assessment is full of checks that confirm a value is present and quietly assume the control is satisfied. Those are not the same thing — and the gap between them is where risk hides in plain sight.
Three of these “presence ≠ validity” traps are worth every security team’s attention.
Trap 1: An owner recorded is not an owner who exists
Every NHI governance framework says the same thing: assign an accountable owner. So tools check: does this identity have an owner? If the field is populated, the box is ticked.
But the point of ownership isn’t the string in the field — it’s that a living human will answer for the identity: rotate its credential, review its access, retire it when the workload is gone. An owner who has left the company satisfies the letter of the control (there’s a value) and violates its entire purpose (no one is accountable).
This is the more dangerous state, not a lesser one. A never-owned identity at least announces itself as ungoverned. A deprovisioned-owner identity actively hides — it passes the ownership check while being, functionally, orphaned. Offboarding happened; the human left; the non-human they were tied to was left behind. That’s the textbook NHI1 scenario, and a presence check sails right past it.
The fix is to treat ownership as a liveness question, not a presence one: resolve the owner, confirm the account is still active, and flag “owner present but deprovisioned” as the high-severity orphan it is.
Trap 2: You cannot offboard what you never measured
The second trap is quieter because it’s an absence. Ask most NHI inventories “when was this identity last used?” and the honest answer is: we don’t collect that. The field is null, so the staleness check never fires, so a population of long-dead identities sits at low risk purely because nothing is watching activity.
On a real tenant, last-used is one of the highest-signal fields there is. Dozens of identities that haven’t authenticated in months aren’t “low risk” — they’re the cleanup list: the fastest, safest risk reduction available, because you can disable them with near-zero blast radius. But only if the assessment actually pulls sign-in activity instead of leaving the field blank and calling the absence “no finding.”
A missing signal is not a passing grade. If your NHI tooling isn’t collecting last-used, it isn’t under-reporting staleness — it’s blind to it.
Trap 3: The privilege your scope check can’t see
The third trap is the most technical. Most tools infer an identity’s privilege from its application permissions — the scopes and API grants it holds. Reasonable, and it catches a lot. But in a directory like Entra, an identity can also be elevated a completely different way: by membership in a directory role — Application Administrator, Privileged Role Administrator, and the like.
An identity can hold almost no app scopes and still be a member of an admin role. Assess it on scopes alone and it reads as scoped — narrow, unremarkable — while it can, in fact, administer the directory. The privilege is real; the assessment just wasn’t looking where it lived.
Least-privilege review has to consider every path to elevation — app scopes and role membership — or it will keep certifying admins as scoped.
The pattern: presence is not a satisfied control
Pull back and the three traps are one mistake wearing three costumes:
- Ownership — a value present (owner string) treated as a control satisfied (accountable human).
- Staleness — a value absent (no last-used) treated as a control passed (not stale).
- Privilege — a value in the obvious place (scopes) treated as the whole picture (all elevation).
Every one of them produces a green result that a security team will trust — and trusting a false green is worse than a red, because it ends the investigation. A scanner that does this doesn’t just miss risk; it trains people to stop looking.
The antidote is to build assessments that ask about reality, not representation: is the owner alive, is the identity actually used, where can it really get elevated? It’s less convenient — it means resolving owners, pulling activity reports, reading role membership — but it’s the difference between a report a CISO can act on and one that quietly launders risk into “governed.”
Why I know these are real
I didn’t find these in a threat model. I found them by pointing my own open-source tool at a real environment and watching it confidently mislabel things — a departed owner as “governed,” a dormant identity as unremarkable, a directory-role admin as “scoped.” Each was a place my own risk logic was checking presence and calling it validity.
So I fixed all three (they’re in the latest release): owner liveness, native staleness from sign-in activity, and directory-role privilege — plus choosing a live human owner over a stale one when an identity has several. The tool is sharper for it, and so is my sense of where NHI programs fool themselves.
That’s the argument for building in the open and running your tools against reality: the environment tells you the truth about your assumptions. For non-human identity specifically, the most important assumption to keep testing is the quiet one behind every green checkmark — that a value being present means the control is actually doing its job.
nhi-scan is open source (MIT): github.com/rpmsft9/nhi-scan. It risk-tiers non-human & agent identities against the OWASP NHI Top 10 — now with owner-liveness, native staleness, and directory-role-aware privilege.
¶ Discussion
Comments are powered by Giscus / GitHub Discussions. They appear here once configured — see
Configure Giscusin the project README and updateGISCUSinsrc/consts.ts.