Exam Prep · CDP · professional · Know It Cold

Know It Cold — the configs you write from memory

The CDP doesn't hand you a multiple-choice question with four bubbles to guess between — it hands you a live environment and five challenges to actually solve, and every minute spent reconstructing syntax you half-remember is a minute the six-hour clock doesn't give back. Practical DevSecOps doesn't run a walled-garden allowlist the way some vendor exams do, and chatbots are explicitly barred regardless — so the honest trap isn't "you can't look anything up," it's that looking up a Dockerfile pattern or a Rego skeleton mid-challenge is always the wrong trade against a ticking clock. This page is the five configs worth having cold enough that you never make that trade: a minimal distroless Dockerfile, an OPA Rego policy skeleton, a Kyverno validate rule, an InSpec control block, and a gitleaks + TruffleHog pre-commit hook config. Not because you're forbidden from typing them slowly with a reference open — because on exam day you won't have the spare minutes to.

☺ Explain it like I'm 10

Imagine a cooking test where the fancy dishes aren't on any recipe card you're allowed to bring — you either know the recipe by heart, or you're improvising under a countdown timer while your other four dishes get cold. This page is five recipe cards. Reading them once feels like learning them. It isn't. Close the page, open a blank file, write one from nothing, and then come back and see what you actually got wrong.

🐰🐢Your hosts for this topic: Remy the Rabbit & Timmy the Turtle — Remy is pure reflex, the one who can type a distroless Dockerfile before you've opened a tab to look one up; Timmy is the guardrail who won't let "I remembered most of it" pass as done. Remy drills the shape; Timmy checks the one field that decides whether the whole thing actually gates anything.

How to use this page

☺ Like you're 10: You don't need every possible option. You need the shape — the handful of lines that carry the actual meaning — and the one field on each config that people forget and lose marks over.

Nobody memorizes every flag of every tool, and the exam doesn't reward trying to. What's worth having cold is the skeleton: the two or three lines that make a Dockerfile actually harden something instead of just building, the one field on a Kyverno policy that decides whether it blocks anything at all, the exact shape InSpec expects a control to take before it'll even parse. Get that much down without hesitation and the rest — an extra flag, an optional block — is something you can reason out live, because you're not also relearning the skeleton under pressure.

Read each block once for shape. Then close the page, open an empty file, and type it from nothing. Compare. Whatever gap you find between what you wrote and what's below is your real study list, and it will almost certainly be shorter and more specific than it feels right now — a missing USER line, not a whole forgotten tool. Anything you get wrong twice belongs on a flashcard in the flashcards deck, not on a third read of this page.

◆ The recovery move when memory genuinely fails

Every one of these five tools can hand you part of its own documentation back, with no network call and no browser tab. kubectl explain clusterpolicy.spec --recursive prints Kyverno's live schema if the CRD is already installed in the environment you're handed. inspec resource docker_container prints that resource's own usage example straight from the CLI. opa eval --help and conftest test --help remind you of exact flag names, even if they won't remind you of Rego syntax itself. docker history <image> and docker inspect --format '{{.Config.User}}' <image> let you verify a build actually landed non-root instead of trusting memory about what the base image defaults to. None of these substitute for knowing the skeleton — they're the fallback for the one field you blanked on, not for the whole shape.

Container hardening — a minimal distroless Dockerfile

☺ Like you're 10: Two building stages — one that's allowed to be messy because it never ships, and one that ships nothing but the finished program and a promise not to run as the most powerful user on the machine.

This is the config candidates reach for constantly across a CDP-style exam — a hardening task, a container-security challenge, a supply-chain step — and it's short enough that typing it from memory should cost under a minute. The shape that matters is a multi-stage build: a first stage with the full compiler toolchain that never gets shipped, and a second, minimal runtime stage that copies only the compiled artifact across.

# Stage 1 — build. Full toolchain, never ships.
FROM golang:1.22 AS build
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /out/app ./cmd/app

# Stage 2 — runtime. Distroless: no shell, no package manager, no coreutils.
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /out/app /app
USER nonroot:nonroot
ENTRYPOINT ["/app"]

