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.
- Encrypt at rest.
- Encrypt in transit.
- Encrypt logs, backups, and even environment variables if you’re feeling spicy.
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:
- Strict-Transport-Security
- X-Content-Type-Options
- Content-Security-Policy
- X-Frame-Options
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.
- Encrypt staging data too. Attackers don’t care where they get data from.
- Don’t copy production secrets into staging.
- Implement monitoring and WAF rules here as well.
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:
- Application logs: Log errors from applications and other important information to help you debug. Make sure not to log sensitive information.
- Access logs: Log who did what and when.
- Database logs: Catch slow queries, weird access patterns, and potential breaches.
- Audit logs: Log changes to permissions, firewall rules, and code deployments. Tools like AWS CloudTrail, GuardDuty, and Config can help you with this.
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
.