diff --git a/Configs.cs b/Configs.cs
index cbd1265..cb13660 100644
--- a/Configs.cs
+++ b/Configs.cs
@@ -43,12 +43,14 @@ public record MessagingConfig(string ConnectionString);
/// The storage
/// The redirect URL for unmatched requests to the server
/// The machine name
+/// Whether to disable encryption on stored reports
public record EnvironmentConfig(
string? Hostname,
bool IsProd,
string Storage,
string? RedirectUrl = null,
- string? MachineName = null
+ string? MachineName = null,
+ bool? DisableReportEncryption = null
);
///
diff --git a/DuplicatiIngress.csproj b/DuplicatiIngress.csproj
index 3b6390d..643403b 100644
--- a/DuplicatiIngress.csproj
+++ b/DuplicatiIngress.csproj
@@ -23,6 +23,8 @@
+
+
diff --git a/IngressHandler.cs b/IngressHandler.cs
index 531345d..2614ed5 100644
--- a/IngressHandler.cs
+++ b/IngressHandler.cs
@@ -30,12 +30,14 @@ namespace DuplicatiIngress;
///
/// Represents the ingress handler
///
+/// The environment configuration
/// The HTTP context accessor
/// The legacy tokens
/// The JWT validator
/// The bus control
/// The storage
public class IngressHandler(
+ EnvironmentConfig environmentConfig,
IHttpContextAccessor httpContextAccessor,
IPreconfiguredTokens preconfiguredTokens,
IJWTValidator jWTValidator,
@@ -71,7 +73,7 @@ public async Task MapPost(string token, CancellationToken ct)
// 2. Generate a filename
var uuid = Uuid7.String();
- var filename = $"{parsedToken.OrganizationId}/{uuid}.json.aes";
+ var filename = $"{parsedToken.OrganizationId}/{uuid}.json{(environmentConfig.DisableReportEncryption ?? false ? "" : ".aes")}";
// 4. Rudimentary validation of input
tempFileToDelete = Path.GetTempFileName();
@@ -102,20 +104,30 @@ public async Task MapPost(string token, CancellationToken ct)
throw new UserReportedException("Payload is not valid JSON", statuscode: 400, exception: ex);
}
- // 5. Encrypt with AESCrypt
- encFileToDelete = Path.GetTempFileName();
- var encHeaders = new KeyValuePair("key", Encoding.UTF8.GetBytes(parsedToken.KeyId));
- var options = new SharpAESCrypt.EncryptionOptions(InsertPlaceholder: false, AdditionalExtensions: [encHeaders]);
+ string fileToUpload;
+ if (environmentConfig.DisableReportEncryption ?? false)
+ {
+ fileToUpload = tempFileToDelete;
+ }
+ else
+ {
+ // 5. Encrypt with AESCrypt
+ encFileToDelete = Path.GetTempFileName();
+ var encHeaders = new KeyValuePair("key", Encoding.UTF8.GetBytes(parsedToken.KeyId));
+ var options = new SharpAESCrypt.EncryptionOptions(InsertPlaceholder: false, AdditionalExtensions: [encHeaders]);
+
+ using (var s1 = File.OpenRead(tempFileToDelete))
+ using (var s2 = File.OpenWrite(encFileToDelete))
+ await SharpAESCrypt.AESCrypt.EncryptAsync(parsedToken.EncryptionKey, s1, s2, options, ct);
- using (var s1 = File.OpenRead(tempFileToDelete))
- using (var s2 = File.OpenWrite(encFileToDelete))
- await SharpAESCrypt.AESCrypt.EncryptAsync(parsedToken.EncryptionKey, s1, s2, options, ct);
+ fileToUpload = encFileToDelete;
+ }
// 6. Store the payload with IKVPS
var uploaded = false;
try
{
- using (var fs = File.OpenRead(encFileToDelete))
+ using (var fs = File.OpenRead(fileToUpload))
await storage.WriteAsync(filename, fs, ct);
uploaded = true;
}
diff --git a/JWTValidator.cs b/JWTValidator.cs
index 87ee9f5..31e04e5 100644
--- a/JWTValidator.cs
+++ b/JWTValidator.cs
@@ -47,15 +47,21 @@ public class JWTValidator : IJWTValidator
///
private readonly IEncryptionKeyProvider EncryptionKeyProvider;
+ ///
+ /// The environment configuration
+ ///
+ private readonly EnvironmentConfig EnvironmentConfig;
+
///
/// Initializes a new instance of the class
///
/// The encryption key provider
/// The JWT configuration
- public JWTValidator(IEncryptionKeyProvider encryptionKeyProvider, JWTConfig jWTConfig)
+ public JWTValidator(IEncryptionKeyProvider encryptionKeyProvider, JWTConfig jWTConfig, EnvironmentConfig environmentConfig)
{
EncryptionKeyProvider = encryptionKeyProvider;
TokenValidationParameters = GetTokenValidationParameters(jWTConfig);
+ EnvironmentConfig = environmentConfig;
}
///
@@ -126,6 +132,9 @@ public ParsedIngressToken Validate(string token)
if (string.IsNullOrWhiteSpace(orgId))
throw new SecurityTokenValidationException("Organization ID is missing");
+ if (EnvironmentConfig.DisableReportEncryption ?? false)
+ return new ParsedIngressToken(orgId, null!, null!);
+
if (string.IsNullOrWhiteSpace(keyId))
throw new SecurityTokenValidationException("Key ID is missing");
diff --git a/PreconfigureTokens.cs b/PreconfigureTokens.cs
index 6bb2118..17ee39b 100644
--- a/PreconfigureTokens.cs
+++ b/PreconfigureTokens.cs
@@ -90,7 +90,7 @@ internal static PreconfiguredTokensConfig CreateEmpty()
///
/// Service for getting information about preconfigured tokens
///
-public class PreconfiguredTokens(IEncryptionKeyProvider encryptionKeyProvider, PreconfiguredTokensConfig preconfiguredTokensConfig) : IPreconfiguredTokens
+public class PreconfiguredTokens(IEncryptionKeyProvider encryptionKeyProvider, PreconfiguredTokensConfig preconfiguredTokensConfig, EnvironmentConfig environmentConfig) : IPreconfiguredTokens
{
///
public ParsedIngressToken? GetPreconfiguredToken(string token)
@@ -101,12 +101,18 @@ public class PreconfiguredTokens(IEncryptionKeyProvider encryptionKeyProvider, P
if (preconfiguredTokensConfig.BlacklistTokens.Contains(token))
throw new SecurityTokenValidationException("Invalid token, blacklisted");
- if (preconfiguredTokensConfig.WhitelistTokens.TryGetValue(token, out var tokenEntry) && !string.IsNullOrWhiteSpace(tokenEntry?.OrganizationId) && !string.IsNullOrWhiteSpace(tokenEntry?.KeyId))
+ if (preconfiguredTokensConfig.WhitelistTokens.TryGetValue(token, out var tokenEntry) && !string.IsNullOrWhiteSpace(tokenEntry?.OrganizationId))
{
- var encryptionKey = encryptionKeyProvider.GetEncryptionKey(tokenEntry.KeyId);
- if (string.IsNullOrWhiteSpace(encryptionKey))
- throw new SecurityTokenValidationException("Invalid token, key not found");
- return new ParsedIngressToken(tokenEntry.OrganizationId, tokenEntry.KeyId, encryptionKey);
+ if (environmentConfig.DisableReportEncryption ?? false)
+ return new ParsedIngressToken(tokenEntry.OrganizationId, null!, null!);
+
+ if (!string.IsNullOrWhiteSpace(tokenEntry?.KeyId))
+ {
+ var encryptionKey = encryptionKeyProvider.GetEncryptionKey(tokenEntry.KeyId);
+ if (string.IsNullOrWhiteSpace(encryptionKey))
+ throw new SecurityTokenValidationException("Invalid token, key not found");
+ return new ParsedIngressToken(tokenEntry.OrganizationId, tokenEntry.KeyId, encryptionKey);
+ }
}
return null;
diff --git a/Program.cs b/Program.cs
index 162a2fe..75611c7 100644
--- a/Program.cs
+++ b/Program.cs
@@ -27,6 +27,7 @@
using Serilog;
using Serilog.Core;
using Serilog.Events;
+using SimpleSecurityFilter;
var builder = WebApplication.CreateBuilder(args);
@@ -85,6 +86,9 @@
builder.Services.AddHttpContextAccessor();
+var securityconfig = builder.Configuration.GetSection("Security").Get();
+builder.AddSimpleSecurityFilter(securityconfig, msg => Log.Warning(msg));
+
// Load encryption keys
var encryptionKeys = builder.Configuration.GetSection("EncryptionKey")
.GetChildren()
@@ -164,7 +168,7 @@
};
});
-app.UseSecurityFilter();
+app.UseSimpleSecurityFilter(securityconfig);
app.MapPost("/backupreports/{token}",
async ([FromServices] IngressHandler handler, [FromRoute] string token, CancellationToken ct) =>
diff --git a/README.md b/README.md
index 19d5a31..abed502 100644
--- a/README.md
+++ b/README.md
@@ -36,14 +36,18 @@ The ingress server is intended to have very few moving parts and generally just
The following environment variables are optional, and should be considered for a production deployment:
-| Variable | Description |
-| -------------------------------- | ----------------------------------------------------------------------------- |
-| ENVIRONMENT\_\_HOSTNAME | The server hostname for logging purposes |
-| ENVIRONMENT\_\_MACHINENAME | Name of the machine for logging purposes |
-| ENVIRONMENT\_\_REDIRECTURL | Url to redirect to when visiting the root path |
-| PRECONFIGUREDTOKENS\_\_STORAGE | The KVPSButter connection string to the storage that contains an IP blacklist |
-| PRECONFIGUREDTOKENS\_\_WHITELIST | The key that contains the IP blacklist |
-| PRECONFIGUREDTOKENS\_\_BLACKLIST | The key that contains the IP blacklist |
+| Variable | Description |
+| -------------------------------------- | ------------------------------------------------------------------------------ |
+| ENVIRONMENT\_\_HOSTNAME | The server hostname for logging purposes |
+| ENVIRONMENT\_\_MACHINENAME | Name of the machine for logging purposes |
+| ENVIRONMENT\_\_REDIRECTURL | Url to redirect to when visiting the root path |
+| ENVIRONMENT\_\_DISABLEREPORTENCRYPTION | Disables encrypting received backup reports on storage |
+| PRECONFIGUREDTOKENS\_\_STORAGE | The KVPSButter connection string to the storage that contains an IP blacklist |
+| PRECONFIGUREDTOKENS\_\_WHITELIST | The key that contains the IP blacklist |
+| PRECONFIGUREDTOKENS\_\_BLACKLIST | The key that contains the IP blacklist |
+| SECURITY\_\_MAXREQUESTSPERSECONDPERIP | The maximum number of request from a single IP per second before throttling it |
+| SECURITY\_\_FILTERPATTERNS | Boolean toggling filtering of scanning patterns |
+| SECURITY\_\_RATELIMITENABLED | Boolean toggling if IP rate limiting is enabled |
## Setting Up Local Development Environment
diff --git a/SecurityMiddleware.cs b/SecurityMiddleware.cs
deleted file mode 100644
index ffd79ca..0000000
--- a/SecurityMiddleware.cs
+++ /dev/null
@@ -1,130 +0,0 @@
-// Copyright (c) 2025 Duplicati Inc.
-//
-// Permission is hereby granted, free of charge, to any person obtaining a copy
-// of this software and associated documentation files (the "Software"), to deal
-// in the Software without restriction, including without limitation the rights to
-// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
-// of the Software, and to permit persons to whom the Software is furnished to do
-// so, subject to the following conditions:
-//
-// The above copyright notice and this permission notice shall be included in all
-// copies or substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
-// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-// SOFTWARE.
-namespace DuplicatiIngress;
-
-///
-/// Middleware to filter against most common security threats and crawlers.
-/// This is a simple implementation and is meant to provide basic protection and
-/// discourage automated attacks as the requests will be blocked with a 403 status code.
-///
-/// This in conjunction with the rate limiting options provides basic protection.
-///
-public class SecurityMiddleware(RequestDelegate next, ILogger logger)
-{
-
- ///
- /// These are the file extensions that are blocked by default.
- ///
- private static readonly HashSet _blockedExtensions = new(StringComparer.OrdinalIgnoreCase)
- {
- ".php", ".cgi", ".asp", ".aspx", ".ashx", ".asmx", ".axd", ".config", ".env",
- ".exe", ".dll", ".bat", ".cmd", ".sh", ".jar", ".jsp", ".jspx", ".war",
- ".pl", ".py", ".rb", ".htaccess", ".htpasswd", ".ini", ".cfg", ".xml", ".conf"
- };
-
- ///
- /// This is a comprehensive list of attack paterns to filter, this is not exhaustive
- /// it can be incremented with more patterns as needed.
- ///
- private static readonly HashSet _blockedPatterns = new(StringComparer.OrdinalIgnoreCase)
- {
- // Path traversal
- "../../", "../", "..\\", "%2e%2e", "%252e", "..;", "%c0%ae",
-
- // XSS
- "
- public async Task InvokeAsync(HttpContext context)
- {
- if (IsBlocked(context))
- {
- var remoteip = context.Request.Headers["X-Forwarded-For"].FirstOrDefault() ?? context.Connection.RemoteIpAddress?.ToString() ?? "unknown";
- logger.LogWarning("Blocked request from IP {IP} with path {Path} and user agent {UserAgent}",
- remoteip,
- context.Request.Path,
- context.Request.Headers.UserAgent.ToString());
-
- context.Response.StatusCode = StatusCodes.Status403Forbidden;
- return;
- }
-
- await next(context);
- }
-
- ///
- /// Returns a requests matches any of the blocked patterns or extensions.
- ///
- /// Context of the request
- /// True if the request should be blocked
- private bool IsBlocked(HttpContext context)
- {
- var path = context.Request.Path.ToString().ToLowerInvariant();
-
- // Check file extensions
- if (_blockedExtensions.Any(x => path.EndsWith(x)))
- return true;
-
- // Check for suspicious patterns in path, query string, and headers
- var valuesToCheck = new[]
- {
- path,
- context.Request.QueryString.ToString().ToLowerInvariant(),
- context.Request.Headers["Referer"].ToString(),
- context.Request.Headers["Cookie"].ToString(),
- context.Request.Headers["X-Forwarded-For"].ToString(),
- context.Request.Headers["X-Forwarded-Host"].ToString()
- };
-
- return valuesToCheck.Any(value =>
- _blockedPatterns.Any(pattern => value.Contains(pattern, StringComparison.OrdinalIgnoreCase)));
- }
-}
-
-///
-/// Helper to register the middleware on app.
-///
-public static class SecurityMiddlewareExtensions
-{
- ///
- /// Use the security filter middleware
- ///
- /// The application builder
- /// The application builder
- public static IApplicationBuilder UseSecurityFilter(this IApplicationBuilder builder)
- => builder.UseMiddleware();
-}
\ No newline at end of file