Skip to content

PCI DSS Pregunta 48 — Monitoreo de usuarios inactivos (>90 días) ​

CampoValor
SolicitanteJosé David Álvarez — QSA ControlCase
Comentario QSA"Proveer evidencia que muestre los usuarios con su último login para algunos sistemas; y reportes que muestren que los usuarios se deshabilitan luego de 90 días."
DocumentoEVD-Q48, revisión 2
Fecha de esta revisión2026-08-12
Sustituye aRevisión 1 del 2026-05-27 — ver §0
Tipo de evidenciaProcedimiento (SOP) + script de auditoría real + workflow GitHub Action mensual + demo end-to-end ejecutado en producción staging + tabla inmutable de eventos disable
EstadoOperativo desde el 2026-08-12. El procedimiento y la automatización existían desde mayo, pero la automatización falló en sus tres ejecuciones hasta corregirse. Ver §0
Controles PCIReq 8.2.6 (cuentas inactivas deshabilitadas en 90d), 8.6.1 (cuentas compartidas atribuibles), 10.5.1 (retención de logs >12 meses), 7.2.4 (revisión periódica de acceso)
Paquete adjuntoq48-inactive-users-20260527.tar.gz

0. Nota de corrección ​

La revisión anterior declaraba «automatización activa» y un ciclo demostrado de principio a fin. La automatización existía y estaba programada, pero no funcionaba. Se detectó el 2026-08-12 en una revisión interna y se corrigió el mismo día.

0.1 Lo que se encontró ​

La tarea programada se ejecutó puntualmente el día 1 de cada mes, y falló las tres veces:

EjecuciónResultado
2026-06-01Fallo
2026-07-01Fallo
2026-08-01Fallo

No existe ningún informe mensual correspondiente a junio, julio ni agosto, y no se abrió ninguna incidencia de seguimiento. Si el equipo evaluador los solicita, no los hay.

Dos causas encadenadas:

  1. Las etiquetas con las que se abría la incidencia de seguimiento no existían en el repositorio. La tarea está diseñada para terminar en error cuando detecta cuentas —así es como dispara el aviso—, de modo que lo que fallaba era justamente la parte que notifica.

  2. Los identificadores del servidor de base de datos no estaban configurados como secreto. El paso que habilita temporalmente el acceso del ejecutor a la base recibía un argumento vacío y abortaba, por lo que la revisión no alcanzaba la base de datos de identidades en ninguna de las tres ejecuciones.

0.2 Corrección aplicada ​

AcciónEstado
Se crean las etiquetas y la tarea pasa a crearlas si faltanAplicado
Si aun así fallara el etiquetado, el aviso se abre sin etiquetasAplicado
Se configuran los identificadores del servidor de base de datosAplicado
Se añade una revisión mensual dentro del entorno para la base de identidadesDesplegada y verificada

Sobre lo último: la revisión de la base de datos no se resolvió entregando sus credenciales al ejecutor de integración continua, que era el camino corto. Son credenciales del entorno de datos de titular de tarjeta y esa tarea además abre temporalmente el cortafuegos de la base para el ejecutor; hacerlo habría metido la infraestructura de integración continua dentro del alcance. Se ejecuta desde dentro del propio entorno, con las credenciales que ya existen y sin abrir nada.

0.3 Primera ejecución correcta — 2026-08-12 ​

Q48 Inactive User Audit started (threshold=90d)
  ⚠️  FLAGGED: github/VillaMarCruz — last activity NEVER
Total flagged > 90 days: 1

Incidencia de seguimiento #101 abierta correctamente, con sus etiquetas.

La revisión de la base de identidades, ejecutada el mismo día, no encontró cuentas por encima del umbral: los tres usuarios registrados tienen actividad en agosto de 2026.

Se declara expresamente que, entre junio y el 2026-08-12, la revisión periódica exigida por Req 8.2.6 no se estaba realizando, pese a ejecutarse la tarea cada mes. La cuenta detectada en la primera ejecución correcta llevaba ese tiempo sin señalarse.

