---
title: "PCI-DSS Level 1 on a Go Backend: The Architecture Decisions That Survive a QSA"
description: "Scope reduction, tokenization and segmentation: the architecture choices that decide a PCI-DSS Level 1 audit, plus the Go-specific ways card data leaks out."
url: https://gqlteam.com/blog/pci-dss-level-1-go-backend-architecture/
markdown: https://gqlteam.com/blog/pci-dss-level-1-go-backend-architecture/index.md
type: blog
date: 2026-09-01
lastmod: 2026-09-01
tags: ["pci-dss","compliance","security","go","architecture"]
---

# PCI-DSS Level 1 on a Go Backend: The Architecture Decisions That Survive a QSA

> Scope reduction, tokenization and segmentation: the architecture choices that decide a PCI-DSS Level 1 audit, plus the Go-specific ways card data leaks out.

In 2022 I designed the PCI-DSS Level 1 architecture for a payment processor that was scaling tenfold. It passed the QSA audit on the first pass.

That outcome was decided long before the assessor arrived, and almost none of it was about writing more secure code. It was about how few systems had any business touching a card number.

---

> **TL;DR** A PCI-DSS Level 1 audit is won or lost on scope. Every system that
> stores, processes or transmits account data, plus everything connected to
> them, is in the assessment. Tokenize at the edge so the primary account
> number never reaches your Go services, segment so the assessor can see the
> boundary, and make logging, secrets and key management architectural rather
> than procedural. On Go, the leaks are usually `fmt`, `pprof` and your error
> tracker.

---

## Scope Is the Only Lever That Matters

Level 1 means a Qualified Security Assessor performs an on-site assessment and signs a Report on Compliance, alongside quarterly external scans by an Approved Scanning Vendor. The QSA does not audit your company. They audit your cardholder data environment: the systems that store, process or transmit account data, plus the systems connected to those, plus the systems that could affect their security.

That last clause is where teams get hurt. Your CI runner deploys into the CDE, so it is in scope. Your logging platform receives logs from CDE hosts, so it is in scope. Your shared Kubernetes cluster runs a payment pod next to a marketing pod on the same flat network, so all of it is in scope.

Every architecture decision below is really the same decision, asked repeatedly: does this system need to touch a card number, and if not, how do we make it structurally incapable of doing so?

---

## Tokenization: Deciding Where the PAN Lives

| Approach | Who sees the PAN | What falls into scope | When it is right |
| --- | --- | --- | --- |
| Hosted fields or iframe from the provider | The provider only | Your payment page and its scripts | Almost always, for card entry |
| Client-side tokenization to the provider | The provider only | The page, plus the script supply chain | Custom checkout UI |
| Your own vault service | One small service | That service and everything connected to it | Recurring billing, multi-provider routing |
| PAN through the main application | Everything | Everything | Effectively never |

The default answer for card capture is that the number goes from the customer's browser to the payment provider and comes back as a token. Your Go services then deal in tokens, which are not account data, and the bulk of your platform stays outside the assessment.

Two consequences people miss. First, sending card data from the browser still puts the page and every script on it inside the standard: PCI-DSS v4 requires you to manage and justify the scripts on payment pages and to detect unauthorized changes to them. A checkout page carrying six analytics tags is a scope problem, not a marketing problem. Second, sensitive authentication data (full track data, the card verification code, the PIN block) must not be stored after authorization at all. Not encrypted, not briefly, not in a debug log. That one is absolute, and it is the finding that ends audits.

If you genuinely need your own vault, make it one small service with its own database, its own credentials, its own deployment path and a single narrow API. The goal is that you can draw its boundary on one slide and nothing else on that slide has a reason to cross it.

---

## Segmentation the Assessor Can See

Segmentation is not required by the standard. It is simply the only practical way to keep the assessment small, and if you claim it, it gets tested: at least annually for merchants and twice a year for service providers, by penetration testing that tries to cross the boundary.

What holds up in practice:

