How to replace Java Security Manager

The Java SecurityManager, a veteran of the platform since JDK 1.0, has been officially deprecated for removal. Besides, Jakarta EE 11 removes support for running with a SecurityManager. This article explores the reasons behind this decision and the future of application security in Java.

The demise of Java’s SecurityManager

Originally designed as a centralized authority for application security, the SecurityManager offered a way to enforce policies through a series of “check” methods. Developers could define permissions within policy files, allowing granular control over resource access.

When Java was new, the set of APIs was much much smaller than currently is. Therefore, the SecurityManager design made sense because it was easy to pinpoint all the methods that needed a security check.

In particular, the SecurityManager was heavily tied to the Applet API, which was the real threat for as it could potentially access the Client resources. However, with the decline of applets in web development, the focus on the Security Manager itself become less relevant.

Modern Security Approaches Offer More

The Java platform itself has become more intrinsically secure over time, mitigating many vulnerabilities once addressed by the SecurityManager. Configuring the SecurityManager can also be cumbersome. While policy files offer granular control, managing intricate permission structures becomes unwieldy for complex applications. For example, imagine you want to control file access permissions for different user groups within your application using the SecurityManager. Here’s an example of how it can get messy:

Scenario:

  • You have three user groups: Admin, Editor, and Reader.
  • Admins can read and write to any file in the /data directory.
  • Editors can read and write to files within their group folder (/data/editors/). They can also read files in the /data/public folder.
  • Readers can only read files within the /data/public folder.

To achieve this with the SecurityManager, you’d need multiple policy entries in a single file or separate files altogether. This can lead to confusion and potential inconsistencies. Here’s a simplified example:

// Grant Admin full access to /data
grant codeBase "file:/path/to/app/" {
  permission java.io.FilePermission "/data/*", "read,write";
};

// Grant Editors access to their folder and public files (read-write)
grant codeBase "file:/path/to/app/" {
  permission java.io.FilePermission "/data/editors/*", "read,write";
  permission java.io.FilePermission "/data/public", "read,write";
};

// Grant Readers read access to public files
grant codeBase "file:/path/to/app/" {
  permission java.io.FilePermission "/data/public", "read";
};

The Future of Java Application Security

The idea of safely running completely untrusted Java code within the JVM sandbox might be a concept to abandon. There are however several alternatives which you could consider such as the following ones:

Introduce Frameworks for High-Risk Function Calls

If you only need to restrict specific methods or functionalities for untrusted code, consider solutions like Spring AOP or AspectJ. These frameworks allow you to wrap critical code sections and control access based on defined rules.

For example, you can break down the definition of permissions and ACL in the following steps:

Define Permissions: Create an interface or enum to represent different permission types required by your application’s operations. For example:

public interface Permission {
    String getName();
}

public enum AppPermissions implements Permission {
    READ_CONFIG("read.config"),
    WRITE_DATA("write.data"),
    ADMIN_ACCESS("admin.access");
}

Access Control Logic: Develop a service or component responsible for verifying access permissions based on user or role information. This component could leverage a database, user session data, or any other mechanism to determine authorized permissions.

@Service
public class AccessControlService {

    public boolean hasPermission(String username, Permission permission) {
        // Implement logic to retrieve user permissions (replace with actual logic)
        List<String> userRoles = getUserRoles(username);
        for (String role : userRoles) {
            if (role.equals("admin") && permission == AppPermissions.ADMIN_ACCESS) {
                return true;
            }
            // ... Implement logic to check role-based permissions for other operations
        }
        return false;
    }

    // ... (Method to retrieve user roles or other authorization logic)
}

Develop an Aspect Class: Create a Spring AOP Aspect class annotated with @Aspect to intercept method calls and enforce access control. This aspect will leverage the AccessControlService you defined earlier.

@Aspect
@Component
public class SecurityAspect {

    @Autowired
    private AccessControlService accessControlService;

