Saltar a contenido

Auditoría de Seguridad - API Nexus Platform

Fecha: 2026-01-30 Versión: 1.0 Estado: CRÍTICO - Requiere acción inmediata Alcance: Orchestrator.Api, Orchestrator.Mcp.Remote, Hubs, Webhooks


Índice

  1. Resumen Ejecutivo
  2. Hallazgos Críticos
  3. Análisis por Componente
  4. Matriz de Vulnerabilidades
  5. Plan de Securización
  6. Implementación Detallada
  7. Checklist de Seguridad

Resumen Ejecutivo

Estado Actual: 🔴 CRÍTICO

La API de Nexus Platform presenta vulnerabilidades críticas de seguridad que requieren atención inmediata:

Categoría Crítico Alto Medio Bajo
Autenticación 3 2 2 0
Autorización 2 4 3 1
Validación de Input 1 3 4 2
Webhooks 2 1 2 0
Secretos 3 2 2 0
TOTAL 11 12 13 3

Hallazgos Principales

┌─────────────────────────────────────────────────────────────┐
│              PROBLEMAS CRÍTICOS IDENTIFICADOS                │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  1. SIN AUTENTICACIÓN GLOBAL EN LA API                      │
│     - No hay app.UseAuthentication()                         │
│     - No hay app.UseAuthorization()                          │
│     - 38 de 39 controllers sin [Authorize]                   │
│                                                              │
│  2. HEADER X-User-Id PUEDE SER SUPLANTADO                   │
│     - Cualquier cliente puede enviar cualquier UserId        │
│     - Sin validación contra token JWT                        │
│     - Permite escalada de privilegios                        │
│                                                              │
│  3. SECRETOS EXPUESTOS EN REPOSITORIO                       │
│     - API keys de Claude, ClickUp, Holded                    │
│     - Credenciales SMTP                                      │
│     - Claves de encriptación                                 │
│                                                              │
│  4. WEBHOOKS SIN VALIDACIÓN DE FIRMA                        │
│     - UltraMsg: SIN validación                               │
│     - Sentry/ClickUp: Validación OPCIONAL                    │
│                                                              │
│  5. TÚNELES PERMITEN EJECUCIÓN REMOTA SIN AUTH             │
│     - TunnelEndpoints completamente abiertos                 │
│     - Cualquiera puede ejecutar comandos remotos             │
│                                                              │
└─────────────────────────────────────────────────────────────┘

Hallazgos Críticos

CRIT-001: Sin Autenticación Global en API

Severidad: 🔴 CRÍTICA Archivo: Orchestrator/src/Orchestrator.Api/Program.cs

Problema: La API no tiene configuración de autenticación ni autorización.

// ❌ FALTA en Program.cs:
// builder.Services.AddAuthentication(...)
// builder.Services.AddAuthorization(...)
// app.UseAuthentication()
// app.UseAuthorization()

Impacto: Todos los endpoints están accesibles sin autenticación.

Solución:

// Program.cs - AGREGAR:
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = builder.Configuration["Jwt:Issuer"],
            ValidAudience = builder.Configuration["Jwt:Audience"],
            IssuerSigningKey = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!))
        };
    });

builder.Services.AddAuthorization(options =>
{
    options.FallbackPolicy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .Build();
});

// En el pipeline:
app.UseAuthentication();
app.UseAuthorization();


CRIT-002: Header X-User-Id Suplantable

Severidad: 🔴 CRÍTICA Archivos: 15+ Controllers

Problema: El UserId se obtiene de un header HTTP que el cliente puede manipular.

// ❌ CÓDIGO ACTUAL (INSEGURO):
private Guid? GetUserId()
{
    // El cliente puede enviar cualquier UserId
    var headerUserId = Request.Headers["X-User-Id"].FirstOrDefault();
    if (Guid.TryParse(headerUserId, out var headerGuid))
        return headerGuid;

    // Solo como fallback usa claims
    var claim = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
    if (Guid.TryParse(claim, out var claimGuid))
        return claimGuid;

    return null;
}

