Policy-as-code interview questions
Policy-as-code interview questions from fresher to senior — fundamentals, Rego and testing, Kubernetes enforcement with Gatekeeper and Kyverno, and advanced authz and supply-chain policy, tagged by experience level.
Fresher 0–1y · Junior 1–3y · Mid 3–6y · Senior 6+y — every answer is tagged so you can prep for your level.
What is policy as code?Fresher
Expressing rules — security, compliance, cost, operational — as versioned, testable code that a machine enforces automatically, instead of wiki pages and manual review. It makes governance repeatable and auditable.
Where does policy run — CI, admission, or runtime?Junior
All three, as layers: in CI (conftest) for fast feedback before merge, at admission (Gatekeeper/Kyverno) as the cluster backstop, and at runtime for authorization decisions. The same rules at multiple gates is defense in depth.
What is admission control in Kubernetes?Junior
A stage in the API request path (after authn/authz) where webhooks can validate or mutate objects before they persist — the hook point where policy engines block non-compliant resources or inject defaults.
OPA vs Gatekeeper vs Kyverno?Mid
OPA is a general policy engine using Rego. Gatekeeper wraps OPA as a Kubernetes admission controller with CRDs. Kyverno is Kubernetes-native, writing policies as YAML (no Rego) with validate/mutate/generate — often easier for pure-k8s teams.
OPA is general-purpose (k8s, microservice authz, Terraform) but you learn Rego. Gatekeeper packages OPA for k8s admission with constraint CRDs and audit. Kyverno is k8s-only and writes policies as YAML, which most platform teams find faster to adopt — the trade is less power than Rego for complex logic.
What is Rego at a glance?Junior
OPA’s declarative query language: you write rules that produce decisions (e.g. deny messages) by querying structured input and data. It is assertion-based — rules define what must be true, and unmatched rules simply yield no result.
package example # allow is true only if the request is a GET default allow := false allow if input.method == "GET"
How is a Rego deny rule structured?Mid
A rule in a package that appends a message to a `deny`/`violation` set when a condition on `input` holds — e.g. deny if a container runs as root. Multiple rules with the same name form a logical OR of violations.
Each deny rule is independent: if its body holds, its message joins the deny set; if not, it contributes nothing. So you write many small rules and the engine unions their messages — no if/else ladder.
package k8s
deny[msg] {
input.kind == "Pod"
c := input.spec.containers[_]
not c.securityContext.runAsNonRoot
msg := sprintf("container %v must set runAsNonRoot", [c.name])
}How does Rego evaluate a rule?Mid
Variables in brackets like [_] iterate every element; the rule body is a set of conditions that must all hold (logical AND); a default gives a value when nothing matches. There are no loops — iteration is implicit over collections.
# true if ANY container exposes port 22
ssh_exposed {
input.spec.containers[_].ports[_].containerPort == 22
}What is conftest used for?Mid
Running Rego policies against structured config files (Kubernetes YAML, Dockerfiles, Terraform plan JSON, HCL) in CI, so violations fail the build before anything reaches a cluster — shift-left policy.
conftest test deployment.yaml -p policy/ terraform show -json tf.plan | conftest test -
How do you test policies?Mid
Rego has a built-in test framework (`opa test`) — write test cases with sample input asserting allow/deny. Treat policies like code: unit tests, coverage, and CI so a policy change cannot silently stop enforcing.
test_denies_root {
count(deny) > 0 with input as {
"kind": "Pod",
"spec": {"containers": [{"name": "c"}]}
}
}
# run: opa test policy/ -vWhat is the data document vs input document?Senior
input is the request being evaluated (the object under admission); data is external context loaded into OPA (allowed registries, exemptions, org rules). Policies combine both to decide, keeping rules generic and data-driven.
Keeping the allowlist in data (not hardcoded in the rule) means updating policy is a data change, reviewable and reloadable without touching Rego. The same rule serves every team by swapping the data it reads.
How do ConstraintTemplate and Constraint work in Gatekeeper?Mid
A ConstraintTemplate defines the reusable Rego and a new CRD; a Constraint instantiates it with parameters and a scope (kinds/namespaces). This separates policy logic from where and how strictly it applies.
kind: K8sRequiredLabels # CRD from the template
metadata: { name: ns-needs-owner }
spec:
match: { kinds: [{ apiGroups: [""], kinds: ["Namespace"] }] }
parameters: { labels: ["owner"] }Audit mode vs enforce mode?Mid
Audit reports existing violations without blocking (safe rollout, backlog visibility); enforce rejects offending requests at admission. You typically ship a policy in audit first, fix violations, then flip to enforce.
spec: enforcementAction: dryrun # audit only; -> deny to block # kubectl get constraint shows current violations
Write a Kyverno policy that requires non-root.Mid
Kyverno policies are YAML: a rule matches resources and a validate block asserts a pattern, with validationFailureAction deciding audit vs enforce. No Rego required.
kind: ClusterPolicy
spec:
validationFailureAction: Enforce
rules:
- name: non-root
match: { any: [{ resources: { kinds: [Pod] } }] }
validate:
message: "runAsNonRoot is required"
pattern:
spec:
containers:
- securityContext: { runAsNonRoot: true }What can Kyverno do beyond validation?Mid
validate (block), mutate (inject sidecars, defaults, securityContext), and generate (create resources like default NetworkPolicies or RoleBindings per namespace) — plus verifyImages for signature checks, all in YAML.
rules:
- name: default-deny
match: { any: [{ resources: { kinds: [Namespace] } }] }
generate:
kind: NetworkPolicy
name: default-deny
namespace: "{{request.object.metadata.name}}"How do you handle exemptions safely?Senior
Data-driven exclusions (namespaces/labels) kept in Git and reviewed, time-bounded where possible, and audited — never ad-hoc disabling. The policy engine should log every exemption so exceptions do not quietly become the norm.
Model exemptions as data (an allowlist of namespaces/labels) committed to Git and PR-reviewed, ideally with an expiry, and make the engine log when an exemption is used. Ad-hoc disabling a policy in the cluster is invisible and becomes permanent.
How do you roll out a new policy without breaking prod?Mid
Ship it in audit/warn first to surface existing violations, fix or exempt them, then flip to enforce — ideally scoped to a few namespaces before going cluster-wide. Never land a new blocking policy straight into enforce.
CI-time policy vs admission-time policy — why both?Senior
CI (conftest) gives fast developer feedback and blocks bad config before merge, but can be bypassed. Admission enforces at the cluster as a backstop for anything applied out-of-band. Defense in depth uses the same policies at both gates.
CI catches issues early and cheaply but a determined user can skip it (direct kubectl, another pipeline). Admission cannot be bypassed but gives feedback late. Running the same policy source at both gates gets you fast feedback and a hard backstop.
How is OPA used for application authorization?Senior
Services query OPA (as a sidecar or library) with the request context and get an allow/deny plus obligations, externalizing fine-grained authz from app code. Policies and data update independently of deploys.
package authz
default allow := false
allow if {
input.method == "GET"
input.user.roles[_] == "reader"
}How do you distribute policy and observe decisions at scale?Senior
Bundle policies and data as versioned artifacts (OCI/bundle server) that OPAs pull, and ship decision logs to a central sink for audit and debugging. This gives one source of truth and a record of every allow/deny.
# OPA pulls versioned policy from a registry opa run -s --set bundles.main.resource=oci://reg/policy:1.4 # decision_logs -> central sink for audit
How does policy enforce supply-chain security?Senior
Admission policies verify image signatures/provenance (cosign, Kyverno verifyImages, Gatekeeper + sigstore) and restrict to allowed registries, so only signed, trusted artifacts run — turning supply-chain requirements into enforced gates.
rules:
- name: verify-signature
match: { any: [{ resources: { kinds: [Pod] } }] }
verifyImages:
- imageReferences: ["registry.example.com/*"]
attestors: [{ entries: [{ keys: { publicKeys: "-----BEGIN..." } }] }]