Integrating Linux with Okta Device Trust

Post Syndicated from original https://mjg59.dreamwidth.org/64311.html

I’ve written about bearer tokens and how much pain they cause me before, but sadly wishing for a better world doesn’t make it happen so I’m making do with what’s available. Okta has a feature called Device Trust which allows to you configure access control policies that prevent people obtaining tokens unless they’re using a trusted device. This doesn’t actually bind the tokens to the hardware in any way, so if a device is compromised or if a user is untrustworthy this doesn’t prevent the token ending up on an unmonitored system with no security policies. But it’s an incremental improvement, other than the fact that for desktop it’s only supported on Windows and MacOS, which really doesn’t line up well with my interests.

Obviously there’s nothing fundamentally magic about these platforms, so it seemed fairly likely that it would be possible to make this work elsewhere. I spent a while staring at the implementation using Charles Proxy and the Chrome developer tools network tab and had worked out a lot, and then Okta published a paper describing a lot of what I’d just laboriously figured out. But it did also help clear up some points of confusion and clarified some design choices. I’m not going to give a full description of the details (with luck there’ll be code shared for that before too long), but here’s an outline of how all of this works. Also, to be clear, I’m only going to talk about the desktop support here – mobile is a bunch of related but distinct things that I haven’t looked at in detail yet.

Okta’s Device Trust (as officially supported) relies on Okta Verify, a local agent. When initially installed, Verify authenticates as the user, obtains a token with a scope that allows it to manage devices, and then registers the user’s computer as an additional MFA factor. This involves it generating a JWT that embeds a number of custom claims about the device and its state, including things like the serial number. This JWT is signed with a locally generated (and hardware-backed, using a TPM or Secure Enclave) key, which allows Okta to determine that any future updates from a device claiming the same identity are genuinely from the same device (you could construct an update with a spoofed serial number, but you can’t copy the key out of a TPM so you can’t sign it appropriately). This is sufficient to get a device registered with Okta, at which point it can be used with Fastpass, Okta’s hardware-backed MFA mechanism.

As outlined in the aforementioned deep dive paper, Fastpass is implemented via multiple mechanisms. I’m going to focus on the loopback one, since it’s the one that has the strongest security properties. In this mode, Verify listens on one of a list of 10 or so ports on localhost. When you hit the Okta signin widget, choosing Fastpass triggers the widget into hitting each of these ports in turn until it finds one that speaks Fastpass and then submits a challenge to it (along with the URL that’s making the request). Verify then constructs a response that includes the challenge and signs it with the hardware-backed key, along with information about whether this was done automatically or whether it included forcing the user to prove their presence. Verify then submits this back to Okta, and if that checks out Okta completes the authentication.

Doing this via loopback from the browser has a bunch of nice properties, primarily around the browser providing information about which site triggered the request. This means the Verify agent can make a decision about whether to submit something there (ie, if a fake login widget requests your creds, the agent will ignore it), and also allows the issued token to be cross-checked against the site that requested it (eg, if g1thub.com requests a token that’s valid for github.com, that’s a red flag). It’s not quite at the same level as a hardware WebAuthn token, but it has many of the anti-phishing properties.

But none of this actually validates the device identity! The entire registration process is up to the client, and clients are in a position to lie. Someone could simply reimplement Verify to lie about, say, a device serial number when registering, and there’d be no proof to the contrary. Thankfully there’s another level to this to provide stronger assurances. Okta allows you to provide a CA root[1]. When Okta issues a Fastpass challenge to a device the challenge includes a list of the trusted CAs. If a client has a certificate that chains back to that, it can embed an additional JWT in the auth JWT, this one containing the certificate and signed with the certificate’s private key. This binds the CA-issued identity to the Fastpass validation, and causes the device to start appearing as “Managed” in the Okta device management UI. At that point you can configure policy to restrict various apps to managed devices, ensuring that users are only able to get tokens if they’re using a device you’ve previously issued a certificate to.

I’ve managed to get Linux tooling working with this, though there’s still a few drawbacks. The main issue is that the API only allows you to register devices that declare themselves as Windows or MacOS, followed by the login system sniffing browser user agent and only offering Fastpass if you’re on one of the officially supported platforms. This can be worked around with an extension that spoofs user agent specifically on the login page, but that’s still going to result in devices being logged as a non-Linux OS which makes interpreting the logs more difficult. There’s also no ability to choose which bits of device state you log: there’s a couple of existing integrations, and otherwise a fixed set of parameters that are reported. It’d be lovely to be able to log arbitrary material and make policy decisions based on that.

This also doesn’t help with ChromeOS. There’s no real way to automatically launch something that’s bound to localhost (you could probably make this work using Crostini but there’s no way to launch a Crostini app at login), and access to hardware-backed keys is kind of a complicated topic in ChromeOS for privacy reasons. I haven’t tried this yet, but I think using an enterprise force-installed extension and the chrome.enterprise.platformKeys API to obtain a device identity cert and then intercepting requests to the appropriate port range on localhost ought to be enough to do that? But I’ve literally never written any Javascript so I don’t know. Okta supports falling back from the loopback protocol to calling a custom URI scheme, but once you allow that you’re also losing a bunch of the phishing protection, so I’d prefer not to take that approach.

Like I said, none of this prevents exfiltration of bearer tokens once they’ve been issued, and there’s still a lot of ecosystem work to do there. But ensuring that tokens can’t be issued to unmanaged machines in the first place is still a step forwards, and with luck we’ll be able to make use of this on Linux systems without relying on proprietary client-side tooling.

(Time taken to code this implementation: about two days, and under 1000 lines of new code. Time taken to figure out what the fuck to write: rather a lot longer)

[1] There’s also support for having Okta issue certificates, but then you’re kind of back to the “How do I know this is my device” situation

comment count unavailable comments