- A separate network or account for the CDE, not a namespace in the shared cluster. Default-deny egress, with an allowlist of the acquirer and provider endpoints.
- Mutual TLS between the CDE service and its callers, so the boundary is authenticated and not merely routed. Set `MinVersion` explicitly in your `tls.Config` rather than trusting a zero value to age well.
- No shared administrative plane. If the same bastion, the same CI runner and the same observability agent reach both sides, you have one environment with an optimistic diagram.
- Multi-tenant platforms carry extra obligations for isolating one customer's environment from another, so decide early whether you are a merchant or a service provider. The requirements and the testing frequencies differ.

---

## Go-Specific Ways Card Data Escapes

Go is a good fit for a small, boring, auditable CDE service: static binaries, a distroless image, a short list of dependencies to patch and scan. The failure modes are specific.

**`fmt` is the biggest one.** A single `log.Printf("%+v", req)` in a handler prints every field of the struct, and if a card number is in that struct it is now in your log aggregator, which is now in scope. Give the value a named type with a `String()` method that redacts, and a `LogValue()` method for `slog`, so the accidental print is harmless. Treat that as a seatbelt, not the design. The design is that the type never exists outside the one service that talks to the provider.

**`net/http/pprof` registers itself.** Importing the package for its side effect adds profiling handlers to `http.DefaultServeMux`. Serve with a nil handler somewhere in the same binary and you have published a heap profile endpoint, and a heap profile of a payment service contains whatever was in memory. Use an explicit mux, and if you need profiling in the CDE, bind it to localhost.

**Panics and core dumps carry memory.** A default panic traceback prints goroutine stacks with argument words, and `GOTRACEBACK=crash` writes a core file containing the process image. Decide where those go before an incident, not during one.

**Your error tracker is an exfiltration path.** Panic recovery middleware that ships the request body to a third-party SaaS moves account data out of the CDE and pulls that vendor into your assessment. Scrub at the source, or do not send bodies.

**Dependency evidence is cheap in Go, so produce it.** The standard requires an inventory of your bespoke software and third-party components and a process for identifying vulnerabilities in them. `govulncheck` in CI and a generated SBOM answer that question with an artifact instead of a meeting. Recent Go releases also ship a FIPS 140-3 mode for the standard library's cryptography behind a `GODEBUG` setting. PCI-DSS does not require FIPS validation, but being able to name the module you use shortens the cryptography conversation considerably.

---

## What the QSA Actually Asks For

| Evidence | What it looks like | Decided by |
| --- | --- | --- |
| Data flow diagram | Every path account data takes, across every system and network | Architecture |
| Network diagram | All connections between the CDE and everything else | Architecture |
| Component inventory | Every system in scope, with its role and owner | Architecture |
| Log retention | At least twelve months, with the recent months immediately searchable | Architecture |
| Key management | Who can use a key, who can manage it, how it rotates | Architecture |
| Access reviews, change control, scan results | Tickets, approvals, quarterly ASV reports | Process |

Read that column. Most of an audit is answering architectural questions with documents. If the data flow diagram is accurate and the boundary in it matches the boundary in the network, the assessment is a walkthrough. If the diagram was drawn for the audit and reality disagrees, the QSA finds the disagreement, and every finding after that is examined with more suspicion.

Two specifics worth designing for rather than discovering. Logs must record who did what, when, and to which data, be protected from the people they describe, and stay queryable for the retention period. And a card number, when it must be shown at all, is masked to at most the first six and last four digits, for people with a documented business need. Both are trivial if they are in the design and expensive if they are retrofitted across forty services.

---

## The Bottom Line

The decisions that survive an audit are made in the first month: card data never enters your platform, one small service owns the boundary, the network enforces what the diagram claims, and logging and key management are properties of the system rather than promises in a policy.

Do that and the QSA spends their week confirming what your diagrams already said. Skip it and you are negotiating about which of your forty services are in scope, which is a negotiation nobody wins.

---

Related service: https://gqlteam.com/services/compliance/

Contact: https://gqlteam.com/contact/ | Book: https://cal.com/dan-podina-snqasy/30min | MCP: https://mcp.gqlteam.com/mcp

