jan-karel.com
Home / Security Measures / Cloud Security / Preventing Cloud Persistence

Preventing Cloud Persistence

Preventing Cloud Persistence

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
done

Conditional 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.

Op de hoogte blijven?

Ontvang maandelijks cybersecurity-inzichten in je inbox.

← Cloud Security ← Home