Introduction to Istio access control

Thursday, July 16th, 2020
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) and run the following command to install the CLI tool (
KUBECONFIG
must be set for your cluster):Want to know more? Get in touch with us, or delve into the details of the latest release.
Or just take a look at some of the Istio features that Backyards automates and simplifies for you, and which we've already blogged about.
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.