Five things in that second stage are doing the actual hardening, and every one of them is worth being able to name unprompted: COPY --from=build is what keeps the compiler and the source tree out of the shipped image entirely — nothing from stage 1 crosses over except the named artifact. CGO_ENABLED=0 in the build stage produces a statically linked binary with no dynamic libc dependency, which is exactly what a static-debian12 base can run, since it has no shared libraries installed to link against at all — a binary built without that flag against a normal Linux toolchain will fail to start in this base image, not fail to build. The :nonroot tag selects the distroless variant that already runs as a fixed non-root UID (65532) by default. And USER nonroot:nonroot is written explicitly anyway, even though the tag already defaults to it, because a static-analysis or IaC scan checking the Dockerfile itself for a missing USER instruction has no way to know what a base image's runtime default is — it only reads the lines in front of it. No shell and no package manager in the final image also means there's nowhere for an attacker with code execution to pivot from — see container & supply-chain security for the full argument, and Container Runtime Security for what that buys you once the container is actually running.

Two more habits are worth having automatic rather than optional on exam day: never leave a base image on a bare :latest tag — it's mutable, so the same line of Dockerfile can resolve to a different image tomorrow than it did today — and when a task specifically asks for supply-chain immutability, pin the base by digest (@sha256:…) alongside or instead of the tag, so the exact bytes being built from can't shift under you even if the tag is later reused.

Policy as code — an OPA Rego policy skeleton

☺ Like you're 10: A rule that says "here's what has to be true," written against the fully-resolved plan of what's about to be built — not the raw source, which can still be full of unfilled-in blanks.

The canonical shape here is a Conftest-style deny rule checked against a rendered Terraform plan — the general-purpose, write-your-own-rules counterpart to a purpose-built scanner, and the one most likely to show up as "write a policy that blocks X" rather than "run a tool that already knows about X." The example below catches the textbook finding: a security group open to the entire internet.

# policy/terraform/security_groups.rego
package main

deny contains msg if {
    some rc in input.resource_changes
    rc.type == "aws_security_group_rule"
    rc.change.after.type == "ingress"
    rc.change.after.cidr_blocks[_] == "0.0.0.0/0"
    msg := sprintf("%s allows unrestricted ingress from 0.0.0.0/0", [rc.address])
}
# run it directly with opa, no Conftest install required
$ opa eval --format=pretty --data policy.rego --input plan.json "data.main.deny"

# or through conftest, against a rendered plan — not the raw .tf source
$ terraform show -json plan.out > plan.json
$ conftest test --policy policy/terraform plan.json

Four things in that skeleton carry the actual meaning. package main is the namespace every rule in the file lives under — Conftest looks for deny and warn specifically inside it by convention. deny contains msg if { ... } is Rego v1 syntax — if introducing the rule body and contains marking a partial-set rule — the default dialect since OPA v1.0; older examples you might half-remember from an earlier tutorial often write the same thing as deny[msg] { ... }, no if or contains at all. If a policy refuses to parse with a keyword-shaped error on exam day, that version mismatch is the first thing to check. some rc in input.resource_changes iterates the plan's array of resource changes one at a time — everything inside that rule body is implicitly ANDed, so all four conditions have to hold for one violation to fire. And critically: this rule is written against input.resource_changes, the shape of a rendered plan (terraform show -json), not a raw .tf file — a raw source file only contains literal values, so a CIDR block set from a variable shows up as the literal string "var.allowed_cidr", not the address it actually resolves to, and the rule silently never fires.

⚠ Watch out — repeated rule names union as OR, not AND

Two separate deny contains msg if { ... } blocks in the same file don't require both to fire — Rego evaluates each independently and unions the results, so either one firing on its own adds a message to deny. That's exactly right for a rule named deny: any one violation should still be a violation. It becomes a real bug the moment the identical habit is applied to an allow-shaped rule under time pressure — two separate allow blocks checking different conditions silently grant access if either one is satisfied. Write every required condition into one rule body for anything gating access; save repeated same-named blocks for deny and warn, where OR-as-union is the whole point.

Kubernetes-native policy — a Kyverno validate rule

☺ Like you're 10: The same idea as the Rego rule above, but written in plain Kubernetes YAML instead of a new language — and with one switch that decides whether it's a real gate or just a note to yourself.