Ataque:

# Un atacante puede suplantar a cualquier usuario:
curl -H "X-User-Id: admin-user-guid-here" \
     http://api.nexus.local/api/projects

# Accede a todos los datos del usuario suplantado

Solución:

// ✅ CÓDIGO SEGURO:
private Guid GetUserId()
{
    // SOLO usar claims del token JWT validado
    var claim = User.FindFirst(ClaimTypes.NameIdentifier)?.Value
        ?? throw new UnauthorizedAccessException("User not authenticated");

    if (!Guid.TryParse(claim, out var userId))
        throw new UnauthorizedAccessException("Invalid user claim");

    return userId;
}

// Para servicios internos (service-to-service):
private Guid? GetUserIdWithServiceFallback()
{
    // Primero intentar claims del usuario
    var claim = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
    if (Guid.TryParse(claim, out var userId))
        return userId;

    // Solo permitir X-User-Id si es un servicio interno autenticado
    if (User.IsInRole("InternalService"))
    {
        var headerUserId = Request.Headers["X-User-Id"].FirstOrDefault();
        if (Guid.TryParse(headerUserId, out var headerGuid))
            return headerGuid;
    }

    return null;
}


CRIT-003: Controllers Sin Atributo [Authorize]

Severidad: 🔴 CRÍTICA Archivos: 38 de 39 Controllers

Controllers SIN protección:

Controller Endpoints Datos Expuestos
AgentsController 14+ Configuración de agentes IA
ProjectsController 20+ Proyectos, secretos, configuración
ExecutionsController 8 Historial de ejecuciones
DevSessionsController 10+ Sesiones de desarrollo, mensajes
BacklogController 12+ Items de backlog
OrganizationsController 12+ Organizaciones, usuarios
CommandsController 15+ Comandos del sistema
TasksController 15+ Tareas programadas
CopilotController 8 Ejecuciones del copiloto
RemoteAgentsController 10+ Agentes remotos, API keys
... ... ...

Solución: Agregar [Authorize] a todos los controllers:

// ✅ APLICAR A TODOS LOS CONTROLLERS:
[ApiController]
[Route("api/[controller]")]
[Authorize]  // ← AGREGAR
public class ProjectsController : ControllerBase
{
    // ...
}

// Para endpoints específicamente públicos:
[AllowAnonymous]
[HttpGet("public/health")]
public IActionResult HealthCheck() => Ok();

CRIT-004: TunnelEndpoints Sin Autenticación

Severidad: 🔴 CRÍTICA Archivo: Orchestrator/src/Orchestrator.Api/Endpoints/TunnelEndpoints.cs

Problema: Endpoints que permiten ejecutar comandos en máquinas remotas están completamente abiertos.

// ❌ CÓDIGO ACTUAL (INSEGURO):
public static void MapTunnelEndpoints(this IEndpointRouteBuilder app)
{
    var group = app.MapGroup("/api/tunnel");

    // Generar tokens - SIN AUTH
    group.MapPost("/tokens", async (request, tokenService) => ...);

    // Listar clientes - SIN AUTH
    group.MapGet("/clients", (tunnelService) => ...);

    // EJECUTAR COMANDOS REMOTOS - SIN AUTH ❌❌❌
    group.MapPost("/clients/{clientId}/execute", async (...) => ...);

    // EJECUTAR CLAUDE CLI REMOTO - SIN AUTH ❌❌❌
    group.MapPost("/clients/{clientId}/claude", async (...) => ...);
}

Impacto: Cualquier persona puede ejecutar comandos arbitrarios en máquinas remotas conectadas.

Solución:

// ✅ CÓDIGO SEGURO:
public static void MapTunnelEndpoints(this IEndpointRouteBuilder app)
{
    var group = app.MapGroup("/api/tunnel")
        .RequireAuthorization("TunnelAdmin");  // ← AGREGAR

    // Endpoints protegidos con política específica
    group.MapPost("/clients/{clientId}/execute", async (...) => ...)
        .RequireAuthorization("TunnelExecute");

    group.MapPost("/clients/{clientId}/claude", async (...) => ...)
        .RequireAuthorization("TunnelExecute");
}

