Cloud security engineer auditing CDN cache keys and personalized response isolation on multiple monitors

Who Gets the Cached Response? Audit CloudFront User Isolation

Tue, Sep 22, 2026

When a CloudFront distribution serves responses, does every user see only the content intended for them? Imagine an owned staging API where User A logs in and fetches /profile; if User B later fetches the same path, a misconfigured shared cache might hand User B a copy of User A's profile response. A prior viewer's private data could therefore be served from an edge cache to another identity. Our goal is to prove or disprove that scenario with evidence: what varies between users, how the cache key treats those differences, and whether any stored response can cross the intended audience boundary. We stay within one owned staging distribution and custom HTTP origin, using inert synthetic identities and endpoints.

Before sending test traffic, classify each response as public, tenant-scoped, or private. Public content may be shared with every viewer. Tenant-scoped content may be shared only with authorized members of one tenant. Private content belongs to one viewer or one request. The proposed /public-info endpoint is the positive control, while /profile is treated as private. If User A's profile marker can reach User B, reject the configuration. For broader acceleration context, see API caching and performance tradeoffs; this playbook focuses on confidentiality rather than hit ratio.

The playbook catalogs the deployed configuration, traces which viewer attributes reach the origin versus the cache key versus the application's authorization checks, runs controlled two-user sequences, and interprets the result. AWS guidance on the CloudFront cache key provides the documented policy boundary. Every link in the evidence chain must be repeatable: policy revision, request order, returned marker, edge headers, and origin correlation. The sequences and example rows below are proposed laboratory evidence, not a claim that live CloudFront tests have already been completed. If a run reveals an unsafe shared hit, hold the release, repair both the policy and previously cached objects, then repeat the original test unchanged.

Decide which responses may be shared

First, define the owner and intended audience for each response class. A public control endpoint returns the same content to everyone. A tenant-scoped endpoint returns content that may be shared only among authorized members of one tenant. A private endpoint returns user-specific or request-specific content, such as a profile or one-time value. Shared caches are acceptable only for genuinely public output or a separately verified scoped-sharing contract. Private responses should never be stored in a shared CloudFront cache.

Response class

Owner or subject

Permitted audience

Cache decision

Required evidence

Public

No individual subject

Every viewer

Allow shared caching

Same body and headers for both identities; public control behaves as declared

Tenant-scoped

Tenant or organization

Authorized members of one tenant

Allow only with verified scoped-sharing contract

Trusted partition, authorization valid on hits, no cross-tenant marker

Private

Individual viewer or request

One viewer or request

Disable caching

Every request receives the correct marker; no stored personalized response

To establish the baseline, list each endpoint and its category in the API contract. For the proposed staging service:

  •         GET /public-info: Returns one fixed public message. Owner: content team. Audience: everyone.

  •         GET /profile: Returns JSON whose synthetic subject field matches the authenticated identity. Owner and audience: that identity only.

  •        Other endpoints: Classify them separately. For example, a tenant report may be tenant-scoped, while a reset secret remains private.

Record the classification table before testing. Gather evidence from CloudFront response headers, origin logs, and the inert response markers. In the synthetic lab, a marker mismatch is a test failure; in a production equivalent, unauthorized sharing would also enter the organization's approved incident-handling process.

Freeze the deployed distribution and origin contract

Before testing, take an immutable snapshot of the effective staging setup. Record the exact distribution ID and deployed revision, matched behavior, cache-policy ID and complete body, origin-request-policy ID and complete body, minimum/default/maximum TTLs, origin code revision, synthetic cookie contract, and every attached edge function revision. The manifest below uses synthetic example values; the executed evidence must contain the actual deployed identifiers.

Configuration field

Synthetic example or required value

Distribution ID and deployed revision

E3ABCXYZ1EXAMPLE; record the actual revision

Personalized behavior path

/profile*

Cache policy

Record ID, name, complete body, and minimum/default/maximum TTLs

Origin request policy

Record ID, name, and every forwarded header, cookie, and query value

Origin

Owned custom HTTP staging origin; record build or commit ID

Edge functions

Record each function and revision, or state none

Synthetic identity contract

sessionId cookie; values supplied at runtime and redacted

Public control

/public-info; no authentication required

Personalized endpoint

/profile; synthetic authentication required

Authorization

