Currently we have a few problems with policy that must be addressed ------------------------------------------------------------------------ Problems 1. Current policy on new kernels
Policy authored with an older feature set abi will compile and load to the current kernel abi, potentially resulting in denials when a kernel with a newer feature set abi than the policy was designed for is booted. The current solution of policy feature abi pinning has a couple of problems. - it currently only supports a single feature set abi, which can cause its own problems when a user is trying to boot between different kernels. - it is applied globally to allow policy loaded from a userspace, potentially breaking distributed policy, which may have been developed against a different abi. - no distro is currently using it. Which means, all users who run non-distro kernels are being opted into policy dev. It's nice for distro maintainers who want bugs reported so the policy can be improved but not a great experience for users, and fatal for our ability to get new features upstream, as this will be counted as a kernel regression. 2. Split packaging of policy Instead of having a single policy package, policy is being split up, distributed and installed via multiple packages. This leads to policy being developed and maintained against different versions of apparmor, different feature set abis, and even with features that are not supported by the installed apparmor user space. - Currently when these policies are loaded, they will all be loaded under the same feature abi, which can result in the same problems as discussed in 1 for some policies. What is needed is the ability to support multiple feature sets/abis simultaneously. The kernel already does this, the userspace needs to grow support for it. - Policy that makes use of newer features available in new versions of apparmor - In addition the split packaging of policy can lead to the problem where either the application policy must support multiple versions of policy, or hope that the newest version of policy is supported on an older release. Experience has shown this not to be the case and application policy for LXD etc. are having to special case policy for different versions of apparmor. Unfortunately apparmor does not provide anyway to do this within policy so the special casing must be done in the application or packaging. 3. We don't directly support policy in early boot. Currently booting into different kernels means a cache flush and recompile. This is problematic for a few reasons. - It slows down boot. - The compile can not be done in early boot, meaning that either the wrong policy is loaded, or no policy is loaded. For tasks that need to have early policy applied neither solution is acceptable. 4. Support for multiple kernels - currently we don't retain a fallback policy for older kernels if there are problems with a newer kernels, or newer binary policy generated for those kernels. - Booting a new kernel means dealing with all of the above from #3 and #4. The chance for policy errors, causing new kernel failures at boot for kernel devs is unacceptable and could lead to more problems for apparmor upstream. 5. Policy being split across multiple locations. Some distros have split policy across multiple locations. This has resulted in initscripts being modified to handle the split instead of having a defined standard of where policy is. 6. Applications managing their own policy Some applications like LXD and libvirt are doing their own policy management, which has made it difficult to properly manage policy as a set. In particular there was a policy replacement "bug" where unknown policy was being removed from the system on policy replacement. I say "bug" because policy replacement was infact functioning as designed (whether design was correct is a different arguement) and the problem was that these applications were loading policy without informing the system about it. This was "fixed" by removing the ability from the policy management scripts to identify and remove unknown and presumably unused and stale policy, with a new script aa-remove-unknown being added that could be called manually. This was an expedient but less than satisfactory solution that introduced a regression in policy management (manually removing policy files no longer results in the correct removal of policy from the kernel on reload). Nor did it really solve other problems around applications managing policy. 7. Conflicting requirements 1. Currently policy on new kernels and 2. Split packaging of policy have conflicting requirements. With policy being split between different packages and having different versions, it becomes very possible we get into the very similar problems as in 1. To be precise if a user is using newer policy with an older kernel parts of that policy may be downgraded or not enforced. When that user moves to a newer kernel, policy rules that weren't enforced before are enforced under the new kernel, potentially leading to breakage for that user's use case. Unfortunately this is impossible to avoid, as we can't control what policy and kernels a user will use. However we can take some measures to help reduce the risk. ------------------------------------------------------------------------ While Problem #1 is the current looming emergency, Problem #2 is real and becoming more of a problem every day. Problem #3 is more of a nice to have until you need early policy (which we are working towards having better support for). Problem #4 is something we need to solve to safely continue landing new features upstream. And Problems 5 and 6 are fairly easy to address and it's more a matter of making sure we can address them or at least not make them worse as we move forward solving problems 1-4. I. Proposed Solution The basic proposal to address the issues is fairly simple, some of the details are harder. However the work can be split into several phases so we can move forward immediately. 1. We extend policy so that the feature file can be included directly into profiles. something like features=/etc/apparmor/featuresX OR #pragma features=/etc/apparmor/featuresX and the feature set will be made available to policy conditionals (see 6. Handling abstractions below). Hiding the features as a pragma comment would allow this policy change to be transparent to older parsers and tools, but I am unsure if it is worth doing. The feature definition is not the only thing in this proposal that would break on older parsers, so I am leaning towards features= The specified features will be used instead of the kernel's exported feature set abi. If a policy feature abi version is not supported by the kernel it may fail to load (at least for the first pass at this). The compiler will of course still be free to take in the live kernel information and use it in conjunction with the policy abi, to generate rule downgrades or broader support. It just won't use new features that the kernel supports and the policy doesn't. Directly referencing the features file however is somewhat ugly and doesn't deal with abstractions, tunables, or making the abi information human friendly. Instead I propose we wrap the feature file with an include, giving the include a meaningful name. include <version/4.14> include <version/ubuntu-17.10> or maybe include <abi/4.14> and of course instead of using the name for specific kernel or release we can use a simple revision number if we so choose. include <version/11> we could use a new keyword but using an include gives use flexibility. Especially wrt supporting older parsers with new policy. Besides declaring the feature file the include should define a newly standardized variable @{version} OR @{abi} dependent on the specifics of the include above; which can be used by policy to find abi specific includes and potentially make conditional policy decisions (see 6. Handling abstractions). Old policy that does not specify the feature file will fallback to the least of the running kernel abi or the 4.14 abi. This is necessary to try and avoid breaking existing behavior on older kernels and to ensure policy doesn't break newer kernels going forward. It will be easy to inspect what policy versions are supported on the system by listing the version/abi directory ls /etc/apparmor.d/version/ 7 8 9 And if a particular policy version include does not exist on the system the policy will fail to compile. 2. Support multiple policy caches To address problems 3-4 we extend the poliy cache so that we retain a compiled policy per kernel installed. When a new kernel is installed we build a new policy cache for it. Handling this correctly is really important. We need to move away from building policy on boot, as that is already not viable for some policy and will become even more so in the future. Nor does it match well with systemd doing policy loads without having to call out to the compiler. 3. Policy hashing for better cache conistency We need to adopt policy hashing to provide better cache consistency. This is not only so we can fix problems with using file time stamps but also as away to detect inconsistencies with the compiled feature set. With the feature abi becoming an integral part of policy compiles it is critical we detect any changes to the features abi. Previously the cache was cleared when the kernel features abi was changed but that is no longer the case, with multiple caches being retained. However within each of those caches profile abis can change and we need to ensure that the change is picked up. 3. Standardize policy config dir and files Problem 5 is addressed by standardizing a config directory and file layout. New locations must be added to the config dir to inform apparmor of new policy locations and how they should be handled. The parser config has proven insufficient so Ubuntu has been modifying the initscript to manage this which is not a solution that can be shared across distros, nor does it provide a solution that works with other parts of apparmor like the tools. Instead we have a directory in which each new location can drop its own config, allowing to set its policy and include location cache, and even compiler options if so desired. 4. Limit distros ability to compile policy to the current kernels feature abi Along with this Distros will no longer be able to set a default policy compile that will use the current kernel's abi. This will not even be supported at the distro level as the project can not afford to break the feature abi of current policy for kernel developers. To address this a new tool will be added to extract the kernel features abi, and tooling will be updated to allow users update a profiles abi and thus begin development on newer versions. Basically a per user opt in only approach. 5. Applications managing policy and unknown profiles The current solution to problem 6 of having unknown policy and relying on aa-remove-unknown is more problematic. We are going to have to break existing behavior to fix it. Applications that want to manage their own policy are going to have to register to do so. This will require a new API for applications to use which could just be a thin layer on top of the policy config file. Ideally that policy will be placed into a unique policy namespace so that it is easy to identify and control. However we will not be able to enforce this at first as we need to get current applications that are dynamically creating and managing policy to migrate. After this is done we can deprecate the use of aa-remove-unknown. The tool itself can still be useful for developers and people who are manually tinkering with policy so it will likely remain but it shouldn't be needed to manage policy reloads. 6. Handling Abstractions With multiple versions of policy needing to be simultaneously supported, we are going to have improve how the abstractions and tunables are handled. I'd like to keep the change as transparent as possible at the regular policy level. ie. I would rather NOT have to have include <abstractions/4.14/base> instead of the current include <abstractions/base> We can achieve this using conditionals, introducing a few variables and extending the include mechanism to allow for conditional includes. Many of the rules will be able to be shared between different version, only when they can't do we need to fallback to the custom includes and conditionals 6.1 Extending conditionals The current conditional statements are rather limited and will need to be extended to support a broader range of tests. There is an open question as to how much needs to be done, partly dependent on how other features like conditional includes are implemented. 6.1 New variables The @{abi} or @{version} variable that will be defined as part of the abi include can be used by the rest of the includes to selectively include rules that are abi specific eg. the <abstractions/base> abstraction can do an include on <abstractions/@{abi}/base> In addition to the @{abi} variable the parser should make the full feature set available for finer grained decision making. if @{features/network/af_unix} { ... } 6.2 Conditional include The current include fails if the file or dir it references doesn't exist. We need to extend the include mechanism that it can conditionally not fail if the referenced entry does not exist. The syntax needs to be decided on, but some suggestions that have been thrown around in the past are: * Make style with a - at the start of the line, in apparmor's case it would be a special qualifier that for the time being only applies to includes. - include <abstractions/@{abi}> * systemd style with a - just before the include parameter. include -<abstractions/@{abi}> * bash style Uh no, lets just throw my NAK on this one right now * a new keyword include_if_exists <abstractions/@{abi}> * wrapping it in a conditional this requires extending conditions to support an existence test if -e <abstractions/@{abi}> { include <abstractions/@(abi}> } I would like to note that the - is going to be used to indicate set subtraction in future expressions, to aid in righting righter expressions. eg. allow rw /** - {/foo,/bar*}, the space will be required (as - is allowed with in paths today). I just raise this point as something to consider when choosing a format. And to make sure if one of the - formats is chosen it will not conflict or be confused with this use. 7. Dealing with new policy features on older releases. Where possible the parser supports downgrading rules. However this only works for rule types that the parser knows about. To support newer policy features on older releases the best solution is dropping the newest version of apparmor into an older release. However this is not always possible. 7.1 Wrapping rules in conditionals With the feature set being exported as conditionals it becomes possible for policy to wrap new feature rules in conditionals. eg. if @{features/network/af_unix} { unix peer=foo, } While this addresses the need to do special casing in policy packaging, it makes policy harder to read. 7.2 Supporting unknown rule templates Instead of wrapping new rule types in conditionals we should extend policy to support rule templates. Rule templates would allow userspace to specify patterns for unknown rule types, so that the parser or tools can parse the rule, and ignore it. The Rule templates could then be dropped into the abstractions, as new features are added providing an easy way to update older userspaces to ignore new rule types. eg. if !supports(key) { template key='key\w.*,' # yes its overly simple } Such rule templates wouldn't completely remove the need for being able to wrap some policy in conditionals, but it done properly it should be able to support most cases. II. Impact on caching 1. There is a cache per kernel feature abi, which obviously means multiple caching support is needed. Extending the cache to support multiple kernels can use the current propsed design with only minor modifications. The current design for multiple versions of policy cache, creates a hash of the kernel features abi, and creates a cache dir based on the hash for each different kernel features abi, with the full kernel feature set stored in the .features file in the cache file to ensure that hash collisions do not occur. The only change needed is that the policy within the cache is NOT built with kernel features abi, but based on the policy version features as its base. 2. Cache contains multiple features abi. Because different parts of policy may be using different feature abi versions, policy caching will have to support having different versions of policy in cache. This can mostly be done with the current cache design, where different compiles drop the compiled policy into the shared cache, with each file being capable of being built against a different feature abi version. The .features abi file changes meaning from the features abi of the cache to the features abi of the kernel the cache was compiled for. There is no features abi for the cache, nor can we completely reconstruct the features abi from the compiled cache files, thankfully this isn't required for this to work, and adding an extension to the binary format or per cache file shadow file to store this information can be done in the future. 3. Caches between kernels can be shared The cache doesn't need to be per kernel, but per kernel feature abi so if kernels use the same feature abi they will share the same cache. For cases where the kernel feature abis differs, while the kernel abi might cause changes to the compile of a policy, since the cache contents is based on the policy feature abi, the cache will actually be the same for many kernels. And cache files can be shared via a symlink or hardlink. Detecting that the generated policy is the same can be done by a dedup operation, which could be sped up by leveraging the per cache file policy hash that is proposed for consistency checks. 4. Improved cache consistency checks are needed This is another improvement that was already needed, but it becomes more important with multiple caches. Instead of just checking time stamps each cache file will contain a hash extension providing a hash of the policy text used to generate the cache. Hashing the policy text is preferred over just hashing the time stamps of the files involved as it is more robust and will only be moderately slower as the policy text has to be read and fully parsed to properly to determine the include files. 5. Cache fallback if kernel doesn't match It is possible that a precompiled policy will be loadable on a new kernel that is not an exact match. While I do think it is generally best to have a per kernel feature set cache, using a fallback policy for cache is preferable in one case, that of kernel developers. This is because it is possible that a user will be using a policy developed against a newer kernel feature set abi than their current kernel, and when they roll forward to a new kernel that starts enforcing rules in the existing policy, something for that user's use case breaks. Unfortunately I don't see a way to guarentee this won't happen as its impossible to guarentee complete coverage and testing of policy under all use cases. The best we can do is encourage policy be extensively updated before distributing it. The fix for this type of breakage would be having the user modify the broken profiles feature abi or using pinning (as now) to the previous kernel feature abi, but as we have learned this is not considered an acceptable solution. Hence for cases where a new custom kernel is in use we should use a best match* fallback (using closest kernel version and maybe some features) to reduce the chance that we will have a policy break when a new kernel is loaded. * The exact definition of best match still needs to be worked out and will likely have to be based on a distance function. 6. Shipping precompiled policy With the cache supporting multiple kernels, shipping precompiled policy cache files along with the policy text becomes possible. This is largely a packaging issue and the details can be resolved later. III. Impact of the compiler and tools - The compiler will have to be modified to support the feature tagging. - The compiler will need to be able to also accept a kernel features files specification (which it already does), that will only be used for generating the cache, and modifying policy specified abi to be loadable on the given kernel. - For policy that is compiled together it will to order and group profiles based on the specified features abis. - Some changes will be needed to properly support conditionals. - a few new tools will need to be created - current tools will need to be updated to support the language extensions IV. Impact on packaging - It will enable shipping precompiled policy - It will enable shipping config snipets to update policy locations - It will require packaging to be able to cleanup old policy caches that are no longer needed V. Roll Out I've tried to break the work down into phase, partly by priority and partly by dependencies. Work items from later phases can always be moved forward as long as their dependencies are met. Phase 1 - add features keyword support - multiple caches - conditional include Phase 2 - fallback to best match cache for kernels that don't have a cache - requires multiple caches - fallback to using pinning/kernel features (up to 4.14) for when features in not preset in policy. - requires features support/detection within policy - interacts with multiple cache - start reworking includes and policy - introduce @{abi} and abstraction structure to use it - requires features keyword - requires conditional include Phase 3 - improving/extending conditionals - hashing of policy text (improved cache consistency) - intersection of kernel features and policy features to determine abi and rule downgrades. Phase 4 - making features available as conditionals - requires: improving/extending conditionals - standardized config file for locations Phase 5 - policy management api/tools - requires: standardized config file for locations - unknown rule templates -- AppArmor mailing list AppArmor@lists.ubuntu.com Modify settings or unsubscribe at: https://lists.ubuntu.com/mailman/listinfo/apparmor