CI/CD interview questions
Prep CI/CD interviews from pipeline basics to secure delivery: artifacts, environments, runners, progressive deploys, OIDC, and supply-chain gates.
Beginner → Intermediate → Advanced → Expert — answers are written the way you’d say them in a real interview. Advanced and Expert go deeper with war-story detail, architecture diagrams, and the follow-up an interviewer often asks next.
What is CI and what is CD?Beginner
The short version: CI merges and automatically builds and tests every change. Continuous Delivery keeps artifacts always releasable with a manual prod gate; Continuous Deployment removes that gate and ships when checks pass.
CI stops at a tested artifact; delivery adds a gate; deployment ships automatically.
What are the typical stages of a pipeline?Beginner
I'd sketch build/package, test — unit, integration, lint, security scans — publish an immutable artifact, then deploy or promote through environments. Each stage should fail fast with logs you can actually read.
stages: [build, test, deploy] build: stage: build script: [make build] test: stage: test script: [make test]
What is an artifact and why store it?Beginner
It's the immutable output of a build — jar, image, binary, package. Build once, store it with a version or digest, and promote those same bits across environments instead of rebuilding per env.
docker build -t registry/app:$CI_COMMIT_SHA . docker push registry/app:$CI_COMMIT_SHA # deploy that exact digest/tag to dev → stg → prod
What is a runner or agent?Beginner
It's the worker that executes pipeline jobs — shared or dedicated, ephemeral or long-lived. I prefer ephemeral runners: clean environment per job and a smaller blast radius if a build gets compromised.
Long-lived runners are faster but they accumulate caches, credentials, and drift. Short-lived VMs or containers that tear down after the job are the default I'd argue for unless you've got a clear reason otherwise.
What triggers a pipeline?Beginner
Usually a push or merge request — also tags, schedules, manual/API triggers, or an upstream pipeline. I scope jobs with rules so deploy only runs where it should.
deploy:
script: [./deploy.sh]
rules:
- if: '$CI_COMMIT_BRANCH == "main"'Caching vs artifacts — what is the difference?Intermediate
Cache reuses inputs like dependencies across jobs and is best-effort. Artifacts are job outputs passed to later stages and have to be present. I'd never use cache to hand the build product to deploy.
build:
script: [make build]
artifacts:
paths: [dist/]
expire_in: 1 weekHow do environments and protected branches fit together?Intermediate
Protected branches — main, release — require reviews and status checks; protected environments add approvals and restrict which jobs and credentials can deploy to prod. Together they stop random branches from shipping.
deploy-prod:
stage: deploy
when: manual
environment:
name: production
rules:
- if: '$CI_COMMIT_BRANCH == "main"'What does “build once, promote many” mean?Beginner
Compile or package a single immutable artifact, then promote that same digest through dev, staging, and prod. Rebuilding per environment reintroduces drift and invalidates the earlier test evidence.
IMAGE=registry/app@sha256:abc123 # same IMAGE in helm values for stg and prod
Fail-fast vs fail-safe in pipelines — what is the tradeoff?Intermediate
Fail-fast stops on the first broken check to save minutes; fail-safe may run more jobs to gather signal. I'd fail-fast for expensive deploys, and sometimes continue on non-blocking linters so one MR surfaces all the issues.
trivy-fs: script: [trivy fs --exit-code 0 .] allow_failure: true
How do you speed up a slow pipeline?Intermediate
I'd measure stage timings first, then cache dependencies, parallelize independent jobs, split slow suites, use path filters in monorepos, and keep images small. Optimize the longest stage, not the loudest complaint.
test:
parallel: 4
cache:
key: $CI_COMMIT_REF_SLUG
paths: [node_modules/]
script: [npm test]How do you promote a build across environments?Intermediate
Promote the same versioned artifact; inject env-specific config at deploy time. Gate production with approvals and automated checks so only config differs between envs — not the bits.
helm upgrade app ./chart -f values-prod.yaml \ --set image.tag=$CI_COMMIT_SHA
How do you design pipelines for a monorepo?Advanced
I'd detect changed paths and run only affected packages — path rules, Nx, Bazel, Turborepo — share caches, and fan out per-service jobs. Otherwise every commit rebuilds everything and people start skipping checks.
Without change detection, CI becomes the bottleneck. Use rules:changes or a build graph to select targets; keep a nightly full build for coverage gaps. Shared templates should still pin tool versions. A change in a shared lib rebuilding all consumers is correct, not a bug. I'd rather fail closed on undetected graph edges than silently skip tests.
api-test:
script: [make -C services/api test]
rules:
- changes: [services/api/**/*]Interviewer often follows with: What do you run when package.json at the repo root changes?
How do you make pipeline config reusable?Intermediate
Factor shared jobs into templates — GitLab include/extends, GitHub reusable workflows or composite actions — so a fix lands everywhere and pipelines stay DRY.
.base-test: image: python:3.12 before_script: [pip install -r requirements.txt] unit: extends: .base-test script: [pytest -q]
How should secrets be handled in CI?Intermediate
Store them in the platform secret store or Vault, inject as masked variables at runtime, scope to jobs and protected branches, never echo them, and prefer short-lived OIDC tokens over static cloud keys.
deploy:
script:
- export TOKEN=$(vault kv get -field=token secret/deploy)
# never: echo "$TOKEN"
environment: productionA job needs cloud access but must not expose long-lived keys to fork PRs. How do you design it?Expert
I'd split untrusted build from trusted deploy: fork and PR jobs get no secrets and can't deploy; only pipelines on protected branches after merge assume an OIDC role. Approvals gate first-time fork workflows.
Classic failure: pull_request_target or shared secrets on all pipelines let a fork exfiltrate credentials — the pwn request. Fix: secrets only on protected refs; OIDC trust conditioned on repo plus ref; environment protection; no privileged checkout of untrusted code with a write token. PR build artifacts can be unsigned and discarded; signed provenance is produced only on the trusted builder after merge.
# Trust policy condition examples: # token.actions.githubusercontent.com:sub # repo:org/app:ref:refs/heads/main # Never attach this role to pull_request from forks
Interviewer often follows with: How does pull_request_target differ from pull_request in GitHub Actions?
What is matrix / fan-out testing?Beginner
One job definition runs across combinations — OS, language version, browser. It catches compatibility bugs early but multiplies minutes, so I'd keep the matrix focused on what we actually support.
strategy:
matrix:
node: [20, 22]
steps:
- uses: actions/setup-node@v4
with: { node-version: ${{ matrix.node }} }Blue-green vs canary vs rolling — how do you choose?Intermediate
Blue-green flips all traffic between two stacks for instant rollback at roughly double cost. Canary shifts a small percentage and watches metrics before ramping — safest when telemetry is good. Rolling replaces in place — cheapest, slower to unwind.
I pick by risk tolerance, infra budget, and observability. Stateless web apps fit all three; sticky sessions and one-shot migrations constrain canary and rolling. Always define abort criteria before you shift traffic.
Walk me through a blue-green cutover in Kubernetes and how you roll back in under a minute.Advanced
I'd run blue and green Deployments behind one Service. Cutover is changing the Service selector — or Ingress weight — to green after smoke tests. Rollback points the selector back at blue; no rebuild required.
Both colors need full capacity before the flip if you want instant rollback. Keep blue scaled until green proves healthy on error rate, latency, and business KPIs. Database migrations must be backward compatible so blue can still serve after rollback. Cost is roughly 2× compute during the window. Alternatives: mesh traffic split or DNS weighted records. Document who can flip, and automate the selector patch in the pipeline with a manual approval gate for prod.
Traffic flips by selector; blue stays hot for instant rollback.
# after green is Ready and smoke-tested:
kubectl patch svc web -p '{"spec":{"selector":{"version":"green"}}}'
# rollback:
kubectl patch svc web -p '{"spec":{"selector":{"version":"blue"}}}'Interviewer often follows with: How do you handle a breaking schema change with blue-green?
How do you roll back a bad deploy?Intermediate
Redeploy the previous known-good artifact or flip traffic back — blue-green or canary. Keep deploys immutable and versioned, and make DB migrations expand/contract so rollback doesn't require restoring the database.
kubectl rollout undo deploy/web helm rollback app 42 # canary: set weight back to 0% on the new version
How do feature flags change deployment?Intermediate
They separate deploy from release: ship code dark, enable per cohort at runtime, kill-switch without redeploying. The cost is flag lifecycle — stale flags become debt.
Flags let you test in production safely, but they need ownership, defaults for failure modes, and removal after rollout. I'd never use flags as a substitute for fixing a broken pipeline.
How do you handle database migrations in a pipeline?Advanced
Use expand/contract: additive migration first, deploy code that uses both old and new, backfill, then drop old columns in a later release. Never couple a destructive migration to a rolling deploy.
During a rolling update, old and new pods coexist. A drop-column migration breaks old pods still reading that column. Expand/contract keeps both versions compatible. Run migrations as an explicit pipeline job or PreSync hook with locks, timeouts, and a dry-run in staging. For high risk, separate the migrate window from the app release. Backup and PITR stay mandatory.
migrate:
script: [./migrate.sh up]
rules: [{ if: '$CI_COMMIT_BRANCH == "main"' }]
deploy:
needs: [migrate]
script: [./deploy.sh]Interviewer often follows with: What's a dual-write period and when do you end it?
How do you gate production deploys?Beginner
Protected environment with required reviewers, required green checks, deploy only from main or release tags, and often a manual promotion step. Prod credentials exist only on that environment.
deploy-prod:
when: manual
environment: production
rules:
- if: '$CI_COMMIT_BRANCH == "main"'Why use OIDC to cloud instead of stored access keys?Advanced
The pipeline presents a short-lived, workload-bound identity token; the cloud exchanges it for temporary credentials scoped to repo and branch. Nothing long-lived to leak or rotate.
Static keys in GitHub or GitLab secrets are shared, hard to attribute, and survive forever if forgotten. OIDC ties STS issuance to claims like sub, aud, and ref. Condition the trust policy on the exact workflow file for critical roles. Combine with least-privilege IAM per environment, and audit CloudTrail for AssumeRoleWithWebIdentity. Locally developers use SSO, not the CI role.
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123:role/ci-deploy
aws-region: us-east-1Interviewer often follows with: What claim would you use to pin a role to one workflow file?
How do you harden CI runners?Advanced
Prefer ephemeral, isolated executors; no privileged Docker-in-Docker by default; pinned images; secrets not written to disk longer than needed; network egress controlled; and separate runners for untrusted vs trusted jobs.
A shared privileged runner is a lateral-movement prize — steal cloud tokens, mine crypto, or poison caches. Mitigations: Kubernetes executor or VM-per-job, drop capabilities, read-only root where possible, rotate registration tokens, and watch for unexpected processes. Never mount the host Docker socket into untrusted jobs. Cache poisoning is real — scope caches by branch and prefer content-addressed dependencies.
# config.toml — do NOT set privileged=true for untrusted projects
[[runners]]
executor = "docker"
[runners.docker]
image = "golang:1.22"
privileged = false
disable_entrypoint_overwrite = trueInterviewer often follows with: Why is mounting /var/run/docker.sock dangerous on a runner?
What supply-chain gates belong in a secure pipeline?Intermediate
SAST, SCA or dependency scan, secret scanning, container and IaC scan, SBOM generation, and artifact signing with verification — with a failure policy that blocks criticals without drowning the team in noise.
security:
stage: test
script:
- gitleaks detect --no-git
- trivy image --exit-code 1 registry/app:$SHA
- syft packages registry/app:$SHA -o cyclonedx > sbom.json
- cosign sign --yes registry/app:$SHAHow do you verify a signed image before it reaches production?Expert
CI signs with cosign — key or keyless — after build; admission or the deploy job verifies the signature and optionally provenance against a trusted identity before apply. Unsigned or wrong-identity images fail closed.
Signing without verification is theater. Verify in two places when you can: deploy pipeline with cosign verify, and cluster admission with Kyverno or Gatekeeper plus sigstore. Keyless binds to the OIDC identity of the builder — pin that identity in policy. Prefer digests over tags. Store SBOM and attestations alongside the image. Rotate trust roots carefully with a dual-allow window.
Build signs; cluster or pipeline verifies before the workload runs.
cosign verify \ --certificate-identity-regexp 'https://github.com/org/app/.github/workflows/.*' \ --certificate-oidc-issuer https://token.actions.githubusercontent.com \ registry/app@sha256:abc... kubectl set image deploy/web app=registry/app@sha256:abc...
Interviewer often follows with: Tag mutable vs digest — which do you put in production manifests?
What is pipeline injection and how do you prevent it?Advanced
Untrusted input — PR titles, branch names, fork code — interpolated into a shell runs with pipeline privileges. Don't run privileged jobs on untrusted PRs; pass input via env vars and quote it; isolate fork builds without secrets.
echo ${{ github.event.issue.title }} is a classic RCE vector. Bind to an environment variable, then use "$TITLE". Prefer curated actions pinned by SHA. Treat YAML from contributors as untrusted code.
- run: echo "title is $TITLE"
env:
TITLE: ${{ github.event.pull_request.title }}Interviewer often follows with: Why pin third-party actions to a full commit SHA?
What is SLSA / build provenance at a high level?Expert
SLSA is a tiered supply-chain integrity framework; provenance is signed metadata about how and from what an artifact was built. Higher levels need a hardened, non-falsifiable builder so consumers can verify origin.
Provenance answers who built this, from which commit, with which workflow. Consumers verify the signature and check that the builder identity matches policy before deploy. Generating provenance on an admin-writable shared runner is weaker than a hermetic, isolated builder. Pair with SBOM and signature verification — provenance alone doesn't mean the code is safe, only that the build path is attested.
cosign attest --predicate provenance.json --type slsaprovenance \ registry/app@$DIGEST cosign verify-attestation --type slsaprovenance registry/app@$DIGEST
Interviewer often follows with: What does non-falsifiable builder mean in SLSA L3?
How do you secure third-party Actions or CI plugins?Intermediate
Pin to a full commit SHA, review permissions requested, minimize the set allowed org-wide, and treat them as code running with your pipeline's privileges.
# mutable tag — can move uses: actions/checkout@v4 # prefer: uses: actions/checkout@11bd4402bf91633182c8a0d2b0c0c8c8c8c8c8c8
What is a poisoned pipeline execution (pwn request)?Expert
A fork PR that triggers privileged CI with secrets or a write token, letting an attacker exfiltrate credentials or push malicious code. Fix: no secrets on fork builds, require approval to run CI on forks, separate untrusted build from trusted deploy.
pull_request_target running in the base-repo context while checking out PR code is the usual footgun. Combine with cache poisoning or script injection and you've got a full compromise. Controls: workflow approvals for first-time contributors, environment secrets only on protected refs, OIDC roles that can't be assumed from PR events, and CODEOWNERS on workflow files.
# Fork PR pipelines: tests only, no cloud roles, no deploy keys # Trusted deploy: only after merge to protected branch
Interviewer often follows with: Where should CODEOWNERS protect workflow YAML?
Canary deploy looks healthy on latency but checkout conversion dropped. What do you do?Advanced
I'd abort the canary on the business KPI, flip traffic back, and treat golden signals as necessary but not sufficient — wire product metrics into the analysis before ramping further.
Infra metrics miss domain failures — wrong price, auth edge case, empty search. Progressive delivery needs SLIs that reflect user outcomes, not only p99. Define abort thresholds and ownership before the release. After rollback, bisect with feature flags if the artifact must stay deployed dark. Log the decision in the incident timeline so the next release reuses the same gates.
# Flagger / Rollouts style: set canary weight to 0 kubectl argo rollouts abort web kubectl argo rollouts undo web
Interviewer often follows with: Which three metrics would you require before a 50% ramp?
How do you keep pipeline secrets out of logs and artifacts?Beginner
Mask variables in the CI platform, never echo secrets, avoid writing them into build artifacts or Docker layers, and scan logs and artifacts for high-entropy tokens in CI.
# GitLab: masked + protected variable DEPLOY_TOKEN script: - curl -H "Authorization: Bearer $DEPLOY_TOKEN" https://api/... # never: echo $DEPLOY_TOKEN | tee token.txt
What is an ephemeral vs persistent runner, and when is each justified?Intermediate
Ephemeral runners are created per job and destroyed after — clean and safer. Persistent runners reuse a machine for speed or special hardware like GPUs. Default to ephemeral; isolate and harden any persistent fleet.
Persistent runners need patching, credential scrubbing between jobs, and strong tenancy. Document why each non-ephemeral runner exists and who owns it.
How do you version and retain build artifacts for audit?Intermediate
Tag images by commit SHA or digest, retain them per policy — say 90 days for feature builds, longer for release tags — and keep SBOM and provenance beside the artifact so you can prove what shipped.
docker push registry/app:$CI_COMMIT_SHA docker tag registry/app:$CI_COMMIT_SHA registry/app:release-2026-07-24 # registry GC policy: keep digests referenced by release-*
Your blue-green cutover succeeded on health checks, but 10% of users still hit the old color for 20 minutes. What went wrong?Advanced
I'd look for sticky sessions, DNS TTLs, CDN caches, or multiple ingress paths that weren't flipped together — healthy green doesn't equal 100% traffic shift.
Blue-green needs an atomic traffic switch at every edge: load balancer listener, ingress, API gateway, and CDN behaviors. Long DNS TTLs and client connection pools delay cutover. Sticky cookies keep sessions on blue. Mitigations: low TTL or ALB weighted target groups, drain connections, invalidate CDN, verify with synthetics from multiple resolvers, and a hard kill switch to flip weights to 0/100. Pair with dual-write or session externalization so color affinity isn't required. Separate "deployed" from "receiving traffic."
# ALB weighted forward config → 0% blue / 100% green # curl -sI https://app.example | grep -i x-color # dig +ttl app.example # watch TTL during cutover
Interviewer often follows with: How do you drain WebSocket clients during a blue-green flip?
A deploy pipeline is green, but production pods crash-loop on a missing ConfigMap key. Where should the gate have caught it?Advanced
I'd add a render-and-validate step — kubeconform, conftest, dry-run — against the exact overlays that ship, plus a smoke test in staging that exercises the new key. Unit tests alone don't prove Kubernetes config.
CI often builds the image and skips manifest validation, or uses a different values file than prod. Fix: helm template or kustomize build of the prod overlay in CI, schema validate, policy checks, and deploy to staging with the identical chart version. Optional: kubectl apply --dry-run=server against a non-prod cluster. At runtime, fail fast on missing required env with a startup check. This is the classic pipeline-green, prod-red scenario.
helm template web charts/web -f values/prod.yaml | kubeconform -strict - helm template web charts/web -f values/prod.yaml | conftest test -
Interviewer often follows with: Would you block merge on missing keys or only block deploy?
An attacker pushes a malicious commit to a dependency submodule that your release pipeline vendors. How do you detect and contain it?Expert
I'd pin submodules and dependencies by commit SHA with signature or hash verification, scan diffs in CI, revoke any builds that included the bad commit, and rotate credentials those builds could have seen.
Submodule and go.mod/npm supply-chain attacks succeed when CI floats on a branch ref. Controls: pin SHAs, verify checksums and lockfiles, disable automatic submodule update on untrusted PRs, CODEOWNERS on dependency manifests, and SBOM comparison between builds. Containment: identify digests built after the poison commit, block those digests, rotate CI OIDC roles and any secrets available to the job, audit git history. Long-term: vendoring policy plus Dependabot with human review. Talk pins, attestations, and credential blast radius together.
git submodule status # expect detached SHA, not branch # CI: fail if submodule points to unexpected SHA cosign verify-attestation registry/app@$DIGEST
Interviewer often follows with: How do lockfiles fail to protect you if CI runs npm install without ci mode?
You need zero-downtime DB migrations that must run exactly once across multiple pipeline replicas. How do you design it?Expert
I'd use expand/contract migrations, a distributed lock or migration runner with leadership — Flyway, Liquibase, or a job with a lease — and never let every canary pod race DDL on startup.
Startup-migrate-on-boot races cause duplicate DDL and locks. Prefer a dedicated migrate job in the pipeline before shifting traffic. Expand/contract: additive schema → deploy dual-write/read code → backfill → remove old columns later. Gate production promote on migrate exit code. For rollback, avoid irreversible destructive DDL in the same release as the app that needs it. Exactly-once leadership plus expand/contract — not "flyway in every pod."
# pipeline: # 1) migrate job (one replica, DB lock) # 2) deploy app # 3) shift traffic / canary flyway -url="$DB" migrate
Interviewer often follows with: How do you roll back an expand/contract release mid-contract phase?
OIDC federation to AWS is misconfigured so every branch can assume the prod deploy role. How do you fix trust and prove it?Expert
I'd tighten the trust policy conditions on sub, ref, and workflow, remove wildcards, require environment protection, and prove it with a failing PR job that cannot AssumeRole into prod.
Common footgun: StringLike sub repo:org/name:* without ref conditions. Fix: allow only refs/heads/main and release tags, optionally pin the workflow file, and separate roles per env. GitHub Environments add required reviewers. Verification: open a feature-branch workflow that attempts sts:AssumeRole and expect AccessDenied — keep that as a conformance test. Audit CloudTrail for unexpected assumers. Show concrete IAM condition keys, not just "we use OIDC."
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:sub": "repo:acme/app:ref:refs/heads/main"
}
}Interviewer often follows with: How do you allow hotfix tags without allowing every tag in the repo?
A monorepo CI takes 45 minutes and teams skip checks. How do you redesign for speed without losing security gates?Expert
I'd introduce path-filtered pipelines, remote build caches, test impact analysis, and tiered gates — always-on secret and policy scans for touched paths, full suites on release paths — so feedback is minutes, not optional.
Slow CI creates shadow IT and force-merge culture. Techniques: change detection, remote cache or container layer cache keyed safely, parallel shards, and split merge-gate vs nightly deep. Security: never skip gitleaks or OPA on path filters that could still introduce secrets or workflows; workflow files always trigger full CI security jobs. Measure p50 PR feedback time and force-merge rate. Balance speed engineering with non-negotiable security jobs.
# security job: on paths [**/.github/workflows/**, **/Dockerfile, **/*] # unit job: only when packages/foo/** changes
Interviewer often follows with: What's a safe cache keying scheme that avoids cache poisoning across forks?
Production rollback must complete in under five minutes after a bad artifact. What must already be true in your CI/CD design?Expert
Previous digests remain pullable, traffic shifting is automated — not a rebuild — config and DB are rollback-compatible, and the rollback path is tested. Hoping that main still builds isn't a plan.
Rollback isn't re-running the pipeline from an old commit if the registry GC'd the tag or migrations are irreversible. Design: immutable digests retained, one-click traffic revert, feature flags for dark code, and expand/contract DB discipline. Run game days that execute rollback under a timebox. Observability abort rules should trigger the same path. Artifact retention plus traffic control plus schema compatibility is the triad.
kubectl argo rollouts undo web # or: set ALB weight previous-TG=100 # confirm: digest still in registry (retention policy)
Interviewer often follows with: How do you handle rollback when the bad release already ran a destructive migration?
A stolen CI deploy token just pushed an unknown image to production. What do you do in the first hour, and how do you stop it recurring?Expert
I'd freeze prod deploys, roll traffic to the last known-good digest, revoke and rotate the token and any secrets it could reach, then replace long-lived deploy tokens with OIDC and protected environments so a leak can't ship alone.
Contain first: pause pipelines, revoke the PAT or deploy key, invalidate registry push credentials, and verify no second backdoor job was added. Evidence: who used the token, which digests landed, CloudTrail for the window. Remediation: short-lived OIDC, environment approvals, signed images with admission verify, CODEOWNERS on workflow files. Rehearse token-leak → deploy-freeze as a game day. Map the blast radius of every secret that token could read, not only the deploy step.
# revoke token in IdP; disable prod environment kubectl set image deploy/web web=registry/app@sha256:GOOD cosign verify --certificate-identity-regexp '...' registry/app@sha256:GOOD
Interviewer often follows with: How do you prove the rolled-back digest is the same bits that passed staging?
Flaky tests are marked allow_failure, and a real regression slipped to staging. How do you redesign the suite without blocking every PR?Advanced
I'd quarantine flakes into a tracked job with ownership and SLOs, keep critical-path tests hard-failing, and ban blanket allow_failure on anything that gates correctness.
allow_failure on the whole suite hides signal. Split must-pass — unit, contract, smoke — from a flake farm with retries, ownership labels, and burn-down. Track flake rate as a KPI; auto-open issues when a test flakes N times. Don't use retries to paper over non-deterministic prod bugs. Gate merge on must-pass only; run broader suites on main or nightly. Distinguish intermittent infra from product non-determinism.
unit:
script: [npm test -- --group=must]
# no allow_failure
flake-quarantine:
script: [npm test -- --group=flaky]
allow_failure: true
retry: { max: 2 }Interviewer often follows with: When is retrying a failed test the wrong mitigation?
Job A builds an unsigned artifact; Job B (privileged) deploys “whatever is latest in the bucket.” An attacker replaces the object mid-pipeline. What failed?Expert
We had a confused-deputy path: deploy trusted a mutable name instead of a digest bound to the trusted build identity. I'd pin by digest, verify signature or provenance, and stop reading latest from shared storage.
Confused deputy: a privileged deployer acts on an attacker-controlled name — latest tag, mutable S3 key, shared cache. Fix: content-address digests, sign at build, verify before promote, separate trust domains so PR builds can't overwrite release artifacts. Use distinct bucket prefixes or repositories per trust level. Admission or deploy-time cosign verify closes the gap if registry tags move. Talk attestation identity — who built — plus immutability — what bits.
DIGEST=$(jq -r .digest build-meta.json) cosign verify registry/app@$DIGEST helm upgrade web ./chart --set image.digest=$DIGEST
Interviewer often follows with: Why is passing an artifact URL between jobs still unsafe without a digest?
OIDC to AWS was “working” until a feature-branch workflow assumed the prod role. How do you find the misconfig and prove the fix?Advanced
I'd read the IAM trust policy for wildcards on sub or ref, tighten to main and protected environments only, then run a negative test from a PR that must get AccessDenied.
Typical bug: StringLike repo:org/app:* or a missing ref condition. Also check audience and which workflow files can request the token. Use GitHub Environments with required reviewers for prod. Keep a conformance workflow on PRs that attempts AssumeRole and asserts failure. Audit CloudTrail AssumeRoleWithWebIdentity. Show concrete condition keys — "we use OIDC so we're safe" isn't an answer.
# on pull_request — expect failure aws sts assume-role-with-web-identity ... || echo "denied-as-expected" # trust: sub = repo:acme/app:ref:refs/heads/main only
Interviewer often follows with: How do you allow release tags without allowing every tag?
Canary auto-promoted to 100% in three minutes; error rate spiked after users woke up. What was wrong with the analysis window?Advanced
The analysis was too short and too narrow — it missed diurnal traffic and business KPIs. I'd lengthen windows, require multi-signal abort rules, and block promote until soak SLOs hold under real load.
Fast canaries pass on synthetic or low-traffic periods. Require error rate, latency, saturation, and at least one business SLI; minimum soak under peak-like load or scheduled windows; manual gate for high-risk. Abort must be automatic and faster than human reaction. Pair with feature flags for dark launch. Progressive delivery is an observability contract, not just a YAML weight schedule.
analysis:
interval: 5m
iterations: 12 # ≥1h soak
thresholds:
- error_rate < 1%
- p99_latency < 300ms
- checkout_success > 99%Interviewer often follows with: Which metric would you refuse to omit for a payments API canary?
A pipeline printed a cloud access key in logs (“debug echo $AWS_SECRET_ACCESS_KEY”). What is your incident playbook?Advanced
I'd treat the key as compromised: rotate or disable it immediately, scrub or restrict log retention access, hunt for use in the leak window, and fix the job to mask secrets and ban echo of credential env vars.
Assume public exposure if logs are readable by many engineers or retained in artifacts. Rotate first, investigate second. Enable secret scanning on logs where available; mark CI variables masked; use OIDC so there's no long-lived key to print. Add a CI lint rule that fails on echo or print of known secret env names. Post-incident: who had log read, and were forks able to see it?
# GitLab: masked + protected variable # GitHub: secrets.* never echo; prefer OIDC # fail CI if script matches echo $.*SECRET
Interviewer often follows with: How do you rotate if the printed credential was an OIDC-assumed role session?
A public fork PR ran on your self-hosted runner and exfiltrated a repo secret. What design mistake enabled that?Expert
Secrets or privileged runners were available to pull_request from forks. I'd isolate fork PRs to untrusted ephemeral runners with no secrets, require approval for first-time contributors, and reserve secrets for protected refs only.
Classic pwn-request: pull_request_target or shared self-hosted runners with org secrets. Controls: Environment secrets only on main, require approval for first contribution, never checkout PR code in a context that has write tokens, and label runners so fork jobs can't schedule onto privileged pools. Cache poisoning across forks is related — scope caches by repo and ref trust. Draw the trust boundary between contributor code and credentialed build.
# secrets: none on pull_request from forks # self-hosted labels: [trusted] only on push to main # workflow_run or environment protection for deploy
Interviewer often follows with: Why is pull_request_target dangerous when combined with checkout of the PR ref?
You rolled back the app binary, but the DB migration in the bad release already dropped a column. Users are down. How do you recover?Expert
I'd stop further deploys, restore schema from backup or a forward-fix migration that recreates the column, then redeploy a build compatible with the restored schema — and ban destructive DDL in the same release as the app that needs it.
Rollback of code isn't rollback of schema. Expand/contract discipline: never drop or rename in the same release that removes readers. Recovery options: PITR, replay a dual-write period, or an emergency additive migration. Pipeline should run migrate as a gated job with dry-run and require backward-compatible migrations for rollback windows. Game-day this failure. Rollback SLA must include the data plane, not only image tags.
# restore column (forward fix) or PITR to pre-migrate # deploy last app digest that matches restored schema # block prune/drop migrations without a compatibility window
Interviewer often follows with: How would expand/contract have prevented this outage?
Nightly rebuild of “the same” release tag produced different digests than what production runs. Leadership asks if prod is still trustworthy. How do you answer?Advanced
I'd explain build-once/promote-many: prod should track the digest that was tested, not a tag rebuilt later. Compare attestations, freeze floating tags, and show the signed provenance of the running digest.
Rebuilding invalidates the evidence chain — dependency drift, base image updates, non-hermetic builds. Controls: immutable digests in prod manifests, retention of release artifacts, cosign/SBOM attached at original build, and CI that refuses to retag release digests. If rebuild is required for a CVE rebase, treat it as a new release with full retest. Reproducibility and immutability both matter, but prod pins bits.
kubectl get deploy web -o jsonpath='{.spec.template.spec.containers[0].image}'
cosign verify registry/app@sha256:PROD
# do not docker build -t v1.2.3 again and pushInterviewer often follows with: What breaks if your registry allows tag overwrite on v1.2.3?
A matrix job uploads coverage after failure, and the “green” status check is a different job that only waits on upload. How do you stop false greens?Advanced
I'd make the required status check the actual test job — or a gate that ANDs all must-pass jobs — and never let an artifact-upload or notify job be the merge gate.
Branch protection that requires a misnamed or soft job is a common footgun. Use a single required CI-passed job that needs unit, integration, and security — or GitLab's equivalent. Disable continue-on-error on required paths. Dashboards that show only the last job mislead on-call. Treat required checks as a security control and review them like IAM.
gate: needs: [unit, integration, gitleaks] script: [echo ok] # branch protection: require "gate", not "upload-coverage"
Interviewer often follows with: How do you handle a known-flaky optional job without making the gate optional?
Supply-chain scan is warn-only; a critical CVE in a base image shipped. Security wants blocking gates tomorrow. How do you roll that out without freezing delivery?Expert
I'd introduce a severity threshold in warn mode with a tracked exception list, fix or rebase the worst images first, then flip to fail-closed for criticals on main while leaving advisories non-blocking.
Big-bang blocking breaks trust in security. Pattern: inventory top base images, set grace windows per severity, CODEOWNERS exceptions with expiry, and alternate patched bases. Fail CI on critical/high for release branches first. Pair with digest pins and rebuild pipelines. Measure MTTR for CVE gates. Risk-based enforcement plus exception hygiene beats perpetual warn-only.
trivy image --severity CRITICAL --exit-code 1 app:$SHA # exceptions: .trivyignore with ticket + expiry date
Interviewer often follows with: How do you prevent .trivyignore from becoming permanent debt?