Cloud Security and Shared Responsibility
On this page
The Shared Responsibility Model#
In the cloud you do not run "a server you control"; you rent capabilities through an API, and the provider's boundaries decide which part of the security job is yours. The Shared Responsibility Model describes that split. The provider secures the cloud itself: data centres, hardware, the hypervisor, the physical network and the managed-service software. You secure what you put in the cloud: accounts and identities, permissions, data, network settings, and any operating system or application you run.
Where the line falls depends on the service type:
| You are responsible for | IaaS (VMs) | PaaS (managed DB, serverless) | SaaS (email, CRM) |
|---|---|---|---|
| Data classification and access | Yes | Yes | Yes |
| Identities and permissions | Yes | Yes | Yes |
| Application code | Yes | Yes | No |
| OS patching, runtime | Yes | No | No |
| Network rules (security groups) | Yes | Partly | No |
| Physical security, hypervisor | No | No | No |
Notice the top two rows never move. Whatever you buy, identity and data configuration stay yours, and that is where most cloud breaches happen. A storage bucket that became public was locked by default; someone changed a setting.
The Main Cloud Threats#
- Misconfiguration: public storage buckets, public disk and database snapshots, security groups allowing
0.0.0.0/0to database or SSH ports, logging switched off. This is historically the most common cause of cloud data exposure. - Over-privileged identities: roles with
"Action": "*", long-lived access keys on developer laptops, the root account used for daily work, cross-account trust that is too broad. - Leaked credentials: keys committed to Git, baked into container images, printed in CI logs or left in infrastructure state files. See secrets management.
- Exposed management planes: Kubernetes API servers, admin consoles and databases reachable from the internet.
- Instance metadata abuse: an SSRF bug makes the server fetch
http://169.254.169.254/..., which hands out the credentials of the role attached to the instance.
IAM Is the Central Control#
Every action in the cloud is an API call authorised by an identity. Your IAM design is your perimeter:
- Root only for break-glass. Put a hardware MFA key on the root or owner account, create administrative roles, and alert on any root login.
- Humans through SSO. People log in via the identity provider and assume roles with short sessions. No long-lived personal access keys.
- Workloads through roles. Attach a role to the VM, function or pod so the SDK receives temporary credentials automatically.
- CI through federation. Instead of storing a cloud key in your CI system, let the pipeline exchange its OIDC token for short-lived credentials.
A least-privilege policy names exact actions and resources:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": "arn:aws:s3:::invoices-prod/exports/*"
}
]
}
Compare that with "Action": "s3:*", "Resource": "*", which lets the same compromised workload delete every bucket in the account. The blast radius of a stolen credential equals the permissions attached to it.
For CI federation, the trust policy on the role must restrict which repository and branch can assume it. A common mistake is trusting the whole identity provider:
{
"Effect": "Allow",
"Principal": {"Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:sub": "repo:acme/billing:ref:refs/heads/main"
}
}
}
Without the sub condition, any repository on the platform could request credentials for your role.
Lock Down Instance Metadata#
On AWS, IMDSv2 requires a session token obtained via a PUT request before metadata can be read. Most SSRF bugs can only make GET requests and cannot set custom headers, so enforcing IMDSv2 blocks the classic credential theft:
aws ec2 modify-instance-metadata-options \
--instance-id i-0abc123def4567890 \
--http-tokens required \
--http-put-response-hop-limit 1
The hop limit of 1 also stops containers on the instance (one network hop further away) from reaching metadata unless you intend them to. Set these defaults in your launch templates, not one instance at a time.
A Worked Example: A Small Account Audit#
Commercial scanners do this at scale, but a short boto3 script shows what they check and why:
from datetime import datetime, timezone
import boto3
from botocore.exceptions import ClientError
s3 = boto3.client("s3")
iam = boto3.client("iam")
print("== S3 buckets without a full public access block ==")
for b in s3.list_buckets()["Buckets"]:
name = b["Name"]
try:
cfg = s3.get_public_access_block(Bucket=name)["PublicAccessBlockConfiguration"]
if not all(cfg.values()):
print(f" {name}: partial block {cfg}")
except ClientError as e:
if e.response["Error"]["Code"] == "NoSuchPublicAccessBlockConfiguration":
print(f" {name}: NO public access block")
else:
raise
print("== Access keys older than 90 days ==")
now = datetime.now(timezone.utc)
for page in iam.get_paginator("list_users").paginate():
for user in page["Users"]:
keys = iam.list_access_keys(UserName=user["UserName"])["AccessKeyMetadata"]
for k in keys:
age = (now - k["CreateDate"]).days
if k["Status"] == "Active" and age > 90:
last = iam.get_access_key_last_used(AccessKeyId=k["AccessKeyId"])
used = last["AccessKeyLastUsed"].get("LastUsedDate", "never")
print(f" {user['UserName']} {k['AccessKeyId']} age={age}d last_used={used}")
Typical output on an older account:
== S3 buckets without a full public access block ==
marketing-assets: NO public access block
== Access keys older than 90 days ==
ci-deploy AKIA...Q7 age=812d last_used=2026-09-29 11:02:13+00:00
jsmith AKIA...3K age=1405d last_used=never
Each line is a finding: a bucket that can become public with one ACL change, a CI key that should be replaced with OIDC federation, and an unused key that should simply be deleted. Note that an account-level public access block can also be set, which covers every bucket at once; check that first. The script needs only read permissions, so run it with a read-only audit role.
Data and Encryption#
- Encrypt at rest with the provider's key management service. The provider guards key material in hardware; you control the key policy, which is a second permission layer: reading an encrypted object needs both storage and key permissions.
- Separate keys by data sensitivity, so revoking one key policy cuts off one dataset without breaking everything.
- Block public sharing of snapshots and backups, and alert when a snapshot is shared outside your organisation.
- Prefer managed services when the team is small. A managed database arrives with patching, backups and encryption defaults; the same database on a VM makes all of that your job.
Logging and Detection#
Turn on control-plane audit logging (CloudTrail, Azure Activity Log, GCP Cloud Audit Logs) in every region and account, and send it to a separate, locked-down logging account with retention locks, so an attacker who compromises production cannot erase their tracks. Then alert on the events that matter:
- Root or break-glass login.
- New access keys, new IAM users, policy changes, especially
AdministratorAccessattached. - Logging being disabled or a trail deleted.
- Security groups opened to
0.0.0.0/0. - API calls from unusual regions or IP ranges.
Provider threat-detection services (GuardDuty, Defender for Cloud, Security Command Center) cover many of these out of the box. The detection and SIEM lesson covers turning logs into alerts.
Continuous Posture Checking#
Configurations drift, so audits must be continuous:
- Before deploy: scan infrastructure-as-code with tools like Checkov or tfsec in CI, and fail the build on public buckets or wildcard policies.
- After deploy: run posture scanners such as Prowler or ScoutSuite on a schedule, and enable the provider's configuration-compliance service.
- Benchmark: the CIS benchmarks for each cloud give a well-known checklist that auditors recognise.
Scanner output is long on day one. Triage by exposure first (anything public), then by privilege (anything admin), then the rest.
Practice#
In a personal sandbox account, create a bucket, disable its public access block, and watch how long your posture tooling takes to flag it. Then create a CI role with an OIDC trust policy, deliberately omit the sub condition, and identify in the policy simulator or documentation why that is dangerous before fixing it. For how this fits a wider strategy, see zero trust.
Check your understanding
4 quick questions on this lesson. Get them all right and the lesson is marked complete.