Skip to main content

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

  1. Trust but verify - Automated checks catch issues before production
  2. Shift security left - Find vulnerabilities in CI, not production
  3. Secrets stay secret - Never log or expose secrets
  4. Least privilege - Workflows only get permissions they need
  5. 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_TOKEN
  • DOCKER_REGISTRY_TOKEN
  • ROCKETCHAT_WEBHOOK

Who can manage: @devops-team only

Repository Secrets

Specific to one repo:

  • ANSIBLE_VAULT_PASSWORD_STAGING
  • ANSIBLE_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)