I recently had to debug Istio authorization policies and I learned what a pain it can be understand how and when policies are applied to protect service on your mesh. This post is my attempt to explain the important bits of how to set this up.
With that said, checkout the Istio official docs for more detail.
Istio is one of the leading service mesh implementations out there. It's used by a lot of companies to manage the growing overhead of having services running on Kubernetes. This is especially true when dealing with microservice architectures, which yield dozens (if not hundreds) services which need to talk to each other securely and reliably.
Using a service mesh, like Istio, handles a lot of this complexity for you. For example app developers do not need implement the following features and can instead use what's configurable in the mesh:
- mTLS between services (encrypted both ways)
- telemetry (common network metrics exposed for Prometheus)
- rate limiting & fault tolerance
- authentication (proving who you are)
- authorization (can you do that?)
There is more the service mesh can do for your, but we're going to focus on the last two in this post.
But to that, you need to understand what the JWT is, and how it's used in conjunction with Istio to enforce RBAC for services.
Above: Anatomy of a JWT
The JWT is the standard token used in OpenIDConnect (OIDC) spec, and widely used as result. It has three pieces to it, the header, payload, and signature. We are focused on the payload. Within the JSON payload one can put data (note: keys in the payload are called claims). That said, there are certain keys (claims) that are reserved by the JWT spec. Non-reserved claims are called custom claims, of which the OIDC spec defines a handful of. These claims are often grouped into scopes. Where a scopes can be thought of as groupings of related permissions. Again, unsurprisingly the OIDC specifies certain scopes.
Because, having a JWT can act as proof of who you are, and claims within the JWT capture what you are allowed to do. Clients usually obtain a JWT by requesting one from a JWT issuer along with some credentials to prove identity (user/pass/2FA). The JWT is then usually provided by the client to the app in the
Authorization: Bearer <token> request header. There's a whole slew of tooling around doing this "handshake", from both client and server-side (Istio included). The neat thing about using a service mesh is that Istio can handle this interaction transparently to services. You only need configure the
Above: JWT 'handshake'
RequestAuthentication object you can specify which workloads require a JWT from which trusted issuers. Note you need to provide
jwksUri so that Istio knows where to grab the certs used in the validation of the tokens (aka the JSON Web Key Set). Your issuer will have an endpoint for this (sometimes linked from the
well-known endpoint ).
In this example, we want our service
httpbin in the foo
namespace to require a valid JWT issues from google.
apiVersion: security.istio.io/v1beta1 kind: RequestAuthentication metadata: name: foo-req-auth namespace: foo spec: selector: matchLabels: app: httpbin jwtRules: - issuer: "https://accounts.google.com" # Whose JWTs do you trust? jwksUri: "https://www.googleapis.com/oauth2/v3/certs" # Certs to verify JWTs
This alone does not however enforce that others cannot hit your endpoint publicly. You need to this this in with Authorization Policies.
Now here is the meat of what you will be configuring when using Istio enforce RBAC for your services. I won't go in depth here (there is a lot you can do) but I will briefly touch on the .
- There are 3 kind of authorization policies:
- allow policies
- deny policies
- custom policies
- There is an ordering in how access is determined with these three policies
- These policies have
rules, which define if certain criteria is met, what level of access is allowed.
In this example, we allow access to our service
httpbin in namespace
foo from any JWT (regardless of the principle) to use the
apiVersion: "security.istio.io/v1beta1" kind: "AuthorizationPolicy" metadata: name: "allow-reads" namespace: foo spec: selector: matchLabels: app: httpbin rules: - from: - source: principals: ["*"] to: - operation: methods: ["GET"]
🚧 I will fill out this section soon. I working on more examples and diagrams to tie these concepts together!
I highly recommend reading Istio By Example for more info!
There is some logic behind how authorization is determined given defined
AuthorizationPolicies. Below is the flow as taken directly from the Istio documentation.
1 - If there are any CUSTOM policies that match the request, evaluate and deny the request if the evaluation result is deny.
2 - If there are any DENY policies that match the request, deny the request.
3 - If there are no ALLOW policies for the workload, allow the request.
4 - If any of the ALLOW policies match the request, allow the request.
5 - Deny the request.
That said, as a visual learner, I need something tangible to keep this model in my head. So I think model the auth flow a person in a cooperate lobby trying to get enter an office.
In this analogy:
- the Custom auth policies can be thought of C-level execs
- They can decided many things, including if your allowed in or not!
- the Deny policies can be thought of a office security
- They are on the look out for features, if you catch their eye they will kick out
- the Allow policies can thought of as employees of the office your are visiting
- They can swipe you into the office if they happen to be expecting you
Above: You enter the lobby and you notice the custom policies. If they are expecting (match) you, they will decide if you should be allowed in the office or not. They make the first call in regards to access.
Using Istio you are just a few configuration files away from handling auth for your services. While the configuration of these files might be a little obtuse, thinking about how they fit and how they get applied shouldn't have to be!