fix bootstrap rate limit, fix download timeout

This commit is contained in:
Oliver Kieselbach
2026-04-01 12:04:17 +02:00
parent 53e6692cad
commit 17d72b9b3f
5 changed files with 86 additions and 7 deletions
@@ -1 +1 @@
752
754
@@ -513,7 +513,9 @@ namespace AutopilotMonitor.Functions.DataAccess.TableStorage
{ "DiagnosticsGlobalLogPathsJson", config.DiagnosticsGlobalLogPathsJson },
{ "NvdApiKey", config.NvdApiKey },
{ "VulnerabilityCorrelationEnabled", config.VulnerabilityCorrelationEnabled },
{ "VulnerabilityDataLastSyncUtc", config.VulnerabilityDataLastSyncUtc }
{ "VulnerabilityDataLastSyncUtc", config.VulnerabilityDataLastSyncUtc },
{ "MaxDiagnosticsDownloadSizeMB", config.MaxDiagnosticsDownloadSizeMB },
{ "DiagnosticsDownloadTimeoutSeconds", config.DiagnosticsDownloadTimeoutSeconds }
};
return entity;
@@ -535,7 +537,9 @@ namespace AutopilotMonitor.Functions.DataAccess.TableStorage
DiagnosticsGlobalLogPathsJson = entity.GetString("DiagnosticsGlobalLogPathsJson"),
NvdApiKey = entity.GetString("NvdApiKey"),
VulnerabilityCorrelationEnabled = entity.GetBoolean("VulnerabilityCorrelationEnabled") ?? true,
VulnerabilityDataLastSyncUtc = entity.GetString("VulnerabilityDataLastSyncUtc")
VulnerabilityDataLastSyncUtc = entity.GetString("VulnerabilityDataLastSyncUtc"),
MaxDiagnosticsDownloadSizeMB = entity.GetInt32("MaxDiagnosticsDownloadSizeMB") ?? 500,
DiagnosticsDownloadTimeoutSeconds = entity.GetInt32("DiagnosticsDownloadTimeoutSeconds") ?? 120
};
}
@@ -23,15 +23,18 @@ namespace AutopilotMonitor.Functions.Functions.Diagnostics
{
private readonly ILogger<DiagnosticsDownloadFunction> _logger;
private readonly TenantConfigurationService _configService;
private readonly AdminConfigurationService _adminConfigService;
private readonly TelemetryClient _telemetryClient;
public DiagnosticsDownloadFunction(
ILogger<DiagnosticsDownloadFunction> logger,
TenantConfigurationService configService,
AdminConfigurationService adminConfigService,
TelemetryClient telemetryClient)
{
_logger = logger;
_configService = configService;
_adminConfigService = adminConfigService;
_telemetryClient = telemetryClient;
}
@@ -98,14 +101,41 @@ namespace AutopilotMonitor.Functions.Functions.Diagnostics
blobUrl = $"{containerSasUrl.TrimEnd('/')}/{blobName}";
}
// Load admin configuration for download limits
var adminConfig = await _adminConfigService.GetConfigurationAsync();
var maxSizeBytes = (long)adminConfig.MaxDiagnosticsDownloadSizeMB * 1024 * 1024;
var timeoutSeconds = adminConfig.DiagnosticsDownloadTimeoutSeconds;
using var cts = timeoutSeconds > 0
? new CancellationTokenSource(TimeSpan.FromSeconds(timeoutSeconds))
: new CancellationTokenSource();
// Download blob server-side and stream to client
var sw = Stopwatch.StartNew();
var blobClient = new BlobClient(new Uri(blobUrl));
var download = await blobClient.DownloadStreamingAsync();
var download = await blobClient.DownloadStreamingAsync(cancellationToken: cts.Token);
sw.Stop();
var contentLength = download.Value.Details.ContentLength;
// Enforce size limit before streaming (fast reject)
if (maxSizeBytes > 0 && contentLength > maxSizeBytes)
{
_logger.LogWarning(
"DiagnosticsDownload: Blob {BlobName} for tenant {TenantId} rejected — size {SizeBytes} bytes exceeds limit {MaxSizeBytes} bytes",
blobName, tenantId, contentLength, maxSizeBytes);
download.Value.Content.Dispose();
var tooLarge = req.CreateResponse(HttpStatusCode.RequestEntityTooLarge);
await tooLarge.WriteAsJsonAsync(new
{
success = false,
message = $"Diagnostics package size ({contentLength / (1024 * 1024)} MB) exceeds the maximum allowed size ({adminConfig.MaxDiagnosticsDownloadSizeMB} MB)."
});
return tooLarge;
}
_logger.LogInformation(
"DiagnosticsDownload: Proxying blob {BlobName} for tenant {TenantId}, size {SizeBytes} bytes, fetch took {DurationMs}ms",
blobName, tenantId, contentLength, sw.ElapsedMilliseconds);
@@ -125,7 +155,7 @@ namespace AutopilotMonitor.Functions.Functions.Diagnostics
["DurationMs"] = sw.ElapsedMilliseconds
});
// Stream blob content to client
// Stream blob content to client (with timeout)
var response = req.CreateResponse(HttpStatusCode.OK);
response.Headers.Add("Content-Type", "application/octet-stream");
response.Headers.Add("Content-Disposition", $"attachment; filename=\"{blobName}\"");
@@ -134,7 +164,7 @@ namespace AutopilotMonitor.Functions.Functions.Diagnostics
response.Headers.Add("Content-Length", contentLength.ToString());
}
await download.Value.Content.CopyToAsync(response.Body);
await download.Value.Content.CopyToAsync(response.Body, cts.Token);
return response;
}
catch (Azure.RequestFailedException ex) when (ex.Status == 404)
@@ -144,6 +174,17 @@ namespace AutopilotMonitor.Functions.Functions.Diagnostics
await notFoundResponse.WriteAsJsonAsync(new { success = false, message = "Diagnostics package not found." });
return notFoundResponse;
}
catch (OperationCanceledException)
{
_logger.LogWarning("DiagnosticsDownload: Operation timed out or was cancelled for requested blob download");
var timeoutResponse = req.CreateResponse(HttpStatusCode.GatewayTimeout);
await timeoutResponse.WriteAsJsonAsync(new
{
success = false,
message = "Diagnostics download timed out. The file may be too large or the connection is too slow."
});
return timeoutResponse;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error proxying diagnostics download");
@@ -173,6 +173,24 @@ namespace AutopilotMonitor.Functions.Security
_logger.LogInformation("Bootstrap token auth accepted for tenant {TenantId} (code {ShortCode})",
tenantId, bootstrapSession.ShortCode);
// Rate limit check for bootstrap auth (DoS protection)
var bsRateLimitValue = config.CustomRateLimitRequestsPerMinute ?? config.RateLimitRequestsPerMinute;
var bsRateLimitResult = _rateLimitService.CheckRateLimit(
$"bootstrap:{bootstrapTokenHeader}",
bsRateLimitValue
);
if (!bsRateLimitResult.IsAllowed)
{
return new SecurityValidationResult
{
IsValid = false,
StatusCode = HttpStatusCode.TooManyRequests,
ErrorMessage = "Rate limit exceeded",
RateLimitResult = bsRateLimitResult
};
}
return new SecurityValidationResult
{
IsValid = true,
@@ -180,7 +198,8 @@ namespace AutopilotMonitor.Functions.Security
BootstrapShortCode = bootstrapSession.ShortCode,
Manufacturer = bsManufacturer,
Model = bsModel,
SerialNumber = bsSerial
SerialNumber = bsSerial,
RateLimitResult = bsRateLimitResult
};
}
@@ -141,6 +141,21 @@ namespace AutopilotMonitor.Shared.Models
}
}
// ===== DIAGNOSTICS DOWNLOAD LIMITS =====
/// <summary>
/// Maximum allowed diagnostics download size in MB.
/// Blobs exceeding this are rejected before streaming (413).
/// 0 = no limit. Default: 500 MB.
/// </summary>
public int MaxDiagnosticsDownloadSizeMB { get; set; } = 500;
/// <summary>
/// Timeout in seconds for the entire diagnostics download+stream operation.
/// 0 = no timeout. Default: 120 seconds.
/// </summary>
public int DiagnosticsDownloadTimeoutSeconds { get; set; } = 120;
// ===== MCP ACCESS CONTROL =====
/// <summary>