CI/CD Security
Overview
GitHub Actions is our CI/CD platform. We use it to automate testing, security scanning, and deployments while maintaining security.
Core Principles
- Trust but verify - Automated checks catch issues before production
- Shift security left - Find vulnerabilities in CI, not production
- Secrets stay secret - Never log or expose secrets
- Least privilege - Workflows only get permissions they need
- Audit everything - All deployments logged and traceable
Required CI Checks
All repositories must have these GitHub Actions workflows:
1. Build & Test (Required Before Merge)
# .github/workflows/ci.yml
name: CI
on: [pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build
run: npm run build # or your build command
- name: Test
run: npm test
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Lint
run: npm run lint
2. Security Scanning (Required Before Merge)
# .github/workflows/security.yml
name: Security Scan
on: [pull_request]
jobs:
secret-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: gitleaks/gitleaks-action@v2
dependency-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run Dependency Check
run: npm audit --audit-level=moderate
sast:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run Semgrep
run: |
pip install semgrep
semgrep --config=auto .
3. Branch Protection (Enforced)
Required on main branch:
- ✅ Require pull request before merging
- ✅ Require 1 approval
- ✅ Require status checks to pass (CI + Security)
- ✅ Require conversation resolution
- ✅ Include administrators (no bypassing)
- ❌ Allow force pushes (disabled)
Deployment Workflows
Staging Deployment (Automatic)
# .github/workflows/deploy-staging.yml
name: Deploy to Staging
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
environment: staging # Uses staging secrets
steps:
- uses: actions/checkout@v3
- name: Deploy via Ansible
env:
ANSIBLE_VAULT_PASSWORD: ${{ secrets.ANSIBLE_VAULT_PASSWORD_STAGING }}
run: |
echo "$ANSIBLE_VAULT_PASSWORD" > /tmp/vault_pass
ansible-playbook playbooks/deploy.yml \
-i inventory/staging \
--vault-password-file /tmp/vault_pass
rm /tmp/vault_pass
- name: Health Check
run: |
curl -f https://staging.company.com/health || exit 1
- name: Notify Rocket.Chat
run: |
curl -X POST "${{ secrets.ROCKETCHAT_WEBHOOK }}" \
-d '{"text":"✅ Staging deployed: '"$GITHUB_SHA"'"}'
Production Deployment (Manual Approval)
# .github/workflows/deploy-production.yml
name: Deploy to Production
on:
workflow_dispatch: # Manual trigger only
inputs:
version:
description: 'Version to deploy'
required: true
jobs:
deploy:
runs-on: ubuntu-latest
environment:
name: production
url: https://company.com
steps:
- uses: actions/checkout@v3
with:
ref: ${{ github.event.inputs.version }}
- name: Deploy via Ansible
env:
ANSIBLE_VAULT_PASSWORD: ${{ secrets.ANSIBLE_VAULT_PASSWORD_PROD }}
run: |
echo "$ANSIBLE_VAULT_PASSWORD" > /tmp/vault_pass
ansible-playbook playbooks/deploy.yml \
-i inventory/production \
--vault-password-file /tmp/vault_pass \
-e "version=${{ github.event.inputs.version }}"
rm /tmp/vault_pass
- name: Health Check
run: |
curl -f https://company.com/health || exit 1
- name: Notify Team
run: |
curl -X POST "${{ secrets.ROCKETCHAT_WEBHOOK }}" \
-d '{"text":"🚀 Production deployed: '"${{ github.event.inputs.version }}"'"}'
Secrets Management in GitHub Actions
Organization-Level Secrets
Shared across all repos:
DIGITALOCEAN_TOKENDOCKER_REGISTRY_TOKENROCKETCHAT_WEBHOOK
Who can manage: @devops-team only
Repository Secrets
Specific to one repo:
ANSIBLE_VAULT_PASSWORD_STAGINGANSIBLE_VAULT_PASSWORD_PROD- Service-specific API keys
Environment Secrets
Production environment:
- Requires manual approval from @devops-team
- Secrets only accessible in prod deployments
Staging environment:
- No approval required
- Auto-deploys from main branch
Security Best Practices
1. Minimize Secret Exposure
# ❌ Bad - Secret exposed in output
- name: Deploy
run: echo "Deploying with key: ${{ secrets.API_KEY }}"
# ✅ Good - Secret used but not logged
- name: Deploy
env:
API_KEY: ${{ secrets.API_KEY }}
run: |
./deploy.sh # Script uses $API_KEY internally
2. Use Least Privilege Permissions
# Explicitly set permissions
permissions:
contents: read # Can read code
pull-requests: write # Can comment on PRs
# Does NOT have write to contents, packages, etc.
3. Pin Action Versions
# ❌ Bad - Uses latest (could change)
- uses: actions/checkout@v3
# ✅ Better - Pinned to specific version
- uses: actions/checkout@v3.5.2
# ✅ Best - Pinned to commit SHA
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab
4. Validate Inputs
- name: Deploy
run: |
# Validate version format
if [[ ! "${{ inputs.version }}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Invalid version format"
exit 1
fi
./deploy.sh "${{ inputs.version }}"
5. Clean Up Sensitive Files
- name: Deploy with secrets
run: |
echo "${{ secrets.KEY }}" > /tmp/key.pem
chmod 600 /tmp/key.pem
./deploy.sh
rm -f /tmp/key.pem # Always clean up
Docker Image Builds
Secure Dockerfile
# Use specific version, not latest
FROM node:18.17.0-alpine
# Run as non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
# Set working directory
WORKDIR /app
# Copy only necessary files
COPY --chown=nodejs:nodejs package*.json ./
RUN npm ci --only=production
COPY --chown=nodejs:nodejs . .
# Switch to non-root user
USER nodejs
# Expose port (documentation only)
EXPOSE 3000
CMD ["node", "server.js"]
Build & Scan Workflow
# .github/workflows/docker-build.yml
name: Build Docker Image
on: [pull_request, push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build image
run: docker build -t myapp:${{ github.sha }} .
- name: Scan for vulnerabilities
uses: aquasecurity/trivy-action@master
with:
image-ref: myapp:${{ github.sha }}
severity: 'HIGH,CRITICAL'
exit-code: '1' # Fail if vulnerabilities found
- name: Push to registry (if main branch)
if: github.ref == 'refs/heads/main'
run: |
echo "${{ secrets.DOCKER_REGISTRY_TOKEN }}" | docker login -u ${{ secrets.DOCKER_REGISTRY_USER }} --password-stdin
docker push myapp:${{ github.sha }}
Monitoring & Auditing
Deployment Notifications
All deployments notify Rocket.Chat:
- name: Notify deployment
if: always() # Run even if previous steps fail
run: |
STATUS="${{ job.status }}"
curl -X POST "${{ secrets.ROCKETCHAT_WEBHOOK }}" \
-H 'Content-Type: application/json' \
-d '{
"text": "'"$STATUS"' - Production deployment",
"attachments": [{
"title": "Version: '"${{ github.event.inputs.version }}"'",
"text": "Deployed by: '"${{ github.actor }}"'",
"color": "'"$([ "$STATUS" = "success" ] && echo "good" || echo "danger")"'"
}]
}'
Audit Trail
All deployments logged:
- Who triggered deployment
- What version deployed
- When deployed
- Success or failure
- Duration
View in: GitHub Actions history + Rocket.Chat notifications
Rollback Procedures
# .github/workflows/rollback.yml
name: Rollback Production
on:
workflow_dispatch:
inputs:
version:
description: 'Version to rollback to'
required: true
jobs:
rollback:
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v3
with:
ref: ${{ github.event.inputs.version }}
- name: Confirm rollback
run: |
echo "⚠️ Rolling back production to ${{ github.event.inputs.version }}"
echo "Current version: $(curl -s https://company.com/version)"
- name: Deploy previous version
run: |
ansible-playbook playbooks/deploy.yml \
-i inventory/production \
-e "version=${{ github.event.inputs.version }}"
- name: Verify rollback
run: |
sleep 10
curl -f https://company.com/health || exit 1
Common Workflows
Deploy to staging: Automatic on merge to main
Deploy to production: Manual via Actions tab → Deploy to Production
Rollback production: Manual via Actions tab → Rollback → Enter version
Run security scan: Automatic on every PR
Troubleshooting
Workflow failing:
- Check Actions tab for logs
- Click failed step for details
- Review recent changes to workflow file
Secret not working:
- Verify secret exists in Settings → Secrets
- Check secret name matches workflow
- Secrets are case-sensitive
Deployment stuck:
- Check if waiting for approval (production)
- Check Ansible logs for errors
- Verify server connectivity (Netbird VPN)