Java's Security Manager, part of the platform since JDK 1.0 in 1996, can no longer be enabled at all as of JDK 24, which reached general availability on March 18, 2025. That change shipped under JEP 486, "Permanently Disable the Security Manager" — the follow-through on a warning Oracle issued four years earlier in JEP 411, "Deprecate the Security Manager for Removal," which targeted JDK 17 in September 2021. Any application that still calls System.setSecurityManager() on JDK 24 or later gets a hard error at startup rather than a working sandbox. If your codebase enforces permissions with a custom Policy file, wraps risky code in AccessController.doPrivileged(), or depends on a plugin system that assumed untrusted code could be constrained inside the JVM, that model is no longer available going forward, and the countdown to full API removal in a later release is already running. This guide covers what actually broke, why Oracle pulled the plug, and the concrete alternatives — OS-level isolation, module boundaries, and language-level static analysis — that teams are migrating to instead of permission-based sandboxing.
What exactly did JEP 411 deprecate, and when?
JEP 411 deprecated java.lang.SecurityManager and its supporting APIs — Policy, AccessController, Permission, and related classes — "for removal," a stronger annotation than ordinary deprecation that signals Oracle's committed intent to delete the API, not just discourage its use. It targeted JDK 17, released in September 2021, and JDK 17 itself only added a warning: calling System.setSecurityManager() on the command line or dynamically still worked exactly as before. That changed one release later — starting in JDK 18, the Security Manager was disabled by default and calling System.setSecurityManager() threw UnsupportedOperationException unless an end user explicitly opted back in with -Djava.security.manager=allow. That staged, multi-release rollout was deliberate — JEP 411 stated the goal was to prepare developers for removal in "a future release" while giving teams an opt-in path to keep functioning code running in the interim, not to break anything immediately. The JEP explicitly named client-side sandboxing (applets, Java Web Start) as the original use case, both of which Oracle had already removed from the platform by that point — the Applet API was deprecated in JDK 9 and the closed-source plugin and Java Web Start were dropped from Oracle's JDK 11 in 2018 — making the Security Manager's continued maintenance cost hard to justify for a threat model that no longer existed.
Why did Oracle actually remove it instead of just deprecating it?
Oracle's stated rationale, per JEP 411, was threefold: the Security Manager was rarely used to secure server-side code, its least-privilege model imposed an ongoing maintenance burden because every new JDK API had to be evaluated and often re-audited for correct behavior when the Security Manager was enabled, and the permission model itself was too brittle and too hard to use correctly — granting a single permission like file-read access still left an application blocked on every other operation it needed, and libraries had to lean on AccessController.doPrivileged() to avoid forcing their own permission requirements onto every caller. Oracle also argued the fine-grained, per-permission access-control model was "unwieldy, slow, and falling out of favor across the industry," and that security was better achieved by hardening module boundaries (JEP 403) and isolating the entire JVM process from sensitive resources using out-of-process mechanisms like containers and hypervisors. JEP 486 completed the job in JDK 24 (GA March 18, 2025): rather than deleting the classes outright, it made SecurityManager-related methods degrade gracefully — most either return null/false, silently pass through the caller's request, or throw UnsupportedOperationException — so the small number of libraries that merely call System.getSecurityManager() defensively still run unchanged. Attempting to actually enable a Security Manager, however, now fails hard at launch.
What replaces permission-based sandboxing at the OS level?
Containers and OS-level sandboxing replace the JVM-internal permission model by isolating the entire process rather than individual call stacks. Docker and Kubernetes-managed containers, backed by Linux namespaces and cgroups, restrict what a process can touch at the kernel boundary, which is the isolation layer JEP 411 pointed to explicitly as a Security Manager replacement. For finer-grained control than a container alone provides, seccomp-bpf profiles restrict which system calls a containerized JVM process can invoke — Docker's default seccomp profile blocks roughly 44 of the ~300+ available syscalls, and custom profiles can narrow that further to just what a specific service needs. gVisor and Kata Containers add an extra isolation layer between the container and host kernel for workloads running genuinely untrusted code. None of these give you the fine-grained "this class can open network sockets but not read /etc/passwd" logic Security Manager offered, but they isolate at a boundary the JVM itself can't be tricked into bypassing.
How do the Java Platform Module System and class loaders help?
The Java Platform Module System (JPMS), introduced in JDK 9 under Project Jigsaw, restricts what internal APIs and packages a module can access at compile and load time via module-info.java declarations — exports and opens statements define a hard boundary that a plugin can't reach past regardless of what it tries to call at runtime, unlike the permission checks Security Manager performed dynamically. This doesn't replace runtime resource control (JPMS won't stop a module from opening a file it's allowed to load code for), but it closes off an entire class of "reach into internal JDK state" attacks that previously relied on Security Manager as a backstop. Custom class loaders remain useful for plugin isolation — loading untrusted code in an isolated loader with a restricted classpath limits what classes it can even resolve — though, as with JPMS, this is compile/load-time containment, not the dynamic per-action permission check Security Manager provided at every file read or socket connection.
What should teams do if they were using SecurityManager for plugin isolation?
Teams running plugin architectures — IDEs, build tools, and multi-tenant platforms that historically loaded third-party code in-process and fenced it with a SecurityManager — face the hardest migration path, because no single JVM-native replacement covers the same ground. The practical pattern that's emerged is to move untrusted plugin execution out of the trusted process entirely: run the plugin in a separate, containerized process communicating over a narrow RPC or IPC interface, so the isolation boundary is the OS process/container rather than a JVM permission check. For static, first-party code where the goal was defense-in-depth rather than executing genuinely untrusted third-party code, static analysis tooling that traces data flow between components can catch a meaningful share of what a coarse-grained permission check was meant to prevent — flagging a module that reads a file path from user input and passes it to a sink without validation — before the code ever runs, rather than blocking it at runtime.
How Safeguard helps
Safeguard's SAST engine scans Java codebases as part of its supported language set, tracing untrusted input from a source — a request parameter, a CLI argument, a file read — to a dangerous sink such as file access, deserialization, or command execution, and reporting each finding with its dataflow trace and CWE/OWASP mapping. That doesn't replace the runtime containment a Security Manager or an OS-level sandbox provides, but for teams migrating off SecurityManager-based checks, it closes part of the gap by catching the same classes of unsafe file, network, and process-execution patterns at scan time — before a plugin or module boundary is ever exercised in production — and it does this without requiring a build, so migration audits can run against a source tree directly.