If you’ve ever had to write an app to process card payments, not like integrating PayPal, but like being PayPal. You’ve probably heard of PCI DSS.

Payment Card Industry Data Security Standard (PCI DSS) is an information security standard for organizations that handle branded credit cards from the major card schemes. - Wikipedia

In other words, it’s a giant checklist that says: “Hey, if you’re going to store or process people’s money, maybe don’t leave your database open to the world.”

Why Am I Even Writing This?

After working on a PCI DSS–compliant app, I had a bit of an existential crisis and asked myself, “Why shouldn’t all apps follow PCI DSS secure coding practices?”

Believe me, PCI DSS is not as terrifying as it sounds. Compliance and privacy folks have a way of making every simple process sound enormous to feel good about themselves. Want to have a stressful day? Listen to them talk about PIA, DPIA, DSAR, or ROPA.

Not to oversimplify things, compliance can be painful. But if it helps protect your data and keeps you from ending up on “Have I Been Pwned”. Isn’t it kind of worth it?

Tips To Secure Coding

If you’re building a payment system or just trying to sleep better at night, here are some easy ways to secure coding that will bring you closer to PCI DSS compliance. Let’s dive in!

Subnets, NAT Gateways, and the Drama of Networking

If your app’s database is in a public subnet, I’m going to need you to stop reading and fix that. Now!

Ensure you use private subnets for anything sensitive, then route their traffic through a NAT Gateway so they can reach the internet (for patches, updates, memes, etc.) without being directly exposed. Think of it as giving your app a VPN to access the world, but telling it not to talk to strangers.

For “The Matrix” lovers, the Matrix world is a private subnet, the real world is the public subnet, and the secure phone Neo and his friends use to travel between worlds is the NAT Gateway. They can go through, but the machines can’t!

I love that reference, hope you do too. It took me a while. 😁

Encrypt All the Things

You’ve heard this before. You’ll hear it again.

For cloud services, managing encryption is easy peasy. You only have to enable them.

And please, Base64 is not an encryption. It’s for people who lie to themselves. You know who you are. Sure, it has its uses, but security is not one of them.

Security Headers

Security headers are HTTP response headers that instruct the browser on how to handle security-related aspects of a website. They help you prevent attacks like Cross-Site Scripting (XSS), clickjacking, and man-in-the-middle attacks. Set It and Forget It

At the very least, add:

They’re easy to configure in most frameworks and cloud services. Do it once, and you’re already better off than most.

Firewalls? Definitely Firewalls

Route public requests to your application through a firewall. It’s like putting a bouncer in front of your app.

Unlike the days of the boomers, where configuring a firewall is like assembling your furniture with instructions from Asgard. You might get it working, but at what cost? Present-day applications make this process a lot easier. Better if you’re on the cloud.

Cloud services like AWS WAF and Azure Firewall already come with pre-configured rules for common threats; you just have to switch them on, and they get to work.

Firewalls with minimum effort will help you stop known attack patterns, filter out bad IPs, and block that one guy still trying SQL injection from 2008.

Access Control

Your app isn’t a public library. Don’t give every service full access to everything just because it’s easier. You’re not Oprah Winfrey: “You get admin! You get admin! Everyone gets admin!”

Follow the “Principle of least privilege”, which means every user gets only what they absolutely need.

Try to rotate credentials and keys regularly and use roles over static keys when possible. Even cloud services prefer this. And they wrote the cloud.

Staging Is Not Production

Your staging environment should mirror production in structure, not in secrets.

Hackers love a staging environment with fewer alarms. What’s a better place to test their scripts?

Change Management

This is adulting for DevOps. PCI DSS requires you to have procedures to track changes in code, infrastructure, and security policies.

In simple terms, use Git, use Infrastructure-as-code (Serverless Framework, Terraform, AWS CDK, etc.), and set up alerts. You want to know when someone (maybe even you) decides to push trauma into production.

Logging Saves Lives

Logs are crucial. You hope to never need them, but if you do, you really need them. Keep secure and central logs for everything that matters:

Prefer ORMs

This is your first line of defense (and sanity). Forget raw SQL unless you’re a database wizard with a death wish. Use an ORM. Yes, you could parameterize your raw SQL and improve security; however, there’s still a large chance that future you or your colleague will forget to parameterize a query properly, and just like that, you’re cooked.

ORMs abstract your database interactions, making code easier to maintain and less prone to injection attacks. Most modern ORMs parameterize queries out of the box. That means fewer chances to accidentally DROP TABLE users; during a late-night commit.

In Conclusion

Now, seriously, was it that deep? It’s a long list, but I’m sure it’s filled with things you’re already doing. Just a few adjustments here and there, and you’re compliant!

Compliance is annoying, but so is a data breach. PCI DSS might feel like a giant list of “No, you can’t” but it’s just trying to keep you and your customers safe. On cloud platforms like AWS, Azure, and GCP, there’s no excuse not to follow these best practices. You’ve got the tools, you’ve got the docs, and now you’ve got a very persuasive blog post.

Stay safe. Stay compliant. And for the love of all that is good, don’t store unencrypted card numbers in a database column called card_number.