Webhook icon
Schedule icon
Script icon
Evaluate icon
If icon
SlackIncomingWebhook icon
Fail icon
HumanTask icon
Log icon
Loop icon
Apply icon

OPA-Gated IaC Applies with Human Approval

Evaluate IaC manifests with OPA - hard violations block the run, soft violations wait for group approval, clean sets apply straight through kubectl.

Categories
Infrastructure

How it works

  1. ci_webhook (event-based) receives the call from CI, or nightly_recheck (shipped disabled) re-evaluates a standing set.
  2. build_policy_input (scripts.python.Script, Process runner) normalizes the manifests input into flat resource facts - kind, name, namespace (falling back to apply_namespace), image, privileged flag and replicas - plus a policy_input document carrying the requester and execution id. Non-list or junk entries are dropped safely, and everything returns through Kestra's ::json:: outputs protocol.
  3. evaluate (io.kestra.plugin.ee.opa.policy.Evaluate) posts those facts to the OPA instance (policyPath, default iac/gate) and returns defined, result and decisionId.
  4. classify_decision (python) splits violations by severity: soft/warning/review become review items, everything else - including bare string violations - becomes a hard denial, and an undefined decision is itself treated as a hard denial so a downed OPA fails closed, not open.
  5. hard_deny_gate (If auto_deny_count > 0) posts the blockers to Slack and core.execution.Fails - no human can wave through what policy forbids outright (mirroring the eligibility-first pattern in aikido-security-exception-approval).
  6. review_gate (If review_count > 0) notifies Slack, then human_approval (io.kestra.plugin.ee.flow.HumanTask) assigns the decision to the platform-approvers group with behavior: FAIL and a structured onResume (approve/deny + reviewer note) - only that group can resume, so approval is genuinely attributable. approval_gate checks (outputs.human_approval.onResume is defined) and ... == 'approve' (the proven aikido condition shape) and fails the run on denial or timeout. Clean sets take the else branch and log the auto-pass.
  7. Only executions that survived both gates reach apply_manifests (core.flow.Loop over the normalized resources), where each kubectl.Apply creates or updates the Deployment with its gated spec (connection/spec shape from kubernetes-vault-rotation-restart), and announce_applied reports the count, the review path taken and the OPA decision id.
  8. Outputs expose manifest_count, resources, opa_decision_id, review_items and human_decision; errors posts gate failures to Slack so infrastructure errors are never confused with denials.

What you get

  • Three-way routing from one policy evaluation: block, review, or pass - the two fast paths keep compliant changes moving without waiting on people.
  • Group-attributed approvals: a generic pause resumes with a click, a HumanTask resumes only from the approver group, with a stored rationale.
  • Fail-closed behavior when OPA is unreachable, so a broken policy engine cannot become a silent approve.
  • A single auditable chain: OPA decision id, reviewer note, and applied resources all live on one execution.

Who it's for

Platform teams running Terraform or manifest-based delivery who want policy as code at apply time, and compliance-minded orgs that need human sign-off on the exceptions policy cannot decide alone.

Why orchestrate this with Kestra

The usual setup spreads this across CI linters, a ticket, and someone's kubectl - with no durable state between them. Kestra holds the approval durably for days, makes the structured verdict a branch condition downstream, fails closed on policy outages, and leaves the plan, the decision and the apply in one execution history auditors can replay.

Prerequisites

  • An OPA instance exposing the gate policy (a small deny/violation document returning severity-tagged items) and its URL/token.
  • Kubernetes credentials able to apply Deployments in the target namespace.
  • A Slack incoming webhook and a platform-approvers group in Kestra.
  • Manifests shaped as Deployments (kind, name, namespace, image, privileged, replicas) - extend the python normalizer and the Apply spec for other kinds.

Secrets

  • OPA_URL: OPA base URL, e.g. http://opa:8181.
  • OPA_TOKEN: bearer token for OPA, if required.
  • K8S_MASTER_URL: Kubernetes API server URL.
  • K8S_TOKEN: service account token with Deployment apply permissions.
  • SLACK_WEBHOOK_URL: Slack incoming webhook for gate notifications.

Quick start

  1. Set the five secrets and create the platform-approvers group.
  2. Deploy a policy at iac/gate that returns {"rule": ..., "severity": "soft", "resource": ...} items - e.g. images must come from registry.example.com (the sample second manifest uses docker.io to demo the review path).
  3. Run the flow with default inputs: the metrics-exporter image trips a soft violation, Slack gets the review message, and the paused HumanTask waits for the group.
  4. Resume with approve + a note - both manifests apply and Slack confirms the path; resume with deny and the run fails with nothing applied.
  5. Add {"privileged": true} to a manifest to see the hard-deny branch block before anyone is asked.

How to extend

  • Split apply by severity: auto-apply clean resources while only the soft set waits, by moving the Loop inside the approval branch.
  • Swap kubectl.Apply for a Terraform plan/apply runner, using the same policy gate around terraform apply.
  • Record every verdict to a database table for quarterly access reviews of who approved what.
  • Add a second HumanTask for a security group when violations touch privileged or public exposure.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.