    @Before("@annotation(Secured)")
    public void checkAccess(JoinPoint joinPoint) throws AccessDeniedException {
        Secured annotation = joinPoint.getSignature().getMethod().getAnnotation(Secured.class);
        Permission requiredPermission = annotation.value();

        String username = /* Get username from security context or session */; // Replace with actual logic

        if (!accessControlService.hasPermission(username, requiredPermission)) {
            throw new AccessDeniedException("User does not have permission: " + requiredPermission.getName());
        }
    }
}

Custom Annotation for Secured Methods: Define a custom annotation @Secured that can be placed on methods requiring access control checks:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Secured {
    Permission value();
}

Annotate Methods: Finally, annotate the methods within your application logic that require access control checks with the @Secured annotation, specifying the required permission.

@Service
public class MyService {

    @Secured(AppPermissions.READ_CONFIG)
    public String getConfigData() {
        // ... Logic to read configuration data
    }

    @Secured(AppPermissions.WRITE_DATA)
    public void saveData(String data) {
        // ... Logic to save data
    }
}

By using a framework like Spring AOP, the security logic is encapsulated within the aspect, promoting cleaner code and maintainability. On the other hand, If you don’t have access to the source code of a Java application you need to apply different approaches depending on the deployment environment.

Run in an isolated environment

A valid alternative is to use the sandboxing mechanisms offered by Containers or virtual machines (VMs). These provide a more robust isolation layer compared to SecurityManager. For example, Podman – which natively supports Rootless execution- allows containers to run with minimal privileges, limiting the potential damage caused by vulnerabilities within the application. By limiting privileges, Podman rootless minimizes the potential impact of vulnerabilities within the Java application.

For example, a Rootless Podman execution does require additional File System ( and Selinux permissions) to run a Container which is not using a Root user in the container:

podman run -d --name database \
-e MYSQL_USER=user \
-e MYSQL_PASSWORD=pass \
-e MYSQL_DATABASE=myschema \
-e MYSQL_ROOT_PASSWORD=pass \
-v /home/user/data:/var/lib/mysql mariadb-105
podman ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
dfdc20cf9a7e mariadb-105:latest run-mysqld
29 seconds ago Exited (1) 29 seconds ago db01

You can take Containers Security at the next level by using OpenShift’s Multi-Layered Security:

  • Role-Based Access Control (RBAC): OpenShift allows you to defines granular access permissions, limiting user ability to modify configurations or access sensitive data.
  • Security Context Constraints (SCCs): Enforce security best practices within containers by restricting capabilities, user IDs, and network access.
    • Focus on Non-Root Users: OpenShift enforces running containers with a non-root user (often user 1001) by default. This principle of least privilege minimizes potential damage from vulnerabilities.
  • Image Scanning: Integrates with security scanners to identify vulnerabilities within container images before deployment.

Introduce Operating System native Security

Finally, consider introducing a robust Native Linux Security such as SELinux. SELinux implements Mandatory Access Control (MAC), which enforces security policies at the operating system level by providing additional layers of protection:

  • Process Labeling: SELinux assigns security labels to processes (including Java processes) and files. These labels define what a process can access and what it can do.
  • Type Enforcement: SELinux policies define how processes with specific labels can interact with files and resources with other labels. This restricts unauthorized access attempts.
  • Auditing: SELinux can log security-relevant events, allowing administrators to monitor and investigate potential security issues.

For example, imagine a Java application deployed on a server with SELinux enabled.

  • Scenario: The application needs to read data from a configuration file located at /etc/appconfig.txt.
  • SELinux Policy: The administrator defines a policy that grants the Java process (labeled “java_app”) read access to the file (labeled “app_config_file”). Any attempt by the application to access other files or perform unauthorized actions would be denied by SELinux.

Conclusion

In conclusion, while the SecurityManager has served its time, there are several comprehensive and future-proof approach to securing Java applications. By leveraging containerization, OpenShift or SELinux you can build and deploy secure applications with minimal attack surfaces and automated security best practices.