Security
Difficulty: Advanced
13 min read

Cloud Security: Securing AWS, GCP and Azure in Production

Advanced cloud security guide 2026: least-privilege IAM, VPC best practices, secrets management (Secrets Manager, Key Vault, Secret Manager), audit and compliance, secure Terraform with checkov/tfsec and a 25+ point multi-cloud checklist.

Back to tutorials
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"]
  }'

Premium Content

This advanced tutorial is reserved for premium members.

9,90€ / month
  • All advanced tutorials
  • New content every week
  • Progress tracking
  • Cancel anytime

Written by

Morgann Riu

Cybersecurity and Linux administration expert. I share my knowledge through free tutorials and training to help system administrators and developers secure their infrastructures.

Frequently asked questions

What is the difference between an AWS Security Group and a NACL?
A Security Group is a stateful firewall attached to an instance or a network interface: if you allow inbound traffic, the return traffic is automatically allowed. It only supports allow rules (no explicit deny). A Network ACL (NACL) is a stateless firewall attached to a subnet: you must explicitly allow inbound AND outbound traffic, and deny rules are supported. NACLs apply to the entire subnet, Security Groups to individual resources. In practice, use Security Groups as the first line of defense (granular, stateful) and NACLs as an additional control layer at the network level to block entire IP ranges.
Why should you never use AWS root IAM access keys?
The AWS root account has unlimited rights over the entire account: deleting resources, closing the account, accessing billing, revoking organization policies. A compromised root key means a total and irreversible compromise. AWS explicitly recommends never creating root access keys: disable them if they exist and lock down the root account with a hardware MFA (YubiKey). For day-to-day operations, create IAM users with the minimum necessary permissions, or better, use IAM roles for workloads and SSO for humans.
How does OIDC authentication work between GitHub Actions and AWS without static keys?
GitHub Actions supports OpenID Connect (OIDC): GitHub acts as a trusted identity provider. The workflow obtains a JWT token signed by GitHub and exchanges it for temporary AWS credentials via sts:AssumeRoleWithWebIdentity. AWS verifies the JWT signature against GitHub's OIDC public keys and validates the claims (repository, branch, environment). No AWS key is ever stored in GitHub Secrets. The obtained credentials are temporary (configurable duration, 1h by default) and scoped to the IAM role. This approach eliminates manual key rotation, reduces the attack surface and provides full traceability in CloudTrail.
What is the difference between AWS Secrets Manager and Parameter Store?
Secrets Manager is designed specifically for secrets: built-in automatic rotation (Lambda), versioning, multi-region replication, mandatory KMS encryption. It costs 0.40 USD per secret per month. Parameter Store (SSM) is more versatile: non-sensitive configurations (Standard tier, free), secrets (Advanced tier, 0.05 USD per parameter) with optional KMS encryption, but without native automatic rotation. Use Secrets Manager for database credentials, API keys and certificates that require automatic rotation. Use Parameter Store for application configurations, feature flags and non-critical parameters. Both integrate natively with IAM, CloudTrail and AWS services.
How do checkov and tfsec integrate into a CI/CD pipeline?
Checkov and tfsec statically analyze Terraform code before any apply. Checkov (Bridgecrew/Prisma) covers more than 1000 CIS Benchmark rules, covers AWS/GCP/Azure/Kubernetes and supports SARIF for GitHub Code Scanning integration. Tfsec is lighter and faster, ideal for quick checks in PRs. In practice, integrate both in fail-on-error mode in the PR: the Terraform plan is not generated if critical misconfigurations are detected. You can exclude specific rules with inline annotations (checkov:skip, tfsec:ignore) when the risk is accepted and documented. Sentinel (HashiCorp) adds a policy-as-code layer on the Terraform Cloud side for organizational guardrails.
How do you detect configuration drift on cloud infrastructure?
Drift occurs when the actual state of the infrastructure diverges from the state declared in the Terraform code. Several complementary approaches: terraform plan in read-only mode in a scheduled pipeline (hourly or nightly) detects out-of-band changes. AWS Config with compliance rules triggers real-time alerts on modifications. CloudTrail + EventBridge lets you trigger a Lambda that re-runs a Terraform plan on every critical resource change. Driftctl (a dedicated tool) compares the real cloud state with the Terraform state and lists unmanaged resources. The best defense remains to block manual changes via SCPs (AWS Organizations) or GCP/Azure organization policies that forbid direct actions in production.

Share this tutorial

Did you enjoy this article?

Comments

Checklist Sécurité Linux

30 points essentiels pour sécuriser un serveur Linux. Recevez aussi les nouveaux tutoriels par email.

Pas de spam. Désabonnement en 1 clic.