diff --git a/AuthKit.slnx b/AuthKit.slnx
index 9b331ad..5636c95 100644
--- a/AuthKit.slnx
+++ b/AuthKit.slnx
@@ -12,6 +12,7 @@
+
@@ -20,12 +21,13 @@
-
+
+
diff --git a/Dockerfile b/Dockerfile
index d655095..58912d7 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -17,6 +17,7 @@ COPY ["src/Core/Core.csproj", "src/Core/"]
COPY ["src/Plugins/Abstractions/AuthKit.Plugins.Abstractions.csproj", "src/Plugins/Abstractions/"]
COPY ["src/Plugins/Solutions/DevTokens/DevTokens.csproj", "src/Plugins/Solutions/DevTokens/"]
COPY ["src/Plugins/Solutions/DevTools/DevTools.csproj", "src/Plugins/Solutions/DevTools/"]
+COPY ["src/Plugins/Solutions/Shield/Shield.csproj", "src/Plugins/Solutions/Shield/"]
COPY ["src/Plugins/Integrations/AuthKit.Plugins.Integrations.csproj", "src/Plugins/Integrations/"]
COPY ["src/Plugins/Solutions/ExamplePlugin/ExamplePlugin.csproj", "src/Plugins/Solutions/ExamplePlugin/"]
@@ -24,6 +25,7 @@ COPY ["src/Plugins/Solutions/ExamplePlugin/ExamplePlugin.csproj", "src/Plugins/S
COPY ["tests/Host/AuthKit.Host.Tests.csproj", "tests/Host/"]
COPY ["tests/Host.IntegrationTests/AuthKit.Host.IntegrationTests.csproj", "tests/Host.IntegrationTests/"]
COPY ["tests/Plugins/Abstractions/AuthKit.Plugins.Abstractions.Tests.csproj", "tests/Plugins/Abstractions/"]
+COPY ["tests/Plugins/Shield/Shield.Tests.csproj", "tests/Plugins/Shield/"]
COPY ["tools/AuthKit.PluginContractValidator/AuthKit.PluginContractValidator.csproj", "tools/AuthKit.PluginContractValidator/"]
COPY ["tools/AuthKit.ManifestGenerator/CLI/AuthKit.ManifestGenerator.CLI.csproj", "tools/AuthKit.ManifestGenerator/CLI/"]
COPY ["tools/AuthKit.ManifestGenerator/Core/AuthKit.ManifestGenerator.Core.csproj", "tools/AuthKit.ManifestGenerator/Core/"]
@@ -61,6 +63,10 @@ RUN if [ -f src/Plugins/Solutions/DevTools/manifest.json ]; then cp src/Plugins/
RUN dotnet publish "src/Plugins/Solutions/ExamplePlugin/ExamplePlugin.csproj" -c Release -o /app/publish/plugins/ExamplePlugin
RUN if [ -f src/Plugins/Solutions/ExamplePlugin/manifest.json ]; then cp src/Plugins/Solutions/ExamplePlugin/manifest.json /app/publish/plugins/ExamplePlugin/manifest.json; else echo "ExamplePlugin manifest.json not in context, skipping"; fi
+RUN dotnet publish "src/Plugins/Solutions/Shield/Shield.csproj" -c Release -o /app/publish/plugins/Shield
+RUN dotnet /tmp/manifest-gen/AuthKit.ManifestGenerator.CLI.dll --input /app/publish/plugins/Shield/Shield.dll --output /app/publish/plugins/Shield/manifest.json
+RUN if [ -f src/Plugins/Solutions/Shield/manifest.json ]; then cp src/Plugins/Solutions/Shield/manifest.json /app/publish/plugins/Shield/manifest.json; fi
+
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
diff --git a/src/Plugins/Solutions/Shield/Composition/ShieldServices.cs b/src/Plugins/Solutions/Shield/Composition/ShieldServices.cs
new file mode 100644
index 0000000..ccb3d2a
--- /dev/null
+++ b/src/Plugins/Solutions/Shield/Composition/ShieldServices.cs
@@ -0,0 +1,36 @@
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Shield.Detection;
+using Shield.Detection.Sensors;
+using Shield.Options;
+using Shield.State;
+
+namespace Shield.Composition;
+
+///
+/// Composition root for Shield services: validated options, shared state,
+/// and detector strategies in evaluation order.
+///
+public static class ShieldServices
+{
+ ///
+ /// Registers Shield into the host DI container.
+ ///
+ public static IServiceCollection AddShield(this IServiceCollection services, IConfiguration configuration)
+ {
+ services.AddOptions()
+ .Bind(configuration)
+ .ValidateDataAnnotations()
+ .ValidateOnStart();
+
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+
+ return services;
+ }
+}
diff --git a/src/Plugins/Solutions/Shield/Detection/ClientIdResolver.cs b/src/Plugins/Solutions/Shield/Detection/ClientIdResolver.cs
new file mode 100644
index 0000000..1f0ff59
--- /dev/null
+++ b/src/Plugins/Solutions/Shield/Detection/ClientIdResolver.cs
@@ -0,0 +1,29 @@
+using Microsoft.AspNetCore.Http;
+using System.Net;
+
+namespace Shield.Detection;
+
+///
+/// Resolves the client identifier used for rate counting, lists, and incidents:
+/// the address before the configured number of trusted proxy hops, counted
+/// from the right of X-Forwarded-For, or the transport address.
+///
+public static class ClientIdResolver
+{
+ ///
+ /// Resolves the client id for the request. Never returns null or empty.
+ ///
+ public static string Resolve(HttpContext context, bool trustForwardedHeader, int trustedProxyHops = 1)
+ {
+ if (trustForwardedHeader
+ && context.Request.Headers.TryGetValue("X-Forwarded-For", out var forwarded))
+ {
+ var entries = forwarded.ToString().Split(',', StringSplitOptions.TrimEntries);
+ var hops = Math.Max(1, trustedProxyHops);
+ if (entries.Length >= hops && IPAddress.TryParse(entries[^hops], out var address))
+ return address.ToString();
+ }
+
+ return context.Connection.RemoteIpAddress?.ToString() ?? "unknown";
+ }
+}
diff --git a/src/Plugins/Solutions/Shield/Detection/IAttackDetector.cs b/src/Plugins/Solutions/Shield/Detection/IAttackDetector.cs
new file mode 100644
index 0000000..fca7085
--- /dev/null
+++ b/src/Plugins/Solutions/Shield/Detection/IAttackDetector.cs
@@ -0,0 +1,62 @@
+namespace Shield.Detection;
+
+///
+/// Normalized input for attack detectors: who is calling, what is targeted,
+/// and when.
+///
+public sealed record ShieldRequest(
+ string ClientId,
+ string Path,
+ string Query,
+ DateTimeOffset Timestamp,
+ string UserAgent = "",
+ long? ContentLength = null);
+
+///
+/// What the host should do with a tripped signal.
+///
+public enum ShieldAction
+{
+ ///
+ /// Temporarily block the offending client; enough distinct offenders trigger global lockdown.
+ ///
+ Lockdown,
+
+ ///
+ /// Block only the offending request, without a global lockdown.
+ ///
+ BlockRequest,
+}
+
+///
+/// A detector verdict. Non-null means the detector tripped and the caller
+/// should apply with .
+///
+public sealed record AttackSignal(string Sensor, string Reason, ShieldAction Action = ShieldAction.Lockdown);
+
+///
+/// One attack-detection strategy. Implementations are pure policy: per-client
+/// storage lives in so HTTP and gRPC share it.
+/// Detectors run in DI registration order; the first signal wins.
+///
+public interface IAttackDetector
+{
+ ///
+ /// Short sensor name used in incidents and diagnostics (e.g. "rate-flood").
+ ///
+ string Sensor { get; }
+
+ ///
+ /// Evaluates the request. Returns a signal when the request trips the
+ /// detector, otherwise null.
+ ///
+ AttackSignal? Evaluate(ShieldRequest request, Options.ShieldOptions options);
+
+ ///
+ /// Observes the completed response (e.g. 401/403 counting). Returns a signal
+ /// when the observation trips the detector; the client block then applies to
+ /// subsequent requests. Only signals are
+ /// honored here — the response is already sent. Default ignores responses.
+ ///
+ AttackSignal? ObserveResponse(ShieldRequest request, int statusCode, Options.ShieldOptions options) => null;
+}
diff --git a/src/Plugins/Solutions/Shield/Detection/Sensors/AttackSignatureDetector.cs b/src/Plugins/Solutions/Shield/Detection/Sensors/AttackSignatureDetector.cs
new file mode 100644
index 0000000..82e3a8d
--- /dev/null
+++ b/src/Plugins/Solutions/Shield/Detection/Sensors/AttackSignatureDetector.cs
@@ -0,0 +1,53 @@
+using Shield.State;
+
+namespace Shield.Detection.Sensors;
+
+///
+/// Trips when path or query contains known attack fragments (traversal,
+/// SQL injection, XSS, suspicious system paths). Matching is case-insensitive
+/// substring search over the raw (still-encoded) target.
+///
+public sealed class AttackSignatureDetector(IShieldState state) : IAttackDetector
+{
+ // Lowercase fragments matched against path + query.
+ private static readonly string[] Signatures =
+ [
+ "../", "..%2f", "..%5c", "%2e%2e",
+ "union select", "union+select",
+ "