PCI DSS Engineering Guide

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.

Implementing the Requirements

Map Controls to System Components

Each PCI DSS requirement must be linked to one or more system components. The mapping is validated via configuration, code, and runtime logs.

  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:

  - 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:

  #!/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"

Enforce Configuration Standards

All systems must use a standard configuration format, validated at build and deployment time.

  # 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'"
    }
  ]
}

Automate Security Scanning

Automated scans validate system configuration against PCI DSS control requirements.

  #!/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:

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:

  0 3 * * * /opt/scripts/run-pxc-scan.sh

Maintain Audit Trails

All system changes must be logged in a centralized audit trail with a standardized format.

  {
    "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:

Failure mode: logs are not timestamped in UTC, leading to misaligned audit trails across time zones.

Validate Compliance in CI/CD

Every deployment must pass a compliance check suite.

  #!/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.

Handle Control Exceptions and Exceptions

Control exceptions must be documented and reviewed.

  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

An active exception overrides the control’s requirement status.

Maintaining Compliance

Synchronize Control State

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:

Automate Re-Verification

Re-verification runs periodically and updates control state.

  #!/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

Track Control Ownership and Accountability

Each control has an owner responsible for its implementation and maintenance.

  {
    "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:

Troubleshooting Common Failures

Failure is silent if:

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.