Comprehensive security guide for LastFM Reader v3, covering secret management, Azure security features, container security, and operational best practices.
- Overview
- Secret Management
- Azure Key Vault Integration
- Managed Identity & RBAC
- Container Security
- Network Security
- Storage Security
- Secret Rotation
- Audit & Compliance
- Security Checklist
LastFM Reader v3 follows these security principles:
- Defense in Depth: Multiple layers of security controls
- Least Privilege: Minimal permissions for each component
- Secret Zero Trust: Never store secrets in code or plaintext
- Audit Everything: Comprehensive logging of security events
- Encryption Everywhere: Data encrypted at rest and in transit
| Threat | Mitigation |
|---|---|
| API key exposure | Key Vault, managed identity, no hardcoded secrets |
| Unauthorized access | Azure RBAC, managed identity, network policies |
| Container escape | Distroless base, non-root user, read-only filesystem |
| Data exfiltration | Private endpoints, storage firewall, audit logs |
| Supply chain attacks | Multi-stage build, verified base images, minimal dependencies |
CRITICAL: Never commit these to version control:
# ❌ NEVER do this:
export LASTFM_API_KEY="abc123..." # Don't commit shell history
echo "LASTFM_API_KEY=abc123" > .env # Don't commit .env filesVerify .gitignore contains:
# Environment and configuration
.env
.env.*
*.local
# Runtime data
*.log
*.watermark
# Azure-specific (if present)
azure.yaml
.azure/Current .gitignore status:
# Check if .env is ignored
git check-ignore .env
# Expected output: .env
# Verify no secrets are tracked
git ls-files | grep -E '\.(env|local|secret)$'
# Expected output: (empty)For local development, use .env files:
# Copy example template
cp .env.example .env
# Edit with your secrets (never commit this file)
nano .envBest practices:
- Store
.envoutside the repository directory (e.g.,~/.config/lastfm-reader/.env) - Use a password manager to store API keys
- Rotate API keys regularly (every 90 days)
- Use different API keys for dev/staging/prod
When passing secrets as environment variables:
# ✅ Good: Read from secure storage
export LASTFM_API_KEY=$(security find-generic-password -s lastfm-api-key -w)
# ✅ Good: Read from password manager
export LASTFM_API_KEY=$(pass show lastfm/api-key)
# ❌ Bad: Hardcoded in shell
export LASTFM_API_KEY="abc123..."
# ❌ Bad: Passed as command argument (visible in ps)
./lastfm-sync --api-key abc123...Azure Key Vault provides:
- Centralized Secret Management: Single source of truth for secrets
- Access Control: Fine-grained RBAC permissions
- Audit Logs: Complete history of secret access
- Encryption: Hardware-backed encryption (HSM)
- Versioning: Track secret changes over time
- Soft Delete: Recover accidentally deleted secrets
# Create Key Vault
az keyvault create \
--name lastfm-kv \
--resource-group lastfm-rg \
--location eastus \
--enable-rbac-authorization true \
--retention-days 90 \
--enable-purge-protection true
# Enable diagnostic logging
az monitor diagnostic-settings create \
--name kv-diagnostics \
--resource $(az keyvault show --name lastfm-kv --query id -o tsv) \
--workspace /subscriptions/{sub-id}/resourceGroups/lastfm-rg/providers/Microsoft.OperationalInsights/workspaces/lastfm-logs \
--logs '[{"category": "AuditEvent", "enabled": true}]'# Store Last.fm API key
az keyvault secret set \
--vault-name lastfm-kv \
--name lastfm-api-key \
--value "your-api-key-here" \
--description "Last.fm API key for production" \
--tags environment=production app=lastfm-reader
# Store storage account connection string (if not using managed identity)
az keyvault secret set \
--vault-name lastfm-kv \
--name storage-connection-string \
--value "DefaultEndpointsProtocol=https;AccountName=..." \
--description "Azure Storage connection string"
# Verify secrets
az keyvault secret list --vault-name lastfm-kv -o table# Grant your user account access (for testing)
az keyvault set-policy \
--name lastfm-kv \
--upn your-email@domain.com \
--secret-permissions get list set delete
# For RBAC-enabled Key Vault (preferred)
az role assignment create \
--role "Key Vault Secrets User" \
--assignee your-email@domain.com \
--scope /subscriptions/{sub-id}/resourceGroups/lastfm-rg/providers/Microsoft.KeyVault/vaults/lastfm-kv# Create managed identity
az identity create \
--name lastfm-identity \
--resource-group lastfm-rg
# Get identity details
IDENTITY_ID=$(az identity show --name lastfm-identity --resource-group lastfm-rg --query id -o tsv)
PRINCIPAL_ID=$(az identity show --name lastfm-identity --resource-group lastfm-rg --query principalId -o tsv)
# Grant Key Vault access to managed identity
az role assignment create \
--role "Key Vault Secrets User" \
--assignee-object-id $PRINCIPAL_ID \
--assignee-principal-type ServicePrincipal \
--scope /subscriptions/{sub-id}/resourceGroups/lastfm-rg/providers/Microsoft.KeyVault/vaults/lastfm-kv
# Deploy container with managed identity
az container create \
--name lastfm-sync \
--resource-group lastfm-rg \
--image ghcr.io/yourusername/lastfm-reader:latest \
--assign-identity $IDENTITY_ID \
--secure-environment-variables \
LASTFM_API_KEY=@Microsoft.KeyVault(SecretUri=https://lastfm-kv.vault.azure.net/secrets/lastfm-api-key/)How it works:
- Container starts with managed identity assigned
- Azure injects Key Vault references as environment variables
- Container runtime fetches secrets from Key Vault using managed identity
- Secrets never stored in container definition or logs
# Create service principal
az ad sp create-for-rbac \
--name lastfm-sp \
--role "Key Vault Secrets User" \
--scopes /subscriptions/{sub-id}/resourceGroups/lastfm-rg/providers/Microsoft.KeyVault/vaults/lastfm-kv
# Store service principal credentials in Key Vault
az keyvault secret set \
--vault-name lastfm-kv \
--name sp-client-id \
--value "sp-app-id"
az keyvault secret set \
--vault-name lastfm-kv \
--name sp-client-secret \
--value "sp-password"Note: Managed identity is preferred over service principal as it eliminates the need to manage credentials.
- Enable RBAC: Use RBAC instead of access policies for better governance
- Enable Purge Protection: Prevent permanent deletion of secrets
- Enable Soft Delete: Recover accidentally deleted secrets (90-day retention)
- Use Separate Key Vaults: One per environment (dev/staging/prod)
- Tag Secrets: Add metadata for organization and compliance
- Monitor Access: Enable diagnostic logs to Log Analytics
- Rotate Secrets: Implement automated rotation (see Secret Rotation)
Managed identity eliminates:
- Storing credentials in code or configuration
- Rotating service principal passwords
- Managing certificate expiration
- Risk of credential leakage
| Type | Use Case | Assignment |
|---|---|---|
| System-assigned | Single resource (e.g., one container instance) | Automatically created with resource |
| User-assigned | Multiple resources (e.g., multiple container instances) | Created separately, assigned to resources |
# Create managed identity
az identity create \
--name lastfm-identity \
--resource-group lastfm-rg \
--location eastus
# Get identity IDs
IDENTITY_ID=$(az identity show --name lastfm-identity --resource-group lastfm-rg --query id -o tsv)
PRINCIPAL_ID=$(az identity show --name lastfm-identity --resource-group lastfm-rg --query principalId -o tsv)
CLIENT_ID=$(az identity show --name lastfm-identity --resource-group lastfm-rg --query clientId -o tsv)
echo "Identity ID: $IDENTITY_ID"
echo "Principal ID: $PRINCIPAL_ID"
echo "Client ID: $CLIENT_ID"# Grant Key Vault Secrets User role (read-only)
az role assignment create \
--role "Key Vault Secrets User" \
--assignee-object-id $PRINCIPAL_ID \
--assignee-principal-type ServicePrincipal \
--scope /subscriptions/{sub-id}/resourceGroups/lastfm-rg/providers/Microsoft.KeyVault/vaults/lastfm-kv# Grant Storage Blob Data Contributor role
az role assignment create \
--role "Storage Blob Data Contributor" \
--assignee-object-id $PRINCIPAL_ID \
--assignee-principal-type ServicePrincipal \
--scope /subscriptions/{sub-id}/resourceGroups/lastfm-rg/providers/Microsoft.Storage/storageAccounts/lastfmstore# Grant Log Analytics Reader role (for custom queries)
az role assignment create \
--role "Log Analytics Reader" \
--assignee-object-id $PRINCIPAL_ID \
--assignee-principal-type ServicePrincipal \
--scope /subscriptions/{sub-id}/resourceGroups/lastfm-rg/providers/Microsoft.OperationalInsights/workspaces/lastfm-logsOnly grant the minimum required permissions:
| Resource | Required Role | Justification |
|---|---|---|
| Key Vault | Key Vault Secrets User | Read API key (not write/delete) |
| Storage Account | Storage Blob Data Contributor | Write scrobble data (not account-level access) |
| Log Analytics | (none) | Container logs auto-forwarded by ACI |
Never grant:
OwnerorContributorat subscription/resource group levelKey Vault Administrator(allows deleting vault)Storage Account Contributor(allows deleting storage account)
# List all role assignments for managed identity
az role assignment list \
--assignee $PRINCIPAL_ID \
--all \
-o table
# Check specific role on Key Vault
az role assignment list \
--scope /subscriptions/{sub-id}/resourceGroups/lastfm-rg/providers/Microsoft.KeyVault/vaults/lastfm-kv \
--query "[?principalId=='$PRINCIPAL_ID'].{Role:roleDefinitionName,Scope:scope}" \
-o tableLastFM Reader uses Google's distroless/static:nonroot base image:
# Final stage: minimal runtime
FROM gcr.io/distroless/static:nonroot
# Benefits:
# - No shell (prevents reverse shell attacks)
# - No package manager (prevents installing malicious packages)
# - Minimal attack surface (~2MB vs ~100MB Alpine)
# - Non-root user (UID 65532)Security advantages:
- No Shell:
docker execwith/bin/shfails (prevents container escape) - No Tools: No
curl,wget,ncfor data exfiltration - Minimal CVEs: Fewer packages = fewer vulnerabilities
- Read-Only: Application can't modify container filesystem
Container runs as non-root user (UID 65532):
# Verify non-root
docker run --rm ghcr.io/yourusername/lastfm-reader:latest /lastfm-sync version
# No permission errors
# Try to write to root filesystem (should fail)
docker run --rm ghcr.io/yourusername/lastfm-reader:latest sh -c "touch /test"
# Error: cannot execute binary fileDeploy container with read-only filesystem:
# Docker
docker run --rm --read-only \
-v lastfm-data:/data \
ghcr.io/yourusername/lastfm-reader:latest
# Azure Container Instances
az container create \
--name lastfm-sync \
--resource-group lastfm-rg \
--image ghcr.io/yourusername/lastfm-reader:latest \
--azure-file-volume-account-name lastfmstore \
--azure-file-volume-account-key "..." \
--azure-file-volume-share-name scrobbles \
--azure-file-volume-mount-path /data \
--command-line "/lastfm-sync fetch -u username -o /data -f ndjson" \
--secure-environment-variables LASTFM_API_KEY=...Note: Application only writes to /data (mounted volume), not root filesystem.
Scan images for vulnerabilities:
# Trivy (recommended)
trivy image ghcr.io/yourusername/lastfm-reader:latest
# Expected output:
# Total: 0 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0)
# Grype (alternative)
grype ghcr.io/yourusername/lastfm-reader:latest
# Docker Scout (if using Docker Desktop)
docker scout cves ghcr.io/yourusername/lastfm-reader:latest# Multi-stage build: separate build and runtime
FROM golang:1.25-alpine AS builder
# Build with static linking
RUN CGO_ENABLED=0 go build -ldflags="-w -s" -o lastfm-sync
# Minimal runtime: no build tools
FROM gcr.io/distroless/static:nonroot
COPY --from=builder /app/lastfm-sync /lastfm-syncBenefits:
- Build tools not in runtime image
- Smaller image size (faster pulls, less storage)
- Static binary (no libc dependencies)
For production, use private endpoints to avoid public internet exposure:
# Create virtual network
az network vnet create \
--name lastfm-vnet \
--resource-group lastfm-rg \
--address-prefix 10.0.0.0/16 \
--subnet-name containers \
--subnet-prefix 10.0.1.0/24
# Create private endpoint for Key Vault
az network private-endpoint create \
--name lastfm-kv-pe \
--resource-group lastfm-rg \
--vnet-name lastfm-vnet \
--subnet containers \
--private-connection-resource-id $(az keyvault show --name lastfm-kv --query id -o tsv) \
--group-id vault \
--connection-name lastfm-kv-connection
# Create private endpoint for Storage Account
az network private-endpoint create \
--name lastfm-storage-pe \
--resource-group lastfm-rg \
--vnet-name lastfm-vnet \
--subnet containers \
--private-connection-resource-id $(az storage account show --name lastfmstore --query id -o tsv) \
--group-id blob \
--connection-name lastfm-storage-connection
# Deploy container in VNet
az container create \
--name lastfm-sync \
--resource-group lastfm-rg \
--image ghcr.io/yourusername/lastfm-reader:latest \
--vnet lastfm-vnet \
--subnet containers \
--assign-identity $IDENTITY_ID \
--secure-environment-variables LASTFM_API_KEY=@Microsoft.KeyVault(...)Restrict outbound traffic using Azure Firewall or Network Security Groups:
# Create NSG
az network nsg create \
--name lastfm-nsg \
--resource-group lastfm-rg
# Allow outbound to Last.fm API
az network nsg rule create \
--name allow-lastfm-api \
--nsg-name lastfm-nsg \
--resource-group lastfm-rg \
--priority 100 \
--direction Outbound \
--destination-address-prefixes "*.last.fm" \
--destination-port-ranges 443 \
--protocol Tcp \
--access Allow
# Allow outbound to Azure services
az network nsg rule create \
--name allow-azure-services \
--nsg-name lastfm-nsg \
--resource-group lastfm-rg \
--priority 110 \
--direction Outbound \
--destination-address-prefixes AzureCloud \
--destination-port-ranges 443 \
--protocol Tcp \
--access Allow
# Deny all other outbound traffic
az network nsg rule create \
--name deny-all-outbound \
--nsg-name lastfm-nsg \
--resource-group lastfm-rg \
--priority 1000 \
--direction Outbound \
--destination-address-prefixes "*" \
--destination-port-ranges "*" \
--protocol "*" \
--access Deny
# Associate NSG with subnet
az network vnet subnet update \
--name containers \
--vnet-name lastfm-vnet \
--resource-group lastfm-rg \
--network-security-group lastfm-nsgAll Azure services use TLS 1.2+ by default:
- Last.fm API: HTTPS only (
https://ws.audioscrobbler.com) - Azure Key Vault: TLS 1.2+ required
- Azure Storage: TLS 1.2+ enforced
Verify TLS version:
# Check Last.fm API
curl -v https://ws.audioscrobbler.com/2.0/ 2>&1 | grep "TLS"
# Expected: TLSv1.2 or TLSv1.3
# Check Azure Key Vault
az keyvault show --name lastfm-kv --query "properties.networkAcls.minTlsVersion"
# Expected: "1.2"Restrict storage account access:
# Disable public access
az storage account update \
--name lastfmstore \
--resource-group lastfm-rg \
--allow-blob-public-access false
# Enable firewall (deny all by default)
az storage account update \
--name lastfmstore \
--resource-group lastfm-rg \
--default-action Deny
# Allow specific VNet
az storage account network-rule add \
--account-name lastfmstore \
--resource-group lastfm-rg \
--vnet-name lastfm-vnet \
--subnet containers
# Allow trusted Azure services (for managed identity)
az storage account update \
--name lastfmstore \
--resource-group lastfm-rg \
--bypass AzureServicesIf using SAS tokens instead of managed identity:
# Generate SAS token with minimal permissions
az storage container generate-sas \
--account-name lastfmstore \
--name scrobbles \
--permissions rw \
--expiry 2024-12-31T23:59:59Z \
--https-only \
--ip 203.0.113.0/24 # Restrict to specific IP range
# Best practices:
# - Use HTTPS only (--https-only)
# - Set short expiry (hours/days, not years)
# - Minimal permissions (rw, not rwdl)
# - Restrict to specific IP ranges (--ip)
# - Use account SAS, not service SAS (more secure)Prefer managed identity over SAS tokens to avoid credential management.
Data encrypted at rest and in transit:
# Verify encryption at rest
az storage account show \
--name lastfmstore \
--resource-group lastfm-rg \
--query "encryption.services"
# Expected output:
# {
# "blob": { "enabled": true },
# "file": { "enabled": true }
# }
# Enable infrastructure encryption (double encryption)
az storage account create \
--name lastfmstore \
--resource-group lastfm-rg \
--require-infrastructure-encryption true
# Use customer-managed keys (optional)
az storage account update \
--name lastfmstore \
--resource-group lastfm-rg \
--encryption-key-source Microsoft.Keyvault \
--encryption-key-vault https://lastfm-kv.vault.azure.net \
--encryption-key-name storage-encryption-keyRotate API keys every 90 days:
# 1. Generate new API key on Last.fm website
# https://www.last.fm/api/account/create
# 2. Store new key in Key Vault with version
az keyvault secret set \
--vault-name lastfm-kv \
--name lastfm-api-key \
--value "new-api-key-here" \
--description "Rotated on $(date +%Y-%m-%d)" \
--tags rotation-date=$(date +%Y-%m-%d)
# 3. Test new key
docker run --rm \
-e LASTFM_API_KEY="new-api-key" \
ghcr.io/yourusername/lastfm-reader:latest \
/lastfm-sync fetch -u testuser -o /dev/null --limit 1
# 4. Update container instances
az container restart --name lastfm-sync --resource-group lastfm-rg
# 5. Revoke old API key on Last.fm website
# 6. Delete old Key Vault secret version (optional)If using storage account keys (not recommended, use managed identity):
# Rotate key1
az storage account keys renew \
--account-name lastfmstore \
--resource-group lastfm-rg \
--key key1
# Update Key Vault with new key
az keyvault secret set \
--vault-name lastfm-kv \
--name storage-account-key \
--value $(az storage account keys list --account-name lastfmstore --resource-group lastfm-rg --query "[0].value" -o tsv)
# Restart containers
az container restart --name lastfm-sync --resource-group lastfm-rg
# Wait 24 hours, then rotate key2
az storage account keys renew \
--account-name lastfmstore \
--resource-group lastfm-rg \
--key key2# Create Azure Function for automated rotation
# (requires Azure Functions deployment, see Azure Functions documentation)
# Example: Rotate secrets every 90 days
# - Azure Function triggered by Timer (0 0 1 * * * - 1st of month)
# - Function generates new API key (if API supports programmatic rotation)
# - Function updates Key Vault
# - Function restarts container instances
# - Function sends notification to admins# List Key Vault secret versions with dates
az keyvault secret list-versions \
--vault-name lastfm-kv \
--name lastfm-api-key \
--query "[].{Version:id, Created:attributes.created, Updated:attributes.updated}" \
-o table
# Alert on secrets older than 90 days
# (Configure Azure Monitor alert rule)
az monitor metrics alert create \
--name secret-rotation-alert \
--resource-group lastfm-rg \
--scopes $(az keyvault show --name lastfm-kv --query id -o tsv) \
--condition "count secrets > 0 where age > 90 days" \
--description "Alert when secrets are older than 90 days"# Create Log Analytics workspace
az monitor log-analytics workspace create \
--name lastfm-logs \
--resource-group lastfm-rg \
--location eastus
# Enable Key Vault audit logs
az monitor diagnostic-settings create \
--name kv-audit-logs \
--resource $(az keyvault show --name lastfm-kv --query id -o tsv) \
--workspace $(az monitor log-analytics workspace show --name lastfm-logs --resource-group lastfm-rg --query id -o tsv) \
--logs '[
{
"category": "AuditEvent",
"enabled": true,
"retentionPolicy": {
"enabled": true,
"days": 90
}
}
]'
# Enable Storage Account audit logs
az monitor diagnostic-settings create \
--name storage-audit-logs \
--resource $(az storage account show --name lastfmstore --query id -o tsv) \
--workspace $(az monitor log-analytics workspace show --name lastfm-logs --resource-group lastfm-rg --query id -o tsv) \
--logs '[
{
"category": "StorageRead",
"enabled": true
},
{
"category": "StorageWrite",
"enabled": true
}
]'// Key Vault secret access
AzureDiagnostics
| where ResourceProvider == "MICROSOFT.KEYVAULT"
| where OperationName == "SecretGet"
| project TimeGenerated, CallerIdentity=identity_claim_upn_s, SecretName=id_s, ResultDescription
| order by TimeGenerated desc
| take 100
// Failed Key Vault access attempts
AzureDiagnostics
| where ResourceProvider == "MICROSOFT.KEYVAULT"
| where OperationName == "SecretGet"
| where ResultSignature == "Forbidden"
| project TimeGenerated, CallerIdentity=identity_claim_upn_s, SecretName=id_s, ResultDescription
| order by TimeGenerated desc
// Storage Account access
StorageBlobLogs
| where OperationName in ("PutBlob", "GetBlob", "DeleteBlob")
| project TimeGenerated, AccountName, ContainerName, Uri, CallerIpAddress, AuthenticationType
| order by TimeGenerated desc
| take 100Generate compliance reports:
# List all RBAC assignments
az role assignment list \
--all \
--query "[?scope contains(@, 'lastfm-rg')].{Principal:principalName, Role:roleDefinitionName, Scope:scope}" \
-o table > rbac-report.txt
# List all Key Vault secrets
az keyvault secret list \
--vault-name lastfm-kv \
--query "[].{Name:name, Enabled:attributes.enabled, Created:attributes.created, Updated:attributes.updated}" \
-o table > secrets-report.txt
# List all storage account configurations
az storage account show \
--name lastfmstore \
--resource-group lastfm-rg \
--query "{Name:name, Encryption:encryption, NetworkRules:networkRuleSet, AllowBlobPublicAccess:allowBlobPublicAccess}" \
-o json > storage-report.jsonConfigure security alerts:
# Alert on Key Vault secret access by unknown identity
az monitor metrics alert create \
--name unknown-secret-access \
--resource-group lastfm-rg \
--scopes $(az keyvault show --name lastfm-kv --query id -o tsv) \
--condition "count operations where identity != 'known-identity'" \
--description "Alert when Key Vault is accessed by unknown identity" \
--action email admin@example.com
# Alert on storage account public access enabled
az monitor activity-log alert create \
--name storage-public-access-enabled \
--resource-group lastfm-rg \
--scope $(az storage account show --name lastfmstore --query id -o tsv) \
--condition category=Administrative and operationName=Microsoft.Storage/storageAccounts/write \
--action email admin@example.com- Secrets: All secrets stored in Azure Key Vault (not .env or code)
- .gitignore: Verify
.env,.env.*,*.localare ignored - RBAC: Managed identity has only required permissions (least privilege)
- Network: Private endpoints or VNet integration configured
- Storage: Public blob access disabled, firewall enabled
- Encryption: Storage account encryption at rest enabled
- Audit Logs: Diagnostic settings enabled for Key Vault and Storage
- Container Image: Scanned for vulnerabilities (Trivy/Grype)
- Base Image: Using distroless/static:nonroot (no shell, non-root)
- Read-Only FS: Container deployed with read-only root filesystem
- Secrets: Using Key Vault references (not plaintext env vars)
- Identity: Managed identity assigned to container instance
- Network: Container deployed in VNet with NSG rules
- Access Logs: Review Key Vault access logs for anomalies
- Rotation: Set calendar reminder for API key rotation (90 days)
- Monitoring: Alerts configured for failed authentication attempts
- Compliance: RBAC and storage reports generated and reviewed
- Testing: Verify container cannot write to root filesystem
- Testing: Verify container cannot access unauthorized resources
- Monthly: Review Key Vault access logs
- Quarterly: Rotate Last.fm API key
- Quarterly: Review and update RBAC assignments
- Semi-Annually: Run container image vulnerability scan
- Annually: Review and update network security policies
# Fully secure Azure Container Instances deployment
az container create \
--name lastfm-sync \
--resource-group lastfm-rg \
--image ghcr.io/yourusername/lastfm-reader:latest \
--assign-identity /subscriptions/{sub-id}/resourceGroups/lastfm-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/lastfm-identity \
--vnet lastfm-vnet \
--subnet containers \
--azure-file-volume-account-name lastfmstore \
--azure-file-volume-share-name scrobbles \
--azure-file-volume-mount-path /data \
--secure-environment-variables \
LASTFM_API_KEY=@Microsoft.KeyVault(SecretUri=https://lastfm-kv.vault.azure.net/secrets/lastfm-api-key/) \
--log-analytics-workspace /subscriptions/{sub-id}/resourceGroups/lastfm-rg/providers/Microsoft.OperationalInsights/workspaces/lastfm-logs \
--log-analytics-workspace-key "workspace-key" \
--restart-policy Never \
--cpu 1 \
--memory 0.5# Verify container security
docker run --rm ghcr.io/yourusername/lastfm-reader:latest /lastfm-sync version
trivy image ghcr.io/yourusername/lastfm-reader:latest
# Verify Azure RBAC
az role assignment list --assignee $PRINCIPAL_ID --all -o table
# Verify Key Vault access
az keyvault secret show --vault-name lastfm-kv --name lastfm-api-key
# Verify storage account security
az storage account show --name lastfmstore --query "{PublicAccess:allowBlobPublicAccess, Encryption:encryption.services.blob.enabled}"- Azure Key Vault Best Practices
- Azure Storage Security Guide
- Azure Container Security
- Azure RBAC Documentation
- OWASP Container Security
For deployment instructions, see docs/azure-deployment.md.
For configuration reference, see docs/configuration.md.