DevSecOps Pipeline: Azure DevOps, GitHub Actions, Container Security, and IaC

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

flowchart TB DEV[Developers] --> GIT[GitHub Repository] GIT --> GHACTIONS[GitHub Actions] subgraph "CI Pipeline - GitHub Actions" GHACTIONS --> LINT[Code Linting] LINT --> SAST[SAST Scanning] SAST --> DEPCHECK[Dependency Check] DEPCHECK --> BUILD[Build Container] BUILD --> IMAGESCAN[Image Scanning] end IMAGESCAN --> ACR[Azure Container Registry] ACR --> AZDO[Azure DevOps Pipeline] subgraph "CD Pipeline - Azure DevOps" AZDO --> IACSCAN[IaC Security Scan] IACSCAN --> DAST[DAST Testing] DAST --> DEPLOY[Deploy to AKS] DEPLOY --> RUNTIME[Runtime Security] end DEPLOY --> AKS[Azure Kubernetes Service] GHACTIONS --> KEYVAULT[Azure Key Vault] AZDO --> KEYVAULT AKS --> DEFENDER[Defender for Containers] AKS --> POLICY[Azure Policy] DEFENDER --> LOGS[Log Analytics] POLICY --> LOGS RUNTIME --> LOGS LOGS --> SENTINEL[Microsoft Sentinel] SENTINEL --> ALERTS[Security Alerts]

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?