Origin maps each synthetic session to one subject marker

Freeze the manifest in version control before any test traffic. Development, security, and reliability owners should agree which build is under review. Record who owns the distribution, the origin, the test fixture, the evidence store, and the repair decision. Also record redaction rules that prevent credentials or customer data from entering logs and screenshots. This focused baseline complements broader AWS security foundations, but acceptance depends on the exact deployed objects listed here.

Configuration check: verify the path patterns through the CloudFront cache behavior settings. Confirm which cache and origin-request policies are attached, and record default, minimum, and maximum TTL values. For the synthetic sessionId cookie, document whether it is forwarded and whether it participates in the cache key. The active distribution must match the manifest exactly before testing. If it does not, stop, refresh the snapshot, and obtain the authorized revision.

Do not test against broken routing. The public endpoint must return its shared marker, and the origin must enforce authentication on /profile even though the data is synthetic. Add a unique non-secret correlation ID to each response and origin log entry. A proposed profile handler might return {"subject": "Alice", "marker": "PROFILE-A"} for User A and the corresponding B marker for User B. These inert values identify whose response arrived without exposing real data.

Separate forwarding, keying and authorization

CloudFront processing separates three concerns: forwarding, which determines what reaches the origin; keying, which determines what distinguishes cached objects; and authorization, which determines whether an application or trusted edge layer permits the request. These are distinct review objects. Build a ledger for every viewer attribute before interpreting any hit or miss.

Request attribute

Reaches origin?

Participates in cache key?

Participates in authorization?

Path: /profile

Yes

Yes

Route requires authenticated subject

Query strings

Only when the effective policy forwards them

Only when the cache policy includes them

Application-dependent

sessionId cookie

Only when forwarded

Only when explicitly included

Yes in this synthetic contract

Authorization header

Configuration-dependent

Configuration-dependent

Only if the application uses it

Accept-Language

Configuration-dependent

Configuration-dependent

No in this fixture

Viewer-supplied X-Tenant-ID

Only when forwarded

Only when included

No unless a trusted layer validates it

Under AWS cache-key guidance, a cache policy determines which request values distinguish cached objects. A separate origin request policy can forward additional headers, cookies, and query strings without adding them to the cache key. Forwarded-only values reach the origin on a miss, but they do not create separate cached representations. For example, an authentication attribute can reach the origin while the stored object remains shared by every viewer whose request produces the same key.

Forwarding an identity, such as a user cookie, does not prevent caching. It lets the origin authenticate a cache miss. If the cache key ignores that identity, two users can match the same stored object even though the origin would authorize them differently. A cache hit can therefore return an object originally authorized for someone else without executing a new origin check. Origin logs alone are insufficient; the review must compare client-visible markers, cache evidence, and whether the origin handled each request.

Treat the cache policy and origin request policy separately. For every cookie, header, and query value, write down whether it reaches the origin, participates in the cache key, and participates in authorization. Capture the application assumptions alongside API authentication and response contracts. A viewer-supplied tenant label is not trusted identity merely because it appears in the key.

AWS documentation for origin requests explains that values included in the cache key are also sent to the origin, while an origin request policy can send additional values that do not vary the cache. An unsafe combination is therefore possible: the origin receives a user identifier on misses, but the cache policy stores one response for every viewer of the same URL. Compare the deployed policy bodies with all three ledger columns before testing.

Build a controlled two-identity fixture

Create two synthetic users, Alice and Bob, with distinct opaque credentials recognized only by the staging origin. Refer to the credentials as tokenA and tokenB; do not retain their values in the evidence bundle. Each user's /profile response contains a harmless marker such as PROFILE-A or PROFILE-B. The public endpoint /public-info returns one fixed marker, PUBLIC-OK, to everyone.

Keep identity and response markers separate

Ensure that the credential labels and response markers are independent. Supply credentials at runtime rather than embedding them in scripts or screenshots. Retain only controlled identity labels, redacted fingerprints, correlation IDs, and the harmless markers. The origin must reject a request without a valid synthetic identity so that each profile marker is known to have passed the fixture's authorization check on an origin fetch.

Make the public endpoint a positive control

The /public-info route must demonstrate intentional response sharing. Configure it without personalized inputs, then verify that the first request can reach the origin and a later request can reuse the same public marker when caching is enabled. If the control returns a 403, different markers, or no origin response, the fixture is invalid. A private endpoint that never reaches the origin because routing is broken is not evidence of isolation.

