Cloud & Container Security · beginner · ~9 min
**What you will learn** - Explain what cloud audit (control-plane) logging is, what it records, and why it is the foundation of cloud detection. - Identify the most common logging misconfigurations reviewers flag: disabled, uncentralized, unalerted, or tamperable logs. - Describe how the serverless security model differs from running your own servers, and where responsibility shifts. - Apply least-privilege IAM roles to individual functions and explain why over-privileged function roles are dangerous. - Treat serverless event inputs as untrusted and reason about injection and other input-driven risks. - Recommend concrete defensive controls: log centralization, alerting on sensitive actions, secrets managers, and minimal dependencies.
This lesson builds directly on The cloud model and IAM. There you learned that in the cloud you do not own the hardware — you rent capabilities, and who is allowed to do what is controlled by IAM (Identity and Access Management) policies. Two questions follow naturally from that model: How do we know what actually happened? and What changes when there is no server to log into at all? Those two questions are exactly what cloud logging and serverless security answer.
Every cloud platform has a control plane — the management layer you talk to when you create, change, or delete resources. When you click a button in the AWS console, run a CLI command, or call an API to spin up a database, that is a control-plane action. The cloud provider records each of these actions in an audit log: AWS calls this CloudTrail, Azure calls it the Activity Log, and Google Cloud calls it Cloud Audit Logs. These logs answer who did what, to which resource, from where, and when.
This matters because in the cloud there is often no physical machine you can walk up to and inspect. The audit log is frequently the only authoritative record of activity. If it is turned off, scattered across accounts, or editable by the very people being audited, defenders are flying blind — and that is itself a serious security finding.
Serverless is the second half of the picture. With services like AWS Lambda or Google Cloud Functions, you upload code and the provider runs it on demand — you never manage an operating system, patch a kernel, or size a server. That removes a whole class of work, but it does not remove security. Instead it concentrates security onto two surfaces you already met in the IAM lesson: the permissions each function is granted, and the untrusted inputs that trigger it. Understanding both is what this lesson is about.
Throughout, keep the shared-responsibility idea from the prerequisite in mind: the provider secures the platform; you secure your configuration, your code, and your data.
Logging is the cloud's detection backbone. You cannot respond to an incident you cannot see, and you cannot prove what happened during one without a trustworthy record. Attackers understand this perfectly, which is why disabling or tampering with audit logging is one of the first moves in many real cloud intrusions — blinding the defender buys them time.
There is a useful irony here that defenders exploit: the attempt to disable logging is itself a logged, high-value event. An attacker who calls StopLogging on CloudTrail generates one last loud record on the way out. Defenders who alert on that single action catch intrusions that would otherwise go unnoticed for weeks.
Serverless raises the stakes on two issues that show up in nearly every modern cloud security review:
These two themes determine what you assess, what you flag, and what you recommend when reviewing a cloud environment.
Definition. The control plane is the management layer: the APIs that create, configure, and delete cloud resources. The data plane is the actual work the resources do — serving a web page, reading a row from a database, returning an object from storage.
Why the distinction matters: audit logs like CloudTrail primarily record control-plane activity ("someone created an IAM user", "someone changed a security group"). Data-plane events (every individual file read) are higher-volume and often logged separately and selectively because of cost.
YOU / an attacker / an automated tool
|
v
+------------------------+
| CONTROL PLANE | <-- "manage resources"
| CreateUser, DeleteDB, | recorded in CloudTrail /
| ModifySecurityGroup | Activity Log / Audit Logs
+-----------+------------+
|
v
+------------------------+
| DATA PLANE | <-- "use resources"
| read object, run query| higher volume, often
| serve HTTP response | logged selectively
+------------------------+
When to care about each: for detecting account takeover, privilege escalation, and sabotage, control-plane logs are the priority. Data-plane logs matter for data-exfiltration questions ("which objects were downloaded?").
Pitfall: assuming "logging is on" means all activity is captured. By default many providers log control-plane actions but not data-plane events; you must opt in to the latter.
Knowledge check (explain in your own words): A teammate says "CloudTrail records everything that happens in our account." What is the important correction?
Definition. A provider-managed, append-oriented record of management API calls, capturing the identity, action, target resource, source IP, and timestamp of each call.
What it enables for defenders:
How it works internally (simplified): the control plane emits an event for each API call; the logging service writes it to durable storage (typically an object-storage bucket and/or a searchable log service). Centralization means shipping those events from every account/region into one protected, separate account that ordinary users cannot edit.
Account A --\
Account B ---\ +---------------------------+
Account C ----+-----> | central, locked-down log |
Account D ---/ | account (write-only for |
/ | sources, read for SOC) |
regions --/ +---------------------------+
When NOT to rely on it alone: audit logs tell you that an API call happened, not always the full payload or business meaning. Pair them with application logs for the complete story.
Pitfall: storing logs in the same account they audit, where a compromised admin can delete them. Logs should be hard to tamper with by the identities they monitor.
Knowledge check (concept): Why is shipping logs to a separate account considered stronger than keeping them in the audited account?
These are the recurring problems a reviewer flags:
| Finding | Why it is a problem | Defensive fix |
|---|---|---|
| Logging disabled in some regions/accounts | Blind spots an attacker can hide in | Enable org-wide, all regions |
| Logs not centralized | Slow, incomplete investigations | Aggregate into one protected account |
| No alerting on sensitive actions | Events are recorded but nobody sees them | Alert on StopLogging, new IAM admin, key deletion |
| Logs writable/deletable by audited accounts | Tampering destroys the evidence | Restrict delete; use immutable/object-lock storage |
Pitfall: treating collecting logs as the goal. A log nobody reads or alerts on detects nothing. Collection plus alerting plus protection is the full control.
Definition. Serverless (Functions-as-a-Service: AWS Lambda, Google Cloud Functions, Azure Functions) runs your code on demand in response to events, with the provider managing the servers, scaling, and patching.
How responsibility shifts:
TRADITIONAL SERVER SERVERLESS FUNCTION
+---------------------+ +---------------------+
| your code | | your code | <- you
| your dependencies | | your dependencies | <- you (supply chain)
| OS / patches | | function IAM role | <- you (least privilege)
| network config | | event input handling| <- you (untrusted)
| physical host | vs. | ------------------- |
+---------------------+ | OS / patching | <- provider
you manage most | scaling / host | <- provider
+---------------------+
The boxes you still own shrink — but the two that remain (the IAM role and the event input) are exactly the high-risk ones.
When serverless is a good fit: event-driven, bursty, or glue workloads (process an uploaded file, handle a webhook). When it is a poor fit / extra care needed: very long-running jobs, code with secrets that must never live in environment variables without protection, or workloads where cold-start latency or vendor lock-in is unacceptable.
Pitfall: assuming "no server to patch" means "nothing to secure." The attack surface moved; it did not disappear.
Knowledge check (find-the-issue): A Lambda function that resizes uploaded images is granted full administrator permissions "so it would stop throwing access errors during development." Name two distinct risks this creates.
Knowledge check (predict): If a function's only job is to write one record to one database table, which single defensive control most reduces the damage from a code-injection bug in it, and why?
Concept lessons have no programming syntax, but the shape of the artifacts you will read and write matters. Two recur constantly.
A least-privilege function IAM policy (JSON) — note the scoping:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:GetObject"], // one action, not s3:*
"Resource": "arn:aws:s3:::uploads-bucket/*" // one bucket, not *
}
]
}
The two things to inspect on any policy: is the Action specific (not a wildcard like s3:* or *), and is the Resource scoped to named resources (not *)?
An alert condition for a high-signal log event (pseudocode):
WHEN audit_event.action == "StopLogging"
OR audit_event.action == "DeleteTrail"
THEN raise SEV-HIGH alert -> on-call channel
These are the patterns the rest of the lesson teaches you to recognize and evaluate.
Two cloud-specific topics round out the picture: visibility and the serverless model.
Cloud platforms log control-plane activity — every API call that manages resources, including who did it and when. This happens through services like CloudTrail (AWS), Azure Activity Log, and GCP Audit Logs.
This is the cloud's authoritative audit trail.
For defenders, it enables:
Common findings:
Attackers often try to disable CloudTrail early. That attempt is itself a logged, high-signal event.
Serverless platforms (AWS Lambda, Google Cloud Functions) run your code without you managing servers. The security model shifts:
Defenses:
Cloud logging is the detection backbone — protect it and centralize it. Serverless removes host management but makes IAM and input handling even more central.
Below is a small, realistic serverless review scenario: an AWS Lambda handler (Python), its IAM role policy, and a logging alert rule. It is written to be correct and safe — it is the secure target you would steer a real review toward. Comments mark only the non-obvious, security-relevant choices.
# handler.py — resize an uploaded image; triggered by an S3 "object created" event.
import os
import json
import boto3
secrets = boto3.client("secretsmanager")
s3 = boto3.client("s3")
ALLOWED_BUCKET = os.environ["UPLOADS_BUCKET"] # config, NOT a secret
MAX_KEY_LEN = 1024
def _validate_event(record):
# Treat every field of the event as untrusted input.
bucket = record["s3"]["bucket"]["name"]
key = record["s3"]["object"]["key"]
if bucket != ALLOWED_BUCKET:
raise ValueError("unexpected bucket") # reject, do not trust
if not key or len(key) > MAX_KEY_LEN or ".." in key:
raise ValueError("invalid object key") # block path tricks
return bucket, key
def _db_token():
# Secret fetched at runtime, never baked into env vars or code.
resp = secrets.get_secret_value(SecretId="prod/image-svc/db")
return json.loads(resp["SecretString"])["token"]
def handler(event, context):
for record in event.get("Records", []):
try:
bucket, key = _validate_event(record)
except ValueError as e:
# Log the rejection WITHOUT logging the secret or full payload.
print(json.dumps({"level": "warn", "msg": "rejected event", "reason": str(e)}))
continue
obj = s3.get_object(Bucket=bucket, Key=key) # role allows ONLY this
size = obj["ContentLength"]
token = _db_token()
# ... resize + record metadata using token (omitted for brevity) ...
print(json.dumps({"level": "info", "msg": "processed", "key": key, "bytes": size}))
return {"ok": True}
// least-privilege role for the function above
{
"Version": "2012-10-17",
"Statement": [
{ "Effect": "Allow", "Action": ["s3:GetObject"],
"Resource": "arn:aws:s3:::uploads-bucket/*" },
{ "Effect": "Allow", "Action": ["secretsmanager:GetSecretValue"],
"Resource": "arn:aws:secretsmanager:*:*:secret:prod/image-svc/db-*" }
]
}
// alert rule on the audit log (high-signal control-plane events)
ALERT when action in { StopLogging, DeleteTrail, UpdateTrail }
-> notify security on-call, SEV-HIGH
What it does and expected behavior. When a file lands in the uploads bucket, the function runs. It first validates the event (right bucket, sane key, no .. path tricks), reading the object only if validation passes. It fetches the database token from a secrets manager at runtime rather than from an environment variable. It logs structured JSON for both successes and rejections but never logs the token or raw event body. The IAM policy grants exactly two actions on two named resources — nothing else. The alert rule fires the moment anyone tries to weaken logging.
Edge cases: an event for the wrong bucket or with a malformed key is rejected and logged, not processed; a missing Records list yields a clean no-op; a secrets-manager outage raises an error that the platform records (so the failure is visible, not silent).
Walking the handler in execution order, with what is in play at each step:
| Step | Code | What happens / why |
|---|---|---|
| 1 | ALLOWED_BUCKET = os.environ[...] |
Loads non-secret config. This is configuration, safe to keep in an env var; contrast with the DB token, which is not. |
| 2 | handler(event, context) invoked |
The provider passes the untrusted event. Nothing in it is trusted yet. |
| 3 | for record in event.get("Records", []) |
Defensive default: a missing key yields an empty list, so no crash, no processing. |
| 4 | _validate_event(record) |
Pulls bucket and key from attacker-influenceable fields and checks them before use. |
| 5 | if bucket != ALLOWED_BUCKET: raise |
Stops the function from being tricked into touching an unexpected bucket. |
| 6 | if ... ".." in key: raise |
Blocks path-traversal-style keys; the input is rejected, not sanitized-and-hoped. |
| 7 | except ValueError: print(... reason ...); continue |
A bad event is logged (reason only) and skipped — visible but contained. |
| 8 | s3.get_object(Bucket, Key) |
Only reachable after validation; the role permits only GetObject on this bucket, so even a logic bug cannot, say, delete objects. |
| 9 | _db_token() -> get_secret_value |
The secret is fetched now, lives briefly in memory, and is never written to logs or env. |
| 10 | final print(json.dumps({...})) |
Structured success log with key and size — useful for audit, contains no secret. |
How values flow: untrusted event -> validated (bucket, key) -> a single scoped s3:GetObject -> a runtime-fetched, never-logged secret -> structured logs. At no point does raw input reach a privileged action or a log line that could leak a secret. The role and the validation together mean the worst case of a bug is small.
Mistake 1 — Logging is "on," so we are covered. Wrong because default logging often captures only control-plane events in the home region, leaving other regions and all data-plane activity dark.
WRONG: trail enabled in us-east-1 only, no central account
RIGHT: org-wide trail, all regions, shipped to a locked-down log account
Recognize it by checking every region/account and whether logs leave the audited account.
Mistake 2 — Broad function role "to make it work."
// WARNING: Intentionally vulnerable training example — use only in a local,
// isolated, authorized lab. Do not deploy.
{ "Effect": "Allow", "Action": "*", "Resource": "*" }
Why unsafe: any bug or malicious dependency in the function now controls the whole account. Fix: scope to the specific actions and resources (see the policy in the code section). Test the fix: in a lab, confirm the function still works and that an unrelated action (e.g., deleting a different bucket) now returns AccessDenied. Detection/logging: alert on AccessDenied spikes and on any *:* policy being attached; never log the policy's secret material (there is none here, but log the principal and action, not credentials).
Mistake 3 — Secrets in environment variables. Wrong because anyone who can read the function's configuration reads the secret, and it often ends up in deployment logs.
WRONG: DB_TOKEN set as a plain Lambda environment variable
RIGHT: fetch from a secrets manager at runtime; env holds only a secret *name*
Prevent it by scanning function configs for high-entropy values and by policy-checking that env vars contain references, not secrets.
Mistake 4 — Trusting event input. Concatenating an event field straight into a query or file path invites injection. The fix is the explicit validation shown earlier; recognize the bug by tracing every event field to its first use and asking "could an attacker set this?"
Mistake 5 — Collecting logs but never alerting.
A log archive with no alert on StopLogging or new-admin grants detects nothing in real time. Fix: pair every critical log source with an alert rule and test that the alert actually fires.
This is a concept lesson, so the "bugs" are misconfigurations and review mistakes rather than compiler errors. Common ones and how to chase them:
Questions to ask when something does not look right: Is logging enabled in all regions and accounts? Are logs centralized and tamper-resistant? Is there an alert on StopLogging/admin changes, and does it fire? For each function: what is the minimum set of actions and resources it needs, and does its role match? Where do its inputs come from, and are they validated before use? Where do its secrets live?
Security and safety. This is a defensive, authorized-lab-only lesson. Any vulnerable example is labeled and meant solely for a local, isolated, authorized environment — never deploy it, and never test these ideas against systems you do not own or have written permission to assess.
Threat model (text):
ASSETS: audit logs (the evidence), IAM roles, secrets, data in buckets/DB
TRUST BOUNDARY 1: provider control plane | your account/config
TRUST BOUNDARY 2: the internet / event sources | your function code
ENTRY POINTS: event triggers (HTTP, queue, storage), the management API,
the function's dependencies (supply chain)
ADVERSARY GOAL: act without being seen -> disable/edit logs;
or escalate -> abuse an over-privileged function role
Defensive practices, in priority order:
StopLogging, DeleteTrail, and new admin grants.Detection and logging guidance: log the identity, action, resource, source IP, and outcome of sensitive operations and of rejected events. Never log secrets, tokens, passwords, full request bodies, or PII. Use placeholders like API_KEY=<development-placeholder> in any examples or docs.
Authorization/ethics note: only experiment in your own cloud sandbox, a provider free tier you control, or an intentionally vulnerable lab. Disabling or modifying logging, or probing functions, on systems you are not authorized to test is unethical and likely illegal.
Concrete uses.
Professional best-practice habits.
Beginner rules:
Advanced habits:
*:* policy) and auto-alert.Beginner 1 — Spot the logging gaps. Objective: given a description, list the logging weaknesses. Scenario: "CloudTrail is enabled in us-east-1; logs stay in the production account; no alerts are configured." Requirements: name at least three distinct findings and a fix for each. Hint: think region coverage, centralization, tamper-resistance, alerting. Concepts: audit logging, common findings.
Beginner 2 — Classify the action.
Objective: sort a list of operations into control-plane vs. data-plane. Input example: CreateUser, read object from bucket, ModifySecurityGroup, run a SELECT query, DeleteTrail. Requirements: label each and state which ones you would most want alerts on and why. Hint: management vs. usage. Concepts: control plane vs. data plane.
Intermediate 1 — Tighten a function role.
Objective: rewrite an over-broad IAM policy. Input: a policy granting "Action": "*", "Resource": "*" to a function that only reads from uploads-bucket and reads one secret. Requirements: produce a least-privilege policy listing exact actions and named resources; explain what an attacker can no longer do. Constraint: no wildcards in Action; Resource must be specific. Hint: compare against the policy in the code section but write your own. Concepts: least privilege, blast radius.
Intermediate 2 — Add input validation.
Objective: given a function that uses an event's key field directly to build a file path, describe the risk and write validation pseudocode. Requirements: reject empty keys, over-long keys, and any .. sequence; log rejections without logging the raw payload. I/O example: input key ../../etc/passwd -> rejected and logged with reason invalid object key. Hint: validate then act; never sanitize-and-hope. Concepts: untrusted event inputs, safe logging.
Challenge — Design a minimal serverless detection plan. Objective: for a small image-processing Lambda, write a one-page defensive plan covering logging, IAM, secrets, inputs, and alerting. Requirements: (a) where logs go and how they are protected; (b) the function's least-privilege role; (c) where secrets live; (d) two specific alert rules with severities; (e) one test that verifies each control works. Constraints: tamper-resistant logs in a separate account; no secrets in env vars; alerts must route to a human. Hint: reuse the threat model from the safety notes to make sure every asset and entry point is covered. Concepts: all of the above, integrated. (Design only — do not deploy against any real account you are not authorized to use.)
Main concepts. Cloud audit logs (CloudTrail, Azure Activity Log, GCP Audit Logs) record control-plane API calls — who did what, to what, from where, when — and are usually the authoritative record of activity in an account. They are the cloud's detection backbone, which is exactly why attackers try to disable them early; that attempt is itself a high-signal logged event. Serverless removes host management but concentrates security onto two surfaces from the IAM lesson: per-function permissions and untrusted event inputs.
Most important "syntax." On any function policy, check that Action is specific (never * or service:*) and Resource is scoped to named resources. On logging, ensure there is an alert on StopLogging/DeleteTrail/new-admin grants, not just collection.
Common mistakes. Assuming logging is complete when it covers one region; granting broad roles "to make it work"; putting secrets in environment variables; trusting event inputs; and collecting logs without alerting on them.
What to remember. Protect and centralize logs first; give each function the minimum IAM role; validate every input; fetch secrets at runtime; minimize dependencies; and only ever experiment in environments you own or are authorized to test.