--- title: "Tutorial: Ingress Validation" --- This tutorial shows how to deploy OPA as an admission controller from scratch. It covers the OPA-kubernetes version that uses kube-mgmt. The [OPA Gatekeeper version](https://open-policy-agent.github.io/gatekeeper) has its own docs. For the purpose of the tutorial we will deploy two policies that ensure: - Ingress hostnames must be on allowlist on the Namespace containing the Ingress. - Two ingresses in different namespaces must not have the same hostname. > 💡 Kubernetes does not guarantee consistency across resources. If two > ingresses are created in parallel, there is no guarantee that OPA (or any > other admission controller) will observe the creation of one ingress before > the other. This means that it's not possible to enforce these policies during > admission control 100% of the time. There will be a small window of time > (usually on the order of milliseconds) when the eventually consistent cache > inside of OPA (or any other admission controller) is out-of-date. To catch > these violations we recommend you periodically audit the state of the cluster > against your policies. Offline auditing is one of the features provided by the > [OPA Gatekeeper](https://github.com/open-policy-agent/gatekeeper) project. ## Prerequisites This tutorial requires Kubernetes 1.20 or later. To run the tutorial locally ensure you start a cluster with Kubernetes version 1.20+, we recommend using [minikube](https://kubernetes.io/docs/setup/) or [KIND](https://kind.sigs.k8s.io/). ## Steps ### 1. Enable recommended Kubernetes Admission Controllers To implement admission control rules that validate Kubernetes resources during create, update, and delete operations, you must enable the [ValidatingAdmissionWebhook](https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#validatingadmissionwebhook) when the Kubernetes API server is started. The ValidatingAdmissionWebhook admission controller is included in the [recommended set of admission controllers](https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#is-there-a-recommended-set-of-admission-controllers-to-use) to enable. Start minikube: ```bash minikube start ``` Make sure that the minikube ingress addon is enabled: ```bash minikube addons enable ingress ``` ### 2. Create a new Namespace to deploy OPA into ```bash kubectl create namespace opa ``` Configure `kubectl` to use this namespace: ```bash kubectl config set-context opa-tutorial --user minikube --cluster minikube --namespace opa kubectl config use-context opa-tutorial ``` ### 3. Create TLS credentials for OPA Communication between Kubernetes and OPA must be secured using TLS. To configure TLS, use `openssl` to create a certificate authority (CA) and certificate/key pair for OPA: ```bash openssl genrsa -out ca.key 2048 openssl req -x509 -new -nodes -sha256 -key ca.key -days 100000 -out ca.crt -subj "/CN=admission_ca" ``` Generate the TLS key and certificate for OPA: ```bash cat >server.conf < Note: the Common Name value and Subject Alternative Name you give to openssl MUST match the name of the OPA service created below. Create a Secret to store the TLS credentials for OPA: ```bash kubectl create secret tls opa-server --cert=server.crt --key=server.key --namespace opa ``` ### 4. Define OPA policy Let's define a couple of policies to test admission control. First create a new folder to store our policies: ```bash mkdir policies && cd policies ``` #### Policy 1: Restrict Hostnames Create a policy that restricts the hostnames that an ingress can use. Only hostnames matching the specified regular expressions will be allowed. ```rego title="ingress-allowlist.rego" package kubernetes.admission import data.kubernetes.namespaces operations := {"CREATE", "UPDATE"} deny contains msg if { input.request.kind.kind == "Ingress" operations[input.request.operation] host := input.request.object.spec.rules[_].host not fqdn_matches_any(host, valid_ingress_hosts) msg := sprintf("invalid ingress host %q", [host]) } valid_ingress_hosts := {host | allowlist := namespaces[input.request.namespace].metadata.annotations["ingress-allowlist"] hosts := split(allowlist, ",") host := hosts[_] } fqdn_matches_any(str, patterns) if { fqdn_matches(str, patterns[_]) } fqdn_matches(str, pattern) if { pattern_parts := split(pattern, ".") pattern_parts[0] == "*" suffix := trim(pattern, "*.") endswith(str, suffix) } fqdn_matches(str, pattern) if { not contains(pattern, "*") str == pattern } ``` #### Policy 2: Prohibit Hostname Conflicts Now let's define another policy to test admission control. The following policy prevents Ingress objects in different namespaces from sharing the same hostname. **ingress-conflicts.rego**: ```rego title="ingress-conflicts.rego" package kubernetes.admission import data.kubernetes.ingresses deny contains msg if { some other_ns, other_ingress input.request.kind.kind == "Ingress" input.request.operation == "CREATE" host := input.request.object.spec.rules[_].host ingress := ingresses[other_ns][other_ingress] other_ns != input.request.namespace ingress.spec.rules[_].host == host msg := sprintf("invalid ingress host %q (conflicts with %v/%v)", [host, other_ns, other_ingress]) } ``` #### Combine Policies Let's define a main policy that imports the [Restrict Hostnames](#policy-1-restrict-hostnames) and [Prohibit Hostname Conflicts](#policy-2-prohibit-hostname-conflicts) policies and provides an overall policy decision. ```rego title="main.rego" package system import data.kubernetes.admission main := { "apiVersion": "admission.k8s.io/v1", "kind": "AdmissionReview", "response": response, } default uid := "" uid := input.request.uid response := { "allowed": false, "uid": uid, "status": {"message": reason}, } if { reason = concat(", ", admission.deny) reason != "" } else := {"allowed": true, "uid": uid} ``` > ⚠️ When OPA receives a request, it executes a query against the document defined `data.system.main` by default. ### 5. Build and Publish OPA Bundle Build an OPA bundle containing policies defined in the previous step. In our setup, OPA will download policies from the bundle service and the `kube-mgmt` container will load Kubernetes resources into OPA. Since we load policy and data into OPA from multiple sources, we need to scope the bundle to a subset of OPA’s policy and data cache by defining a manifest. More information about this can be found [here](../management-bundles#multiple-sources-of-policy-and-data). Run the following commands in the `policies` folder created in the previous step. ```bash cat > .manifest < ``` # Grant OPA/kube-mgmt read-only access to resources. This lets kube-mgmt # replicate resources into OPA so they can be used in policies. kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: opa-viewer roleRef: kind: ClusterRole name: view apiGroup: rbac.authorization.k8s.io subjects: - kind: Group name: system:serviceaccounts:opa apiGroup: rbac.authorization.k8s.io --- # Define role for OPA/kube-mgmt to update configmaps with policy status. kind: Role apiVersion: rbac.authorization.k8s.io/v1 metadata: namespace: opa name: configmap-modifier rules: - apiGroups: [""] resources: ["configmaps"] verbs: ["update", "patch"] --- # Grant OPA/kube-mgmt role defined above. kind: RoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: namespace: opa name: opa-configmap-modifier roleRef: kind: Role name: configmap-modifier apiGroup: rbac.authorization.k8s.io subjects: - kind: Group name: system:serviceaccounts:opa apiGroup: rbac.authorization.k8s.io --- kind: Service apiVersion: v1 metadata: name: opa namespace: opa spec: selector: app: opa ports: - name: https protocol: TCP port: 443 targetPort: 8443 --- apiVersion: apps/v1 kind: Deployment metadata: labels: app: opa namespace: opa name: opa spec: replicas: 1 selector: matchLabels: app: opa template: metadata: labels: app: opa name: opa spec: containers: # WARNING: OPA is NOT running with an authorization policy configured. This # means that clients can read and write policies in OPA. If you are # deploying OPA in an insecure environment, be sure to configure # authentication and authorization on the daemon. See the Security page for # details: https://www.openpolicyagent.org/docs/security.html. - name: opa image: openpolicyagent/opa:{{ current_version_docker }} args: - "run" - "--server" - "--tls-cert-file=/certs/tls.crt" - "--tls-private-key-file=/certs/tls.key" - "--addr=0.0.0.0:8443" - "--addr=http://127.0.0.1:8181" - "--set=services.default.url=http://host.minikube.internal:8888" - "--set=bundles.default.resource=bundle.tar.gz" - "--log-format=json-pretty" - "--set=status.console=true" - "--set=decision_logs.console=true" volumeMounts: - readOnly: true mountPath: /certs name: opa-server readinessProbe: httpGet: path: /health?plugins&bundle scheme: HTTPS port: 8443 initialDelaySeconds: 3 periodSeconds: 5 livenessProbe: httpGet: path: /health scheme: HTTPS port: 8443 initialDelaySeconds: 3 periodSeconds: 5 - name: kube-mgmt image: openpolicyagent/kube-mgmt:{{ current_version_kube_mgmt }} args: - "--replicate-cluster=v1/namespaces" - "--replicate=networking.k8s.io/v1/ingresses" volumes: - name: opa-server secret: secretName: opa-server ``` > ⚠️ If using `kind` to run a local Kubernetes cluster, the bundle service URL should be `http://host.docker.internal:8888`. ```bash kubectl apply -f admission-controller.yaml ``` When OPA starts, the `kube-mgmt` container will load Kubernetes Namespace and Ingress objects into OPA. You can configure the sidecar to load any kind of Kubernetes object into OPA. The sidecar establishes watches on the Kubernetes API server so that OPA has access to an eventually consistent cache of Kubernetes objects. Next, generate the manifest that will be used to register OPA as an admission controller. This webhook will ignore any namespace with the label `openpolicyagent.org/webhook=ignore`. ```bash cat > webhook-configuration.yaml <