The Infrastructure Hub -- Parte 6
Automatizacion del CAB: Aprobaciones Firmadas y Evidencia de Compliance
El Problema
En el articulo 4 construimos el workflow del CAB: la IA resume el plan de Terraform, genera una evaluacion de riesgo, crea un Change Request, y el CAB lo revisa en Backstage. El workflow funciona. Los cambios fluyen desde el PR hasta produccion con la evidencia correcta.
Entonces llega el auditor.
“Muestrame prueba de que este cambio fue aprobado por la persona correcta.” Le ensenas el Change Request en la base de datos. “Como se que ese registro no fue modificado despues?” No tienes respuesta.
“Muestrame cada cambio que toco produccion en el ultimo trimestre, con la cadena de aprobacion.” Puedes hacer una query a la base de datos. Pero como verifica el auditor que los datos no han sido manipulados? Un registro en base de datos es solo una fila. Cualquiera con acceso de escritura puede cambiarlo.
Esta es la diferencia entre “tenemos un proceso” y “podemos demostrar que el proceso se siguio.” Las industrias reguladas — finanzas, energia, salud, cualquier cosa bajo NIS2 — necesitan lo segundo. Y con las regulaciones de identidad digital de la UE, “prueba” cada vez mas significa prueba criptografica.
La solucion: firmar cada aprobacion con una firma digital post-quantum. La identidad del aprobador se verifica a traves de QuantumID. La firma usa ML-DSA — quantum-safe, no repudiable, y verificable por cualquier auditor sin acceso a tus sistemas internos.
La Solucion
Tres adiciones al workflow del CAB del articulo 4:
1. Aprobaciones firmadas — Cuando un revisor del CAB hace clic en “Approve”, el sistema firma el registro de aprobacion con la clave ML-DSA del revisor via QuantumAPI. La firma prueba quien aprobo, que aprobo, y cuando — y no se puede falsificar ni antedatar.
2. Paquetes de evidencia sellados — La evidencia completa (resumen del plan, evaluacion de riesgo, URL del PR, resultados de escaneo, resultados de tests, firma de aprobacion) se hashea y firma como un unico paquete. Si modificas cualquier campo, la firma se rompe.
3. Reportes de compliance — Un job programado genera reportes de auditoria desde los registros firmados: todos los cambios en un periodo, quien aprobo cada uno, niveles de riesgo, planes de rollback. Exportable como PDF o JSON para auditores.
Execute
1. El Endpoint de Aprobacion Firmada
Cuando un revisor del CAB aprueba un cambio, el backend firma la aprobacion:
app.MapPost("/api/cab/approve", async (ApprovalRequest request, IConfiguration config) =>
{
var apiKey = config["QuantumApi:ApiKey"];
if (string.IsNullOrEmpty(apiKey))
return Results.Json(new { error = "QuantumAPI not configured." }, statusCode: 503);
// Build the approval payload — everything the auditor needs to verify
var payload = new ApprovalPayload
{
ChangeRequestId = request.ChangeRequestId,
ApprovedBy = request.ApprovedBy,
ApprovedAt = DateTimeOffset.UtcNow,
Module = request.Module,
Client = request.Client,
RiskLevel = request.RiskLevel,
PlanHash = request.PlanHash,
};
// Serialize to deterministic JSON (property order matches class definition, no whitespace)
var canonical = JsonSerializer.Serialize(payload, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = false,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
});
// Sign with ML-DSA via QuantumAPI
var client = new QuantumApiClient(new QuantumApiOptions { ApiKey = apiKey });
var signature = await client.Signing.SignAsync(new SignRequest
{
Data = canonical,
Algorithm = "ML-DSA-65",
});
// Store the signed approval
var connStr = config["Rag:PostgresConnection"];
if (!string.IsNullOrEmpty(connStr))
{
await using var dataSource = NpgsqlDataSource.Create(connStr);
await using var cmd = dataSource.CreateCommand();
cmd.CommandText = """
INSERT INTO cab_approvals
(change_request_id, approved_by, approved_at, module, client,
risk_level, plan_hash, payload_json, signature, key_id)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
""";
cmd.Parameters.AddWithValue(payload.ChangeRequestId);
cmd.Parameters.AddWithValue(payload.ApprovedBy);
cmd.Parameters.AddWithValue(payload.ApprovedAt);
cmd.Parameters.AddWithValue(payload.Module);
cmd.Parameters.AddWithValue(payload.Client);
cmd.Parameters.AddWithValue(payload.RiskLevel);
cmd.Parameters.AddWithValue(payload.PlanHash);
cmd.Parameters.AddWithValue(canonical);
cmd.Parameters.AddWithValue(signature.Signature);
cmd.Parameters.AddWithValue(signature.KeyId);
await cmd.ExecuteNonQueryAsync();
}
return Results.Ok(new
{
approved = true,
signature = signature.Signature,
keyId = signature.KeyId,
algorithm = "ML-DSA-65",
timestamp = payload.ApprovedAt,
});
});
record ApprovalRequest(
string ChangeRequestId,
string ApprovedBy,
string Module,
string Client,
string RiskLevel,
string PlanHash);
record ApprovalPayload
{
public string ChangeRequestId { get; init; } = default!;
public string ApprovedBy { get; init; } = default!;
public DateTimeOffset ApprovedAt { get; init; }
public string Module { get; init; } = default!;
public string Client { get; init; } = default!;
public string RiskLevel { get; init; } = default!;
public string PlanHash { get; init; } = default!;
}
El PlanHash es un hash SHA-256 de la salida del plan de Terraform. Vincula la aprobacion a un plan concreto — no puedes aprobar un plan y aplicar otro diferente.
El Schema de Base de Datos
CREATE TABLE cab_approvals (
id SERIAL PRIMARY KEY,
change_request_id VARCHAR(100) NOT NULL,
approved_by VARCHAR(255) NOT NULL,
approved_at TIMESTAMPTZ NOT NULL,
module VARCHAR(255) NOT NULL,
client VARCHAR(100),
risk_level VARCHAR(20) NOT NULL,
plan_hash VARCHAR(64) NOT NULL,
payload_json TEXT NOT NULL,
signature TEXT NOT NULL,
key_id VARCHAR(100) NOT NULL,
UNIQUE (change_request_id)
);
CREATE INDEX idx_approvals_module ON cab_approvals(module);
CREATE INDEX idx_approvals_client ON cab_approvals(client);
CREATE INDEX idx_approvals_date ON cab_approvals(approved_at);
2. Endpoint de Verificacion
Un auditor — o una comprobacion automatica de compliance — puede verificar cualquier aprobacion:
app.MapGet("/api/cab/verify/{changeRequestId}", async (string changeRequestId, IConfiguration config) =>
{
var apiKey = config["QuantumApi:ApiKey"];
var connStr = config["Rag:PostgresConnection"];
if (string.IsNullOrEmpty(apiKey) || string.IsNullOrEmpty(connStr))
return Results.Json(new { error = "Not configured." }, statusCode: 503);
await using var dataSource = NpgsqlDataSource.Create(connStr);
await using var cmd = dataSource.CreateCommand();
cmd.CommandText = """
SELECT payload_json, signature, key_id, approved_by, approved_at
FROM cab_approvals
WHERE change_request_id = $1
""";
cmd.Parameters.AddWithValue(changeRequestId);
await using var reader = await cmd.ExecuteReaderAsync();
if (!await reader.ReadAsync())
return Results.NotFound(new { error = "Approval not found." });
var payloadJson = reader.GetString(0);
var signature = reader.GetString(1);
var keyId = reader.GetString(2);
var approvedBy = reader.GetString(3);
var approvedAt = reader.GetFieldValue<DateTimeOffset>(4);
// Verify the signature with QuantumAPI
var client = new QuantumApiClient(new QuantumApiOptions { ApiKey = apiKey });
var verification = await client.Signing.VerifyAsync(new VerifyRequest
{
Data = payloadJson,
Signature = signature,
KeyId = keyId,
});
return Results.Ok(new
{
changeRequestId,
approvedBy,
approvedAt,
signatureValid = verification.Valid,
algorithm = "ML-DSA-65",
tampered = !verification.Valid,
});
});
Si signatureValid es false, el registro fue manipulado. El auditor no necesita acceso a tu base de datos — puede llamar a este endpoint con el ID del change request y obtener una prueba criptografica.
3. Paquetes de Evidencia Sellados
El paquete de evidencia del articulo 4 incluye: resumen del plan, evaluacion de riesgo, URL del PR, aprobadores, resultados de escaneo, resultados de tests, y URL del pipeline. Sellamos el paquete completo con una firma:
app.MapPost("/api/cab/seal-evidence", async (EvidencePackage evidence, IConfiguration config) =>
{
var apiKey = config["QuantumApi:ApiKey"];
if (string.IsNullOrEmpty(apiKey))
return Results.Json(new { error = "QuantumAPI not configured." }, statusCode: 503);
// Canonical JSON of the entire evidence package
var canonical = JsonSerializer.Serialize(evidence, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = false,
});
// Hash the package
var hash = Convert.ToHexString(
SHA256.HashData(Encoding.UTF8.GetBytes(canonical))).ToLowerInvariant();
// Sign the hash
var client = new QuantumApiClient(new QuantumApiOptions { ApiKey = apiKey });
var signature = await client.Signing.SignAsync(new SignRequest
{
Data = hash,
Algorithm = "ML-DSA-65",
});
return Results.Ok(new
{
evidenceHash = hash,
signature = signature.Signature,
keyId = signature.KeyId,
algorithm = "ML-DSA-65",
sealedAt = DateTimeOffset.UtcNow,
});
});
record EvidencePackage(
string ChangeRequestId,
string Module,
string Client,
string PlanSummary,
string RiskLevel,
string[] RiskFactors,
string RollbackPlan,
string PrUrl,
string[] PrApprovers,
string SecurityScan,
string TestResults,
string PipelineUrl,
string ApprovedBy,
DateTimeOffset ApprovedAt);
4. Generacion de Reportes de Compliance
Un endpoint programado genera reportes de auditoria:
app.MapGet("/api/cab/report", async (
string? client, int? days, string? format, IConfiguration config) =>
{
var connStr = config["Rag:PostgresConnection"];
if (string.IsNullOrEmpty(connStr))
return Results.Json(new { error = "Not configured." }, statusCode: 503);
var daysValue = days ?? 90;
await using var dataSource = NpgsqlDataSource.Create(connStr);
await using var cmd = dataSource.CreateCommand();
cmd.CommandText = """
SELECT change_request_id, approved_by, approved_at, module, client,
risk_level, plan_hash, signature, key_id
FROM cab_approvals
WHERE approved_at >= NOW() - INTERVAL '1 day' * $1
AND ($2 = '' OR client = $2)
ORDER BY approved_at DESC
""";
cmd.Parameters.AddWithValue(daysValue);
cmd.Parameters.AddWithValue(client ?? "");
var records = new List<object>();
await using var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
records.Add(new
{
ChangeRequestId = reader.GetString(0),
ApprovedBy = reader.GetString(1),
ApprovedAt = reader.GetFieldValue<DateTimeOffset>(2),
Module = reader.GetString(3),
Client = reader.IsDBNull(4) ? "internal" : reader.GetString(4),
RiskLevel = reader.GetString(5),
PlanHash = reader.GetString(6),
SignaturePresent = !string.IsNullOrEmpty(reader.GetString(7)),
KeyId = reader.GetString(8),
});
}
var report = new
{
GeneratedAt = DateTimeOffset.UtcNow,
Period = $"Last {daysValue} days",
Client = client ?? "all",
TotalChanges = records.Count,
ByRiskLevel = records.GroupBy(r => ((dynamic)r).RiskLevel)
.ToDictionary(g => (string)g.Key, g => g.Count()),
AllSignaturesPresent = records.All(r => ((dynamic)r).SignaturePresent),
Changes = records,
};
return Results.Ok(report);
});
Ejemplos de queries:
# All changes for client ACME in the last 90 days
curl "http://localhost:5100/api/cab/report?client=acme&days=90"
# All changes across all clients in the last 30 days
curl "http://localhost:5100/api/cab/report?days=30"
# Verify a specific approval
curl "http://localhost:5100/api/cab/verify/CR-1712345678"
5. La Integracion con Backstage
Actualizamos la UI de revision del CAB del articulo 4 para que llame al endpoint de firma al aprobar:
// Updated handleApprove in ChangeRequestReview.tsx
const handleApprove = async (cr: ChangeRequest) => {
const { userEntityRef } = await identityApi.getBackstageIdentity();
const proxyUrl = await discoveryApi.getBaseUrl('proxy');
// 1. Hash the plan for the approval record
const planHash = await hashString(cr.summary + cr.rollbackPlan);
// 2. Sign the approval via QuantumAPI
const signRes = await fetchApi.fetch(`${proxyUrl}/ai-service/api/cab/approve`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
changeRequestId: cr.id,
approvedBy: userEntityRef,
module: cr.module,
client: cr.client,
riskLevel: cr.riskAssessment.level,
planHash,
}),
});
if (!signRes.ok) {
alert('Approval signing failed. The change was NOT approved.');
return;
}
// 3. Now approve in the change management backend
const baseUrl = await discoveryApi.getBaseUrl('change-management');
await fetchApi.fetch(`${baseUrl}/requests/${cr.id}/approve`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ approvedBy: userEntityRef }),
});
// 4. Seal the full evidence package
await fetchApi.fetch(`${proxyUrl}/ai-service/api/cab/seal-evidence`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
changeRequestId: cr.id,
module: cr.module,
client: cr.client,
planSummary: cr.summary,
riskLevel: cr.riskAssessment.level,
riskFactors: cr.riskAssessment.factors,
rollbackPlan: cr.rollbackPlan,
prUrl: cr.evidence.prUrl,
prApprovers: cr.evidence.prApprovers,
securityScan: cr.evidence.securityScan,
testResults: cr.evidence.testResults,
pipelineUrl: cr.evidence.pipelineRunUrl,
approvedBy: userEntityRef,
approvedAt: new Date().toISOString(),
}),
});
setRequests(prev => prev.filter(r => r.id !== cr.id));
};
async function hashString(input: string): Promise<string> {
const encoder = new TextEncoder();
const data = encoder.encode(input);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
}
El orden importa: primero firmar, luego aprobar, luego sellar. Si la firma falla, la aprobacion no se ejecuta. Una aprobacion sin firma no es una aprobacion.
Lo Que Ve el Auditor
El auditor solicita el reporte de compliance:
{
"generatedAt": "2026-04-05T10:00:00Z",
"period": "Last 90 days",
"client": "acme",
"totalChanges": 23,
"byRiskLevel": {
"low": 14,
"medium": 7,
"high": 1,
"critical": 1
},
"allSignaturesPresent": true,
"changes": [
{
"changeRequestId": "CR-1712345678",
"approvedBy": "user:default/victor.zaragoza",
"approvedAt": "2026-04-01T14:30:00Z",
"module": "tf-azurerm-aks",
"client": "acme",
"riskLevel": "high",
"planHash": "a3f7c2d1e4b5...",
"signaturePresent": true,
"keyId": "key-mldsa-cab-001"
}
]
}
Para cualquier cambio, el auditor puede llamar a /api/cab/verify/CR-1712345678 y obtener prueba criptografica de que:
- Victor Zaragoza aprobo este cambio concreto
- La aprobacion no ha sido manipulada
- El hash del plan coincide (el plan no fue cambiado despues de la aprobacion)
- El algoritmo de firma es ML-DSA-65 (quantum-safe)
Sin actas de reunion. Sin hilos de email. Sin “confias en mi, yo lo aprobe.” Prueba criptografica.
Checklist
- Tabla
cab_approvalscreada con indices - Endpoint
/api/cab/approvefirma aprobaciones con ML-DSA - Endpoint
/api/cab/verify/{id}valida firmas - Endpoint
/api/cab/seal-evidencehashea y firma el paquete de evidencia completo - Endpoint
/api/cab/reportgenera reportes de compliance - La UI del CAB en Backstage llama al endpoint de firma antes de aprobar
- Las firmas fallidas bloquean la aprobacion (sin firma = no aprobado)
- El hash del plan vincula la aprobacion al plan de Terraform concreto
- Reportes filtrables por cliente y rango de fechas
- Todos los endpoints devuelven 503 cuando QuantumAPI o PostgreSQL no estan configurados
Challenge
Antes del siguiente articulo:
- Crea la tabla
cab_approvalsen tu instancia de PostgreSQL - Aprueba un change request a traves de la UI de Backstage — verifica que la firma se almacena
- Llama al endpoint de verificacion — confirma
signatureValid: true - Modifica el
payload_jsonen la base de datos directamente — llama a verify otra vez — confirmasignatureValid: false
Ese ultimo paso es la prueba real. Si puedes modificar un registro y la verificacion sigue pasando, algo esta mal.
En el siguiente articulo, construimos Chat with Your Infrastructure — un panel conversacional en Backstage donde haces preguntas sobre tus modulos, pipelines y entornos. No es un chatbot generico — lee tu catalogo, tus archivos de estado y tu documentacion antes de responder.
El codigo completo esta en GitHub.
Si esta serie te ayuda, considera invitarme a un cafe.
Este es el articulo 6 de la serie Infrastructure Hub. Anterior: Secrets and Post-Quantum Identities. Siguiente: Chat with Your Infrastructure — acceso conversacional a toda tu plataforma.
Loading comments...