Security & Performance
CI/CD Pipeline Security
CI/CD Pipeline Security
CI/CD pipelines are a critical attack vector. Compromising a pipeline can give attackers access to production systems, credentials, and the ability to inject malicious code into releases.
Common CI/CD Threats
- Secrets exposure - API keys, passwords, tokens leaked in logs or code
- Dependency poisoning - Malicious packages in npm, pip, composer
- Supply chain attacks - Compromised third-party actions/plugins
- Privilege escalation - Overly permissive pipeline permissions
- Code injection - Untrusted input in pipeline scripts
Critical: A compromised CI/CD pipeline can deploy backdoors to production without any code review. Treat pipeline security as seriously as production security.
Secrets Management
Never hardcode secrets. Use secure secret management:
<!-- BAD: Secrets in code -->
const API_KEY = 'sk_live_abc123xyz789';
const DB_PASSWORD = 'MyP@ssw0rd123';
<!-- GOOD: Use environment variables -->
const API_KEY = process.env.STRIPE_API_KEY;
const DB_PASSWORD = process.env.DB_PASSWORD;
<!-- GitHub Actions: Use encrypted secrets -->
# .github/workflows/deploy.yml
name: Deploy
on: push
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Deploy to production
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
run: |
aws s3 sync ./dist s3://my-bucket
<!-- GitLab CI: Use protected variables -->
# .gitlab-ci.yml
deploy:
script:
- echo "$SSH_PRIVATE_KEY" | ssh-add -
- ssh user@server "deploy.sh"
only:
- main
const API_KEY = 'sk_live_abc123xyz789';
const DB_PASSWORD = 'MyP@ssw0rd123';
<!-- GOOD: Use environment variables -->
const API_KEY = process.env.STRIPE_API_KEY;
const DB_PASSWORD = process.env.DB_PASSWORD;
<!-- GitHub Actions: Use encrypted secrets -->
# .github/workflows/deploy.yml
name: Deploy
on: push
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Deploy to production
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
run: |
aws s3 sync ./dist s3://my-bucket
<!-- GitLab CI: Use protected variables -->
# .gitlab-ci.yml
deploy:
script:
- echo "$SSH_PRIVATE_KEY" | ssh-add -
- ssh user@server "deploy.sh"
only:
- main
Best Practices: Rotate secrets regularly, use separate secrets for dev/staging/prod, limit secret access to specific branches, enable secret scanning in your repository.
Static Application Security Testing (SAST)
SAST scans source code for vulnerabilities before deployment:
<!-- GitHub Actions: CodeQL SAST -->
# .github/workflows/security.yml
name: Security Scan
on: [push, pull_request]
jobs:
analyze:
runs-on: ubuntu-latest
permissions:
security-events: write
steps:
- uses: actions/checkout@v3
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: javascript, python
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
<!-- SonarCloud integration -->
- name: SonarCloud Scan
uses: SonarSource/sonarcloud-github-action@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
<!-- Semgrep for custom rules -->
- name: Semgrep Scan
uses: returntocorp/semgrep-action@v1
with:
config: p/security-audit
# .github/workflows/security.yml
name: Security Scan
on: [push, pull_request]
jobs:
analyze:
runs-on: ubuntu-latest
permissions:
security-events: write
steps:
- uses: actions/checkout@v3
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: javascript, python
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
<!-- SonarCloud integration -->
- name: SonarCloud Scan
uses: SonarSource/sonarcloud-github-action@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
<!-- Semgrep for custom rules -->
- name: Semgrep Scan
uses: returntocorp/semgrep-action@v1
with:
config: p/security-audit
Dynamic Application Security Testing (DAST)
DAST tests running applications for vulnerabilities:
<!-- OWASP ZAP scanning -->
# .github/workflows/dast.yml
name: DAST Scan
on:
schedule:
- cron: '0 2 * * *' # Daily at 2 AM
jobs:
zap_scan:
runs-on: ubuntu-latest
steps:
- name: ZAP Baseline Scan
uses: zaproxy/action-baseline@v0.7.0
with:
target: 'https://staging.example.com'
rules_file_name: '.zap/rules.tsv'
cmd_options: '-a'
<!-- Nuclei vulnerability scanner -->
- name: Nuclei Scan
uses: projectdiscovery/nuclei-action@main
with:
target: https://staging.example.com
templates: cves,vulnerabilities
# .github/workflows/dast.yml
name: DAST Scan
on:
schedule:
- cron: '0 2 * * *' # Daily at 2 AM
jobs:
zap_scan:
runs-on: ubuntu-latest
steps:
- name: ZAP Baseline Scan
uses: zaproxy/action-baseline@v0.7.0
with:
target: 'https://staging.example.com'
rules_file_name: '.zap/rules.tsv'
cmd_options: '-a'
<!-- Nuclei vulnerability scanner -->
- name: Nuclei Scan
uses: projectdiscovery/nuclei-action@main
with:
target: https://staging.example.com
templates: cves,vulnerabilities
SAST vs DAST: SAST analyzes code without running it (fast, finds coding errors). DAST tests running apps (slower, finds runtime vulnerabilities). Use both for comprehensive security.
Dependency Scanning
Scan dependencies for known vulnerabilities:
<!-- npm audit in CI -->
# .github/workflows/audit.yml
name: Dependency Audit
on: [push, pull_request]
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run npm audit
run: |
npm audit --audit-level=moderate
npm audit fix
<!-- Dependabot configuration -->
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
<!-- Snyk security scanning -->
- name: Snyk Security Scan
uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
command: test
args: --severity-threshold=high
# .github/workflows/audit.yml
name: Dependency Audit
on: [push, pull_request]
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run npm audit
run: |
npm audit --audit-level=moderate
npm audit fix
<!-- Dependabot configuration -->
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
<!-- Snyk security scanning -->
- name: Snyk Security Scan
uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
command: test
args: --severity-threshold=high
Container Security Scanning
Scan Docker images for vulnerabilities before deployment:
<!-- Trivy container scanning -->
# .github/workflows/container-scan.yml
name: Container Security
on: push
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build image
run: docker build -t myapp:${{ github.sha }} .
- name: Run Trivy scan
uses: aquasecurity/trivy-action@master
with:
image-ref: 'myapp:${{ github.sha }}'
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
- name: Upload results
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: 'trivy-results.sarif'
<!-- Fail build on critical vulnerabilities -->
- name: Scan with Grype
run: |
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh
./grype myapp:${{ github.sha }} --fail-on critical
# .github/workflows/container-scan.yml
name: Container Security
on: push
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build image
run: docker build -t myapp:${{ github.sha }} .
- name: Run Trivy scan
uses: aquasecurity/trivy-action@master
with:
image-ref: 'myapp:${{ github.sha }}'
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
- name: Upload results
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: 'trivy-results.sarif'
<!-- Fail build on critical vulnerabilities -->
- name: Scan with Grype
run: |
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh
./grype myapp:${{ github.sha }} --fail-on critical
Warning: Always scan images before pushing to production registries. A single critical vulnerability can compromise your entire infrastructure.
Infrastructure as Code (IaC) Security
Scan Terraform, CloudFormation, and Kubernetes configs for misconfigurations:
<!-- Checkov for IaC scanning -->
# .github/workflows/iac-scan.yml
name: IaC Security
on: [push, pull_request]
jobs:
checkov:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run Checkov
uses: bridgecrewio/checkov-action@master
with:
directory: ./terraform
framework: terraform
soft_fail: false
<!-- tfsec for Terraform -->
- name: Run tfsec
uses: aquasecurity/tfsec-action@v1.0.0
with:
working_directory: ./terraform
soft_fail: false
<!-- kubesec for Kubernetes manifests -->
- name: Scan K8s manifests
run: |
docker run -i kubesec/kubesec:512c5e0 scan /dev/stdin < k8s/deployment.yaml
# .github/workflows/iac-scan.yml
name: IaC Security
on: [push, pull_request]
jobs:
checkov:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run Checkov
uses: bridgecrewio/checkov-action@master
with:
directory: ./terraform
framework: terraform
soft_fail: false
<!-- tfsec for Terraform -->
- name: Run tfsec
uses: aquasecurity/tfsec-action@v1.0.0
with:
working_directory: ./terraform
soft_fail: false
<!-- kubesec for Kubernetes manifests -->
- name: Scan K8s manifests
run: |
docker run -i kubesec/kubesec:512c5e0 scan /dev/stdin < k8s/deployment.yaml
GitHub Actions Security
Secure your GitHub Actions workflows:
<!-- Pin actions to full commit SHA (not tags) -->
# BAD: Tags can be changed
- uses: actions/checkout@v3
# GOOD: Commit SHA is immutable
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab
<!-- Limit permissions with least privilege -->
name: Deploy
on: push
permissions:
contents: read
id-token: write # Only for OIDC
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@8e5e7e5
with:
persist-credentials: false # Don't persist GitHub token
<!-- Prevent script injection -->
# BAD: User input in run command
- name: Comment
run: echo "${{ github.event.comment.body }}"
# GOOD: Pass as environment variable
- name: Comment
env:
COMMENT_BODY: ${{ github.event.comment.body }}
run: echo "$COMMENT_BODY"
# BAD: Tags can be changed
- uses: actions/checkout@v3
# GOOD: Commit SHA is immutable
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab
<!-- Limit permissions with least privilege -->
name: Deploy
on: push
permissions:
contents: read
id-token: write # Only for OIDC
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@8e5e7e5
with:
persist-credentials: false # Don't persist GitHub token
<!-- Prevent script injection -->
# BAD: User input in run command
- name: Comment
run: echo "${{ github.event.comment.body }}"
# GOOD: Pass as environment variable
- name: Comment
env:
COMMENT_BODY: ${{ github.event.comment.body }}
run: echo "$COMMENT_BODY"
GitHub Security: Enable branch protection rules, require status checks, enable secret scanning, use environment protection rules for production deployments.
Pipeline Hardening Checklist
<!-- Complete CI/CD security checklist -->
✓ Secrets Management:
- No hardcoded secrets in code
- Use encrypted secret stores
- Rotate secrets regularly
- Audit secret access
✓ Code Scanning:
- SAST enabled (CodeQL, SonarCloud, Semgrep)
- DAST scheduled (OWASP ZAP, Nuclei)
- Dependency scanning (npm audit, Snyk)
- License compliance checks
✓ Container Security:
- Image scanning (Trivy, Grype)
- Base image updates automated
- Multi-stage builds for minimal images
- Non-root containers
✓ Infrastructure:
- IaC scanning (Checkov, tfsec)
- Network policies enforced
- Resource limits configured
- Logging and monitoring enabled
✓ Access Control:
- Least privilege permissions
- Branch protection enabled
- Required reviews configured
- Environment-specific approvals
✓ Secrets Management:
- No hardcoded secrets in code
- Use encrypted secret stores
- Rotate secrets regularly
- Audit secret access
✓ Code Scanning:
- SAST enabled (CodeQL, SonarCloud, Semgrep)
- DAST scheduled (OWASP ZAP, Nuclei)
- Dependency scanning (npm audit, Snyk)
- License compliance checks
✓ Container Security:
- Image scanning (Trivy, Grype)
- Base image updates automated
- Multi-stage builds for minimal images
- Non-root containers
✓ Infrastructure:
- IaC scanning (Checkov, tfsec)
- Network policies enforced
- Resource limits configured
- Logging and monitoring enabled
✓ Access Control:
- Least privilege permissions
- Branch protection enabled
- Required reviews configured
- Environment-specific approvals
Exercise: Add security scanning to your CI/CD pipeline. Implement at least: (1) npm audit or Snyk for dependencies, (2) a SAST tool like CodeQL or Semgrep, (3) Docker image scanning with Trivy. Fix at least one high-severity vulnerability discovered by the scans.