Bridging On-Prem BankCore to AWS: Building a PCI-DSS Tokenization Platform
For most of the second half of 2025 and into early 2026, I was building a credit card tokenization platform at Telered. The shape of the problem will sound familiar to anyone who's worked in payments: a regulator wants you to stop storing raw card numbers, your core system is on-prem and not going anywhere soon, and your team has six months. This is what we shipped, and what I'd do differently next time.
The constraints, up front
A few things were non-negotiable:
- The core couldn't move. BankCore lives in a hardened on-prem environment. Migrating it to AWS was a multi-year conversation, not a sprint.
- PCI-DSS scope had to shrink, not expand. Adding a cloud surface to an environment already in scope means you're now in scope plus AWS. Bad math.
- No PAN at rest outside the vault. If the tokenization service holds the raw PAN for longer than the call that needs it, you've built a second card vault. That's not what was asked for.
So the design question was less "how do we put cards in AWS?" and more "how do we put tokens in AWS, while the actual card data stays where the auditors already accept it?"
The architecture, in one diagram
Here's the rough shape — leaving out the boring boxes:
[Merchant API]
│ HTTPS
▼
[API Gateway]──auth──▶[Lambda: tokenize]
│
│ envelope-encrypt PAN ──▶ [KMS]
│
▼
[DynamoDB: token ↔ encrypted PAN]
│
│ (token returned to merchant)
▼
[Fortinet VPN tunnel]
│
▼
[On-prem BankCore]
(authoritative source of truth)
A merchant calls our API. Lambda generates a format-preserving token, envelope-encrypts the PAN with KMS, writes the (token, encrypted_pan) pair to DynamoDB, and returns the token. When the merchant later wants to charge that token, we reverse the lookup, decrypt, and forward the actual transaction to BankCore through a Fortinet site-to-site VPN.
Why Fortinet, why not Direct Connect
Direct Connect was the obvious "AWS textbook" answer. It also had a 4–6 month lead time and a hardware buy. We had Fortinet appliances on both ends already, and the bank's network team trusted that vendor. Picking the boring tool the existing team can operate is almost always the right call. We left a placeholder in the design doc for migrating to Direct Connect later — I doubt anyone touches it.
The tunnel was IPsec, not SSL VPN. Lower CPU overhead per packet, and easier to write a NetworkPolicy-equivalent rule about what AWS subnets can talk to what on-prem CIDRs.
PCI-DSS gotchas I'd warn the next person about
A few things that bit us, in roughly the order they bit us:
- CloudWatch Logs are in scope. If you ever
console.log(card.pan)— even by accident — you've put your log retention bucket in PCI scope. We added a log scrubber as middleware that masks anything matching the PAN regex before the log statement reaches CloudWatch. You can't trust developers to remember at 3 AM. - KMS key rotation isn't transparent. AWS will rotate the key material annually if you ask, but the key ID doesn't change — that's good. What's not good is that you still need to track which version encrypted which row. Envelope encryption with versioned data keys solved this; the master key just wraps DEKs.
- DynamoDB encryption at rest ≠ enough. Yes, the bytes on disk are encrypted. The IAM role calling
GetItemstill sees plaintext if you don't envelope-encrypt at the application layer too. Two layers of encryption isn't paranoia in payments; it's the spec. - Tokens themselves leak metadata. Format-preserving tokens that look like cards (same length, same Luhn-checkable structure) are nice for legacy merchant code but they tell observers "this is a card token." For internal storage we used opaque random IDs and only converted to FPE at the merchant boundary.
- Backups need their own plan. DynamoDB point-in-time recovery is fantastic. It's also an in-scope copy of your encrypted PAN database. Treat it like the production table.
What Lambda is good at, and what it isn't
The tokenize/detokenize handlers themselves are perfect Lambda workloads — short-lived, stateless, modest memory, bursty traffic. Cold starts on Node.js with the Resend-style minimal deps were under 250ms. We didn't need provisioned concurrency.
What I would not do again on Lambda is the long-running reconciliation jobs. Pulling daily settlement files from BankCore over the VPN, validating, and writing back was originally a Lambda fronted by EventBridge. It worked, but every time the on-prem side hiccuped we hit the 15-minute Lambda ceiling. We moved that to ECS Fargate with a longer timeout and slept better.
The thing I'd do differently
If I started this project today, I'd push harder on idempotency keys at the merchant layer. We made tokenize idempotent at the database (same PAN → same token, deterministic), which sounds clever, but it means the same card always tokenizes to the same value across all merchants. From a fraud-correlation perspective that's a feature; from a privacy-segregation perspective it's a leak. A per-merchant salt before the deterministic step would have been a one-line change at the start, and a much harder change to retrofit.
Closing thought
The interesting work in payments isn't the cryptography — KMS does the heavy lifting and you'd better not be writing your own. It's the interface between the regulated thing (BankCore, PCI scope) and the elastic thing (AWS, Lambda, DynamoDB). Most of the architectural decisions are about where you draw that boundary, what crosses it, and who's responsible when something on the other side breaks.
Tokenization is a good first project to take across that boundary because the data flow is unidirectional and the contracts are simple. I wouldn't recommend it as the last.