Deny-by-default device trust in Okta, with nobody locked out

Moving 142 apps to a policy that blocks unmanaged devices is a one-line change in Okta and a multi-week change for the people who use it. The order of operations is what kept it uneventful.

Okta has supported device assurance and "managed device required" policies for years, and most tenants I have seen still run them in the softest possible mode: a handful of sensitive apps require a managed device, everything else lets anyone in with a password and a push. The gap is understandable. Flipping the default to deny means that the first time the policy is wrong, somebody is locked out of their email in the middle of a customer call, and the whole project gets rolled back by the end of the day.

Earlier this year I moved our tenant to deny-by-default across all 142 apps. No user lost access. That was not luck, and it was not because the tenant was small. It came from treating the policy change as the last step of the project rather than the first.

Know what "managed" means before you enforce it

Device trust policies only work if the signal they depend on is reliable. In our case the signal is the Okta Verify registration on a device that our MDM has enrolled and is reporting on. Before touching any policy I pulled every device record out of Okta and every enrolled device out of the MDM and joined them. The mismatches fall into predictable buckets: people with a second laptop that was never enrolled, contractors on personal machines, a few devices where Okta Verify had been installed but never registered, and stale records for hardware that had been returned months ago.

Each of those buckets needs a decision, not a policy. Contractors got a documented exception group with an expiry. Second laptops either got enrolled or were cut off ahead of time with a conversation. Stale records got cleaned up. Only once the join was clean did the policy stand a chance of being right.

Put the policy in Terraform first

I had already imported the tenant into Terraform: groups, group rules, SAML and OIDC apps, authenticators, and the authentication policies themselves, all at zero diff. That turned out to be the most important preparation for this project, for three reasons.

Roll out in the order of least regret

The apps went over in waves. The first wave was internal tools that IT owned and used daily, so that any mistake would hit me first. The next waves were grouped by how bad a false denial would be and how many people it would touch. Email and chat went last, because they are the apps where a lockout is a crisis and also the ones where every other app has already proven that the device signal is correct for every user.

Between waves I watched the System Log for policy denials. A denial from a user who should have had access was treated as a bug in the inventory, not as a reason to loosen the policy. There were a few, and every one of them traced back to a device that had slipped through the join. Fix the record, retry the sign-in, move on.

The rule I kept coming back to: if the policy denies someone, the data was wrong somewhere upstream. Fix the data. Do not add an exception to the policy, because exceptions never get removed.

What deny-by-default changed for us

The obvious result is that a stolen password alone no longer gets anyone into a company app from a device we do not manage. The less obvious result is that the policy now enforces the inventory. A device that is not enrolled is not a device that can reach anything, so enrollment is no longer something IT has to chase. People show up with the laptop and get it enrolled because that is the only way their day starts.

It also made the next projects easier. Just-in-time admin elevation, which I will write about separately, assumes that an admin session is coming from a managed device. The dormant-account sweep assumes that an account with no managed device registered is probably not in use. Both of those depend on the device signal being trustworthy, and it is trustworthy because the policy forces it to be.

If you are about to do this

  1. Join your identity provider's device records against your MDM's and resolve every mismatch before you write a policy.
  2. Get the policies into code so the change is reviewable and the rollback is a revert.
  3. Move apps in waves, starting with the ones where a mistake hurts only you.
  4. Treat every unexpected denial as a data bug, not a policy bug.
  5. Put email and chat last. By then the signal has proven itself on everything else.