447 lines
19 KiB
ReStructuredText
447 lines
19 KiB
ReStructuredText
|
.. SPDX-License-Identifier: GPL-2.0
|
||
|
|
||
|
Integrity Policy Enforcement (IPE) - Kernel Documentation
|
||
|
=========================================================
|
||
|
|
||
|
.. NOTE::
|
||
|
|
||
|
This is documentation targeted at developers, instead of administrators.
|
||
|
If you're looking for documentation on the usage of IPE, please see
|
||
|
:doc:`IPE admin guide </admin-guide/LSM/ipe>`.
|
||
|
|
||
|
Historical Motivation
|
||
|
---------------------
|
||
|
|
||
|
The original issue that prompted IPE's implementation was the creation
|
||
|
of a locked-down system. This system would be born-secure, and have
|
||
|
strong integrity guarantees over both the executable code, and specific
|
||
|
*data files* on the system, that were critical to its function. These
|
||
|
specific data files would not be readable unless they passed integrity
|
||
|
policy. A mandatory access control system would be present, and
|
||
|
as a result, xattrs would have to be protected. This lead to a selection
|
||
|
of what would provide the integrity claims. At the time, there were two
|
||
|
main mechanisms considered that could guarantee integrity for the system
|
||
|
with these requirements:
|
||
|
|
||
|
1. IMA + EVM Signatures
|
||
|
2. DM-Verity
|
||
|
|
||
|
Both options were carefully considered, however the choice to use DM-Verity
|
||
|
over IMA+EVM as the *integrity mechanism* in the original use case of IPE
|
||
|
was due to three main reasons:
|
||
|
|
||
|
1. Protection of additional attack vectors:
|
||
|
|
||
|
* With IMA+EVM, without an encryption solution, the system is vulnerable
|
||
|
to offline attack against the aforementioned specific data files.
|
||
|
|
||
|
Unlike executables, read operations (like those on the protected data
|
||
|
files), cannot be enforced to be globally integrity verified. This means
|
||
|
there must be some form of selector to determine whether a read should
|
||
|
enforce the integrity policy, or it should not.
|
||
|
|
||
|
At the time, this was done with mandatory access control labels. An IMA
|
||
|
policy would indicate what labels required integrity verification, which
|
||
|
presented an issue: EVM would protect the label, but if an attacker could
|
||
|
modify filesystem offline, the attacker could wipe all the xattrs -
|
||
|
including the SELinux labels that would be used to determine whether the
|
||
|
file should be subject to integrity policy.
|
||
|
|
||
|
With DM-Verity, as the xattrs are saved as part of the Merkel tree, if
|
||
|
offline mount occurs against the filesystem protected by dm-verity, the
|
||
|
checksum no longer matches and the file fails to be read.
|
||
|
|
||
|
* As userspace binaries are paged in Linux, dm-verity also offers the
|
||
|
additional protection against a hostile block device. In such an attack,
|
||
|
the block device reports the appropriate content for the IMA hash
|
||
|
initially, passing the required integrity check. Then, on the page fault
|
||
|
that accesses the real data, will report the attacker's payload. Since
|
||
|
dm-verity will check the data when the page fault occurs (and the disk
|
||
|
access), this attack is mitigated.
|
||
|
|
||
|
2. Performance:
|
||
|
|
||
|
* dm-verity provides integrity verification on demand as blocks are
|
||
|
read versus requiring the entire file being read into memory for
|
||
|
validation.
|
||
|
|
||
|
3. Simplicity of signing:
|
||
|
|
||
|
* No need for two signatures (IMA, then EVM): one signature covers
|
||
|
an entire block device.
|
||
|
* Signatures can be stored externally to the filesystem metadata.
|
||
|
* The signature supports an x.509-based signing infrastructure.
|
||
|
|
||
|
The next step was to choose a *policy* to enforce the integrity mechanism.
|
||
|
The minimum requirements for the policy were:
|
||
|
|
||
|
1. The policy itself must be integrity verified (preventing trivial
|
||
|
attack against it).
|
||
|
2. The policy itself must be resistant to rollback attacks.
|
||
|
3. The policy enforcement must have a permissive-like mode.
|
||
|
4. The policy must be able to be updated, in its entirety, without
|
||
|
a reboot.
|
||
|
5. Policy updates must be atomic.
|
||
|
6. The policy must support *revocations* of previously authored
|
||
|
components.
|
||
|
7. The policy must be auditable, at any point-of-time.
|
||
|
|
||
|
IMA, as the only integrity policy mechanism at the time, was
|
||
|
considered against these list of requirements, and did not fulfill
|
||
|
all of the minimum requirements. Extending IMA to cover these
|
||
|
requirements was considered, but ultimately discarded for a
|
||
|
two reasons:
|
||
|
|
||
|
1. Regression risk; many of these changes would result in
|
||
|
dramatic code changes to IMA, which is already present in the
|
||
|
kernel, and therefore might impact users.
|
||
|
|
||
|
2. IMA was used in the system for measurement and attestation;
|
||
|
separation of measurement policy from local integrity policy
|
||
|
enforcement was considered favorable.
|
||
|
|
||
|
Due to these reasons, it was decided that a new LSM should be created,
|
||
|
whose responsibility would be only the local integrity policy enforcement.
|
||
|
|
||
|
Role and Scope
|
||
|
--------------
|
||
|
|
||
|
IPE, as its name implies, is fundamentally an integrity policy enforcement
|
||
|
solution; IPE does not mandate how integrity is provided, but instead
|
||
|
leaves that decision to the system administrator to set the security bar,
|
||
|
via the mechanisms that they select that suit their individual needs.
|
||
|
There are several different integrity solutions that provide a different
|
||
|
level of security guarantees; and IPE allows sysadmins to express policy for
|
||
|
theoretically all of them.
|
||
|
|
||
|
IPE does not have an inherent mechanism to ensure integrity on its own.
|
||
|
Instead, there are more effective layers available for building systems that
|
||
|
can guarantee integrity. It's important to note that the mechanism for proving
|
||
|
integrity is independent of the policy for enforcing that integrity claim.
|
||
|
|
||
|
Therefore, IPE was designed around:
|
||
|
|
||
|
1. Easy integrations with integrity providers.
|
||
|
2. Ease of use for platform administrators/sysadmins.
|
||
|
|
||
|
Design Rationale:
|
||
|
-----------------
|
||
|
|
||
|
IPE was designed after evaluating existing integrity policy solutions
|
||
|
in other operating systems and environments. In this survey of other
|
||
|
implementations, there were a few pitfalls identified:
|
||
|
|
||
|
1. Policies were not readable by humans, usually requiring a binary
|
||
|
intermediary format.
|
||
|
2. A single, non-customizable action was implicitly taken as a default.
|
||
|
3. Debugging the policy required manual steps to determine what rule was violated.
|
||
|
4. Authoring a policy required an in-depth knowledge of the larger system,
|
||
|
or operating system.
|
||
|
|
||
|
IPE attempts to avoid all of these pitfalls.
|
||
|
|
||
|
Policy
|
||
|
~~~~~~
|
||
|
|
||
|
Plain Text
|
||
|
^^^^^^^^^^
|
||
|
|
||
|
IPE's policy is plain-text. This introduces slightly larger policy files than
|
||
|
other LSMs, but solves two major problems that occurs with some integrity policy
|
||
|
solutions on other platforms.
|
||
|
|
||
|
The first issue is one of code maintenance and duplication. To author policies,
|
||
|
the policy has to be some form of string representation (be it structured,
|
||
|
through XML, JSON, YAML, etcetera), to allow the policy author to understand
|
||
|
what is being written. In a hypothetical binary policy design, a serializer
|
||
|
is necessary to write the policy from the human readable form, to the binary
|
||
|
form, and a deserializer is needed to interpret the binary form into a data
|
||
|
structure in the kernel.
|
||
|
|
||
|
Eventually, another deserializer will be needed to transform the binary from
|
||
|
back into the human-readable form with as much information preserved. This is because a
|
||
|
user of this access control system will have to keep a lookup table of a checksum
|
||
|
and the original file itself to try to understand what policies have been deployed
|
||
|
on this system and what policies have not. For a single user, this may be alright,
|
||
|
as old policies can be discarded almost immediately after the update takes hold.
|
||
|
For users that manage computer fleets in the thousands, if not hundreds of thousands,
|
||
|
with multiple different operating systems, and multiple different operational needs,
|
||
|
this quickly becomes an issue, as stale policies from years ago may be present,
|
||
|
quickly resulting in the need to recover the policy or fund extensive infrastructure
|
||
|
to track what each policy contains.
|
||
|
|
||
|
With now three separate serializer/deserializers, maintenance becomes costly. If the
|
||
|
policy avoids the binary format, there is only one required serializer: from the
|
||
|
human-readable form to the data structure in kernel, saving on code maintenance,
|
||
|
and retaining operability.
|
||
|
|
||
|
The second issue with a binary format is one of transparency. As IPE controls
|
||
|
access based on the trust of the system's resources, it's policy must also be
|
||
|
trusted to be changed. This is done through signatures, resulting in needing
|
||
|
signing as a process. Signing, as a process, is typically done with a
|
||
|
high security bar, as anything signed can be used to attack integrity
|
||
|
enforcement systems. It is also important that, when signing something, that
|
||
|
the signer is aware of what they are signing. A binary policy can cause
|
||
|
obfuscation of that fact; what signers see is an opaque binary blob. A
|
||
|
plain-text policy, on the other hand, the signers see the actual policy
|
||
|
submitted for signing.
|
||
|
|
||
|
Boot Policy
|
||
|
~~~~~~~~~~~
|
||
|
|
||
|
IPE, if configured appropriately, is able to enforce a policy as soon as a
|
||
|
kernel is booted and usermode starts. That implies some level of storage
|
||
|
of the policy to apply the minute usermode starts. Generally, that storage
|
||
|
can be handled in one of three ways:
|
||
|
|
||
|
1. The policy file(s) live on disk and the kernel loads the policy prior
|
||
|
to an code path that would result in an enforcement decision.
|
||
|
2. The policy file(s) are passed by the bootloader to the kernel, who
|
||
|
parses the policy.
|
||
|
3. There is a policy file that is compiled into the kernel that is
|
||
|
parsed and enforced on initialization.
|
||
|
|
||
|
The first option has problems: the kernel reading files from userspace
|
||
|
is typically discouraged and very uncommon in the kernel.
|
||
|
|
||
|
The second option also has problems: Linux supports a variety of bootloaders
|
||
|
across its entire ecosystem - every bootloader would have to support this
|
||
|
new methodology or there must be an independent source. It would likely
|
||
|
result in more drastic changes to the kernel startup than necessary.
|
||
|
|
||
|
The third option is the best but it's important to be aware that the policy
|
||
|
will take disk space against the kernel it's compiled in. It's important to
|
||
|
keep this policy generalized enough that userspace can load a new, more
|
||
|
complicated policy, but restrictive enough that it will not overauthorize
|
||
|
and cause security issues.
|
||
|
|
||
|
The initramfs provides a way that this bootup path can be established. The
|
||
|
kernel starts with a minimal policy, that trusts the initramfs only. Inside
|
||
|
the initramfs, when the real rootfs is mounted, but not yet transferred to,
|
||
|
it deploys and activates a policy that trusts the new root filesystem.
|
||
|
This prevents overauthorization at any step, and keeps the kernel policy
|
||
|
to a minimal size.
|
||
|
|
||
|
Startup
|
||
|
^^^^^^^
|
||
|
|
||
|
Not every system, however starts with an initramfs, so the startup policy
|
||
|
compiled into the kernel will need some flexibility to express how trust
|
||
|
is established for the next phase of the bootup. To this end, if we just
|
||
|
make the compiled-in policy a full IPE policy, it allows system builders
|
||
|
to express the first stage bootup requirements appropriately.
|
||
|
|
||
|
Updatable, Rebootless Policy
|
||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||
|
|
||
|
As requirements change over time (vulnerabilities are found in previously
|
||
|
trusted applications, keys roll, etcetera). Updating a kernel to change the
|
||
|
meet those security goals is not always a suitable option, as updates are not
|
||
|
always risk-free, and blocking a security update leaves systems vulnerable.
|
||
|
This means IPE requires a policy that can be completely updated (allowing
|
||
|
revocations of existing policy) from a source external to the kernel (allowing
|
||
|
policies to be updated without updating the kernel).
|
||
|
|
||
|
Additionally, since the kernel is stateless between invocations, and reading
|
||
|
policy files off the disk from kernel space is a bad idea(tm), then the
|
||
|
policy updates have to be done rebootlessly.
|
||
|
|
||
|
To allow an update from an external source, it could be potentially malicious,
|
||
|
so this policy needs to have a way to be identified as trusted. This is
|
||
|
done via a signature chained to a trust source in the kernel. Arbitrarily,
|
||
|
this is the ``SYSTEM_TRUSTED_KEYRING``, a keyring that is initially
|
||
|
populated at kernel compile-time, as this matches the expectation that the
|
||
|
author of the compiled-in policy described above is the same entity that can
|
||
|
deploy policy updates.
|
||
|
|
||
|
Anti-Rollback / Anti-Replay
|
||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||
|
|
||
|
Over time, vulnerabilities are found and trusted resources may not be
|
||
|
trusted anymore. IPE's policy has no exception to this. There can be
|
||
|
instances where a mistaken policy author deploys an insecure policy,
|
||
|
before correcting it with a secure policy.
|
||
|
|
||
|
Assuming that as soon as the insecure policy is signed, and an attacker
|
||
|
acquires the insecure policy, IPE needs a way to prevent rollback
|
||
|
from the secure policy update to the insecure policy update.
|
||
|
|
||
|
Initially, IPE's policy can have a policy_version that states the
|
||
|
minimum required version across all policies that can be active on
|
||
|
the system. This will prevent rollback while the system is live.
|
||
|
|
||
|
.. WARNING::
|
||
|
|
||
|
However, since the kernel is stateless across boots, this policy
|
||
|
version will be reset to 0.0.0 on the next boot. System builders
|
||
|
need to be aware of this, and ensure the new secure policies are
|
||
|
deployed ASAP after a boot to ensure that the window of
|
||
|
opportunity is minimal for an attacker to deploy the insecure policy.
|
||
|
|
||
|
Implicit Actions:
|
||
|
~~~~~~~~~~~~~~~~~
|
||
|
|
||
|
The issue of implicit actions only becomes visible when you consider
|
||
|
a mixed level of security bars across multiple operations in a system.
|
||
|
For example, consider a system that has strong integrity guarantees
|
||
|
over both the executable code, and specific *data files* on the system,
|
||
|
that were critical to its function. In this system, three types of policies
|
||
|
are possible:
|
||
|
|
||
|
1. A policy in which failure to match any rules in the policy results
|
||
|
in the action being denied.
|
||
|
2. A policy in which failure to match any rules in the policy results
|
||
|
in the action being allowed.
|
||
|
3. A policy in which the action taken when no rules are matched is
|
||
|
specified by the policy author.
|
||
|
|
||
|
The first option could make a policy like this::
|
||
|
|
||
|
op=EXECUTE integrity_verified=YES action=ALLOW
|
||
|
|
||
|
In the example system, this works well for the executables, as all
|
||
|
executables should have integrity guarantees, without exception. The
|
||
|
issue becomes with the second requirement about specific data files.
|
||
|
This would result in a policy like this (assuming each line is
|
||
|
evaluated in order)::
|
||
|
|
||
|
op=EXECUTE integrity_verified=YES action=ALLOW
|
||
|
|
||
|
op=READ integrity_verified=NO label=critical_t action=DENY
|
||
|
op=READ action=ALLOW
|
||
|
|
||
|
This is somewhat clear if you read the docs, understand the policy
|
||
|
is executed in order and that the default is a denial; however, the
|
||
|
last line effectively changes that default to an ALLOW. This is
|
||
|
required, because in a realistic system, there are some unverified
|
||
|
reads (imagine appending to a log file).
|
||
|
|
||
|
The second option, matching no rules results in an allow, is clearer
|
||
|
for the specific data files::
|
||
|
|
||
|
op=READ integrity_verified=NO label=critical_t action=DENY
|
||
|
|
||
|
And, like the first option, falls short with the execution scenario,
|
||
|
effectively needing to override the default::
|
||
|
|
||
|
op=EXECUTE integrity_verified=YES action=ALLOW
|
||
|
op=EXECUTE action=DENY
|
||
|
|
||
|
op=READ integrity_verified=NO label=critical_t action=DENY
|
||
|
|
||
|
This leaves the third option. Instead of making users be clever
|
||
|
and override the default with an empty rule, force the end-user
|
||
|
to consider what the appropriate default should be for their
|
||
|
scenario and explicitly state it::
|
||
|
|
||
|
DEFAULT op=EXECUTE action=DENY
|
||
|
op=EXECUTE integrity_verified=YES action=ALLOW
|
||
|
|
||
|
DEFAULT op=READ action=ALLOW
|
||
|
op=READ integrity_verified=NO label=critical_t action=DENY
|
||
|
|
||
|
Policy Debugging:
|
||
|
~~~~~~~~~~~~~~~~~
|
||
|
|
||
|
When developing a policy, it is useful to know what line of the policy
|
||
|
is being violated to reduce debugging costs; narrowing the scope of the
|
||
|
investigation to the exact line that resulted in the action. Some integrity
|
||
|
policy systems do not provide this information, instead providing the
|
||
|
information that was used in the evaluation. This then requires a correlation
|
||
|
with the policy to evaluate what went wrong.
|
||
|
|
||
|
Instead, IPE just emits the rule that was matched. This limits the scope
|
||
|
of the investigation to the exact policy line (in the case of a specific
|
||
|
rule), or the section (in the case of a DEFAULT). This decreases iteration
|
||
|
and investigation times when policy failures are observed while evaluating
|
||
|
policies.
|
||
|
|
||
|
IPE's policy engine is also designed in a way that it makes it obvious to
|
||
|
a human of how to investigate a policy failure. Each line is evaluated in
|
||
|
the sequence that is written, so the algorithm is very simple to follow
|
||
|
for humans to recreate the steps and could have caused the failure. In other
|
||
|
surveyed systems, optimizations occur (sorting rules, for instance) when loading
|
||
|
the policy. In those systems, it requires multiple steps to debug, and the
|
||
|
algorithm may not always be clear to the end-user without reading the code first.
|
||
|
|
||
|
Simplified Policy:
|
||
|
~~~~~~~~~~~~~~~~~~
|
||
|
|
||
|
Finally, IPE's policy is designed for sysadmins, not kernel developers. Instead
|
||
|
of covering individual LSM hooks (or syscalls), IPE covers operations. This means
|
||
|
instead of sysadmins needing to know that the syscalls ``mmap``, ``mprotect``,
|
||
|
``execve``, and ``uselib`` must have rules protecting them, they must simple know
|
||
|
that they want to restrict code execution. This limits the amount of bypasses that
|
||
|
could occur due to a lack of knowledge of the underlying system; whereas the
|
||
|
maintainers of IPE, being kernel developers can make the correct choice to determine
|
||
|
whether something maps to these operations, and under what conditions.
|
||
|
|
||
|
Implementation Notes
|
||
|
--------------------
|
||
|
|
||
|
Anonymous Memory
|
||
|
~~~~~~~~~~~~~~~~
|
||
|
|
||
|
Anonymous memory isn't treated any differently from any other access in IPE.
|
||
|
When anonymous memory is mapped with ``+X``, it still comes into the ``file_mmap``
|
||
|
or ``file_mprotect`` hook, but with a ``NULL`` file object. This is submitted to
|
||
|
the evaluation, like any other file. However, all current trust properties will
|
||
|
evaluate to false, as they are all file-based and the operation is not
|
||
|
associated with a file.
|
||
|
|
||
|
.. WARNING::
|
||
|
|
||
|
This also occurs with the ``kernel_load_data`` hook, when the kernel is
|
||
|
loading data from a userspace buffer that is not backed by a file. In this
|
||
|
scenario all current trust properties will also evaluate to false.
|
||
|
|
||
|
Securityfs Interface
|
||
|
~~~~~~~~~~~~~~~~~~~~
|
||
|
|
||
|
The per-policy securityfs tree is somewhat unique. For example, for
|
||
|
a standard securityfs policy tree::
|
||
|
|
||
|
MyPolicy
|
||
|
|- active
|
||
|
|- delete
|
||
|
|- name
|
||
|
|- pkcs7
|
||
|
|- policy
|
||
|
|- update
|
||
|
|- version
|
||
|
|
||
|
The policy is stored in the ``->i_private`` data of the MyPolicy inode.
|
||
|
|
||
|
Tests
|
||
|
-----
|
||
|
|
||
|
IPE has KUnit Tests for the policy parser. Recommended kunitconfig::
|
||
|
|
||
|
CONFIG_KUNIT=y
|
||
|
CONFIG_SECURITY=y
|
||
|
CONFIG_SECURITYFS=y
|
||
|
CONFIG_PKCS7_MESSAGE_PARSER=y
|
||
|
CONFIG_SYSTEM_DATA_VERIFICATION=y
|
||
|
CONFIG_FS_VERITY=y
|
||
|
CONFIG_FS_VERITY_BUILTIN_SIGNATURES=y
|
||
|
CONFIG_BLOCK=y
|
||
|
CONFIG_MD=y
|
||
|
CONFIG_BLK_DEV_DM=y
|
||
|
CONFIG_DM_VERITY=y
|
||
|
CONFIG_DM_VERITY_VERIFY_ROOTHASH_SIG=y
|
||
|
CONFIG_NET=y
|
||
|
CONFIG_AUDIT=y
|
||
|
CONFIG_AUDITSYSCALL=y
|
||
|
CONFIG_BLK_DEV_INITRD=y
|
||
|
|
||
|
CONFIG_SECURITY_IPE=y
|
||
|
CONFIG_IPE_PROP_DM_VERITY=y
|
||
|
CONFIG_IPE_PROP_DM_VERITY_SIGNATURE=y
|
||
|
CONFIG_IPE_PROP_FS_VERITY=y
|
||
|
CONFIG_IPE_PROP_FS_VERITY_BUILTIN_SIG=y
|
||
|
CONFIG_SECURITY_IPE_KUNIT_TEST=y
|
||
|
|
||
|
In addition, IPE has a python based integration
|
||
|
`test suite <https://github.com/microsoft/ipe/tree/test-suite>`_ that
|
||
|
can test both user interfaces and enforcement functionalities.
|