
Policy enforcement and dynamic data-masking engine intercepting serialization streams to declaratively hide or redact sensitive fields per resolved caller policies, failing closed and minimizing runtime reflection.
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:
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.
IhawuPrincipal.kotlinx.serialization). It references your active policy matrix
and dynamically drops (HIDE) or overwrites (REDACT) restricted fields on the fly.{}, 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.benchmark module.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-core is a policy-driven,
principal-aware @JsonFilter with 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:
REDACT a String to a placeholder or any nullable field to null; HIDE an 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 any HIDE) field now requires
it to be nullable, and MaskingStrategy.defaultValue is renamed defaultPlaceholder.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.
@IhawuResource masking (sealed and OPEN) on
the kotlinx backend (ADR 0010), and observable,
configurable fail-closed: Micrometer ihawu.masking.failures metrics and an opt-in fail-request mode
(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:
/samples.good-first-issue.ihawu-core include a compiling code sample update within the samples
directory 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.
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:
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.
IhawuPrincipal.kotlinx.serialization). It references your active policy matrix
and dynamically drops (HIDE) or overwrites (REDACT) restricted fields on the fly.{}, 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.benchmark module.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-core is a policy-driven,
principal-aware @JsonFilter with 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:
REDACT a String to a placeholder or any nullable field to null; HIDE an 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 any HIDE) field now requires
it to be nullable, and MaskingStrategy.defaultValue is renamed defaultPlaceholder.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.
@IhawuResource masking (sealed and OPEN) on
the kotlinx backend (ADR 0010), and observable,
configurable fail-closed: Micrometer ihawu.masking.failures metrics and an opt-in fail-request mode
(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:
/samples.good-first-issue.ihawu-core include a compiling code sample update within the samples
directory 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.