The situation
A payments-capable web app, structure:
Browser -> CloudFront -> ALB -> ECS (web) -> Internal fraud service -> External payments processor
The checkout form POSTs JSON with {pan, cvc, name, email, billingAddress, ...} to /checkout. Every downstream component currently sees all fields in plaintext. The compliance impact is:
- PCI scope. Every component that processes cardholder data is in PCI scope. The web tier, the fraud service, the APM agent, application logs, and the ECS task definitions are all in scope because they potentially see the PAN. In-scope components need PCI DSS controls applied.
- Leak surface. A bug in the web tier that inadvertently logs the request body leaks cardholder data. A misconfigured APM agent captures cardholder data in span attributes. A compromised ECS task has access to cardholder data.
The desired end state: only the payments processor sees the PAN and CVC in plaintext. Every other component sees ciphertext that they cannot decrypt. PCI scope narrows to just the edge (CloudFront) and the processor; the web tier and fraud service are out of scope for PAN.
CloudFront Field-Level Encryption (FLE) is the AWS feature for this.
What actually matters
TLS is point-to-point encryption between hops; it protects nothing once the connection terminates. Every time a TLS connection ends (at the edge, at the ALB, at the application) the data is in plaintext memory on that component. Any component that holds plaintext is in scope. Narrowing scope means ensuring components in the middle of the chain can’t decrypt the data in the first place.
Field-level encryption does this by encrypting specific fields in the request body at the very first hop the client’s request reaches (CloudFront’s edge) with a public key whose private counterpart is held only by the component that legitimately needs to read it. The fields the customer doesn’t want to leak are encrypted before any other component sees them. The other fields (non-sensitive metadata, routing info, identifiers) stay in plaintext so the rest of the pipeline can still operate.
Field-level encryption is not a replacement for TLS; it’s an additional layer. TLS still wraps the whole request on the wire. FLE encrypts specific fields within the TLS-wrapped body so that when TLS terminates, the sensitive fields remain ciphertext inside the body.
The cryptographic shape is straightforward: an asymmetric algorithm where a public key is held by the edge and the corresponding private key stays with the component that needs to decrypt. The edge processes each incoming request: it parses the body, finds the configured fields, encrypts each one’s value with the public key, and reinserts the ciphertext into the body. The request continues through the pipeline with the sensitive fields now as ciphertext strings.
The component holding the private key decrypts the fields when it receives them. It’s the only component with the key; everything in between (excluding edge caches for sensitive requests, the load balancer, the web tier, the fraud service) either doesn’t handle those fields at all, or treats them as opaque ciphertext strings.
What we’ll filter on
- Encryption scope. Whole body, specific fields, or nothing?
- Key custody. Where does the private key live?
- Algorithm and key length. RSA-OAEP 2048, 4096, other?
- Body format support. JSON, form-url-encoded, multipart?
- PCI scope reduction. Does it take middle components out of scope?
The field-protection landscape
1. CloudFront Field-Level Encryption (FLE). Edge-level field encryption. Public key uploaded to CloudFront; sensitive fields encrypted at the edge; ciphertext flows through the rest of the pipeline. Algorithm: RSA-OAEP-SHA-256. Key length: 2048-bit (and some higher). Supports application/x-www-form-urlencoded and application/json body formats. Free to configure; no per-request charge beyond normal CloudFront.
2. Application-level field encryption in the web tier. The web tier itself encrypts fields before passing them downstream. Works, but the web tier is still in scope because it holds plaintext briefly. Moves the boundary one hop downstream, doesn’t remove it.
3. Payments tokenisation services (Stripe.js, Braintree Hosted Fields, etc.). The browser sends the cardholder data directly to the payments processor’s servers (via the processor’s JavaScript SDK), which returns a token; the token is posted to the merchant’s backend. The merchant never sees the PAN at all. Widely used in real payments integrations, frequently the correct answer when the merchant is integrating with a PCI-Level-1 processor that provides this SDK. Not CloudFront-native; implemented by the merchant’s front-end code.
4. End-to-end encryption protocols (mTLS-to-processor, VPN to processor’s VPC). Protect the transport from edge to processor but still leave plaintext at the edge and at any intermediate hop that terminates the protocol.
5. Application-level tokenisation with AWS Payment Cryptography. AWS Payment Cryptography is a managed HSM-based service for issuing and validating payment tokens. Complementary to FLE; used inside the payments processor or merchant to handle tokens, not for edge encryption.
Side by side
| Option | Scope | Key custody | Algorithm | Body formats | PCI scope impact |
|---|---|---|---|---|---|
| CloudFront FLE | Specific fields | Merchant (uploaded pub, held private) | RSA-OAEP-SHA-256 | JSON, form-urlencoded | Narrows to edge + processor |
| App-level encryption | Specific fields | Merchant | Any | Any | Web tier stays in scope |
| Processor-provided SDK (Stripe.js etc.) | Entire PAN | Processor only | Processor-defined | N/A (browser-direct) | Merchant fully out of scope |
| mTLS to processor | Full body | Both endpoints | TLS | N/A | Edge + hops still in scope |
| AWS Payment Cryptography | Tokens | HSM-managed | Payment-specific | N/A | Complementary |
Reading the table: Processor-provided SDKs are the narrowest scope (the merchant never sees the PAN at all). CloudFront FLE is the answer when the merchant must receive the PAN but wants to minimise the components that process it in plaintext.
Where the ciphertext forms, and where it thaws
The picks in depth
Key management. Generate an RSA 2048-bit key pair offline. Upload the public key to CloudFront as a public key (via aws cloudfront create-public-key). Store the private key on the payments processor’s side, ideally in a CloudHSM cluster or a KMS custom key store in the processor’s account, with access controlled to the decrypt handler only. The public key CloudFront holds is metadata; compromise of the public key is not a security issue because it’s public. The private key is the sensitive artefact; guard it like the root of a PKI.
FLE configuration. Create a field-level encryption profile that names the public key and lists the fields to encrypt:
{
"Name": "checkout-pan-cvc",
"CallerReference": "2028-08-20",
"EncryptionEntities": {
"Items": [{
"PublicKeyId": "K2JCJMD3X…",
"ProviderId": "payments-processor-v1",
"FieldPatterns": {
"Items": ["pan", "cvc"]
}
}]
}
}
Then create a field-level encryption configuration that references the profile and maps content types:
{
"Name": "checkout-config",
"CallerReference": "2028-08-20",
"ContentTypeProfileConfig": {
"ForwardWhenContentTypeIsUnknown": false,
"ContentTypeProfiles": {
"Items": [{
"Format": "URLEncoded",
"ProfileId": "<profile-id>",
"ContentType": "application/x-www-form-urlencoded"
}, {
"Format": "URLEncoded",
"ProfileId": "<profile-id>",
"ContentType": "application/json"
}]
}
}
}
Attach the configuration to the specific cache behavior (the one matching /checkout*). FLE only applies to that behaviour’s requests.
Origin handling. The origin (ALB → web tier) receives the request with pan and cvc as base64-encoded RSA-OAEP ciphertext strings. The web tier doesn’t decrypt; it treats them as opaque strings and forwards to the payments processor. The processor (which might be an external service or an internal bounded context in an isolated account) holds the private key, decrypts the fields, processes the transaction, returns a token. The web tier and fraud service never see the plaintext.
Non-sensitive fields flow unchanged. Email, name, billing address, order ID, currency, and amount aren’t in the encryption profile, so CloudFront passes them through in plaintext. The fraud service reads them to score the transaction; the web tier uses them for logging and receipts. Nothing in the non-sensitive path needs to know FLE exists.
A worked request
Browser submits the checkout form:
POST /checkout HTTP/1.1
Content-Type: application/x-www-form-urlencoded
pan=4111111111111111&cvc=123&name=Alice&email=alice@example.com&amount=19.99
CloudFront receives, matches the /checkout* cache behaviour, matches the content type to the FLE profile, encrypts pan and cvc with the uploaded public key, base64-encodes the ciphertexts:
pan=AQEB...base64...==&cvc=AQEB...base64...==&name=Alice&email=alice@example.com&amount=19.99
ALB receives, TLS-terminates, forwards to web tier. Web tier parses the form, reads email/amount/name for session and logging, serialises the full form (including the encrypted pan/cvc) into a POST to the payments processor. Processor receives, reads the encrypted fields, decrypts with its private key, charges the card, returns a token. Web tier stores the token in the order record; responds to the browser with a thank-you page.
At no point between the edge and the processor did any component hold the PAN or CVC in plaintext.
A worked audit
The PCI auditor asks “demonstrate that the web tier is out of scope for PAN.” The evidence:
- CloudFront FLE configuration showing
panandcvcare in the encryption profile for the/checkout*behaviour. - CloudTrail
CreatePublicKeyevent showing the public key was uploaded on a specific date from a specific admin role. - Application logs from the web tier during a test transaction, showing the encrypted blob for
pan(e.g.AQEB...==) in the request body but no plaintext PAN anywhere in the log. Grep the logs for the known test PAN; no results. - APM span data showing the encrypted blob, not plaintext.
- Runbook demonstrating the private key lives in the processor’s CloudHSM cluster, with a separate audit trail for key-use.
Scope reduction argument: the web tier handles only ciphertext; the ciphertext is useless without the private key which the web tier does not have access to; therefore the web tier does not process cardholder data.
What’s worth remembering
- CloudFront Field-Level Encryption encrypts specific POST fields at the edge. RSA-OAEP-SHA-256 with a customer-uploaded public key; the private key stays with the decryption endpoint.
- Configuration is in two parts: profile and configuration. The profile names fields + public key; the configuration maps content types + profile to cache behaviours.
- Only
application/jsonandapplication/x-www-form-urlencodedbody formats are supported. Multipart uploads are not field-level encrypted by FLE. - FLE disables CloudFront caching on matched requests. Sensitive POSTs are never cacheable anyway; FLE enforces it.
- FLE does not replace TLS. TLS still wraps the whole request; FLE encrypts specific fields within the body.
- Private-key custody is the critical decision. HSM-held private key for PCI workloads; strict access controls; audit on key-use.
- Processor-provided SDKs (Stripe.js, Braintree, etc.) are often the simpler path. If the processor offers a client-side tokenisation library, it removes the merchant from PCI scope entirely, a stronger reduction than FLE.
- Non-sensitive fields stay plaintext through the pipeline. Email, billing address, order ID flow in plain so downstream services can continue to function.
Encrypt the field, not the request. Decrypt only where necessary. The middle of the stack holds ciphertext it cannot read, and the PCI scope follows the plaintext.