Sandboxing Java Applications: Implementing Landlock LSM for Enhanced Container Security

Article

As Java applications become more security-sensitive, traditional container isolation often proves insufficient against sophisticated attacks. Landlock Linux Security Module (LSM) provides a powerful, programmatic way to sandbox applications at the filesystem level, offering Java developers fine-grained control over what their applications can access, even within already-containerized environments.

What is Landlock LSM?

Landlock is a Linux Security Module that enables unprivileged processes to create filesystem sandboxes. Unlike traditional LSMs that require root privileges, Landlock allows applications to voluntarily restrict their own access rights. For Java applications, this means you can enforce the principle of least privilege at runtime, limiting file and directory access to only what's strictly necessary.

Why Landlock Matters for Java Applications

  1. Defense in Depth: Even if an attacker compromises your Java application, Landlock restrictions prevent lateral movement and data exfiltration.
  2. Zero-Trust Filesystem Access: Enforces that Java applications can only access explicitly allowed paths, reducing the impact of path traversal vulnerabilities.
  3. Container Security Enhancement: Complements container isolation by adding an additional layer of filesystem security within the container itself.
  4. Runtime Security: Unlike static analysis, Landlock enforces restrictions at runtime, catching zero-day exploits and unexpected behavior.

Landlock Capabilities Relevant to Java

Landlock restricts filesystem operations through access rights:

  • File operations: read, write, execute, create, remove
  • Directory operations: traverse, remove_dir, make_dir
  • File attribute operations: getattr, setattr, chmod, chown

Implementing Landlock in Java Applications

1. Native Integration via JNI
Since Landlock is a Linux kernel feature, Java applications need to use JNI to interact with it:

// Landlock JNI wrapper
public class LandlockSandbox {
static {
System.loadLibrary("landlockjni");
}
// Native method to apply Landlock rules
public native void applySandbox(String[] allowedPaths, int[] accessRights);
// Helper method for common Java scenarios
public void restrictJavaApplication() {
// Define exactly what the Java application needs
String[] allowedPaths = {
"/app/myapp.jar",           // Application JAR
"/tmp",                     // Temporary files
"/var/log/myapp",           // Log directory
"/etc/ssl/certs",           // SSL certificates
"/usr/lib/jvm",             // JVM installation
"/proc/self/fd/1",          // stdout
"/proc/self/fd/2"           // stderr
};
int[] accessRights = {
0x1 | 0x2,  // READ | WRITE for JAR
0x2 | 0x4,  // WRITE | CREATE for /tmp
0x2 | 0x4,  // WRITE | CREATE for logs
0x1,        // READ for SSL certs
0x1 | 0x8,  // READ | EXECUTE for JVM
0x2,        // WRITE for stdout/stderr
};
applySandbox(allowedPaths, accessRights);
}
}

2. Native C Implementation
The JNI native code that implements Landlock rules:

