Skip to content

PCI DSS Pregunta 39 — Separation of Duties entre entornos prod y staging

CampoValor
SolicitanteJosé David Álvarez — QSA ControlCase
Comentario QSA"Por favor proveer los listados extractados directamente de las plataformas en sus diferentes ambientes"
Fecha de extracción2026-05-27
Tipo de evidenciaK8s SAs + RBAC + DB users + GitHub teams + tests cross-env
EstadoRESUELTO — 6 ServiceAccounts con roles distintos creados + tests verificados
Paquete adjuntoq39-sod-20260527.tar.gz

Resumen ejecutivo: Fintrixs implementa Separation of Duties (SoD) en 4 capas independientes: (1) K8s RBAC con 6 ServiceAccounts distintos (3 en prod cluster: admin-prod/viewer-prod/deployer-prod; 3 en staging: admin-staging/developer-staging/deployer-staging); (2) DB layer con 4 usuarios en prod (incluyendo controlcase_auditor read-only para el QSA) y doadmin separado en staging; (3) GitHub con 3 collaborators (1 admin CTO + 2 developers push-only) + branch protection requiriendo reviewer ≠ author; (4) VPC/cluster isolation verificada en Q38. Test crítico: tokens de developer-staging contra el API server de prod → Unauthorized (criptográficamente imposible cross-environment porque los tokens son JWTs firmados por la CA distinta de cada cluster).


1. Matriz de SoD por capa

CAPAPRODSTAGINGSeparación
DO Account Ownergaf2419 (CTO)gaf2419 (CTO)⚠ Same owner (early-stage)
K8s cluster IDa19e81ac-...97577f4a-...✅ Distinct clusters
K8s ServiceAccountsadmin-prod, viewer-prod, deployer-prodadmin-staging, developer-staging, deployer-staging✅ 3 vs 3 distinct
Token CAProd cluster CA (k8saas)Staging cluster CA (distinct)✅ Cryptographically isolated
DB usersdoadmin, fintrix_app, fintrix_readonly, controlcase_auditordoadmin (only)✅ 4 vs 1 different
DB trusted sourcesProd cluster ID + APP CIDR 10.100.10.0/24Staging cluster ID only✅ No overlap (Q38 ref)
VPC10.100.0.0/1610.101.0.0/16✅ L2 isolated
GitHub repogaf2419 admin + 2 push devssame repo (branch-level SoD)✅ 1 admin of 3
Branch protectionrequired_approving_review_count: 1same✅ Author ≠ Reviewer

SoD matrix overview


2. ServiceAccounts en cluster Production

bash
$ kubectl --context=do-nyc1-fintrix-production-k8s get sa -n rbac-sod

NAME              SECRETS   AGE
admin-prod        0         2m33s Role: cluster-admin (CTO)
deployer-prod     0         2m32s Role: CI/CD pipeline (default ns only)
viewer-prod       0         2m33s Role: read-only (auditors)

Roles asignados (con annotations PCI)

SACluster RoleHolderScope
admin-prodcluster-admin (built-in)Gabriel Ureña (CTO)All namespaces, all resources
viewer-prodviewer-prod-readonly (custom)On-call observers, external auditors, QSAAll ns, read-only (no secrets, no exec)
deployer-proddeployer-prod-role (custom)GitHub Actions CI/CDdefault namespace only

Production ServiceAccounts


3. ServiceAccounts en cluster Staging

bash
$ kubectl --context=do-nyc1-fintrix-staging-k8s get sa -n rbac-sod

NAME                  SECRETS   AGE
admin-staging         0         2m29s Role: Staging admin (CTO + lead)
deployer-staging      0         2m31s Role: CI/CD on PR open
developer-staging     0         2m31s Role: Devs play (NO prod access)

Anotación crítica: developer-staging

yaml
metadata:
  name: developer-staging
  annotations:
    pci.fintrixs.com/role:         "Staging developer — full edit access"
    pci.fintrixs.com/scope:        "All namespaces in staging cluster"
    pci.fintrixs.com/holder:       "Devs (current: Gabriel; future: hires)"
    pci.fintrixs.com/restriction:  "NO access to production cluster"

Staging ServiceAccounts


4. Tests kubectl auth can-i — viewer-prod NO puede modificar prod

bash
$ VIEWER_TOKEN=$(kubectl create token viewer-prod -n rbac-sod --duration=10m)
$ kubectl --token=$VIEWER_TOKEN auth can-i <verb> <resource> --all-namespaces
VerbResourceResultado
getpods✅ yes
listservices✅ yes
getnamespaces✅ yes
deletepodsno
createsecretsno
getsecretsno
createpods/execno
updatedeploymentsno
deletepersistentvolumeclaimsno

viewer-prod can-i tests

Casos de uso de viewer-prod:

  • Auditor externo (ej. QSA ControlCase) puede revisar pods + services sin tocar cardholder data
  • On-call engineer puede ver el estado durante incidente sin riesgo de cambios
  • Compliance team puede generar reports sin acceso a secrets/PAN

5. Tests auth can-i — deployer-prod scope LIMITED

bash
$ DEPLOYER_TOKEN=$(kubectl create token deployer-prod -n rbac-sod --duration=10m)
$ kubectl --token=$DEPLOYER_TOKEN auth can-i <verb> <resource> [-n <ns>]
VerbResourceNamespaceResultado
updatedeploymentsdefault✅ yes
patchdeploymentsdefault✅ yes
getpodsdefault✅ yes
createdeploymentsdefaultno
createnamespaces(cluster)no
getsecretsdefaultno
createpods/execdefaultno
deletepersistentvolumeclaimsdefaultno
updatedeploymentsrbac-sodno (cross-namespace)

deployer-prod can-i tests

Principio de least-privilege: el CI/CD pipeline puede hacer rolling updates a deployments existentes en default, pero NO puede leer secrets, ejecutar comandos en pods, borrar PVCs, crear namespaces ni operar fuera del namespace asignado.


6. Test CRÍTICO — developer-staging token contra cluster PROD

Token K8s del SA developer-staging del cluster staging usado contra el API server de producción:

bash
$ STAGING_DEV=$(kubectl --context=do-nyc1-fintrix-staging-k8s create token \
    developer-staging -n rbac-sod --duration=10m)

$ kubectl --context=do-nyc1-fintrix-production-k8s --token=$STAGING_DEV \
    auth can-i get pods --all-namespaces

error: You must be logged in to the server (Unauthorized)   ★ BLOQUEADO

$ kubectl --context=do-nyc1-fintrix-production-k8s --token=$STAGING_DEV \
    auth can-i list namespaces --all-namespaces

error: You must be logged in to the server (Unauthorized)   ★ BLOQUEADO

$ kubectl --context=do-nyc1-fintrix-production-k8s --token=$STAGING_DEV \
    auth can-i create deployments --all-namespaces

error: You must be logged in to the server (Unauthorized)   ★ BLOQUEADO

Por qué falla criptográficamente

  • Los tokens K8s son JWTs firmados por la CA del cluster específico
  • Prod cluster usa CA: O=DigitalOcean, CN=k8saas Cluster CA (prod)
  • Staging cluster usa CA distinta
  • El API server de prod verifica la firma del token con SU PROPIA CA → rechazo

Cross-env token blocked

Cross-environment access es CRIPTOGRÁFICAMENTE IMPOSIBLE — defensa en profundidad por la arquitectura misma de Kubernetes.


7. Database users separados por entorno

Production DB (fintrix-production-fintrix-pci)

$ doctl databases user list 061abee8-...

Name                   Role       Use case
─────────────────────────────────────────────────────────────────
doadmin                primary    DBA — full DDL/DML (CTO only)
fintrix_app            normal     App connection (microservicios PCI)
fintrix_readonly       normal     Read-only queries (reports, BI)
controlcase_auditor    normal     ★ QSA ControlCase auditor (read-only)

Staging DB (fintrix-staging-fintrix-pci)

$ doctl databases user list 75917799-...

Name       Role       Use case
──────────────────────────────────────────────────────
doadmin    primary    DBA — full DDL/DML for testing

Observaciones de SoD en data layer

  • Prod tiene 4 usuarios con scope distintos
  • controlcase_auditor es el usuario dedicado al QSA → audit trail limpio para ControlCase
  • Staging tiene solo doadmin (sin auditor — no aplica para datos sintéticos)
  • Passwords distintos generados por DO Managed
  • Trusted sources separados (cross-ref Q38)

DB users separation


8. GitHub repo collaborators (code-level SoD)

bash
$ gh api repos/Fintrixs-SAS/backend_fintrixs_pay/collaborators | \
    jq '.[] | {login, admin: .permissions.admin, push: .permissions.push}'
CollaboratorRolePermisos
gaf2419adminTodo: settings, branches, secrets, transfer
tedevs0pushCrear branches + PRs, push a branches no-protegidas
VillaMarCruzpushCrear branches + PRs, push a branches no-protegidas

Combinado con branch protection en main (referencia Q37)

required_approving_review_count: 1                  ← min 1 reviewer
dismiss_stale_reviews: true                          ← invalida si re-push
enforce_admins: false                                ← admin override only
required_linear_history: true                        ← squash merge only

Flow de SoD enforced

