☁️ Cloud Security

Cloud Misconfiguration Reference

AWS, Azure, and GCP misconfiguration patterns — how attackers exploit them, how to detect them, and how to fix them. With copy-paste CLI commands.

Filter:
CRITICALS3 Bucket Publicly AccessibleS3StorageData ExposureT1530
An S3 bucket has public access enabled — either via bucket ACL, bucket policy, or the account-level Public Access Block not configured. Any data in the bucket is readable (and sometimes writeable) by anyone on the internet.
Attacker uses tools like S3Scanner, GrayhatWarfare, or Shodan S3 searches to enumerate buckets. Common bucket naming patterns (company-name-backup, company-logs, company-prod-data) are easily guessable. Data exfiltration is silent — no authentication, no logging required.
# Check if Public Access Block is enabled on account aws s3control get-public-access-block --account-id $(aws sts get-caller-identity --query Account --output text) # Check specific bucket public access settings aws s3api get-bucket-acl --bucket BUCKET_NAME aws s3api get-bucket-policy --bucket BUCKET_NAME aws s3api get-public-access-block --bucket BUCKET_NAME # List all buckets and their public access status (AWS CLI) for bucket in $(aws s3 ls | awk '{print $3}'); do echo -n "$bucket: " aws s3api get-public-access-block --bucket $bucket 2>/dev/null | \ python3 -c "import sys,json; d=json.load(sys.stdin)['PublicAccessBlockConfiguration']; print('PROTECTED' if all(d.values()) else 'EXPOSED')" 2>/dev/null || echo "No block config" done
# Enable account-level Public Access Block (blocks all public access) aws s3control put-public-access-block \ --account-id $(aws sts get-caller-identity --query Account --output text) \ --public-access-block-configuration BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true # Enable on individual bucket aws s3api put-public-access-block --bucket BUCKET_NAME \ --public-access-block-configuration BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true # Enable S3 server access logging aws s3api put-bucket-logging --bucket BUCKET_NAME \ --bucket-logging-status '{"LoggingEnabled":{"TargetBucket":"your-log-bucket","TargetPrefix":"s3-logs/"}}'
CRITICALIAM User with AdministratorAccess or *:* PolicyIAMPrivilegeLateral MovementT1078.004
An IAM user, role, or group is attached to a policy that grants wildcard (*) actions on wildcard (*) resources — effectively giving full AWS account control. Common in developer accounts that were never tightened.
Attacker who compromises credentials (via phishing, code repo leak, SSRF, or supply chain) immediately has full account takeover — create new IAM users, exfiltrate all data, spin up crypto mining, delete all resources. No further privilege escalation needed.
# Find all IAM entities with AdministratorAccess aws iam list-entities-for-policy \ --policy-arn arn:aws:iam::aws:policy/AdministratorAccess # Find inline policies with wildcard permissions aws iam get-account-authorization-details --filter User Role Group LocalManagedPolicy | \ python3 -c " import sys,json data=json.load(sys.stdin) for user in data.get('UserDetailList',[]): for pol in user.get('UserPolicyList',[]): doc=pol['PolicyDocument'] for s in doc['Statement']: if s.get('Effect')=='Allow' and s.get('Action')=='*': print(f'WILDCARD: {user[\"UserName\"]} - {pol[\"PolicyName\"]}') " # AWS Access Analyser — detects overly permissive policies aws accessanalyzer list-analyzers aws accessanalyzer list-findings --analyzer-arn ARN
# Apply least privilege — replace AdministratorAccess with specific permissions # Use AWS IAM Access Advisor to see what permissions are actually used aws iam generate-service-last-accessed-details --arn USER_ARN # (wait ~30s) aws iam get-service-last-accessed-details --job-id JOB_ID # Enable IAM Access Analyser aws accessanalyzer create-analyzer --analyzer-name account-analyser --type ACCOUNT # Enforce MFA for console access aws iam create-policy --policy-name EnforceMFA --policy-document file://enforce-mfa.json # enforce-mfa.json: Deny all actions unless MFA present (aws.amazon.com/premiumsupport/knowledge-center/mfa-iam-user-cli/)
CRITICALIMDSv1 Enabled — SSRF to Credential TheftEC2SSRFCredential AccessT1552.005
EC2 Instance Metadata Service version 1 (IMDSv1) allows any HTTP request from the instance (including from web applications running on it) to retrieve instance credentials by hitting http://169.254.169.254/. A Server-Side Request Forgery (SSRF) vulnerability in a web app can be exploited to steal these credentials.
Attacker finds an SSRF in a web application (common in Indian fintech apps). They redirect the server-side request to http://169.254.169.254/latest/meta-data/iam/security-credentials/. The instance role credentials are returned in plaintext. Attacker now has AWS credentials with whatever permissions the instance role has.
# Check if IMDSv2 is required (Token Required = IMDSv2 only) aws ec2 describe-instances --query \ "Reservations[*].Instances[*].[InstanceId,MetadataOptions.HttpTokens,MetadataOptions.HttpEndpoint]" \ --output table # Find instances still using IMDSv1 (optional) aws ec2 describe-instances \ --filters "Name=metadata-options.http-tokens,Values=optional" \ --query "Reservations[*].Instances[*].InstanceId" \ --output text # SSRF payload to demonstrate (test only on your own systems) # curl "http://YOUR_APP_SSRF_ENDPOINT?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/"
# Enforce IMDSv2 on existing instances aws ec2 modify-instance-metadata-options \ --instance-id INSTANCE_ID \ --http-tokens required \ --http-put-response-hop-limit 1 # Enforce IMDSv2 for ALL new instances (account default) aws ec2 modify-instance-metadata-defaults \ --http-tokens required # If IMDSv1 not needed at all, disable metadata endpoint aws ec2 modify-instance-metadata-options \ --instance-id INSTANCE_ID \ --http-endpoint disabled
HIGHSecurity Groups with 0.0.0.0/0 Inbound on SSH/RDPEC2NetworkInitial AccessT1190
An EC2 security group has inbound rules allowing SSH (port 22) or RDP (port 3389) from 0.0.0.0/0 — any IP address on the internet. Exposes instances to brute force, credential stuffing, and exploitation of SSH/RDP vulnerabilities.
Automated scanners find exposed SSH/RDP within minutes of deployment. Attackers attempt default credentials, leaked credential databases, or CVE-based exploits. Once in, full host access. Common finding in Indian startup environments where developers opened SSH for convenience during development.
# Find security groups with SSH/RDP open to world aws ec2 describe-security-groups \ --filters "Name=ip-permission.from-port,Values=22" \ "Name=ip-permission.cidr,Values=0.0.0.0/0" \ --query "SecurityGroups[*].[GroupId,GroupName]" --output table # Same for RDP (3389) aws ec2 describe-security-groups \ --filters "Name=ip-permission.from-port,Values=3389" \ "Name=ip-permission.cidr,Values=0.0.0.0/0" \ --query "SecurityGroups[*].[GroupId,GroupName]" --output table # Find which instances use these security groups aws ec2 describe-instances \ --filters "Name=instance.group-id,Values=sg-XXXXXXXX" \ --query "Reservations[*].Instances[*].[InstanceId,PublicIpAddress]" --output table
# Remove world-open SSH rule aws ec2 revoke-security-group-ingress \ --group-id sg-XXXXXXXX \ --protocol tcp --port 22 --cidr 0.0.0.0/0 # Add restricted rule (your office IP only) aws ec2 authorize-security-group-ingress \ --group-id sg-XXXXXXXX \ --protocol tcp --port 22 --cidr YOUR_OFFICE_IP/32 # Better: use AWS Systems Manager Session Manager (no SSH port needed) # Enable SSM agent on instance, remove all SSH security group rules entirely
HIGHCloudTrail Disabled or Missing in RegionsLoggingDetection GapCloudTrailT1562.008
AWS CloudTrail is not enabled in all regions, or has been disabled in one or more regions. CloudTrail logs all API calls — without it, attacker activity is undetectable and forensic investigation is impossible.
Attacker who gains initial access first checks for and disables CloudTrail (StopLogging API call) or deletes the S3 log bucket. Without logs, all subsequent actions — data exfiltration, IAM changes, new resource creation — leave no audit trail.
# Check CloudTrail status in all regions aws cloudtrail get-trail-status --name default # List all trails aws cloudtrail describe-trails --include-shadow-trails false # Check if multi-region trail exists aws cloudtrail describe-trails --query "trailList[?IsMultiRegionTrail==\`true\`].[Name,S3BucketName,IsLogging]" --output table # GuardDuty finding: check for StopLogging events aws cloudtrail lookup-events \ --lookup-attributes AttributeKey=EventName,AttributeValue=StopLogging \ --start-time 2024-01-01
# Enable multi-region CloudTrail aws cloudtrail create-trail \ --name org-trail \ --s3-bucket-name YOUR_LOG_BUCKET \ --is-multi-region-trail \ --enable-log-file-validation \ --include-global-service-events aws cloudtrail start-logging --name org-trail # Protect CloudTrail logs — S3 Object Lock aws s3api put-object-lock-configuration \ --bucket YOUR_LOG_BUCKET \ --object-lock-configuration '{"ObjectLockEnabled":"Enabled","Rule":{"DefaultRetention":{"Mode":"COMPLIANCE","Days":90}}}' # Alert on StopLogging via CloudWatch alarm # Create metric filter on CloudTrail → CloudWatch → SNS alert
HIGHRDS Publicly AccessibleRDSDatabaseData ExposureT1190
An RDS database instance has PubliclyAccessible=true, making it reachable from the internet. Combined with weak credentials or known vulnerabilities, this is a direct path to database compromise.
Automated scanners identify public RDS endpoints. Attackers attempt default credentials (admin/admin, root/root, postgres/postgres) or exploit database-specific vulnerabilities. MySQL, PostgreSQL, and MSSQL are all exposed. Indian companies frequently expose dev/staging databases this way.
# Find publicly accessible RDS instances aws rds describe-db-instances \ --query "DBInstances[?PubliclyAccessible==\`true\`].[DBInstanceIdentifier,Endpoint.Address,DBInstanceClass,Engine]" \ --output table # Check security groups attached to RDS aws rds describe-db-instances \ --query "DBInstances[*].[DBInstanceIdentifier,VpcSecurityGroups[*].VpcSecurityGroupId]"
# Disable public accessibility aws rds modify-db-instance \ --db-instance-identifier YOUR_DB_ID \ --no-publicly-accessible \ --apply-immediately # Move to private subnet — requires modifying subnet group # Access via bastion host or VPN only # Enable RDS encryption at rest aws rds modify-db-instance \ --db-instance-identifier YOUR_DB_ID \ --storage-encrypted # (requires snapshot + restore if not already encrypted)
CRITICALAzure Blob Container with Public AccessBlob StorageData ExposureT1530
An Azure Storage blob container has public access set to "Blob" or "Container" level, making files accessible without authentication. Azure now allows disabling this at account level but many legacy accounts still have it enabled.
Same as S3 public exposure — automated tools enumerate storage account names (company.blob.core.windows.net) and brute-force container names. Azure storage account names are globally unique and guessable from company names. Grey Hat Warfare specifically indexes Azure blobs.
# Check if public access is disabled at storage account level az storage account list --query "[].{Name:name, AllowBlobPublicAccess:allowBlobPublicAccess}" --output table # Check specific container public access az storage container show --name CONTAINER_NAME --account-name ACCOUNT_NAME --query "properties.publicAccess" # List all containers with public access az storage container list --account-name ACCOUNT_NAME \ --query "[?properties.publicAccess!='None'].[name,properties.publicAccess]" --output table
# Disable public blob access at account level az storage account update --name ACCOUNT_NAME --resource-group RG_NAME --allow-blob-public-access false # Enable Azure Defender for Storage az security pricing create --name StorageAccounts --tier Standard # Enable diagnostic logging to Log Analytics az monitor diagnostic-settings create --name blob-diag \ --resource /subscriptions/SUB/resourceGroups/RG/providers/Microsoft.Storage/storageAccounts/ACCOUNT \ --workspace LOG_ANALYTICS_WORKSPACE_ID \ --logs '[{"category":"StorageRead","enabled":true},{"category":"StorageWrite","enabled":true}]'
CRITICALAzure AD — No MFA for Global AdministratorsAzure ADIdentityPrivilegeT1078.004
Global Administrator accounts in Azure AD do not have Multi-Factor Authentication enforced. A single compromised password gives full tenant control — all subscriptions, all data, all resources.
Attacker uses password spray, phishing, or credential stuffing against Global Admin accounts. Without MFA, a valid password is sufficient for complete tenant takeover. Azure AD Global Admin can read all mail, access all SharePoint, and control all Azure subscriptions.
# Check MFA status for Global Admins # Via Azure Portal: Azure AD → Users → Per-user MFA # Via PowerShell (MSOnline module) Get-MsolUser -All | Where-Object {$_.StrongAuthenticationRequirements.Count -eq 0} | Select-Object DisplayName,UserPrincipalName # Check Conditional Access policies requiring MFA az ad policy list # Via Microsoft Graph (modern approach) # GET /reports/authenticationMethods/userRegistrationDetails # Filter: isAdmin=true AND isMfaRegistered=false
# Enable Security Defaults (enables MFA for all, free tier) az rest --method PATCH \ --url "https://graph.microsoft.com/v1.0/policies/identitySecurityDefaultsEnforcementPolicy" \ --body '{"isEnabled":true}' # Better: Create Conditional Access policy requiring MFA for admins # Azure Portal → Azure AD → Security → Conditional Access → New Policy # Assignments: Users → Directory roles → Global Administrator # Grant: Require MFA # Enforce Privileged Identity Management (PIM) for admin roles # Requires Azure AD P2 — admins must activate roles with MFA + justification
HIGHNSG Allowing Any Inbound on Management PortsNSGNetworkInitial AccessT1190
A Network Security Group (NSG) has an inbound rule with Source=Any and Destination Port=22, 3389, 5985 (WinRM), or 1433 (SQL). Management protocols are exposed to the internet.
Same as AWS security groups — automated scanners find exposed management ports. Azure VMs are frequently targeted for RDP brute force. Azure has seen mass credential stuffing campaigns specifically targeting Azure VMs.
# Find NSGs with overly permissive inbound rules az network nsg list --query \ "[*].{NSG:name,RG:resourceGroup,Rules:securityRules[?access=='Allow' && direction=='Inbound' && sourceAddressPrefix=='*' && contains('22 3389 5985 1433',destinationPortRange)]}" \ --output table # Or use Azure Policy compliance view az policy state list --resource-group RG_NAME
# Remove internet-facing RDP az network nsg rule update --nsg-name NSG_NAME --resource-group RG_NAME \ --name RDP-Rule --source-address-prefix YOUR_VPN_IP/32 # Better: Use Azure Bastion (no public ports needed) az network bastion create --name MyBastion --public-ip-address MyBastionIP \ --resource-group RG_NAME --vnet-name MY_VNET --location eastus # Enable Just-in-Time VM Access (Microsoft Defender for Cloud) az security jit-policy create --resource-group RG_NAME --location eastus \ --name default --virtual-machines ID
HIGHNo Diagnostic Logging on Key Azure ServicesLoggingAzure MonitorDetection GapT1562
Diagnostic settings not configured on Azure Key Vault, Azure AD, Azure Storage, and NSGs. Without logs sent to Log Analytics, security incidents cannot be detected or investigated.
Any attacker action on these resources leaves no audit trail. Key Vault secret access (credential theft), Azure AD sign-in anomalies, and storage account data access all become invisible.
# Check Key Vault diagnostic settings az monitor diagnostic-settings list \ --resource $(az keyvault show --name KV_NAME --query id --output tsv) # Check Azure AD audit logs are being sent somewhere az monitor diagnostic-settings list --resource /tenants/TENANT_ID/providers/microsoft.aadiam # List all resources with NO diagnostic settings az resource list --query "[*].id" --output tsv | while read id; do count=$(az monitor diagnostic-settings list --resource "$id" --query "length(@)" --output tsv 2>/dev/null) [ "$count" = "0" ] && echo "NO LOGGING: $id" done
# Enable Key Vault diagnostic logging az monitor diagnostic-settings create \ --name keyvault-diag \ --resource $(az keyvault show --name KV_NAME --query id --output tsv) \ --workspace LOG_ANALYTICS_ID \ --logs '[{"category":"AuditEvent","enabled":true,"retentionPolicy":{"enabled":true,"days":90}}]' # Use Azure Policy to enforce diagnostic settings org-wide # Built-in policy: "Deploy Diagnostic Settings for Key Vault to Log Analytics workspace" az policy assignment create \ --name "enforce-kv-diag" \ --policy "/providers/Microsoft.Authorization/policyDefinitions/KV_POLICY_DEF_ID" \ --scope /subscriptions/SUB_ID
MEDIUMAzure Subscription Without Privileged Identity ManagementPIMPrivilegeIdentityT1078
Azure Privileged Identity Management (PIM) is not configured. Privileged roles (Owner, Contributor, Global Admin) are permanently assigned rather than just-in-time activated. Compromised admin accounts have perpetual elevated access.
A compromised Owner account with permanent access can immediately destroy resources, exfiltrate data, and establish persistence. With PIM, a compromised account that has not activated its role is significantly less dangerous.
# Check PIM status az rest --method GET \ --url "https://management.azure.com/subscriptions/SUB_ID/providers/Microsoft.Authorization/roleEligibilityScheduleInstances?api-version=2020-10-01" # List permanent role assignments (should be minimal) az role assignment list --subscription SUB_ID \ --query "[?roleDefinitionName=='Owner' || roleDefinitionName=='Global Administrator'].[principalName,roleDefinitionName,scope]" --output table
# Requires Azure AD P2 # Enable PIM for a role assignment az rest --method PUT \ --url "https://management.azure.com/subscriptions/SUB_ID/providers/Microsoft.Authorization/roleEligibilitySchedules/SCHEDULE_ID?api-version=2020-10-01" \ --body @pim-schedule.json # Remove permanent Owner assignments, replace with eligible # Azure Portal → PIM → Azure Resources → Roles → Owner → Add assignments → Eligible
CRITICALGCS Bucket with allUsers or allAuthenticatedUsersGCSStorageData ExposureT1530
A Google Cloud Storage bucket has an IAM binding granting access to "allUsers" (anonymous internet users) or "allAuthenticatedUsers" (any Google account). Either setting exposes bucket data publicly.
GCS buckets are discoverable via Google dorking (site:storage.googleapis.com company-name), GrayhatWarfare, and bucket enumeration tools. allAuthenticatedUsers means ANY Google account holder — 3 billion people — can access the data.
# Check bucket IAM policies for public access gsutil iam get gs://BUCKET_NAME # Find all buckets with public access in project for bucket in $(gsutil ls); do echo -n "$bucket: " gsutil iam get $bucket | python3 -c " import sys,json d=json.load(sys.stdin) public=[b for b in d.get('bindings',[]) if any(m in ['allUsers','allAuthenticatedUsers'] for m in b.get('members',[]))] print('PUBLIC: '+str(public) if public else 'OK') " 2>/dev/null done # Using gcloud gcloud projects get-iam-policy PROJECT_ID \ --flatten="bindings[].members" \ --filter="bindings.members:(allUsers OR allAuthenticatedUsers)"
# Remove public access from bucket gsutil iam ch -d allUsers gs://BUCKET_NAME gsutil iam ch -d allAuthenticatedUsers gs://BUCKET_NAME # Enable uniform bucket-level access (disables per-object ACLs) gsutil uniformbucketlevelaccess set on gs://BUCKET_NAME # Enable public access prevention (org policy) gcloud resource-manager org-policies set-policy \ --organization=ORG_ID public-access-prevention.json # policy: storage.publicAccessPrevention = enforced
CRITICALService Account Key File Exposed / Long-Lived KeysIAMService AccountCredentialT1552.001
GCP service account key files (.json) have been downloaded and stored in source code, CI/CD systems, or public repositories. Service account keys do not expire by default — a leaked key may have been valid for years.
Service account key files found in GitHub repos, Docker images, or npm packages provide persistent, MFA-bypass access to GCP resources. Unlike user accounts, service account keys cannot be protected with MFA. GCP security incidents involving key leaks are extremely common in Indian startup environments.
# List all service account keys and their age gcloud iam service-accounts list --format="value(email)" | while read sa; do gcloud iam service-accounts keys list --iam-account="$sa" \ --format="table(name.basename(),validAfterTime,validBeforeTime,keyType)" echo "---" done # Find keys older than 90 days python3 -c " import subprocess, json from datetime import datetime, timezone, timedelta threshold = datetime.now(timezone.utc) - timedelta(days=90) # Run: gcloud iam service-accounts keys list --iam-account=SA --format=json # Check validAfterTime against threshold print('Keys older than 90 days should be rotated') "
# Delete unused service account keys gcloud iam service-accounts keys delete KEY_ID --iam-account=SA_EMAIL # Create new key (rotate) gcloud iam service-accounts keys create new-key.json --iam-account=SA_EMAIL # Better: use Workload Identity Federation instead of key files # For GKE: use Workload Identity gcloud container clusters update CLUSTER_NAME \ --workload-pool=PROJECT_ID.svc.id.goog # Set org policy to disable service account key creation gcloud resource-manager org-policies set-policy \ --organization=ORG_ID no-sa-keys-policy.json
HIGHCompute Instance with Default Service Account and Full API AccessComputeIAMPrivilegeT1078.004
A Compute Engine VM was created with the default service account and "Allow full access to all Cloud APIs" scope. The default service account often has broad project editor permissions. Any workload on the VM can access GCP APIs as an editor.
SSRF on a web app or code execution on the VM allows accessing the metadata server (metadata.google.internal) to get the service account token. With Editor permissions, attacker can read all data, create new VMs, and establish persistence.
# List VMs with their service accounts and scopes gcloud compute instances list \ --format="table(name,serviceAccounts[0].email,serviceAccounts[0].scopes)" # Find instances using default SA with full cloud-platform scope gcloud compute instances list \ --format=json | python3 -c " import sys,json instances=json.load(sys.stdin) for i in instances: for sa in i.get('serviceAccounts',[]): if 'default' in sa.get('email',''): scopes=sa.get('scopes',[]) if any('cloud-platform' in s for s in scopes): print(f'RISKY: {i[\"name\"]} - {sa[\"email\"]} - full scope') "
# Create dedicated service account with minimal permissions gcloud iam service-accounts create app-sa --display-name "App Service Account" gcloud projects add-iam-policy-binding PROJECT_ID \ --member=serviceAccount:app-sa@PROJECT_ID.iam.gserviceaccount.com \ --role=roles/logging.logWriter # Only the roles actually needed # Recreate VM with minimal scope gcloud compute instances create VM_NAME \ --service-account=app-sa@PROJECT_ID.iam.gserviceaccount.com \ --scopes=logging-write,monitoring-write # Minimal scopes # Disable default SA usage via org policy gcloud resource-manager org-policies set-policy \ --organization=ORG_ID no-default-sa-policy.json
HIGHCloud Audit Logs DisabledLoggingAuditDetection GapT1562
Cloud Audit Logs (Admin Activity, Data Access, System Events) are not enabled or are not being exported to Cloud Logging / SIEM. Without audit logs, attacker actions within GCP leave no trace.
Admin Activity logs are always enabled and cannot be disabled — but Data Access logs (who read/modified data) are disabled by default due to volume. Attackers who exfiltrate data from GCS or BigQuery leave no trace without Data Access logging.
# Check current audit log configuration gcloud projects get-iam-policy PROJECT_ID \ --flatten="auditConfigs[].service" \ --format="table(auditConfigs.service,auditConfigs.auditLogConfigs)" # Check if Data Access logs are enabled for all services gcloud projects get-iam-policy PROJECT_ID | grep -A5 "auditLogConfigs"
# Enable Data Access audit logs for all services gcloud projects set-iam-policy PROJECT_ID - << 'EOF' { "auditConfigs": [{ "service": "allServices", "auditLogConfigs": [ {"logType": "ADMIN_READ"}, {"logType": "DATA_READ"}, {"logType": "DATA_WRITE"} ] }] } EOF # Export logs to Cloud Storage for long-term retention gcloud logging sinks create audit-export-sink \ storage.googleapis.com/YOUR_AUDIT_BUCKET \ --log-filter='logName=~"cloudaudit.googleapis.com"'