Auditoría de acciones en Azure Blob Storage: una bitácora sencilla y segura
Daniel J. Saldaña- 17 de octubre de 2026
- Puntuación de feedback

Cuando alguien cambia una conexión de Azure, refresca un inventario o ejecuta una operación administrativa desde un dashboard, es útil poder responder tres preguntas:
- ¿Qué ocurrió?
- ¿Cuándo ocurrió?
- ¿Qué identidad de la aplicación lo solicitó?
Para empezar no necesitas una plataforma de auditoría compleja. Goliat Dashboard usa una pieza muy pequeña: guardar un documento JSON por acción en un contenedor privado de Azure Blob Storage.
Acción protegida en el dashboard ↓Evento de auditoría con datos mínimos ↓Blob privado: actions/2026-10-17/... ↓Proceso de consulta, retención o exportaciónNo es un reemplazo de Azure Activity Log, Log Analytics ni una solución SIEM. Es la bitácora de lo que sucede dentro de tu aplicación, donde Azure no conoce el significado de «un usuario conectó esta suscripción a este proyecto».
Diseña primero el evento, no el contenedor
Un evento de auditoría debe ser pequeño, estable y seguro de transportar. Este contrato es suficiente para muchas integraciones:
type AuditEvent = { id: string; occurredAt: string; action: 'azure_connection.created' | 'inventory.refreshed' | 'resource.exported'; outcome: 'success' | 'failure'; actorId: string; projectId?: string; target?: { kind: 'azure-connection' | 'resource' | 'report'; id: string; }; requestId: string; metadata?: Record<string, string | number | boolean>;};No copies aquí la cabecera Authorization, tokens de Azure, secretos, cuerpos de formularios ni el correo completo si un identificador interno basta. La auditoría tiende a retenerse más tiempo que los datos operativos, así que conviene aplicar minimización desde el principio.
Una ruta de blobs que luego puedas consultar
Guardar un blob por evento hace que las escrituras sean simples y evita problemas de concurrencia. Una convención de nombre útil es:
actions/2026-10-17/2026-10-17T08-43-21-122Z_5d2e.jsonEl prefijo de fecha permite localizar, exportar o aplicar políticas de ciclo de vida por periodos. El sufijo aleatorio evita colisiones cuando varias solicitudes ocurren en el mismo milisegundo.
Autenticación sin claves en el código
En el recurso que ejecuta el backend, configura una identidad administrada y otórgale el rol de datos de Blob con el mínimo alcance posible: idealmente el contenedor de auditoría, o la cuenta de almacenamiento si tu organización no permite un alcance más fino.
Para local puedes usar DefaultAzureCredential; no hace falta copiar una clave de la cuenta de almacenamiento a .env.
npm install @azure/identity @azure/storage-blobimport { DefaultAzureCredential } from '@azure/identity';import { BlobServiceClient } from '@azure/storage-blob';import { randomUUID } from 'node:crypto';
const accountUrl = process.env.AZURE_AUDIT_STORAGE_URL;if (!accountUrl) throw new Error('Falta AZURE_AUDIT_STORAGE_URL');
const service = new BlobServiceClient(accountUrl, new DefaultAzureCredential());const container = service.getContainerClient('dashboard-audit');
function blobName(event: AuditEvent) { const day = event.occurredAt.slice(0, 10); const safeTime = event.occurredAt.replaceAll(':', '-').replace('.', '-'); return `actions/${day}/${safeTime}_${event.id}.json`;}
export async function writeAuditEvent(event: Omit<AuditEvent, 'id' | 'occurredAt'>) { const record: AuditEvent = { ...event, id: randomUUID(), occurredAt: new Date().toISOString(), };
const payload = JSON.stringify(record); const blob = container.getBlockBlobClient(blobName(record));
await blob.upload(payload, Buffer.byteLength(payload), { blobHTTPHeaders: { blobContentType: 'application/json; charset=utf-8' }, });
return record;}El contenedor debe ser privado. La aplicación escribe mediante Microsoft Entra ID; ni el navegador ni una URL pública de blob necesitan acceso. Si ejecutas el servicio en Azure, evita habilitar claves compartidas sólo para simplificar esta integración.
Llámalo desde una acción de negocio
La auditoría tiene sentido cuando queda al lado de la acción que explica. Este handler ilustra el orden:
export async function POST({ locals, request }: ApiContext) { const user = await requireUser(locals); const input = await validateConnectionInput(request);
try { const connection = await createAzureConnection(user.organizationId, input);
await writeAuditEvent({ action: 'azure_connection.created', outcome: 'success', actorId: user.id, projectId: connection.projectId, target: { kind: 'azure-connection', id: connection.id }, requestId: locals.requestId, });
return Response.json(connection, { status: 201 }); } catch (error) { await writeAuditEvent({ action: 'azure_connection.created', outcome: 'failure', actorId: user.id, requestId: locals.requestId, metadata: { errorType: error instanceof Error ? error.name : 'UnknownError' }, }); throw error; }}Decide por acción si un fallo al auditar debe bloquearla. Para una exportación no crítica puede ser aceptable encolar un reintento y avisar al sistema de observabilidad. Para un cambio de permisos o un borrado sensible quizá prefieras fallar la operación si no se puede registrar. Lo que no recomiendo es ignorar silenciosamente el fallo.
Consulta: separa lectura de escritura
El backend que ejecuta acciones sólo necesita escribir. Una herramienta de soporte o un trabajo de exportación puede recibir permisos de lectura por separado y recorrer el prefijo de un día:
for await (const item of container.listBlobsFlat({ prefix: 'actions/2026-10-17/' })) { console.log(item.name);}No construyas una pantalla que descargue todos los JSON de la cuenta de almacenamiento cada vez que se abre. Para una vista administrativa:
- filtra por fecha, proyecto o
requestId; - pagina los resultados;
- indexa los eventos en una base de datos o Log Analytics si el volumen crece;
- conserva Blob Storage como archivo económico y verificable.
Retención y protección: decide cuánto dura cada dato
Antes de desplegar, define una política de retención con seguridad, legal y producto. Blob Storage puede ayudarte con reglas de ciclo de vida y protección frente a borrados accidentales, pero ninguna opción sustituye una decisión sobre qué guardar y durante cuánto tiempo.
Una configuración inicial razonable suele incluir:
| Medida | Objetivo |
|---|---|
| Contenedor sin acceso público | Evitar que una bitácora se convierta en un índice de actividad |
| Identidad administrada y RBAC | Eliminar claves de larga vida del código |
| Versionado o protección contra borrado | Reducir el riesgo de pérdida accidental |
| Regla de ciclo de vida | Borrar o archivar cuando termina la retención |
| Alertas de errores de escritura | Detectar que hay acciones sin registrar |
Si los eventos deben ser inmutables por requisitos regulatorios, estudia las políticas de inmutabilidad de Blob Storage con el equipo responsable. Es una decisión de gobierno, no una casilla que deba activarse sin un periodo de retención validado.
Lista de comprobación
- El evento contiene identificadores internos, no secretos ni datos personales innecesarios.
- El nombre del blob incluye fecha y un identificador único.
- El contenedor no permite acceso anónimo.
- La identidad de ejecución sólo tiene permisos de datos necesarios.
- Los errores de auditoría se registran y tienen una estrategia de reintento o bloqueo.
- Hay una política explícita de retención, borrado y acceso de lectura.
La guía de Azure Blob Storage para JavaScript cubre los fundamentos del cliente y la autenticación. Con esta integración, cada nuevo flujo del dashboard puede emitir el mismo contrato de evento sin acoplarse a una base de datos concreta. Es una base pequeña, pero deja preparada la ruta hacia exportaciones, investigación de incidencias y trazabilidad real.