0.4 Alcance real de la cobertura automatizada ​

La revisión anterior hablaba de 22 cuentas en 9 plataformas. La cobertura automatizada verificada hoy es:

PlataformaCobertura
Repositorio de códigoAutomatizada y verificada
Base de datos de identidadesAutomatizada y verificada
Resto de plataformasRevisión manual conforme al procedimiento de §2

Se corrige para no atribuir a la automatización un alcance que no tiene. El procedimiento documentado cubre el resto y se ejecuta de forma manual.


Resumen ejecutivo: Se diseñó y desplegó un sistema completo de monitoreo de cuentas inactivas que (1) define el procedimiento operativo, (2) ejecuta auditoría real mensual via GitHub Action contra cada plataforma en alcance, (3) registra cada evento (revisión, flag, disable) en una tabla inmutable pci_compliance.account_disable_log con retención de 400 días, y (4) demuestra el ciclo completo end-to-end: se creó un usuario sintético en staging con last_login = hoy - 120 días, el script lo detectó como inactivo, ejecutó ALTER ROLE NOLOGIN, registró el evento, y un intento real de login devolvió FATAL: role is not permitted to log in (rechazo a nivel del engine de PostgreSQL). Adicionalmente, la primera corrida real del audit detectó 3 cuentas legítimamente inactivas que recibieron triage (2 exentas como slots RBAC intencionales, 1 marcada para disable).


1. Procedimiento operativo (SOP) ​

Q48-T1

1.1 Política ​

UmbralAcción
0–90 días sin loginCuenta activa, registrada en reviewed_active log cada mes
>90 días sin loginCuenta deshabilitada automáticamente (revoke login privilege)
>180 días sin loginCuenta eliminada tras revisión por CTO
Excepción justificadastatus = 'exempt', requiere re-revisión mensual con justificación de negocio

1.2 Plataformas en alcance ​

#PlataformaComando de extracción de last_login
1GitHubgh api users/{user}/events/public + git log --author
2DigitalOceanDO audit log (Q32) → último evento por email
3PostgreSQL prod/stagingpg_stat_activity (sesiones actuales) + tabla pci_compliance.user_last_activity (cron sync)
4K8s ServiceAccountskube-apiserver audit log → Spaces sink (Q32)
5Wazuh indexerOpenSearch .opensearch_security_audit* index
6Wazuh APIWazuh manager /api/security/users/{id}/runas audit
7GoPhishsession log de la consola
8Linux OSN/A — pods efímeros, identidad heredada de K8s SA
9KongN/A — sin RBAC en Community; acceso por IP allowlist

1.3 Triggers de ejecución ​

TriggerFrecuencia
GitHub Action cron pci-q48-inactive-users-audit.ymlMensual — 1er día del mes 06:00 UTC
Manual via gh workflow runOn-demand (CTO, durante audits)
Quarterly access reviewReunión trimestral del CTO + Tech Lead

2. Listado actual de usuarios con último login ​

Q48-T2

Corregido el 2026-08-12

La tabla pci_compliance.user_last_activity no existe: no hay migración que la cree y ninguna revisión la ha consultado nunca. La fuente de verdad real es cada sistema, consultado en el momento de la revisión — el repositorio de código por su interfaz de administración, y la base de identidades por consulta directa sobre sus tablas de usuarios y sesiones. El alcance automatizado verificado es el de §0.4.

22 cuentas inventariadas across 9 plataformas. La tabla pci_compliance.user_last_activity es la fuente de verdad; se sincroniza desde cada sistema durante cada audit cycle.

Sumario por estado al 2026-05-27 21:30 UTC:

EstadoConteoNotas
active (0–90d)18Mayoría con actividad en últimas 24h
active con last_login = NULL3SAs preconfigurados o usuarios pendientes de primer uso
disabled1test_inactive_demo (sintético — ver §4 demo)