// En Program.cs:
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("TunnelAdmin", policy =>
        policy.RequireRole("Admin", "TunnelOperator"));

    options.AddPolicy("TunnelExecute", policy =>
        policy.RequireRole("Admin")
              .RequireClaim("tunnel:execute", "true"));
});


CRIT-005: Webhooks Sin Validación de Firma

Severidad: 🔴 CRÍTICA Archivo: Orchestrator/src/Orchestrator.Api/Controllers/WebhooksController.cs

UltraMsg (WhatsApp) - SIN VALIDACIÓN

// ❌ CÓDIGO ACTUAL (INSEGURO):
[HttpPost("ultramsg")]
public async Task<IActionResult> ReceiveUltraMsgWebhook()
{
    // NO HAY VALIDACIÓN DE FIRMA
    Request.EnableBuffering();
    using var reader = new StreamReader(Request.Body, leaveOpen: true);
    var body = await reader.ReadToEndAsync();

    // Procesa directamente sin validar origen
    var payload = JsonSerializer.Deserialize<UltraMsgWebhookPayload>(body);
    // ...
}

Sentry/ClickUp - Validación OPCIONAL

// ❌ CÓDIGO ACTUAL (INSEGURO):
var secret = _configuration["Sentry:WebhookSecret"];
if (!string.IsNullOrEmpty(secret))  // ← SI NO HAY SECRET, SE ACEPTA TODO
{
    if (!ValidateSentrySignature(body, signature, secret))
        return Unauthorized();
}
// Continúa sin validación si secret está vacío

Solución:

// ✅ CÓDIGO SEGURO:
[HttpPost("ultramsg")]
public async Task<IActionResult> ReceiveUltraMsgWebhook()
{
    var secret = _configuration["UltraMsg:WebhookSecret"]
        ?? throw new InvalidOperationException("UltraMsg webhook secret not configured");

    var signature = Request.Headers["X-UltraMsg-Signature"].FirstOrDefault();
    if (string.IsNullOrEmpty(signature))
        return Unauthorized(new { error = "Missing signature" });

    Request.EnableBuffering();
    using var reader = new StreamReader(Request.Body, leaveOpen: true);
    var body = await reader.ReadToEndAsync();
    Request.Body.Position = 0;

    if (!ValidateUltraMsgSignature(body, signature, secret))
        return Unauthorized(new { error = "Invalid signature" });

    // Procesar webhook validado
}

// Para Sentry/ClickUp - Hacer validación OBLIGATORIA:
[HttpPost("sentry")]
public async Task<IActionResult> ReceiveSentryWebhook()
{
    var secret = _configuration["Sentry:WebhookSecret"]
        ?? throw new InvalidOperationException("Sentry webhook secret MUST be configured");

    var signature = Request.Headers["Sentry-Hook-Signature"].FirstOrDefault()
        ?? return Unauthorized(new { error = "Missing signature" });

    // ...validación obligatoria...
}


CRIT-006: Secretos Expuestos en Repositorio

Severidad: 🔴 CRÍTICA Archivos: appsettings.json, appsettings.Development.json

Secretos encontrados en el repositorio:

Secreto Ubicación Tipo
Claude API Key appsettings.json sk-ant-api03-...
ClickUp API Token appsettings.json pk_93835201_...
ClickUp Webhook Secret appsettings.json JNJEHSBA5P1M3C7B...
Holded API Key appsettings.json 9449c7c66349b8d5...
SMTP Password appsettings.json kyzk brec gubo rsod
Encryption Key appsettings.json 0221b2e45b9e4e03...
PostgreSQL Password appsettings.json 6e43b64b5fe04d69...
RabbitMQ Password appsettings.json admin
Tunnel Secret Key appsettings.json tunnel-dev-key-...

