Ihawu is a policy enforcement and dynamic data-masking engine for Kotlin — serialization-neutral and multiplatform (JVM + JS) at the core, with first-class adapters for Spring Boot and Ktor, masking on both Jackson and kotlinx.serialization. See the Roadmap below.
The developer community is plagued by security boilerplate leakage. Infrastructure tools like Keycloak can tell you who a user is (Authentication), and engines like Open Policy Agent (OPA) can tell you what rules apply (Authorization Decisions). However, neither tool can touch your application code nor interact with a JVM network socket stream.
As a result, developers are forced to contaminate clean business logic with manual if/else visibility checks or
maintain dozens of near-identical Data Transfer Objects (DTOs) like UserFullResponse and
UserMaskedResponse across microservices just to mask a credit card or hide a salary field.
Ihawu shifts access control out of your code and into a declarative data layer. It does not compete with identity providers or policy brains. It acts as the missing Policy Enforcement Point (PEP) that executes their decisions transparently at the serialization boundary.
Ihawu operates as an automated, stateless data filter embedded natively inside your web application's ingress and egress lifecycles:
- Ingress HTTP Request ────► Framework validates OAuth2 JWT (Keycloak)
- Ihawu Adapter ──► Maps raw claims into IhawuPrincipal
- ResourcePolicyResolver ──► Resolves this caller's field policies for the resource
- Controller Logic Runs ──► Returns raw, complete Database Entity class
- Ihawu Masker ──► Intercepts the serializer (Jackson or kotlinx.serialization)
- Outbound JSON Stream ──► Transparently stripped (HIDE) or obfuscated (REDACT)
Step 3 goes through the ResourcePolicyResolver SPI. Two implementations ship: RoleBasedResourcePolicyResolver
for static role-based rules, and — on Spring Boot — ConfigResourcePolicyProvider, which binds rules straight
from ihawu.policies configuration. Either can be wrapped in CachingResourcePolicyResolver. To source rules
from somewhere else — a database, a per-tenant service, OPA — you implement the SPI; Ihawu does not ship those
integrations.
- Ingress Capture: Your host framework (Spring Boot or Ktor) handles the network cryptography and token
verification. Ihawu instantly captures that verified identity and converts it into a uniform, framework-neutral
IhawuPrincipal. - Stateless Processing: Your business controller runs completely unpolluted, querying your application database and returning its raw, strongly-typed domain entity exactly as it is.
- Transparent Egress Masking: Right before that object is written to the network pipe as a JSON string,
Ihawu intercepts the serialization engine (Jackson or
kotlinx.serialization). It references your active policy matrix and dynamically drops (HIDE) or overwrites (REDACT) restricted fields on the fly.
- Zero-Friction Coexistence: Let Keycloak handle passwords, UI login screens, and MFA. Let OPA evaluate complex corporate-wide Rego files. Ihawu takes the identity your framework has already verified and the decisions your policy source returns, and handles the framework-specific task of enforcing them on the way out. It is a Policy Enforcement Point, not a second decision engine.
- Fail-Closed by Default: If no verified principal is attached, or a policy lookup fails, Ihawu masks every field
of the resource rather than serializing it — you get an empty JSON block
{}, not a leak. Forgetting to wire Ihawu up produces an empty object; with hand-rolled masking, forgetting produces a leak. The failure modes run in opposite directions. - No Reflection on the Hot Path: Property writers are wrapped once per type, while Jackson builds that type's
serializer — not per request. Policies are resolved once per (call, resource) and memoized for the rest of the write,
so a response containing a thousand instances of a resource resolves its policy once. There is no runtime reflection
on the serialization path. JMH numbers are published in
ADR 0008: masking costs roughly 3× plain
Jackson throughput on a representative payload (~2.5 µs/op absolute); reproduce with the
benchmarkmodule.
Fair question, and for a small enough problem the answer is: you should. Jackson ships both, and Ihawu is built on Jackson's serializer SPI rather than as an alternative to it.
@JsonView— if you have two or three fixed response shapes known at compile time, this is the right tool and Ihawu is overkill. It cannot substitute a value, though, so a redacted***-**-****is not expressible.@JsonFilter— dynamic, and much closer to what Ihawu does. Read honestly,ihawu-coreis a policy-driven, principal-aware@JsonFilterwith the wiring and the policy model supplied for you.
What you would build yourself to get there: a PropertyFilter subclass to substitute placeholders rather than
merely drop fields; a MappingJacksonValue wrapper at every handler, where the one you forget is the one that
leaks; policy plumbing to load rules from config, a database, or OPA; and manual assembly of the filter set for
nested types and collections. Ihawu supplies those, registers once on the ObjectMapper, recurses automatically,
and fails closed when identity is missing.
Full comparison — including masking in the database, which is stronger than Ihawu wherever it applies: Comparison.
Ihawu enforces at one exit: a serializer with Ihawu's masking installed — a Jackson ObjectMapper with
IhawuModule, or a kotlinx.serialization masking serializer — writing a value with an IhawuPrincipal attached.
Your object still travels through the application in full, and nothing masks it on any other path out of the
process. It is not masked when it is written to a log, published to Kafka, written to a cache, exported to CSV,
rendered into a server-side template, or serialized by a second serializer without Ihawu's masking installed.
That is inherent to enforcing at the serialization boundary, and it is a deliberate trade — masking where the response is written is what lets your controllers return whole, truthful domain objects. But it means Ihawu masks API responses, not "sensitive data" in general. If an SSN must never appear in a log line, Ihawu is not what stops it.
Treat Ihawu as last-mile, defense-in-depth enforcement, layered with controls that act closer to the data — column-level grants, row-level security, vendor dynamic masking — which are stronger wherever they apply, because the value never reaches your JVM at all. Full threat model: What Ihawu does not protect.
Add the Spring Boot starter — it pulls ihawu-core transitively, so it's the only dependency you add.
Gradle (Kotlin DSL)
implementation("org.ihawu:ihawu-spring-boot-starter:0.4.1")Gradle (Groovy DSL)
implementation "org.ihawu:ihawu-spring-boot-starter:0.4.1"Maven
<dependency>
<groupId>org.ihawu</groupId>
<artifactId>ihawu-spring-boot-starter</artifactId>
<version>0.4.1</version>
</dependency>Not on Spring? Depend on org.ihawu:ihawu-jackson (it pulls ihawu-core transitively), register
IhawuModule on your ObjectMapper, and drive masking through a ResourcePolicyResolver. Full guides
at docs.ihawu.org.
Not on the JVM, or on kotlinx.serialization? org.ihawu:ihawu-kotlinx is a multiplatform (JVM +
JS) backend: register a masking serializer per @IhawuResource type and encode via IhawuKotlinxJson.
It reuses the same neutral engine, so masking behaves identically — see
ADR 0008 for its design and performance tradeoffs.
On Ktor? org.ihawu:ihawu-ktor gives Ktor apps the same zero-wiring experience as the Spring Boot
starter: a single install(IhawuKtor) { … } masks every @IhawuResource response per the caller's
role. It builds on ihawu-kotlinx and reuses the same neutral engine — see
ADR 0009 and the runnable samples/ktor-sample.
Migrating from 0.2.0. The Jackson engine moved out of
ihawu-coreinto a newihawu-jacksonartifact, soihawu-coreitself is now serialization-neutral. Directihawu-coreusers who registeredIhawuModule— or loaded config viaRoleBasedResourcePolicyResolver.fromJson(nowJacksonPolicyConfig.fromJson) — must addorg.ihawu:ihawu-jackson. Spring Boot starter users are unaffected; the starter pulls it in.
With Ihawu, your business service controllers remain clean, explicit, and unpolluted by authorization rules. You return your raw, strongly-typed database records directly:
Declare every maskable field nullable — a masked field is either omitted (HIDE) or set to null (REDACT
on a non-String), so its type has to permit that. (String fields may stay non-nullable — they redact to a
placeholder.)
@IhawuResource(name = "UserProfile")
data class UserProfile(
val userId: String,
val fullName: String,
val email: String,
val socialSecurityNumber: String?,
val performanceReviewNotes: String?
)Masking respects your type contract. A masked response still has to deserialize into the type it claims to be, so what a strategy may do depends on how the field is declared.
REDACTwrites a placeholder on aString, andnullon any other nullable field — so a numeric or boolean field is redactable only when it is nullable, never to a fake0orfalse.HIDEomits the field, so it too needs a nullable/optional field. A non-nullable non-Stringfield has no honest masked form; declare it nullable to mask it. On Spring Boot these rules are checked at startup — a policy that cannot be honoured fails the application context, naming the field (ADR 0005).
On Spring Boot, bind the rules straight from configuration — no code:
ihawu:
policies:
- resource: UserProfile
roles:
EMPLOYEE:
- field: socialSecurityNumber
strategy: REDACT
placeholder: "***-**-****"
- field: performanceReviewNotes
strategy: HIDEFor rules that live elsewhere — a database, a per-tenant service, OPA — implement the
ResourcePolicyResolver SPI and register it as a bean. Ihawu calls it; it does not care where your rules
come from.
So for an EMPLOYEE requesting this object:
socialSecurityNumber──► Strategy:REDACT(Placeholder:"***-**-****")performanceReviewNotes──► Strategy:HIDE(Removes property entirely)
When an unauthorized user makes an HTTP request, Ihawu automatically transforms the serialization stream. The client safely receives:
{
"userId": "usr_9482",
"fullName": "Jane Doe",
"email": "jane.doe@company.com",
"socialSecurityNumber": "***-**-****"
}Ihawu fails closed: when it cannot mask safely it omits fields rather than leak them, so a resource
serializes as {} (or a partial object) with an HTTP 200. That is the correct default — but it makes
an empty {} ambiguous: it can mean "fully masked for this caller" or "policy resolution
failed and Ihawu fell back to masking everything." The two are byte-identical on the wire.
Until richer signals land (below), tell them apart from the logs — every fail-closed decision is logged with the resource name, never the protected value:
| Level | When | Message |
|---|---|---|
WARN |
No IhawuPrincipal on the serialization call |
"No IhawuPrincipal attached … serialization failing closed for resource '…'" |
ERROR |
The resolver threw (misconfig or policy-store outage) | "Ihawu masking failed for resource '…', serialization failing closed" |
ERROR |
A field's policy can't be honoured at runtime (HIDE on a non-nullable field, or REDACT on a non-nullable non-String) |
"Ihawu HIDE on required field …" / "Ihawu REDACT on non-nullable non-String …" |
Alert on the resolver-failure ERROR. A sustained rate of it means your policy source is down and
every @IhawuResource is degrading to {} — an outage wearing a 200, not normal masking. (The runtime
type-contract ERRORs should be rare: statically-configured policies are rejected at startup
(ADR 0005); they only reach runtime from a
dynamic resolver.)
A Micrometer failure metric (ihawu.masking.failures) and an optional fail-request mode — turning a
resolver outage into a 5xx your monitoring already understands — are planned for the
serialization-neutral core (#89).
Following the strict code documentation principles engineered by the JetBrains standard library team, all documentation
code samples in Ihawu are not static Markdown strings. Every reference code block lives inside our runnable test
suites within the /samples project subdirectory.
Dokka matches these targets via the @sample compiler tag. This guarantees that if our API layout ever shifts,
our public user documentation breaks at compile time during CI verification builds. Your guides are guaranteed to
be 100% accurate and functional forever.
Work is tracked in GitHub milestones. The first four shipped, in order:
- Docs: claims match behaviour — every claim in the README, CONTRIBUTING, and the docs site is either true or removed. No feature described that does not exist.
- 0.2.0 — Type-correct masking — masked output must satisfy the declared type contract. Type-aware masking
(
REDACTaStringto a placeholder or any nullable field tonull;HIDEan optional field), with policies that cannot be honoured failing at startup (#67, merging #68; ADR 0005), and observable fail-closed behaviour (#72). Breaking: masking a non-String(or anyHIDE) field now requires it to be nullable, andMaskingStrategy.defaultValueis renameddefaultPlaceholder. - 0.3.0 — Serialization-neutral core — the enforcement point now sits behind a serialization-neutral SPI (ADR 0006). This unlocked Kotlin Multiplatform and the Ktor adapter, and it is the same seam that would let Ihawu mask at sinks other than HTTP JSON.
That refactor is done: ihawu-core no longer depends on jackson-databind or slf4j-api and compiles to JVM and
JS. Jackson masking moved to ihawu-jackson; kotlinx.serialization masking lives in ihawu-kotlinx — a second
engine, since kotlinx has no equivalent of Jackson's BeanSerializerModifier. The full explanation is on the docs
site: Multiplatform.
- 0.4.0 — Polymorphism & observability — polymorphic
@IhawuResourcemasking (sealed and OPEN) on the kotlinx backend (ADR 0010), and observable, configurable fail-closed: Micrometerihawu.masking.failuresmetrics and an opt-infail-requestmode (ADR 0011). All additive. (0.4.1 patches the Spring Boot starter's metric auto-registration.)
Further work — a JMH benchmark, and a Dokka version dropdown once the API stabilises at 1.0 — is tracked in the issues.
On the road to 1.0, the public API is now deliberate (Kotlin explicit API mode) and mechanically locked (binary-compatibility-validator); what the 1.0 promise covers — and the dependency baselines it ships on — is written down in COMPATIBILITY.md.
We love open-source contributions! Ihawu uses interactive GitHub Issue Forms to streamline triage and project board management.
To get started:
- Fork the repository and explore our runnable test applications under
/samples. - Check our GitHub issue board for cards labeled
good-first-issue. - Ensure that any logic changes to
ihawu-coreinclude a compiling code sample update within thesamplesdirectory to maintain self-testing integrity.
For a comprehensive layout of code styles and PR submission steps, see CONTRIBUTING.md.
Ihawu is open-source software licensed under the Apache License 2.0.