Kyverno checks a live admission request at the moment a Kubernetes resource is actually being created, not a Terraform plan before anything exists — a different chokepoint than the Rego rule above, and one worth being able to write cold specifically because its YAML shape is easy to half-remember into something that parses but does nothing.

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-non-root
spec:
  validationFailureAction: Enforce   # default is Audit — logs only, blocks nothing
  background: true
  rules:
  - name: containers-must-run-as-non-root
    match:
      any:
      - resources:
          kinds: [Pod]
    validate:
      message: "containers must set securityContext.runAsNonRoot: true"
      pattern:
        spec:
          =(securityContext):
            runAsNonRoot: true
          containers:
          - =(securityContext):
              runAsNonRoot: true

One field decides whether this policy does anything at all: validationFailureAction. It defaults to Audit — the policy evaluates, findings land in a PolicyReport, and every non-compliant Pod is admitted anyway. Only Enforce turns a validate rule into an actual gate. Writing this skeleton from memory and forgetting to set it explicitly is a config that looks completely correct and blocks nothing, which is exactly the kind of mistake that's expensive to debug live and free to avoid by having the field memorized. The =() anchor around securityContext is a conditional match — it only checks the field if it's present, so a Pod that sets runAsNonRoot at the Pod level or per-container (either placement) satisfies the rule, but if neither is set anywhere, at least one =() block has nothing to match and the rule fails. match.any.resources.kinds is what scopes the rule to Pods in the first place — leave it too broad or too narrow and the rule either fires on the wrong resources or never fires on the right ones. background: true is what lets the background controller re-scan resources that already existed before this policy was ever applied, closing the gap a brand-new Enforce rule otherwise leaves for anything already running.

Compliance as code — an InSpec control block

☺ Like you're 10: A tiny program, not a sentence in a document — it states what a system should look like, and then it actually goes and checks, on the real machine, instead of hoping someone remembers to.

InSpec is one of the tools the CDP toolchain names directly, and its Ruby DSL is short enough to type cold once the shape is automatic: a control block with a metadata header, followed by one or more describe blocks asserting against a resource through an RSpec-style matcher.

# controls/ssh_and_packages.rb
control 'ssh-01' do
  impact 1.0
  title 'SSH root login must be disabled'
  desc 'Root must authenticate as a named user and use sudo, not log in directly over SSH.'
  tag cis: '5.2.8'

  describe sshd_config do
    its('PermitRootLogin') { should cmp 'no' }
  end
end

control 'pkg-01' do
  impact 0.5
  title 'OpenSSL must be at or above the fleet minimum'
  desc 'CVE tracking requires a patched OpenSSL version on every host.'

  only_if('only when openssl is actually installed on this target') do
    package('openssl').installed?
  end

  describe package('openssl') do
    its('version') { should cmp >= input('min_openssl_version') }
  end
end

The header lines aren't decoration — impact (a float from 0.0 to 1.0) is what InSpec's own reporters bucket into severity, and title/desc are what a report actually shows an auditor reading the results later, not just a developer watching the terminal. describe <resource> do ... end wraps whatever built-in resource the check is against — sshd_config, package, docker_container, port — and every assertion inside it is a matcher: its('field') { should ... } for a named property, it { should ... } for a resource-level check like be_running. cmp is the version- and type-tolerant comparator — deliberately treats "no", :no, and false as equivalent, and handles version-string comparison correctly, which plain eq won't. And only_if is the field people forget under pressure: a control with no guard fails on a target where the check doesn't even apply — no openssl package installed, no systemd present — reading as a real finding instead of "not applicable." Wrapping the check in only_if lets it skip cleanly instead of failing loudly for the wrong reason.

Secrets — a gitleaks + TruffleHog pre-commit hook config

☺ Like you're 10: One fast check that runs every single time you save your work, and one slower, more certain check that only runs right before your work leaves your machine — both live in the same file, but they don't run at the same moment.

gitleaks and TruffleHog answer the same core question — does this diff contain something credential-shaped — but at very different costs: gitleaks is regex-and-entropy matching with zero network calls, cheap enough for every commit; TruffleHog's real value is live verification against the issuing provider's API, which needs a network round-trip and is too slow to run on every keystroke. A single .pre-commit-config.yaml can run both, deliberately staged at two different points:

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.18.4                        # pin a real released tag, not a moving branch
    hooks:
      - id: gitleaks
        stages: [pre-commit]            # fast, zero-network — every commit

  - repo: local
    hooks:
      - id: trufflehog
        name: TruffleHog (verified secrets only)
        entry: trufflehog filesystem . --only-verified --fail
        language: system
        pass_filenames: false
        stages: [pre-push]              # slower, network calls — only at push time

