ums

ADR-0062: Configuración Serilog Segura de PII (HARDENING-04)

Estado: Aceptado
Fecha: 2026-05-24
Responsable: Arquitectura
Disposición Evolith: Propuesto para adopción en Evolith — el patrón es neutro en cuanto al runtime; aplicable a cualquier satélite .NET que use Serilog
Relacionados:


Contexto

UMS procesa información de identificación personal (PII): direcciones de correo electrónico, referencias de identidad, contraseñas, tokens e IDs nacionales. ADR-0053 exige logging estructurado vía Serilog, pero un logging estructurado sin protección crea riesgo de filtrado de PII:

// Riesgo: el email se filtra a todos los sinks de log
_logger.LogInformation("User {Email} activated by {ActorId}", user.Email, actorId);

Existen tres capas de riesgo:

  1. Captura explícita — el desarrollador registra deliberadamente un campo PII por nombre
  2. Destructuring — las expansiones {@object} de Serilog registran todas las propiedades de una clase, incluyendo campos PII
  3. Filtrado por texto libre — interpolación de cadenas o plantillas de mensajes que contienen cadenas con forma de email

Por qué enmascaramiento por nombre de propiedad vs. anotaciones de atributo

El enfoque de anotaciones ([Sensitive] en propiedades de dominio) acopla la capa de Dominio a una librería de logging, violando la pureza del dominio. El enmascaramiento por nombre de propiedad en el nivel del pipeline de Serilog no requiere cambios en el dominio.


Decisión

Aplicar el enmascaramiento de PII a nivel del pipeline de Serilog mediante dos mecanismos complementarios: una política de destructuring y un enricher de eventos de log.

1. PiiMaskingPolicyIDestructuringPolicy de Serilog

Registrado vía .Destructure.With<PiiMaskingPolicy>(). Intercepta el destructuring de objetos antes de que cualquier sink lo procese.

La política intercepta a nivel del nombre de propiedad dentro del enricher (ver más abajo) — el método TryDestructure devuelve false para que Serilog continúe la destrucción normal; el enricher luego escanea y redacta.

2. PiiSanitizerEnricherILogEventEnricher de Serilog

Registrado vía .Enrich.With<PiiSanitizerEnricher>(). Se ejecuta después del renderizado del template del mensaje, escaneando todas las propiedades escalares de tipo cadena:

// Nombres de propiedad enmascarados (insensible a mayúsculas/minúsculas):
"email", "emailaddress", "mail",
"password", "passwordhash", "passwordtext",
"identityreference",
"token", "accesstoken", "refreshtoken", "bearertoken", "idtoken",
"secret", "apikey", "apisecret", "clientsecret",
"ssn", "nationalid", "taxid"

Barrido por regex de email — cualquier valor escalar de texto libre que coincida con [^@\s]+@[^@\s]+\.[^@\s]+ se enmascara parcialmente: jo***@***.com. Esto captura filtraciones a través de propiedades con nombres no-PII.

3. LoggingExtensions.ConfigureUmsSerilog

Único método de extensión que configura completamente Serilog:

builder.Host.UseSerilog((ctx, cfg) => cfg.ConfigureUmsSerilog(ctx));

Estrategia de salida:

Entorno Formato Justificación
Development Consola con texto coloreado Legible por humanos; prefijo trace/correlación visible
Staging/Production JSON compacto (CompactJsonFormatter) Legible por máquinas; consumido por Fluentd / drivers de log de contenedor

Configuración (appsettings.json):

"Observability": {
  "Logging": {
    "ConsoleFormat": "CompactJson",   // "Text" o "CompactJson"
    "MinimumLevel": "Information",    // cualquier nivel de Serilog
    "OutputTemplate": "..."           // solo para modo Text
  }
}

Enrichers siempre aplicados:

Sobreescrituras de nivel:

.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
.MinimumLevel.Override("Microsoft.Hosting.Lifetime", LogEventLevel.Information)
.MinimumLevel.Override("Microsoft.EntityFrameworkCore.Database.Command", LogEventLevel.Warning)

4. Patrones prohibidos y requeridos de log

Prohibidos:

// Concatenación de cadenas — sin campos estructurados
_logger.LogInformation("User " + userId);
// Volcado de objeto no estructurado
_logger.LogInformation(user.ToString());
// Valor PII directo en el template
_logger.LogInformation("Email: {email}", user.Email);

Requeridos:

// Campos estructurados con nombres no-PII
_logger.LogInformation("User {UserId} activated by {ActorId}", userId, actorId);
// Cuando el nombre de campo PII es inevitable, el enricher lo enmascarará
_logger.LogInformation("Verified {Email}", maskedForDisplay);

Consecuencias

Positivas

Compromisos

Nota operacional

Para enviar logs a un sink remoto (Seq, Elasticsearch, Application Insights, Loki):

  1. Añadir el paquete NuGet del sink a Ums.Presentation
  2. Configurar el endpoint en appsettings.json bajo la sección "Serilog"
  3. Serilog lee su propia configuración de forma nativa — no se requieren cambios en el código

Checklist de Extracción Evolith

Los siguientes tienen namespace de UMS pero son trivialmente portables:


Registro ADR CP-06 Logging PII ADR-0053 OTel