Gatekeeper with Istio
Tags: gatekeeper, istio, opa
Published: 2022-07-19 00:40:00

For those who have been using [Istio](https://istio.io/) to manage microservices you shouldn't be too surprised about its superpower. While working with various teams and organizations within the Enterprise I have to admit that Istio sometimes can be very complicated. Fortunately, Google, [Solo](https://www.solo.io), and many others continue to drive the innovation in this space so you can eventually benefit as the end user.

According to [ZTA](https://www.nist.gov/blogs/taking-measure/zero-trust-cybersecurity-never-trust-always-verify) we should **Never Trust, Always Verify**.

In this article I want to touch upon how to use PaC (aka. Policy-As-Code) to enforce the correct implementation of the Istio (to be clear that there is no absolute right or wrong, but by following the best practices you achieve the correctness for the time being), for example [Protocol Selection](https://istio.io/latest/docs/ops/configuration/traffic-management/protocol-selection/). By default, Istio can automatically detect HTTP(/2) traffic otherwise it will be treated as plain TCP traffic. As the [Zen of Python](https://peps.python.org/pep-0020/) teaches us that **Explicit is better than implicit**. We should always strive for code's readability and maintainability. So, let's enforce this rule at design-time and run-time.

This article is not meant for a deep dive on [Rego](https://www.openpolicyagent.org/docs/latest/policy-language/) which I will leave it to the readers.

According to [Istio Explicit Protocol Selection](https://istio.io/latest/docs/ops/configuration/traffic-management/protocol-selection/#explicit-protocol-selection):
> This can be configured in two ways:

> By the name of the port: `name: <protocol>[-<suffix>]`.
> In Kubernetes 1.18+, by the appProtocol field: `appProtocol: <protocol>`.

Let's dissect that.

- port: name

You can definitely use the build-in function [re_match](https://www.openpolicyagent.org/docs/v0.12.2/language-reference/#regex), but it might complicate the Gatekeeper's ConstraintTemplate which we will talked about it in a moment. Instead, we can just `split` the port_name by `-`, and check its membership.

```
protocol := split(port_name, "-")[0]
protocol in protocols
```


- port: appProtocol

The are a couple of ways to check the membership:
```
#1 Unification, which is different from `:=` and `==` in Rego.

protocol = protocols[_]

#2 `in`, which requires to `import future.keywords`

protocol in protocols

#3 Set

protocol_set := { p | p := input.parameters.protocols[_] }
protocol_set[protocol]

```

By following [Styra](https://www.styra.com/)'s [Rego Style Guide](https://github.com/StyraInc/rego-style-guide#use-in-to-check-for-membership) the option #2 will be the preferred way.

There is one little subtlety which we haven't talked about yet. When [Service](https://kubernetes.io/docs/concepts/services-networking/service/) has both port name and port appProtocol the latter takes precedence by Istio. So, how do we express that in Rego?

In Rego within the Rule the `AND` condition implies which means all Rule body needs to be True to make this Rule True. The `OR` condition is achieved by multiple Rules with the same Rule name.

```
# when port.appProtocl exists just use it and ignore port name altogether.
_is_valid(port, protocols) {
  port.appProtocol

  _match_app_protocol(port.appProtocol, protocols)
}

# when port.appProtocol doesn't exit port name has to exist and match the protocols we specified.
_is_valid(port, protocols) {
  not port.appProtocol
  port.name
  
  _match_port_name(port.name, protocols)
}
```

Let's put it all together:
```
package istio.security.protocolselection

import future.keywords

violation[{"msg": msg}] {
  protocols := input.parameters.protocols
  
  some port in input.review.object.spec.ports
  not _is_valid(port, protocols)
  
  msg := sprintf("port: %v name or appProtocol is invalid", [port])
}

_is_valid(port, protocols) {
  port.appProtocol

  _match_app_protocol(port.appProtocol, protocols)
}

_is_valid(port, protocols) {
  not port.appProtocol
  port.name
  
  _match_port_name(port.name, protocols)
}

_match_app_protocol(protocol, protocols) {
  protocol in protocols
}

_match_port_name(port_name, protocols) {
  protocol := split(port_name, "-")[0]
  
  protocol in protocols
}
```

Now, we have the hardest part resolved and let's turn our attention to the [OPA Gatekeeper](https://github.com/open-policy-agent/gatekeeper). Gatekeeper uses the [OPA Constraint Framework](https://github.com/open-policy-agent/frameworks/tree/master/constraint) to describe and enforce policy. Right now there are mainly 3 parts we should pay attention:

1. ContraintTemplate: describe both Rego that enforces the constraint and the schema of the contraint.
2. Constraint: describe what ContraintTemplate needs to be enforced, and how.
3. Config: describe behaviors for certain processes.

####1: ContraintTemplate
```
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  annotations:
    description: Explicit protocol selection either by name or appProtocol
  name: istioexplicitprotocolselection
spec:
  crd:
    spec:
      names:
        kind: IstioExplicitProtocolSelection
      validation:
        openAPIV3Schema:
          type: object
          properties:
            prefixes:
              type: string
            protocols:
              type: array
              items:
                type: string
  targets:
  - target: admission.k8s.gatekeeper.sh
    rego: |-
      package istio.security.protocolselection

      import future.keywords

      violation[{"msg": msg}] {
        protocols := input.parameters.protocols
        
        some port in input.review.object.spec.ports
        not _is_valid(port, protocols)
        
        msg := sprintf("port: %v name or appProtocol is invalid", [port])
      }

      _is_valid(port, protocols) {
        port.appProtocol

        _match_app_protocol(port.appProtocol, protocols)
      }

      _is_valid(port, protocols) {
        not port.appProtocol
        port.name
        
        _match_port_name(port.name, protocols)
      }

      _match_app_protocol(protocol, protocols) {
        protocol in protocols
      }

      _match_port_name(port_name, protocols) {
        protocol := split(port_name, "-")[0]
        
        protocol in protocols
      }
```

####2: Constraint
```
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: IstioExplicitProtocolSelection
metadata:
  name: explicitprotocolselection
spec:
  enforcementAction: deny
  match:
    kinds:
    - apiGroups:
      - ""
      kinds:
      - Service
  parameters:
    protocols:
    - http
    - https
    - http2
    - grpc
    - grpc-web
    - tcp
    - tls
```

####3: Config
We will use it to ignore all namespaces that we don't want to enforce the policy.
```
apiVersion: config.gatekeeper.sh/v1alpha1
kind: Config
metadata:
  name: config
  namespace: "gatekeeper-system"
spec:
  match:
    - excludedNamespaces: ["kube-*", "istio-*"]
      processes: ["*"]
```

Voila!

OPA Gatekeeper has more than I just showed you here. I'll leave it to you to explore: Gator, Mutation, Audit, Replication, etc.

Ref: https://github.com/feiyao/gatekeeper-istio