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.
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.
In the context of CI/CD, explain the difference between 'secrets' and general configuration values. Provide concrete examples of each, describe why secrets require special handling, and list three practical controls you would apply in a pipeline to protect secrets from accidental exposure.
Sample Answer
A secret is any credential that grants access to something (an API key, a database password, a private signing key, an OAuth token) where possession of the value itself is the entire authorization check. General configuration (a timeout value, a feature flag, a log level, a service's hostname) describes how the system should behave but grants no access on its own.
Concrete examples
- Secrets: a database connection password, an AWS access key, a third-party API token, a TLS private key, a webhook signing secret.
- Configuration: a request-timeout value, a retry count, a feature-flag boolean, a public API base URL, a log-verbosity level.
Why secrets need special handling
The practical distinction is blast radius on exposure: if a configuration value leaks (say, into a public log or a committed file), nothing bad happens by itself; at worst it reveals an implementation detail. If a secret leaks the same way, whoever finds it can immediately act as your application, which might mean reading a production database, calling a paid third-party API on your account, or impersonating your service to another system entirely. Secrets also need a lifecycle configuration values don't: rotation, expiry, and revocation, since a leaked secret needs to be actively invalidated, while a leaked configuration value just needs to be corrected.
Three practical controls
- Store secrets in a dedicated secrets manager (HashiCorp Vault, AWS Secrets Manager, or equivalent), never in environment variables baked into a config file or committed alongside application configuration. This keeps secrets out of version control by construction and gives you a single place to rotate and audit access.
- Inject secrets at runtime via short-lived, scoped credentials (an OIDC (OpenID Connect)-issued token exchanged for a temporary cloud credential) rather than long-lived static keys checked into a pipeline's configuration. A short-lived credential that leaks has a bounded window of usefulness to an attacker; a static key that leaks is valid until someone notices and manually revokes it.
- Run automated secret scanning on every commit and every build artifact, so a secret that accidentally ends up in code, a log line, or a built container image is caught and flagged for rotation before it reaches a public or widely-accessible location, rather than relying on developers to notice manually.
The trade-off
Treating every string that looks sensitive as a secret (over-classifying configuration as secret) adds unnecessary rotation and access-control overhead to values that don't need it; the practical test is always 'does possessing this value alone grant access to something', and applying that test consistently is what keeps the secrets-management overhead proportionate to the actual risk.
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.
In Python 3, sketch pseudocode for a CI/CD pipeline step that retrieves a per-build ephemeral secret from a Vault-compatible API, uses it to decrypt build artifacts in-memory, and ensures the secret is never written to logs or persisted to disk. Include comments explaining integration points, error handling, and safe cleanup.
Sample Answer
The key requirement here is that the fetched secret exists in memory only for exactly as long as it's needed, and never touches a log line or a file on disk, since a pipeline step that writes a secret to disk "just for this operation" creates a leak surface even if the file is deleted afterward (the artifact cache, a core dump, or a container layer can still capture it).
import requests
import hvac # HashiCorp Vault's official Python client
def decrypt_artifact_with_vault_secret(artifact_path, vault_addr, vault_token,
secret_path, secret_key):
"""Fetch a per-build secret from Vault, use it to decrypt an artifact
in memory, and ensure the secret value is never logged or persisted."""
client = hvac.Client(url=vault_addr, token=vault_token)
if not client.is_authenticated():
raise RuntimeError("Vault authentication failed")
# Fetch the secret. Do NOT log `secret_response` or any field extracted
# from it; hvac itself does not log secret values, but a naive debug
# print of the full response would leak the key material.
secret_response = client.secrets.kv.v2.read_secret_version(path=secret_path)
decryption_key = secret_response["data"]["data"][secret_key]
try:
with open(artifact_path, "rb") as f:
encrypted_bytes = f.read()
decrypted_bytes = _decrypt_in_memory(encrypted_bytes, decryption_key)
return decrypted_bytes
finally:
# Best-effort scrub: overwrite the local reference so it doesn't
# linger in an obvious local variable for the rest of the process's
# life. CPython cannot guarantee immediate memory zeroing for an
# immutable str/bytes object, so this is defense in depth, not a
# cryptographic guarantee.
decryption_key = None
def _decrypt_in_memory(encrypted_bytes, key):
# Placeholder for the actual symmetric-decryption call (e.g. via the
# `cryptography` library's Fernet or AESGCM); omitted here since the
# decryption algorithm itself isn't what this question is testing.
raise NotImplementedError
Integration points and safe cleanup
The Vault client authenticates using a short-lived token issued to this specific CI job (via Vault's Kubernetes or JWT auth method, not a static root token), the secret is read once via the KV v2 API, and the decrypted bytes are returned directly to the caller rather than written to a temp file; if the caller needs the decrypted artifact ON disk for a subsequent step (say, to hand off to another process), it should write to a location on an in-memory filesystem (tmpfs) that is guaranteed to not persist to the container image layer or a shared cache.
Why this is pseudocode, and what a real implementation needs
The actual decryption call is deliberately left unimplemented since the specific algorithm (Fernet, AES-GCM) isn't what this question is testing; a real implementation would use an authenticated-encryption scheme (AES-GCM, not plain AES-CBC) so a tampered ciphertext fails to decrypt rather than silently producing corrupted output. The comment about CPython's memory model is an honest limitation, not a solved problem: Python does not give a hard guarantee that clearing a local variable actually zeroes the underlying memory immediately, so an environment with genuinely strict secret-hygiene requirements (a regulated, high-security context) would use a language or library with real memory-zeroing guarantees (a Rust zeroize-style crate, or hardware-backed decryption via an HSM (hardware security module) that never exposes the raw key to the application process at all) rather than relying on Python's garbage collector.
Provide a sample CI/CD workflow (YAML or pseudocode) that enforces separation of duties: developers can build and push artifacts but cannot promote to production; release and deployment require an independent approver and only signed artifacts are promoted. Include artifact signing and verification and least-privilege runner identities.
Sample Answer
Enforcing separation of duties in a pipeline means the same identity that can build and push an artifact structurally cannot also be the one that approves and executes its promotion to production; this has to be enforced by permissions and explicit configuration, not by a policy document asking people not to do both.
Workflow design
name: build-and-promote
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write
steps:
- uses: actions/checkout@v4
- name: Build and push (unsigned, dev-only tag)
run: docker build -t "$IMAGE:dev-${{ github.sha }}" . && docker push "$IMAGE:dev-${{ github.sha }}"
- name: Sign image
run: cosign sign --yes "$IMAGE:dev-${{ github.sha }}"
promote-to-production:
needs: build
runs-on: ubuntu-latest
environment:
name: production # requires configured required reviewers AND "Prevent self-review" enabled
permissions:
contents: read
packages: write
id-token: write
steps:
- name: Verify signature before promoting
run: cosign verify --certificate-identity-regexp ".*" --certificate-oidc-issuer https://token.actions.githubusercontent.com "$IMAGE:dev-${{ github.sha }}"
- name: Retag and push production tag
run: |
docker pull "$IMAGE:dev-${{ github.sha }}"
docker tag "$IMAGE:dev-${{ github.sha }}" "$IMAGE:prod-${{ github.sha }}"
docker push "$IMAGE:prod-${{ github.sha }}"
How separation of duties is actually enforced
The build job runs under the identity of whoever pushed the commit, with permissions scoped to push a dev-tagged image and sign it, but with NO permission to push a prod- tag directly. The promote-to-production job is gated by GitHub's own environment protection rule, which requires a configured human approver before the job runs at all. That alone is not enough to guarantee separation of duties: by default GitHub does not stop the person who triggered the workflow from also being the required reviewer who approves their own promotion. The environment's protection settings must explicitly enable "Prevent self-review" for the approver to be guaranteed distinct from whoever pushed the change; without that setting turned on, a developer with reviewer permissions could approve their own promotion, which would defeat the entire point of this control. With it enabled, "developers can build and push but cannot promote" becomes an actually-enforced permission boundary rather than a documented convention someone could bypass.
Signature verification and least-privilege runner identities
The promotion job re-verifies the image's signature before retagging it for production, so promotion depends on the signature actually being valid at promotion time, not merely on trusting that the build job signed it correctly earlier. For keyless verification, cosign verify requires both a certificate-identity match and a certificate-OIDC-issuer match (here, GitHub Actions' own OIDC issuer); a verify command that supplies only the identity regex and omits the issuer will fail with a missing-required-flag error rather than silently passing, so both flags have to be present for the check to run at all. Each job uses its own scoped permissions block (least privilege per job, following GitHub Actions' job-level permission model) rather than one broad permission set shared across the whole workflow.
Trade-offs
Requiring a re-verification of the signature at promotion time, rather than trusting the build job's own signing step, is a small amount of redundant work but is exactly what prevents a compromised or buggy build job from being able to push an unsigned or improperly-signed image straight to the production tag by skipping its own signing step. Similarly, explicitly enabling "Prevent self-review" costs nothing beyond a one-time configuration change, but skipping it leaves the entire separation-of-duties guarantee resting on an assumption about GitHub's default behavior that does not actually hold; the promotion job's independent signature check plus the self-review-blocked environment rule together are what actually enforce the separation, not just the presence of a signing step and an approval gate somewhere in the pipeline.
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.