Cloud & Container Security · beginner · ~10 min

Storage buckets, security groups, and public exposure

**What you will learn** - Recognise the classic cloud public-exposure misconfigurations: public object-storage buckets, security groups open to `0.0.0.0/0`, and public snapshots, databases, and dashboards. - Explain *why* these resources end up public — checkbox defaults, overly broad policies, and ACLs that grant access to `AllUsers`. - Read a security group rule and decide whether a source range is safe or dangerously wide, building on CIDR notation from *The cloud model and IAM*. - Apply the universal fix pattern: default-deny, account-level public-access blocks, least-privilege policies, and restricted source ranges. - Test a cloud account for exposure by asking two questions of every resource: *Is this reachable from the internet? Should it be?* - Set up basic detection and logging so exposure is caught early, without ever logging the secrets you are trying to protect.

Overview

Most real-world cloud security incidents are not clever exploits. They are accidents of configuration — a resource that was meant to be private but was left open to the entire internet. No malware, no zero-day, no password cracking is required: the attacker simply requests the data and the cloud hands it over.

Two cases dominate the findings auditors and attackers see again and again:

  • Storage buckets made public, which leaks customer data, backups, and secrets.
  • Security groups (virtual firewalls) that open SSH (22), RDP (3389), or database ports to 0.0.0.0/0, which is shorthand for the whole internet.

This lesson builds directly on The cloud model and IAM. There you learned that the cloud is a shared-responsibility model — the provider secures the infrastructure, but you are responsible for how you configure access. You also met CIDR notation (0.0.0.0/0, 10.0.0.0/8) and the principle of least privilege. Public-exposure bugs are what happens when least privilege is forgotten: a permission that should have been narrow was left wide open.

The terminology is small and worth fixing early. A bucket is a container for files ("objects") in object storage such as Amazon S3, Azure Blob Storage, or Google Cloud Storage. An ACL (access control list) and a bucket policy are two ways to say who may read or write those objects. A security group is a stateful, instance-level firewall that lists which inbound and outbound connections are allowed. 0.0.0.0/0 is the CIDR block that matches every possible IPv4 address. Keep these straight and the rest of the lesson reads naturally.

Why it matters

Public buckets and 0.0.0.0/0 firewall rules are among the most common and most damaging cloud security findings, year after year, across every major provider.

The damage is direct and immediate:

  • Public buckets expose data at rest — personal records, financial data, source code, and (worst of all) credentials stored as files. Anyone who can guess the bucket name can download everything.
  • Open security groups hand an attacker an internet-facing front door. Port 3389 (RDP) and port 22 (SSH) exposed to the world are top entry points for brute-force attacks and ransomware. Exposed database ports let an attacker connect straight to the data layer.

What makes these so dangerous is the asymmetry: they are trivial for an attacker to find (automated scanners sweep the entire internet continuously) and they require no exploitation skill, yet they can leak an entire company's data or compromise a server in minutes. They are also embarrassingly easy to introduce — one wrong checkbox, one copied-from-a-tutorial firewall rule. Understanding them is the highest-leverage cloud security skill a beginner can learn.

Core concepts

1. Public storage buckets

Definition. A storage bucket is a named container for files in a cloud object store (S3, Azure Blob, GCS). Buckets are private by default — only the owning account can read them — but they can be opened to the public through configuration.

How it goes wrong. Exposure usually arrives by one of three routes:

  • A single "make public" / "allow public access" checkbox left enabled.
  • A bucket policy with a Principal of * (everyone) and an Allow on read actions.
  • An ACL that grants READ to the predefined AllUsers group.

Once public, the bucket's contents are readable by anyone who knows or guesses the name. Names are often predictable (acme-backups, acme-prod-logs), so attackers run bucket-name enumeration — trying lists of likely names — as routine reconnaissance.

Structure — how a request reaches a public object:

Attacker            Internet            Cloud object store
  |  GET /acme-backups/db.sql  ----->  [ bucket: acme-backups ]
  |                                       policy: Principal "*" Allow s3:GetObject
  |  <-----  200 OK + file contents  ---  (no auth checked -> returns data)

When public is legitimate: a public website's static assets, published open data, software downloads. When it is NOT: anything containing personal data, backups, logs, configuration, or secrets. The default should always be private; public is a deliberate, reviewed exception.

