Istio claims that it helps to connect, secure, control and observe services. We’ve blogged a lot about connect, even more about observe, and also had a few articles about secure. But so far, we haven’t really touched control.
This post tries to fill that gap, and discusses Istio’s access control model, or more specifically AuthorizationPolicies
.
Architecture 🔗︎
Istio Authorization can be used to enforce access control rules between workloads. It basically answers the question: who can access what, under which specific conditions? Just like any other mesh configuration, authorization rules can be specified through Kubernetes CRDs. The API is quite simple, it consists of a single CRD, called AuthorizationPolicy
, but more on the YAML details later. First, let’s see how are these rules enforced in Istio.
If you’re reading this article, you should already be familiar with Istio’s high level architecture, but here’s a (very) brief recap. Istio has a data plane, and a control plane. The data plane consists of sidecar proxies running alongside the application containers in the same pod, and they are responsible for forwarding all incoming, and outgoing traffic to the application. The control plane on the other hand is accepting user configuration through CRDs, and - among a few other things - transforms these CRDs to Envoy configuration and delivers it to the proxies. The sidecars are Envoy proxies, and the control plane is now basically a single service, called istiod
.
Similarly to telemetry and traffic management, the real deal happens in the data plane. All checks are performed runtime by the Envoy proxy’s authorization engine. A request is evaluated against the authorization policies when it arrives to the proxy. Then Envoy returns the result, either ALLOW
or DENY
.
A brief history of Istio access control 🔗︎
For someone who’s just getting to know Istio, it can be confusing that they may bump into blog posts about Istio access control containing mentions of CRDs like ClusterRbacConfig
, ServiceRole
, ServiceRoleBinding
. Those resources were part of the v1alpha1
API, that is now completely replaced by the v1beta1
API.
The new API was introduced in Istio 1.4, and from Istio 1.6, the old API is not supported anymore. If you’re looking for a migration path, I’d recommend to read the official blog post.
The new model simplifies configuration (one CRD instead of three), supports ingress and egress gateways, and better aligns with the Istio configuration model, as it is applied to workloads instead of services.
Kubernetes NetworkPolicies
🔗︎
When talking about AuthorizationPolicies
, we have to mention Kubernetes NetworkPolicies
, because they are quite similar in terms of what problem they are trying to solve. The Kubernetes docs define network policies as follows:
A network policy is a specification of how groups of pods are allowed to communicate with each other and other network endpoints. For more details about network policies check out our blog post, Exploring Network Policies in Kubernetes.
There’s no easy answer to which one is better?, because they are good at different things. Istio policy enforcement works at the application layer (L7), - that’s where the Envoy proxies operate - while Kubernetes network policies work at the network (L3) and transport layers (L4). Kubernetes network policies are implemented by different networking solutions, like Calico. These solutions are running a controller that’s watching NetworkPolicies
, and configures the underlying networking layer accordingly.
Operating at the application layer has its advantages. Because Envoy understands different protocols (most commonly HTTP), it allows for a rich set of attributes to base policy decisions on. A few examples are policies based on HTTP methods, URIs, or HTTP headers. A NetworkPolicy
cannot do these, because these concepts are unknown at the network and transport layers. But operating at the network layer has the advantage of being universal, since all network applications use IP. So you can apply policies regardless of the layer 7 protocol, and these will be enforced in the kernel space. It’s extremely fast, but not as flexible as Envoy policies.
Another difference worth mentioning is that NetworkPolicies
work in an additive, whitelist model. When a NetworkPolicy
selects a specific pod, that pod will reject any connections, except those that are explicitly allowed. These policies are additive, they do not conflict, and order of evaluation is irrelevant. AuthorizationPolicies
on the other hand have DENY
and ALLOW
rules as well, that complicates things a bit, but again, allows for more flexible rules.
So should you use Istio AuthorizationPolicies
over plain Kubernetes NetworkPolicies
? Well, it always depends on your use case. If you want to have a finer grained authorization model, you should go with Istio, but if your only requirement is that “pod A should only be able to communicate with pod B”, then NetworkPolicies
are just as good. Or you can even use the two concepts side-by-side.
Authorization policies 🔗︎
Istio authorization doesn’t need to be explicitly enabled. When no AuthorizationPolicies
select a workload, all requests are allowed. To enforce access control, you have to apply at least one AuthorizationPolicy
resource.
To start experimenting with Istio and
AuthorizationPolicies
, we suggest to try Backyards (now Cisco Service Mesh Manager) and get up and running with an example application in minutes. Backyards (now Cisco Service Mesh Manager) provides an Istio control panel where you can track, visualize or even manage your Istio YAML configuration.Register for an evaluation version](https://eti.cisco.com/appnet/smm/download) and run the following command to install the CLI tool (
KUBECONFIG
must be set for your cluster):Register for the free tier version of Cisco Service Mesh Manager (formerly called Banzai Cloud Backyards) and follow the Getting Started Guide for up-to-date instructions on the installation.
Scope 🔗︎
AuthorizationPolicies
can be mesh-, namespace-, and workload-wide depending on the namespace
and the spec/selector
field. The namespace
of the resource determines the namespace where the rules will be enforced. When the spec/selector
field is omitted, the rules are namespace-wide. The selector, that is a standard Kubernetes label selector, can be used to restrict the policy to specific workload(s) in the namespace, making the policy workload-wide.
Just like with the PeerAuthentication
resource, putting it in the root Istio namespace (usually istio-system
), without a selector has a special effect: these rules will be enforced mesh-wide, in all namespaces.
The following is a workload-wide policy, that applies to pods in the backyards-demo
namespace that have the app=catalog
label.
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: get-only
namespace: backyards-demo
spec:
selector:
matchLabels:
app: catalog
action: ALLOW
rules:
- to:
- operation:
methods: ["GET"]
Action 🔗︎
Unlike NetworkPolicies
, AuthorizationPolicies
support both ALLOW
and DENY
actions. If any ALLOW
policies are applied to a workload, traffic is denied to that workload by default, and only those requests that are explicitly configured are allowed. It could be a bit confusing at first, especially that the default action is ALLOW
, so a policy like this will deny all traffic in a namespace:
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: deny-all
namespace: backyards-demo
spec:
{}
While this one allows all traffic:
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: allow-any
namespace: backyards-demo
spec:
rules:
- {}
The deny policies take precedence over allow policies, so for example if there are conflicting rules, where a policy allows GET requests, and another denies them, the deny policy will be applied. When multiple policies are applied to the same workload, Istio applies them additively.
Rules 🔗︎
An authorization policy contains a list of rules, that describe which requests are matched, and then allowed or denied based on the action.
Rules are built of three parts: sources, operations and conditions. Sources are specified in the from
field, and answer the who? question. Operations are listed in the to
field, and answer the what? question. Then at last, conditions are described in the when
field and answer the when? question.
Let’s see a concrete example:
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: catalog
namespace: backyards-demo
spec:
selector:
matchLabels:
app: catalog
rules:
- from:
- source:
principals: ["cluster.local/ns/backyards-demo/sa/frontpage"]
to:
- operation:
methods: ["GET"]
This AuthorizationPolicy
is applied to the catalog
workload in the backyards-demo
namespace, and while not explicitly specified, it’s an ALLOW
rule, so it will deny all traffic that doesn’t match the rules described here. The rules contain a source
, that means that traffic is allowed only from a workload with the cluster.local/ns/backyards-demo/sa/frontpage
identity (service account). It also contains an operation
, that only matches GET
requests. It doesn’t contain a condition
, which means match any conditions. So to recap, the above policy allows GET requests from workloads with the cluster.local/ns/backyards-demo/sa/frontpage
identity to backyard-demo/catalog
, and denies everything else.
In the example, the source is a principal, but it can be requestPrincipals
, namespaces
or ipBlocks
as well. Istio also support exclusion matching, by providing the same fields with a not
prefix. So for example notNamespaces: default
would match sources from all namespaces, except from default
.
Let’s take a look at the operation
field as well: along methods
, valid matchers are hosts
, ports
, paths
and their exclusion pairs, like notHosts
.
In most cases the when
field can be omitted, it’s usually only used in complex scenarios, but it can be used to further customize request matching with a list of supported Istio attributes. For example the below example matches request header values:
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: header-matching
namespace: backyards-demo
spec:
selector:
matchLabels:
app: catalog
action: ALLOW
rules:
- from:
- source:
principals: ["cluster.local/ns/backyards-demo/sa/frontpage"]
to:
- operation:
methods: ["GET"]
when:
- key: request.headers[version]
values: ["v1", "v2"]
Finally, take a look at a more complex rule to see how it matches requests when most fields contain multiple entries:
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: example-policy
namespace: backyards-demo
spec:
selector:
matchLabels:
app: movies
action: ALLOW
rules:
- from:
- source:
principals: ["cluster.local/ns/backyards-demo/sa/catalog"]
- source:
namespaces: ["backyards-test"]
to:
- operation:
methods: ["GET"]
paths: ["/api/v1*"]
- operation:
methods: ["POST"]
paths: ["/api/v1*"]
- from:
- source:
principals: ["cluster.local/ns/backyards-demo/sa/bookings"]
to:
- operation:
methods: ["GET"]
paths: ["/api/v1/movies*"]
when:
- key: request.auth.claims[iss]
values: ["https://accounts.banzaicloud.io"]
This final example contains two separate rules in one policy with an ALLOW
action.
- these rules are enforced for the pods that match the label selector
app=movies
in thebackyards-demo
namespace - it allows
GET
requests to/api/v1*
ORPOST
requests to/api/v1*
from workloads in thebackyards-test
namespace, OR from workloads with thecluster.local/ns/backyards-demo/sa/catalog
service account - it also allows
GET
requests to/api/v1/movies*
from workloads with thecluster.local/ns/backyards-demo/sa/bookings
service account, when the request has a valid JWT token, issued by “https://accounts.banzaicloud.io” - it denies every other request to the movies workload
Notes 🔗︎
- the same goal could have been achieved with two different
AuthorizationPolicy
entries for the two different rules - mutual TLS is required to securely pass information between Envoy proxies, and it’s needed for some of the fields, like
source.principals
,source.namespaces
, or theconnection.sni
condition - plain TCP traffic can also be authorized by Istio, but in that case the
hosts
,methods
andpaths
operations have no effect, as well as therequest_principals
field in the source section and some of the custom conditions - most fields support exact, prefix, suffix and presence value matching: prefix and suffix is when the value starts or ends with a
*
, presence matching is*
and it’s used to specify anything but empty - an example for presence matching is
source.principals: ["*"]
, that means all authenticated requests
Summary 🔗︎
Istio can be used to enforce access control between workloads in the service mesh using the AuthorizationPolicy
custom resource. This kind of access control is enforced at the application layer by the Envoy sidecar proxies. It gives the user a very powerful and flexible, yet performant way of authorization between Kubernetes workloads.