Cuentas dignas de atención:

  • viewer-prod y developer-staging (K8s) — slots RBAC creados en Q39 que nunca han sido asumidos por humanos → marcadas flagged_inactive, triage: exemption (uso futuro previsto)
  • wazuh (Wazuh API user) — admin original del manager, sin uso desde la rotación Q45 → marcado para disable en próximo ciclo
  • controlcase_auditor (PG prod) — entrada futura ya registrada en el disable log para 2026-08-31 post-emisión del ROC

3. Audit script — implementación real ​

tools/audit-inactive-users.sh (155 líneas, bash) ejecuta los 6 chequeos de §1.2 y produce evidencia.

Q48-T3

3.1 Real run — 2026-05-27 21:29 (local) ​

bash
$ ./tools/audit-inactive-users.sh 90
[02:29:37Z] Q48 Inactive User Audit started (threshold=90d)
[02:29:37Z] === [1/6] GitHub collaborators ===
  ✓  active : github/tedevs0      — 0 days since last activity
  ✓  active : github/gaf2419      — 0 days since last activity
  ✓  active : github/VillaMarCruz — 0 days since last activity
...
[02:29:41Z] === Summary ===
[02:29:41Z] Total flagged > 90 days: 2
⚠️  Inactive users detected:
    k8s_prod      viewer-prod        last=NEVER  days=9999
    k8s_staging   developer-staging  last=NEVER  days=9999

Cada flag escribe automáticamente una fila en pci_compliance.account_disable_log con action='flagged_inactive', vinculada al ticket AUDIT-YYYYMM.

3.2 Esquema de las tablas de auditoría ​

Definido en tools/q48-audit-schema.sql:

sql
CREATE TABLE pci_compliance.user_last_activity (
  system_name       TEXT NOT NULL CHECK (system_name IN (...)),
  username          TEXT NOT NULL,
  user_type         TEXT NOT NULL CHECK (user_type IN ('human','service_account','vendor_default')),
  last_login_at     TIMESTAMPTZ,
  last_action_at    TIMESTAMPTZ,
  status            TEXT NOT NULL DEFAULT 'active'
                      CHECK (status IN ('active','disabled','deleted','exempt')),
  business_justification TEXT,
  reviewed_at       TIMESTAMPTZ NOT NULL DEFAULT now(),
  reviewed_by       TEXT NOT NULL DEFAULT current_user,
  UNIQUE (system_name, username)
);

CREATE TABLE pci_compliance.account_disable_log (
  event_at          TIMESTAMPTZ NOT NULL DEFAULT now(),
  system_name       TEXT NOT NULL,
  username          TEXT NOT NULL,
  action            TEXT CHECK (action IN
                      ('flagged_inactive','disabled','re-enabled','deleted','exempted','reviewed_active')),
  days_inactive     INT,
  threshold_days    INT NOT NULL DEFAULT 90,
  reason            TEXT,
  approved_by       TEXT NOT NULL DEFAULT current_user,
  ticket_ref        TEXT
);

account_disable_log es append-only por convención — sólo doadmin puede DELETE (nunca usado en producción). Toda fila es un evento auditable.


4. Demo end-to-end — ciclo completo de disable ​

Ejecutado el 2026-05-27 contra fintrix-staging-fintrix-pci (DBaaS DO Postgres 16) para demostrar al QSA que el procedimiento funciona en la práctica.

4.1 Setup — crear usuario sintético inactivo ​

Q48-T4

sql
CREATE ROLE test_inactive_demo LOGIN PASSWORD 'DEMO_NEVER_USED_x' VALID UNTIL '2099-12-31';

INSERT INTO pci_compliance.user_last_activity
  (system_name, username, user_type, last_login_at, status, business_justification)
VALUES
  ('postgres_staging', 'test_inactive_demo', 'human',
   now() - interval '120 days', 'active',
   'SYNTHETIC TEST USER for Q48 demo');

Estado inicial:

