DEV Community

Cover image for Gatekeeper with Istio
Fei Y
Fei Y

Posted on • Updated on

Gatekeeper with Istio

For those who have been using Istio 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, and many others continue to drive the innovation in this space so you can eventually benefit as the end user.

According to ZTA 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. By default, Istio can automatically detect HTTP(/2) traffic otherwise it will be treated as plain TCP traffic. As the Zen of Python 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 which I will leave it to the readers.

According to Istio 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, 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
Enter fullscreen mode Exit fullscreen mode
  • 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]

Enter fullscreen mode Exit fullscreen mode

By following Styra's Rego Style Guide the option #2 will be the preferred way.

There is one little subtlety which we haven't talked about yet. When 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)
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

Now, we have the hardest part resolved and let's turn our attention to the OPA Gatekeeper. Gatekeeper uses the OPA Constraint Framework 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
      }
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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: ["*"]
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)