Solución Inmediata: 1. Rotar TODOS los secretos expuestos 2. Mover a variables de entorno o Key Vault 3. Agregar appsettings.json a .gitignore (mantener solo appsettings.example.json)

// Program.cs - Usar configuración segura:
builder.Configuration
    .AddEnvironmentVariables()
    .AddUserSecrets<Program>(optional: true);

// Para producción - Azure Key Vault:
if (!builder.Environment.IsDevelopment())
{
    builder.Configuration.AddAzureKeyVault(
        new Uri($"https://{vaultName}.vault.azure.net/"),
        new DefaultAzureCredential());
}

Análisis por Componente

1. Controllers API

Estado de Autorización por Controller

Controller [Authorize] GetUserId() Multi-tenant Rate Limit Estado
AgentsController Parcial 🔴
ProjectsController 🟡
ExecutionsController Parcial 🔴
DevSessionsController 🟡
BacklogController 🟡
OrganizationsController 🔴
TasksController Parcial 🔴
CopilotController 🔴
RemoteAgentsController 🔴
CommandsController 🔴
PluginsController 🔴
ProceduresController Parcial 🔴
WebhooksController N/A N/A N/A 🟡
... ... ... ... ... ...

Endpoints Públicos Intencionalmente

// Estos endpoints DEBEN ser públicos pero con validación:
[AllowAnonymous] - /webhooks/sentry (con firma HMAC)
[AllowAnonymous] - /webhooks/clickup (con firma HMAC)
[AllowAnonymous] - /webhooks/ultramsg (con firma HMAC)
[AllowAnonymous] - /health (health check)
[AllowAnonymous] - /api/agent-updates/version (versión pública)

2. SignalR Hubs

RemoteAgentHub

Vulnerabilidades: - API Key hash sin salt (vulnerable a rainbow tables) - Sin rate limiting en Register - Sin validación de permisos en SendCommand

// ❌ CÓDIGO ACTUAL:
private static string HashApiKey(string apiKey)
{
    using var sha256 = SHA256.Create();
    var bytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(apiKey));
    return Convert.ToBase64String(bytes);
    // Sin salt = vulnerable
}

Solución:

// ✅ CÓDIGO SEGURO:
private static string HashApiKey(string apiKey, string salt)
{
    using var pbkdf2 = new Rfc2898DeriveBytes(
        apiKey,
        Encoding.UTF8.GetBytes(salt),
        iterations: 100000,
        HashAlgorithmName.SHA256);

    return Convert.ToBase64String(pbkdf2.GetBytes(32));
}

TunnelHub

Vulnerabilidades: - ClientId aceptado sin validación - Sin autenticación de clientes - Permite registro de cualquier máquina


3. OAuth2 (MCP Remote)

Vulnerabilidades: - Plain PKCE aceptado (debe ser solo S256) - Sin rate limiting en /oauth2/token - Redirect URI validation incompleta

// ❌ CÓDIGO ACTUAL:
if (method == "S256") { ... }
return codeVerifier == codeChallenge;  // Plain PKCE aceptado

// ✅ CÓDIGO SEGURO:
if (method != "S256")
    return false;  // Rechazar plain PKCE

Matriz de Vulnerabilidades

Por Severidad

ID Vulnerabilidad Severidad CVSS Estado
CRIT-001 Sin autenticación global CRÍTICO 9.8 ⏳ Pendiente
CRIT-002 X-User-Id suplantable CRÍTICO 9.1 ⏳ Pendiente
CRIT-003 Controllers sin [Authorize] CRÍTICO 9.1 ⏳ Pendiente
CRIT-004 TunnelEndpoints sin auth CRÍTICO 10.0 ⏳ Pendiente
CRIT-005 Webhooks sin validación CRÍTICO 8.6 ⏳ Pendiente
CRIT-006 Secretos en repositorio CRÍTICO 9.8 ⏳ Pendiente
HIGH-001 Sin rate limiting ALTO 7.5 ⏳ Pendiente
HIGH-002 Hash API key sin salt ALTO 7.4 ⏳ Pendiente
HIGH-003 IDOR en endpoints ALTO 8.1 ⏳ Pendiente
HIGH-004 Path traversal potencial ALTO 7.5 ⏳ Pendiente
MED-001 Plain PKCE aceptado MEDIO 5.9 ⏳ Pendiente
MED-002 Error details expuestos MEDIO 5.3 ⏳ Pendiente
MED-003 CORS permisivo MEDIO 4.3 ⏳ Pendiente
MED-004 Security headers faltantes MEDIO 4.3 ⏳ Pendiente