The staging origin should log every request with a unique non-secret request ID, the synthetic identity label, and the marker returned. For each client observation, correlate that ID with the origin. A cache hit should not generate a fresh origin entry, but the body and response headers still reveal which stored representation the viewer received. Keep the marker convention simple: public content uses PUBLIC-OK, Alice uses PROFILE-A, and Bob uses PROFILE-B.

Run both cold-cache and warm-cache orders

Run three sequences under the frozen configuration, with no policy changes between requests. Prepare a known cache state for the tested URL, execute the viewers in both warming orders, and include a same-identity control.

  •         Order 1, A then B: request /profile as Alice from a cold state, request the same URL as Bob, then repeat Alice to check consistency.

  •         Order 2, B then A: request /profile as Bob from a cold state, then request the same URL as Alice.

  •         Same-identity control: request the same URL twice as Alice or twice as Bob to confirm marker stability and the expected cache behavior without an identity switch.

Keep the URL exactly /profile; do not add a random query string that changes the cache key. Record UTC timestamps, distribution revision, HTTP status, body marker, X-Cache, Age, X-Amz-Cf-Pop, correlation ID, and whether the origin logged a request. Keep the client location bounded for each run and record the actual point of presence returned by CloudFront rather than assuming a regional cache location.

Use a result table like the following. The rows describe expected evidence fields, not executed observations.

Sequence / request

Viewer

Expected cache evidence

Expected marker

Expected origin evidence

Acceptance decision

A then B / 1

Alice

Cold miss or fresh fetch

PROFILE-A

Origin records Alice

Baseline

A then B / 2

Bob

Policy-dependent hit or miss

PROFILE-B

Origin records Bob if miss

Reject if PROFILE-A is returned

A then B / 3

Alice

Same-identity control

PROFILE-A

Policy-dependent

Marker must remain correct

B then A

Reverse the viewers

Repeat the same evidence fields

Each viewer receives own marker

Correlate every miss

Reject any cross-identity marker

In an isolated configuration, Bob's request must return PROFILE-B, not Alice's marker. Under the primary non-caching branch it should reach the origin. Under any separately justified scoped-sharing branch, a hit is acceptable only when the stored response is authorized for Bob by the verified contract. If Bob receives PROFILE-A, reject the configuration and confirm the interpretation with origin correlation: a cache hit should show no new origin request for Bob. Repeat the same reasoning for the reverse order.

Begin each cold sequence only after a scoped invalidation for /profile has reached Completed, or use a fresh disposable distribution revision approved for the lab. Keep /public-info and neighboring controls unchanged. Do not use unique query parameters merely to force misses, because doing so changes the key and can conceal the defect.

Never treat one header as universal proof of cache state. X-Cache: Hit from CloudFront and a positive Age value are client-side signals; correlate them with the origin request log and distribution revision. A hit with no new origin request supports a stored response. A miss should have a matching origin fetch unless an edge function, error response, or logging gap explains the difference.

Interpret completed rows with these acceptance rules:

  •         Pass only when every viewer receives the marker permitted by the response-sharing contract and the edge and origin evidence agree.

  •         Fail immediately when a viewer receives another identity's body marker, a sensitive response header crosses identities, or the expected origin correlation is missing.

Run the same cold/warm sequence against /public-info. Both identities should receive PUBLIC-OK, and the warm request may legitimately be served from cache. This positive control proves that the fixture can observe intentional sharing and that a routing failure is not being mistaken for isolation. Record inconsistent status codes, missing headers, or unexpected origin calls as test-setup defects.

Test TTL settings against origin privacy headers

CloudFront TTL settings and origin response directives must be reviewed together. Test one deliberate defect at a time. AWS cache behavior documentation warns that a minimum TTL greater than zero can make CloudFront cache content for at least that duration even when the origin sends Cache-Control: no-cache, no-store, or private. A minimum TTL of zero removes that forced floor, but it does not by itself disable caching for every response; positive default or maximum settings can still permit caching when the origin response is cacheable or lacks restrictive directives. Compare these two proposed branches.