Pitfall. Teams disable account-level "Block Public Access" to make one harmless asset reachable, then forget — every future bucket is now one bad policy away from exposure.

Knowledge check. A bucket policy contains "Principal": "*" with "Action": "s3:GetObject" and "Effect": "Allow". In one sentence, who can download objects from this bucket?

2. Security groups and the 0.0.0.0/0 rule

Definition. A security group is a virtual, stateful firewall attached to cloud instances. Each rule allows traffic to a port from a source CIDR range. "Stateful" means return traffic for an allowed connection is automatically permitted.

How it works internally. When a packet arrives for an instance, the cloud checks it against the security group's inbound rules. If any rule matches the destination port and the source IP falls inside an allowed CIDR, the packet is permitted; otherwise it is dropped (default-deny). Recall CIDR matching from The cloud model and IAM: 203.0.113.5/32 matches one address; 0.0.0.0/0 matches all 4.3 billion IPv4 addresses.

The classic mistake is opening a sensitive port to 0.0.0.0/0:

Port Service Risk when open to 0.0.0.0/0
22 SSH Internet-wide brute force, key-guessing
3389 RDP Brute force, ransomware foothold
3306 MySQL Direct attack on the database
5432 PostgreSQL Direct attack on the database
6379 Redis (often no auth) Trivial data theft / takeover

Data flow — narrow vs wide source:

WIDE  (insecure):  inbound TCP 22  <-  0.0.0.0/0      every host on earth can knock
NARROW (better):   inbound TCP 22  <-  203.0.113.0/24  only the office network can
BEST   (no public ingress): SSH only via a bastion host or VPN; instance has no public 22

When 0.0.0.0/0 is acceptable: ports 80/443 on a public web server — those are meant to serve the world. It is almost never acceptable on admin or database ports.

Pitfall. Copying a "quick start" rule that opens 0.0.0.0/0 to get something working, intending to tighten it later, and never doing so.

Knowledge check (predict). A security group has one inbound rule: TCP 3389 from 0.0.0.0/0. An attacker in another country runs an RDP scanner. Will their connection attempt reach the instance? Why?

3. Other commonly exposed surfaces

The same default-deny lesson applies beyond buckets and SSH:

  • Public disk snapshots and machine images (AMIs) — a snapshot shared publicly exposes a full copy of a disk, secrets included.
  • Public databases — a managed database with a public endpoint and a wide security group.
  • Unauthenticated dashboards — search/analytics UIs (e.g. Kibana), metrics consoles, or admin panels reachable without login.
  • Open management APIs — orchestration or container APIs left listening on a public interface.

Threat model (text diagram):

Assets:        customer data, backups, credentials, compute capacity
Trust boundary:  [ Internet ]  |  [ Your cloud account ]
Entry points:   public bucket URL, open SG port, public snapshot,
                unauthenticated dashboard, public DB endpoint
Goal:           keep every entry point either closed or behind auth

Knowledge check (explain). In your own words, why is "public exposure + no authentication" a complete breach on its own, with no exploit needed?

4. The universal fix pattern

Every one of these findings has the same remedy shape:

  • Default-deny. Start closed; open only what is required.
  • Block public access at the account level so a single bad bucket policy cannot leak data.
  • Least-privilege policies — name specific principals and actions, never * for sensitive data.
  • Restrict source ranges to known networks; reach internal hosts through a bastion or VPN, not a public port.
  • Keep secrets out of buckets — use a dedicated secrets manager (covered next in Secrets, metadata, and cloud privilege escalation).

Syntax notes

Two configuration shapes appear constantly. Learn to read them at a glance.

A dangerously public bucket policy (JSON):

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": "*",          // "*" = everyone on the internet  <-- DANGER
    "Action": "s3:GetObject",  // anyone may download objects
    "Resource": "arn:aws:s3:::acme-backups/*"
  }]
}

A security group inbound rule (conceptual table form):

Type   Protocol   Port   Source
SSH    TCP        22     0.0.0.0/0      <-- the whole internet (DANGER for SSH)
SSH    TCP        22     203.0.113.0/24  <-- only the office range (safer)

The two signals to scan for: Principal": "*" in any policy on sensitive data, and 0.0.0.0/0 on any admin or database port.

Lesson

