Tema
PCI DSS Pregunta 39 — Separation of Duties entre entornos prod y staging
| Campo | Valor |
|---|---|
| Solicitante | José David Álvarez — QSA ControlCase |
| Comentario QSA | "Por favor proveer los listados extractados directamente de las plataformas en sus diferentes ambientes" |
| Fecha de extracción | 2026-05-27 |
| Tipo de evidencia | K8s SAs + RBAC + DB users + GitHub teams + tests cross-env |
| Estado | RESUELTO — 6 ServiceAccounts con roles distintos creados + tests verificados |
| Paquete adjunto | q39-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 (incluyendocontrolcase_auditorread-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 dedeveloper-stagingcontra 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
| CAPA | PROD | STAGING | Separación |
|---|---|---|---|
| DO Account Owner | gaf2419 (CTO) | gaf2419 (CTO) | ⚠ Same owner (early-stage) |
| K8s cluster ID | a19e81ac-... | 97577f4a-... | ✅ Distinct clusters |
| K8s ServiceAccounts | admin-prod, viewer-prod, deployer-prod | admin-staging, developer-staging, deployer-staging | ✅ 3 vs 3 distinct |
| Token CA | Prod cluster CA (k8saas) | Staging cluster CA (distinct) | ✅ Cryptographically isolated |
| DB users | doadmin, fintrix_app, fintrix_readonly, controlcase_auditor | doadmin (only) | ✅ 4 vs 1 different |
| DB trusted sources | Prod cluster ID + APP CIDR 10.100.10.0/24 | Staging cluster ID only | ✅ No overlap (Q38 ref) |
| VPC | 10.100.0.0/16 | 10.101.0.0/16 | ✅ L2 isolated |
| GitHub repo | gaf2419 admin + 2 push devs | same repo (branch-level SoD) | ✅ 1 admin of 3 |
| Branch protection | required_approving_review_count: 1 | same | ✅ Author ≠ Reviewer |

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)
| SA | Cluster Role | Holder | Scope |
|---|---|---|---|
admin-prod | cluster-admin (built-in) | Gabriel Ureña (CTO) | All namespaces, all resources |
viewer-prod | viewer-prod-readonly (custom) | On-call observers, external auditors, QSA | All ns, read-only (no secrets, no exec) |
deployer-prod | deployer-prod-role (custom) | GitHub Actions CI/CD | default namespace only |

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" ★
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| Verb | Resource | Resultado |
|---|---|---|
| get | pods | ✅ yes |
| list | services | ✅ yes |
| get | namespaces | ✅ yes |
| delete | pods | ❌ no |
| create | secrets | ❌ no |
| get | secrets | ❌ no |
| create | pods/exec | ❌ no |
| update | deployments | ❌ no |
| delete | persistentvolumeclaims | ❌ no |

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>]| Verb | Resource | Namespace | Resultado |
|---|---|---|---|
| update | deployments | default | ✅ yes |
| patch | deployments | default | ✅ yes |
| get | pods | default | ✅ yes |
| create | deployments | default | ❌ no |
| create | namespaces | (cluster) | ❌ no |
| get | secrets | default | ❌ no |
| create | pods/exec | default | ❌ no |
| delete | persistentvolumeclaims | default | ❌ no |
| update | deployments | rbac-sod | ❌ no (cross-namespace) |

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) ★ BLOQUEADOPor 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-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 testingObservaciones de SoD en data layer
- Prod tiene 4 usuarios con scope distintos
controlcase_auditores 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)

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}'| Collaborator | Role | Permisos |
|---|---|---|
gaf2419 | admin | Todo: settings, branches, secrets, transfer |
tedevs0 | push | Crear branches + PRs, push a branches no-protegidas |
VillaMarCruz | push | Crear 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 onlyFlow 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
9. Cumplimiento PCI DSS Q39 — mapping
| Requisito PCI v4 | Control Fintrixs | Status |
|---|---|---|
| Req 6.5.3.1 Separation of roles and data between environments | K8s SAs distintos + DB users distintos + VPCs separados | ✅ |
| Req 7.2 Access components by job role | RBAC roles (admin/viewer/deployer) + DB roles + GitHub roles | ✅ |
| Req 7.3 Access privileges per role | Tests kubectl auth can-i confirman least-privilege enforced | ✅ |
| Req 6.4.5 Production code changes follow change control | Branch protection + PR review + author ≠ reviewer | ✅ |
| Req 8.2.2 Distinct user IDs | 3 GitHub users + 4 DB users + 6 K8s SAs cada uno con ID único | ✅ |
| Req 7.2.5 Application/system accounts least privilege | deployer-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 logno 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 main13. Conclusión para el QSA
6 ServiceAccounts K8s distintos creados con annotations PCI (holder, role, scope) — 3 en producción + 3 en staging.
Tests
kubectl auth can-iejecutados demostrando que:viewer-prodsolo puede LEER (no secrets, no exec, no mutations)deployer-prodsolo puede operar endefaultnamespace (no cross-ns, no secrets, no exec)developer-stagingtoken contra prod cluster → Unauthorized (cross-env imposible criptográficamente)
4 DB users en producción vs 1 en staging — incluyendo
controlcase_auditordedicado al QSA ControlCase (read-only).3 GitHub collaborators con roles diferenciados (1 admin CTO + 2 developers push-only) + branch protection enforced.
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
| Fecha | Revisor | Cambios |
|---|---|---|
| 2026-05-27 | Gabriel Ureña (CTO Fintrixs) | Creación inicial — 6 SAs creados, tests auth can-i ejecutados, cross-env blocked verified |