Plan de Securización

Fase 1: Crítico (Días 1-3)

┌─────────────────────────────────────────────────────────────┐
│                    FASE 1: CRÍTICO                          │
│                    Días 1-3                                  │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  DÍA 1:                                                      │
│  ├── 1. Rotar TODOS los secretos expuestos                  │
│  ├── 2. Mover secretos a variables de entorno               │
│  └── 3. Actualizar .gitignore                               │
│                                                              │
│  DÍA 2:                                                      │
│  ├── 4. Implementar JWT Authentication en Program.cs        │
│  ├── 5. Agregar [Authorize] a TODOS los controllers         │
│  └── 6. Eliminar confianza en X-User-Id header              │
│                                                              │
│  DÍA 3:                                                      │
│  ├── 7. Proteger TunnelEndpoints                            │
│  ├── 8. Hacer validación de webhooks OBLIGATORIA            │
│  └── 9. Agregar validación de firma a UltraMsg              │
│                                                              │
└─────────────────────────────────────────────────────────────┘

Fase 2: Alto (Días 4-7)

┌─────────────────────────────────────────────────────────────┐
│                    FASE 2: ALTO                             │
│                    Días 4-7                                  │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  DÍA 4-5:                                                    │
│  ├── 10. Implementar rate limiting                          │
│  ├── 11. Agregar security headers middleware                │
│  └── 12. Mejorar hash de API keys (PBKDF2)                  │
│                                                              │
│  DÍA 6-7:                                                    │
│  ├── 13. Validar path traversal en endpoints                │
│  ├── 14. Completar multi-tenant filtering                   │
│  └── 15. Implementar IDOR prevention                        │
│                                                              │
└─────────────────────────────────────────────────────────────┘

Fase 3: Medio (Días 8-14)

┌─────────────────────────────────────────────────────────────┐
│                    FASE 3: MEDIO                            │
│                    Días 8-14                                 │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  ├── 16. Rechazar Plain PKCE (solo S256)                    │
│  ├── 17. Sanitizar mensajes de error                        │
│  ├── 18. Refinar políticas CORS                             │
│  ├── 19. Implementar audit logging                          │
│  ├── 20. Agregar input validation attributes                │
│  └── 21. Security scanning en CI/CD                         │
│                                                              │
└─────────────────────────────────────────────────────────────┘

Implementación Detallada

1. Configuración de JWT Authentication

// Program.cs

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;

// === AUTHENTICATION ===
builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = true,
        ValidateAudience = true,
        ValidateLifetime = true,
        ValidateIssuerSigningKey = true,
        ValidIssuer = builder.Configuration["Jwt:Issuer"],
        ValidAudience = builder.Configuration["Jwt:Audience"],
        IssuerSigningKey = new SymmetricSecurityKey(
            Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!)),
        ClockSkew = TimeSpan.FromMinutes(5)
    };

    options.Events = new JwtBearerEvents
    {
        OnAuthenticationFailed = context =>
        {
            if (context.Exception is SecurityTokenExpiredException)
            {
                context.Response.Headers.Append("Token-Expired", "true");
            }
            return Task.CompletedTask;
        }
    };
});

// === AUTHORIZATION ===
builder.Services.AddAuthorization(options =>
{
    // Política por defecto: requiere autenticación
    options.FallbackPolicy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .Build();

    // Políticas específicas
    options.AddPolicy("Admin", policy =>
        policy.RequireRole("Admin"));

    options.AddPolicy("TunnelOperator", policy =>
        policy.RequireRole("Admin", "TunnelOperator"));

    options.AddPolicy("ApiAccess", policy =>
        policy.RequireClaim("api:access", "true"));
});