The most common cloud findings are resources accidentally exposed to the internet.

Public storage buckets

Object storage (S3, Azure Blob, GCS) is private by default. But it is frequently made public by mistake.

This can happen through:

  • A single checkbox left enabled.
  • A bucket policy that is too broad.
  • ACLs (access control lists) that grant access to AllUsers.

The result: customer data, backups, and secrets become readable by anyone who knows, or guesses, the bucket name. Guessing bucket names (bucket-name enumeration) is a routine part of attacker reconnaissance.

The fix:

  • Block public access at the account level.
  • Use least-privilege bucket policies.
  • Do not store secrets in buckets.

Security groups and firewall rules

Cloud security groups are virtual firewalls attached to instances.

The classic mistake is opening sensitive ports to 0.0.0.0/0, which means the whole internet. For example:

  • SSH (port 22)
  • RDP (port 3389)
  • Databases (ports 3306 or 5432)
  • Admin panels

When these are exposed globally, anyone on the internet can reach them.

The fix:

  • Restrict source ranges to known IPs.
  • Use bastions or a VPN for access.
  • Default-deny inbound traffic.

Other exposed surfaces

Watch for:

  • Public snapshots and AMIs.
  • Public databases.
  • Unauthenticated dashboards, such as Kibana.
  • Management APIs left open.

Testing

List the account's resources. For each one, ask two questions:

  1. Is this reachable from the internet?
  2. Should it be?

Public exposure combined with weak or missing authentication is a direct breach. No exploit is required.

Code examples

Below is a small, self-contained defensive audit script you can run against a local, authorised lab (for example LocalStack or a test account you own). It reads a JSON description of resources and flags public exposure. It invents no cloud APIs — it works on a plain data file, so you can study the logic without credentials.

#!/usr/bin/env python3
"""exposure_audit.py - flag publicly exposed cloud resources.
Authorised lab use only. Run against accounts/data you own.
Reads a JSON inventory; never contacts real cloud APIs.
"""
import json
import sys

# Ports that must never be open to the whole internet.
SENSITIVE_PORTS = {22: "SSH", 3389: "RDP", 3306: "MySQL",
                   5432: "PostgreSQL", 6379: "Redis"}
WHOLE_INTERNET = "0.0.0.0/0"


def audit_bucket(bucket):
    findings = []
    name = bucket.get("name", "<unnamed>")
    # A wildcard principal with a read action == public download.
    for stmt in bucket.get("policy_statements", []):
        if stmt.get("effect") == "Allow" and stmt.get("principal") == "*":
            findings.append(f"Bucket '{name}': policy allows '*' (public)")
    if "AllUsers" in bucket.get("acl_grants", []):
        findings.append(f"Bucket '{name}': ACL grants AllUsers (public)")
    return findings


def audit_security_group(sg):
    findings = []
    name = sg.get("name", "<unnamed>")
    for rule in sg.get("inbound", []):
        port = rule.get("port")
        src = rule.get("source")
        if src == WHOLE_INTERNET and port in SENSITIVE_PORTS:
            svc = SENSITIVE_PORTS[port]
            findings.append(
                f"SG '{name}': {svc} (port {port}) open to {WHOLE_INTERNET}")
    return findings


def main():
    if len(sys.argv) != 2:
        print("usage: exposure_audit.py <inventory.json>", file=sys.stderr)
        return 2
    try:
        with open(sys.argv[1], "r", encoding="utf-8") as f:
            inventory = json.load(f)        # closes via the with-block
    except (OSError, json.JSONDecodeError) as e:
        print(f"could not read inventory: {e}", file=sys.stderr)
        return 1

    findings = []
    for b in inventory.get("buckets", []):
        findings += audit_bucket(b)
    for sg in inventory.get("security_groups", []):
        findings += audit_security_group(sg)

    if findings:
        print("PUBLIC EXPOSURE FINDINGS:")
        for f in findings:
            print("  -", f)
        return 1            # non-zero exit so CI/CD can fail the build
    print("No public exposure found.")
    return 0


if __name__ == "__main__":
    sys.exit(main())

Example input (inventory.json):

