E2 — Seguridad de Base de Datos¶
Competencia CE0224 · Usuarios, Roles, Backup, Auditoría y Optimización
Proyecto: LOGYX — Sistema Operativo Logístico Colaborativo para PYMEs
Equipo: Jorge Gutiérrez Miranda · Fabrizio Sanchez Saravia · Alex Coila Jarita
Motor: PostgreSQL 16
Versión: 1.0 · Junio 2026
1. Modelo de Usuarios y Roles de Base de Datos¶
LOGYX implementa el principio de mínimo privilegio: cada componente del sistema tiene su propio usuario de base de datos con acceso limitado estrictamente a las operaciones que necesita.
1.1 Roles de Base de Datos¶
-- ============================================================
-- LOGYX · Creación de roles PostgreSQL
-- ============================================================
-- Rol base de solo lectura (heredado por roles de monitoreo)
CREATE ROLE logyx_readonly;
-- Rol para la aplicación backend (Quarkus Monolith)
CREATE ROLE logyx_app;
-- Rol para el módulo de analítica / reportes
CREATE ROLE logyx_analytics;
-- Rol para DBA (mantenimiento y migraciones)
CREATE ROLE logyx_dba;
1.2 Permisos por Rol¶
Rol logyx_app — Backend principal (Quarkus)¶
-- Otorgar uso del schema público
GRANT USAGE ON SCHEMA public TO logyx_app;
-- SELECT, INSERT, UPDATE, DELETE en tablas operacionales
GRANT SELECT, INSERT, UPDATE, DELETE ON
organizations,
profiles,
vehicles,
drivers,
shipment_requests,
auctions,
bids,
counter_offers,
carrier_trips,
trip_bookings,
messages,
shipments,
shipment_stops,
reviews,
reputation_scores,
documents,
notifications,
incidents,
incident_comments,
smart_load_suggestions,
cost_calculations
TO logyx_app;
-- Solo INSERT en audit_log (no puede borrar ni modificar auditoría)
GRANT INSERT ON audit_log TO logyx_app;
-- Solo SELECT en audit_log (para lecturas del operador)
GRANT SELECT ON audit_log TO logyx_app;
-- Uso de funciones de negocio
GRANT EXECUTE ON FUNCTION
fn_generate_tracking_code(),
fn_calculate_bayesian_rating(UUID),
fn_calculate_composite_score(UUID),
fn_check_vehicle_capacity(UUID, DECIMAL),
fn_contains_contact_info(TEXT),
fn_close_expired_auctions()
TO logyx_app;
-- Acceso a secuencias (para las columnas con DEFAULT uuid_generate_v4())
GRANT USAGE ON ALL SEQUENCES IN SCHEMA public TO logyx_app;
Rol logyx_analytics — Reportes y dashboards¶
GRANT USAGE ON SCHEMA public TO logyx_analytics;
-- Solo lectura en tablas (sin datos PII sensibles)
GRANT SELECT ON
organizations,
vehicles,
shipment_requests,
auctions,
bids,
shipments,
shipment_stops,
reviews,
reputation_scores,
cost_calculations,
incidents,
carrier_trips,
trip_bookings
TO logyx_analytics;
-- Sin acceso a: profiles (contiene passwords y PII), messages, documents, notifications
Rol logyx_readonly — Monitoreo y diagnóstico¶
GRANT USAGE ON SCHEMA public TO logyx_readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO logyx_readonly;
-- Sin acceso a profiles (PASSWORD_HASH)
REVOKE SELECT ON profiles FROM logyx_readonly;
Rol logyx_dba — Administrador (solo para migraciones y mantenimiento)¶
-- logyx_dba tiene acceso completo al schema para ejecutar migraciones Flyway
GRANT ALL PRIVILEGES ON SCHEMA public TO logyx_dba;
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO logyx_dba;
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO logyx_dba;
GRANT ALL PRIVILEGES ON ALL FUNCTIONS IN SCHEMA public TO logyx_dba;
1.3 Usuarios de Base de Datos¶
-- ============================================================
-- Creación de usuarios (contraseñas en variables de entorno)
-- ============================================================
CREATE USER logyx_app_user
WITH PASSWORD :'LOGYX_APP_DB_PASSWORD'
CONNECTION LIMIT 50;
GRANT logyx_app TO logyx_app_user;
-- ---
CREATE USER logyx_analytics_user
WITH PASSWORD :'LOGYX_ANALYTICS_DB_PASSWORD'
CONNECTION LIMIT 10;
GRANT logyx_analytics TO logyx_analytics_user;
-- ---
CREATE USER logyx_dba_user
WITH PASSWORD :'LOGYX_DBA_DB_PASSWORD'
CONNECTION LIMIT 5;
GRANT logyx_dba TO logyx_dba_user;
1.4 Separación de Acceso por Entorno¶
| Entorno | Host | Puerto | Usuario activo |
|---|---|---|---|
| Desarrollo | localhost | 5432 | logyx_app_user |
| Staging | db-staging.logyx.internal | 5432 | logyx_app_user |
| Producción | db-prod.logyx.internal | 5432 | logyx_app_user (sin dba) |
| Analytics | db-replica.logyx.internal | 5433 | logyx_analytics_user |
Estado de implementación (verificado 2026-07-06): ⬜ No implementado en el MVP actual.
docker-compose.ymlcrea un único usuario (POSTGRES_USER=logyx_app, sinCONNECTION LIMITni roles separados) y ninguna migración Flyway crealogyx_readonly/logyx_analytics/logyx_dbani los usuarios*_userde esta sección. El backend Quarkus se conecta hoy con ese único usuario para todo (lectura y escritura de todas las tablas). Este diseño de mínimo privilegio queda documentado como objetivo de Fase 2 (junto con RLS, ver sección 2.3) — no es una regresión de este sprint, nunca se llegó a implementar desde que se escribió este documento.
2. Seguridad de Datos¶
2.1 Cifrado en Reposo¶
| Capa | Mecanismo |
|---|---|
| Contraseñas de usuario | bcrypt, cost factor 12 (password_hash) |
| Datos de configuración sensibles | Variables de entorno (nunca en código) |
| Disco del servidor PostgreSQL | Cifrado del volumen (AES-256 en prod) |
| Backups | Cifrados con AES-256 antes de subir a storage |
-- Ejemplo: verificar que nunca se almacene contraseña en texto plano.
-- La función de validación está en la capa de aplicación (Quarkus):
-- BCrypt.checkpw(plainPassword, profile.getPasswordHash())
-- La BD solo recibe el hash final.
-- Restricción: password_hash no puede ser un string corto (sería texto plano)
ALTER TABLE profiles
ADD CONSTRAINT chk_profiles_password_hash_format
CHECK (password_hash IS NULL OR LENGTH(password_hash) >= 60);
2.2 Cifrado en Tránsito¶
-- postgresql.conf — configuración de TLS
-- ssl = on
-- ssl_cert_file = '/etc/ssl/certs/logyx-db.crt'
-- ssl_key_file = '/etc/ssl/private/logyx-db.key'
-- ssl_min_protocol_version = 'TLSv1.3'
-- pg_hba.conf — forzar SSL para todos los hosts remotos
-- host logyx_db all 0.0.0.0/0 scram-sha-256
-- hostssl logyx_db all 0.0.0.0/0 scram-sha-256
2.3 Row-Level Security (RLS)¶
Las políticas de RLS aseguran que un transportista no pueda leer los datos de otro transportista directamente en consultas que lleguen sin pasar por la capa de aplicación.
-- Habilitar RLS en la tabla de solicitudes de carga
ALTER TABLE shipment_requests ENABLE ROW LEVEL SECURITY;
-- Una PYME solo ve sus propias solicitudes en acceso directo
CREATE POLICY rls_requests_owner
ON shipment_requests
FOR ALL
TO logyx_app
USING (requester_org_id = current_setting('app.current_org_id')::UUID);
-- El operador ve todo
CREATE POLICY rls_requests_operator
ON shipment_requests
FOR SELECT
TO logyx_app
USING (
EXISTS (
SELECT 1 FROM organizations
WHERE id = current_setting('app.current_org_id')::UUID
AND type IN ('operator', 'admin')
)
);
-- Habilitar RLS en mensajes de negociación
ALTER TABLE messages ENABLE ROW LEVEL SECURITY;
CREATE POLICY rls_messages_participant
ON messages
FOR ALL
TO logyx_app
USING (
carrier_org_id = current_setting('app.current_org_id')::UUID
OR
(SELECT requester_org_id FROM shipment_requests
WHERE id = messages.request_id) =
current_setting('app.current_org_id')::UUID
);
La aplicación Quarkus establece
SET app.current_org_id = '<org_id>'al inicio de cada request autenticado, antes de ejecutar cualquier consulta.Estado de implementación (verificado 2026-07-06): ⬜ No implementado en el MVP actual. No existe ninguna migración (
src/main/resources/db/migration/) que ejecuteALTER TABLE ... ENABLE ROW LEVEL SECURITYni que cree las políticasrls_*de arriba. El datasource de Quarkus (quarkus.datasource.username=${DB_USER:logyx_app}, verapplication.properties) tampoco ejecutaSET app.current_org_iden ningún request — de hecho el usuariologyx_appque usa hoy el backend es el único rol de BD que existe (creado por la variablePOSTGRES_USERdedocker-compose.yml), no el modelo de roles de mínimo privilegio de la sección 1 (logyx_readonly/logyx_app/logyx_analytics/logyx_dbacomo roles separados conGRANTacotado) — esos tampoco tienen migración que los cree. Hoy el único control de acceso por organización es a nivel de aplicación (SecurityContext.getOrgId()+ filtrosWHERE requester_org_id = ...en cada repository/service, verificado módulo por módulo enROADMAP.md). RLS es una defensa en profundidad adicional (protege contra un bug de aplicación que olvide el filtro, o contra acceso directo a la BD con credenciales delogyx_app) — vale la pena implementarla antes de producción, pero no bloquea el MVP mientras el filtro a nivel de aplicación sea consistente. Recomendado para Fase 2 / antes de exponer la BD fuera de la red interna.
3. Estrategia de Backup¶
3.1 Tipos de Backup¶
| Tipo | Frecuencia | Retención | Herramienta |
|---|---|---|---|
| Backup completo (full dump) | Diario (02:00 PET) | 30 días | pg_dump |
| Backup incremental (WAL archiving) | Continuo (cada transacción) | 7 días de WAL | pg_basebackup + WAL |
| Backup semanal (cold) | Domingo (01:00 PET) | 90 días | pg_dump comprimido |
| Snapshot de volumen | Diario | 14 días | Cloud provider snapshot |
3.2 Scripts de Backup¶
#!/bin/bash
# logyx_backup_daily.sh
# Ejecutado por cron: 0 2 * * * /opt/logyx/scripts/logyx_backup_daily.sh
set -euo pipefail
DB_NAME="logyx_db"
DB_USER="logyx_dba_user"
BACKUP_DIR="/mnt/backups/logyx"
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="${BACKUP_DIR}/logyx_full_${DATE}.dump"
ENCRYPT_KEY="${LOGYX_BACKUP_ENCRYPTION_KEY}"
# 1. Generar dump en formato custom (comprimido, restaurable parcialmente)
pg_dump \
--host=localhost \
--port=5432 \
--username="${DB_USER}" \
--format=custom \
--compress=9 \
--file="${BACKUP_FILE}" \
"${DB_NAME}"
# 2. Cifrar el archivo
openssl enc -aes-256-cbc -pbkdf2 -k "${ENCRYPT_KEY}" \
-in "${BACKUP_FILE}" \
-out "${BACKUP_FILE}.enc"
# 3. Eliminar el archivo sin cifrar
rm "${BACKUP_FILE}"
# 4. Subir a storage S3-compatible
aws s3 cp "${BACKUP_FILE}.enc" \
"s3://logyx-backups/db/$(date +%Y/%m/)/" \
--storage-class STANDARD_IA
# 5. Eliminar backups locales de más de 7 días
find "${BACKUP_DIR}" -name "*.enc" -mtime +7 -delete
echo "[$(date)] Backup completado: ${BACKUP_FILE}.enc"
3.3 Procedimiento de Restauración¶
# Restaurar a un punto específico (Point-in-Time Recovery)
# Paso 1: Descargar y descifrar
aws s3 cp "s3://logyx-backups/db/2026/06/logyx_full_20260629_020000.dump.enc" .
openssl enc -aes-256-cbc -pbkdf2 -d \
-k "${LOGYX_BACKUP_ENCRYPTION_KEY}" \
-in logyx_full_20260629_020000.dump.enc \
-out logyx_full_20260629_020000.dump
# Paso 2: Restaurar sobre una base nueva
createdb logyx_db_restored
pg_restore \
--host=localhost \
--username=logyx_dba_user \
--dbname=logyx_db_restored \
--no-owner \
--no-acl \
--jobs=4 \
logyx_full_20260629_020000.dump
echo "Restauración completada. Verificar integridad con: SELECT COUNT(*) FROM shipments;"
3.4 Prueba de Backups (RTO / RPO)¶
| Métrica | Objetivo | Procedimiento de verificación |
|---|---|---|
| RPO (máx. pérdida de datos) | < 1 hora | WAL archiving continuo |
| RTO (tiempo de recuperación) | < 2 horas | Prueba mensual de restauración en entorno staging |
| Verificación de integridad | Semanal | Restaurar en staging, ejecutar consultas de validación |
-- Script de validación post-restauración
DO $$
DECLARE
v_orgs INTEGER;
v_ships INTEGER;
v_reviews INTEGER;
BEGIN
SELECT COUNT(*) INTO v_orgs FROM organizations;
SELECT COUNT(*) INTO v_ships FROM shipments;
SELECT COUNT(*) INTO v_reviews FROM reviews;
ASSERT v_orgs > 0, 'ERROR: organizations vacía — restauración fallida';
ASSERT v_ships >= 0, 'ERROR: shipments inaccesible';
ASSERT v_reviews >= 0, 'ERROR: reviews inaccesible';
RAISE NOTICE 'Validación OK — orgs: %, shipments: %, reviews: %',
v_orgs, v_ships, v_reviews;
END $$;
4. Auditoría de Base de Datos¶
4.1 Mecanismo de Auditoría¶
La auditoría se implementa mediante triggers trg_audit_* (definidos en E2_Scripts.md, aplicados en la migración real V8__triggers.sql) que registran automáticamente INSERT/UPDATE en las tablas críticas.
Tablas auditadas (verificado contra V8__triggers.sql, 2026-07-06):
| Tabla | Eventos auditados | Razón | Estado |
|---|---|---|---|
shipments |
INSERT, UPDATE | Estado del envío y asignaciones | ✅ Implementado (trg_audit_shipments) |
bids |
INSERT, UPDATE | Trazabilidad de ofertas | ✅ Implementado (trg_audit_bids) |
auctions |
INSERT, UPDATE | Cierre y manipulación de subastas | ✅ Implementado (trg_audit_auctions) |
incidents |
INSERT, UPDATE | Resolución de disputas | ✅ Implementado (trg_audit_incidents) |
organizations |
INSERT, UPDATE | Cambios de verificación y trust_score | ⬜ Pendiente — no existe trg_audit_organizations en ninguna migración |
profiles |
INSERT, UPDATE | Cambios de rol y credenciales | ⬜ Pendiente — no existe trg_audit_profiles en ninguna migración |
Corrección (2026-07-06): esta tabla originalmente listaba las 6 filas como si todas estuvieran activas. Solo 4 de los 6 triggers de auditoría existen hoy. Esto es relevante porque RNF15 ("todas las acciones de negocio críticas deben quedar registradas") incluye explícitamente cambios de verificación de organización y de rol de perfil — ambos marcados como "críticos" en este mismo documento — que hoy no quedan en
audit_log. Además, la segunda consulta de ejemplo en la sección 4.2 (auditoría deorganizations) no devolverá resultados mientrastrg_audit_organizationsno se implemente; se deja documentada como referencia para cuando se agregue el trigger (candidato natural: junto con el Módulo 2 — Organizations, o el Módulo 20 — Admin, que ya exponePATCH /verifyy/suspend).
4.2 Consultas de Auditoría¶
-- Historial completo de cambios en un envío específico
SELECT
al.action,
al.old_values ->> 'status' AS estado_anterior,
al.new_values ->> 'status' AS estado_nuevo,
al.performed_at,
p.full_name AS ejecutado_por
FROM audit_log al
LEFT JOIN profiles p ON p.id = al.performed_by
WHERE al.table_name = 'shipments'
AND al.record_id = '00000000-0000-0000-0041-000000000001'
ORDER BY al.performed_at;
-- ---
-- Actividad sospechosa: organización que cambió de tipo en las últimas 24h
SELECT
al.record_id AS org_id,
al.old_values ->> 'type' AS tipo_anterior,
al.new_values ->> 'type' AS tipo_nuevo,
al.performed_at
FROM audit_log al
WHERE al.table_name = 'organizations'
AND al.action = 'UPDATE'
AND (al.old_values ->> 'type') != (al.new_values ->> 'type')
AND al.performed_at >= NOW() - INTERVAL '24 hours';
-- ---
-- Ofertas canceladas o rechazadas de un transportista en el último mes
SELECT
al.record_id AS bid_id,
al.old_values ->> 'status' AS estado_anterior,
al.new_values ->> 'status' AS estado_nuevo,
al.performed_at
FROM audit_log al
JOIN bids b ON b.id = al.record_id
WHERE al.table_name = 'bids'
AND al.action = 'UPDATE'
AND al.new_values ->> 'status' IN ('rejected', 'expired')
AND b.carrier_org_id = '00000000-0000-0000-0003-000000000001'
AND al.performed_at >= NOW() - INTERVAL '30 days'
ORDER BY al.performed_at DESC;
4.3 Integridad del Log de Auditoría¶
-- La tabla audit_log solo acepta INSERT (definido en roles).
-- Nadie puede UPDATE o DELETE registros de auditoría:
REVOKE UPDATE, DELETE ON audit_log FROM logyx_app;
REVOKE UPDATE, DELETE ON audit_log FROM logyx_dba;
-- Política de retención: mover a tabla de archivo tras 1 año
CREATE TABLE audit_log_archive (LIKE audit_log INCLUDING ALL);
-- Procedure de archivado (ejecutar mensualmente)
CREATE OR REPLACE PROCEDURE proc_archive_old_audit_log()
LANGUAGE plpgsql AS $$
BEGIN
INSERT INTO audit_log_archive
SELECT * FROM audit_log
WHERE performed_at < NOW() - INTERVAL '1 year';
DELETE FROM audit_log
WHERE performed_at < NOW() - INTERVAL '1 year';
RAISE NOTICE 'Archivado completado. Registros movidos a audit_log_archive.';
END;
$$;
5. Optimización y Rendimiento¶
5.1 Estrategia de Indexación¶
Los índices están definidos en V6__indexes.sql (ver E2_Scripts.md). La estrategia sigue tres principios:
- Índices compuestos para filtros frecuentes:
(status, required_date)en lugar de índices individuales. - Índices parciales para conjuntos activos:
WHERE status IN ('open', 'filling')para viajes activos — la mayoría de los viajes en la BD estarán en estadocompletedy no se buscan frecuentemente. - Sin índices en columnas de baja cardinalidad por sí solas (p.ej.
typeenorganizations), ya que PostgreSQL preferirá un seq scan.
5.2 Consultas Críticas y su Optimización¶
CQ-01: Marketplace — Cargas disponibles para un transportista¶
-- Búsqueda de solicitudes abiertas dentro de un radio geográfico
-- Índices usados: idx_requests_status_date, idx_requests_origin_geo
EXPLAIN ANALYZE
SELECT
sr.id,
sr.origin_address,
sr.destination_address,
sr.weight_kg,
sr.required_date,
sr.suggested_price,
o.trust_score AS pyme_trust_score,
cc.distance_km
FROM shipment_requests sr
JOIN organizations o ON o.id = sr.requester_org_id
LEFT JOIN cost_calculations cc ON cc.request_id = sr.id
WHERE sr.status = 'open'
AND sr.required_date >= CURRENT_DATE
-- Radio de 100 km alrededor de Lima (lat=-12.04, lng=-77.03)
AND (
6371 * acos(
cos(radians(-12.04)) * cos(radians(sr.origin_lat))
* cos(radians(sr.origin_lng) - radians(-77.03))
+ sin(radians(-12.04)) * sin(radians(sr.origin_lat))
)
) <= 100
ORDER BY sr.required_date ASC, o.trust_score DESC
LIMIT 20;
Optimizaciones aplicadas:
- Filtro status = 'open' activado por idx_requests_status_date.
- La columna price_floor es calculada (GENERATED), no requiere cálculo en runtime.
- LIMIT 20 evita escaneos completos de resultado.
CQ-02: Mis ofertas activas (Carrier dashboard)¶
-- Índice usado: idx_bids_carrier_status
SELECT
b.id,
b.amount,
b.status,
b.submitted_at,
sr.origin_address,
sr.destination_address,
a.closes_at
FROM bids b
JOIN auctions a ON a.id = b.auction_id
JOIN shipment_requests sr ON sr.id = a.request_id
WHERE b.carrier_org_id = $1
AND b.status = 'pending'
ORDER BY b.submitted_at DESC;
CQ-03: Notificaciones no leídas (llamada frecuente cada 30s)¶
-- Índice parcial usado: idx_notifications_org_unread (WHERE read = FALSE)
SELECT id, type, title, body, related_id, created_at
FROM notifications
WHERE org_id = $1
AND read = FALSE
ORDER BY created_at DESC
LIMIT 50;
5.3 Configuración de PostgreSQL para Producción¶
# postgresql.conf — parámetros de rendimiento para el servidor de producción
# (ajustados para 8GB RAM, 4 vCPU)
# Memoria
shared_buffers = 2GB # 25% de RAM total
effective_cache_size = 6GB # estimado de cache OS disponible
work_mem = 64MB # por operación de sorting
maintenance_work_mem = 512MB # para VACUUM, CREATE INDEX
# WAL y checkpoints
wal_buffers = 64MB
checkpoint_completion_target = 0.9
max_wal_size = 2GB
# Conexiones
max_connections = 100 # combinado con PgBouncer en producción
pool_mode = transaction # PgBouncer en modo transacción
# Autovacuum
autovacuum = on
autovacuum_vacuum_scale_factor = 0.05 # vacuumar si el 5% de filas cambió
autovacuum_analyze_scale_factor = 0.02
# Logging para análisis de queries lentos
log_min_duration_statement = 500 # loguear queries > 500ms
log_statement = 'ddl' # loguear solo DDL en prod (no DML)
5.4 Particionamiento (Planificado para Fase 2)¶
Para cuando el volumen de datos supere los 10 millones de registros, se implementará particionamiento por rango en las tablas de mayor crecimiento:
-- Ejemplo de particionamiento futuro en shipments por año
CREATE TABLE shipments (
-- ... columnas ...
created_at TIMESTAMP NOT NULL
) PARTITION BY RANGE (created_at);
CREATE TABLE shipments_2026 PARTITION OF shipments
FOR VALUES FROM ('2026-01-01') TO ('2027-01-01');
CREATE TABLE shipments_2027 PARTITION OF shipments
FOR VALUES FROM ('2027-01-01') TO ('2028-01-01');
5.5 Pool de Conexiones (PgBouncer)¶
# pgbouncer.ini
[databases]
logyx_db = host=db.logyx.internal port=5432 dbname=logyx_db
[pgbouncer]
pool_mode = transaction
max_client_conn = 200
default_pool_size = 20
min_pool_size = 5
reserve_pool_size = 5
reserve_pool_timeout = 3
server_idle_timeout = 600
6. Resumen de la Arquitectura de Seguridad¶
| Capa | Control implementado |
|---|---|
| Red | PostgreSQL solo accesible desde la red interna (Docker network / VPC) |
| Transporte | TLS 1.3 obligatorio para conexiones remotas |
| Autenticación | SCRAM-SHA-256 (reemplazo de MD5) |
| Autorización | Roles por mínimo privilegio + RLS por org_id |
| Datos en reposo | Cifrado de disco AES-256 en producción |
| Passwords | bcrypt cost=12 (nunca texto plano en BD) |
| Auditoría | Triggers automáticos + tabla audit_log inmutable |
| Backup | Diario + WAL continuo + cifrado AES-256 antes de subir |
| Disponibilidad | RPO < 1h, RTO < 2h, pruebas mensuales de restauración |
7. Cumplimiento Normativo (Ley N° 29733 y PCI-DSS)¶
Las obligaciones identificadas en el análisis de Gobierno de TI bajo COBIT 2019 (Gestión TI, Entregable 1, sección 1.7, y Entregable 3, sección 3.7) se satisfacen a nivel de datos con los siguientes controles:
| Obligación (Ley N° 29733) | Control a nivel de base de datos / backend |
|---|---|
| Consentimiento informado para tratamiento de datos (Art. 13) | Columna consent_accepted_at en la tabla de usuarios, poblada obligatoriamente durante el onboarding (RNF18, ver SRS sección 7.2); ningún registro se activa sin este campo |
| Registro de actividades de tratamiento (Art. 39, Reglamento) | La tabla audit_log (sección 4) registra creación, modificación y acceso a datos personales sensibles, con timestamp y usuario responsable |
| Medidas de seguridad técnicas — cifrado, control de acceso (Art. 39, Reglamento) | Cifrado en reposo AES-256 (sección 2.1), TLS 1.3 en tránsito (sección 2.2), RBAC + RLS por organización (secciones 1.2 y 2.3) |
| Notificación de brechas a la ANPD en ≤72 horas (Art. 39bis, Reglamento) | Procedimiento operativo documentado en Gestión TI (Entregable 3, sección 3.7); a nivel técnico, las alertas de integridad del log de auditoría (sección 4.3) son el disparador de detección temprana |
| Designación de responsable del tratamiento (Art. 37, Reglamento) | Asignado formalmente al CISO funcional del equipo (Gestión TI, Entregable 3, sección 3.7) |
PCI-DSS. LOGYX no almacena datos de tarjeta en ninguna tabla de la base de datos: el procesamiento de pagos se delega íntegramente a una pasarela certificada PCI-DSS (tokenización delegada). Esta restricción de diseño está reflejada en el RNF20 del SRS (Entregable 1, sección 7.2) y no requiere columnas ni cifrado adicional en el esquema, ya que el dato nunca llega al sistema.
LOGYX · E2 Seguridad de Base de Datos · Competencia CE0224 · Versión 1.0 · Junio 2026