Ktor adapter for the [guard-core](https://github.com/Guard-Core/guard-core-jvm) engine for the JVM.
1.1.1, released on Maven Central as com.guard-core:ktor-guard:1.1.1. ktor-guard is a true port of
the guard adapter pattern (fastapi-guard's SecurityMiddleware, JVM-idiomized for Ktor 3.x):
it wraps Ktor ApplicationCall into the guard-core-jvm facade protocols and drives the engine's
security pipeline per request through suspend-native Ktor server plugin hooks. It contains no
security logic of its own; every decision is the engine's.
- Engine:
com.guard-core:guard-core:4.4.0(built from source through a composite build, see Engine dependency). - Ktor 3.x (built and tested against 3.6.0), Kotlin 2.4.21, JDK 17, suspend-native: the facade's suspend APIs are called directly (no runBlocking bridges on this adapter).
- Tests: real Ktor
testApplicationsuites covering the pipeline, byte-exact facade error shapes, bypass, per-route decorator configs, CORS, HTTPS enforcement, the Redis-backed rate limit, and the bounded body-read contract.
Published on Maven Central:
dependencies {
implementation("com.guard-core:ktor-guard:1.1.1")
}It brings in the engine facade com.guard-core:guard-core:4.4.0. To build from source, see Engine dependency and Development.
Install the plugin with an engine SecurityConfig and wire per-route configuration through a
GuardRouteDecorator:
import com.guardcore.ktor.GuardCore
import com.guardcore.ktor.GuardRouteDecorator
import com.guardcore.ktor.SecurityConfigBuilder
import com.guardcore.ktor.guard
import com.guardcore.ktor.guardRateLimit
import io.ktor.server.application.install
import io.ktor.server.engine.embeddedServer
fun main() {
val config = SecurityConfigBuilder().apply {
blockedUserAgents = listOf("badbot")
enableRateLimiting = true
}.build()
val guard = GuardRouteDecorator(config)
embeddedServer(CIO, port = 8080) {
install(GuardCore) {
this.config = config
this.decorator = guard
}
routing {
get("/public") { call.respondText("hello") }
get("/limited") {
call.respondText("limited")
}.guardRateLimit(guard, requests = 5, window = 60)
get("/admin") {
call.respondText("admin")
}.guard(guard) {
requireIp(whitelist = listOf("10.0.0.1"))
}
}
}.start(wait = true)
}A runnable version lives in examples/demo-ktor.
The dispatch chain mirrors the reference adapter exactly:
- Lazy single-flight initialization; a Redis failure at init answers
503 Service temporarily unavailablewithRetry-After: 5. - The call wraps into the facade's
GuardRequest(headers, method, path, query, client IP, bounded body reads with replay to the handler). - CORS preflight runs the pipeline and answers with the engine's preflight response.
- Path exclusions pass through (fail-secure 403 when no client address resolves under
fail_secure). - The client IP resolves through the engine (
trusted_proxies,X-Forwarded-Forchain,X-Forwarded-Protohonoring). - Route configuration resolves per request from the stamped route id.
- Route-level all-checks bypass: the event fires and the response passes through the custom response modifier.
- The 17-check fixed-order pipeline runs; a block short-circuits the route handler and responds with the engine's byte-exact error shape.
- Behavioral usage rules run for decorated routes.
- The response exit path runs: metrics, security headers, CORS headers, behavioral return rules, and the custom response modifier.
A block responds through the facade's error-response factory (status, body, and security headers are the engine's), and the route handler never executes.
GuardRouteDecorator carries the facade SecurityDecorator factories 1:1 (same parameter
surface, route key bound by the extension). Grouped blocks mirror the Python mixins; every
factory is also a direct route extension:
guardAccessControl { requireIp(...) / blockCountries(...) / allowCountries(...) / blockClouds(...) / bypass(...) }andguardRequireIp,guardBlockCountries,guardAllowCountries,guardBlockClouds,guardBypassguardRateLimiting { rateLimit(...) / geoRateLimit(...) }andguardRateLimit,guardGeoRateLimitguardBehavioral { usageMonitor(...) / returnMonitor(...) / behaviorAnalysis(...) / suspiciousFrequency(...) }andguardUsageMonitor,guardReturnMonitor,guardBehaviorAnalysis,guardSuspiciousFrequencyguardAuthentication { requireHttps() / requireAuth(...) / apiKeyAuth(...) / requireAuthorizationHeader(...) / requireHeaders(...) }andguardRequireHttps,guardRequireAuth,guardApiKeyAuth,guardRequireAuthorizationHeader,guardRequireHeadersguardContentFiltering { blockUserAgents(...) / contentTypeFilter(...) / maxRequestSize(...) / requireReferrer(...) / customValidation(...) / detectionExclusion(...) }andguardBlockUserAgents,guardContentTypeFilter,guardMaxRequestSize,guardRequireReferrer,guardCustomValidation,guardDetectionExclusionguardAdvanced { timeWindow(...) / suspiciousDetection(...) / honeypotDetection(...) }andguardTimeWindow,guardSuspiciousDetection,guardHoneypotDetectionguard(decorator) { ... }applies any mix through one bound surface.
Stacked groups on one route mutate the same RouteConfig (one stamp per route, the TS WeakMap
analog), and the pipeline rebuilds when route configs change.
| Option | Type | Meaning |
|---|---|---|
config |
SecurityConfig |
The engine configuration (required). |
decorator |
GuardRouteDecorator? |
The per-route configuration surface adopted by the middleware. |
agentHandler |
AgentHandlerProtocol? |
guard-agent-jvm handler: pass GuardAgentTelemetry(agent) from com.guard-core:guard-agent-jvm (see its README). Without one, enable_agent degrades like the reference adapter's missing-package path. |
connectingIpResolver |
((ApplicationCall) -> String?)? |
Optional connecting-IP hook (defaults to the engine connection point's remote host). |
The middleware handle is reachable with application.guardCore().middleware:
reset(), refreshCloudIpRanges(), createErrorResponse(...), getInitializationStatus(),
agentStats(), and shutdown() mirror the reference adapter's guard surface.
ktor-guard contains no security logic; the engine lives in
Guard-Core/guard-core-jvm and is built from
source through a composite build (includeBuild + dependency substitution in
settings.gradle.kts, the Gradle analog of the Rust path+version pattern). Declared coordinates
stay com.guard-core:guard-core:4.4.0 (lockstep with the guard-core 4.4.0 release); substitution
redirects them to the included build until the artifacts publish to Maven Central.
CI checks the engine out at the pinned ref as a sibling (path: guard-core-jvm); locally use
the same layout:
git clone https://github.com/Guard-Core/guard-core-jvm ../guard-core-jvm
./gradlew build --no-daemonSet GUARD_CORE_USE_COMPOSITE=false to build without the composite: the declared coordinates
then resolve from the repositories (mavenLocal first, then Maven Central) - this is how the
release workflow proves the published artifact graph (docs/release.md).
Adapter parity with the reference middleware releases is assessed per upstream feature release:
docs/adapter-parity-8.1.0.md maps fastapi-guard 8.1.0's
PureASGISecurityMiddleware onto this adapter's Ktor server-plugin hooks (all three
pure-streaming properties hold natively; zero code changes).
./gradlew build --no-daemon # compile + ktfmt check + detekt + tests
./gradlew test --no-daemon # tests (Redis-backed cases need localhost:6379; they skip otherwise)
./gradlew spotlessApply --no-daemon # formatThe engine and the facade are external here; adapter work never touches security logic. Facade bugs go through guard-core-jvm's own PR process.