{
  "buckets": [
    {"name": "acme-backups",
     "policy_statements": [{"effect": "Allow", "principal": "*"}]},
    {"name": "acme-website-assets", "acl_grants": ["OwnerFullControl"]}
  ],
  "security_groups": [
    {"name": "db-sg", "inbound": [{"port": 5432, "source": "0.0.0.0/0"}]},
    {"name": "web-sg", "inbound": [{"port": 443, "source": "0.0.0.0/0"}]}
  ]
}

What it does and expected output. The script loads the inventory, checks each bucket for a wildcard principal or an AllUsers ACL, and checks each security group for sensitive ports open to 0.0.0.0/0. With the input above it prints:

PUBLIC EXPOSURE FINDINGS:
  - Bucket 'acme-backups': policy allows '*' (public)
  - SG 'db-sg': PostgreSQL (port 5432) open to 0.0.0.0/0

and exits with status 1. Note what it correctly does not flag: acme-website-assets (no public grant) and web-sg (port 443 to the world is normal for a web server). Edge cases: a missing or malformed JSON file is caught and reported; a port not in SENSITIVE_PORTS open to the world is ignored on purpose; this is a static check and would not see a setting that exists only in the live cloud — treat it as one layer, not the whole defence.

Line by line

Walking the key paths of exposure_audit.py:

  1. SENSITIVE_PORTS / WHOLE_INTERNET — the policy of the tool, expressed as data. Putting the rules at the top makes them easy to review and extend.
  2. main() argument check — if the inventory path is missing, print usage to stderr and return 2. Distinct exit codes let an automated pipeline tell "bad usage" from "findings" from "clean".
  3. open(...) as f + json.load — the with block guarantees the file is closed even if parsing raises. The try/except turns a crash into a clean 1 with a message.
  4. audit_bucket — for each policy statement, the condition effect == "Allow" and principal == "*" is the single line that defines "public". It also checks for the AllUsers ACL grant, the other common route to a public bucket.
  5. audit_security_group — for each inbound rule, it flags only when both source == 0.0.0.0/0 and port is sensitive. This is why web-sg's 443 rule is not flagged — the port is not in the sensitive set.
  6. Accumulating findings — bucket and SG findings are concatenated into one list, so a single pass over the report shows everything.
  7. Exit status — findings present → return 1; clean → return 0. A CI job running python exposure_audit.py inventory.json will fail the build on any exposure, turning the lesson into an enforced guardrail.

Trace with the example input:

Step Resource Test Result
1 bucket acme-backups principal * Allow FLAG
2 bucket acme-website-assets no *, no AllUsers clean
3 SG db-sg port 5432 + 0.0.0.0/0 FLAG
4 SG web-sg port 443 (not sensitive) clean

Two findings reach the output; exit code 1.

Common mistakes

Mistake 1 — "Make it public to fix the 403."

Wrong: an asset returns Access Denied, so the developer toggles the bucket to public.

{"Effect": "Allow", "Principal": "*", "Action": "s3:GetObject"}

Why it is wrong: it solves the symptom by exposing the entire bucket to the internet. The real problem was usually a missing grant for one specific identity.

Corrected: grant access to the exact principal that needs it, and keep account-level Block Public Access on.

{"Effect": "Allow",
 "Principal": {"AWS": "arn:aws:iam::123456789012:role/app-reader"},
 "Action": "s3:GetObject",
 "Resource": "arn:aws:s3:::acme-backups/reports/*"}

Recognise it: any Principal": "*" on a bucket holding non-public data.

Mistake 2 — 0.0.0.0/0 on SSH "just for now."

Wrong: inbound TCP 22 from 0.0.0.0/0 to log in from a coffee shop.

Why it is wrong: it advertises an SSH login prompt to every scanner on the planet within minutes; brute-force traffic starts almost immediately.

Corrected: restrict to your network (203.0.113.0/24), or remove public SSH entirely and connect through a bastion or VPN.

Recognise it: 0.0.0.0/0 paired with port 22, 3389, or any database port.

Mistake 3 — Confusing "private network" with "private resource."

Wrong: assuming a database is safe because it is "in the VPC," while its security group still allows 0.0.0.0/0 on 5432 and it has a public IP.

Why it is wrong: network placement does not override an open firewall rule and a public endpoint.

Corrected: no public IP/endpoint, and a security group that only allows the app tier's range.

Recognise it: a resource with both a public endpoint and a wide ingress rule.

Mistake 4 — Storing secrets in a bucket "because it is convenient."