// landlock_jni.c
#include <jni.h>
#include <linux/landlock.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <errno.h>
JNIEXPORT void JNICALL Java_LandlockSandbox_applySandbox
(JNIEnv *env, jobject obj, jobjectArray paths, jintArray rights) {
// Create Landlock ruleset
struct landlock_ruleset_attr ruleset_attr = {
.handled_access_fs = LANDLOCK_ACCESS_FS_EXECUTE |
LANDLOCK_ACCESS_FS_WRITE_FILE |
LANDLOCK_ACCESS_FS_READ_FILE |
LANDLOCK_ACCESS_FS_READ_DIR |
LANDLOCK_ACCESS_FS_REMOVE_DIR |
LANDLOCK_ACCESS_FS_REMOVE_FILE |
LANDLOCK_ACCESS_FS_MAKE_CHAR |
LANDLOCK_ACCESS_FS_MAKE_DIR |
LANDLOCK_ACCESS_FS_MAKE_REG |
LANDLOCK_ACCESS_FS_MAKE_SOCK |
LANDLOCK_ACCESS_FS_MAKE_FIFO |
LANDLOCK_ACCESS_FS_MAKE_BLOCK |
LANDLOCK_ACCESS_FS_MAKE_SYM,
};
int ruleset_fd = syscall(SYS_landlock_create_ruleset, 
&ruleset_attr, 
sizeof(ruleset_attr), 0);
if (ruleset_fd < 0) {
// Handle error - Landlock not supported or insufficient permissions
return;
}
// Get array sizes
jsize path_count = (*env)->GetArrayLength(env, paths);
jsize right_count = (*env)->GetArrayLength(env, rights);
for (int i = 0; i < path_count && i < right_count; i++) {
jstring path_str = (*env)->GetObjectArrayElement(env, paths, i);
jint right = (*env)->GetIntArrayElements(env, rights, NULL)[i];
const char *path = (*env)->GetStringUTFChars(env, path_str, NULL);
struct landlock_path_beneath_attr path_attr = {
.allowed_access = right,
.parent_fd = open(path, O_PATH | O_CLOEXEC),
};
if (path_attr.parent_fd >= 0) {
syscall(SYS_landlock_add_rule, 
ruleset_fd, 
LANDLOCK_RULE_PATH_BENEATH,
&path_attr, 0);
close(path_attr.parent_fd);
}
(*env)->ReleaseStringUTFChars(env, path_str, path);
}
// Apply the ruleset to current process
syscall(SYS_landlock_restrict_self, ruleset_fd, 0);
close(ruleset_fd);
}

