Preventing Cloud Persistence
Policy In Code, Not In Hope
Cloud environments change rapidly. That's why security here must be default and automated to keep pace.
For Preventing Cloud Persistence, success depends on policy-as-code and controls that continuously run within the delivery chain.
This way you maintain speed in the cloud, without security depending on manual luck.
Immediate actions (15 minutes)
Why this matters
The core of Preventing Cloud Persistence is risk reduction in practice. Technical context supports the choice of measures, but implementation and embedding are central.
Defensive measures
CloudTrail Monitoring
# CloudTrail events that may indicate persistence
# IAM-related events
aws logs filter-log-events \
--log-group-name CloudTrail/management-events \
--filter-pattern '{
($.eventName = "CreateUser") ||
($.eventName = "CreateAccessKey") ||
($.eventName = "CreateRole") ||
($.eventName = "UpdateAssumeRolePolicy") ||
($.eventName = "AttachUserPolicy") ||
($.eventName = "AttachRolePolicy") ||
($.eventName = "PutUserPolicy") ||
($.eventName = "PutRolePolicy") ||
($.eventName = "CreateLoginProfile") ||
($.eventName = "CreateSAMLProvider")
}'
# Lambda and EventBridge events
aws logs filter-log-events \
--log-group-name CloudTrail/management-events \
--filter-pattern '{
($.eventName = "CreateFunction*") ||
($.eventName = "UpdateFunctionCode*") ||
($.eventName = "PutRule") ||
($.eventName = "PutTargets") ||
($.eventName = "AddPermission")
}'
# Compute persistence events
aws logs filter-log-events \
--log-group-name CloudTrail/management-events \
--filter-pattern '{
($.eventName = "ModifyInstanceAttribute") ||
($.eventName = "CreateLaunchTemplateVersion") ||
($.eventName = "PutBucketNotificationConfiguration")
}'Azure AD Audit Logs
# Monitor app registrations and consent grants
az rest --method GET \
--url "https://graph.microsoft.com/v1.0/auditLogs/directoryAudits?\$filter=activityDisplayName eq 'Add application' or activityDisplayName eq 'Consent to application' or activityDisplayName eq 'Add service principal credentials'&\$top=50" \
| jq '.value[] | {activity: .activityDisplayName, time: .activityDateTime, actor: .initiatedBy.user.userPrincipalName}'
# Monitor role assignments
az rest --method GET \
--url "https://graph.microsoft.com/v1.0/auditLogs/directoryAudits?\$filter=activityDisplayName eq 'Add member to role'&\$top=50" \
| jq '.value[] | {time: .activityDateTime, actor: .initiatedBy.user.userPrincipalName, target: .targetResources[0].displayName}'Access Key Rotation
# Force access key rotation with a maximum age
# Script: find and report old access keys
aws iam generate-credential-report
aws iam get-credential-report --query Content --output text | base64 -d | \
awk -F',' 'NR>1 {
if ($9 == "true" && $10 != "N/A") {
split($10, a, "T");
print "User: "$1, "Key 1 last rotated:", a[1]
}
if ($14 == "true" && $15 != "N/A") {
split($15, a, "T");
print "User: "$1, "Key 2 last rotated:", a[1]
}
}'
# Deactivate keys older than 90 days
for user in $(aws iam list-users --query 'Users[].UserName' --output text); do
for key in $(aws iam list-access-keys --user-name "$user" --query 'AccessKeyMetadata[?Status==`Active`].AccessKeyId' --output text); do
created=$(aws iam list-access-keys --user-name "$user" --query "AccessKeyMetadata[?AccessKeyId=='$key'].CreateDate" --output text)
age=$(( ($(date +%s) - $(date -d "$created" +%s)) / 86400 ))
if [ "$age" -gt 90 ]; then
echo "ALERT: $user has key $key that is $age days old"
# aws iam update-access-key --user-name "$user" --access-key-id "$key" --status Inactive
fi
done
doneConditional Access
# Azure: Conditional Access policy that restricts service principal access
az rest --method POST \
--url "https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies" \
--body '{
"displayName": "Restrict service principal locations",
"state": "enabled",
"conditions": {
"clientApplications": {
"includeServicePrincipals": ["All"]
},
"locations": {
"includeLocations": ["All"],
"excludeLocations": ["KNOWN_IP_RANGES"]
}
},
"grantControls": {
"operator": "OR",
"builtInControls": ["block"]
}
}'Reference table
| Technique | MITRE ATT&CK | AWS | Azure | GCP |
|---|---|---|---|---|
| Extra access keys | T1098.001 - Additional Cloud Credentials | iam:CreateAccessKey |
App credential addition | Service account key creation |
| Backdoor IAM user/role | T1136.003 - Cloud Account | iam:CreateUser, iam:CreateRole |
az ad user create |
gcloud iam service-accounts create |
| Trust policy manipulation | T1484.002 - Trust Modification | iam:UpdateAssumeRolePolicy |
External identity providers | Workload Identity pool trust |
| OAuth app registration | T1098.003 - Additional Cloud Roles | SAML/OIDC provider | App Registration + consent | OAuth2 client credentials |
| Compute startup scripts | T1059 - Command and Scripting | EC2 User Data | VM Extensions | GCE startup-script |
| Launch template poisoning | T1525 - Implant Internal Image | ec2:CreateLaunchTemplateVersion |
VMSS model update | Instance template modification |
| Scheduled Lambda trigger | T1053.007 - Container Orchestration Job | EventBridge + Lambda | Automation Runbooks | Cloud Scheduler + Functions |
| Storage event trigger | T1546 - Event Triggered Execution | S3 notifications + Lambda | Blob trigger + Function | GCS notification + Function |
| DNS record manipulation | T1584.002 - DNS Server | Route53 record sets | Azure DNS records | Cloud DNS records |
| Subdomain takeover | T1584.001 - Domains | Dangling S3/EB CNAMEs | Dangling Azure CNAMEs | Dangling GCP CNAMEs |
| Golden SAML | T1606.002 - SAML Tokens | SAML IdP certificate theft | ADFS signing cert theft | SAML IdP compromise |
| Refresh token persistence | T1550.001 - Application Access Token | Cognito refresh tokens | Azure AD refresh tokens | Google OAuth refresh tokens |
| Lifecycle policy abuse | T1053.007 - Container Orchestration Job | S3 Lifecycle + notifications | Blob lifecycle + triggers | GCS lifecycle + notifications |
| Cross-account role backdoor | T1098.003 - Additional Cloud Roles | Cross-account trust policy | Lighthouse delegation | Cross-project SA impersonation |
| Automation runbook | T1053.005 - Scheduled Task | Lambda + EventBridge | Automation Account + Runbook | Cloud Scheduler job |
The best persistence is persistence that looks like a feature. And in the cloud, the difference between a feature and a backdoor is often nothing more than the intent of the person who configured it.
Further reading in the knowledge base
These articles in the portal provide more background and practical context:
- The cloud — someone else's computer, your responsibility
- Containers and Docker — what it is and why you need to secure it
- Encryption — the art of making things unreadable
- Least Privilege — give people only what they need
You need an account to access the knowledge base. Log in or register.
Related security measures
These articles provide additional context and depth: