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
Securing the delivery pipeline is a systemic discipline—not a collection of one‑off scanners. Effective DevSecOps establishes policy as code, verifiable provenance (SBOM + build attestation), least‑privilege automation (OIDC to Entra workload identities instead of static secrets), runtime guardrails (Policy, Defender), and continuous feedback through SIEM + metrics. This expansion introduces a pragmatic operating model with concrete controls at each stage: plan, code, build, package, deploy, run, respond.
### DevSecOps Operating Model
- Governance: risk register per product, exception process time‑boxed with remediation plan.
- Standards: minimum baseline (SAST, SCA, secret scan, IaC policy, container scan, supply chain attestations).
- Separation of duties: CI and CD identities isolated; production deploy requires policy gate approval.
- Evidence: audits draw from centralized logs, SARIF artifacts, SBOMs, release notes, and attestation bundles.
### Threat Scenarios Addressed
- Dependency supply chain compromise, leaked secrets, image vulnerabilities, misconfigured cloud resources, malicious pull requests, tampered artifacts, runtime drift.
### Key Artifacts
- SBOM (CycloneDX/SPDX), attestation (SLSA provenance), security test reports (SARIF), policy audit logs, exception register.
run: dotnet restore
### Control Mapping
| Stage | Control | Tooling |
|-------|---------|---------|
| Code | SAST, secret scan | CodeQL, TruffleHog |
| Dependencies | SCA | Dependency Review, OWASP DC |
| Build | Reproducible builds, attestation | GitHub OIDC, SLSA provenance |
| Package | Image scan, SBOM attach | Trivy, Syft/CycloneDX |
| Deploy | IaC policy gates | Azure Policy, Checkov |
| Run | Runtime threats | Defender for Containers |
| Respond | SIEM & playbooks | Microsoft Sentinel + Logic Apps |
- name: Code linting
run: dotnet format --verify-no-changes
- name: Run tests
run: dotnet test --configuration Release --collect:"XPlat Code Coverage"
**Bicep Lint + Policy:**
```bash
# Bicep build validation
bicep build main.bicep
# Azure Policy assignment for allowed SKUs / tags
az policy assignment create \
--name enforce-tags-skus \
--policy "/providers/Microsoft.Authorization/policyDefinitions/allowed-skus-and-tags" \
--scope "/subscriptions/<sub>/resourceGroups/rg-platform" \
--params '{"allowedSkus": ["Standard_B2s","P1V3"], "requiredTags": ["Owner","CostCenter","Environment"]}'
```
**OPA Gate in CI:**
```yaml
jobs:
policy-gate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Rego policies
uses: open-policy-agent/opa-action@v1
with:
policy-path: policies/
input-path: iac-plan.json
```
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
files: ./coverage.cobertura.xml
sast-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
### Supply Chain Security
Attach SBOMs and attestations; verify on deploy.
**Generate SBOM (Syft) & Attestation (SLSA):**
```bash
syft $REGISTRY/$IMAGE_NAME:$GITHUB_SHA -o cyclonedx-json > sbom.json
slsa-provenance --artifact $REGISTRY/$IMAGE_NAME:$GITHUB_SHA --out provenance.json
```
**Verify at CD:**
```bash
cosign verify-attestation --type slsaprovenance $REGISTRY/$IMAGE_NAME:$GITHUB_SHA \
--key cosign.pub
```
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: csharp
- name: Autobuild
uses: github/codeql-action/autobuild@v3
- name: Perform CodeQL Analysis
Replace static passwords with managed identities or federated credentials. Rotate remaining secrets via Key Vault; enable purge protection.
uses: github/codeql-action/analyze@v3
secret-scan:
runs-on: ubuntu-latest
steps:
Run containers with non‑root users, read‑only filesystems where feasible, drop NET_RAW, leverage seccomp profiles, and minimal images (Alpine or distroless) to shrink attack surface.
- uses: actions/checkout@v4
with:
fetch-depth: 0
Enable Defender for Containers (plan on subscription), Kubernetes policy packs (Pod Security Standards), and ACR image quarantine pattern (only signed images admitted to prod).
- name: TruffleHog Secret Scan
uses: trufflesecurity/trufflehog@main
with:
path: ./
base: ${{ github.event.repository.default_branch }}
Add playbooks: auto‑open GitHub issue on high severity finding; auto‑quarantine image; notify #secops channel.
head: HEAD
dependency-check:
Issue: SBOM verification fails during CD
Solution: Ensure generator and verifier formats match (CycloneDX vs SPDX); pin tool versions; validate artifact digest alignment with provenance.
runs-on: ubuntu-latest
steps:
- Treat security results as code: store SARIF, SBOM, attestation in artifact store; link to release
- Schedule chaos/security days: simulate secret leak, image tampering, policy misconfiguration and rehearse incident response
uses: actions/dependency-review-action@v4
with:
- Add SLSA level target and roadmap; instrument SBOM coverage metric across services
- 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:**
```bicep
@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: {
```yaml
type: 'SystemAssigned'```
}
properties: {
```yaml
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: {
```yaml
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: {
```yaml
retentionInDays: 90
sku: {
name: 'PerGB2018'
}```
}
}
IaC Security Scanning (Checkov):
# .github/workflows/iac-scan.yml
name: IaC Security Scan
on:
pull_request:
```yaml
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):**
```yaml
trigger: none
resources:
pipelines:
```text
- pipeline: ci-pipeline
source: 'DevSecOps-CI'
trigger:
branches:
include:
- main
variables:
- group: devsecops-secrets
- name: aksResourceGroup
value: 'rg-devsecops-prod'```
- name: aksClusterName
```yaml
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:**
```bash
## 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;..."
Expected output:
{ "name": "kv-myapp", "properties": { "enableRbacAuthorization": true, "provisioningState": "Succeeded", "vaultUri": "https://kv-myapp.vault.azure.net/" } }
Kubernetes SecretProviderClass:
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: azure-keyvault-secrets
namespace: api-service
spec:
provider: azure
parameters:
```yaml
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:
```text
- secretName: app-secrets
type: Opaque
data:
- objectName: database-connection-string
key: connectionString
**Pod Configuration:**
```yaml
apiVersion: v1
kind: Pod
metadata:
name: api-service
namespace: api-service
spec:
containers:
```text
- 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:
```text
- 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:**
```bash
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:
```yaml
kinds:
- apiGroups: [""]
kinds: ["Pod"]
excludedNamespaces:
- kube-system```
parameters: {}
---
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sPSPCapabilities
metadata:
name: psp-capabilities
spec:
match:
```yaml
kinds:
- apiGroups: [""]
kinds: ["Pod"]```
parameters:
```yaml
allowedCapabilities: []
requiredDropCapabilities:
- ALL
**Security Monitoring KQL:**
```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 '{
```text
"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 '{
```text
"tagName": {
"value": "Environment"
}```
}'
Expected output:
{ "displayName": "Require tags on resources", "enforcementMode": "Default" }
Compliance Reporting:
// Policy compliance summary
PolicyEvent
| where TimeGenerated > ago(30d)
| summarize
```text
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
Architecture Decision and Tradeoffs
When designing integrated solutions solutions with Azure + Power Platform, consider these key architectural trade-offs:
| Approach | Best For | Tradeoff |
|---|---|---|
| Managed / platform service | Rapid delivery, reduced ops burden | Less customisation, potential vendor lock-in |
| Custom / self-hosted | Full control, advanced tuning | Higher operational overhead and cost |
Recommendation: Start with the managed approach for most workloads and move to custom only when specific requirements demand it.
Validation and Versioning
- Last validated: April 2026
- Validate examples against your tenant, region, and SKU constraints before production rollout.
- Keep module, CLI, and SDK versions pinned in automation pipelines and review quarterly.
Security and Governance Considerations
- Apply least-privilege access using RBAC roles and just-in-time elevation for admin tasks.
- Store secrets in managed secret stores and avoid embedding credentials in scripts or source files.
- Enable audit logging, data protection policies, and periodic access reviews for regulated workloads.
Cost and Performance Notes
- Define budgets and alerts, then monitor usage and cost trends continuously after go-live.
- Baseline performance with synthetic and real-user checks before and after major changes.
- Scale resources with measured thresholds and revisit sizing after usage pattern changes.
Official Microsoft References
- https://learn.microsoft.com/azure/architecture/
- https://learn.microsoft.com/azure/well-architected/
- https://learn.microsoft.com/power-platform/guidance/
Public Examples from Official Sources
- These examples are sourced from official public Microsoft documentation and sample repositories.
- Documentation examples: https://learn.microsoft.com/azure/well-architected/
- Sample repositories: https://github.com/Azure/ArchitectureCenter
- Prefer adapting these examples to your tenant, subscriptions, and governance requirements before production use.
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
Conclusion
DevSecOps succeeds when security becomes a first‑class, automated quality attribute across every stage of delivery. By enforcing policy as code, verifying supply chain provenance, eliminating static secrets via federated identities, and instrumenting runtime with actionable telemetry, teams reduce risk while accelerating throughput. Treat findings and attestations as versioned artifacts, rehearse incident playbooks, and continuously refine gates based on real metrics—this turns compliance from a bottleneck into a paved path for safe, repeatable releases.
Ready to secure your cloud-native applications?
Discussion