3. Maven Build Configuration

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>17</source>
<target>17</target>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>native-maven-plugin</artifactId>
<executions>
<execution>
<id>compile-landlock-jni</id>
<goals>
<goal>compile</goal>
</goals>
<configuration>
<sources>
<source>
<directory>src/main/native</directory>
<includes>
<include>**/*.c</include>
</includes>
</source>
</sources>
<compilerStartOptions>
<compilerStartOption>-I${java.home}/../include</compilerStartOption>
<compilerStartOption>-I${java.home}/../include/linux</compilerStartOption>
<compilerStartOption>-fPIC</compilerStartOption>
</compilerStartOptions>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>

Security Profiles for Java Applications

1. Web Application Profile

public class WebAppLandlockProfile implements LandlockProfile {
public void apply() {
LandlockSandbox sandbox = new LandlockSandbox();
String[] paths = {
"/app/application.war",             // WAR file
"/tmp",                             // Temporary files
"/var/log/tomcat",                  // Tomcat logs
"/etc/ssl/certs",                   // SSL certificates
"/usr/share/zoneinfo",              // Timezone data
"/proc/self/fd/1",                  // stdout
"/proc/self/fd/2",                  // stderr
"/sys/fs/cgroup",                   // CGroup info (optional)
"/dev/urandom",                     // Random source
"/dev/null"                         // Null device
};
int[] rights = {
0x1,        // READ for WAR
0x2 | 0x4,  // WRITE | CREATE for /tmp
0x2,        // WRITE for logs
0x1,        // READ for SSL certs
0x1,        // READ for timezone
0x2,        // WRITE for stdout/stderr
0x1,        // READ for cgroups
0x1,        // READ for /dev/urandom
0x2         // WRITE for /dev/null
};
sandbox.applySandbox(paths, rights);
}
}

2. Batch Processing Profile

public class BatchJobLandlockProfile implements LandlockProfile {
public void apply() {
LandlockSandbox sandbox = new LandlockSandbox();
String[] paths = {
"/app/batch-job.jar",
"/input/data",                      // Input directory
"/output/results",                  // Output directory
"/tmp",
"/var/log/batch",
"/etc/passwd",                      // For user lookup
"/etc/group",                       // For group lookup
"/dev/urandom"
};
int[] rights = {
0x1,        // READ for JAR
0x1,        // READ for input
0x2 | 0x4,  // WRITE | CREATE for output
0x2 | 0x4,  // WRITE | CREATE for /tmp
0x2,        // WRITE for logs
0x1,        // READ for passwd
0x1,        // READ for group
0x1         // READ for /dev/urandom
};
sandbox.applySandbox(paths, rights);
}
}

Integration with Containerized Java Applications

1. Dockerfile with Landlock Support

FROM eclipse-temurin:17-jdk AS builder
# Install build dependencies
RUN apt-get update && apt-get install -y gcc libc6-dev
WORKDIR /app
COPY . .
# Build Java application and JNI library
RUN javac -h src/main/native src/main/java/com/company/LandlockSandbox.java
RUN gcc -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux \
-shared -fPIC -o liblandlockjni.so src/main/native/landlock_jni.c
# Runtime stage
FROM eclipse-temurin:17-jre
# Copy JNI library
COPY --from=builder /app/liblandlockjni.so /usr/lib/
# Copy application
COPY --from=builder /app/target/myapp.jar /app.jar
# Ensure Landlock is available (requires Linux kernel 5.13+)
RUN grep -q "landlock" /proc/self/status || echo "WARNING: Landlock not available"
# Run with Landlock sandbox
ENTRYPOINT ["java", "-Djava.library.path=/usr/lib", "-jar", "/app.jar"]

2. Kubernetes Security Context

apiVersion: apps/v1
kind: Deployment
metadata:
name: java-landlock-app
spec:
template:
spec:
containers:
- name: java-app
image: myapp:landlock
securityContext:
privileged: false
capabilities:
drop: ["ALL"]
seccompProfile:
type: RuntimeDefault
# Landlock requires specific kernel features
env:
- name: JAVA_OPTS
value: "-Djava.library.path=/usr/lib -XX:+UseContainerSupport"

Best Practices for Java Teams

1. Gradual Implementation Strategy

public class LandlockEnforcer {
public static void enableSandbox() {
try {
LandlockSandbox sandbox = new LandlockSandbox();
sandbox.restrictJavaApplication();
System.out.println("Landlock sandbox enabled successfully");
} catch (UnsatisfiedLinkError e) {
System.out.println("Landlock not available: " + e.getMessage());
// Continue without sandbox - fail open for compatibility
} catch (Exception e) {
System.err.println("Failed to enable Landlock: " + e.getMessage());
// Log but continue - security shouldn't break functionality
}
}
// Call this early in application startup
static {
enableSandbox();
}
}

2. Testing Landlock Restrictions

public class LandlockTest {
@Test
public void testSandboxRestrictions() {
// Test allowed paths
assertTrue(canRead("/app/myapp.jar"));
assertTrue(canWrite("/tmp/test.txt"));
// Test disallowed paths
assertFalse(canRead("/etc/shadow"));
assertFalse(canWrite("/root/.bashrc"));
assertFalse(canExecute("/bin/bash"));
}
private boolean canRead(String path) {
try {
Files.readAllBytes(Paths.get(path));
return true;
} catch (AccessDeniedException e) {
return false;
} catch (Exception e) {
return false;
}
}
}

3. Monitoring and Logging

public class LandlockMonitor {
private static final Logger logger = LoggerFactory.getLogger(LandlockMonitor.class);
public void monitorAccessAttempts() {
// Use Linux audit subsystem or eBPF to monitor access attempts
// This helps identify legitimate needs vs. attack attempts
try {
Process process = Runtime.getRuntime().exec(new String[]{
"auditctl", "-a", "always,exit", "-F", "arch=b64",
"-S", "openat", "-S", "execve", "-k", "landlock_java"
});
int exitCode = process.waitFor();
if (exitCode == 0) {
logger.info("Landlock audit rules installed");
}
} catch (Exception e) {
logger.warn("Failed to install audit rules: {}", e.getMessage());
}
}
}

Limitations and Considerations

1. Kernel Requirements

# Check Landlock availability
grep -q "landlock" /proc/self/status && echo "Landlock available" || echo "Landlock not available"
# Minimum kernel version: 5.13 for basic support
uname -r

2. Java-Specific Challenges

  • Dynamic Class Loading: Java applications that load classes at runtime may need access to unexpected paths
  • JNI Libraries: Native libraries may require additional filesystem access
  • Reflection: Some Java frameworks use reflection to access files, which might be blocked
  • Spring Boot DevTools: Development-time file watching features conflict with strict sandboxing

3. Fallback Strategies

public class AdaptiveSandbox {
private enum SandboxMode {
LANDBOX,
APPARMOR,
SELINUX,
NONE
}
public static SandboxMode detectAvailableSandbox() {
if (isLandlockAvailable()) {
return SandboxMode.LANDBOX;
} else if (isAppArmorAvailable()) {
return SandboxMode.APPARMOR;
} else if (isSELinuxAvailable()) {
return SandboxMode.SELINUX;
} else {
return SandboxMode.NONE;
}
}
public static void applyBestSandbox() {
switch (detectAvailableSandbox()) {
case LANDBOX:
applyLandlockSandbox();
break;
case APPARMOR:
applyAppArmorProfile();
break;
case SELINUX:
applySELinuxContext();
break;
case NONE:
logWarning("No LSM available - running without sandbox");
break;
}
}
}

Conclusion

Landlock LSM represents a significant advancement in application self-protection for Java developers. By allowing applications to voluntarily restrict their own filesystem access, it provides a powerful tool for implementing the principle of least privilege at runtime. While implementing Landlock in Java requires JNI and careful consideration of access patterns, the security benefits are substantial: reduced attack surface, containment of successful attacks, and enhanced compliance with security best practices.

For Java teams running security-sensitive applications, Landlock offers a way to add defense-in-depth that's complementary to containerization and other security measures. As Linux distributions adopt newer kernels with Landlock support, this technology will become an increasingly important part of the Java security toolkit, enabling applications to protect themselves even in compromised environments.

Title: Advanced Java Security: OAuth 2.0, Strong Authentication & Cryptographic Best Practices

Summary: These articles collectively explain how modern Java systems implement secure authentication, authorization, and password protection using industry-grade standards like OAuth 2.0 extensions, mutual TLS, and advanced password hashing algorithms, along with cryptographic best practices for generating secure randomness.

Links with explanations:
https://macronepal.com/blog/dpop-oauth-demonstrating-proof-of-possession-in-java-binding-tokens-to-clients/ (Explains DPoP, which binds OAuth tokens to a specific client so stolen tokens cannot be reused by attackers)
https://macronepal.com/blog/beyond-bearer-tokens-implementing-mutual-tls-for-strong-authentication-in-java/ (Covers mTLS, where both client and server authenticate each other using certificates for stronger security than bearer tokens)
https://macronepal.com/blog/oauth-2-0-token-exchange-in-java-implementing-rfc-8693-for-modern-identity-flows/ (Explains token exchange, allowing secure swapping of access tokens between services in distributed systems)
https://macronepal.com/blog/true-randomness-integrating-hardware-rngs-for-cryptographically-secure-java-applications/ (Discusses hardware-based random number generation for producing truly secure cryptographic keys)
https://macronepal.com/blog/the-password-hashing-dilemma-bcrypt-vs-pbkdf2-in-java/ (Compares BCrypt and PBKDF2 for password hashing and their resistance to brute-force attacks)
https://macronepal.com/blog/scrypt-implementation-in-java-memory-hard-password-hashing-for-jvm-applications/ (Explains Scrypt, a memory-hard hashing algorithm designed to resist GPU/ASIC attacks)
https://macronepal.com/blog/modern-password-security-implementing-argon2-in-java-applications/ (Covers Argon2, a modern and highly secure password hashing algorithm with strong memory-hard protections)

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper