Supply Chain Security, SBOM & Secrets Management
Supply Chain Attacks
A supply chain attack targets the software supply chain — the dependencies, build tools, and infrastructure used to build and deliver software, rather than attacking the end product directly.
Famous Incidents
| Incident | Year | Attack Vector |
|---|---|---|
| SolarWinds | 2020 | Malicious code injected into build system |
| event-stream (npm) | 2018 | Malicious maintainer added malicious package |
| colors.js sabotage | 2022 | Maintainer intentionally broke their own package |
| ua-parser-js | 2021 | Maintainer's account compromised, crypto-miner injected |
| node-ipc | 2022 | Maintainer added geopolitical protest code |
| PyTorch (torchtriton) | 2022 | Typosquatting on PyPI |
Attack Vectors
1. Typosquatting
npm install lodahs ← looks like 'lodash'
npm install crossenv ← looks like 'cross-env'
Targets: fast typists, CI pipelines
2. Dependency Confusion
Company has internal package 'mycompany-utils' on private registry
Attacker publishes 'mycompany-utils' to public npm with higher version
npm resolves public (higher version) over private → executes attacker code
3. Account Takeover
Maintainer credentials stolen → malicious version published
Package downloads trigger malicious postInstall script
4. Malicious PR merge
PR adds malicious code, maintainer merges without careful review
5. Abandoned packages
Maintainer transfers ownership, new owner adds malwareDefending Against Supply Chain Attacks
1. Lock Files — ALWAYS commit them
bash# package-lock.json or yarn.lock or pnpm-lock.yaml
# Locks exact versions AND hashes of every transitive dependency
# Never install without lockfile in production
npm ci # installs EXACTLY what's in lockfile
npm install --frozen-lockfile # yarn equivalent
# NOT this in production (updates lockfile)
npm install2. Dependency Auditing
bash# Built-in audit
npm audit
npm audit --audit-level high # fail only on high/critical
npm audit fix # auto-fix
# More detailed
npx better-npm-audit audit --level moderate
# Snyk (comprehensive)
npx snyk test
npx snyk monitor # continuous monitoring3. Subresource Integrity (SRI) for CDN scripts
html<!-- Hash ensures the CDN file hasn't been tampered with -->
<script
src="https://cdn.example.com/lib.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
crossorigin="anonymous"
></script>4. Constrain Permissions (npm)
json// .npmrc — prevent postinstall scripts (use with care)
// Some packages legitimately need postinstall (esbuild, puppeteer)
ignore-scripts=true
// Or allowlist specific packagesbash# Review what postinstall scripts run
npm install --ignore-scripts
# Then manually run trusted scripts:
npx prisma generate5. Private Registry + Proxying
Developer → Private Registry (Artifactory, Verdaccio, GitHub Packages)
↓ (proxy/allowlist)
Public npm Registry (only approved packages)yaml# .npmrc
registry=https://registry.mycompany.com
@mycompany:registry=https://registry.mycompany.com
# Artifactory config: allowlist specific packages
# All others blocked — prevents dependency confusion6. Pinning exact versions (controversial)
json// package.json — pin exact versions (no ^)
{
"dependencies": {
"express": "4.18.2", // exact, NOT "^4.18.2"
"lodash": "4.17.21"
}
}Trade-off: security patches won't auto-apply, but rogue minor version can't sneak in.
7. Automated Dependency Updates
yaml# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
groups:
production-dependencies:
dependency-type: "production"
ignore:
- dependency-name: "*"
update-types: ["version-update:semver-major"]SBOM — Software Bill of Materials
An SBOM is a machine-readable inventory of all software components in your application.
Why SBOM?
- Vulnerability tracking: If Log4Shell is discovered, immediately know if you're affected
- License compliance: Know if any dependency has a restrictive license (GPL, AGPL)
- Regulatory compliance: Required by US Executive Order 14028, EU Cyber Resilience Act
- Audit: Prove to customers what's in your software
Formats
| Format | Standard Body | Use Case |
|---|---|---|
| SPDX | Linux Foundation | Open source, regulatory |
| CycloneDX | OWASP | Security-focused, richer metadata |
Generating SBOM
bash# CycloneDX for Node.js
npm install -g @cyclonedx/cyclonedx-npm
cyclonedx-npm --output-file sbom.json
# SPDX
npx spdx-sbom-generator -p . -o sbom.spdx
# Syft (multi-ecosystem, Docker images too)
brew install syft
syft packages dir:. -o cyclonedx-json > sbom.json
syft packages nginx:latest -o spdx-json # Docker image SBOMSBOM in CI/CD
yaml# GitHub Actions
- name: Generate SBOM
uses: anchore/sbom-action@v0
with:
path: .
format: cyclonedx-json
output-file: sbom.json
- name: Scan SBOM for vulnerabilities
uses: anchore/scan-action@v3
with:
sbom: sbom.json
fail-build: true
severity-cutoff: highScanning SBOM for vulnerabilities
bash# Grype — scan SBOM against vulnerability databases
grype sbom:./sbom.json
# Output:
# NAME INSTALLED FIXED-IN TYPE VULNERABILITY SEVERITY
# lodash 4.17.20 4.17.21 npm CVE-2021-23337 High
# glob-parent 3.1.0 5.1.2 npm CVE-2020-28469 HighSecrets Management
The Problem
BAD:
.env file committed to git → all secrets exposed
Secrets in Docker image → anyone who pulls image sees secrets
Hardcoded in source → stays in git history forever
GOOD:
Secrets injected at runtime from a secrets manager
Short-lived credentials, auto-rotated
Audit log of who accessed whatHashiCorp Vault
The most widely used secrets manager.
┌─────────────────────────────────────┐
│ Vault │
│ │
│ ┌──────────┐ ┌────────────────┐ │
│ │ KV │ │ Dynamic │ │
│ │ Secrets │ │ Secrets │ │
│ │(static) │ │(DB creds, AWS) │ │
│ └──────────┘ └────────────────┘ │
│ │
│ Auth Methods: AppRole, k8s, JWT, OIDC│
│ Audit: who accessed what, when │
└──────────────────┬──────────────────┘
│
┌──────────────────▼──────────────────┐
│ Your Application │
│ vault token → read secret → use │
└─────────────────────────────────────┘Vault — Basic Usage
jsimport Vault from 'node-vault';
const vault = Vault({
endpoint: process.env.VAULT_ADDR,
token: process.env.VAULT_TOKEN, // or use AppRole auth
});
// Read a static secret (KV v2)
async function getSecret(path) {
const { data } = await vault.read(`secret/data/${path}`);
return data.data;
}
const dbCreds = await getSecret('production/database');
// { username: 'app_user', password: 'super-secret' }Dynamic Database Credentials (Vault's killer feature)
Traditional: With Vault Dynamic Secrets:
One set of creds App requests creds at startup
Shared across all instances Vault creates a unique DB user
Never rotated (fear of breakage) TTL: 1 hour, auto-deleted
One breach = all compromised Each app has own credentialsjs// Vault generates a unique, time-limited DB credential
async function getDatabaseCredentials() {
const { data } = await vault.read('database/creds/my-app-role');
// Returns: { username: 'v-app-AbCd1234', password: 'A1B2-...', lease_duration: 3600 }
return {
host: process.env.DB_HOST,
username: data.username, // unique per request
password: data.password, // expires in 1 hour
database: 'myapp',
};
}
// Renew before expiry
async function renewLease(leaseId) {
await vault.write('sys/leases/renew', {
lease_id: leaseId,
increment: 3600,
});
}AppRole Authentication (for services)
bash# Setup (Vault admin)
vault auth enable approle
vault write auth/approle/role/my-app \
secret_id_ttl=10m \
token_num_uses=10 \
token_ttl=20m \
token_max_ttl=30m \
secret_id_num_uses=40 \
policies=my-app-policy
# Get role-id (stored in config, not secret)
vault read auth/approle/role/my-app/role-id
# Get secret-id (short-lived, injected at deploy time)
vault write -f auth/approle/role/my-app/secret-idjs// Application startup — authenticate with AppRole
async function vaultLogin() {
const { auth } = await vault.approleLogin({
role_id: process.env.VAULT_ROLE_ID,
secret_id: process.env.VAULT_SECRET_ID, // short-lived, injected at deploy
});
vault.token = auth.client_token; // now authenticated
scheduleTokenRenewal(auth.lease_duration);
}Kubernetes Auth (most common in k8s environments)
js// Pod's service account token auto-mounted
const token = fs.readFileSync('/var/run/secrets/kubernetes.io/serviceaccount/token', 'utf-8');
const { auth } = await vault.kubernetesLogin({
role: 'my-app',
jwt: token,
});
vault.token = auth.client_token;AWS Secrets Manager / Parameter Store
jsimport { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';
const client = new SecretsManagerClient({ region: 'us-east-1' });
async function getSecret(secretName) {
const response = await client.send(
new GetSecretValueCommand({ SecretId: secretName })
);
return JSON.parse(response.SecretString);
}
// In Lambda/ECS: IAM role automatically provides credentials
// No explicit credentials needed!
const dbCreds = await getSecret('prod/myapp/database');Automatic Secret Rotation (AWS)
json// Rotation config in secrets manager
{
"RotationEnabled": true,
"RotationRules": {
"AutomaticallyAfterDays": 30
},
"RotationLambdaARN": "arn:aws:lambda:us-east-1:123:function:rotate-db-secret"
}Environment Variables Best Practices
bash# .env.example — commit this (no real values)
DATABASE_URL=postgresql://user:password@host:5432/db
REDIS_URL=redis://host:6379
JWT_SECRET=<generate with: openssl rand -base64 32>
AWS_REGION=us-east-1
# .env — NEVER commit
DATABASE_URL=postgresql://prod_user:real_password@prod-host:5432/mydb
JWT_SECRET=actual-secret-herejs// Validate env vars at startup — fail fast
import { z } from 'zod';
const envSchema = z.object({
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string().min(32),
NODE_ENV: z.enum(['development', 'test', 'production']),
PORT: z.coerce.number().default(3000),
REDIS_URL: z.string().url().optional(),
});
export const env = envSchema.parse(process.env);
// Throws with helpful error if any required env var is missingSecrets in Docker & CI/CD
Docker — Never put secrets in image
dockerfile# BAD — secret baked into image layer
ENV DATABASE_URL=postgresql://user:password@host/db
# GOOD — inject at runtime
# docker run -e DATABASE_URL=$DATABASE_URL myapp
# or use docker secrets / k8s secretsDocker BuildKit secrets (for build-time secrets)
dockerfile# Dockerfile
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
npm install
# Secret not stored in any layer!bashdocker build --secret id=npmrc,src=.npmrc .GitHub Actions Secrets
yaml# Repository secrets set in GitHub Settings
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
VAULT_TOKEN: ${{ secrets.VAULT_TOKEN }}
# OIDC — no long-lived secrets needed for AWS
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/github-actions
aws-region: us-east-1
# GitHub OIDC token exchanged for short-lived AWS credentials
# NO AWS_ACCESS_KEY_ID needed!Interview Questions
Q: What is a supply chain attack and how do you defend against it?
Supply chain attacks target software dependencies or build infrastructure rather than your code directly (e.g., compromised npm package). Defenses: lock files (commit package-lock.json, use npm ci), regular npm audit, lock to exact versions for critical deps, use a private registry with allowlist, review postinstall scripts, automated tools like Snyk/Dependabot.
Q: What is an SBOM and why does it matter? Software Bill of Materials is a machine-readable inventory of all software components. Allows you to quickly check if a newly disclosed CVE affects your application, track license compliance, and meet regulatory requirements (US EO 14028, EU CRA). Generated with tools like Syft, CycloneDX, SPDX.
Q: What are Vault dynamic secrets and why are they better than static credentials? Dynamic secrets are credentials generated on-demand with a TTL (e.g., 1 hour). Each application instance gets unique credentials that auto-expire. Benefits: blast radius is minimal if credentials leak (they expire), each app has an audit trail, no credential sprawl or "the password nobody changes." Static shared passwords are a single point of failure.
Q: How do you avoid secrets in environment variables? Best practice: use a secrets manager (Vault, AWS Secrets Manager) and fetch secrets at application startup, not via environment variables. In Kubernetes, use k8s secrets or a Vault sidecar injector. For CI/CD, use OIDC federation (GitHub Actions → AWS IAM role) to avoid long-lived credentials entirely.
Q: What is dependency confusion and how do you prevent it?
Attacker publishes a package to public npm with the same name as your private internal package but a higher version number. npm resolves public (higher version). Prevention: use a private registry that proxies public npm but blocks packages matching private namespace patterns (Artifactory scoped packages allowlist), or use npm's publishConfig to pin private packages to internal registry.