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."
Fecha de extracción2026-05-27
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
EstadoRESUELTO — Proceso documentado, automatización activa, ciclo de disable demostrado end-to-end con rechazo a nivel DB engine
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

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

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