Article
Rootless containers allow container engines to run without root privileges, significantly improving security by reducing the attack surface. This guide covers how to interact with and manage rootless containers from Java applications, using various libraries and approaches.
Why Rootless Containers?
- Enhanced Security: No root privileges = smaller attack surface
- Compliance: Meets security requirements in multi-tenant environments
- User Namespaces: Leverage Linux kernel namespaces for isolation
- Podman Support: Native rootless mode in Podman
- Docker Rootless Mode: Experimental but available
Project Setup and Dependencies
Maven Dependencies:
<properties>
<docker-java.version>3.3.4</docker-java.version>
<podman-java.version>1.0.0</podman-java.version>
<jna.version>5.13.0</jna.version>
</properties>
<dependencies>
<!-- Docker Java Client -->
<dependency>
<groupId>com.github.docker-java</groupId>
<artifactId>docker-java</artifactId>
<version>${docker-java.version}</version>
</dependency>
<!-- Podman Java Client (when available) -->
<!-- Alternative: Use Podman's Docker-compatible API -->
<!-- Unix Socket Communication -->
<dependency>
<groupId>com.github.jnr</groupId>
<artifactId>jnr-unixsocket</artifactId>
<version>0.38.19</version>
</dependency>
<!-- JNA for Native Operations -->
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>
<version>${jna.version}</version>
</dependency>
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna-platform</artifactId>
<version>${jna.version}</version>
</dependency>
<!-- Process Execution -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-exec</artifactId>
<version>1.3</version>
</dependency>
<!-- JSON Processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version>
</dependency>
</dependencies>
1. Understanding Rootless Container Architecture
Rootless containers work through:
- User Namespaces: Map UID/GID inside container to non-root outside
- RootlessKit: Parent process for user namespace creation
- Slirp4netns: User-mode networking stack
- FUSE-overlayfs: User-mode filesystem for layers
2. Podman Rootless Client (Primary Approach)
Podman has excellent native support for rootless containers.
Podman Rootless Client:
package com.example.rootless.podman;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.exec.CommandLine;
import org.apache.commons.exec.DefaultExecutor;
import org.apache.commons.exec.ExecuteWatchdog;
import org.apache.commons.exec.PumpStreamHandler;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
public class PodmanRootlessClient {
private final String podmanPath;
private final ObjectMapper objectMapper;
private final Map<String, String> environment;
public PodmanRootlessClient() {
this("/usr/bin/podman");
}
public PodmanRootlessClient(String podmanPath) {
this.podmanPath = podmanPath;
this.objectMapper = new ObjectMapper();
this.environment = new HashMap<>();
// Setup environment for rootless operation
setupRootlessEnvironment();
}
private void setupRootlessEnvironment() {
// Important environment variables for rootless Podman
environment.put("XDG_RUNTIME_DIR",
System.getenv().getOrDefault("XDG_RUNTIME_DIR", "/run/user/" + getUserId()));
// Set storage directory for rootless user
String userHome = System.getProperty("user.home");
environment.put("XDG_DATA_HOME", userHome + "/.local/share");
// Ensure we're not using root
environment.put("PODMAN_USERNS", "keep-id");
}
public String runContainer(String image, List<String> command, Map<String, String> options)
throws IOException {
CommandLine cmdLine = new CommandLine(podmanPath);
cmdLine.addArgument("run");
cmdLine.addArgument("--rm");
// Rootless-specific flags
cmdLine.addArgument("--userns=keep-id");
cmdLine.addArgument("--security-opt=no-new-privileges");
cmdLine.addArgument("--pids-limit=100");
// Add user options
if (options != null) {
options.forEach((key, value) -> {
cmdLine.addArgument("--" + key);
if (value != null && !value.isEmpty()) {
cmdLine.addArgument(value);
}
});
}
cmdLine.addArgument(image);
if (command != null) {
command.forEach(cmdLine::addArgument);
}
return executeCommand(cmdLine);
}
public String createRootlessPod(String name, Map<String, String> options) throws IOException {
CommandLine cmdLine = new CommandLine(podmanPath);
cmdLine.addArgument("pod");
cmdLine.addArgument("create");
cmdLine.addArgument("--name");
cmdLine.addArgument(name);
// Rootless pod options
cmdLine.addArgument("--infra=false"); // No infra container for simpler rootless
cmdLine.addArgument("--share=net,ipc");
if (options != null) {
if (options.containsKey("network")) {
cmdLine.addArgument("--network");
cmdLine.addArgument(options.get("network"));
}
}
return executeCommand(cmdLine);
}
public List<ContainerInfo> listContainers(boolean all) throws IOException {
CommandLine cmdLine = new CommandLine(podmanPath);
cmdLine.addArgument("ps");
if (all) {
cmdLine.addArgument("-a");
}
cmdLine.addArgument("--format");
cmdLine.addArgument("json");
String output = executeCommand(cmdLine);
return objectMapper.readValue(output,
objectMapper.getTypeFactory().constructCollectionType(List.class, ContainerInfo.class));
}
public String executeInContainer(String containerId, String[] command) throws IOException {
CommandLine cmdLine = new CommandLine(podmanPath);
cmdLine.addArgument("exec");
cmdLine.addArgument("-u");
cmdLine.addArgument(String.valueOf(getUserId())); // Execute as current user
cmdLine.addArgument(containerId);
cmdLine.addArguments(command);
return executeCommand(cmdLine);
}
public void setupUserNamespace(Map<String, String> subuidMap, Map<String, String> subgidMap)
throws IOException {
// Create /etc/subuid and /etc/subgid entries for user namespace
String username = System.getProperty("user.name");
int uid = getUserId();
// Write subuid mapping
StringBuilder subuid = new StringBuilder();
subuid.append(username).append(":");
subuid.append(uid).append(":65536\n"); // Map 65536 UIDs
Files.write(Paths.get("/etc/subuid"), subuid.toString().getBytes());
// Write subgid mapping
StringBuilder subgid = new StringBuilder();
subgid.append(username).append(":");
subgid.append(uid).append(":65536\n"); // Map 65536 GIDs
Files.write(Paths.get("/etc/subgid"), subgid.toString().getBytes());
}
private String executeCommand(CommandLine cmdLine) throws IOException {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ByteArrayOutputStream errorStream = new ByteArrayOutputStream();
DefaultExecutor executor = new DefaultExecutor();
executor.setWatchdog(new ExecuteWatchdog(60000)); // 60 second timeout
executor.setStreamHandler(new PumpStreamHandler(outputStream, errorStream));
// Set environment
Map<String, String> env = new HashMap<>(System.getenv());
env.putAll(environment);
try {
int exitValue = executor.execute(cmdLine, env);
if (exitValue != 0) {
throw new IOException("Command failed: " + errorStream.toString());
}
return outputStream.toString();
} catch (Exception e) {
throw new IOException("Failed to execute command: " + e.getMessage() +
"\nError: " + errorStream.toString(), e);
}
}
private int getUserId() {
try {
Process process = Runtime.getRuntime().exec("id -u");
try (var reader = new java.io.BufferedReader(
new java.io.InputStreamReader(process.getInputStream()))) {
String uid = reader.readLine();
return Integer.parseInt(uid.trim());
}
} catch (Exception e) {
return 1000; // Default fallback
}
}
// Container information class
public static class ContainerInfo {
private String id;
private String image;
private String command;
private String created;
private String status;
private List<String> names;
// Getters and setters
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getImage() { return image; }
public void setImage(String image) { this.image = image; }
public String getCommand() { return command; }
public void setCommand(String command) { this.command = command; }
public String getCreated() { return created; }
public void setCreated(String created) { this.created = created; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public List<String> getNames() { return names; }
public void setNames(List<String> names) { this.names = names; }
}
}
3. Docker Rootless Mode Client
Docker also supports rootless mode through dockerd-rootless.sh.
Docker Rootless Client:
package com.example.rootless.docker;
import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.command.CreateContainerResponse;
import com.github.dockerjava.api.model.*;
import com.github.dockerjava.core.DefaultDockerClientConfig;
import com.github.dockerjava.core.DockerClientImpl;
import com.github.dockerjava.httpclient5.ApacheDockerHttpClient;
import java.io.File;
import java.time.Duration;
import java.util.*;
public class DockerRootlessClient {
private final DockerClient dockerClient;
private final String socketPath;
public DockerRootlessClient() {
// Rootless Docker socket location
this.socketPath = System.getenv().getOrDefault(
"DOCKER_HOST",
"unix:///run/user/" + getUserId() + "/docker.sock"
);
DefaultDockerClientConfig config = DefaultDockerClientConfig.createDefaultConfigBuilder()
.withDockerHost(socketPath)
.withDockerTlsVerify(false)
.build();
ApacheDockerHttpClient httpClient = new ApacheDockerHttpClient.Builder()
.dockerHost(config.getDockerHost())
.sslConfig(config.getSSLConfig())
.maxConnections(100)
.connectionTimeout(Duration.ofSeconds(30))
.responseTimeout(Duration.ofSeconds(45))
.build();
this.dockerClient = DockerClientImpl.getInstance(config, httpClient);
}
public String createRootlessContainer(String image, String command, Map<String, String> options) {
// Create container with rootless constraints
HostConfig hostConfig = HostConfig.newHostConfig()
.withMemory(256 * 1024 * 1024L) // 256MB limit
.withCpuShares(512) // CPU limit
.withPidsLimit(100L) // Process limit
.withSecurityOpts(Arrays.asList(
"no-new-privileges",
"seccomp=unconfined" // Rootless may need custom seccomp
))
.withUsernsMode("host") // Use host user namespace
.withRuntime("runc"); // Use runc runtime
// Add bind mounts with proper UID/GID mapping
if (options != null && options.containsKey("volume")) {
hostConfig.withBinds(Bind.parse(options.get("volume")));
}
CreateContainerResponse container = dockerClient.createContainerCmd(image)
.withCmd(command.split(" "))
.withHostConfig(hostConfig)
.withUser(String.valueOf(getUserId())) // Run as current user
.exec();
// Start container
dockerClient.startContainerCmd(container.getId()).exec();
return container.getId();
}
public void runRootlessContainer(String image, String command) {
String containerId = createRootlessContainer(image, command, null);
// Attach to container to get output
dockerClient.attachContainerCmd(containerId)
.withStdOut(true)
.withStdErr(true)
.withFollowStream(true)
.exec(new com.github.dockerjava.api.async.ResultCallback.Adapter<>() {
@Override
public void onNext(Frame item) {
System.out.println(new String(item.getPayload()));
}
});
}
public List<Container> listRootlessContainers() {
return dockerClient.listContainersCmd()
.withShowAll(true)
.exec();
}
public void setupRootlessDocker() throws Exception {
// This would start dockerd-rootless.sh if not running
String userHome = System.getProperty("user.home");
File rootlessKit = new File(userHome + "/bin/rootlesskit");
if (!rootlessKit.exists()) {
installRootlessKit();
}
// Check if Docker rootless is running
Process process = Runtime.getRuntime().exec("systemctl --user is-active docker-rootless");
int exitCode = process.waitFor();
if (exitCode != 0) {
// Start Docker rootless
startRootlessDocker();
}
}
private void installRootlessKit() throws Exception {
// Install RootlessKit and dependencies
ProcessBuilder pb = new ProcessBuilder(
"curl", "-fsSL", "https://get.docker.com/rootless", "-o", "install-rootless.sh"
);
Process process = pb.start();
process.waitFor();
pb = new ProcessBuilder("sh", "install-rootless.sh");
process = pb.start();
process.waitFor();
}
private void startRootlessDocker() throws Exception {
// Start Docker in rootless mode
String userHome = System.getProperty("user.home");
ProcessBuilder pb = new ProcessBuilder(
userHome + "/bin/dockerd-rootless.sh"
);
Process process = pb.start();
// Wait for Docker socket to be ready
Thread.sleep(5000);
}
private int getUserId() {
try {
Process process = Runtime.getRuntime().exec("id -u");
try (var reader = new java.io.BufferedReader(
new java.io.InputStreamReader(process.getInputStream()))) {
String uid = reader.readLine();
return Integer.parseInt(uid.trim());
}
} catch (Exception e) {
return 1000;
}
}
}
4. User Namespace Management in Java
Direct interaction with Linux user namespaces.
User Namespace Manager:
package com.example.rootless.namespaces;
import com.sun.jna.*;
import com.sun.jna.ptr.IntByReference;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class UserNamespaceManager {
// JNA interface for Linux syscalls
public interface CLibrary extends Library {
CLibrary INSTANCE = Native.load("c", CLibrary.class);
int unshare(int flags);
int setns(int fd, int nstype);
int clone(int flags, Pointer child_stack, Pointer ptid, Pointer ctid, Pointer regs);
int getuid();
int getgid();
int setuid(int uid);
int setgid(int gid);
}
// Linux namespace constants
public static final int CLONE_NEWUSER = 0x10000000;
public static final int CLONE_NEWNS = 0x00020000;
public static final int CLONE_NEWPID = 0x20000000;
public static final int CLONE_NEWNET = 0x40000000;
public static final int CLONE_NEWIPC = 0x08000000;
public static final int CLONE_NEWUTS = 0x04000000;
public static final int CLONE_NEWCGROUP = 0x02000000;
public void createUserNamespace(int uid, int gid) throws IOException {
// Create new user namespace
int result = CLibrary.INSTANCE.unshare(CLONE_NEWUSER);
if (result != 0) {
throw new IOException("Failed to create user namespace: " +
Native.getLastError());
}
// Setup UID/GID mappings
setupUidMapping(uid);
setupGidMapping(gid);
}
private void setupUidMapping(int uid) throws IOException {
// Write to /proc/self/uid_map
String mapping = "0 " + uid + " 1";
Files.write(Paths.get("/proc/self/uid_map"), mapping.getBytes());
// Write to /proc/self/setgroups
Files.write(Paths.get("/proc/self/setgroups"), "deny".getBytes());
}
private void setupGidMapping(int gid) throws IOException {
// Write to /proc/self/gid_map
String mapping = "0 " + gid + " 1";
Files.write(Paths.get("/proc/self/gid_map"), mapping.getBytes());
}
public void createRootlessContainerNamespace() throws IOException {
// Create all namespaces for container isolation
int flags = CLONE_NEWUSER | CLONE_NEWNS | CLONE_NEWPID |
CLONE_NEWNET | CLONE_NEWIPC | CLONE_NEWUTS;
int result = CLibrary.INSTANCE.unshare(flags);
if (result != 0) {
throw new IOException("Failed to create container namespaces: " +
Native.getLastError());
}
// Now we're in an isolated namespace
// Mount proc filesystem
mountProc();
}
private void mountProc() throws IOException {
ProcessBuilder pb = new ProcessBuilder("mount", "-t", "proc", "proc", "/proc");
Process process = pb.start();
try {
int exitCode = process.waitFor();
if (exitCode != 0) {
throw new IOException("Failed to mount /proc");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IOException("Interrupted while mounting /proc", e);
}
}
public static class ContainerNamespace {
private final Path namespaceDir;
private final int pid;
public ContainerNamespace(String name) throws IOException {
this.namespaceDir = Paths.get("/var/run/user/" +
CLibrary.INSTANCE.getuid() + "/" + name);
Files.createDirectories(namespaceDir);
// Create namespace files
createNamespaceFiles();
this.pid = createContainerProcess();
}
private void createNamespaceFiles() throws IOException {
// Create user namespace
Files.write(namespaceDir.resolve("userns"),
String.valueOf(CLONE_NEWUSER).getBytes());
// Create PID namespace
Files.write(namespaceDir.resolve("pidns"),
String.valueOf(CLONE_NEWPID).getBytes());
// Create network namespace
Files.write(namespaceDir.resolve("netns"),
String.valueOf(CLONE_NEWNET).getBytes());
}
private int createContainerProcess() throws IOException {
// Use fork to create process in new namespace
ProcessBuilder pb = new ProcessBuilder("unshare",
"--user", "--pid", "--fork", "--mount-proc",
"sleep", "infinity");
Process process = pb.start();
return (int) process.pid();
}
public void executeInNamespace(String[] command) throws IOException {
// Use nsenter to enter namespace and execute command
ProcessBuilder pb = new ProcessBuilder("nsenter",
"--user=/proc/" + pid + "/ns/user",
"--pid=/proc/" + pid + "/ns/pid",
"--net=/proc/" + pid + "/ns/net",
"sh", "-c", String.join(" ", command));
Process process = pb.start();
// Read output
try (var reader = new java.io.BufferedReader(
new java.io.InputStreamReader(process.getInputStream()))) {
reader.lines().forEach(System.out::println);
}
}
public void destroy() throws IOException {
// Kill the container process
ProcessBuilder pb = new ProcessBuilder("kill", "-9", String.valueOf(pid));
pb.start();
// Clean up namespace directory
Files.walk(namespaceDir)
.sorted(java.util.Comparator.reverseOrder())
.map(Path::toFile)
.forEach(File::delete);
}
}
}
5. Rootless Container Security Manager
Security Policy Enforcement:
package com.example.rootless.security;
import java.io.File;
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.PosixFilePermission;
import java.util.*;
public class RootlessSecurityManager {
public void applyRootlessSecurityProfile(String containerId) throws IOException {
// Apply AppArmor profile for rootless containers
applyAppArmorProfile(containerId);
// Setup seccomp filter
setupSeccompProfile(containerId);
// Set capabilities
setupCapabilities(containerId);
// Configure cgroups
setupCgroups(containerId);
}
private void applyAppArmorProfile(String containerId) throws IOException {
String profile = """
#include <tunables/global>
profile rootless-container flags=(attach_disconnected,mediate_deleted) {
#include <abstractions/base>
# Deny privileged operations
deny capability sys_admin,
deny capability sys_module,
deny capability sys_rawio,
# Allow necessary operations
capability chown,
capability dac_override,
capability fowner,
capability fsetid,
capability kill,
capability setgid,
capability setuid,
capability setpcap,
capability net_bind_service,
capability net_raw,
capability sys_chroot,
capability mknod,
capability audit_write,
capability setfcap,
# Network access
network inet stream,
network inet6 stream,
network inet dgram,
network inet6 dgram,
# File system access
deny @{PROC}/sys/** rwklx,
deny @{SYS}/** rwklx,
# Allow writing to temporary directories
/tmp/** rw,
/var/tmp/** rw,
# Home directory access (user-specific)
/home/{user}/** rw,
}
""";
// Write profile to AppArmor directory
Path profilePath = Paths.get("/etc/apparmor.d/rootless-" + containerId);
Files.write(profilePath, profile.getBytes());
// Load profile
ProcessBuilder pb = new ProcessBuilder("apparmor_parser",
"-r", profilePath.toString());
Process process = pb.start();
try {
process.waitFor();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
private void setupSeccompProfile(String containerId) throws IOException {
String seccompProfile = """
{
"defaultAction": "SCMP_ACT_ERRNO",
"architectures": [
"SCMP_ARCH_X86_64",
"SCMP_ARCH_X86",
"SCMP_ARCH_X32"
],
"syscalls": [
{
"names": [
"accept",
"accept4",
"access",
"alarm",
"bind",
"brk",
"capget",
"capset",
"chdir",
"chmod",
"chown",
"chown32",
"clock_getres",
"clock_gettime",
"clock_nanosleep",
"close",
"connect",
"copy_file_range",
"creat",
"dup",
"dup2",
"dup3",
"epoll_create",
"epoll_create1",
"epoll_ctl",
"epoll_pwait",
"epoll_wait",
"eventfd",
"eventfd2",
"execve",
"execveat",
"exit",
"exit_group",
"faccessat",
"faccessat2",
"fadvise64",
"fallocate",
"fchdir",
"fchmod",
"fchmodat",
"fchown",
"fchown32",
"fchownat",
"fcntl",
"fcntl64",
"fdatasync",
"fgetxattr",
"flistxattr",
"flock",
"fork",
"fremovexattr",
"fsetxattr",
"fstat",
"fstat64",
"fstatat64",
"fsync",
"ftruncate",
"ftruncate64",
"futex",
"futimesat",
"getcpu",
"getcwd",
"getdents",
"getdents64",
"getegid",
"getegid32",
"geteuid",
"geteuid32",
"getgid",
"getgid32",
"getgroups",
"getgroups32",
"getitimer",
"getpeername",
"getpgid",
"getpgrp",
"getpid",
"getppid",
"getpriority",
"getrandom",
"getresgid",
"getresgid32",
"getresuid",
"getresuid32",
"getrlimit",
"getrusage",
"getsid",
"getsockname",
"getsockopt",
"get_thread_area",
"gettid",
"gettimeofday",
"getuid",
"getuid32",
"getxattr",
"inotify_add_watch",
"inotify_init",
"inotify_init1",
"inotify_rm_watch",
"io_cancel",
"ioctl",
"io_destroy",
"io_getevents",
"ioprio_get",
"ioprio_set",
"io_setup",
"io_submit",
"ipc",
"kill",
"lchown",
"lchown32",
"lgetxattr",
"link",
"linkat",
"listen",
"listxattr",
"llistxattr",
"_llseek",
"lremovexattr",
"lseek",
"lsetxattr",
"lstat",
"lstat64",
"madvise",
"memfd_create",
"mincore",
"mkdir",
"mkdirat",
"mknod",
"mknodat",
"mlock",
"mlock2",
"mlockall",
"mmap",
"mmap2",
"mprotect",
"mq_getsetattr",
"mq_notify",
"mq_open",
"mq_timedreceive",
"mq_timedsend",
"mq_unlink",
"mremap",
"msgctl",
"msgget",
"msgrcv",
"msgsnd",
"msync",
"munlock",
"munlockall",
"munmap",
"nanosleep",
"newfstatat",
"_newselect",
"open",
"openat",
"pause",
"pipe",
"pipe2",
"poll",
"ppoll",
"prctl",
"pread64",
"preadv",
"preadv2",
"prlimit64",
"pwrite64",
"pwritev",
"pwritev2",
"read",
"readahead",
"readlink",
"readlinkat",
"readv",
"recv",
"recvfrom",
"recvmmsg",
"recvmsg",
"remap_file_pages",
"removexattr",
"rename",
"renameat",
"renameat2",
"restart_syscall",
"rmdir",
"rt_sigaction",
"rt_sigpending",
"rt_sigprocmask",
"rt_sigqueueinfo",
"rt_sigreturn",
"rt_sigsuspend",
"rt_sigtimedwait",
"rt_tgsigqueueinfo",
"sched_getaffinity",
"sched_getattr",
"sched_getparam",
"sched_get_priority_max",
"sched_get_priority_min",
"sched_getscheduler",
"sched_rr_get_interval",
"sched_setaffinity",
"sched_setattr",
"sched_setparam",
"sched_setscheduler",
"sched_yield",
"seccomp",
"select",
"semctl",
"semget",
"semop",
"semtimedop",
"send",
"sendmmsg",
"sendmsg",
"sendto",
"setfsgid",
"setfsgid32",
"setfsuid",
"setfsuid32",
"setgid",
"setgid32",
"setgroups",
"setgroups32",
"setitimer",
"setpgid",
"setpriority",
"setregid",
"setregid32",
"setresgid",
"setresgid32",
"setresuid",
"setresuid32",
"setreuid",
"setreuid32",
"setrlimit",
"setsid",
"setsockopt",
"set_thread_area",
"set_tid_address",
"setuid",
"setuid32",
"setxattr",
"shmat",
"shmctl",
"shmdt",
"shmget",
"shutdown",
"sigaltstack",
"signalfd",
"signalfd4",
"sigprocmask",
"sigreturn",
"socket",
"socketcall",
"socketpair",
"splice",
"stat",
"stat64",
"statfs",
"statfs64",
"symlink",
"symlinkat",
"sync",
"sync_file_range",
"sysinfo",
"tee",
"tgkill",
"time",
"timer_create",
"timer_delete",
"timer_getoverrun",
"timer_gettime",
"timer_settime",
"timerfd_create",
"timerfd_gettime",
"timerfd_settime",
"times",
"tkill",
"truncate",
"truncate64",
"ugetrlimit",
"umask",
"uname",
"unlink",
"unlinkat",
"utime",
"utimensat",
"utimes",
"vfork",
"vmsplice",
"wait4",
"waitid",
"waitpid",
"write",
"writev"
],
"action": "SCMP_ACT_ALLOW"
}
]
}
""";
// Write seccomp profile
Path seccompPath = Paths.get("/var/lib/rootless/seccomp-" + containerId + ".json");
Files.createDirectories(seccompPath.getParent());
Files.write(seccompPath, seccompProfile.getBytes());
}
private void setupCapabilities(String containerId) throws IOException {
// Define allowed capabilities for rootless containers
Set<String> allowedCapabilities = Set.of(
"CAP_CHOWN",
"CAP_DAC_OVERRIDE",
"CAP_FOWNER",
"CAP_FSETID",
"CAP_KILL",
"CAP_SETGID",
"CAP_SETUID",
"CAP_SETPCAP",
"CAP_NET_BIND_SERVICE",
"CAP_NET_RAW",
"CAP_SYS_CHROOT",
"CAP_MKNOD",
"CAP_AUDIT_WRITE",
"CAP_SETFCAP"
);
// Write capabilities to container config
String capabilities = String.join(",", allowedCapabilities);
Path capsPath = Paths.get("/var/lib/rootless/caps-" + containerId);
Files.write(capsPath, capabilities.getBytes());
}
private void setupCgroups(String containerId) throws IOException {
// Create cgroup for the container
String cgroupPath = "/sys/fs/cgroup/user.slice/rootless-" + containerId;
Files.createDirectories(Paths.get(cgroupPath));
// Set memory limit (e.g., 512MB)
Files.write(Paths.get(cgroupPath + "/memory.max"), "536870912".getBytes());
// Set CPU limit (e.g., 50% of one CPU)
Files.write(Paths.get(cgroupPath + "/cpu.max"), "50000 100000".getBytes());
// Set PID limit
Files.write(Paths.get(cgroupPath + "/pids.max"), "100".getBytes());
}
public void secureContainerFilesystem(String containerId, String rootfs) throws IOException {
// Apply filesystem security measures
// 1. Make rootfs read-only where possible
ProcessBuilder pb = new ProcessBuilder("mount", "-o", "bind,ro",
rootfs, rootfs);
pb.start();
// 2. Setup tmpfs for /tmp and /run
Path tmpPath = Paths.get(rootfs, "tmp");
Files.createDirectories(tmpPath);
pb = new ProcessBuilder("mount", "-t", "tmpfs", "tmpfs", tmpPath.toString());
pb.start();
// 3. Set proper permissions
Files.setPosixFilePermissions(tmpPath, Set.of(
PosixFilePermission.OWNER_READ,
PosixFilePermission.OWNER_WRITE,
PosixFilePermission.OWNER_EXECUTE
));
// 4. Prevent SUID binaries
Process findSuid = Runtime.getRuntime().exec(
new String[]{"find", rootfs, "-type", "f", "-perm", "/4000"});
try (var reader = new java.io.BufferedReader(
new java.io.InputStreamReader(findSuid.getInputStream()))) {
reader.lines().forEach(suidFile -> {
try {
Files.setPosixFilePermissions(Paths.get(suidFile),
Files.getPosixFilePermissions(Paths.get(suidFile))
.stream()
.filter(p -> p != PosixFilePermission.GROUP_EXECUTE)
.filter(p -> p != PosixFilePermission.OTHERS_EXECUTE)
.collect(java.util.stream.Collectors.toSet()));
} catch (IOException e) {
e.printStackTrace();
}
});
}
}
}
6. Spring Boot Integration for Rootless Containers
Spring Boot Configuration:
package com.example.rootless.config;
import com.example.rootless.podman.PodmanRootlessClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RootlessContainerConfig {
@Value("${rootless.podman.path:/usr/bin/podman}")
private String podmanPath;
@Value("${rootless.container.user:${user.name}}")
private String containerUser;
@Bean
public PodmanRootlessClient podmanRootlessClient() {
return new PodmanRootlessClient(podmanPath);
}
@Bean
public RootlessContainerService containerService(PodmanRootlessClient client) {
return new RootlessContainerService(client);
}
}
@Service
public class RootlessContainerService {
private final PodmanRootlessClient podmanClient;
private final Map<String, String> activeContainers = new ConcurrentHashMap<>();
public RootlessContainerService(PodmanRootlessClient podmanClient) {
this.podmanClient = podmanClient;
}
@Async
public CompletableFuture<String> startApplicationContainer(
String image,
String applicationJar,
Map<String, String> env) throws IOException {
// Create volume mapping for application JAR
Map<String, String> options = new HashMap<>();
options.put("volume", applicationJar + ":/app/application.jar");
// Add environment variables
StringBuilder envString = new StringBuilder();
env.forEach((key, value) -> envString.append("-e ").append(key).append("=").append(value).append(" "));
// Run container with Java
List<String> command = Arrays.asList(
"java", "-jar", "/app/application.jar"
);
String containerId = podmanClient.runContainer(image, command, options);
activeContainers.put(containerId, image);
return CompletableFuture.completedFuture(containerId);
}
public List<ContainerInfo> getRunningContainers() throws IOException {
return podmanClient.listContainers(false);
}
@PreDestroy
public void cleanup() {
// Stop all containers on application shutdown
activeContainers.keySet().forEach(containerId -> {
try {
stopContainer(containerId);
} catch (IOException e) {
System.err.println("Failed to stop container: " + containerId);
}
});
}
public void stopContainer(String containerId) throws IOException {
// Implementation using podman stop command
activeContainers.remove(containerId);
}
}
7. Monitoring and Metrics for Rootless Containers
package com.example.rootless.monitoring;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
@Service
public class RootlessContainerMetrics {
private final MeterRegistry meterRegistry;
private final PodmanRootlessClient podmanClient;
private final Map<String, ContainerStats> containerStats = new ConcurrentHashMap<>();
public RootlessContainerMetrics(MeterRegistry meterRegistry,
PodmanRootlessClient podmanClient) {
this.meterRegistry = meterRegistry;
this.podmanClient = podmanClient;
}
@Scheduled(fixedRate = 30000) // Every 30 seconds
public void collectContainerMetrics() throws IOException {
List<ContainerInfo> containers = podmanClient.listContainers(false);
containers.forEach(container -> {
// Collect CPU and memory usage
ContainerStats stats = getContainerStats(container.getId());
containerStats.put(container.getId(), stats);
// Record metrics
meterRegistry.gauge("rootless.container.memory.usage",
stats.getMemoryUsage());
meterRegistry.gauge("rootless.container.cpu.usage",
stats.getCpuUsage());
meterRegistry.counter("rootless.container.network.bytes",
"container", container.getId())
.increment(stats.getNetworkBytes());
});
}
private ContainerStats getContainerStats(String containerId) {
// Use podman stats command to get container statistics
try {
Process process = Runtime.getRuntime().exec(
new String[]{"podman", "stats", "--no-stream", "--format", "json", containerId});
try (var reader = new java.io.BufferedReader(
new java.io.InputStreamReader(process.getInputStream()))) {
String json = reader.lines().collect(java.util.stream.Collectors.joining());
return parseStatsJson(json);
}
} catch (IOException e) {
return new ContainerStats(0, 0, 0);
}
}
private ContainerStats parseStatsJson(String json) {
// Parse podman stats JSON output
// Simplified implementation
return new ContainerStats(
Math.random() * 100, // CPU %
Math.random() * 1024 * 1024 * 100, // Memory bytes
Math.random() * 1024 * 1024 // Network bytes
);
}
public static class ContainerStats {
private final double cpuUsage;
private final long memoryUsage;
private final long networkBytes;
public ContainerStats(double cpuUsage, long memoryUsage, long networkBytes) {
this.cpuUsage = cpuUsage;
this.memoryUsage = memoryUsage;
this.networkBytes = networkBytes;
}
// Getters
public double getCpuUsage() { return cpuUsage; }
public long getMemoryUsage() { return memoryUsage; }
public long getNetworkBytes() { return networkBytes; }
}
}
8. Best Practices for Production
Security Checklist:
public class RootlessSecurityChecklist {
public SecurityReport verifyRootlessSetup() {
SecurityReport report = new SecurityReport();
// 1. Verify user namespace support
if (!hasUserNamespaceSupport()) {
report.addIssue("User namespace not supported or enabled");
}
// 2. Verify subuid/subgid mappings
if (!hasSubuidMapping()) {
report.addIssue("No subuid mapping for current user");
}
// 3. Verify no root privileges
if (hasRootPrivileges()) {
report.addIssue("Process has root privileges");
}
// 4. Verify seccomp support
if (!hasSeccompSupport()) {
report.addWarning("Seccomp not available");
}
// 5. Verify AppArmor/SELinux
if (!hasMandatoryAccessControl()) {
report.addWarning("No mandatory access control enabled");
}
return report;
}
private boolean hasUserNamespaceSupport() {
try {
Process process = Runtime.getRuntime().exec(
"grep user_namespace /proc/self/uid_map");
return process.waitFor() == 0;
} catch (Exception e) {
return false;
}
}
private boolean hasSubuidMapping() {
String username = System.getProperty("user.name");
try {
Process process = Runtime.getRuntime().exec(
"grep " + username + " /etc/subuid");
return process.waitFor() == 0;
} catch (Exception e) {
return false;
}
}
private boolean hasRootPrivileges() {
return System.getProperty("user.name").equals("root") ||
new File("/.dockerenv").exists();
}
public static class SecurityReport {
private final List<String> issues = new ArrayList<>();
private final List<String> warnings = new ArrayList<>();
public void addIssue(String issue) {
issues.add(issue);
}
public void addWarning(String warning) {
warnings.add(warning);
}
public boolean isSecure() {
return issues.isEmpty();
}
}
}
Conclusion
Rootless containers in Java provide a secure way to run containers without root privileges. Key takeaways:
- Use Podman: Best native support for rootless containers
- Leverage User Namespaces: Core technology enabling rootless operation
- Apply Security Profiles: AppArmor, seccomp, and capabilities
- Monitor Resource Usage: Cgroups for resource limits
- Implement Proper Cleanup: Stop containers on application shutdown
This approach significantly reduces the attack surface while maintaining container functionality, making it ideal for multi-tenant environments and security-conscious deployments.
Advanced Java Container Security, Sandboxing & Trusted Runtime Environments
https://macronepal.com/blog/sandboxing-java-applications-implementing-landlock-lsm-for-enhanced-container-security/
Explains using Linux Landlock LSM to sandbox Java applications by restricting file system and resource access without root privileges, improving application-level isolation and reducing attack surface.
https://macronepal.com/blog/gvisor-sandbox-integration-in-java-complete-guide/
Explains integrating gVisor with Java to provide a user-space kernel sandbox that intercepts system calls and isolates applications from the host operating system for stronger security.
https://macronepal.com/blog/selinux-for-java-mandatory-access-control-for-jvm-applications/
Explains how SELinux enforces Mandatory Access Control (MAC) policies on Java applications, strictly limiting what files, processes, and network resources the JVM can access.
https://macronepal.com/java/a-comprehensive-guide-to-intel-sgx-sdk-integration-in-java/
Explains Intel SGX integration in Java, allowing sensitive code and data to run inside secure hardware enclaves that remain protected even if the OS is compromised.
https://macronepal.com/blog/building-a-microvm-runtime-with-aws-firecracker-in-java-a-comprehensive-guide/
Explains using AWS Firecracker microVMs with Java to run workloads in lightweight virtual machines that provide strong isolation with near-container performance efficiency.
https://macronepal.com/blog/enforcing-mandatory-access-control-implementing-apparmor-for-java-applications/
Explains AppArmor security profiles for Java applications, enforcing rules that restrict file access, execution rights, and system-level permissions.
https://macronepal.com/blog/rootless-containers-in-java-secure-container-operations-without-root/
Explains running Java applications in rootless containers using Linux user namespaces so containers operate securely without requiring root privileges.
https://macronepal.com/blog/unlocking-container-security-harnessing-user-namespaces-in-java/
Explains Linux user namespaces, which isolate user and group IDs inside containers to improve privilege separation and enhance container security for Java workloads.
https://macronepal.com/blog/secure-bootstrapping-in-java-comprehensive-trust-establishment-framework/
Explains secure bootstrapping in Java, focusing on how systems establish trust during startup using secure key management, identity verification, and trusted configuration loading.
https://macronepal.com/blog/securing-java-applications-with-chainguard-wolfi-a-comprehensive-guide-2/
Explains using Chainguard/Wolfi minimal container images to secure Java applications by reducing unnecessary packages, minimizing vulnerabilities, and providing a hardened runtime environment.