1.     Defect reproduction in disposable staging: set the minimum TTL to 60 seconds, retain positive default and maximum TTLs, and return Cache-Control: private, no-store from /profile. The documented expectation is that CloudFront can retain the object for the minimum TTL. Send the second identity within that interval and stop the run immediately if it receives the first identity's marker or Set-Cookie value.

2.     Non-caching branch: set minimum, default, and maximum TTLs to zero for the personalized behavior, while retaining the origin's restrictive directives as defense in depth. The expected result is a fresh origin authorization for every request. Any personalized cache hit, stale Age value, cross-identity marker, or replayed synthetic cookie is a failure.

Record the expected result and stop condition before applying either policy:

Scenario

TTL policy

Origin response directive

Expected result

Immediate reject condition

Defect reproduction, disposable staging only

Minimum 60s; positive default and maximum

private, no-store

Object can remain cached for at least the minimum TTL

Second identity receives the first marker

Non-caching branch

Minimum 0; default 0; maximum 0

private, no-store

Every personalized request reaches origin authorization

Any personalized cache hit or marker mismatch

If execution contradicts the documented expectation, preserve the mismatch and hold the release rather than silently reconciling it. Confirm the actual matched behavior, attached policy revision, edge functions, and response directives. Do not promote the deliberately permissive policy beyond the disposable lab. The primary repair is an explicitly non-caching policy for /profile, followed by removal of previously stored objects and a repeat of both warming orders.

Repeat the cold/warm test under the fixed manifest. If the configuration says caching is disabled but a personalized request still returns a hit, the repair is not proven. Treat the result as a policy, behavior, edge-logic, or stale-object mismatch and keep the release on hold.

Inspect headers and key dimensions, not only the body

The body marker is the primary signal, but it is not the only response element that can cross identities. Inspect response headers and declared variation as well. The synthetic origin may return X-Synthetic-Subject, Set-Cookie, Vary, Cache-Control, and other controlled headers. Compare each value across both warming orders and preserve only redacted evidence.

Check cookie response handling

Use one inert synthetic cookie for the sub-test. AWS cookie-handling documentation describes how Set-Cookie can be cached and replayed when cookies are forwarded. Fetch /profile cold, then warm the same URL with the other identity and compare the response headers. Test the exact modern cache-policy and origin-request-policy combination in the manifest. Do not generalize from legacy forwarding options or assume that changing request-cookie forwarding alone proves safe response-header handling.

Reject untrusted identity partitions

A cache partition label is not an authorization decision. If X-Tenant-ID or X-User is supplied by the viewer, the viewer can choose its value unless a trusted component derives and validates it. Adding that string to the cache key may separate stored objects, but it does not prove entitlement to the selected partition. The origin or another independently verified layer must enforce membership. See server-side authorization foundations for the application-side boundary.

Capture Set-Cookie, Vary, Cache-Control, Age, and relevant CloudFront headers for every request. Remove credential values before retention. A header that contains another identity's marker, a replayed synthetic cookie, or a declared response variation absent from the key rejects the sharing contract. Age and X-Cache support the interpretation, but they do not replace origin correlation.

Account for behavior precedence and edge rewrites

Verify that each request matched the intended cache behavior. A distribution can contain overlapping path patterns, a default behavior, URI normalization, and edge functions that rewrite the path or key inputs. Under the cache behavior path-matching rules, CloudFront evaluates behaviors in their configured order and applies the first matching path pattern, with the default behavior last. Do not assume that the most specific-looking pattern automatically wins.

Test the intended route and a bounded set of neighboring controls, such as /profile, /profile/view, and /public-info. Record any normalization or rewrite performed by CloudFront Functions, Lambda@Edge, or the origin. Freeze every unrelated variation so the experiment introduces only one policy defect at a time. If an edge function is attached, record its revision and account for its effect rather than silently disabling it.

State the effective match explicitly: which behavior handled /profile, which handled descendants, and whether the origin saw the original or rewritten path. If the observed match differs from the frozen manifest, stop and refresh the baseline before interpreting any cache result. A neighboring behavior with different TTLs can otherwise create a false pass or false failure.

This is not a full distribution architecture survey. Limit the control set to the personalized route, the public endpoint, and neighboring paths that could mask the result. Leave unrelated behaviors unchanged and mark them out of scope. The public control should fail the setup check if it accidentally matches a non-caching or authenticated behavior.