Wrong: s3://acme-config/prod.env containing DB_PASSWORD=....

Why it is wrong: the day that bucket is misconfigured, the secret leaks with it, and bucket exposure is the single most common finding.

Corrected: store secrets in a dedicated secrets manager and reference them at runtime (the focus of the next lesson).

Debugging tips

Because this is configuration rather than code, "debugging" means verifying the state of the cloud and your audit logic.

Audit-script errors

  • json.decoder.JSONDecodeError — your inventory file is not valid JSON. Validate it (python -m json.tool inventory.json) before running the audit.
  • KeyError while extending the script — use .get(key, default) rather than dict[key] so a missing field does not crash the audit; missing data should be treated as "unknown," not silently "safe."

Verifying exposure findings (authorised lab only)

  • For a suspected public bucket, request an object without credentials from your own test bucket. A 200 with content confirms public read; a 403 confirms it is locked down. Never do this against systems you do not own.
  • For a security group, check from a host outside the allowed range whether the port is reachable. If a sensitive port answers from anywhere, the rule is too wide.

Logic errors to watch for

  • False clean: the script only sees what is in the inventory file. If your inventory was generated incompletely, it may miss a real public bucket. Cross-check the count of resources against the provider console.
  • False alarm: port 80/443 open to 0.0.0.0/0 on a web server is expected; do not "fix" it. Keep an allow-list of intentionally public ports.

Questions to ask when something looks wrong

  1. Is this resource meant to be reachable from the internet?
  2. If yes, is there authentication in front of it?
  3. What is the smallest source range / narrowest principal that still makes it work?
  4. Would I be comfortable if this exact config appeared on the front page of a news site?

Memory safety

Security & safety

Authorisation and ethics. Everything here is defensive and must run only on systems you own or are explicitly authorised to test — a local lab (LocalStack/MinIO), a container, or your own sandbox account. Bucket-name guessing or port scanning against third parties is unauthorised access and is unlawful in most jurisdictions. Practising the audit side on your own resources is the right way to learn.

The risks, restated as a defender.

Asset            Exposure mechanism        Consequence
------------     ---------------------     -----------------------------
Object data      public bucket policy/ACL  full data download by anyone
Server access    SG 22/3389 -> 0.0.0.0/0   brute force, ransomware
Database         SG 3306/5432 -> 0/0       direct data theft
Disk image       public snapshot/AMI       offline secret extraction
Dashboard        no auth + public          data browsing / control

Defensive practices, in priority order.

  1. Default-deny everywhere. Buckets private; security groups deny inbound except what is required.
  2. Account-level Block Public Access so one bad policy cannot leak data.
  3. Least privilege: specific principals and actions, never * on sensitive data; narrow CIDR ranges, never 0.0.0.0/0 on admin/DB ports.
  4. No public path to admin/data planes: use a bastion or VPN; keep secrets in a secrets manager, not in buckets.
  5. Continuous detection: run an audit like the one above in CI and on a schedule.

Detection and logging — what to log (and what never to). Enable object-access logging on buckets and flow logs on networks. Log who accessed which resource, from where, and whether it was allowed — plus configuration changes ("bucket made public," "SG rule added for 0.0.0.0/0"). Never log the data itself, object contents, passwords, API keys, or tokens. When you must reference a secret in a log, log a non-reversible identifier, not the value. Alert on any new Principal": "*" or 0.0.0.0/0 on a sensitive port.

Mitigation verification. After locking a bucket down, re-request an object unauthenticated from your lab and confirm a 403. After tightening a security group, confirm the sensitive port no longer answers from outside the allowed range. A fix is not done until you have re-tested that the hole is closed.

Real-world uses

Concrete use case. A company stores nightly database backups in a bucket named acme-prod-backups. A developer, fighting an access error during a late-night deploy, sets the bucket policy to Principal": "*" to "unblock" themselves and forgets to revert. Within hours, automated scanners that enumerate predictable bucket names find it and download every backup — including a .env file with credentials. The same class of mistake on the network side (RDP open to 0.0.0.0/0) is a leading initial-access vector for ransomware. Both are prevented by the habits in this lesson.

