DevSecOps Pipeline: Azure DevOps, GitHub Actions, Container Security, and IaC
Introduction
DevSecOps integrates security practices throughout the software development lifecycle—from code commit to production deployment. This deep dive builds a comprehensive DevSecOps pipeline leveraging Azure DevOps, GitHub Actions, container image scanning, secret management with Key Vault, infrastructure as code (IaC) security validation, and automated compliance reporting.
Solution Architecture
Components Overview
| Component | Role | Key Features |
|---|---|---|
| GitHub Actions | CI orchestration | SAST, dependency scanning, secret detection |
| Azure DevOps | CD orchestration | Release gates, approvals, IaC validation |
| Azure Container Registry | Image registry | Defender scanning, vulnerability assessment |
| Azure Key Vault | Secret management | Managed identities, secret rotation |
| Azure Policy | Compliance | Pod Security Standards, resource tagging |
| Defender for Containers | Runtime security | Threat detection, anomaly detection |
| Microsoft Sentinel | SIEM | Security analytics, incident response |
Implementation Guide
Phase 1: GitHub Actions CI Pipeline
Workflow Configuration (.github/workflows/ci.yml):
name: DevSecOps CI Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
REGISTRY: contosoacr.azurecr.io
IMAGE_NAME: api-service
jobs:
code-quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Restore dependencies
run: dotnet restore
- name: Code linting
run: dotnet format --verify-no-changes
- name: Run tests
run: dotnet test --configuration Release --collect:"XPlat Code Coverage"
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
files: ./coverage.cobertura.xml
sast-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: csharp
- name: Autobuild
uses: github/codeql-action/autobuild@v3
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
secret-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: TruffleHog Secret Scan
uses: trufflesecurity/trufflehog@main
with:
path: ./
base: ${{ github.event.repository.default_branch }}
head: HEAD
dependency-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Dependency Review
uses: actions/dependency-review-action@v4
with:
fail-on-severity: high
- name: OWASP Dependency Check
uses: dependency-check/Dependency-Check_Action@main
with:
project: 'api-service'
path: '.'
format: 'HTML'
args: >
--enableRetired
--enableExperimental
build-and-scan:
needs: [code-quality, sast-scan, secret-scan, dependency-check]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Login to ACR
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.ACR_USERNAME }}
password: ${{ secrets.ACR_PASSWORD }}
- name: Build container image
run: |
docker build -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} .
docker build -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest .
- name: Scan image with Trivy
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
- name: Upload Trivy results to GitHub Security
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: 'trivy-results.sarif'
- name: Push to ACR
run: |
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
Phase 2: Infrastructure as Code Security
Bicep Template with Security Controls:
@description('Location for all resources')
param location string = resourceGroup().location
@description('AKS cluster name')
param aksClusterName string = 'aks-devsecops'
@description('Enable RBAC')
param enableRBAC bool = true
@description('Network plugin')
@allowed(['azure', 'kubenet'])
param networkPlugin string = 'azure'
resource aks 'Microsoft.ContainerService/managedClusters@2024-01-01' = {
name: aksClusterName
location: location
identity: {
type: 'SystemAssigned'
}
properties: {
dnsPrefix: '${aksClusterName}-dns'
enableRBAC: enableRBAC
// Security features
aadProfile: {
managed: true
enableAzureRBAC: true
adminGroupObjectIDs: [
'admin-group-object-id'
]
}
// Defender for Containers
securityProfile: {
defender: {
securityMonitoring: {
enabled: true
}
}
imageCleaner: {
enabled: true
intervalHours: 24
}
}
// Network security
networkProfile: {
networkPlugin: networkPlugin
networkPolicy: 'azure'
serviceCidr: '10.0.0.0/16'
dnsServiceIP: '10.0.0.10'
}
// Pod Security Standards
podIdentityProfile: {
enabled: true
}
agentPoolProfiles: [
{
name: 'systempool'
count: 3
vmSize: 'Standard_D4s_v3'
mode: 'System'
enableAutoScaling: true
minCount: 3
maxCount: 10
// Security hardening
enableNodePublicIP: false
enableEncryptionAtHost: true
osDiskType: 'Ephemeral'
}
]
// Azure Policy add-on
addonProfiles: {
azurepolicy: {
enabled: true
}
azureKeyvaultSecretsProvider: {
enabled: true
}
}
}
}
// Enable diagnostic logging
resource diagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = {
name: 'aks-diagnostics'
scope: aks
properties: {
workspaceId: logAnalyticsWorkspace.id
logs: [
{
category: 'kube-apiserver'
enabled: true
}
{
category: 'kube-audit'
enabled: true
}
{
category: 'kube-controller-manager'
enabled: true
}
]
metrics: [
{
category: 'AllMetrics'
enabled: true
}
]
}
}
resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2022-10-01' = {
name: 'law-devsecops'
location: location
properties: {
retentionInDays: 90
sku: {
name: 'PerGB2018'
}
}
}
IaC Security Scanning (Checkov):
# .github/workflows/iac-scan.yml
name: IaC Security Scan
on:
pull_request:
paths:
- '**/*.bicep'
- '**/*.tf'
jobs:
checkov-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Checkov
uses: bridgecrewio/checkov-action@master
with:
directory: ./infrastructure
framework: bicep
soft_fail: false
output_format: sarif
output_file_path: checkov-results.sarif
- name: Upload results
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: checkov-results.sarif
Phase 3: Azure DevOps CD Pipeline
Release Pipeline (azure-pipelines-cd.yml):
trigger: none
resources:
pipelines:
- pipeline: ci-pipeline
source: 'DevSecOps-CI'
trigger:
branches:
include:
- main
variables:
- group: devsecops-secrets
- name: aksResourceGroup
value: 'rg-devsecops-prod'
- name: aksClusterName
value: 'aks-devsecops-prod'
stages:
- stage: SecurityValidation
displayName: 'Security Validation'
jobs:
- job: IaCSecurityScan
displayName: 'Infrastructure Security Scan'
steps:
- task: AzureCLI@2
displayName: 'Deploy infrastructure (validate mode)'
inputs:
azureSubscription: 'Azure-Prod'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
az deployment group validate \
--resource-group $(aksResourceGroup) \
--template-file infrastructure/main.bicep \
--parameters @infrastructure/prod.parameters.json
- task: Bash@3
displayName: 'Run Terraform compliance checks'
inputs:
targetType: 'inline'
script: |
terraform-compliance -f compliance/ -p terraform-plan.json
- job: ContainerScan
displayName: 'Container Vulnerability Scan'
steps:
- task: AzureCLI@2
displayName: 'ACR vulnerability assessment'
inputs:
azureSubscription: 'Azure-Prod'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
IMAGE_DIGEST=$(az acr repository show-tags \
--name contosoacr \
--repository api-service \
--orderby time_desc \
--output tsv \
--query "[0]")
VULN_COUNT=$(az security assessment list \
--resource-id "/subscriptions/$(SUBSCRIPTION_ID)/resourceGroups/$(aksResourceGroup)/providers/Microsoft.ContainerRegistry/registries/contosoacr" \
--query "[?displayName=='Container registry images should have vulnerability findings resolved'].status.code" \
-o tsv | grep -c "Unhealthy" || echo 0)
if [ "$VULN_COUNT" -gt 0 ]; then
echo "##vso[task.logissue type=error]Critical vulnerabilities found in container image"
exit 1
fi
- stage: DeployDev
displayName: 'Deploy to Dev'
dependsOn: SecurityValidation
jobs:
- deployment: DeployAKS
displayName: 'Deploy to AKS Dev'
environment: 'dev'
strategy:
runOnce:
deploy:
steps:
- task: KubernetesManifest@0
displayName: 'Deploy to Kubernetes'
inputs:
action: 'deploy'
kubernetesServiceConnection: 'AKS-Dev'
namespace: 'api-service'
manifests: |
k8s/deployment.yaml
k8s/service.yaml
containers: 'contosoacr.azurecr.io/api-service:$(Build.BuildId)'
- stage: DastTesting
displayName: 'DAST Testing'
dependsOn: DeployDev
jobs:
- job: RunDast
displayName: 'OWASP ZAP Scan'
steps:
- task: Bash@3
displayName: 'Run ZAP baseline scan'
inputs:
targetType: 'inline'
script: |
docker run -v $(pwd):/zap/wrk/:rw \
-t ghcr.io/zaproxy/zaproxy:stable \
zap-baseline.py \
-t https://api-dev.contoso.com \
-r zap-report.html \
-J zap-report.json
- stage: DeployProd
displayName: 'Deploy to Production'
dependsOn: DastTesting
jobs:
- deployment: DeployProd
displayName: 'Deploy to AKS Prod'
environment: 'production'
strategy:
canary:
increments: [10, 25, 50, 100]
preDeploy:
steps:
- task: AzureCLI@2
displayName: 'Snapshot configuration'
inputs:
azureSubscription: 'Azure-Prod'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
kubectl create configmap deployment-backup-$(Build.BuildId) \
--from-literal=timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \
--namespace api-service
deploy:
steps:
- task: KubernetesManifest@0
displayName: 'Deploy canary'
inputs:
action: 'deploy'
kubernetesServiceConnection: 'AKS-Prod'
namespace: 'api-service'
manifests: 'k8s/deployment.yaml'
containers: 'contosoacr.azurecr.io/api-service:$(Build.BuildId)'
strategy: 'canary'
trafficSplitMethod: 'smi'
percentage: $(strategy.increment)
routeTraffic:
steps:
- task: Bash@3
displayName: 'Monitor canary metrics'
inputs:
targetType: 'inline'
script: |
ERROR_RATE=$(kubectl top pods -n api-service --containers | grep canary | awk '{print $3}')
if (( $(echo "$ERROR_RATE > 5.0" | bc -l) )); then
echo "##vso[task.logissue type=error]Canary error rate too high: $ERROR_RATE%"
exit 1
fi
postRouteTraffic:
steps:
- task: Bash@3
displayName: 'Verify deployment health'
inputs:
targetType: 'inline'
script: |
kubectl rollout status deployment/api-service -n api-service --timeout=300s
Phase 4: Secret Management with Key Vault
Key Vault Setup:
# Create Key Vault
az keyvault create \
--name kv-devsecops-prod \
--resource-group rg-devsecops-prod \
--location eastus \
--enable-rbac-authorization true
# Assign permissions to AKS managed identity
AKS_IDENTITY=$(az aks show \
--resource-group rg-devsecops-prod \
--name aks-devsecops-prod \
--query identityProfile.kubeletidentity.clientId -o tsv)
az role assignment create \
--assignee $AKS_IDENTITY \
--role "Key Vault Secrets User" \
--scope /subscriptions/<subscription-id>/resourceGroups/rg-devsecops-prod/providers/Microsoft.KeyVault/vaults/kv-devsecops-prod
# Store secrets
az keyvault secret set \
--vault-name kv-devsecops-prod \
--name database-connection-string \
--value "Server=sqlserver.database.windows.net;Database=mydb;..."
Kubernetes SecretProviderClass:
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: azure-keyvault-secrets
namespace: api-service
spec:
provider: azure
parameters:
usePodIdentity: "false"
useVMManagedIdentity: "true"
userAssignedIdentityID: "<kubelet-identity-client-id>"
keyvaultName: "kv-devsecops-prod"
cloudName: ""
objects: |
array:
- |
objectName: database-connection-string
objectType: secret
objectVersion: ""
tenantId: "<tenant-id>"
secretObjects:
- secretName: app-secrets
type: Opaque
data:
- objectName: database-connection-string
key: connectionString
Pod Configuration:
apiVersion: v1
kind: Pod
metadata:
name: api-service
namespace: api-service
spec:
containers:
- name: api
image: contosoacr.azurecr.io/api-service:latest
env:
- name: DATABASE_CONNECTION_STRING
valueFrom:
secretKeyRef:
name: app-secrets
key: connectionString
volumeMounts:
- name: secrets-store
mountPath: "/mnt/secrets-store"
readOnly: true
volumes:
- name: secrets-store
csi:
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: "azure-keyvault-secrets"
Phase 5: Runtime Security with Defender for Containers
Enable Defender:
az security pricing create \
--name Containers \
--tier Standard
az aks update \
--resource-group rg-devsecops-prod \
--name aks-devsecops-prod \
--enable-defender
Pod Security Policy (Gatekeeper Constraints):
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sPSPPrivilegedContainer
metadata:
name: psp-privileged-container
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
excludedNamespaces:
- kube-system
parameters: {}
---
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sPSPCapabilities
metadata:
name: psp-capabilities
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
parameters:
allowedCapabilities: []
requiredDropCapabilities:
- ALL
Security Monitoring KQL:
// Suspicious process execution in containers
ContainerLog
| where TimeGenerated > ago(1h)
| where LogEntry contains "chmod +x" or LogEntry contains "wget" or LogEntry contains "curl"
| where ContainerName !in ("monitoring-agent", "log-collector")
| project TimeGenerated, Computer, ContainerName, LogEntry
// Failed authentication attempts
AzureDiagnostics
| where Category == "kube-audit"
| where log_s contains "authentication failed"
| summarize FailedAttempts = count() by user_username_s, bin(TimeGenerated, 5m)
| where FailedAttempts > 5
| order by TimeGenerated desc
// Privilege escalation detection
| where Category == "kube-audit"
| where verb_s in ("create", "update", "patch")
| where objectRef_resource_s in ("clusterroles", "clusterrolebindings", "roles", "rolebindings")
| project TimeGenerated, user_username_s, verb_s, objectRef_name_s, objectRef_resource_s
Phase 6: Compliance Automation
Azure Policy Assignments:
# Pod Security Baseline
az policy assignment create \
--name 'aks-pod-security-baseline' \
--policy '/providers/Microsoft.Authorization/policySetDefinitions/a8640138-9b0a-4a28-b8cb-1666c838647d' \
--scope /subscriptions/<subscription-id>/resourceGroups/rg-devsecops-prod \
--params '{
"effect": {
"value": "audit"
}
}'
# Required tags
az policy assignment create \
--name 'require-resource-tags' \
--policy '/providers/Microsoft.Authorization/policyDefinitions/96670d01-0a4d-4649-9c89-2d3abc0a5025' \
--scope /subscriptions/<subscription-id> \
--params '{
"tagName": {
"value": "Environment"
}
}'
Compliance Reporting:
// Policy compliance summary
PolicyEvent
| where TimeGenerated > ago(30d)
| summarize
TotalEvaluations = count(),
NonCompliant = countif(ComplianceState == "NonCompliant"),
Compliant = countif(ComplianceState == "Compliant")
by PolicyDefinitionName
| extend ComplianceRate = (Compliant * 100.0) / TotalEvaluations
| order by ComplianceRate asc
// Security recommendations by severity
SecurityRecommendation
| where TimeGenerated > ago(7d)
| summarize Count = count() by RecommendationSeverity, RecommendationState
| order by RecommendationSeverity desc
Best Practices
- Shift-Left Security: Integrate security checks early in CI pipeline (SAST before build)
- Least Privilege: Use managed identities with minimal RBAC permissions
- Secrets Rotation: Implement automated secret rotation with Key Vault
- Image Signing: Sign container images with Notation and verify signatures at admission
- Network Segmentation: Use Azure Policy to enforce network policies in AKS
- Audit Logging: Enable comprehensive audit logs for all control plane operations
- Vulnerability SLA: Define SLA for patching critical vulnerabilities (e.g., 24 hours)
Troubleshooting
Issue: Trivy scan fails with registry authentication error
Solution: Ensure ACR credentials are correctly configured in GitHub secrets; use managed identity where possible
Issue: Key Vault CSI driver pod crashes
Solution: Verify kubelet identity has "Key Vault Secrets User" role; check SecretProviderClass syntax
Issue: Azure Policy blocks legitimate deployments
Solution: Review policy parameters; add exemptions for specific namespaces/resources; use audit mode initially
Key Takeaways
- DevSecOps embeds security throughout the entire software lifecycle
- GitHub Actions and Azure DevOps provide complementary CI/CD capabilities
- Container scanning catches vulnerabilities before runtime deployment
- Azure Policy automates compliance enforcement at scale
- Defender for Containers provides runtime threat detection
Next Steps
- Implement software bill of materials (SBOM) generation with Syft
- Add supply chain security with Sigstore Cosign image signing
- Integrate chaos engineering with Azure Chaos Studio
- Deploy service mesh (Istio/Linkerd) for mTLS and fine-grained policies
Additional Resources
Ready to secure your cloud-native applications?