OSGi Security Manager
Overview
The OSGi Security Manager allows you to secure your applications by utilizing the built in Java Security Manager and Java's permission system. This does not provide security in the sense that a lot of people tend to think of it: authentication, authorization, etc. The Security Manager allows you to assign/revoke permissions to/from code with a policy.
All of the examples and code that I show in this guide will be part of DDF which is running on top of Apache Karaf that is using the Equinox OSGi implementation. Felix does have a beta version of the Security Manager in their implementation, but I ran into many more issues with Felix than Equinox, so I would recommend sticking with Equinox for now.
So for example, you could create a conditional permission that applies to a bundle that allows that bundle to read a single file on the file system and nothing else. This would allow you to protect your application against coding mistakes that could result in a path traversal vulnerability. Coding mistakes in 3rd party code (or our own code!) can be extremely easy to miss, since we generally don't comb over the huge amounts of code that we pull in. We rely on things like the OWASP maven plugin to tell us when a dependency has a CVE. Unfortunately, most open source code projects don't even push out CVEs and the vulnerabilities are worked within bug fixes, tech debt, etc.
Actually using the OSGi Security Manager can be a bit of a headache at first, since there is very little documentation covering its usage outside of the specification itself and the spec. doesn't go into detail on how to actually use the security manager. The first thing that you have to realize is that nothing is going to actually work out of the box. Karaf and some other OSGi based containers will tell you that all you need to do is flip a couple of system properties and "voila" you're now magically secure... Not so much. Following their directions, you'll usually end up in a state where the container is simply looking for signed bundles and if the bundle isn't signed, it won't be loaded. That is great, but it doesn't actually protect your application against the types of coding mistakes that were presented above.
Turning on the Security Manager
If you'd like to use the built in SecurityManager class provided by your OSGi implementation, you can simply turn that on by adding a system property that looks like:
org.osgi.framework.security=osgi
You can also opt to use a custom SecurityManager class by dropping in the standard Java system property instead of using this OSGi specific property. You cannot use both properties at the same time. Adding the Java system property will have the same effect as the above property, except that it will use your custom SecurityManager implmentation, for example:
java.security.manager=net.sourceforge.prograde.sm.ProGradeJSM
You then must add an "all policy" that will allow all of the non-OSGi code to execute with all privileges:
java.security.policy=all.policy
This policy file will look pretty simple inside:
grant {
permission java.security.AllPermission;
};
This just grants the AllPermission to all non-OSGi code. This policy must be in place, because most OSGi implementations literally check for the existence of the AllPermission before even proceeding.
So this turns on the OSGi Security Manager code, but doesn't actually do anything by itself. Nothing is protected yet and permission checks are just going to be checked against an empty permission table, enter: the OSGi Conditional Permission Admin service.
Conditional Permission Admin Service
The ConditionalPermissionAdmin service manages a table of conditions with permissions attached to them. When the OSGi Security Manager encounters a permission check, it will take the information it knows about the context and check that against each of the conditions within this service to determine if it has any sets of permissions that apply to the context.
There first thing to realize is that no Condition implementations actually exist, you must create them yourself. You need to implement the org.osgi.service.condpermadmin.Condition interface and also provide a constructor that looks like:
public MyCondition(Bundle bundle, ConditionInfo conditionInfo) {
...
}
This allows the Security Manager code to instantiate these objects using reflection. Using the Bundle and ConditionInfo that is passed into the constructor, the class can make a determination of whether or not this condition applies to that bundle. DDF comes out of the box with 3 different Condition implementations:
- BundleNameCondition - checks bundles against the / delimited list of bundles in the Condition to see if the bundle matches
- PrincipalCondition - checks whether or not the bundle is running the code as a particular user
- SignerCondition - checks whether or not the bundle was signed by a certain party
The next step is to get your Conditions dropped into the service. For that you'll need to implement the BundleActivator interface to create a class that can load these permissions. This class is going to start up very early in the container and you'll need to stay away from anything that isn't part of OSGi or base Java, because it won't actually be loaded yet. For Karaf, you'll need to edit the startup.properties file and start your bundle with a startup level of 2, right after the features.
DDF utilizes a .policy file to hold all of the permissions using the standard Java syntax for policy files. This part is largely up to however you want to store the permissions. You can also use permissions.perm files within each bundle's OSGI-INF folder to add permissions, however I found this approach to be somewhat buggy and would not produce results that were as consistent as loading conditional permissions into the ConditionalPermissionAdmin service. I would recommend skipping the .perm files for now. Going with the conditional permissions also results in a much more flexible solution if you can load those permissions without recompiling any code.
Once a ConditionalPermissionUpdate has been committed to the service, it will register those permissions with the OSGi security manager and it will begin granting or denying permissions and you might see some permission checks fail in your log with a stack trace. Unfortunately, that stack trace is completely worthless, because it doesn't take into account the protection domains for the bundles, which is really all that matters. There are a couple of options at this point to figure out what bundles need what permissions. You can drop a breakpoint in the AccessControlContext.checkPermission(...) method (this is a base Java class) on the "throw new AccessControlException..." line, or you can use the custom debugger that was developed for DDF: https://github.com/codice/acdebugger (this will most likely get contributed to Codice soon, so watch for an updated binary version that we package with DDF itself).
Using the Custom Access Control Debugger
This debugger will watch for permission failures and print out the bundle that encountered the failure along with the permission you need to add for that bundle. The one thing to keep in mind here is that there could be an entire stack of bundles that require the permission, depending on how and where the permission check is being encountered. For example, a check within something spawned off by Aries Blueprint, will most likely only have a single bundle in the stack, but a check that happens as a result of OSGi wiring might have 10+ bundles that all require the same permission added to them. The custom debugger will only print out the first failure and you'll need to run it multiple times to get all of the bundles. You could also use a debugger from within an IDE, stop execution at the "throw new AccessControlException..." line in AccessControlContext.checkPermission(...) and use tools that the debugger provides to see the state of variables at the point the check failed. This will allow you to see each of the protection domains and the index that failed. You can simply walk up from that index and add the necessary permission for each of the protection domains in the list.
This debugger is still a work in progress and may not print out actions for every type of permission that could be encountered. For those situations, you'll need to rely on a standard debugger to see the exact permission that needs to be added.
Potential False Positives
Occasionally, the acdebugger will log denied permissions which have no apparent effect on the attached DDF. If you encounter such an error, please document it here for further investigation.
Operation (Include steps to reproduce) | Denied Permissions | Classification |
---|---|---|
Attach to running DDF using client script | {org.jline=[java.io.FilePermission "<<ALL FILES>>", "execute"]} | Expected |
$ profile:install standard | Linux (CentOS 7 - varies by distro): {org.eclipse.jetty.util=[java.io.FilePermission "/", "read"]} Windows: {org.apache.cxf.cxf-rt-transports-http=[java.io.FilePermission "\dev\urandom", "read”]} |
User Home Directory
Starting the OSGi container inside user.home
impacts security manager decisions. The OSGi container should not be started inside the user's defined home directory. Karaf also reads from the local Maven repository which is usually in the user's home directory and inherits its permissions. If the Maven repository is moved outside of user.home
then the user.home
property needs to be updated to avoid changes to the policy.
JAVA_TOOL_OPTIONS="-Duser.home=C:\projects"