[apparmor] RFC: Policy versioning

John Johansen john.johansen at canonical.com
Sun Dec 10 11:05:53 UTC 2017


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




More information about the AppArmor mailing list