Microsoft released its findings from its investigation into the Storm-0558 incident. I read the post, but I’d be lying if I said I learned much of anything that was useful to me. The security work that Microsoft is doing is on such a different scale to almost every other company in the world that it makes it very hard to relate to. I can understand what they’re talking about, but I’ve not done anything like it myself.
There was one part that stood out, though. That section read:
Developers in the mail system incorrectly assumed libraries performed complete validation and did not add the required issuer/scope validation. Thus, the mail system would accept a request for enterprise email using a security token signed with the consumer key (this issue has been corrected using the updated libraries).
This stands out because it highlights a problem that all companies face. Two things are true:
1. Developers make a ton of mistakes, and
2. Making mistakes with authentication and authorization has serious consequences.
These two facts don’t play well together, as Microsoft discovered.
This isn’t an earth shattering discovery, but it does affect how we should think about the security of the systems we build.
If you go down the rabbit hole of microservices, as so many of us have, you might be tempted to think about authentication and authorization as services themselves. You split authentication and authorization into APIs, and get your other services to call them.
If that is all you do, you will quickly start to get nervous. What if the other services are using these APIs incorrectly? So, you also make a nice client library that each service can use. Now, all a service has to do is import the client library and call the right function, and everything will work as it should.
But life isn’t that simple. Each service needs the latest version of the client library, first of all. And while “calling the right function” sounds easy, each service also needs to call that function at the right time. It also needs to handle the response, including any errors, appropriately.
There is nothing inherently wrong with this approach to securing your systems, and in certain contexts it might even be the best decision. However, it is vulnerable to the conflict mentioned at the start of this post: developers make mistakes, and mistakes in this area are costly. Every service is still a potential security risk, and every developer has the opportunity to mess it all up.
Keeping Services Internal
An alternative approach is to minimize the number of services that are exposed to end users. The goal is to ensure that the majority of applications can’t compromise your security even if they’re written by a clown.
Say you have thirty different microservices. You might have an API proxy that sits in front of them, but instead of having that API proxy call directly to each, you instead only give it access to two or three services that sit on top. Those services then handle authentication and authorization, do business logic stuff, and use the rest of the services as internal helpers of sorts.
As a result, the responsibility for handling authentication and authorization is on those two or three services alone, not the twenty or thirty services you have created internally. It also means that far fewer developers need to be right for this to work.
Of course, this approach has its tradeoffs, but that’s always the case. There is no perfect solution. However, what I like about this is that it acknowledges the reality that developers make mistakes, and keeps as many developers away from security-critical situations as possible.
No matter how you choose to design your systems, you should at least have thought about this problem. Think about how many developers need to be “right” in order to avoid catastrophic security flaws, and try to minimize that number where you can. If you are doing that, you’re at least headed in the right direction.