// === MIDDLEWARE ORDER ===
app.UseAuthentication();  // ANTES de UseAuthorization
app.UseAuthorization();

2. Rate Limiting

// Program.cs

using System.Threading.RateLimiting;

builder.Services.AddRateLimiter(options =>
{
    options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(context =>
        RateLimitPartition.GetFixedWindowLimiter(
            partitionKey: context.User.Identity?.Name ?? context.Request.Headers.Host.ToString(),
            factory: partition => new FixedWindowRateLimiterOptions
            {
                AutoReplenishment = true,
                PermitLimit = 100,
                Window = TimeSpan.FromMinutes(1)
            }));

    options.AddPolicy("webhook", context =>
        RateLimitPartition.GetFixedWindowLimiter(
            partitionKey: context.Connection.RemoteIpAddress?.ToString() ?? "unknown",
            factory: _ => new FixedWindowRateLimiterOptions
            {
                PermitLimit = 50,
                Window = TimeSpan.FromMinutes(1)
            }));

    options.AddPolicy("auth", context =>
        RateLimitPartition.GetSlidingWindowLimiter(
            partitionKey: context.Connection.RemoteIpAddress?.ToString() ?? "unknown",
            factory: _ => new SlidingWindowRateLimiterOptions
            {
                PermitLimit = 10,
                Window = TimeSpan.FromMinutes(1),
                SegmentsPerWindow = 4
            }));

    options.OnRejected = async (context, token) =>
    {
        context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
        await context.HttpContext.Response.WriteAsJsonAsync(new
        {
            error = "Too many requests",
            retryAfter = context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter)
                ? retryAfter.TotalSeconds
                : 60
        }, token);
    };
});

app.UseRateLimiter();

3. Security Headers Middleware

// Middleware/SecurityHeadersMiddleware.cs

public class SecurityHeadersMiddleware
{
    private readonly RequestDelegate _next;

    public SecurityHeadersMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        // Prevent MIME type sniffing
        context.Response.Headers.Append("X-Content-Type-Options", "nosniff");

        // Prevent clickjacking
        context.Response.Headers.Append("X-Frame-Options", "DENY");

        // XSS protection
        context.Response.Headers.Append("X-XSS-Protection", "1; mode=block");

        // Referrer policy
        context.Response.Headers.Append("Referrer-Policy", "strict-origin-when-cross-origin");

        // Content Security Policy
        context.Response.Headers.Append("Content-Security-Policy",
            "default-src 'self'; " +
            "script-src 'self' 'unsafe-inline' 'unsafe-eval'; " +
            "style-src 'self' 'unsafe-inline'; " +
            "img-src 'self' data: https:; " +
            "font-src 'self'; " +
            "connect-src 'self' wss: https:; " +
            "frame-ancestors 'none';");

        // HSTS (solo en producción con HTTPS)
        if (context.Request.IsHttps)
        {
            context.Response.Headers.Append("Strict-Transport-Security",
                "max-age=31536000; includeSubDomains; preload");
        }

        // Permissions Policy
        context.Response.Headers.Append("Permissions-Policy",
            "accelerometer=(), camera=(), geolocation=(), gyroscope=(), " +
            "magnetometer=(), microphone=(), payment=(), usb=()");

        await _next(context);
    }
}

// Program.cs
app.UseMiddleware<SecurityHeadersMiddleware>();

4. Input Validation

// Validators/ProjectValidator.cs

using FluentValidation;

public class CreateProjectRequestValidator : AbstractValidator<CreateProjectRequest>
{
    public CreateProjectRequestValidator()
    {
        RuleFor(x => x.Name)
            .NotEmpty()
            .MaximumLength(200)
            .Matches(@"^[\w\s\-\.]+$")
            .WithMessage("Name contains invalid characters");

        RuleFor(x => x.Slug)
            .NotEmpty()
            .MaximumLength(50)
            .Matches(@"^[a-z0-9\-]+$")
            .WithMessage("Slug must be lowercase alphanumeric with hyphens");

        RuleFor(x => x.RepoPath)
            .Must(BeValidPath)
            .When(x => !string.IsNullOrEmpty(x.RepoPath))
            .WithMessage("Invalid repository path");
    }