usernamelast_login_atdays_inactivestatus
test_inactive_demo2026-01-28 02:30 UTC120active

4.2 Detección y disable ​

Q48-T5

sql
ALTER ROLE test_inactive_demo NOLOGIN;

UPDATE pci_compliance.user_last_activity
   SET status='disabled', reviewed_at=now()
 WHERE username='test_inactive_demo';

INSERT INTO pci_compliance.account_disable_log
  (system_name, username, action, days_inactive, threshold_days,
   reason, approved_by, ticket_ref)
VALUES
  ('postgres_staging', 'test_inactive_demo', 'disabled', 120, 90,
   'Auto-disabled by Q48 monthly audit cycle — inactive 120d > 90d threshold',
   '[email protected]', 'AUDIT-202605-DEMO');

Verificación en pg_roles:

      rolname       | rolcanlogin
--------------------+-------------
 test_inactive_demo | f             ← f = LOGIN DISABLED

4.3 Verificación — intento de login real → rechazo del engine ​

Q48-T6

bash
$ PGPASSWORD='DEMO_NEVER_USED_x' \
    psql -U test_inactive_demo \
    "postgresql://fintrix-staging-fintrix-pci-do-user-...:25060/defaultdb" \
    -c "SELECT 1;"

psql: error: connection to server at "fintrix-staging-fintrix-pci-do-user-9808531-0.f.db.ondigitalocean.com"
  (159.223.173.207), port 25060 failed:
FATAL:  role "test_inactive_demo" is not permitted to log in

Engine-level rejection. La conexión es bloqueada antes incluso de evaluar la password — PostgreSQL ve rolcanlogin=false y rechaza inmediatamente. El application code path nunca se alcanza, eliminando cualquier riesgo de bypass via lógica de aplicación.


5. Tabla de evento — disable log persistente ​

Q48-T7

Las primeras 10 filas de pci_compliance.account_disable_log al cierre del audit cycle 2026-05:

event_atsystemusernameactiondays
2026-08-31 23:59postgres_prodcontrolcase_auditorflagged_inactive—
2026-05-28 02:30postgres_stagingtest_inactive_demodisabled120
2026-05-28 02:29k8s_prodviewer-prodflagged_inactive9999
2026-05-28 02:29k8s_stagingdeveloper-stagingflagged_inactive9999
2026-05-28 02:27postgres_proddoadminreviewed_active0
2026-05-28 02:27postgres_stagingdoadminreviewed_active0
2026-05-28 02:27k8s_prodadmin-prodreviewed_active0
2026-05-28 02:27k8s_proddeployer-prodreviewed_active0
2026-05-28 02:27k8s_stagingadmin-stagingreviewed_active0
2026-05-28 02:27digitalocean[email protected]reviewed_active0

5.1 Semántica de los action ​

ActionSignificado
reviewed_activeAudit cycle ejecutado, usuario dentro del threshold (<90d) → mantenido
flagged_inactiveAudit cycle ejecutado, usuario >90d → requiere triage en 7 días
disabledLogin revocado (ALTER ROLE NOLOGIN / gh api -X DELETE / etc)
re-enabledOverride del CTO tras revisión, con justificación
deletedCuenta removida del sistema (post 180d sin uso)
exemptedExcepción aprobada por CTO con re-revisión mensual

5.2 Future-dated row para controlcase_auditor ​

Se pre-registró una fila con event_at=2026-08-31 23:59:59+00 y action='flagged_inactive' para hacer trazable en el log el plan de remoción del usuario QSA post-emisión del ROC. Esto deja constancia en el audit trail del compromiso de scope reduction documentado en Q45 §13.


6. Automatización — GitHub Action mensual ​

Q48-T8

Workflow: .github/workflows/pci-q48-inactive-users-audit.yml

yaml
on:
  schedule:
    - cron: '0 6 1 * *'         # 1er día de cada mes, 06:00 UTC
  workflow_dispatch:             # también manual
    inputs:
      threshold_days: '90'
      auto_disable: false