Choose between no caching and justified sharing

After the tests, choose between no caching and justified sharing. The default safe choice for personalized output is no caching. A shared-cache branch is acceptable only when the permitted audience, cache partition, and authorization behavior on a cache hit are independently verified. The desired hit ratio is not part of the confidentiality decision.

  •         Public responses: Allow shared caching when the body and response headers are identical for every viewer and no authentication-dependent value is present.

  •         Scoped shared responses: Allow only when a trusted identity or tenant partition is bound to the response and authorization remains effective before a cached object is returned. The design must document who controls the partition, how entitlement is validated, how revocation is handled, and which fields are identical within the audience. An arbitrary viewer header is not sufficient.

  •         Personalized or private responses: Disable CloudFront caching for the matched behavior by setting minimum, default, and maximum TTLs to zero, retain restrictive origin directives as defense in depth, and verify both warming orders.

Use the following decision table during release review:

Response category

Condition for shared caching

Action when condition is absent

Public

Body and headers are identical for every viewer

Allow public caching

Scoped shared variant

Trusted partition plus authorization that remains valid on cache hits

Disable caching and hold the shared design

Personalized or private

No shared audience contract

Disable caching and verify both warming orders

Origin directives such as Cache-Control: private are valuable defense in depth, but they do not replace the distribution policy. A positive minimum TTL can override them, and a zero minimum TTL alone does not mean all caching is disabled. The safest private-response branch combines an explicitly non-caching policy with CloudFront invalidation when older objects may exist. The performance tradeoff is acceptable when the alternative is uncertain response isolation.

A justified sharing design needs more than a user value in the key. Evidence must show that the partition is derived from trusted identity, that a viewer cannot select another audience, that authorization remains valid for cache hits, and that response bodies and headers are identical within the scoped group. If any element is unresolved, disable caching and hold the shared design for separate security review.

Repair configuration and previously cached responses

If the test discovers unsafe sharing, contain the route first. Attach the authorized non-caching policy to the exact personalized behavior, record the old and new policy IDs and bodies, confirm the deployed distribution revision, and preserve the original evidence. A policy change affects future cache decisions; it does not prove that previously stored representations have disappeared or that a response already delivered to a viewer can be withdrawn.

Verify invalidation or an approved version change

Use the AWS invalidation workflow or an approved version change to address stored objects. Record the operation ID, exact path scope, submission time, completion state, and distribution revision. Wait for Completed before beginning the fresh test series. If versioning changes the URL, retain evidence for the old URL as well; a safer policy or new path does not demonstrate that every earlier object is gone.

After removal completes, repeat the cold/warm sequence under the fixed manifest. Do not rely on an arbitrary waiting period. Update the evidence table with the new run ID and configuration digest. If either warming order exposes an earlier marker or returns a personalized hit under the non-caching branch, the route remains on hold.

Retest without masking the original failure

Use the same URL, synthetic identities, warming orders, headers, public control, and neighboring routes that exposed or could have exposed the defect. Do not add a query parameter, change the path, or filter the response in a way that masks the original condition. Keep the failure evidence next to the repair evidence so reviewers can see that the test, not the scenario, changed. Any unexplained mismatch remains a hold.

If real data may have reached an unauthorized viewer, preserve the technical evidence and engage the designated incident, privacy, and legal owners to determine the required response. This playbook does not invent notification obligations. In the synthetic lab, record only controlled markers, UTC timestamps, request IDs, and containment actions.

Assemble an evidence bundle that survives review

Compile a compact evidence bundle that another reviewer can reconstruct. Each run needs a run ID, configuration digest, distribution revision, identity label, sequence, cold or warm state, expected marker, observed marker, status, cache headers, point of presence, correlation ID, and origin-request evidence. Store raw outputs only in an approved location and redact all credentials.

Run ID

Configuration digest

Sequence

Client observation

Origin correlation

Assessment

R1-A-B

abc123 (synthetic)

Alice then Bob

Bob received PROFILE-A on a hit

Origin recorded Alice only

FAIL, synthetic example

Separate expected results from executed observations. The configuration digest can be a version-controlled policy checksum or infrastructure commit. Link each client row to edge and origin records. Use logs, metrics and traces for operational evidence to correlate the layers, while remembering that a missing origin request is expected for a cache hit and does not by itself reveal which response the viewer received.

