Capability-based security for the modern JVM
jGuard is a capability-based security framework for the modern JVM (JDK 21+).
jGuard enables JVM applications to run plugins, extensions, and embedded code with explicit, least-privilege access to sensitive resources such as the filesystem, network, threads, and native libraries.
Policies are declared declaratively using a module-style descriptor inspired by module-info.java and enforced at runtime with clear, auditable failure semantics.
Modern JVM applications increasingly execute code that is not fully trusted:
- plugins and extensions
- user-defined connectors
- embedded automation or agent runtimes
- long-lived, multi-tenant services
At the same time, the JVM no longer provides an in-process “deny by default” security boundary. Operating system sandboxing helps at the process boundary, but many applications need in-JVM least privilege:
“This module may read from
data/models/**, but may not open sockets or write files.”
jGuard addresses this need with a capability-oriented security model designed for the post–Security Manager JVM.
jGuard is built around a small set of explicit principles:
Code does not inherit access to sensitive resources implicitly. All sensitive operations require an explicit capability.
JPMS module identity is the root of trust. Packages refine privileges within a module.
Security decisions are based on what a module is allowed to do, not on global permissions or stack inspection.
If a capability is not explicitly granted, the operation fails.
Policies compile to deterministic metadata and can be rendered for human review and auditing.
- A capability framework for JVM applications
- Designed for plugin-based and extensible systems
- Compatible with JDK 21 and newer
- Incrementally adoptable
- A Java Security Manager replacement
- A full language sandbox
- A container or OS-level security system
- A blanket interceptor for all JVM APIs
jGuard complements OS-level isolation rather than replacing it.
jGuard policies are authored using a simple descriptor format inspired by module-info.java.
Example:
security module com.example.myplugin {
entitle com.example.myplugin.http.. to network.outbound;
entitle com.example.myplugin.io.. to fs.read(data, "models/**");
entitle com.example.myplugin.. to threads.create;
entitle module to fs.read(config, "**");
}
com.foo.bar— exact packagecom.foo.bar.*— direct subpackagescom.foo.bar..— recursive subpackages
For build environments that require Java-only sources, jGuard optionally supports a Java-backed descriptor that compiles to the same policy model.
import static io.jguard.policy.Descriptor.*;
public final class security_policy {
public static final Policy POLICY =
module("com.example.myplugin",
entitle("com.example.myplugin.http..", networkOutbound()),
entitle("com.example.myplugin.io..", fsRead(DATA, "models/**")),
entitle("com.example.myplugin..", threadsCreate()),
entitle(MODULE, fsRead(CONFIG, "**"))
);
}Both formats compile to identical policy metadata.
Capabilities represent explicit permission to perform sensitive operations.
Examples include:
fs.read(root, glob)— read files matching glob pattern under rootfs.write(root, glob)— write files matching glob pattern under rootnetwork.outbound(hostPattern?, portSpec?)— open outbound network connections with optional host/port filteringnetwork.listen(portSpec?)— bind server sockets to ports (supports port ranges)threads.create— create new threadsnative.load(pattern?)— load native librariesenv.read(pattern?)— read environment variablessystem.property.read(pattern?)— read system propertiessystem.property.write(pattern?)— write system properties
Host patterns for network.outbound:
*— matches one DNS segment (e.g.,*.example.com)**— matches one or more DNS segments (e.g.,**.example.com)
Port specs can be integers (443) or ranges ("80-443").
Capabilities are intentionally narrow and composable.
jGuard enforces policy at a small number of high-impact guard points:
- filesystem access
- outbound network connections
- thread creation and management
- native library loading
- environment variable access
- system property access
At runtime:
- A guarded operation is attempted
- jGuard attributes the call to a
(module, package) - The compiled policy is consulted
- The operation is allowed or denied deterministically
Unauthorized access fails fast with a clear, auditable exception.
When a policy violation occurs, jGuard throws a deterministic exception describing:
- the attempted operation
- the calling module and package
- the missing capability
This makes violations actionable and debuggable rather than silent.
jGuard is designed for:
- extensible JVM servers
- plugin-based platforms
- embedded scripting or agent execution
- enterprise JVM applications requiring least-privilege execution
plugins {
id "java"
id "application"
id "io.jguard.policy"
}
dependencies {
implementation("io.jguard:core")
}Create src/main/java/module-info.jguard:
security module com.example.myapp {
// Grant filesystem read access to all code in the module
entitle module to fs.read("src", "**/*");
entitle module to fs.read("config", "*.properties");
// Grant network access only to specific packages
entitle com.example.myapp.http.. to network.outbound;
}
# Development - with Gradle plugin
./gradlew runWithAgent
# Audit mode - log violations without blocking
./gradlew runWithAgent -Pjguard.mode=auditFor production, use an external policy file so administrators can update entitlements without rebuilding:
# Compile policy separately
jguardc -o /etc/myapp/policy.bin module-info.jguard
# Run with external policy
java -javaagent:jguard-agent.jar=/etc/myapp/policy.bin \
-Djguard.mode=strict \
-jar your-app.jarEnable hot reload to update entitlements without restarting the JVM:
java -javaagent:jguard-agent.jar=/etc/myapp/policy.bin \
-Djguard.mode=strict \
-Djguard.reload=true \
-Djguard.reload.interval=5 \
-jar your-app.jarWhen enabled, the agent polls the policy file for changes. Update the policy and changes take effect automatically:
# Update policy - no restart needed!
jguardc -o /etc/myapp/policy.bin updated-policy.jguard
# Changes detected and applied within 5 secondsSee agent/README.md for full agent documentation and gradle-plugin/README.md for build integration.
External policies allow administrators to modify entitlements at deployment time without rebuilding applications. External policies support both granting and denying capabilities.
| Scenario | Solution |
|---|---|
| Non-JPMS library needs permissions | External policy grants capabilities |
| Upstream library is overly permissive | External policy denies capabilities |
| Dev forgot a permission | External policy adds missing grant |
| Airgapped environment | Global policy denies all network access |
security module com.example.app {
// Grant: adds to effective permissions
entitle com.example.app.reports.. to fs.write("/var/reports", "**");
// Deny: removes from effective permissions
deny com.example.app.. to network.outbound;
// Deny (defensive): suppress warning if capability not already granted
deny(defensive) com.example.app.. to native.load;
}
java -javaagent:jguard-agent.jar \
-Djguard.policy.override=/etc/myapp/policies \
-Djguard.reload=true \
-jar myapp.jarDirectory structure:
/etc/myapp/policies/
├── _global.bin # Applies to ALL modules
├── com.example.app.bin # Policy for com.example.app
└── org.locationtech.proj4j.bin # Policy for non-JPMS library
External policies merge with embedded policies using this formula:
effective = (embedded ∪ external_grants ∪ global_grants) - (external_denials ∪ global_denials)
- Grants are combined (union)
- Denials remove from effective permissions (set difference)
- Denials always win over grants
// /etc/myapp/policies/com.overly.permissive.bin
security module com.overly.permissive {
// Library's embedded policy grants network.outbound to entire module
// We restrict it to only specific packages
deny module to network.outbound;
entitle com.overly.permissive.http.. to network.outbound;
}
// /etc/myapp/policies/_global.bin
security module _global {
// No network access for any module
deny module to network.outbound;
deny module to network.listen;
// No native code
deny(defensive) module to native.load;
}
See docs/roadmap/EXTERNAL_POLICY_GRANT_DENY.md for full documentation.
jGuard supports restricting third-party libraries that were not built with jGuard awareness. This is essential for applying least-privilege to dependencies from Maven Central or other repositories.
- External policy file: Create a
.jguardfile named after the library's module name - Grant specific capabilities: Only grant what the library actually needs
- Deny by default: Any capability not granted is blocked
For a library published as legacy-library.jar (automatic module name: legacy.library):
// policies-src/legacy.library.jguard
security module legacy.library {
// Only allow specific capabilities
entitle module to fs.read("/data", "**");
entitle module to system.property.read("java.version");
// Everything else is denied by default:
// - No network access
// - No thread creation
// - No native code
}
For libraries without module-info.java, Java derives the module name from the JAR filename:
legacy-library-1.0.jar→ module name:legacy.librarymy.awesome.lib-2.0.jar→ module name:my.awesome.lib
jGuard's MODULE pattern correctly matches all packages within automatic modules, regardless of package naming conventions.
# 1. Compile external policies
jguardc -o /etc/myapp/policies/legacy.library.bin policies-src/legacy.library.jguard
# 2. Run with external policy directory
java -javaagent:jguard-agent.jar \
-Djguard.policy.override=/etc/myapp/policies \
-jar myapp.jarSee samples/sandbox-legacy-library for a complete working example.
jGuard supports JPMS multi-module applications where each module has its own security policy. No configuration needed — the agent automatically discovers policies from signed JARs.
- Policy per module: Each module has its own
module-info.jguardnext tomodule-info.java - Policies embedded in JARs: Compiled policies are embedded at
META-INF/jguard/policy.bin - Auto-discovery: Agent automatically finds policies in signed JARs (no flags required)
- Module isolation: Each module can only use its own entitlements (no cross-module access)
my-app/
├── core/
│ └── src/main/java/
│ ├── module-info.java
│ └── module-info.jguard # fs.read entitlements
├── network/
│ └── src/main/java/
│ ├── module-info.java
│ └── module-info.jguard # network.outbound entitlements
└── app/
└── src/main/java/
├── module-info.java
└── module-info.jguard # minimal entitlements (delegates to core/network)
# Just attach the agent - policies are discovered automatically
./gradlew :app:runWithAgent
# Or manually:
java -javaagent:jguard-agent.jar -jar myapp.jarThe agent discovers all module policies from signed JARs and enforces each module's entitlements independently.
For local development with unsigned JARs:
// app/build.gradle
jguardPolicy {
allowUnsignedPolicies = true // Only for development!
}See samples/sandbox-multimodule for a complete working example.
jGuard is ready for production use with comprehensive enforcement.
Completed:
- Stable policy model with deterministic compilation
- Comprehensive filesystem enforcement (
fs.read,fs.write) - Network enforcement with host/port filtering (
network.outbound,network.listen) - Thread creation enforcement (
threads.create) - Native library loading enforcement (
native.load) - Environment variable access enforcement (
env.read) - System property access enforcement (
system.property.read,system.property.write) - Policy hot reload (update entitlements without restart)
- Clear failure semantics with configurable modes (STRICT, PERMISSIVE, AUDIT)
- Strong JPMS integration with module verification
- Single dispatch architecture for efficient capability checking
- Multi-module support with per-module policies and signed JAR verification
- External policies with grant/deny semantics for deployment-time customization
- Legacy library support for restricting third-party dependencies without jGuard policies
- CLI tools (
jguardccompiler,jguardinspector) for policy management
Apache 2.0
