Key takeaways
- Shared responsibility stops at the hypervisor: no provider catches your misconfigurations by default. The five that cause most incidents are always the same — a public object bucket, AWS root access keys, an
"Action": "*", "Resource": "*"policy (the cloud equivalent ofchmod 777 /), a Security Group open to0.0.0.0/0on 22/3389/3306/5432, and secrets hardcoded in source or environment variables. - A Security Group is stateful, attached to an instance, and only accepts allow rules: return traffic is implicit. A NACL is stateless, attached to the subnet, requires explicit inbound AND outbound rules, but supports deny. Use Security Groups as the granular first line and NACLs on top to block entire IP ranges.
- OIDC authentication removes static keys from CI/CD: GitHub Actions presents a signed JWT, exchanged for temporary AWS credentials through
sts:AssumeRoleWithWebIdentity(1 hour by default), with the repository, branch and environment claims validated. No key ever sits in GitHub Secrets, no manual rotation, and every assume-role is traced in CloudTrail. - Secrets Manager vs Parameter Store: Secrets Manager brings Lambda-driven automatic rotation, versioning and cross-region replication for $0.40 per secret per month; Parameter Store is free on the Standard tier and $0.05 per parameter on Advanced (SecureString + KMS) but has no native rotation. Database credentials and API keys that must rotate every 60 to 90 days: Secrets Manager. Configuration and feature flags: Parameter Store.
- Terraform code must be scanned before any
apply: checkov ships over 1000 CIS rules across AWS/GCP/Azure/Kubernetes and exports SARIF into GitHub Code Scanning, while tfsec is lighter for fast PR feedback — both set to fail on CRITICAL and HIGH. Against drift, combine a nightly scheduledterraform plan, AWS Config or driftctl, and SCPs that simply forbid manual changes in production.
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"]
}'
Premium Content
This advanced tutorial is reserved for premium members.
- All advanced tutorials
- New content every week
- Progress tracking
- Cancel anytime