Preserve negative results. A synthetic example could state: Run R1: Bob received PROFILE-A on a cache hit; no new origin request was recorded for Bob. Label that row as a failure. If a required policy body, edge-function revision, or log is missing, record the gap and hold the decision rather than converting uncertainty into a pass.

Use client, edge, and origin evidence together because a cache hit does not execute the origin's authorization code. A sanitized CloudFront log can identify the request and edge location, while the origin log confirms which misses were authorized. Keep all user and session values synthetic or hashed, and never retain real session material in screenshots or exports.

The evidence is self-contained only when it identifies the tested scope, exact configuration, proposed or executed status, identities, steps, observations, and decision owner. Add a concise assessment for each run: pass, fail, or hold, followed by the approved repair or release action.

Automate the isolation gate and its negative controls

Automate the same two-identity sequences in owned staging. A Python or shell gate can accept the distribution URL and synthetic credentials at runtime, request both warming orders, and exit nonzero for any cross-identity body or header, unexplained personalized hit, missing public control, or configuration mismatch. Keep expected inert markers in the fixture, but do not embed credential values in source code or retained logs.

Grant the automation only the permissions needed to read the frozen distribution configuration, invoke the test endpoints, inspect authorized logs, and create a scoped invalidation when the lab owner permits it. Parameterize the distribution and endpoint values so the script cannot drift to another account. Record every mutation and clean up synthetic state after the run.

Include a deliberately unsafe policy only as a disposable negative control. The gate must fail when that policy omits the synthetic identity from the sharing contract and the second viewer receives the first marker. Keep the unsafe variant isolated from production, prevent the pipeline role from attaching it outside the controlled lab, and never scan a third-party distribution.

A successful safe run is not enough. Periodically execute the negative control to prove that the assertions still detect the modeled fault. Promotion is allowed only when the safe configuration passes, the unsafe fixture fails as expected, cleanup succeeds, and the evidence bundle is retained.

Approve, disable, hold or escalate with an owner

Record one of four outcomes and assign a decision owner:

Decision

Technical condition

Next step and owner

Approve

Verified public or scoped-sharing contract, or verified non-caching branch

Release owner signs the tested scope

Disable

Personalized sharing is unsafe or not justified

Platform owner disables caching and retests

Hold

Policy, behavior, response, or evidence mismatch remains

Release reviewer pauses promotion and gathers evidence

Escalate

Suspected real unauthorized exposure

Security or incident owner begins the approved response process

Approve only the tested public or scoped-sharing contract, or the verified non-caching branch. Disable caching when private content could be shared or the authorization model is not independently established. Hold when configuration, behavior matching, response headers, or evidence disagree. Escalate suspected real exposure to the designated security and incident owners.

Tie every outcome to technical evidence, not a generic assurance. A review record might state: Run R1 failed because Bob received PROFILE-A on a hit and the origin logged no request for Bob. A repaired record might state: Run R2 used the non-caching policy; both warming orders reached the origin and returned the correct marker. Use only statements supported by an executed run; do not insert invented dates or results.

Scope the final statement narrowly. Identify the endpoint, distribution revision, policy bodies, identities, client locations, and sequences covered. If the evidence shows a failure, state that the tested configuration did not isolate the response. If the repaired run passes, state only that no cross-identity response was observed within that tested scope. Do not claim that the entire service is secure, and do not claim a pass when the playbook contains only proposed tests.

Keep the response-sharing contract in the release checklist

Add one release item for every personalized route: verify the matched CloudFront behavior, cache policy, origin request policy, TTLs, edge logic, and two-identity warming orders. Re-run the gate after any change that can alter the cache key, forwarded attributes, response headers, authorization contract, or stored-object state. This is a correctness check, not a performance benchmark.

If caching is allowed for a scoped audience, keep the response-sharing contract with the release evidence. If the contract is absent, use the non-caching branch. A high cache hit ratio is not a security metric, and origin authentication alone does not protect a request that CloudFront fulfills from a shared cached object.

Refonte Learning's Cloud Security Engineer Program is described as a three-month programme requiring 10-12 hours per week, with published topics including cloud-security architecture, identity and access management, encryption, threat detection, incident response, and security monitoring. Review the published curriculum and certificate details to see how those foundations relate to this kind of delivery-security work.