Implement PCI DSS technical controls for payment card data protection. Covers network segmentation, encryption requirements, access controls, logging and monitoring, vulnerability management, and the patterns that make PCI compliance achievable for engineering teams.
PCI DSS Engineering Guide: Compliance
This guide details the engineering practices, tooling, and workflows required to implement and maintain PCI DSS compliance in production systems. It applies at every stage of the software lifecycle: from initial system design through continuous deployment, monitoring, and audit readiness. The guide assumes a cloud-native, microservices-based architecture where security controls are codified and verified continuously.
Each PCI DSS requirement must be linked to one or more system components. The mapping is validated via configuration, code, and runtime logs.
requirements.csv with columns: requirement_id, control_name, component, owner, verification_method, last_updated. requirement_id,control_name,component,owner,verification_method,last_updated
1.1,Firewall configuration,API Gateway,Network Team,Configuration audit,2024-03-15
1.2,Inbound traffic rules,Load Balancer,Network Team,Network trace,2024-03-15
2.1,Secure OS baseline,Payment Service,DevOps Team,OS hardening script,2024-03-10
Verification occurs via:
inventory.json: list of all systems with component_id, host, os, ip, tags.component-registry.yml: - component: payment-service
type: microservice
deployment: kubernetes
environment: production
tags:
- payment
- pci-dss
- critical
requirements:
- 1.1
- 1.2
- 2.1
- 3.5
configuration:
- name: ingress-nginx
path: /etc/nginx/nginx.conf
version: 1.24.0
checksum: sha256:8d12a4b3e1c9f7a4d0d7d2b6f9f1a8a7b4c1d3c2e5a6f0b1a2c3d4e5f6a7b8c9
A control is considered implemented when:
verify-control.sh script runs and returns 0: #!/bin/bash
set -euo pipefail
REQUIREMENT_ID="$1"
COMPONENT="$2"
case "$REQUIREMENT_ID" in
1.1)
# Validate firewall configuration
if ! grep -q "allow tcp from 10.0.0.0/8 to 192.168.0.0/16" /etc/firewall/rules.conf; then
echo "ERROR: Missing inbound rule for 10.0.0.0/8 to 192.168.0.0/16"
exit 1
fi
;;
2.1)
# Validate OS hardening
if ! grep -q "Password minimum length: 12" /etc/security/pwquality.conf; then
echo "ERROR: Password length not set to 12"
exit 1
fi
if ! grep -q "Disable SSH root login" /etc/ssh/sshd_config; then
echo "ERROR: SSH root login enabled"
exit 1
fi
;;
*)
echo "Unknown requirement: $REQUIREMENT_ID"
exit 1
;;
esac
exit 0
Failures in verify-control.sh are logged to /var/log/compliance/verify-control.log with timestamped entries:
2024-03-15T12:34:56Z | ERROR | requirement_id=1.1 | component=api-gateway | message="Missing inbound rule for 10.0.0.0/8 to 192.168.0.0/16"
All systems must use a standard configuration format, validated at build and deployment time.
config.yaml with a schema validated via jsonschema or yq. # config.yaml
database:
host: db.payment.example.com
port: 5432
ssl: true
connection_pool:
min: 5
max: 50
audit_log: true
encryption:
type: pgp
key: "PGP-12345"
column: "card_number"
services:
- name: payment-service
environment: production
replicas: 4
image: registry.example.com/payment:v1.4.2
resources:
limits:
cpu: "2"
memory: "4Gi"
requests:
cpu: "1"
memory: "2Gi"
Validate with:
yq eval 'all(.database | keys | sort) == ["host", "port", "ssl", "connection_pool", "audit_log", "encryption"]' config.yaml
Deployment pipelines must validate config against a schema:
// config-schema.json
{
"type": "object",
"properties": {
"database": {
"type": "object",
"required": ["host", "port", "ssl"],
"properties": {
"host": { "type": "string", "pattern": "^\\S+$" },
"port": { "type": "integer", "minimum": 1, "maximum": 65535 },
"ssl": { "type": "boolean" },
"connection_pool": {
"type": "object",
"properties": {
"min": { "type": "integer", "minimum": 1 },
"max": { "type": "integer", "minimum": 1 }
},
"required": ["min", "max"]
}
},
"required": ["host", "port", "ssl"]
}
},
"required": ["database"]
}
Failure:
{
"error": "Invalid configuration schema",
"details": [
{
"instance": "database.port",
"schema": "integer",
"expected": "integer",
"actual": "string",
"message": "Expected integer but found string"
},
{
"instance": "database.connection_pool.min",
"schema": "integer",
"expected": "integer",
"actual": "null",
"message": "Missing required field 'min'"
}
]
}
Automated scans validate system configuration against PCI DSS control requirements.
nmap for network layer checks, ansible for host-level validation, sql for database checks.run-pxc-scan.sh: #!/bin/bash
set -euo pipefail
SCAN_ID=$(date +%Y%m%d-%H%M%S)
OUTPUT_DIR="/var/reports/pci-scan/$SCAN_ID"
mkdir -p "$OUTPUT_DIR"
# Network scan: open ports, services
nmap -sT -p 22,80,443,5432,6379 -oX "$OUTPUT_DIR/nmap.xml" --host-timeout 120s 192.168.0.10
# Host scan: OS, packages, config
ansible-playbook -i inventory.yaml -e "scan_id=$SCAN_ID" playbooks/host-check.yml
# Database scan
psql -U pciuser -d payment -c "
SELECT
name,
value,
CASE WHEN value IS NULL THEN 'MISSING' ELSE 'VALID' END AS status
FROM (
SELECT 'audit_log' AS name, audit_log FROM database
UNION ALL
SELECT 'encryption', encryption FROM database
) AS config
" -t -o "$OUTPUT_DIR/db-check.txt"
# Generate report
cat << EOF > "$OUTPUT_DIR/report.md"
# PCI DSS Scan Report
- Scan ID: $SCAN_ID
- Target: 192.168.0.10
- Date: $(date -I)
## Network
$(xmllint --format "$OUTPUT_DIR/nmap.xml" | grep -A 3 -B 1 "<host" | head -10)
## Host
$(cat "$OUTPUT_DIR/ansible-report.txt")
## Database
$(cat "$OUTPUT_DIR/db-check.txt")
EOF
# Upload to storage
aws s3 cp "$OUTPUT_DIR" s3://compliance-reports/pci-scan/$SCAN_ID/ --recursive
Each scan result includes:
scan_id, target, start_time, end_time.checks with id, name, status (pass, fail, warning), details.Example checks:
[
{
"id": "network.port.443",
"name": "HTTPS service on port 443",
"status": "pass",
"details": "Service 'nginx' on port 443 is listening"
},
{
"id": "host.os.audit_log",
"name": "Audit logging enabled on OS",
"status": "fail",
"details": "No /var/log/audit/audit.log found"
},
{
"id": "db.encryption.column",
"name": "Card number encrypted in database",
"status": "warning",
"details": "Column 'card_number' exists but is not encrypted"
}
]
Scans are triggered:
git push → CI → deploy → scan). 0 3 * * * /opt/scripts/run-pxc-scan.sh
All system changes must be logged in a centralized audit trail with a standardized format.
audit-log.json per component, with a common schema: {
"event_id": "a1b2c3d4-e5f6-7890-g1h2-i3j4k5l6m7n8",
"timestamp": "2024-03-15T12:34:56.789Z",
"source": "payment-service",
"event_type": "config_change",
"user": "[email protected]",
"details": {
"action": "update",
"component": "database",
"old": {
"ssl": true,
"connection_pool": { "min": 5, "max": 50 }
},
"new": {
"ssl": true,
"connection_pool": { "min": 10, "max": 100 },
"audit_log": true
}
},
"metadata": {
"commit_id": "abc123def456",
"pipeline_id": "ci-2024-03-15-1234",
"deployment_id": "prod-2024-03-15-001"
}
}
Audit logs are ingested into a central store (e.g., Elasticsearch, PostgreSQL, or Kafka) with the following contract:
application/json format./var/log/audit/ and rotated daily.log-aggregator service consumes the logs and indexes them.Failure mode: logs are not timestamped in UTC, leading to misaligned audit trails across time zones.
Every deployment must pass a compliance check suite.
check-compliance.sh to CI pipeline: #!/bin/bash
set -euo pipefail
# Check all requirements
for requirement in $(cat requirements.txt); do
if ! ./verify-control.sh "$requirement" "payment-service"; then
echo "COMPLIANCE FAILED: $requirement"
exit 1
fi
done
# Run security scan
./run-pxc-scan.sh
# Validate audit logs
if ! grep -q "event_type.*config_change" /var/log/audit/audit.log; then
echo "COMPLIANCE FAILED: No config_change events in audit log"
exit 1
fi
# Validate configuration
if ! yq eval '.database | has("connection_pool")' config.yaml; then
echo "COMPLIANCE FAILED: Missing connection_pool in database config"
exit 1
fi
echo "COMPLIANCE PASS"
exit 0
CI job output:
{
"status": "success",
"checks": [
{
"name": "verify-control",
"result": "pass",
"details": [
"1.1: passed",
"1.2: passed",
"2.1: passed"
]
},
{
"name": "security-scan",
"result": "pass",
"details": [
"Network scan completed",
"Host scan completed",
"Database check completed"
]
},
{
"name": "audit-log-validation",
"result": "fail",
"details": [
"No config_change events in audit log"
]
}
]
}
Failures halt the pipeline and are annotated in the pull request or CI dashboard.
Control exceptions must be documented and reviewed.
exceptions.csv: control_id,description,justification,approved_by,approved_date,review_cycle,active
1.1,"Firewall rules not applied to staging environment","Staging has different network topology","[email protected]","2024-01-15","quarterly",true
3.4,"No PCI DSS 3.5 validation for mobile app","Mobile app uses different encryption standard","[email protected]","2023-12-01","annual",false
approved_by email.approved_date in ISO 8601 format.review_cycle in quarterly, annual, or biennial.An active exception overrides the control’s requirement status.
Control state is maintained via a control-state.json file updated by automated tools.
{
"control_id": "1.1",
"status": "active",
"last_verified": "2024-03-15T12:34:56Z",
"last_updated": "2024-03-15T12:34:56Z",
"verification_methods": [
"configuration audit",
"network trace"
],
"evidence": [
{
"type": "file",
"path": "/etc/firewall/rules.conf",
"checksum": "sha256:8d12a4b3e1c9f7a4d0d7d2b6f9f1a8a7b4c1d3c2e5a6f0b1a2c3d4e5f6a7b8c9",
"timestamp": "2024-03-15T12:34:56Z"
},
{
"type": "report",
"path": "s3://compliance-reports/pci-scan/20240315-123456/report.md",
"checksum": "sha256:e5a6f0b1a2c3d4e5f6a7b8c9d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0",
"timestamp": "2024-03-15T12:35:00Z"
}
],
"exceptions": [
"1.1-staging"
]
}
A control is considered compliant when:
status is active.last_verified is within review_cycle (e.g., within 90 days for quarterly).Re-verification runs periodically and updates control state.
reverify-control.sh: #!/bin/bash
set -euo pipefail
CONTROL_ID="$1"
DATE=$(date -I)
# Run verification
if ! ./verify-control.sh "$CONTROL_ID" "payment-service"; then
echo "Re-verification failed for $CONTROL_ID"
exit 1
fi
# Update control state
jq --arg date "$DATE" '
.last_verified = $date
| .last_updated = $date
| .status = "active"
| .evidence += [
{
"type": "verification",
"path": "s3://compliance-reports/verification/$CONTROL_ID-$date.json",
"checksum": "sha256:$(sha256sum < /tmp/verification.json)",
"timestamp": $date
}
]
' control-state.json > temp.json && mv temp.json control-state.json
# Upload to storage
aws s3 cp control-state.json s3://compliance-state/$CONTROL_ID.json
Runs via cron:
0 2 * * 1 /opt/scripts/reverify-control.sh 1.1
0 2 * * 1 /opt/scripts/reverify-control.sh 2.1
0 2 * * 1 /opt/scripts/reverify-control.sh 3.4
Each control has an owner responsible for its implementation and maintenance.
owners.json: {
"1.1": {
"name": "Network Team",
"contact": "[email protected]",
"responsibilities": [
"Maintain firewall rules",
"Respond to audit findings",
"Update documentation"
],
"last_review": "2024-03-15T12:34:56Z"
},
"2.1": {
"name": "DevOps Team",
"contact": "[email protected]",
"responsibilities": [
"Apply OS hardening",
"Deploy updates",
"Monitor compliance status"
],
"last_review": "2024-03-10T08:00:00Z"
}
}
Ownership is enforced via:
owner field.verify-control.sh runs in CI and audit-log.json includes event_type and user fields.config-schema.json is versioned and checked into the project root.log-aggregator is running and connected to the central store.aws s3 cp command has correct credentials and bucket name.reverify-control.sh runs on correct schedule and updates the file correctly.Failure is silent if:
verify-control.sh returns 0 but echo statements are missing.control-state.json is updated but not uploaded to storage.exceptions.csv is not parsed correctly due to inconsistent quoting or missing headers.Use jq and yq to validate all JSON and YAML files in CI and deployment workflows.
This page was rewritten on 18 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy