Comprehensive security guide for deploying and maintaining CommunityRAPP.
Security is built into every layer of CommunityRAPP. This guide covers:
Best Practices:
Rotate Function Keys:
# Regenerate function key
az functionapp keys renew \
--name YOUR_FUNCTION_APP \
--resource-group YOUR_RESOURCE_GROUP \
--key-type functionKeys \
--key-name default
# Get new key
NEW_KEY=$(az functionapp keys list \
--name YOUR_FUNCTION_APP \
--resource-group YOUR_RESOURCE_GROUP \
--query functionKeys.default --output tsv)
echo "New function key: $NEW_KEY"
Store Keys Securely:
# Option 1: Azure Key Vault
az keyvault secret set \
--vault-name YOUR_KEYVAULT \
--name FunctionKey-Prod \
--value "$NEW_KEY"
# Option 2: GitHub Secrets (for CI/CD)
# GitHub → Repository → Settings → Secrets → New repository secret
Enable Azure AD for Function App:
# Configure authentication
az webapp auth update \
--name YOUR_FUNCTION_APP \
--resource-group YOUR_RESOURCE_GROUP \
--enabled true \
--action LoginWithAzureActiveDirectory \
--aad-client-id YOUR_AAD_APP_ID \
--aad-client-secret YOUR_AAD_SECRET \
--aad-allowed-token-audiences https://YOUR_FUNCTION_APP.azurewebsites.net
Require authentication for all requests:
az webapp auth update \
--name YOUR_FUNCTION_APP \
--resource-group YOUR_RESOURCE_GROUP \
--unauthenticated-client-action RedirectToLoginPage
Enable System-Assigned Managed Identity:
# Enable managed identity
az functionapp identity assign \
--name YOUR_FUNCTION_APP \
--resource-group YOUR_RESOURCE_GROUP
# Get principal ID
PRINCIPAL_ID=$(az functionapp identity show \
--name YOUR_FUNCTION_APP \
--resource-group YOUR_RESOURCE_GROUP \
--query principalId --output tsv)
echo "Principal ID: $PRINCIPAL_ID"
Grant Permissions:
# Storage Blob Data Contributor
az role assignment create \
--assignee $PRINCIPAL_ID \
--role "Storage Blob Data Contributor" \
--scope /subscriptions/YOUR_SUB/resourceGroups/YOUR_RG/providers/Microsoft.Storage/storageAccounts/YOUR_STORAGE
# Cognitive Services OpenAI User
az role assignment create \
--assignee $PRINCIPAL_ID \
--role "Cognitive Services OpenAI User" \
--scope /subscriptions/YOUR_SUB/resourceGroups/YOUR_RG/providers/Microsoft.CognitiveServices/accounts/YOUR_OPENAI
Update Code to Use Managed Identity:
from azure.identity import DefaultAzureCredential
from azure.storage.filedatalake import DataLakeServiceClient
# Use managed identity instead of connection string
credential = DefaultAzureCredential()
service_client = DataLakeServiceClient(
account_url=f"https://{STORAGE_ACCOUNT}.dfs.core.windows.net",
credential=credential
)
Define Custom Roles:
Create custom-role.json:
{
"Name": "Copilot365 Developer",
"Description": "Can manage Copilot365 function app and storage",
"Actions": [
"Microsoft.Web/sites/functions/read",
"Microsoft.Web/sites/functions/write",
"Microsoft.Storage/storageAccounts/fileServices/read",
"Microsoft.Storage/storageAccounts/fileServices/write"
],
"NotActions": [
"Microsoft.Web/sites/delete",
"Microsoft.Storage/storageAccounts/delete"
],
"AssignableScopes": [
"/subscriptions/YOUR_SUBSCRIPTION_ID/resourceGroups/YOUR_RESOURCE_GROUP"
]
}
# Create custom role
az role definition create --role-definition custom-role.json
# Assign to user
az role assignment create \
--assignee user@company.com \
--role "Copilot365 Developer" \
--scope /subscriptions/YOUR_SUB/resourceGroups/YOUR_RG
Azure Storage Encryption:
Enable Customer-Managed Keys:
# Create Key Vault
az keyvault create \
--name contoso-rapp-keyvault \
--resource-group YOUR_RESOURCE_GROUP \
--location eastus
# Create encryption key
az keyvault key create \
--vault-name contoso-rapp-keyvault \
--name storage-encryption-key \
--protection software
# Update storage account
az storage account update \
--name YOUR_STORAGE \
--resource-group YOUR_RESOURCE_GROUP \
--encryption-key-source Microsoft.Keyvault \
--encryption-key-vault https://contoso-rapp-keyvault.vault.azure.net \
--encryption-key-name storage-encryption-key
Enforce HTTPS:
# Function App - enforce HTTPS
az functionapp update \
--name YOUR_FUNCTION_APP \
--resource-group YOUR_RESOURCE_GROUP \
--set httpsOnly=true
# Storage Account - enforce HTTPS
az storage account update \
--name YOUR_STORAGE \
--resource-group YOUR_RESOURCE_GROUP \
--https-only true
# Require TLS 1.2 minimum
az storage account update \
--name YOUR_STORAGE \
--resource-group YOUR_RESOURCE_GROUP \
--min-tls-version TLS1_2
Input Validation:
import re
from html import escape
def sanitize_user_input(user_input: str) -> str:
"""
Sanitize user input to prevent injection attacks.
Args:
user_input: Raw user input
Returns:
Sanitized input
"""
# Remove potential script tags
user_input = re.sub(r'<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>', '', user_input, flags=re.IGNORECASE)
# Escape HTML
user_input = escape(user_input)
# Limit length
max_length = 10000
if len(user_input) > max_length:
user_input = user_input[:max_length]
return user_input
Output Sanitization:
def sanitize_agent_output(output: str) -> str:
"""
Sanitize agent output before returning to user.
Args:
output: Raw agent output
Returns:
Sanitized output
"""
# Remove any API keys or secrets (simple pattern matching)
patterns = [
r'api[_-]?key["\']?\s*[:=]\s*["\']?[\w-]+',
r'password["\']?\s*[:=]\s*["\']?[\w-]+',
r'secret["\']?\s*[:=]\s*["\']?[\w-]+',
]
for pattern in patterns:
output = re.sub(pattern, '[REDACTED]', output, flags=re.IGNORECASE)
return output
Detect and Mask PII:
import re
def mask_pii(text: str) -> str:
"""
Mask PII in text before logging or storing.
Args:
text: Input text potentially containing PII
Returns:
Text with PII masked
"""
# Email addresses
text = re.sub(r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b', '[EMAIL]', text)
# Phone numbers (US format)
text = re.sub(r'\b\d{3}[-.]?\d{3}[-.]?\d{4}\b', '[PHONE]', text)
# Social Security Numbers
text = re.sub(r'\b\d{3}-\d{2}-\d{4}\b', '[SSN]', text)
# Credit card numbers (simple pattern)
text = re.sub(r'\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b', '[CREDIT_CARD]', text)
return text
# Use in logging
import logging
class PIIMaskingFilter(logging.Filter):
def filter(self, record):
record.msg = mask_pii(str(record.msg))
return True
# Add filter to logger
logger = logging.getLogger()
logger.addFilter(PIIMaskingFilter())
Restrict Function App Access:
# Allow only specific IP ranges
az functionapp config access-restriction add \
--name YOUR_FUNCTION_APP \
--resource-group YOUR_RESOURCE_GROUP \
--rule-name AllowCorporateNetwork \
--priority 100 \
--ip-address 203.0.113.0/24
# Allow Power Platform (regional IPs)
az functionapp config access-restriction add \
--name YOUR_FUNCTION_APP \
--resource-group YOUR_RESOURCE_GROUP \
--rule-name AllowPowerPlatform \
--priority 200 \
--service-tag AzureCloud.eastus
# Deny all others (implicit)
Get current restrictions:
az functionapp config access-restriction show \
--name YOUR_FUNCTION_APP \
--resource-group YOUR_RESOURCE_GROUP
Enable VNet Integration:
# Create VNet
az network vnet create \
--name contoso-rapp-vnet \
--resource-group YOUR_RESOURCE_GROUP \
--location eastus \
--address-prefix 10.0.0.0/16
# Create subnet for function app
az network vnet subnet create \
--name function-subnet \
--vnet-name contoso-rapp-vnet \
--resource-group YOUR_RESOURCE_GROUP \
--address-prefixes 10.0.1.0/24 \
--delegations Microsoft.Web/serverFarms
# Enable VNet integration
az functionapp vnet-integration add \
--name YOUR_FUNCTION_APP \
--resource-group YOUR_RESOURCE_GROUP \
--vnet contoso-rapp-vnet \
--subnet function-subnet
Create Private Endpoint for Storage:
# Create subnet for private endpoint
az network vnet subnet create \
--name private-endpoint-subnet \
--vnet-name contoso-rapp-vnet \
--resource-group YOUR_RESOURCE_GROUP \
--address-prefixes 10.0.2.0/24
# Create private endpoint
az network private-endpoint create \
--name storage-private-endpoint \
--resource-group YOUR_RESOURCE_GROUP \
--vnet-name contoso-rapp-vnet \
--subnet private-endpoint-subnet \
--private-connection-resource-id /subscriptions/YOUR_SUB/resourceGroups/YOUR_RG/providers/Microsoft.Storage/storageAccounts/YOUR_STORAGE \
--group-id file \
--connection-name storage-connection
# Disable public network access
az storage account update \
--name YOUR_STORAGE \
--resource-group YOUR_RESOURCE_GROUP \
--public-network-access Disabled
Deploy Azure Front Door with WAF:
# Create Front Door profile
az afd profile create \
--profile-name contoso-rapp-frontdoor \
--resource-group YOUR_RESOURCE_GROUP \
--sku Premium_AzureFrontDoor
# Create WAF policy
az network front-door waf-policy create \
--name contoso-rappwaf \
--resource-group YOUR_RESOURCE_GROUP \
--sku Premium_AzureFrontDoor \
--mode Prevention
# Enable managed rules
az network front-door waf-policy managed-rules add \
--policy-name contoso-rappwaf \
--resource-group YOUR_RESOURCE_GROUP \
--type Microsoft_DefaultRuleSet \
--version 2.1
Create Key Vault:
az keyvault create \
--name contoso-rapp-keyvault \
--resource-group YOUR_RESOURCE_GROUP \
--location eastus \
--enable-rbac-authorization false
Store Secrets:
# OpenAI API Key
az keyvault secret set \
--vault-name contoso-rapp-keyvault \
--name OpenAI-API-Key \
--value "YOUR_OPENAI_API_KEY"
# Storage connection string
az keyvault secret set \
--vault-name contoso-rapp-keyvault \
--name Storage-Connection-String \
--value "YOUR_STORAGE_CONNECTION_STRING"
# Function key
az keyvault secret set \
--vault-name contoso-rapp-keyvault \
--name Function-Key-Prod \
--value "YOUR_FUNCTION_KEY"
Grant Function App Access:
# Get function app identity
PRINCIPAL_ID=$(az functionapp identity show \
--name YOUR_FUNCTION_APP \
--resource-group YOUR_RESOURCE_GROUP \
--query principalId --output tsv)
# Grant secret read permissions
az keyvault set-policy \
--name contoso-rapp-keyvault \
--object-id $PRINCIPAL_ID \
--secret-permissions get list
Reference in Function App:
az functionapp config appsettings set \
--name YOUR_FUNCTION_APP \
--resource-group YOUR_RESOURCE_GROUP \
--settings \
"AZURE_OPENAI_API_KEY=@Microsoft.KeyVault(SecretUri=https://contoso-rapp-keyvault.vault.azure.net/secrets/OpenAI-API-Key/)" \
"AzureWebJobsStorage=@Microsoft.KeyVault(SecretUri=https://contoso-rapp-keyvault.vault.azure.net/secrets/Storage-Connection-String/)"
Automate Key Rotation:
Create an Azure Function to rotate keys automatically:
import azure.functions as func
from azure.keyvault.secrets import SecretClient
from azure.identity import DefaultAzureCredential
def rotate_function_key(timer: func.TimerRequest):
"""
Rotate function keys every 90 days.
Schedule: 0 0 0 1 */3 * (first day of every 3 months)
"""
# Generate new function key
new_key = generate_secure_key()
# Update in Key Vault
credential = DefaultAzureCredential()
client = SecretClient(vault_url="https://contoso-rapp-keyvault.vault.azure.net", credential=credential)
client.set_secret("Function-Key-Prod", new_key)
# Notify administrators
send_notification(f"Function key rotated successfully at {datetime.now()}")
Select Appropriate Region:
# Deploy in specific region for compliance
az group create \
--name rg-contoso-rapp-eu \
--location westeurope
# All resources will be in EU region
az functionapp create \
--name contoso-rapp-function-eu \
--resource-group rg-contoso-rapp-eu \
--consumption-plan-location westeurope \
...
Configure Storage Lifecycle:
Create lifecycle-policy.json:
{
"rules": [
{
"enabled": true,
"name": "DeleteOldLogs",
"type": "Lifecycle",
"definition": {
"actions": {
"baseBlob": {
"delete": {
"daysAfterModificationGreaterThan": 90
}
}
},
"filters": {
"blobTypes": ["blockBlob"],
"prefixMatch": ["logs/"]
}
}
},
{
"enabled": true,
"name": "ArchiveOldMemory",
"type": "Lifecycle",
"definition": {
"actions": {
"baseBlob": {
"tierToCool": {
"daysAfterModificationGreaterThan": 30
},
"tierToArchive": {
"daysAfterModificationGreaterThan": 180
}
}
},
"filters": {
"blobTypes": ["blockBlob"],
"prefixMatch": ["memory/"]
}
}
}
]
}
az storage account management-policy create \
--account-name YOUR_STORAGE \
--resource-group YOUR_RESOURCE_GROUP \
--policy @lifecycle-policy.json
Enable Diagnostic Settings:
# Create Log Analytics workspace
az monitor log-analytics workspace create \
--workspace-name contoso-rapp-logs \
--resource-group YOUR_RESOURCE_GROUP \
--location eastus
# Get workspace ID
WORKSPACE_ID=$(az monitor log-analytics workspace show \
--workspace-name contoso-rapp-logs \
--resource-group YOUR_RESOURCE_GROUP \
--query id --output tsv)
# Enable diagnostic settings for Function App
az monitor diagnostic-settings create \
--name FunctionAppLogs \
--resource /subscriptions/YOUR_SUB/resourceGroups/YOUR_RG/providers/Microsoft.Web/sites/YOUR_FUNCTION_APP \
--workspace $WORKSPACE_ID \
--logs '[{"category":"FunctionAppLogs","enabled":true}]' \
--metrics '[{"category":"AllMetrics","enabled":true}]'
# Enable diagnostic settings for Storage
az monitor diagnostic-settings create \
--name StorageLogs \
--resource /subscriptions/YOUR_SUB/resourceGroups/YOUR_RG/providers/Microsoft.Storage/storageAccounts/YOUR_STORAGE \
--workspace $WORKSPACE_ID \
--logs '[{"category":"StorageRead","enabled":true},{"category":"StorageWrite","enabled":true}]'
Query for suspicious activity:
// Failed authentication attempts
AzureDiagnostics
| where Category == "FunctionAppLogs"
| where resultCode >= 400
| summarize FailedAttempts=count() by clientIP, bin(TimeGenerated, 5m)
| where FailedAttempts > 10
| order by FailedAttempts desc
// Unusual data access patterns
StorageFileLogs
| where OperationName == "GetFile"
| summarize FilesAccessed=dcount(Uri) by CallerIpAddress, bin(TimeGenerated, 1h)
| where FilesAccessed > 100
| order by FilesAccessed desc
// OpenAI API errors
traces
| where message contains "OpenAI"
| where severityLevel >= 3
| summarize ErrorCount=count() by message, bin(timestamp, 5m)
| order by ErrorCount desc
Create security alerts:
# Alert on failed authentication
az monitor metrics alert create \
--name "High Failed Authentication Rate" \
--resource-group YOUR_RESOURCE_GROUP \
--scopes /subscriptions/YOUR_SUB/resourceGroups/YOUR_RG/providers/Microsoft.Web/sites/YOUR_FUNCTION_APP \
--condition "total Http4xx > 50" \
--window-size 5m \
--evaluation-frequency 1m \
--action email YOUR_SECURITY_TEAM@company.com
# Alert on unusual activity
az monitor metrics alert create \
--name "Unusual Request Volume" \
--resource-group YOUR_RESOURCE_GROUP \
--scopes /subscriptions/YOUR_SUB/resourceGroups/YOUR_RG/providers/Microsoft.Web/sites/YOUR_FUNCTION_APP \
--condition "total Requests > 1000" \
--window-size 5m
1. Detection:
2. Containment:
# Immediately disable function app if compromised
az functionapp stop \
--name YOUR_FUNCTION_APP \
--resource-group YOUR_RESOURCE_GROUP
# Revoke all function keys
az functionapp keys delete \
--name YOUR_FUNCTION_APP \
--resource-group YOUR_RESOURCE_GROUP \
--key-type functionKeys \
--key-name default
# Disable storage account public access
az storage account update \
--name YOUR_STORAGE \
--resource-group YOUR_RESOURCE_GROUP \
--public-network-access Disabled
3. Investigation:
# Export audit logs
az monitor activity-log list \
--resource-group YOUR_RESOURCE_GROUP \
--start-time 2025-01-01T00:00:00Z \
--end-time 2025-01-31T23:59:59Z \
--output json > incident-logs.json
# Review access logs
az storage account show-connection-string \
--name YOUR_STORAGE \
--resource-group YOUR_RESOURCE_GROUP
4. Recovery:
# Restore from backup
az storage file download-batch \
--source agents \
--destination ./restore \
--account-name YOUR_STORAGE_BACKUP
# Redeploy with new keys
func azure functionapp publish YOUR_FUNCTION_APP
# Update all keys
./scripts/rotate-all-keys.sh
5. Post-Incident:
Weekly:
Monthly:
Quarterly:
Annually:
Security concerns? Report a security issue