## Changes
This PR is a large-scale refactor of the entire permission-hardene…r script.
Previously, permission-hardener worked by scanning through a set of config files, applying the changes required by those files in "real time" as it scanned through the filesystem. This had a number of problems, mostly with undoing changes. For one, if you had a rule set to restrict the permissions on a particular executable, and then removed that rule, the next `permission-hardener enable` would not "fix" the executable and return it to its original permissions. For two, `permission-hardener disable` was unreliable because different paths pointing at the same file might have different "original" permissions. For instance, if a policy applies to `/usr/bin/sudo` and `/bin/sudo`, permission-hardener would save `/usr/bin/sudo`'s original permissions, and change the permissions to match the policy. Then, when permission-hardener got around to `/bin/sudo`, it would save its "original" permissions again, unintentionally saving the permissions that it applied to `/usr/bin/sudo`. This meant that the same file would end up with two sets of "original" permissions, which of course confused `permission-hardener remove` quite badly.
In order to fix these issues and hopefully dodge any further edge cases that might be lurking, I changed the architecture of permission-hardener entirely.
* There are now four distinct data "areas" that are handled by permission-hardener - the configuration, the policy, the state, and the filesystem state.
* The configuration is defined by the config files under /usr/lib/permission-hardener.d and other config directories.
* The policy is the concrete "this file should be owned by this user and this group and have these permissions" results calculated from the configuration.
* The state is the *original* user, group, and permissions for each file that the policy has ever had an effect on during any run of permission-hardener.
* The filesystem state is the actual user, group, and permissions set on each file.
The basic idea behind the new architecture is to first calculate the policy and state, then apply the policy to the state, then ensure that the filesystem state matches the calculated policy-enhanced state. In this way, if the policy changes so that it no longer modifies the permissions of a file that it used to modify, the original permissions will "show through", and be restored on the next `permission-hardener enable` run. By carefully maintaining the state separately from the policy and ensuring no non-original permissions end up in the state, the policy can change however the user wants it to, and permission-hardener will ensure that the state described by the policy is the state applied to the filesystem. One need not worry that configuration changes will end up "piling up" and resulting in an inconsistent filesystem state.
While the new permission-hardener obviously does not behave identically to the old one (most notably because it cleans up after itself when configured rules are removed), I have been careful to preserve the way in which the configuration is interpreted. This means that if the old permission-hardener would have applied a particular configuration to a "clean" filesystem in a particular way, the new permission-hardener should apply the same configuration to the same filesystem in the same way. I also wrote the script to use mostly the same state format as the original permission-hardener, although I did away with the need for the `private/passwd` and `private/group` files by using Bash regex matching instead of `grep`.
To test that the behavior of the new permission-hardener is correct, I created a Python script named `statall.py` that basically captures a snapshot of the system's current file ownership and permissions state. The script is as follows:
```python
import os
import stat
stat_list=[ '/bin', '/boot', '/etc', '/home', '/lib', '/lib64', '/media', '/mnt', '/opt', '/root', '/sbin', '/srv', '/tmp', '/usr', '/var', '/initrd.img', '/vmlinuz' ]
output_file='/root/statall'
idx=0
with open(output_file, "w") as f:
while idx < len(stat_list):
item = stat_list[idx]
try:
statrslt = os.stat(item)
print("{} |-| {} |-| {} |-| {}".format(item, statrslt[stat.ST_UID], statrslt[stat.ST_GID], stat.filemode(statrslt[stat.ST_MODE])), file=f)
if os.path.isdir(item) and not os.path.islink(item):
subitems=os.listdir(item)
for subitem in subitems:
stat_list.append(item + '/' + subitem)
except:
pass
idx += 1
```
To run it, use `sudo python3 statall.py`. It will save the results to `/root/statall`.
I then took a mostly clean installation of Kicksecure, and ran this script on it, extracting the `/root/statall` file and saving it in a safe location as `kicksecure-statall-old`. With that done, I then did the following sequence of steps to restore the filesystem to an unhardened state, before re-hardening it with the refactored permission-hardener:
```bash
sudo permission-hardener disable all
# /usr/bin/passwd's permissions will be messed up because of the 'multiple old permissions' bug described above, so fix it manually
sudo chmod 4755 /usr/bin/passwd
sudo cp /path/to/new/permission-hardener /usr/bin/
sudo permission-hardener enable
```
(Note to testers, you should probably `sudo safe-rm -rf /var/lib/permission-hardener` before running `sudo permission-hardener enable`, to get rid of potentially messed-up state that the original permission-hardener created. I didn't remember to do this, thus why I don't mention it above, but it should be done and the state files themselves should be audited in some way.)
With that done, I captured another snapshot of the system's permissions state using `statall.py`, and copied the results to a file named `kicksecure-statall-new`. I then compared the files using Meld. Aside from some temp files and files that didn't have anything to do with the permission hardener policy, the resulting snapshots were identical. (I did have quite a few reboots and snapshot restores in between capturing the old and new statall files, since my initial iterations of permission-hardener were buggy.) These are the files produced:
[kicksecure-statall-old.txt](https://github.com/user-attachments/files/18250511/kicksecure-statall-old.txt)
[kicksecure-statall-new.txt](https://github.com/user-attachments/files/18250513/kicksecure-statall-new.txt)
One situations that would be good to test in that I haven't tested yet is this: Undo all of the original permission-hardener's changes, wipe the old permission-hardener state, and then take a statall snapshot. Then, using the new permission-hardener, harden the filesystem with `sudo permission-hardener enable`, and then unharden it with `sudo permission-hardener disable all`. Then take another statall snapshot and compare them. Are they basically identical, or are there worrying discrepancies with files like `/usr/bin/sudo` or `/usr/bin/passwd`? I'll probably test this myself soon.
I've tested this code in a variety of situations and expect it to behave mostly correctly, but due to the scope and scale of the changes made I don't expect it to be perfect. Other than the ever-present risk of bugs, there's probably room for some hardening, and while I tried to optimize the script some, there may be room for speed improvements (which would be welcome since both the old and new scripts are kind of slow).
## Mandatory Checklist
- [x] Legal agreements accepted. By contributing to this organisation, you acknowledge you have read, understood, and agree to be bound by these these agreements:
[Terms of Service](https://www.kicksecure.com/wiki/Terms_of_Service), [Privacy Policy](https://www.kicksecure.com/wiki/Privacy_Policy), [Cookie Policy](https://www.kicksecure.com/wiki/Cookie_Policy), [E-Sign Consent](https://www.kicksecure.com/wiki/E-Sign_Consent), [DMCA](https://www.kicksecure.com/wiki/DMCA), [Imprint](https://www.kicksecure.com/wiki/Imprint)
## Optional Checklist
The following items are optional but might be requested in certain cases.
- [x] I have tested it locally
- [x] I have reviewed and updated any documentation if relevant
- [x] I am providing new code and test(s) for it