DevSecOps β Practical Security
Secrets hygiene, image scanning, dependency awareness, K8s RBAC, least privilege, and SBOM
Secrets Hygiene
Secrets in the wrong place are the most common cause of security incidents. The rule is simple: secrets never enter your codebase, ever.
Where Secrets Go Wrong
# WRONG β secret in codeDATABASE_URL="postgres://admin:password123@db.example.com/prod"
# WRONG β secret in DockerfileENV DATABASE_URL=postgres://admin:password123@db.example.com/prod
# WRONG β secret in docker-compose.yml committed to gitenvironment: - DATABASE_URL=postgres://admin:password123@db.example.com/prod
# WRONG β secret in CI logsecho "Running with key: $API_KEY" # logs are often public!What to Do Instead
# 1. Environment injection at runtimedocker run -e DATABASE_URL="$DATABASE_URL" myimage
# 2. Secrets manager referenceaws secretsmanager get-secret-value --secret-id prod/myapp/database-url
# 3. K8s Secret (from external secrets operator β not manual yaml with secret value)kubectl get secret myapp-secrets -o jsonpath='{.data.database-url}' | base64 -d
# 4. Vault dynamic credentials (most secure β short-lived, auto-rotated)vault read database/creds/myapp-roleDetect Leaked Secrets
# Scan git history for secretstrufflehog git https://github.com/myorg/myapp --only-verified
# Pre-commit hook to prevent committing secretspip install detect-secretsdetect-secrets scan > .secrets.baselinedetect-secrets audit .secrets.baseline
# Add to .pre-commit-config.yamlrepos: - repo: https://github.com/Yelp/detect-secrets rev: v1.4.0 hooks: - id: detect-secretsIf a Secret Is Leaked
- Immediately rotate/revoke the secret (assume itβs compromised)
- Check access logs for unauthorized use
- Remove from git history (this is hard β better to rotate first)
- Add to
.gitignoreand secrets baseline
# Remove sensitive file from git history (complex β use BFG or git-filter-repo)git filter-repo --path secrets.txt --invert-paths# Requires force push β coordinate with your teamImage Scanning
Every Docker image you build contains software with known vulnerabilities. Scan before you push.
Trivy (most common)
# Installbrew install trivy # macOSapt install trivy # Ubuntudocker pull aquasec/trivy # Docker
# Scan a local imagetrivy image myapp:latest
# Scan with specific severity (fail on critical/high)trivy image --severity CRITICAL,HIGH --exit-code 1 myapp:latest
# Scan a remote imagetrivy image nginx:1.25
# Scan in CI (GitHub Actions)- uses: aquasecurity/trivy-action@master with: image-ref: myapp:${{ github.sha }} severity: 'CRITICAL,HIGH' exit-code: '1' ignore-unfixed: true # ignore vulns with no fix available format: 'sarif' output: 'trivy-results.sarif'
# Upload to GitHub Security tab- uses: github/codeql-action/upload-sarif@v3 with: sarif_file: trivy-results.sarifWhat to Do With Scan Results
| CVE Severity | Action |
|---|---|
| Critical | Fix immediately β update base image or dependency |
| High | Fix within sprint |
| Medium | Add to backlog, track |
| Low | Accept risk or add to ignore list with justification |
# Update base image (most common fix)FROM node:20-alpine # β check for newer patch versionFROM node:20.11-alpine # pin exact version after verifying it's clean
# Check which package has the vulnerabilitytrivy image --format json myapp:latest | jq '.Results[].Vulnerabilities[] | select(.Severity == "CRITICAL")'Registry Scanning
# AWS ECR β enable scan on push (automatic)aws ecr put-image-scanning-configuration \ --repository-name myapp \ --image-scanning-configuration scanOnPush=true
# Get scan resultsaws ecr describe-image-scan-findings \ --repository-name myapp \ --image-id imageTag=latestDependency Awareness
Your application code is only a small fraction of what you ship. You also ship every npm package, pip package, and system library you depend on.
# Node.jsnpm audit # check for known vulnerabilitiesnpm audit fix # auto-fix where possiblenpm audit --audit-level=high # fail only on high/critical
# Pythonpip install safetysafety check # scan installed packagessafety check -r requirements.txt
# In CI β keep it in the pipeline- name: Dependency audit run: npm audit --audit-level=highKeeping Dependencies Updated
# Check for outdated packagesnpm outdatedpip list --outdated
# Automated dependency updates# Dependabot (GitHub) β opens PRs for outdated dependencies# Renovate β similar, more configurable
# .github/dependabot.ymlversion: 2updates: - package-ecosystem: "npm" directory: "/" schedule: interval: "weekly" ignore: - dependency-name: "eslint" # ignore specific packageKubernetes RBAC Basics
RBAC (Role-Based Access Control) controls who can do what in your cluster.
Key Resources
ServiceAccount β bound to β RoleBinding/ClusterRoleBinding β references β Role/ClusterRole| Resource | Scope | Use case |
|---|---|---|
Role | Single namespace | Pod reader in production namespace |
ClusterRole | All namespaces | Node reader, persistent volume admin |
RoleBinding | Single namespace | Binds a Role to a subject in a namespace |
ClusterRoleBinding | All namespaces | Binds ClusterRole globally |
Creating Roles
# Role β only in production namespaceapiVersion: rbac.authorization.k8s.io/v1kind: Rolemetadata: name: pod-reader namespace: productionrules: - apiGroups: [""] resources: ["pods", "pods/log"] verbs: ["get", "list", "watch"] - apiGroups: ["apps"] resources: ["deployments"] verbs: ["get", "list"]
---# RoleBinding β grants pod-reader role to service accountapiVersion: rbac.authorization.k8s.io/v1kind: RoleBindingmetadata: name: read-pods namespace: productionsubjects: - kind: ServiceAccount name: myapp namespace: production - kind: User name: alice@example.com apiGroup: rbac.authorization.k8s.ioroleRef: kind: Role name: pod-reader apiGroup: rbac.authorization.k8s.ioServiceAccount for Apps
# ServiceAccount β identity for your appapiVersion: v1kind: ServiceAccountmetadata: name: myapp namespace: production annotations: eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/myapp-role # β For EKS: maps K8s SA to AWS IAM role (IRSA)
---# Use in deploymentspec: serviceAccountName: myapp # not the default service account! automountServiceAccountToken: false # only mount if your app needs K8s API access# Check what a service account can dokubectl auth can-i get pods --as=system:serviceaccount:production:myappkubectl auth can-i list secrets --as=system:serviceaccount:production:myapp
# List all permissions for a rolekubectl describe role pod-reader -n production
# Check who has cluster-adminkubectl get clusterrolebindings -o json | \ jq '.items[] | select(.roleRef.name=="cluster-admin") | .subjects'Least Privilege Mindset
Every principal (user, service account, IAM role) should have exactly the permissions it needs β no more.
Checklist
IAM (AWS):
- No
*actions in production - EC2 instances get roles, not access keys
- Rotate access keys if they exist at all
- Use IAM Access Analyzer to find unused permissions
- Enable CloudTrail for audit logs
Kubernetes:
- No workload should use the
defaultservice account - No workload should have
cluster-admin - Mount service account tokens only if needed (
automountServiceAccountToken: false) - Use network policies to restrict pod-to-pod communication
Containers:
- Run as non-root user (
USER 1000in Dockerfile) - Read-only root filesystem where possible
- Drop all capabilities, add back only whatβs needed
# Secure container security contextspec: securityContext: runAsNonRoot: true runAsUser: 1000 fsGroup: 1000 containers: - name: myapp securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: drop: - ALL add: - NET_BIND_SERVICE # only if app binds to port < 1024 volumeMounts: - name: tmp mountPath: /tmp # need writable /tmp? mount it separately volumes: - name: tmp emptyDir: {}Supply Chain Awareness (SBOM)
SBOM = Software Bill of Materials. A list of every component in your software β like a nutrition label.
Why It Matters
The SolarWinds and Log4Shell attacks showed that attackers can compromise software through dependencies. An SBOM lets you:
- Know whatβs in your software
- Quickly identify if youβre affected by a new CVE
- Meet compliance requirements (many enterprises now require SBOMs)
Generate an SBOM
# Using syft (by Anchore)brew install syft
# Generate SBOM for a Docker imagesyft myapp:latest -o cyclonedx-json > sbom.jsonsyft myapp:latest -o spdx-json > sbom.spdx.json
# Generate for local directorysyft dir:./myapp -o cyclonedx-json > sbom.json
# In GitHub Actions- uses: anchore/sbom-action@v0 with: image: myapp:${{ github.sha }} artifact-name: sbom.spdx.json format: spdx-jsonScan SBOM for Vulnerabilities
# Using grype (by Anchore) β scan SBOMgrype sbom:./sbom.jsongrype sbom:./sbom.json --fail-on highAttestation (Proving the SBOM is Real)
# Sign an image and attach SBOM with cosigncosign sign myapp:latestcosign attest --predicate sbom.json --type cyclonedx myapp:latest
# Verify signaturecosign verify myapp:latest