🛡️ Cloud Security Baseline: A Checklist You Can Audit

Twenty things that stop most cloud incidents, grouped by identity, network, data, servers, containers, logging and backups, with the pages of this series that explain each one, the public-storage settings on all four clouds, the posture tools that audit it for you, and a small auditor written and run for real

💡 How this page was made. The small auditor on this page was written and run for real on 6 October 2026 (Python, no downloads): it reads a description of cloud resources and prints findings. It is my own teaching tool, not a security product, it checks fourteen simple things, and the sample resources are made up. The facts about each cloud's settings and tools are from the providers' documentation, read on 6 October 2026; nothing was run on a cloud account. Almost every item here is explained in more depth on an earlier page, and each section says which: account pages, IAM, networking, EC2, containers, Kubernetes, first boot, backups and cost control.

What a baseline is

A baseline is the short list of things that must be true before you worry about anything fancy. Cloud security is shared: the provider secures the data centers, the hardware and the platform, and you secure what you put on it: the accounts, the access, the network rules, the data settings and the servers (AWS's Kubernetes documentation uses exactly the words "shared responsibility model"). Most incidents come from the customer's side, and from a small number of causes: an account without MFA, a key in a repository, a storage bucket opened to the world, a database reachable from the internet, a server nobody patched, and nobody watching the logs.

The checklist below has twenty items. Each is something you can look at and answer yes or no, which is what makes it auditable. Do them in order: the earlier ones matter most.

The twenty

Identity (the front door)

  1. Every human account has MFA, and the root or owner account is locked away. No daily use, no access keys for it (the AWS account and Azure account pages).
  2. Everyone has their own identity, and access goes through groups or roles, not shared logins. People sign in through SSO where you have it.
  3. Least privilege. No "allow everything on everything" policies, no Owner or Administrator for people who only need to read. Give the smallest role at the smallest scope (the IAM page).
  4. No long-lived keys for machines. Servers, containers and functions use an attached identity (instance role, managed identity, service account, instance principal). Keys that must exist are rotated and never in a repository.
  5. Old accounts, keys and roles are removed. Review them on a calendar: the clouds show "last used" information to help.

Network (the doors and windows)

  1. SSH (22) and RDP (3389) are never open to the whole internet. Allow your own address, a VPN or a bastion, or use a managed session service (the EC2 page; AWS's own best practices say the same).
  2. Databases, caches and internal services have no public path. They sit in private subnets and accept connections from the application tier only (the networking page).
  3. Rules are default-deny and written down. Open only what a service needs, from the sources that need it. No wide port ranges.

Data

  1. No public storage unless public is the point. Block public access at the account level where you can (the next section shows how on each cloud).
  2. Data is encrypted at rest and in transit, and you know who holds the keys. (Each cloud encrypts storage by default in most services: confirm it for the ones you use, and use your own keys where a rule requires it.)
  3. Secrets live in a secrets service, not in code, images, user data or tags (the first boot and Kubernetes pages explain why: user data is not secret, and a Kubernetes Secret is only base64).

Servers and containers

  1. Servers are patched, on a schedule you can name, and a reboot after kernel updates is part of it. New servers start from a recent image and update themselves at first boot.
  2. SSH uses keys, not passwords, and root login is off.
  3. The metadata service requires the authenticated version (IMDSv2 on AWS), so a flaw in one application cannot read the machine's credentials with a single request (the EC2 page).
  4. Containers run as non-root from images you pinned and scanned, and the Kubernetes API is not open to everyone (the containers and Kubernetes pages; ECR can scan on push, as its documentation says).

Watching and recovering

  1. Audit logging is on, goes somewhere the attacker cannot edit, and someone reads it. AWS CloudTrail records who called which API, Azure has the activity log, Google Cloud has Cloud Audit Logs, Oracle has the Audit service (named on the earlier pages).
  2. Alerts exist for the loud events: a root or owner sign-in, a new access key, a firewall rule opened to the world, a cost spike (the cost page).
  3. Backups exist, are kept apart from the originals, are immutable where the cloud allows it, and a restore has been tested (the backups page).
  4. Infrastructure is code (Terraform or similar), reviewed like code, so that a change has an author and a reason, and a lost environment can be rebuilt (the Terraform page).
  5. You know what to do on a bad day: who decides, how to revoke access (disable the key, the user, the role), how to preserve evidence (snapshot before you delete) and who to call. Write it down now, not during the incident.

A small auditor, run for real

Most of items 1 to 9 and 13 to 14 can be checked by a program reading a description of your resources. That is exactly what the cloud providers' posture tools do at scale (next section). To see the idea in its simplest form, here is a small auditor (59 lines) I wrote. It takes a JSON description (made-up here), applies fourteen rules, and prints findings, sorted by severity, and exits with status 1 if it finds anything, so it could fail a build:

#!/usr/bin/env python3
"""A tiny baseline auditor: reads a description of cloud resources (JSON) and prints findings."""
import json, sys, datetime as dt

def audit(data, today):
    f = []
    def add(sev, where, msg): f.append((sev, where, msg))

    for sg in data.get("firewall_rules", []):
        for r in sg["inbound"]:
            world = r["source"] in ("0.0.0.0/0", "::/0")
            if world and r["port"] in (22, 3389):
                add("HIGH", sg["name"], "port %d is open to the whole internet" % r["port"])
            elif world and r["port"] in (3306, 5432, 1433, 6379, 27017):
                add("HIGH", sg["name"], "database port %d is open to the whole internet" % r["port"])
            elif world and r["port"] not in (80, 443):
                add("MEDIUM", sg["name"], "port %s is open to the whole internet" % r["port"])

    for p in data.get("policies", []):
        for st in p["statements"]:
            if st["effect"] == "Allow" and st["action"] == "*" and st["resource"] == "*":
                add("HIGH", p["name"], "allows every action on every resource")
            if st["effect"] == "Allow" and st.get("principal") == "*":
                add("HIGH", p["name"], "grants access to any principal (*)")

    for u in data.get("users", []):
        if u["admin"] and not u["mfa"]:
            add("HIGH", u["name"], "administrator without multi-factor authentication")
        if u.get("root_keys"):
            add("HIGH", u["name"], "the root/owner account has access keys")
        for k in u.get("keys", []):
            age = (today - dt.date.fromisoformat(k["created"])).days
            if age > 90:
                add("MEDIUM", u["name"], "access key %s is %d days old (rotate or remove)" % (k["id"], age))

    for b in data.get("buckets", []):
        if b["public"]:
            add("HIGH", b["name"], "bucket allows public access")
        if not b["encrypted"]:
            add("MEDIUM", b["name"], "bucket is not encrypted")

    for i in data.get("instances", []):
        if i["metadata_v1"]:
            add("MEDIUM", i["name"], "metadata service still accepts the old unauthenticated version (IMDSv1)")
        if i["password_ssh"]:
            add("MEDIUM", i["name"], "SSH password logins are enabled")
        if not i["disk_encrypted"]:
            add("MEDIUM", i["name"], "disk is not encrypted")
        if not i["backup"]:
            add("MEDIUM", i["name"], "no backup policy is attached")
    order = {"HIGH": 0, "MEDIUM": 1}
    return sorted(f, key=lambda x: (order[x[0]], x[1]))

data = json.load(open(sys.argv[1]))
findings = audit(data, dt.date(2026, 10, 6))
for sev, where, msg in findings:
    print("%-6s %-14s %s" % (sev, where, msg))
print("%d findings (%d high)" % (len(findings), sum(1 for x in findings if x[0] == "HIGH")))
sys.exit(1 if findings else 0)

Run on a deliberately bad description (an admin without MFA, a database open to the world, an "allow everything" policy, access keys on the root account, a public bucket, port 22 open to everyone, an old key, and a server with every weak setting) and then on a fixed one (as run):

user@lab:~$ python3 audit.py sample-bad.json
HIGH   alice          administrator without multi-factor authentication
HIGH   db-sg          database port 5432 is open to the whole internet
HIGH   dev-policy     allows every action on every resource
HIGH   owner          the root/owner account has access keys
HIGH   share-policy   grants access to any principal (*)
HIGH   site-assets    bucket allows public access
HIGH   web-sg         port 22 is open to the whole internet
MEDIUM alice          access key AKIA-EXAMPLE1 is 219 days old (rotate or remove)
MEDIUM reports        bucket is not encrypted
MEDIUM web1           metadata service still accepts the old unauthenticated version (IMDSv1)
MEDIUM web1           SSH password logins are enabled
MEDIUM web1           disk is not encrypted
MEDIUM web1           no backup policy is attached
13 findings (7 high)
exit status: 1
user@lab:~$ python3 audit.py sample-good.json
0 findings (0 high)
exit status: 0

Public storage: the settings, on each cloud

Item 9 has caused more headlines than any other, so here is what each cloud's documentation says (read on 6 October 2026).

The pattern: block it at the highest level (account, organization), and make exceptions deliberately.

The posture tools: an auditor you do not have to write

All four clouds sell or provide a service that continuously checks your configuration against best practices and lists the problems. The documentation (read on 6 October 2026):

Start by switching on the free or trial tier and reading the first list. Expect a long list on an old account: sort by severity, fix the highs first, and treat the rest as a backlog.

A routine

Common mistakes

What this page did not cover

Compliance frameworks in depth, threat detection and response, web application firewalls and DDoS protection, key management and hardware security modules, zero trust network design, data classification and privacy law, penetration testing and supply-chain security for code and images. Each is a subject of its own.

The cheat sheet

← Cloud Backups and Disaster Recovery: Protect, Copy, Restore · ↑ All tutorials · DNS and Domain Names Across the Clouds →