Two decisions in that file are doing real work and are easy to forget under pressure. First, TruffleHog is wired as a repo: local hook with a plain entry command rather than pointed at a pinned upstream repo:/rev: pair — a defensible exam-day default, since it sidesteps having to remember whether the exact hook id and repo URL you last saw are still current, and it's guaranteed to work as long as trufflehog is on the runner's PATH. Second, --fail is not optional decoration on that command — by default TruffleHog exits 0 even when it finds and prints a verified, currently-active credential; without --fail, a real leaked secret can sit in the log while the hook reports success.

The stages field is the part that makes this config actually behave the way it reads. pre-commit install on its own only wires up the default pre-commit git hook — a hook declared with stages: [pre-push] sits in the config file, correctly written, and never fires until a second, separate command installs that hook type too: pre-commit install --hook-type pre-push (or -t pre-push). Forgetting that second install is the single most common way this exact two-tool config quietly does half its job. Pre-commit's own stage-name spelling has also shifted across major versions — older configs write commit/push, current ones write pre-commit/pre-push to match the underlying git hook names — so if a stage name is silently ignored, that mismatch is the first thing worth checking against whatever pre-commit version is actually pinned in the environment.

The fields people forget

☺ Like you're 10: Almost nobody forgets a whole recipe. People forget one pinch of salt — the same one, every time, on the same dish.

If you only revise one thing on this page in the last few minutes before a challenge, revise this table. Every row is a config that types out looking completely correct and does nothing when the named field is missing or wrong.

ConfigThe field people forget
Distroless DockerfileAn explicit USER nonroot:nonroot — a Dockerfile-level scan can't see the base image's runtime default, only the instructions in front of it; CGO_ENABLED=0 when the runtime base is static-debian12, or the binary won't start; pinning the base by digest, not just the mutable :nonroot tag
OPA Rego policydeny contains msg if { ... } (Rego v1) vs. the older deny[msg] { ... } — a parse error on exam day is almost always this; testing against terraform show -json output, never a raw .tf file, where variables are still literal placeholder strings
Kyverno ClusterPolicyvalidationFailureAction: Enforce — the default, Audit, evaluates and logs but blocks nothing; match.any.resources.kinds scoping the rule correctly; the =() anchor meaning "check this field only if present," not "require it"
InSpec controlonly_if to skip a check that doesn't apply to the target, instead of letting it fail for the wrong reason; impact/title/desc on every control — a report an auditor reads needs them even when the terminal output doesn't; a waiver file does nothing unless the run actually passes --waiver-file
gitleaks + TruffleHog pre-commit configA pinned rev: tag for gitleaks, not a moving branch; TruffleHog's --fail flag, or a verified live credential still exits 0; a second pre-commit install --hook-type pre-push for any hook declared with stages: [pre-push] — the default install alone never wires it up

Drill it — blank page, no notes

☺ Like you're 10: Reading these five blocks again feels productive. It isn't the same skill as writing one with the page closed — and only one of those is what gets tested.

Recognizing a config and being able to produce it from nothing are different skills, and a timed live-environment exam only tests the second one. Every block on this page will look obviously familiar after one read — that feeling of fluency is exactly what fools people at minute twenty of a real challenge, when the config still isn't actually on the page.

🐰 Remy's workshop · 20 min

Close this page. Open a blank file with no reference open. Write, from memory: a two-stage distroless Dockerfile for a Go binary, non-root, no :latest anywhere; an OPA Rego deny rule blocking a security group open to 0.0.0.0/0, tested with opa eval against a plan you'd get from terraform show -json; a Kyverno ClusterPolicy that actually enforces non-root Pods, not one that silently audits; an InSpec control checking sshd_config for a disabled root login, guarded with only_if; and a .pre-commit-config.yaml running gitleaks on every commit and a --fail-flagged, verified-only TruffleHog scan on every push. Then check each one against this page while you still can. Anything wrong twice goes on a flashcard in the flashcards deck. Then get the hands-on reps that make these skeletons automatic: Part 4 of the capstone for the Dockerfile, Part 5 for the Rego and Kyverno policy, and the policy-as-code drill and the leaked-credential triage drill for isolated, timed practice on the rest.

Run the drill more than once across your final study stretch. The first pass tells you what you genuinely don't know; a second pass, a few days later, tells you what didn't actually stick the first time. By the third, these five skeletons should come out without deliberation — because on the day, the config itself is meant to be the easy part of a task, and every second spent recalling it is a second not spent reading what the challenge actually asked for.