    private bool BeValidPath(string? path)
    {
        if (string.IsNullOrEmpty(path)) return true;

        // Prevenir path traversal
        if (path.Contains("..") || path.Contains("~"))
            return false;

        // Validar que es una ruta absoluta válida
        try
        {
            var fullPath = Path.GetFullPath(path);
            return fullPath == path;
        }
        catch
        {
            return false;
        }
    }
}

// Program.cs
builder.Services.AddValidatorsFromAssemblyContaining<CreateProjectRequestValidator>();
builder.Services.AddFluentValidationAutoValidation();

5. Audit Logging

// Services/AuditService.cs

public interface IAuditService
{
    Task LogAsync(AuditEvent auditEvent);
}

public class AuditService : IAuditService
{
    private readonly NexusDbContext _db;
    private readonly ILogger<AuditService> _logger;

    public async Task LogAsync(AuditEvent auditEvent)
    {
        // Log estructurado
        _logger.LogInformation(
            "AUDIT: {Action} on {Resource} by {UserId} from {IpAddress}",
            auditEvent.Action,
            auditEvent.Resource,
            auditEvent.UserId,
            auditEvent.IpAddress);

        // Persistir en base de datos
        _db.AuditLogs.Add(new AuditLog
        {
            Id = Guid.NewGuid(),
            Action = auditEvent.Action,
            Resource = auditEvent.Resource,
            ResourceId = auditEvent.ResourceId,
            UserId = auditEvent.UserId,
            IpAddress = auditEvent.IpAddress,
            UserAgent = auditEvent.UserAgent,
            Details = auditEvent.Details,
            Timestamp = DateTime.UtcNow
        });

        await _db.SaveChangesAsync();
    }
}

// Uso en controllers:
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(Guid id)
{
    await _auditService.LogAsync(new AuditEvent
    {
        Action = "DELETE",
        Resource = "Project",
        ResourceId = id.ToString(),
        UserId = GetUserId(),
        IpAddress = HttpContext.Connection.RemoteIpAddress?.ToString(),
        UserAgent = Request.Headers.UserAgent.ToString()
    });

    // ... delete logic
}

Checklist de Seguridad

Pre-Deploy

  • [ ] Todos los secretos movidos a Key Vault/variables de entorno
  • [ ] JWT Authentication configurado
  • [ ] [Authorize] en todos los controllers
  • [ ] Rate limiting implementado
  • [ ] Security headers middleware activo
  • [ ] Webhooks con validación obligatoria
  • [ ] TunnelEndpoints protegidos
  • [ ] X-User-Id header eliminado o restringido

Configuración

  • [ ] HTTPS forzado en producción
  • [ ] CORS restringido a dominios específicos
  • [ ] Cookies con Secure y HttpOnly
  • [ ] Token expiration < 24 horas
  • [ ] Refresh token rotation activo

Monitoreo

  • [ ] Audit logging implementado
  • [ ] Alertas de intentos de autenticación fallidos
  • [ ] Monitoreo de rate limiting
  • [ ] Logs de acceso a datos sensibles

CI/CD

  • [ ] Secret scanning habilitado
  • [ ] SAST (Semgrep) en pipeline
  • [ ] Dependency scanning
  • [ ] Security tests automatizados

Próximos Pasos

  1. Inmediato: Rotar secretos y configurar autenticación
  2. Esta semana: Completar Fase 1 (Crítico)
  3. Próxima semana: Completar Fase 2 (Alto)
  4. Mes siguiente: Completar Fase 3 (Medio) + auditoría de penetración

Documento generado como parte de la auditoría de seguridad de Nexus Platform Última actualización: 2026-01-30