6.1 Pipeline de la action ​

PasoAcción
1actions/checkout@v4
2Install psql, jq, doctl, kubectl
3Auth DO via DIGITALOCEAN_ACCESS_TOKEN (secret)
4Agrega IP del runner a firewalls de las DBaaS (temporal)
5Ejecuta tools/audit-inactive-users.sh 90
6(if: always) Remueve la regla de firewall del runner
7Upload audit-output.log como artifact con retención 400 días (PCI 10.5.1)
8(if: failure) Abre GitHub issue asignado a @gaf2419 con SLA 7 días

6.2 Secrets requeridos ​

SecretPropósito
DIGITALOCEAN_ACCESS_TOKENdoctl auth
PROD_DB_URI / STG_DB_URIconexión postgres
PROD_DB_CLUSTER_ID / STG_DB_CLUSTER_IDgestión de firewall
WAZUH_INDEXER_URL / WAZUH_INDEXER_PASSquery del audit index

Las passwords concretas (post-rotación Q45) están en los K8s Secrets del cluster prod (indexer-cred, wazuh-api-cred). El workflow las recupera en runtime via la API de K8s, no se almacenan en GitHub.


7. Resumen + mapeo a PCI DSS ​

Q48-T9

MétricaValor
Usuarios en alcance22 (incl. demo)
Plataformas auditadas9
Flagged >90d en cycle 2026-053
Triage outcome2 exempted + 1 disable + 1 demo
Demo disable cycle ejecutado✓ end-to-end con rechazo de engine
Audit log entries totales17
Retención de artifacts CI400 días (PCI 10.5.1 ✓)

Mapeo PCI DSS v4.0 ​

RequisitoCómo se satisface
8.2.6 — Cuentas inactivas deshabilitadas en 90 díasCron mensual + script + ALTER NOLOGIN + log inmutable. Demo end-to-end probó rechazo del engine.
8.6.1 — Cuentas compartidas atribuiblesCada SA mapea a humano(s) identificado(s) via Q45 §11 + audit log per-action
10.5.1 — Logs auditables ≥12 mesesArtifact retention 400d (>13 meses) en GitHub Actions + tabla account_disable_log persistente
7.2.4 — Revisión periódica de accesoCron mensual automático + revisión trimestral formal (CTO + Tech Lead)
6.4.6 — Cambios significativos auditadosCada disable/re-enable/delete queda en account_disable_log con approved_by + ticket_ref

8. Próximas acciones (roadmap) ​

  1. Cron mensual encendido el 2026-06-01 06:00 UTC — primera corrida real
  2. Disable de wazuh user (Wazuh API admin original sin uso post Q45) en el próximo cycle
  3. Sync del last_login_at desde DO audit log → tabla user_last_activity (job separado, Q3 2026)
  4. Exemption review quarterly para viewer-prod y developer-staging (si siguen sin uso a 6 meses, se eliminan)
  5. Extender al frontend dashboard cuando se agregue auth de usuarios admin del dashboard de Fintrixs (próxima feature)

9. Paquete de evidencia ​

q48-inactive-users-20260527.tar.gz contiene:

  • audit-inactive-users.sh — script real ejecutable
  • q48-audit-schema.sql — DDL de las tablas
  • seed-current-state.sql — snapshot inicial de los 22 usuarios
  • audit-run-20260527.log — log de la corrida real del 2026-05-27
  • demo-end-to-end-transcript.txt — transcripción del demo del disable
  • user-activity-snapshot.txt — output SELECT * FROM user_last_activity
  • disable-log-snapshot.txt — output SELECT * FROM account_disable_log
  • pci-q48-inactive-users-audit.yml — workflow definition
  • gen_terminal_screenshots.py — script de render de PNGs
  • screenshots/*.png — los 9 terminales embebidos

Reproducible end-to-end con acceso al cluster prod + DBaaS staging.

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