CI/CD Security: Building Secure Deployment Pipelines
The challenge: You’ve built an amazing application. Now you need to deploy it. Manually deploying is slow and error-prone. CI/CD automates this. But here’s the problem: if your pipeline isn’t secure, attackers can inject malicious code, steal secrets, or deploy vulnerable software. Let’s learn how to build secure CI/CD pipelines.
What is CI/CD, Really?
CI/CD stands for:
- CI (Continuous Integration) - Automatically build and test code when changes are pushed
- CD (Continuous Deployment) - Automatically deploy code that passes tests
Think of it like this:
- Without CI/CD: Write code → Manually test → Manually deploy → Hope it works
- With CI/CD: Write code → Push to Git → Pipeline automatically tests → Automatically deploys if tests pass
The security problem: If your pipeline isn’t secure, it becomes an attack vector. Attackers can:
- Inject malicious code
- Steal secrets (API keys, passwords)
- Deploy backdoors
- Access production systems
CI/CD Security Principles
1. Shift Left Security
“Shift left” means adding security early in the development process, not at the end.
Bad approach:
1
Code → Deploy → Security Review → Oops, found vulnerabilities!
Good approach:
1
Code → Security Scan → Fix Issues → Deploy
2. Least Privilege
Give your pipeline only the permissions it needs, nothing more.
3. Secrets Management
Never hardcode secrets. Use secret management systems.
4. Code Scanning
Automatically scan code for vulnerabilities before deployment.
GitHub Actions: Secure Pipeline Example
Here’s a secure CI/CD pipeline for the compliance tool:
.github/workflows/ci-cd.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
name: CI/CD Pipeline
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
env:
PYTHON_VERSION: '3.11'
DOCKER_IMAGE: compliance-tool
jobs:
# Job 1: Code Quality Checks
code-quality:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: $
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pylint flake8
- name: Run Pylint
run: pylint **/*.py --fail-under=7.0
- name: Run Flake8
run: flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# Job 2: Security Scanning
security-scan:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run Bandit (SAST)
uses: securecodewarrior/github-action-bandit@v1
with:
path: .
exit_zero: false
- name: Run Safety (Dependency Check)
run: |
pip install safety
safety check --json
- name: Run Trivy (Container Scan)
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
scan-ref: '.'
format: 'sarif'
output: 'trivy-results.sarif'
- name: Upload Trivy results
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: 'trivy-results.sarif'
# Job 3: Run Tests
test:
runs-on: ubuntu-latest
needs: [code-quality, security-scan]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: $
- name: Install dependencies
run: |
pip install -r requirements.txt
pip install pytest pytest-cov
- name: Run tests
run: pytest tests/ --cov=. --cov-report=xml
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
# Job 4: Build Docker Image
build:
runs-on: ubuntu-latest
needs: [test]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: $
password: $
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: $/$:latest
cache-from: type=registry,ref=$/$:buildcache
cache-to: type=registry,ref=$/$:buildcache,mode=max
# Job 5: Deploy to Kubernetes
deploy:
runs-on: ubuntu-latest
needs: [build]
if: github.ref == 'refs/heads/main'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: $
aws-secret-access-key: $
aws-region: us-east-1
- name: Set up kubectl
uses: azure/setup-kubectl@v3
- name: Deploy to EKS
run: |
aws eks update-kubeconfig --name compliance-cluster
kubectl set image deployment/compliance-scanner \
scanner=$/$:latest \
-n compliance-system
Security Best Practices in CI/CD
1. Use Secrets, Never Hardcode
Bad:
1
2
3
4
- name: Deploy
run: |
aws configure set aws_access_key_id "AKIA..."
aws configure set aws_secret_access_key "secret..."
Good:
1
2
3
4
5
- name: Configure AWS
uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: $
aws-secret-access-key: $
2. Scan Dependencies
Always scan dependencies for vulnerabilities:
1
2
3
4
- name: Safety check
run: |
pip install safety
safety check --json
3. Scan Container Images
Scan Docker images before deployment:
1
2
3
4
5
6
- name: Trivy scan
uses: aquasecurity/trivy-action@master
with:
image-ref: 'my-image:latest'
format: 'sarif'
output: 'trivy-results.sarif'
4. Use IAM Roles (Better than Access Keys)
For AWS, use OIDC instead of access keys:
1
2
3
4
5
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
with:
role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
aws-region: us-east-1
5. Require Approvals for Production
1
2
3
4
5
6
deploy-production:
runs-on: ubuntu-latest
environment: production # Requires approval
steps:
- name: Deploy
run: ./deploy.sh
GitLab CI Example
Here’s the same pipeline in GitLab CI:
.gitlab-ci.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
stages:
- quality
- security
- test
- build
- deploy
variables:
DOCKER_IMAGE: compliance-tool
PYTHON_VERSION: "3.11"
code-quality:
stage: quality
image: python:${PYTHON_VERSION}
script:
- pip install -r requirements.txt
- pip install pylint flake8
- pylint **/*.py --fail-under=7.0
- flake8 . --count --select=E9,F63,F7,F82
only:
- merge_requests
- main
security-scan:
stage: security
image: python:${PYTHON_VERSION}
script:
- pip install safety bandit
- safety check
- bandit -r . -f json -o bandit-report.json
artifacts:
reports:
sast: bandit-report.json
only:
- merge_requests
- main
test:
stage: test
image: python:${PYTHON_VERSION}
script:
- pip install -r requirements.txt
- pip install pytest pytest-cov
- pytest tests/ --cov=. --cov-report=xml
coverage: '/TOTAL.*\s+(\d+%)$/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage.xml
only:
- merge_requests
- main
build:
stage: build
image: docker:latest
services:
- docker:dind
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker build -t $CI_REGISTRY_IMAGE:latest .
- docker push $CI_REGISTRY_IMAGE:latest
only:
- main
deploy:
stage: deploy
image: bitnami/kubectl:latest
script:
- kubectl set image deployment/compliance-scanner \
scanner=$CI_REGISTRY_IMAGE:latest \
-n compliance-system
environment:
name: production
only:
- main
when: manual # Requires manual approval
Security Scanning Tools
1. Bandit (Python SAST)
Scans Python code for security issues:
1
2
pip install bandit
bandit -r . -f json -o bandit-report.json
Finds:
- Hardcoded passwords
- SQL injection risks
- Insecure random number generation
- And more
2. Safety (Dependency Check)
Checks Python dependencies for known vulnerabilities:
1
2
pip install safety
safety check
3. Trivy (Container Scanning)
Scans Docker images for vulnerabilities:
1
trivy image my-image:latest
4. Snyk (Multi-language)
Scans code, dependencies, and containers:
1
2
snyk test
snyk monitor
Secure Deployment Practices
1. Blue-Green Deployment
Deploy to a new environment, test it, then switch traffic:
1
2
3
4
5
deploy-blue:
script:
- kubectl apply -f k8s/blue/
- ./test-deployment.sh blue
- kubectl switch blue
2. Canary Deployment
Deploy to a small percentage, monitor, then roll out:
1
2
3
4
5
6
deploy-canary:
script:
- kubectl set image deployment/app app=my-image:new -n production
- kubectl scale deployment/app --replicas=1 -n production
- sleep 300 # Monitor for 5 minutes
- kubectl scale deployment/app --replicas=10 -n production
3. Rollback Strategy
Always have a rollback plan:
1
2
3
rollback:
script:
- kubectl rollout undo deployment/app -n production
Key Takeaways
- CI/CD = Automation - But must be secure
- Shift Left Security - Scan early, not late
- Use Secrets Management - Never hardcode
- Scan Everything - Code, dependencies, containers
- Least Privilege - Minimal permissions
- Require Approvals - For production deployments
- Have Rollback Plans - Things will break
Practice Exercise
Try this yourself:
- Create a GitHub Actions workflow
- Add code quality checks
- Add security scanning (Bandit, Safety)
- Add container scanning (Trivy)
- Configure secrets properly
- Add deployment step
Resources to Learn More
What’s Next?
Now that you understand CI/CD security, you’re ready to:
- Build production-ready pipelines
- Integrate security scanning
- Deploy securely to cloud platforms
Remember: Secure CI/CD is about automation AND security. Don’t sacrifice one for the other!
💡 Pro Tip: Start with a simple pipeline, then gradually add security checks. Don’t try to implement everything at once. Get the basics working first, then enhance security!
Ready to manage infrastructure as code? Check out our next post on Infrastructure as Code, where we’ll learn Terraform and Pulumi!