Where these controls live in professional work:

  • Web/SaaS: static assets are public on purpose; everything else (user uploads, backups, logs) is private with signed, time-limited URLs for legitimate access.
  • DevOps/Platform: infrastructure-as-code templates are reviewed so no one can introduce 0.0.0.0/0 on a sensitive port without it showing up in a pull request.
  • Security teams: Cloud Security Posture Management (CSPM) tools and CI checks (like the audit script) continuously scan for exactly these findings.

Professional best-practice habits.

Beginner rules:

  • Keep account-level Block Public Access on; treat any exception as a reviewed decision.
  • Never put a secret in a bucket.
  • Never open 22/3389/DB ports to 0.0.0.0/0.
  • Restrict to the narrowest source range that works.

Advanced practices:

  • Manage all access in version-controlled infrastructure-as-code so changes are reviewable and revertible.
  • Run automated posture checks in CI that fail the build on public exposure.
  • Use access via bastion/VPN/identity-aware proxy instead of public ports; prefer signed URLs over public objects.
  • Enable access logs and network flow logs, and alert on new public grants in near real time.

Practice tasks

Beginner 1 — Spot the public bucket. Given three bucket policies, identify which one is public.

  • Requirements: read each Statement; flag any with Effect: Allow and Principal: "*".
  • Example: policy A has Principal: "*"; policies B and C name a specific role. Answer: A is public.
  • Hint: the wildcard principal is the tell.
  • Concepts: bucket policies, least privilege.

Beginner 2 — Classify firewall rules. For five security-group rules (port + source), label each safe, risky, or depends.

  • Requirements: 443 from 0.0.0.0/0 → safe; 22 from 0.0.0.0/0 → risky; 5432 from 10.0.1.0/24 → depends (only safe if that range is trusted and internal).
  • Constraint: justify each label in one sentence.
  • Hint: sensitive port + 0.0.0.0/0 = risky.
  • Concepts: CIDR ranges, default-deny.

Intermediate 1 — Extend the audit script. Add a check that flags any bucket whose name appears in a configurable list of "never-public" prefixes (e.g. *-backups, *-secrets) when it is public.

  • Requirements: read prefixes from the inventory or a constant; print a distinct finding type; keep exit-code behaviour.
  • Input/output: a *-backups bucket that is public produces a HIGH severity line.
  • Hint: reuse audit_bucket; do not break existing tests.
  • Concepts: least privilege, detection in CI.

Intermediate 2 — Write the lock-down policy. Given a public acme-backups bucket, write (a) a least-privilege bucket policy granting read only to one named role on one prefix, and (b) the account setting that prevents future public access.

  • Requirements: no wildcard principal; scoped Resource.
  • Constraint: explain how you would verify the fix.
  • Hint: see the corrected policy in Mistakes.
  • Concepts: least privilege, mitigation verification.

Challenge — Build a mini posture report. Extend the audit to also handle snapshots (flag public: true) and dashboards (flag auth: "none" with a public endpoint), assign each finding a severity, and emit a sorted report (highest severity first) plus a non-zero exit on any HIGH finding.

  • Requirements: a severity map, stable sorting, and a summary count by severity.
  • Constraint: no real cloud calls; pure JSON inventory; log resource identifiers but never any secret values.
  • Hint: model new resource types the same way audit_bucket and audit_security_group are modelled.
  • Concepts: threat modelling, detection, defensive logging.

Summary

  • The most common and most damaging cloud findings are accidental public exposure, not exploits: public storage buckets and security groups that open sensitive ports to 0.0.0.0/0.
  • Buckets are private by default but go public via a checkbox, a Principal: "*" policy, or an AllUsers ACL. Attackers find them with bucket-name enumeration.
  • 0.0.0.0/0 means the entire internet. It is fine on web ports 80/443 but dangerous on SSH (22), RDP (3389), and database ports (3306/5432/6379).
  • Watch the other surfaces too: public snapshots/AMIs, public databases, and unauthenticated dashboards.
  • The fix is always the same shape: default-deny, account-level Block Public Access, least-privilege policies, restricted source ranges, bastion/VPN instead of public admin ports, and secrets in a secrets manager — not in buckets.
  • Test every resource with two questions: Is it reachable from the internet? Should it be? Then verify each fix by re-testing that the hole is closed, log access and config changes, and never log the secrets you are protecting. This sets up the next lesson, Secrets, metadata, and cloud privilege escalation.

Practice with these exercises