Prerequisites
This guide assumes a basic knowledge of the three major clouds (IAM, VPC, CLI) and of Terraform. The code examples are production-ready but must be adapted to your context.
Introduction: the shared responsibility model
Cloud security rests on a fundamental principle that the majority of incidents validate in the worst possible way: the shared responsibility model. The cloud provider secures the physical infrastructure, the hypervisor, the global network and the managed services. You secure everything else — IAM configurations, data, virtual network, applications, secrets.
In practice, this means that AWS, GCP and Azure cannot protect you against a public S3 bucket, root keys running in CI/CD, or a Security Group that opens port 22 to 0.0.0.0/0. You make these mistakes yourself, and the cloud provider does not detect them by default.
The 5 most common cloud mistakes
These five patterns are responsible for the vast majority of documented cloud incidents.
1. Public S3 (or GCS / Azure Blob) buckets — The default configuration has evolved: AWS now blocks public access by default since 2023. But a single misconfigured property in Terraform, a bucket created manually before the global policy, or a developer who checks "public" without thinking is enough to expose terabytes of data. Millions of credentials, application logs and personal data have been exfiltrated this way.
2. Using AWS root keys — The AWS root account has unlimited and irreversible rights. Creating root access keys to "go faster" is a serious mistake: these keys cannot be restricted by IAM policies, and their compromise means total compromise of the account. Disable root keys, enable hardware MFA and never use them for day-to-day operations.
3. IAM policies with a wildcard (*) — "Action": "*", "Resource": "*" gives any resource all rights over the entire account. It is the cloud equivalent of chmod 777 /. Yet this configuration regularly shows up in automation roles "to go fast". The blast radius of a compromise is then maximal.
4. Security Groups open to 0.0.0.0/0 — Opening the SSH (22), RDP (3389) or database (3306, 5432) ports to the entire Internet exposes your instances to automated scanners that constantly attempt authentications. Every day, thousands of bot machines scan the entire AWS IP ranges looking for these open ports.
5. Secrets in environment variables or source code — Hardcoded API keys in code, database credentials in the environment variables of an ECS task definition, tokens in Dockerfiles. These secrets end up in logs, Docker images, Git repositories and snapshots. Use the managed secrets services described in this guide.
1. IAM Security: least privilege in practice
The principle of least privilege is simple to state and hard to maintain at scale. A correct IAM policy grants exactly the rights a resource needs to do its job, no more, no less.
AWS IAM: roles vs users
The general rule: humans use SSO (AWS IAM Identity Center), workloads use IAM roles, and traditional IAM users with long-lived access keys should be avoided except in specific documented cases.
Example of a least-privilege IAM policy for a Lambda that reads from S3 and writes to DynamoDB:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ReadSpecificS3Bucket",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:GetObjectVersion"
],
"Resource": "arn:aws:s3:::mon-bucket-prod/data/*"
},
{
"Sid": "WriteSpecificDynamoTable",
"Effect": "Allow",
"Action": [
"dynamodb:PutItem",
"dynamodb:UpdateItem",
"dynamodb:GetItem"
],
"Resource": "arn:aws:dynamodb:eu-west-1:123456789012:table/MaTable"
},
{
"Sid": "DecryptWithSpecificKey",
"Effect": "Allow",
"Action": [
"kms:Decrypt",
"kms:GenerateDataKey"
],
"Resource": "arn:aws:kms:eu-west-1:123456789012:key/mrk-abc123"
}
]
}
Create this role and attach it to a Lambda via the AWS CLI:
# 1. Create the role with a trust policy for Lambda
aws iam create-role
--role-name lambda-data-processor
--assume-role-policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"Service": "lambda.amazonaws.com"},
"Action": "sts:AssumeRole"
}]
}'
# 2. Create the inline policy (or managed)
aws iam put-role-policy
--role-name lambda-data-processor
--policy-name least-privilege-policy
--policy-document file://policy.json
# 3. Attach the AWSLambdaBasicExecutionRole policy for CloudWatch logs
aws iam attach-role-policy
--role-name lambda-data-processor
--policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
# 4. Verify the effective permissions
aws iam simulate-principal-policy
--policy-source-arn arn:aws:iam::123456789012:role/lambda-data-processor
--action-names s3:GetObject s3:DeleteObject
--resource-arns arn:aws:s3:::mon-bucket-prod/data/fichier.csv
AWS Organizations and SCPs
Service Control Policies (SCPs) are organizational guardrails: they define the maximum permissions an account can have, regardless of internal IAM policies. Even an administrator of a member account cannot exceed what the SCP allows.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyRootAccountActions",
"Effect": "Deny",
"Principal": "*",
"Action": "*",
"Resource": "*",
"Condition": {
"StringLike": {
"aws:PrincipalArn": "arn:aws:iam::*:root"
}
}
},
{
"Sid": "DenyLeavingOrganization",
"Effect": "Deny",
"Action": "organizations:LeaveOrganization",
"Resource": "*"
},
{
"Sid": "DenyDisablingCloudTrail",
"Effect": "Deny",
"Action": [
"cloudtrail:StopLogging",
"cloudtrail:DeleteTrail",
"cloudtrail:UpdateTrail"
],
"Resource": "*"
},
{
"Sid": "RestrictRegions",
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {
"StringNotEquals": {
"aws:RequestedRegion": [
"eu-west-1",
"eu-west-3",
"us-east-1"
]
}
}
}
]
}
AWS IAM Access Analyzer
Access Analyzer automatically detects resources accessible from outside the account or the organization. Enable it in every region and the global region:
# Enable Access Analyzer in all active regions
for region in $(aws ec2 describe-regions --query 'Regions[].RegionName' --output text); do
aws accessanalyzer create-analyzer
--analyzer-name "org-analyzer-${region}"
--type ORGANIZATION
--region "$region" 2>/dev/null && echo "Created: $region"
done
# List the findings (public or cross-account resources)
aws accessanalyzer list-findings
--analyzer-arn arn:aws:access-analyzer:eu-west-1:123456789012:analyzer/org-analyzer-eu-west-1
--filter '{"status": {"eq": ["ACTIVE"]}}'
GCP: Workload Identity and conditional bindings
On GCP, Workload Identity Federation replaces service account keys for external workloads. The principle is identical to OIDC: the workload authenticates with its native token, and GCP exchanges it for a temporary service account token.
# workload-identity-binding.yaml
# Lets GitHub Actions impersonate a GCP service account
apiVersion: iam.googleapis.com/v1
kind: WorkloadIdentityPoolProvider
metadata:
name: github-provider
spec:
workloadIdentityPool: projects/123456/locations/global/workloadIdentityPools/github-pool
displayName: "GitHub Actions Provider"
oidc:
issuerUri: "https://token.actions.githubusercontent.com"
attributeMapping:
google.subject: "assertion.sub"
attribute.repository: "assertion.repository"
attribute.ref: "assertion.ref"
attributeCondition: >
attribute.repository == "mon-org/mon-repo" &&
attribute.ref == "refs/heads/main"
# Create the pool and the provider
gcloud iam workload-identity-pools create "github-pool"
--project="mon-projet-gcp"
--location="global"
--display-name="GitHub Actions Pool"
gcloud iam workload-identity-pools providers create-oidc "github-provider"
--project="mon-projet-gcp"
--location="global"
--workload-identity-pool="github-pool"
--display-name="GitHub Actions Provider"
--attribute-mapping="google.subject=assertion.sub,attribute.repository=assertion.repository"
--issuer-uri="https://token.actions.githubusercontent.com"
--attribute-condition="attribute.repository=='mon-org/mon-repo'"
# Bind the provider to the service account (conditional binding)
gcloud iam service-accounts add-iam-policy-binding
"[email protected]"
--project="mon-projet-gcp"
--role="roles/iam.workloadIdentityUser"
--member="principalSet://iam.googleapis.com/projects/123456/locations/global/workloadIdentityPools/github-pool/attribute.repository/mon-org/mon-repo"
Azure: PIM and Conditional Access
Azure Privileged Identity Management (PIM) lets you grant elevated rights on a time-bound (just-in-time) basis rather than permanently. Administrators activate their role for a limited duration with justification and optional approval.
# Assign an eligible role (not permanent) via PIM
az role assignment create
--assignee "[email protected]"
--role "Contributor"
--scope "/subscriptions/sub-id/resourceGroups/rg-prod"
--description "PIM eligible assignment - requires activation"
# List the active PIM assignments
az rest
--method GET
--url "https://management.azure.com/subscriptions/sub-id/providers/Microsoft.Authorization/roleEligibilityScheduleInstances?api-version=2020-10-01"
# Conditional Access: block access without MFA for admins
az ad conditional-access policy create
--name "Require MFA for admins"
--state "enabled"
--conditions '{
"users": {"includeRoles": ["62e90394-69f5-4237-9190-012177145e10"]},
"applications": {"includeApplications": ["All"]}
}'
--grant-controls '{
"operator": "OR",
"builtInControls": ["mfa"]
}'
Comments