🎬 At the Shift-Left Squad
🐰

Remy: Blank file. Ninety seconds. Distroless Dockerfile, non-root. Go.

🦫

Benny: Two stages, COPY --from=build, base is gcr.io/distroless/static-debian12:nonroot. Done — it's already non-root by default, so that's the whole thing.

🐢

Timmy: Where's your USER line?

🦫

Benny: The tag already runs as UID 65532, Timmy, I checked that once.

🐢

Timmy: The tag does. Your Dockerfile still doesn't say so anywhere a scanner reading the file can see. Write USER nonroot:nonroot or the check flags a missing instruction, regardless of what the base image happens to do at runtime.

🤖

Recon: My turn — Rego, blocking a wide-open security group. package main, deny contains msg if, matched on the plan's resource changes.

🦊

Foxy: Tested against the raw .tf file, or the rendered plan?

🤖

Recon: ...the .tf. Which means any CIDR block coming from a variable shows up as the literal string var.allowed_cidr, not the address it resolves to. terraform show -json. Every time, not just when I remember.

🐘

Ellie: Pre-commit config — gitleaks and TruffleHog, one file, both hooks defined. pre-commit install. Shipped it.

🐢

Timmy: Did you install the push hook too, or just the default?

🐘

Ellie: ...just the default. TruffleHog's on stages: [pre-push] and I never ran pre-commit install -t pre-push. It's sitting in the file doing nothing.

🐰

Remy: And that's the whole page in one drill — everyone had the shape right, and everyone lost the one field that actually gates something. Again. Blank file.

That's the page — five skeletons, the field on each that quietly costs marks, and one drill that turns reading into recall. Pair it with the CDP exam guide for the exam's format and logistics, the CDP study plan for how these fit a week-by-week schedule, Static Analysis & Secrets Detection and Infrastructure as Code Hardening for the concepts behind the Rego and Kyverno skeletons, Compliance as Code at Scale for where the InSpec control fits a fleet-wide evidence story, the command & tool reference for the muscle-memory flags around these configs, and the triage playbook for when one of them applies cleanly and still doesn't do what you expected. Then close all of them and open an empty file.

✓ Checkpoint

1. In the distroless Dockerfile, why write USER nonroot:nonroot explicitly when the :nonroot tag already defaults to a non-root UID? 2. Why does an OPA Rego policy tested against a raw .tf file often pass when the real, rendered Terraform plan would have failed the same check? 3. Which field on a Kyverno ClusterPolicy decides whether a validate rule actually blocks anything, and what does it default to? 4. In an InSpec control, what does wrapping a check in only_if change about the result on a target where the check doesn't apply? 5. You wired both gitleaks and TruffleHog into one .pre-commit-config.yaml, ran pre-commit install once, and TruffleHog never fires on a push. What's missing, and what would silently keep going wrong even if it did fire?

Check your answers
  1. Because a static-analysis or IaC scan checking the Dockerfile itself has no visibility into what a base image defaults to at runtime — it only reads the instructions written in the file. The :nonroot tag genuinely does run as a non-root UID by default, but a scan (or a reviewer) checking for hardening evidence in the Dockerfile needs the USER line stated explicitly to see it.
  2. A raw .tf file only contains literal source values — any value set from a variable, module input, or provider default appears as an unresolved placeholder string (like "var.allowed_cidr"), not the value it actually resolves to. terraform show -json produces the fully resolved plan, which is the only version that reflects what will actually be applied.
  3. spec.validationFailureAction. It defaults to Audit, which evaluates the rule and records findings in a PolicyReport without blocking anything; only Enforce makes the rule an actual admission-time gate.
  4. only_if makes the control skip cleanly — reported as not applicable — on a target where the guard condition isn't met, instead of running the check anyway and having it fail for a reason that has nothing to do with real non-compliance (a missing package, a tool that isn't installed, an OS feature that doesn't exist on that host).
  5. Missing: a second, separate pre-commit install --hook-type pre-push — the default pre-commit install only wires up the standard pre-commit git hook, and a hook declared with stages: [pre-push] sits in the config doing nothing until that push-hook type is installed too. Even once it fires, the --fail flag on the TruffleHog command still has to be present, or a verified, currently-active credential gets printed to the log and the hook still exits 0.