Secure Software Delivery: DevSecOps, Pipeline, and Supply Chain Security Questions
Embedding security into how software is built, assembled from dependencies, and shipped. Covers shift-left and secure-SDLC practices, infrastructure-as-code security, CI/CD pipeline and secrets management, integrating security scanning into build and deploy, and configuration and secret management across environments, together with software supply chain security: software composition analysis (SCA), dependency and open-source vulnerability management, build-provenance and artifact integrity, and mitigating supply-chain attack vectors. The 'secure the delivery pipeline and everything it pulls in' discipline, distinct from vendor-risk governance.
Describe the common ways secrets accidentally end up in Git history or CI artifacts. For each leakage vector, provide two concrete preventive controls you would implement (tooling, process, or policy) to stop that class of leak.
Sample Answer
Secrets end up in Git history or CI artifacts through a small number of well-known paths, and each one has its own concrete, checkable prevention control rather than a single blanket fix.
Leakage vectors and controls
A developer commits a secret directly into source code (a hardcoded API key, a .env file checked in by accident). Prevention: a pre-commit hook running a secret-detection tool (gitleaks or trufflehog) that blocks the commit locally before it ever reaches the remote, plus a server-side push-protection check as a backstop for the developer who bypasses or doesn't have the local hook installed.
A secret leaks into CI build logs (an environment variable printed by a verbose build step, or a script echoing its own inputs for debugging). Prevention: enable the CI provider's built-in log masking for any value sourced from a secrets store (most CI systems mask a value automatically once it's referenced as a secret), and explicitly discourage set -x or verbose debug flags in scripts that handle secret-bearing environment variables.
A secret gets baked into a built artifact or container image (a config file with a real credential copied into the image during a multi-stage build, rather than injected at runtime). Prevention: scan every built artifact and container layer for secret patterns before it's published, and enforce a runtime-injection pattern (secrets manager, sidecar, or environment variable set at deploy time) rather than baking any credential into the image at build time.
Historical leakage, meaning a secret that was committed and later removed, but still exists in the Git history of an old commit. Prevention: run a periodic (for example nightly) scan of the FULL Git history, not just the current HEAD, since deleting a file in a later commit does not remove it from history; and gate any change that widens a repository's exposure (making a private repository public, adding an external collaborator, connecting a new third-party integration) on that full-history scan passing clean first, so a secret from years ago cannot be newly exposed the moment access to the repository widens.
If a secret is found in a log or artifact
The response is always rotate first, investigate second: revoke and reissue the credential immediately, since the window between detection and rotation is exactly the window an attacker could exploit, and only afterward investigate how it leaked and whether it was actually accessed by anyone unauthorized. A secret found by the historical scan gets the same treatment: it must be treated as compromised and rotated, not merely scrubbed from history, since history rewrites don't help once a repository has been cloned elsewhere, and the safe assumption is that any exposed secret has already been seen.
Trade-offs
Pre-commit hooks add a small amount of friction to every commit (a moment's pause while the scan runs) in exchange for catching the leak at the cheapest possible point, before it ever reaches a shared remote; skipping local hooks in favor of only a server-side check still catches the leak, but only after it has already reached the remote repository, which several tools and CI systems may have already cloned or cached by the time it's caught.
Write an OPA/Rego policy that denies creation or modification of object storage buckets that do not have server-side encryption enabled or that allow public access. Explain how you would integrate this policy into CI (pre-commit hooks and pipeline checks) and into runtime enforcement (admission controller or cloud governance). Describe unit and integration tests you would write to validate the policy.
Sample Answer
This policy denies two distinct storage-bucket misconfigurations: missing server-side encryption, and public ACL access, checked as separate rules so each produces its own specific, actionable message.
package storage.security
import rego.v1
deny contains msg if {
some resource in input.resource_changes
resource.type == "aws_s3_bucket"
not has_encryption_sibling(resource.address)
msg := sprintf("bucket %q has no associated server-side encryption configuration", [resource.address])
}
has_encryption_sibling(bucket_address) if {
some r in input.resource_changes
r.type == "aws_s3_bucket_server_side_encryption_configuration"
count(r.change.after.rule) > 0
startswith(r.address, sprintf("aws_s3_bucket_server_side_encryption_configuration.%s", [split(bucket_address, ".")[1]]))
}
deny contains msg if {
some resource in input.resource_changes
resource.type == "aws_s3_bucket_public_access_block"
resource.change.after.block_public_acls == false
msg := sprintf("bucket %q allows public ACLs (block_public_acls=false)", [resource.address])
}
Why encryption is checked via a sibling resource
In Terraform's AWS provider, server-side encryption for an S3 bucket is configured as a SEPARATE resource (aws_s3_bucket_server_side_encryption_configuration) linked to the bucket, not an inline attribute on the bucket resource itself; the policy has to look for that sibling resource's existence and content rather than checking a field directly on the bucket resource, which correctly models how the actual infrastructure is described and would silently miss the check entirely if it only inspected the bucket resource in isolation.
Integration into CI: pre-commit hooks and pipeline checks
Two distinct CI integration points matter here, and they catch different things. A pre-commit hook runs this same policy against the LOCAL Terraform plan (or a quick terraform validate plus a locally-generated plan JSON) before the developer even pushes, giving the fastest possible feedback loop, entirely on the developer's own machine; because it runs pre-push, it cannot be relied on as the actual gate, since a developer can skip or misconfigure a local hook. The pipeline check is what actually enforces the policy: it evaluates against terraform plan's JSON output on every pull request touching storage resources, as a pre-merge gate that cannot be bypassed the way a local hook can. The pre-commit hook exists purely to shift feedback left and save the round-trip to CI for an obvious violation; the pipeline check is the one that's actually authoritative.
Runtime enforcement
As a runtime backstop (since a resource created outside this specific pipeline, through the console or a different automation path, wouldn't be caught by a plan-time check at all), the same logic should also run as a cloud-governance policy (an AWS Config rule, or an admission-style check for infrastructure-as-code applied through a different path), catching drift or out-of-band changes the CI gate never saw.
Tests
A table-driven test suite should cover: a bucket with no encryption sibling at all (should deny), a bucket with an encryption sibling but an empty rule list (should deny, since a resource existing with no actual rule configured is not the same as being encrypted), a bucket with a properly configured encryption rule (should pass), and separately, a public-access-block resource with block_public_acls set to both true and false (should pass and deny respectively).
Verified
Evaluated with opa eval against five fixtures: a bucket with no encryption sibling (denied, as expected), a bucket whose encryption sibling has an empty rule list (denied), a bucket with a properly configured encryption rule (passed, no deny), a public-access-block resource with block_public_acls=false (denied) and with block_public_acls=true (passed). A combined fixture (unencrypted bucket plus public ACLs allowed) produced both deny messages together, and a combined fixture with both settings correct produced an empty result.
Trade-offs
Checking for the encryption sibling by matching on a naming convention (startswith against the bucket's resource name) assumes a consistent Terraform module naming pattern across the organization; a codebase with inconsistent naming between a bucket and its encryption configuration would need a more robust matching approach, such as checking the sibling resource's bucket reference attribute directly rather than inferring the relationship from resource address naming.
Explain how you would assess third-party dependencies and supply-chain risk for an application. Cover creation and use of an SBOM, static and dynamic SCA tools, version pinning, dependency update policies, and how to handle transitive dependencies or private packages in CI/CD.
Sample Answer
Assessing third-party dependency and supply-chain risk for an application means treating every dependency, direct or transitive, as something you've implicitly extended trust to, and building the process around continuously validating that trust rather than checking it once at adoption time.
SBOM as the foundation
Generate an SBOM for every build, capturing the full dependency tree with exact versions, since you can't assess risk in what you can't enumerate; this becomes the input every other step in this process queries against.
Static and dynamic SCA
Static SCA checks the SBOM's package list against known-vulnerability databases without running anything, catching the majority of known issues cheaply and continuously. Dynamic SCA (or reachability analysis layered on top of static SCA) checks whether the application's actual code paths call the specific vulnerable function, distinguishing a theoretically-present risk from a practically-exploitable one, which matters directly for prioritization.
Version pinning and dependency-update policy
Pin dependencies to exact versions via a lockfile rather than a semver range, so upgrades are deliberate, reviewed events rather than silent automatic changes; pair this with a defined update policy (routine dependency bumps reviewed and merged on a regular cadence, versus emergency patches for actively-exploited critical vulnerabilities fast-tracked outside the normal cadence).
Transitive dependencies and private packages
Transitive dependencies need the same SBOM-and-SCA coverage as direct ones, since a vulnerability several levels deep in the tree is just as reachable if the code path calls into it; private, internally-published packages need the same treatment as public ones in this pipeline, since an internal package can also become outdated or compromised (a stolen internal-registry credential publishing a malicious internal package update is a real, if less commonly discussed, variant of the same risk class).
The full loop
flowchart LR
SBOM[Generate SBOM] --> StaticSCA[Static SCA scan]
StaticSCA --> Reach[Reachability analysis]
Reach --> Prioritize[Prioritize by severity + reachability]
Prioritize --> Fix[Pinned version bump or emergency patch]
Fix --> SBOM
This is a continuous loop, not a one-time assessment, since new CVEs get disclosed against already-adopted dependencies with no code change on your side, meaning the SBOM and SCA scan need to re-run on a recurring cadence against dependencies that haven't changed, not just at the moment a dependency is first added.
Trade-offs
Pinning every dependency by exact version and gating every upgrade through review adds real process overhead compared to letting semver ranges auto-update; that overhead buys control over exactly what code is running at any moment, which is the property this entire risk-assessment process depends on, since you cannot meaningfully assess the risk of a dependency tree that silently changes underneath you between assessments.
You discover a third-party library used in production may have licensing or security exposure. Describe how you would investigate the risk, propose remediation options, communicate trade-offs to product and legal, and implement a plan to remediate while minimizing disruption to shipping schedules.
Sample Answer
A third-party library with a potential licensing or security exposure needs two genuinely different investigation tracks, since a license risk and a security risk have different consequences, different owners, and different urgency profiles, even though they can arrive from the same discovery.
Investigating the risk
For the licensing angle, determine the library's actual license terms and whether your product's distribution model (SaaS, on-prem, bundled with a commercial product) triggers any specific obligation that license imposes (a copyleft license's source-disclosure requirement behaves very differently for a SaaS product than for shipped, distributed software, for instance); this genuinely needs a legal read, not just an engineering judgment call, since license interpretation carries real business risk if guessed wrong. For the security angle, follow the same investigation steps discussed throughout this topic: confirm the CVE or vulnerability is real and reachable in your specific usage, not just present in the dependency tree.
Proposing remediation options
For a licensing exposure, options typically include: negotiating a different license or commercial agreement with the maintainer or a company managing the license, replacing the library with a differently-licensed alternative, or, if the risk is deemed acceptable after legal review, formally documenting the accepted risk with legal sign-off rather than leaving it as an unresolved, unowned question. For a security exposure, the options follow the same version-bump-or-replace pattern used elsewhere in this topic for dependency vulnerabilities.
Communicating trade-offs to product and legal
Product needs to understand the SHIPPING SCHEDULE impact of each option (a license replacement might require meaningful engineering time that could push a release date; a security patch might be nearly free), framed in terms of the actual trade-off rather than technical jargon; legal needs the FACTUAL specifics of how the library is actually used and distributed, since a license risk assessment depends entirely on those usage facts, which legal can't determine on their own without engineering input.
Implementing a remediation plan while minimizing shipping disruption
If the chosen remediation is a library replacement (the highest-disruption option), stage it the same way any other risky dependency change would be staged: behind a feature flag or in a non-critical-path service first, validating the replacement's behavior matches before rolling it out to the full, critical-path usage; if the remediation is a version bump or a documented accepted-risk decision, the disruption is minimal and can proceed on a normal cadence.
Trade-offs
Documenting an accepted licensing risk with legal sign-off, rather than insisting on replacing every library with any theoretical licensing question, is a legitimate outcome when legal genuinely concludes the risk is acceptable given the specific usage; the key discipline is that this has to be an explicit, owned, legal-reviewed decision, not a default that happens by simply not addressing the question at all.
Write an OPA (Rego) policy snippet that enforces two Kubernetes admission rules: 1) container images must come from registries 'mycompany.registry/' or 'gcr.io/mycompany/', and 2) containers must not be allowed to run as root (either securityContext.runAsNonRoot == true or securityContext.runAsUser != 0). Include brief comments explaining your logic. (Assume input is the Kubernetes admission review JSON.)
Sample Answer
This admission policy enforces two independent security properties on any container about to run in the cluster: that its image comes from a trusted source, and that it can't run with root privileges.
package kubernetes.admission
import rego.v1
allowed_registries := ["mycompany.registry/", "gcr.io/mycompany/"]
deny contains msg if {
some container in input_containers
not image_from_allowed_registry(container.image)
msg := sprintf("container %q uses image %q from a non-approved registry", [container.name, container.image])
}
deny contains msg if {
some container in input_containers
not runs_as_non_root(container)
msg := sprintf("container %q is not enforced to run as non-root", [container.name])
}
image_from_allowed_registry(image) if {
some prefix in allowed_registries
startswith(image, prefix)
}
runs_as_non_root(container) if {
container.securityContext.runAsNonRoot == true
}
runs_as_non_root(container) if {
container.securityContext.runAsUser != 0
}
input_containers := input.request.object.spec.containers
The two rules, and why non-root has two paths
The registry rule checks the image reference's prefix against an explicit allowlist, denying anything that doesn't match one of the two approved registries; this closes the same trust gap discussed for the supply-chain capstone elsewhere in this topic, enforced here at the actual point a container is scheduled, not just earlier in the pipeline. The non-root rule accepts EITHER of two ways a Pod spec can express "don't run as root": the explicit runAsNonRoot: true flag, or an explicit runAsUser set to any non-zero UID; a policy that only checked one of these two equivalent expressions would incorrectly deny a container that's actually correctly configured via the other path, which is why both are modeled as alternative ways to satisfy the same underlying rule.
Comments explaining the logic
The input_containers binding pulls directly from the Kubernetes AdmissionReview object's standard shape (request.object.spec.containers), which is the same structure every admission webhook receives regardless of what specific resource triggered the review, as long as it has a pod spec. Each deny rule is independent, meaning a container that fails BOTH checks produces two separate messages, giving the developer full visibility into everything wrong at once rather than surfacing one issue, requiring a fix, then surfacing the next.
Verified
Evaluated with opa eval against two fixture AdmissionReview payloads: a container using an unapproved registry (docker.io/library/nginx) with no non-root enforcement correctly produced both deny messages; a container using an approved registry with runAsNonRoot: true correctly produced an empty result.
Trade-offs
This policy checks only the FIRST container in a pod spec implicitly through the some container in input_containers iteration, which actually does correctly check every container in a multi-container pod (including init containers if they're included in the same array in your admission review shape); the real limitation is that it doesn't separately address initContainers, which live in a different field of the pod spec and would need their own explicit check if your threat model requires equally strict treatment of init containers.
Unlock Full Question Bank
Get access to all 24 Secure Software Delivery: DevSecOps, Pipeline, and Supply Chain Security interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.