Security Best Practices for DevOps¶
Home Home | Documentation | DevOps
Overview¶
This guide provides comprehensive security best practices for DevOps processes in the CSA-in-a-Box project, covering secure CI/CD pipelines, secret management, infrastructure security, and compliance automation.
Table of Contents¶
- Secure CI/CD Pipelines
- Secret Management
- Infrastructure as Code Security
- Container Security
- Access Control
- Compliance and Auditing
- Incident Response
- Security Monitoring
Secure CI/CD Pipelines¶
Pipeline Security Architecture¶
graph TB
A[Code Commit] --> B[Security Scan]
B --> C{Vulnerabilities?}
C -->|Yes| D[Block Pipeline]
C -->|No| E[Build]
E --> F[Static Analysis]
F --> G{Security Issues?}
G -->|Yes| D
G -->|No| H[Containerize]
H --> I[Image Scan]
I --> J{Vulnerabilities?}
J -->|Yes| D
J -->|No| K[Deploy to Staging]
K --> L[Security Tests]
L --> M{Tests Pass?}
M -->|Yes| N[Deploy to Prod]
M -->|No| D
style D fill:#f44336
style N fill:#4CAF50 Security Scanning Integration¶
Azure DevOps Pipeline with Security:
trigger:
branches:
include:
- main
- develop
pool:
vmImage: 'ubuntu-latest'
variables:
- group: security-scan-credentials
- name: SCAN_SEVERITY_THRESHOLD
value: 'HIGH'
stages:
- stage: SecurityScan
displayName: 'Security Scanning'
jobs:
- job: CredentialScanning
displayName: 'Credential Scanner'
steps:
- task: CredScan@3
displayName: 'Run Credential Scanner'
inputs:
toolMajorVersion: 'V2'
suppressionsFile: 'CredScanSuppressions.json'
- task: ComponentGovernanceComponentDetection@0
displayName: 'Component Detection'
inputs:
scanType: 'Register'
verbosity: 'Verbose'
alertWarningLevel: 'High'
- job: StaticCodeAnalysis
displayName: 'Static Analysis Security Testing (SAST)'
steps:
- task: SonarCloudPrepare@1
displayName: 'Prepare SonarCloud'
inputs:
SonarCloud: 'SonarCloud-Connection'
organization: 'csa-inabox'
scannerMode: 'CLI'
configMode: 'manual'
- task: SonarCloudAnalyze@1
displayName: 'Run Code Analysis'
- task: SonarCloudPublish@1
displayName: 'Publish Quality Gate Result'
inputs:
pollingTimeoutSec: '300'
- job: DependencyScanning
displayName: 'Dependency Vulnerability Scan'
steps:
- task: dependency-check-build-task@6
displayName: 'OWASP Dependency Check'
inputs:
projectName: 'CSA-in-a-Box'
scanPath: '$(Build.SourcesDirectory)'
format: 'ALL'
suppressionPath: '$(Build.SourcesDirectory)/dependency-check-suppressions.xml'
- task: PublishTestResults@2
displayName: 'Publish Dependency Check Results'
inputs:
testResultsFormat: 'JUnit'
testResultsFiles: '**/dependency-check-junit.xml'
failTaskOnFailedTests: true
- stage: Build
displayName: 'Build and Package'
dependsOn: SecurityScan
condition: succeeded()
jobs:
- job: BuildJob
displayName: 'Build Application'
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: '3.11'
- script: |
pip install -r requirements.txt
pip install -r requirements-test.txt
displayName: 'Install dependencies'
- script: |
pytest tests/ --cov=src --cov-report=xml
displayName: 'Run tests with coverage'
- task: PublishCodeCoverageResults@1
displayName: 'Publish code coverage'
inputs:
codeCoverageTool: 'Cobertura'
summaryFileLocation: '$(Build.SourcesDirectory)/coverage.xml'
- stage: ContainerSecurity
displayName: 'Container Security Scan'
dependsOn: Build
condition: succeeded()
jobs:
- job: ContainerScan
displayName: 'Scan Container Images'
steps:
- task: Docker@2
displayName: 'Build Docker Image'
inputs:
command: 'build'
Dockerfile: '**/Dockerfile'
tags: '$(Build.BuildId)'
- task: AzureCLI@2
displayName: 'Scan with Microsoft Defender for Containers'
inputs:
azureSubscription: 'Azure-Service-Connection'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
# Scan image with Azure Defender
az acr task run \
--registry csa-acr \
--name security-scan \
--context /dev/null \
--arg IMAGE=$(Build.BuildId)
- task: aquasecScanner@4
displayName: 'Aqua Security Scanner'
inputs:
image: 'csa-acr.azurecr.io/csa-docs:$(Build.BuildId)'
scanner: 'aqua'
connection: 'Aqua-Security-Connection'
hideBase: false
- stage: DeployStaging
displayName: 'Deploy to Staging'
dependsOn: ContainerSecurity
condition: succeeded()
jobs:
- deployment: DeployStaging
environment: 'staging'
strategy:
runOnce:
deploy:
steps:
- task: AzureWebApp@1
displayName: 'Deploy to Staging'
inputs:
azureSubscription: 'Azure-Service-Connection'
appName: 'csa-docs-staging'
deploymentMethod: 'zipDeploy'
- stage: SecurityTesting
displayName: 'Security Testing'
dependsOn: DeployStaging
condition: succeeded()
jobs:
- job: DAST
displayName: 'Dynamic Application Security Testing'
steps:
- task: owaspzap@1
displayName: 'OWASP ZAP Security Scan'
inputs:
aggressivemode: false
threshold: '50'
targetUrl: 'https://csa-docs-staging.azurewebsites.net'
- task: PublishTestResults@2
displayName: 'Publish ZAP Results'
inputs:
testResultsFormat: 'JUnit'
testResultsFiles: '**/OWASP-ZAP-Report.xml'
- job: PenetrationTesting
displayName: 'Automated Penetration Testing'
steps:
- script: |
# Run automated penetration tests
docker run --rm \
-v $(Build.SourcesDirectory):/zap/wrk/:rw \
owasp/zap2docker-stable \
zap-full-scan.py \
-t https://csa-docs-staging.azurewebsites.net \
-r penetration-test-report.html
displayName: 'Run penetration tests'
- stage: DeployProduction
displayName: 'Deploy to Production'
dependsOn: SecurityTesting
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
jobs:
- deployment: DeployProduction
environment: 'production'
strategy:
runOnce:
deploy:
steps:
- task: AzureWebApp@1
displayName: 'Deploy to Production'
inputs:
azureSubscription: 'Azure-Service-Connection'
appName: 'csa-docs-prod'
deploymentMethod: 'zipDeploy'
Security Gates¶
Quality Gate Configuration:
security_gates:
code_quality:
coverage_threshold: 80
quality_gate: "PASSED"
security_hotspots: "REVIEWED"
vulnerabilities:
critical: 0
high: 0
medium: 5
low: 10
license_compliance:
prohibited_licenses:
- GPL-3.0
- AGPL-3.0
action: "BLOCK"
secrets_detection:
block_on_detection: true
allowed_patterns:
- "test-*"
- "example-*"
Secret Management¶
Azure Key Vault Integration¶
Secure Secret Retrieval:
# secure_config.py
from azure.identity import DefaultAzureCredential
from azure.keyvault.secrets import SecretClient
import os
class SecureConfigManager:
"""Secure configuration management using Azure Key Vault."""
def __init__(self, vault_url: str = None):
self.vault_url = vault_url or os.getenv("KEY_VAULT_URL")
self.credential = DefaultAzureCredential()
self.client = SecretClient(
vault_url=self.vault_url,
credential=self.credential
)
self._cache = {}
def get_secret(self, secret_name: str) -> str:
"""
Retrieve secret from Key Vault with caching.
Args:
secret_name: Name of the secret to retrieve
Returns:
Secret value
Raises:
SecretNotFoundError: If secret doesn't exist
"""
# Check cache first
if secret_name in self._cache:
return self._cache[secret_name]
try:
secret = self.client.get_secret(secret_name)
self._cache[secret_name] = secret.value
return secret.value
except Exception as e:
raise SecretNotFoundError(
f"Failed to retrieve secret '{secret_name}': {str(e)}"
)
def set_secret(self, secret_name: str, secret_value: str):
"""Store secret in Key Vault."""
self.client.set_secret(secret_name, secret_value)
# Invalidate cache
if secret_name in self._cache:
del self._cache[secret_name]
def rotate_secret(self, secret_name: str, new_value: str):
"""Rotate secret with zero-downtime strategy."""
# Store new version
self.client.set_secret(secret_name, new_value)
# Update cache
self._cache[secret_name] = new_value
# Application should use the new value immediately
class SecretNotFoundError(Exception):
"""Raised when secret cannot be retrieved."""
pass
Pipeline Secret Usage:
variables:
- group: production-secrets # Azure DevOps variable group linked to Key Vault
steps:
- task: AzureKeyVault@2
displayName: 'Fetch secrets from Key Vault'
inputs:
azureSubscription: 'Azure-Service-Connection'
KeyVaultName: 'csa-prod-kv'
SecretsFilter: '*'
RunAsPreJob: true
- script: |
# Secrets are now available as environment variables
echo "Connecting to database..."
# Use $(DB-CONNECTION-STRING) from Key Vault
displayName: 'Use secrets securely'
env:
DB_CONNECTION_STRING: $(DB-CONNECTION-STRING)
Secret Rotation Automation¶
#!/bin/bash
# rotate_secrets.sh - Automated secret rotation
# Configuration
KEY_VAULT_NAME="csa-prod-kv"
SECRETS_TO_ROTATE=(
"storage-account-key"
"api-key"
"database-password"
)
# Function to rotate secret
rotate_secret() {
local secret_name=$1
echo "Rotating secret: $secret_name"
# Generate new secret value
new_value=$(openssl rand -base64 32)
# Store new secret version
az keyvault secret set \
--vault-name $KEY_VAULT_NAME \
--name $secret_name \
--value $new_value
# Update dependent services
update_dependent_services $secret_name $new_value
echo "Secret $secret_name rotated successfully"
}
# Function to update services
update_dependent_services() {
local secret_name=$1
local new_value=$2
case $secret_name in
"storage-account-key")
# Update storage account connections
az webapp config appsettings set \
--name csa-docs-prod \
--resource-group rg-csa-docs \
--settings STORAGE_KEY="$new_value"
;;
"api-key")
# Update API configurations
# ...
;;
esac
}
# Rotate all secrets
for secret in "${SECRETS_TO_ROTATE[@]}"; do
rotate_secret "$secret"
done
Infrastructure as Code Security¶
Terraform Security Scanning¶
# security_rules.tf
# Enforce security best practices in IaC
resource "azurerm_policy_assignment" "security_policies" {
name = "security-compliance"
scope = azurerm_resource_group.main.id
policy_definition_id = "/providers/Microsoft.Authorization/policySetDefinitions/1f3afdf9-d0c9-4c3d-847f-89da613e70a8"
parameters = <<PARAMETERS
{
"effect": {
"value": "Audit"
}
}
PARAMETERS
}
# Enable Azure Defender
resource "azurerm_security_center_subscription_pricing" "defender" {
tier = "Standard"
resource_type = "VirtualMachines"
}
# Network security group with restrictive rules
resource "azurerm_network_security_group" "main" {
name = "nsg-secure"
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
security_rule {
name = "DenyAllInbound"
priority = 4096
direction = "Inbound"
access = "Deny"
protocol = "*"
source_port_range = "*"
destination_port_range = "*"
source_address_prefix = "*"
destination_address_prefix = "*"
}
}
Checkov Security Scanning:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/bridgecrewio/checkov
rev: '2.3.187'
hooks:
- id: checkov
args: [
'--framework', 'terraform',
'--skip-check', 'CKV_AZURE_1',
'--quiet',
'--compact'
]
Container Security¶
Secure Dockerfile¶
# Use specific version tags, not 'latest'
FROM python:3.11.6-slim-bullseye AS base
# Create non-root user
RUN groupadd -r appuser && useradd -r -g appuser appuser
# Install security updates
RUN apt-get update && \
apt-get upgrade -y && \
apt-get install -y --no-install-recommends \
ca-certificates && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Set working directory
WORKDIR /app
# Copy requirements first (layer caching)
COPY requirements.txt .
# Install dependencies
RUN pip install --no-cache-dir --upgrade pip && \
pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY --chown=appuser:appuser . .
# Switch to non-root user
USER appuser
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD python health_check.py || exit 1
# Run application
CMD ["python", "app.py"]
Container Registry Security¶
# Enable vulnerability scanning
az acr update \
--name csa-acr \
--resource-group rg-csa-docs \
--anonymous-pull-enabled false
# Enable Defender for Container Registries
az security pricing create \
--name ContainerRegistry \
--tier Standard
# Configure image retention
az acr config retention update \
--registry csa-acr \
--status enabled \
--days 30 \
--type UntaggedManifests
Access Control¶
Role-Based Access Control (RBAC)¶
# rbac-config.yaml
rbac_assignments:
developers:
role: "Contributor"
scope: "/subscriptions/{sub-id}/resourceGroups/rg-csa-docs-dev"
condition: "environment == 'development'"
devops_team:
role: "DevOps Engineer"
scope: "/subscriptions/{sub-id}/resourceGroups/rg-csa-docs-prod"
permissions:
- "Microsoft.Web/sites/write"
- "Microsoft.Web/sites/config/write"
- "Microsoft.KeyVault/vaults/secrets/read"
security_team:
role: "Security Reader"
scope: "/subscriptions/{sub-id}"
permissions:
- "Microsoft.Security/*/read"
- "Microsoft.Security/assessments/read"
Just-In-Time (JIT) Access¶
# Enable JIT VM access
az security jit-policy create \
--resource-group rg-csa-docs \
--name jit-policy \
--location eastus2 \
--kind "Basic" \
--security-contact-emails "security@company.com" \
--security-contact-phone "+1234567890"
Compliance and Auditing¶
Compliance Automation¶
# compliance_check.py
from azure.mgmt.policyinsights import PolicyInsightsClient
from azure.identity import DefaultAzureCredential
def check_compliance():
"""Check Azure Policy compliance status."""
credential = DefaultAzureCredential()
client = PolicyInsightsClient(credential)
# Get compliance summary
subscription_id = "your-subscription-id"
summary = client.policy_states.summarize_for_subscription(
subscription_id=subscription_id
)
compliance_report = {
"total_resources": summary.results.resource_details.compliant +
summary.results.resource_details.non_compliant,
"compliant": summary.results.resource_details.compliant,
"non_compliant": summary.results.resource_details.non_compliant,
"compliance_percentage": (
summary.results.resource_details.compliant /
(summary.results.resource_details.compliant +
summary.results.resource_details.non_compliant)
) * 100
}
return compliance_report
Audit Logging¶
# Enable diagnostic settings for audit logs
az monitor diagnostic-settings create \
--name audit-logs \
--resource /subscriptions/{sub-id}/resourceGroups/rg-csa-docs/providers/Microsoft.Web/sites/csa-docs-prod \
--logs '[
{
"category": "AuditEvent",
"enabled": true,
"retentionPolicy": {
"days": 365,
"enabled": true
}
}
]' \
--workspace /subscriptions/{sub-id}/resourceGroups/rg-monitoring/providers/Microsoft.OperationalInsights/workspaces/law-csa-audit
Incident Response¶
Security Incident Playbook¶
# incident_response.yaml
incident_types:
credential_exposure:
severity: CRITICAL
response_time: 15_minutes
actions:
- Rotate exposed credentials immediately
- Revoke access tokens
- Notify security team
- Review access logs
- Update breach register
vulnerability_detected:
severity: HIGH
response_time: 24_hours
actions:
- Assess impact and exploitability
- Apply patches or workarounds
- Update dependency versions
- Run security scans
- Document remediation
unauthorized_access:
severity: CRITICAL
response_time: 15_minutes
actions:
- Disable compromised accounts
- Review access patterns
- Enable MFA if not already enabled
- Investigate breach scope
- Report to management
Security Monitoring¶
Security Metrics Dashboard¶
// Azure Sentinel - Security Monitoring Query
// Monitor for suspicious activities
SecurityEvent
| where TimeGenerated > ago(24h)
| where EventID in (4625, 4648, 4672) // Failed logins, explicit credentials, special privileges
| summarize
FailedLogins = countif(EventID == 4625),
ExplicitCreds = countif(EventID == 4648),
SpecialPrivileges = countif(EventID == 4672)
by Account, Computer
| where FailedLogins > 5 or SpecialPrivileges > 10
| project Account, Computer, FailedLogins, ExplicitCreds, SpecialPrivileges
| order by FailedLogins desc
Best Practices Checklist¶
- All secrets stored in Azure Key Vault
- Credential scanning enabled in CI/CD
- Container images scanned for vulnerabilities
- RBAC properly configured with least privilege
- Network segmentation implemented
- Audit logging enabled for all resources
- Security monitoring and alerting configured
- Incident response plan documented
- Regular security assessments scheduled
- Compliance policies automated
Additional Resources¶
Last Updated: December 9, 2025 Version: 1.0.0 Maintainer: CSA Security Team