All writing

Report-Only First: What Writing Identity Controls as Code Taught Me

Notes from building an Entra ID hardening lab in Terraform: why identity is where I'd start, why every Conditional Access policy should start in report-only mode, and why 'it validates' isn't the same as 'it works.'

  • Cloud Security
  • Leadership

I've been studying for Microsoft's security architect and cloud security exams, and I learn best by building something. So instead of only reading about Conditional Access and Privileged Identity Management, I put together a small lab: azure-entra-identity-hardening, a baseline set of Microsoft Entra ID identity controls written in Terraform.

I want to be upfront about where it stands. The code passes formatting, validation, linting, a policy scan, and a secret scan on my machine. It has not been applied to a tenant yet. That gap turned out to be one of the more useful things the project taught me, and I'll come back to it.

Why I started with identity

In a cloud environment, identity is the control plane. Whoever holds a privileged account can change almost everything else. A lot of cloud compromises don't start with anything exotic. They start with a stolen or phished credential and get worse through admin rights that are always on and that nobody reviews.

That's the same lesson I took from my capstone on water utilities: the damage usually comes from basics that weren't in place. In the cloud, the basics live in identity. So the lab covers a short list:

  • MFA for every user
  • Blocking legacy authentication, which can't do modern MFA
  • Phishing-resistant MFA and a four-hour sign-in frequency for admin roles
  • MFA for Azure management
  • Blocking device code flow, a sign-in method that's easy to abuse with phishing
  • Optional sign-in risk and user risk policies for tenants with the higher license tier

None of that is new. Microsoft documents all of it. The part I wanted to practice was rolling it out without breaking anything.

Why I'd write it as code

Most identity configuration I've seen discussed gets done by clicking through an admin portal. That works until you need to answer simple questions. Who changed this policy? Why is this person excluded? What did it look like last month?

Writing the controls in Terraform turns those into normal engineering questions. Every change can go through a pull request someone reviews. The history is in Git. If a change goes wrong, there's a known-good version to roll back to. Every pull request runs the same checks automatically, so a change doesn't skip the checks just because someone was in a hurry.

It also forces decisions into the open. It's much harder to quietly add an exclusion when it shows up in a diff with your name on it.

Report-only first

This is the habit I'd bring into any security program.

Conditional Access has a report-only mode. The policy evaluates each sign-in it applies to and logs what it would have done, but it doesn't block anyone. In my lab, every policy deploys in report-only by default. The rollout plan is to let normal sign-ins run for at least a few days, query the logs to see who would have been blocked, test specific accounts with Microsoft's What If tool, and only then switch the policies on.

The reason is simple. A security control that locks out the finance team on a Monday morning doesn't stay in place for long. People lose trust in the security team, and the exceptions start piling up. Measuring the impact first lets you find the forgotten service account or the old mail client before it turns into an outage.

I think about it the same way I think about leading people through any change. You don't announce a new standard and walk away. You find out what it will actually do to the people living under it, fix what you can, and then enforce it.

Never lock yourself out

The other rule in the lab is that every policy excludes a break-glass group. Break-glass accounts are emergency admin accounts, kept for the day something goes wrong with your normal sign-in path, like a bad policy or an outage at your MFA provider.

I made one deliberate choice here. Terraform manages the exclusion group, but the break-glass accounts themselves are created by hand. That keeps their credentials out of Terraform state entirely. Since those accounts skip the policies everyone else lives under, the lab also sends Entra sign-in logs to a Log Analytics workspace and raises an alert any time a break-glass account signs in. An emergency account nobody is watching is just a back door.

Standing access is the real risk

The last piece is privilege. In the lab, security operators don't hold privileged roles all the time. Through PIM, they're eligible for a role and activate it when they need it. VM operators get a PIM eligibility that expires after 30 days, for a custom role scoped to one resource group that lets them start and stop (deallocate) machines but not delete them.

Least privilege is easy to agree with and hard to keep. Writing it as code at least makes the current state visible and puts any change to it through review.

"It validates" is not "it works"

Back to that gap. terraform validate checks syntax and provider schemas. It doesn't prove a single policy behaves the way I intended. It can't tell me whether the admin policy targets the right roles or whether the break-glass alert actually fires.

So the repo spells out what's still unproven. It lists the validation steps to run in a real lab tenant and says to record actual results before calling the lab done. I'd rather publish a clear "not deployed yet" than imply something works because the pipeline is green.

I think that's a leadership habit as much as an engineering one. Separate what you've verified from what you expect. Say which is which. People can work with an honest status. They can't work with a confident guess.

What's next

The next step is to deploy this in a dedicated lab tenant, run through the validation checklist, and write up what I actually observe, including whatever breaks. When I do, I'll post the results here.

In the meantime, I wrote the other half: what to do once an identity is already compromised. The Entra ID account-compromise IR runbook has the response steps, KQL queries, and a tabletop. Like this lab, its queries haven't been run against a tenant yet.

If you're building out identity controls yourself, my advice is the same as the lab's rollout order: start in report-only, protect your emergency access, watch it closely, and only enforce what you've measured.