Skip to content

Commit bc2a16c

Browse files
committed
first commit
0 parents  commit bc2a16c

4 files changed

Lines changed: 2860 additions & 0 deletions

File tree

README.md

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
# SafeSetID Policy Manager
2+
3+
This repository contains the `safesetid` script — a policy manager for the SafeSetID Linux Security Module (LSM).
4+
5+
The script writes base policies to the SafeSetID sysfs interface, detects newly created users/UIDs, and can restrict them. It features a robust self-healing model (wrapper, hardened backups, `tmpfs` runtime store) to ensure the integrity of the system.
6+
7+
Important: This is a Bash script and requires kernel support for SafeSetID exposed at `/sys/kernel/security/safesetid`. If this directory is missing, no kernel policies will be modified.
8+
9+
## Why Use SafeSetID? A Comparison with `NoNewPrivileges`
10+
11+
Before using this tool, it's helpful to understand what SafeSetID provides compared to other security mechanisms like systemd's `NoNewPrivileges=yes`.
12+
13+
In short:
14+
- `NoNewPrivileges=yes` is a hammer. It's a one-way switch that prevents a process and all of its children from gaining new privileges (e.g., from setuid binaries). It's powerful but coarse.
15+
- SafeSetID is a scalpel. It's a rule-based LSM that lets you define exactly which UID/GID identity transitions are allowed. It doesn’t block all privilege transitions, only those not explicitly on the allow-list.
16+
17+
### Feature Comparison
18+
19+
| Feature | `NoNewPrivileges=yes` (systemd) | SafeSetID (Kernel LSM) |
20+
| :--- | :--- | :--- |
21+
| **Goal** | Prevent any future privilege gains in a process tree. | Control identity transitions by explicitly allow-listing UID/GID changes. |
22+
| **Granularity** | Coarse (global on/off per process tree). | Fine-grained (per-transition rules). |
23+
| **Flexibility** | Low—can break legitimate workflows. | High—allows specific transitions while blocking unexpected ones. |
24+
| **Use Case** | Long-running daemons that never need privilege changes. | Tools or workflows that require specific, controlled transitions. |
25+
| **Operational Complexity** | Easy to enable, but blunt for complex apps. | Requires policy management (which this script automates). |
26+
| **Recovery / Self-Healing** | None. | **Supported by this project** (wrapper, backups, tmpfs, monitors). |
27+
28+
This `safesetid` script acts as a policy manager, making the powerful SafeSetID kernel feature practical and automated.
29+
30+
---
31+
32+
## Current Status and Behavior
33+
34+
- Oneshoot and path-triggered units:
35+
- `safesetid.service` is `Type=oneshot`. It may appear `inactive` after successful execution (`Result=success`).
36+
- `safesetid-monitor.service` and `safesetid-file-monitor.service` are triggered by their respective `.path` units and are typically `static/inactive` until a file change occurs.
37+
- Self-healing:
38+
- Hardened backups are kept in `/var/lib/safesetid` (optionally immutable).
39+
- A runtime trust anchor (`tmpfs` at `/var/tmp/safesetid`) holds authoritative copies; monitors and verify repair deviations.
40+
- Internal commands (protected):
41+
- Internal maintenance commands (prefixed with `_` and `check_function`) cannot be run directly from a terminal and are executed only via the wrapper or systemd.
42+
- DoS mitigation:
43+
- High-frequency events are tracked; a mitigator may be scheduled to reduce load and perform conservative repairs.
44+
- Locks:
45+
- Global locking avoids concurrent modifications; if `flock` is unavailable, the script falls back to best-effort temporary locks.
46+
47+
---
48+
49+
## Installation & Administration
50+
51+
### Install or Update
52+
The installer is idempotent and sets up all components, backups, hashes, and systemd units.
53+
```bash
54+
sudo ./safesetid install_script
55+
```
56+
During installation a 32-bit uninstall token (hex, 8 chars) is generated and stored at `/var/lib/safesetid/.uninstall_token`. The installer logs the token once — store it securely.
57+
58+
### Configure Base Policies
59+
Edit and apply UID/GID policies. Must be invoked via `sudo`/`doas` (not from a direct root shell).
60+
```bash
61+
sudo safesetid configure_uid
62+
sudo safesetid configure_gid
63+
```
64+
65+
### Manual Verification
66+
Run integrity checks and self-healing. Typically triggered automatically by `.path` units.
67+
```bash
68+
sudo safesetid verify
69+
```
70+
71+
### Uninstall (Protected)
72+
Uninstall requires:
73+
1. Invocation via `sudo`/`doas` from a regular user.
74+
2. The uninstall token as the second argument (generated at install, stored in `/var/lib/safesetid/.uninstall_token`).
75+
3. Interactive confirmation.
76+
```bash
77+
sudo safesetid uninstall_script <UNINSTALL_TOKEN>
78+
```
79+
80+
---
81+
82+
## Systemd Units
83+
84+
- `var-tmp-safesetid.mount`: mounts the runtime `tmpfs`.
85+
- `safesetid.service`: oneshot boot-time application of policies.
86+
- `safesetid-wrapper-restore.path` + `.service`: restores the wrapper if changed/deleted.
87+
- `safesetid-monitor.path` + `.service`: watches `/etc/passwd` and runs a UID check.
88+
- `safesetid-file-monitor.path` + `.service`: watches critical script/config files and triggers verification.
89+
90+
Note: Path-triggered services are expected to be `inactive/static` and only run on file events. Oneshoot `safesetid.service` is `inactive` after successful completion.
91+
92+
---
93+
94+
## Smoke Test
95+
96+
A non-destructive smoke test is provided under `Test/smoke_test`. It:
97+
- Runs syntax and ShellCheck.
98+
- Invokes `verify` and validates datastore integrity and wrapper backups.
99+
- Tests lock behavior by running verify and check_function concurrently (check_function is blocked from terminal, as designed).
100+
- Checks all systemd units, treating oneshot/static/path-triggered units correctly.
101+
- Optionally triggers `.path` units via `touch /etc/passwd` and `touch /etc/group` and prints recent journal entries.
102+
103+
Run:
104+
```bash
105+
sudo ./Test/smoke_test
106+
```
107+
108+
---
109+
110+
## Troubleshooting
111+
112+
- View unit status and logs:
113+
```bash
114+
sudo systemctl status safesetid-*.path safesetid-*.service var-tmp-safesetid.mount
115+
sudo journalctl -u safesetid.service -n 50 --no-pager
116+
sudo journalctl -u safesetid-file-monitor.service -n 50 --no-pager
117+
```
118+
- If `/sys/kernel/security/safesetid` is missing, kernel support is not available; the script will skip kernel policy writes.
119+
120+
---
121+
122+
## Security Notes
123+
124+
- This is not a high-security product; it’s a practical policy manager with bonus self-protection.
125+
- Self-healing and immutable backups raise the bar against accidental changes and simple tampering. A local root attacker can still disable protections.
126+
- For stronger guarantees, consider Secure Boot, IMA/EVM, and remote/WORM logging.
127+
128+
---
129+
130+
## License
131+
132+
MIT License

