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.