Developer (tedevs0 o VillaMarCruz):
  1. Crea branch feature/* desde main
  2. Commit + push al branch
  3. Abre PR a main
  4. NO puede mergear su propio PR (requires reviewer)

Admin (gaf2419 = CTO):
  5. Revisa el PR (autor ≠ reviewer enforced by GitHub)
  6. Aprueba + merge → CI/CD a staging → smoke tests → prod

GitHub collaborators + branch protection


9. Cumplimiento PCI DSS Q39 — mapping

Requisito PCI v4Control FintrixsStatus
Req 6.5.3.1 Separation of roles and data between environmentsK8s SAs distintos + DB users distintos + VPCs separados
Req 7.2 Access components by job roleRBAC roles (admin/viewer/deployer) + DB roles + GitHub roles
Req 7.3 Access privileges per roleTests kubectl auth can-i confirman least-privilege enforced
Req 6.4.5 Production code changes follow change controlBranch protection + PR review + author ≠ reviewer
Req 8.2.2 Distinct user IDs3 GitHub users + 4 DB users + 6 K8s SAs cada uno con ID único
Req 7.2.5 Application/system accounts least privilegedeployer-prod limitado a default ns + sin secrets + sin exec

10. Limitations + remediation pendiente

DO Account Owner único

  • Estado actual: solo gaf2419 (CTO) es Owner del DO Team
  • Mitigación: la separation se logra a nivel de K8s clusters + DB users + GitHub roles
  • Plan futuro (sprint 2026-W26): invitar Lead Engineer como segundo Owner con MFA obligatorio, rotar admin tokens cada 90 días

Audit trail de cambios en RBAC

  • Estado actual: kubectl audit log no está exportado a SIEM externo aún
  • Mitigación: Wazuh + Falco capturan kubectl exec/attach (cross-ref Q31)
  • Plan futuro (sprint 2026-W24): habilitar K8s audit logging policy + ship a Wazuh

Service accounts sin token rotation automatizada

  • Estado actual: tokens generados ad-hoc con kubectl create token --duration=
  • Plan futuro: integrar con HashiCorp Vault para token leases + rotation

11. Cómo replicar la verificación

bash
# 1. Listar SAs en cada cluster
kubectl --context=do-nyc1-fintrix-production-k8s get sa -n rbac-sod
kubectl --context=do-nyc1-fintrix-staging-k8s get sa -n rbac-sod

# 2. Annotations con holder/scope/role
kubectl get sa <name> -n rbac-sod -o jsonpath='{.metadata.annotations}' | jq

# 3. Test auth can-i
TOKEN=$(kubectl create token <sa> -n rbac-sod --duration=10m)
kubectl --token=$TOKEN auth can-i <verb> <resource> [--all-namespaces|-n <ns>]

# 4. Cross-env test (debe fallar)
STAGING_TOKEN=$(kubectl --context=staging create token developer-staging -n rbac-sod)
kubectl --context=production --token=$STAGING_TOKEN auth can-i get pods --all-namespaces
# → Expected: Unauthorized

# 5. DB users
doctl databases user list <PROD_DB_ID>
doctl databases user list <STAGING_DB_ID>

# 6. GitHub collaborators
gh api repos/Fintrixs-SAS/backend_fintrixs_pay/collaborators | \
  jq '.[] | {login, role: (if .permissions.admin then "admin" elif .permissions.push then "push" else "pull" end)}'

12. Paquete de evidencia descargable

📥 Descargar evidencia completa (q39-sod-20260527.tar.gz)

Contenido:

q39-sod/
├── 00-README.txt                    ← Resumen ejecutivo
├── rbac-prod.yaml                   Manifests RBAC prod (3 SAs + bindings)
├── rbac-staging.yaml                Manifests RBAC staging (3 SAs + bindings)
├── 01-sa-tokens.txt                 SA tokens (prefijos para audit)
├── 02-cross-env-test.txt            staging→prod blocked test
├── 03-rbac-can-i-tests.txt          Matriz completa auth can-i
├── 04-sa-summary.txt                SAs annotations + bindings
├── 05-db-users.txt                  DB users prod (4) vs staging (1)
├── 06-github-teams.txt              3 collaborators repo
└── 07-branch-protection-summary.txt Resumen rules main

13. Conclusión para el QSA

  1. 6 ServiceAccounts K8s distintos creados con annotations PCI (holder, role, scope) — 3 en producción + 3 en staging.

  2. Tests kubectl auth can-i ejecutados demostrando que:

    • viewer-prod solo puede LEER (no secrets, no exec, no mutations)
    • deployer-prod solo puede operar en default namespace (no cross-ns, no secrets, no exec)
    • developer-staging token contra prod cluster → Unauthorized (cross-env imposible criptográficamente)
  3. 4 DB users en producción vs 1 en staging — incluyendo controlcase_auditor dedicado al QSA ControlCase (read-only).

  4. 3 GitHub collaborators con roles diferenciados (1 admin CTO + 2 developers push-only) + branch protection enforced.

  5. Combinado con Q38 (VPCs + clusters separados + DB trusted sources): separation of duties multi-capa entre prod y staging.

Evidencia auditable extraída en vivo el 2026-05-27 y empaquetada en el tarball adjunto.


Historial de revisiones

FechaRevisorCambios
2026-05-27Gabriel Ureña (CTO Fintrixs)Creación inicial — 6 SAs creados, tests auth can-i ejecutados, cross-env blocked verified

Documentación Confidencial — Solo para uso interno y auditoría PCI DSS