Test/dos_test_flood

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
#!/usr/bin/env bash
2+
# Simple Denial‑of‑Service flood tester for safesetid monitor.
3+
# WARNING: run only on test systems or with care. Do NOT modify /etc/passwd.
4+
# Usage:
5+
# sudo ./dos_test_flood.sh [-d duration_seconds] [-p writers] [-i interval] [--group] [--stop]
6+
#
7+
set -euo pipefail
8+
9+
DURATION=30
10+
PARALLEL=2
11+
INTERVAL=0.2
12+
USE_GROUP=0
13+
PIDFILE="/var/run/dos_test_flood.pids"
14+
FILES=(/etc/safesetid/*)
15+
16+
print_usage() {
17+
cat <<USAGE
18+
dos_test_flood.sh - simple tester to simulate frequent file changes.
19+
20+
Options:
21+
-d <seconds> Duration to run the flood (default: ${DURATION})
22+
-p <writers> Number of parallel writer processes (default: ${PARALLEL})
23+
-i <interval> Interval between writes per writer in seconds (float, default: ${INTERVAL})
24+
--group Also touch /etc/group (use with extreme care)
25+
--stop Stop running flood writers (reads PIDFILE=${PIDFILE})
26+
-h | --help Show this help
27+
USAGE
28+
}
29+
30+
if [ "$#" -gt 0 ]; then
31+
# simple args parser
32+
while [ "$#" -gt 0 ]; do
33+
case "$1" in
34+
-d) DURATION="$2"; shift 2 ;;
35+
-p) PARALLEL="$2"; shift 2 ;;
36+
-i) INTERVAL="$2"; shift 2 ;;
37+
--group) USE_GROUP=1; shift ;;
38+
--stop) ACTION="stop"; shift ;;
39+
-h|--help) print_usage; exit 0 ;;
40+
*) echo "Unknown arg: $1"; print_usage; exit 2 ;;
41+
esac
42+
done
43+
fi
44+
45+
if [ "$(id -u)" -ne 0 ]; then
46+
echo "This script must be run as root (to write /etc/safesetid/*). Use sudo." >&2
47+
exit 1
48+
fi
49+
50+
if [ "${ACTION:-run}" = "stop" ]; then
51+
if [ -f "${PIDFILE}" ]; then
52+
echo "Stopping flood writers (pids in ${PIDFILE})..."
53+
while read -r pid; do
54+
[ -n "$pid" ] || continue
55+
if kill -0 "$pid" 2>/dev/null; then
56+
kill "$pid" 2>/dev/null || true
57+
fi
58+
done < "${PIDFILE}" || true
59+
rm -f "${PIDFILE}" || true
60+
echo "Stopped."
61+
exit 0
62+
else
63+
echo "No PID file (${PIDFILE}) found."
64+
exit 1
65+
fi
66+
fi
67+
68+
if [ "${USE_GROUP}" -eq 1 ]; then
69+
FILES+=(/etc/group)
70+
echo "WARNING: /etc/group will be modified during test. Ensure this is acceptable."
71+
fi
72+
73+
# ensure files exist (create if missing) with safe permissions
74+
for f in "${FILES[@]}"; do
75+
dir=$(dirname "$f")
76+
mkdir -p "$dir" 2>/dev/null || true
77+
touch "$f" 2>/dev/null || true
78+
chown root:root "$f" 2>/dev/null || true
79+
chmod 600 "$f" 2>/dev/null || true
80+
done
81+
82+
# worker function
83+
_writer_main() {
84+
local id="$1"
85+
local interval="$2"
86+
local files=("${!3}")
87+
# loop writing timestamp + id + random to files
88+
while true; do
89+
local t ts rand s
90+
ts="$(date -u +%Y%m%dT%H%M%SZ)"
91+
rand="$(head -c 6 /dev/urandom | od -A n -t x1 2>/dev/null | tr -d ' \n')"
92+
s="${ts} flood-w${id} r=${rand}"
93+
for f in "${files[@]}"; do
94+
# atomic write: write to tmp and move
95+
tmp="$(mktemp "${f}.tmp.XXXXXX" 2>/dev/null || true)"
96+
if [ -n "$tmp" ]; then
97+
printf '%s\n' "$s" > "$tmp" 2>/dev/null || true
98+
chmod 600 "$tmp" 2>/dev/null || true
99+
mv -f "$tmp" "$f" 2>/dev/null || true
100+
else
101+
# fallback append to file (less atomic)
102+
printf '%s\n' "$s" >> "$f" 2>/dev/null || true
103+
fi
104+
done
105+
sleep "$interval"
106+
done
107+
}
108+
109+
# spawn writers
110+
echo "Starting ${PARALLEL} writer(s) for ${DURATION}s, interval=${INTERVAL}s, files=${FILES[*]}"
111+
rm -f "${PIDFILE}" || true
112+
pids=()
113+
for i in $(seq 1 "$PARALLEL"); do
114+
# pass array by name: use indirect expansion
115+
_writer_main "$i" "$INTERVAL" FILES[@] &
116+
p=$!
117+
echo "$p" >> "${PIDFILE}"
118+
pids+=("$p")
119+
echo "spawned writer pid $p"
120+
done
121+
122+
# wait for duration, then stop
123+
SECONDS=0
124+
while [ "$SECONDS" -lt "$DURATION" ]; do
125+
sleep 1
126+
done
127+
128+
echo "Duration expired: killing writers..."
129+
for p in "${pids[@]}"; do
130+
if kill -0 "$p" 2>/dev/null; then
131+
kill "$p" 2>/dev/null || true
132+
sleep 0.5
133+
if kill -0 "$p" 2>/dev/null; then
134+
kill -KILL "$p" 2>/dev/null || true
135+
fi
136+
fi
137+
done
138+
139+
rm -f "${PIDFILE}" || true
140+
echo "Done. Check safesetid logs: journalctl -f -t safesetid -n 200 --no-pager"
141+
exit 0

0 commit comments

Comments
 (0)