The JVM Security Manager, once a bastion of fine-grained access control, is effectively dead in modern Java versions, but its legacy continues to haunt developers through the need for migration.

Let’s see how it actually worked, and why it’s so tricky to move away from. Imagine a Java application that needs to read a configuration file. In the old days, before Java 17, you’d likely have a SecurityManager set up.

// Old school security policy file (security.policy)
grant {
    // Allows reading any file in the current directory
    permission java.io.FilePermission "file:./*", "read";
};

And you’d start your app like this:

java -Djava.security.policy=security.policy MyApp

Inside MyApp.java:

public class MyApp {
    public static void main(String[] args) {
        System.out.println("Attempting to read config.properties...");
        try {
            java.io.FileInputStream fis = new java.io.FileInputStream("config.properties");
            java.util.Properties props = new java.util.Properties();
            props.load(fis);
            System.out.println("Config loaded: " + props.getProperty("some.key"));
            fis.close();
        } catch (java.io.IOException e) {
            System.err.println("Error reading config: " + e.getMessage());
            e.printStackTrace();
        }
    }
}

When you run this with the security.policy, if config.properties exists in the same directory, it works. If it doesn’t, or if the policy file had a more restrictive path, you’d get a java.security.AccessControlException. This was the core idea: Java code, by default, had no permissions. Permissions were explicitly granted via the SecurityManager and a policy file.

The problem today is that Java 17 and later remove the SecurityManager by default. This means that code that relied on the SecurityManager for its security model, or that was constrained by it, now behaves differently. Applications that previously worked fine might now be able to access resources they shouldn’t, or they might fail in subtle ways if they expected a SecurityManager to be present and throw an exception. The SecurityManager was often used to sandbox untrusted code, like applets or plugins, but also for internal application partitioning.

The most direct migration path, if you absolutely must retain some form of the old security model, is to re-enable the SecurityManager explicitly, but this is generally discouraged for new development and is a temporary stopgap. You can do this by providing a policy file, even if it’s a very permissive one, or by writing a custom SecurityManager subclass.

// Example of a custom SecurityManager that allows everything
public class PermissiveSecurityManager extends SecurityManager {
    @Override
    public void checkPermission(java.security.Permission perm) {
        // No-op: Allow all permissions
    }

    @Override
    public void checkPermission(java.security.Permission perm, Object context) {
        // No-op: Allow all permissions
    }
}

And then starting your application with:

java -Djava.security.manager -Djava.security.policy=some_policy_file.policy MyApp
# OR if using a custom SecurityManager class
java -Djava.security.manager=com.example.PermissiveSecurityManager MyApp

The -Djava.security.manager flag alone, without a policy file, will attempt to load a default policy file (often java.policy in the user’s home directory) and use that. If no policy file is found or specified, and you only use -Djava.security.manager, it usually defaults to a very restrictive policy, blocking most operations and causing AccessControlExceptions.

A common cause of migration pain is that libraries or frameworks you use might still have internal checks that rely on SecurityManager methods like checkRead() or checkWrite(). If the SecurityManager is absent, these methods might throw NullPointerExceptions or behave unexpectedly, rather than the intended AccessControlException. This is because the SecurityManager class itself is still present, but the instance is null.

// Example of a library check that fails without a SecurityManager
public class SomeLibrary {
    public void readFile(String path) {
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            sm.checkRead(path); // This will throw NullPointerException if sm is null
        }
        // ... actual file reading logic ...
    }
}

The fix here is to ensure System.getSecurityManager() never returns null if your code or dependencies expect it. The simplest, albeit insecure, way is to provide a permissive policy file or a permissive custom SecurityManager as shown above. This allows the sm != null check to pass, and then the (permissive) sm.checkRead(path) call will also succeed.

Another frequent issue arises from code that intended to be restricted by a SecurityManager but now runs without one. For instance, an application might have had a policy allowing read access to /tmp/app_data/* but not /etc/*. Without the SecurityManager, it can now read /etc/* as well. The migration here is to implement equivalent access control using other means, such as filesystem permissions, OS-level controls, or by designing your application to explicitly validate paths and user inputs.

If you’re seeing AccessControlException after enabling the SecurityManager (which is the desired behavior if you want to enforce policies), the most common culprit is an incomplete or incorrect security.policy file. You need to ensure every single operation that requires a permission is covered. For example, if your application uses java.net.SocketPermission to connect to a database on db.example.com:5432, your policy must include:

// security.policy
grant {
    // ... other permissions ...
    permission java.net.SocketPermission "db.example.com:5432", "connect,resolve";
};

A very common oversight is forgetting the resolve action for network permissions, which is needed to look up hostnames.

For applications that need to perform operations like exit (which requires java.lang.RuntimePermission "exitVM"), the policy must explicitly grant it. If your application is failing with java.lang.SecurityException: exitVM when you re-enable the SecurityManager, add this to your policy:

// security.policy
grant {
    // ... other permissions ...
    permission java.lang.RuntimePermission "exitVM";
};

The "right" way to migrate, however, is to remove reliance on SecurityManager entirely. This involves auditing your codebase and dependencies for SecurityManager calls. For any legitimate need for restricted execution, consider modern alternatives:

  1. Module System (JPMS): Java 9+ introduced modules. You can use module-info.java to control which packages are exported and which other modules your code depends on. This provides compile-time and runtime encapsulation, preventing unintended access between modules.
  2. Sandboxing Libraries: For truly untrusted code execution, specialized sandboxing libraries or techniques (e.g., using separate OS processes, containers, or JVMs) are more robust than the old SecurityManager.
  3. Configuration and Input Validation: For restricting file access or network connections, implement explicit checks within your application logic. Validate file paths against a whitelist, check user input for malicious patterns, and use secure configuration management practices.
  4. OS-Level Controls: Leverage filesystem permissions, network firewalls, and user/group privileges at the operating system level.

A subtle point often missed is that even when the SecurityManager is not explicitly enabled, the System.getSecurityManager() method can still be called. If code expects it to return non-null and you haven’t set one, you’ll get a NullPointerException. This is why simply removing -Djava.security.manager might break things if libraries perform this check. The most robust migration strategy is to refactor code to not rely on the presence of System.getSecurityManager() for its core logic.

The next hurdle you’ll likely face after migrating away from SecurityManager is ensuring consistent behavior across different JVM implementations or versions, as the absence of the SecurityManager can expose subtle differences in how standard Java APIs are implemented or how they handle edge cases without explicit permission checks.

Want structured learning?

Take the full Jvm course →