Policy Controller is a Kubernetes controller that enforces organizational policies on your GKE clusters.

Let’s see it in action. Imagine you have a strict policy: no public IP addresses allowed on LoadBalancer services.

First, we need to install Policy Controller. This is typically done via kubectl or the Google Cloud console. Assuming you’ve enabled it on your GKE cluster, you’ll start seeing policy violations.

Here’s a sample Service manifest that violates our hypothetical policy:

apiVersion: v1
kind: Service
metadata:
  name: my-public-service
spec:
  selector:
    app: my-app
  ports:
    - protocol: TCP
      port: 80
      targetPort: 8080
  type: LoadBalancer
  # This is what we want to prevent
  # loadBalancerIP: 35.230.12.34

When you try to apply this, Policy Controller, configured with the appropriate constraint, will reject it. You’ll see an error message like:

Error from server (Forbidden): admission webhook "validation.gatekeeper.sh" denied the request: [no-public-ips] Service spec.type LoadBalancer is disallowed from having external IPs.

The core of Policy Controller is the use of Open Policy Agent (OPA) Gatekeeper. Gatekeeper uses a declarative policy language called Rego to define constraints. These constraints are applied as Kubernetes custom resources.

Here’s what a constraint for our "no public IPs" policy might look like:

apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sNoPublicIP
metadata:
  name: disallow-public-lb-ips
spec:
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["Service"]
    namespaces:
      - "default" # Or your specific namespace
  parameters:
    loadBalancerIPs: ["*"] # This parameter is illustrative; actual constraint logic is in the template

The actual policy logic is defined in a ConstraintTemplate. This template is a custom resource that defines the schema for your constraints and the Rego code that enforces the policy. For K8sNoPublicIP, the template would contain Rego that checks if spec.type is LoadBalancer and if spec.loadBalancerIP is set.

Let’s walk through the mental model. Policy Controller acts as a validating and mutating admission webhook. When you create or update a Kubernetes resource, the API server sends the request to Policy Controller before it’s persisted. Policy Controller evaluates the resource against its configured constraints. If a constraint is violated, Policy Controller rejects the request. It can also mutate resources, for example, by automatically adding labels or annotations.

The power comes from defining your organizational policies as code. This means policies are versionable, testable, and auditable. You can enforce anything from naming conventions and resource quotas to security best practices and compliance requirements.

Here are some common policy types you’ll encounter or want to implement:

  • K8sRequiredLabels: Ensures specific labels are present on resources.
  • K8sContainerImage: Restricts container images to an allowed registry or pattern.
  • K8sPSPSafety: Enforces Pod Security Standards.
  • K8sRequireNamespace: Ensures resources are created in specific namespaces.

To manage these, you first define a ConstraintTemplate which contains the Rego logic. Then, you create a Constraint resource that references the template and specifies the match criteria (which resources it applies to) and any parameters the template expects.

You can scope policies to specific namespaces, or apply them cluster-wide. The match section in your Constraint resource is crucial for this. You can match on kinds, apiGroups, namespaces, names, and even labelSelectors.

The Rego language itself is powerful. It allows you to express complex logic by querying the input document (the resource being validated) and comparing it against defined rules. For instance, checking if a container image is in a forbidden registry involves iterating through spec.containers[*].image and checking if any part of the image string matches a disallowed pattern.

A common pattern is to use a ConstraintTemplate that takes a list of allowed values as a parameter. For example, a K8sContainerImage template might accept a list of allowed registries. Your Constraint would then specify parameters: { allowedRegistries: ["gcr.io/my-org", "us-central1-docker.pkg.dev/my-org"] }. The Rego code in the template would then check if input.review.object.spec.containers[*].image starts with any of the provided allowedRegistries.

The flexibility of Policy Controller means you can start with simple policies and gradually increase their strictness. You can also run policies in "dryrun" mode, which logs violations without blocking resource creation, allowing you to identify potential issues before enforcing them.

One thing that often trips people up is understanding how parameters work between ConstraintTemplate and Constraint. The ConstraintTemplate defines the structure and logic of a policy, including the expected parameters and their types. The Constraint then instantiates that template for a specific use case, providing the actual values for those parameters. It’s like a function definition (ConstraintTemplate) and a function call (Constraint) where the parameters are passed. If your Rego code expects input.parameters.allowedNamespaces and your constraint is missing that parameters block or has a typo, Gatekeeper will complain during the Constraint creation or validation.

Once you have your policies defined and enforced, the next step is often to integrate them into your CI/CD pipelines for automated policy checks before deployment.

Want structured learning?

Take the full Gke course →