diff --git a/.cargo/config.toml b/.cargo/config.toml
index fcd04d69..0e5f0739 100644
--- a/.cargo/config.toml
+++ b/.cargo/config.toml
@@ -48,6 +48,10 @@ RAR_CHSR_EDITOR_PATH = "/usr/bin/vim"
RAR_TIMEOUT_TYPE = "ppid"
RAR_TIMEOUT_DURATION = "00:05:00"
RAR_TIMEOUT_MAX_USAGE = ""
+RAR_PASSWORD_TRIES = "3"
+RAR_PASSWORD_DELAY = "00:00:05"
+# Who owns the terminal of a command run as another user: caller, or target like sudo.
+RAR_TTY_OWNER = "caller"
RAR_PATH_DEFAULT = "delete"
RAR_PATH_ADD_LIST = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin"
RAR_PATH_REMOVE_LIST = ""
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index f69055a0..b6475ee0 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -28,8 +28,12 @@ jobs:
session [success=1 default=ignore] pam_permit.so
session requisite pam_permit.so
session required pam_permit.so" | tee /etc/pam.d/dosr'
+ - name: Install Dependencies
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y clang pkg-config libpam0g-dev libseccomp-dev pandoc
- name: Install RootAsRole
- run: cargo xtask install -bip sudo
+ run: cargo xtask install -bp sudo
- name: print config
run: dosr cat /etc/security/rootasrole.json
- name: getenv
diff --git a/.github/workflows/install-tests.yml b/.github/workflows/install-tests.yml
index 3c61e1c3..3700f658 100644
--- a/.github/workflows/install-tests.yml
+++ b/.github/workflows/install-tests.yml
@@ -20,7 +20,7 @@ jobs:
- name: Install git and build dependencies
run: |
apt update -y
- apt install -y git curl build-essential pkg-config libpam0g-dev
+ apt install -y git curl build-essential clang pkg-config libpam0g-dev libseccomp-dev pandoc
apt install -y sudo ca-certificates
- name: Checkout code
@@ -47,7 +47,7 @@ jobs:
- name: Install RootAsRole
run: |
- cargo xtask install --debug -bip sudo
+ cargo xtask install --debug -bp sudo
- name: Verify installation
run: |
@@ -71,8 +71,7 @@ jobs:
dnf config-manager --set-enabled crb || true
dnf install -y epel-release || true
dnf makecache -y
- dnf install -y pandoc || true
- dnf install -y git gcc pkg-config pam-devel
+ dnf install -y git clang pkg-config pam-devel libseccomp-devel pandoc
dnf install -y sudo ca-certificates
- name: Checkout code
@@ -99,7 +98,7 @@ jobs:
- name: Install RootAsRole
run: |
- cargo xtask install -bip sudo
+ cargo xtask install -bp sudo
- name: Verify installation
run: |
@@ -126,7 +125,7 @@ jobs:
- name: Install git and build dependencies
run: |
zypper refresh
- zypper install -y bash rustup git curl gcc pkg-config pam-devel
+ zypper install -y bash rustup git curl clang pkg-config pam-devel libseccomp-devel pandoc
zypper install -y sudo
rustup toolchain install stable
rustup update
@@ -152,7 +151,7 @@ jobs:
- name: Install RootAsRole
run: |
- cargo xtask install -bip sudo
+ cargo xtask install -bp sudo
- name: Verify installation
run: |
diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml
index abf7e534..e4d28b61 100644
--- a/.github/workflows/quality.yml
+++ b/.github/workflows/quality.yml
@@ -40,8 +40,10 @@ jobs:
components: clippy
override: false
- - name: Install RootAsRole
- run: cargo xtask dependencies -dip sudo
+ - name: Install Dependencies
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y pkg-config libpam0g-dev libseccomp-dev pandoc
# if pull request review only
- uses: mbrobbel/rustfmt-check@master
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 6c9f4c46..1a58c727 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -39,7 +39,7 @@ jobs:
session required pam_permit.so" | tee /etc/pam.d/dosr'
- name: Install Dependencies
- run: cargo xtask dependencies -dip sudo
+ run: apt install -y pkg-config libpam0g-dev libseccomp-dev pandoc
- name: run integration coverage
run: cargo +nightly coverage
diff --git a/.gitignore b/.gitignore
index e58a642d..9a00e125 100644
--- a/.gitignore
+++ b/.gitignore
@@ -61,9 +61,10 @@ obj/
bin/
# Hidden folders (for Linux IDE)
-/.idea/
*target/
-*/src/xml_version.rs
+/.*/
+!/.cargo/
+!/.github/
# Rust crates
*/Cargo.lock
diff --git a/Cargo.lock b/Cargo.lock
index 44049680..c8b79c8d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -800,7 +800,7 @@ dependencies = [
[[package]]
name = "rootasrole"
-version = "4.1.0"
+version = "4.2.0"
dependencies = [
"bitflags 2.13.2",
"bon",
@@ -834,7 +834,7 @@ dependencies = [
[[package]]
name = "rootasrole-core"
-version = "4.1.0"
+version = "4.2.0"
dependencies = [
"bitflags 2.13.2",
"bon",
@@ -856,7 +856,7 @@ dependencies = [
[[package]]
name = "rootasrole-exec"
-version = "4.1.0"
+version = "4.2.0"
dependencies = [
"libc",
"log",
@@ -1065,7 +1065,7 @@ checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "xtask"
-version = "4.1.0"
+version = "4.2.0"
dependencies = [
"anyhow",
"capctl",
diff --git a/Cargo.toml b/Cargo.toml
index 2394bacb..a88ff23e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -5,7 +5,7 @@ exclude = ["fuzz", "book", "sudoers-reader"]
[workspace.package]
edition = "2024"
rust-version = "1.93.0"
-version = "4.1.0"
+version = "4.2.0"
license = "LGPL-3.0-or-later"
homepage = "https://lechatp.github.io/RootAsRole/"
repository = "https://github.com/LeChatP/RootAsRole"
@@ -97,8 +97,8 @@ undocumented_unsafe_blocks = { level = "warn", priority = 2 }
allow-unwrap-in-tests = true
[workspace.dependencies]
-rootasrole-core = { version = "4.1.0", path = "core" }
-rootasrole-exec = { version = "4.1.0", path = "exec" }
+rootasrole-core = { version = "4.2.0", path = "core" }
+rootasrole-exec = { version = "4.2.0", path = "exec" }
[dependencies]
rootasrole-core = { workspace = true }
diff --git a/book/src/background/performance.md b/book/src/background/performance.md
index 7bbfed19..7b0e936b 100644
--- a/book/src/background/performance.md
+++ b/book/src/background/performance.md
@@ -4,21 +4,34 @@ Infrastructure-as-code environments can generate large and complex privilege pol
With RaR policies, every role and task can introduce additional access control rules. In large infrastructures, this can result in thousands of policy entries being evaluated during production operations. Therefore, the privilege evaluation engine must scale efficiently.
-The initial implementation of dosr revealed a scalability challenge. The first JSON-based policy engine was faster than sudo for small policies, but its execution time increased significantly as the number of tasks grew.
+> Update for 4.2.0 of the performance evaluation is co-written with Claude for correctness.
-
+## How it is measured
+
+The measures are performed with the dedicated [RaR-perf](https://github.com/LeChatP/RaR-perf) repository. Each tool runs `/usr/bin/true` as root, without a password, with a policy of N rules where only the last rule allows the command. [hyperfine](https://github.com/sharkdp/hyperfine) measures the time of each run. Since a remark from reddit, 4.2.0 evaluations verifies the `/usr/bin/true` performance too for completeness.
+
+The tools do not do the same work for the same rule: sudo and dosr (since 4.0) run the PAM account and session stacks, even when they ask no password, while doas and `please` do not call PAM when they ask no password. A comparison shows how much each design costs.
+
+After some thoughts, run0 is not in the figures. Its rules aren't read at each run: polkitd loads them once, and keep it loaded in memory, while the other tools read their policy each time. Its numbers would not measure the same work. That said, the time to make a dbus call is significantly slower than reading a policy of 10000 tasks.
+Another point is : polkit's run0 rules has no idea about what command is being executed as input. So that makes the comparison not even feasible.
+
+## The first implementation
+
+The initial implementation of the policy engine revealed a scalability challenge. It was measured in April 2025, while 3.1.0 was in development, when dosr was still named `sr`. The first JSON-based policy engine was faster than sudo for small policies, but its execution time increased significantly as the number of tasks grew.
+
+
As shown above, the initial implementation suffered from poor scalability. While it performed well with small policies, the execution time quickly surpassed sudo as policy size increased, making it unsuitable for large-scale deployments.
-## Moving from JSON to CBOR
+### Moving from JSON to CBOR
The first optimization focused on the policy storage format. JSON is human-readable and convenient, but parsing large JSON documents introduces unnecessary overhead. To reduce this cost, dosr introduced support for Compact Binary Object Representation (CBOR), a binary format designed for efficient serialization and parsing, and completely compatible with the JSON structure.
-
+
Although CBOR reduced part of the parsing overhead, the improvement remained limited. The performance curve continued to increase significantly with policy size, demonstrating that file parsing was only one part of the problem. The main bottleneck was located deeper in the policy evaluation architecture.
-## Architectural Optimisation
+### Architectural Optimisation
The next optimization phase focused on the complete execution path of dosr. Instead of only improving the policy format, the internal architecture was redesigned to remove unnecessary overhead.
@@ -31,21 +44,64 @@ The main improvements included:
These changes, combined with the CBOR policy format, resulted in a significant improvement in both execution time and scalability.
-
+
-The optimized dosr engine now outperforms sudo by up to 77% for a single-rule policy. More importantly, its execution time scales linearly and grows approximately 40% slower than sudo, maintaining a performance advantage even with policies containing tens of thousands of tasks.
+In July 2025, just before the release of 3.1.0, this engine ran 77% faster than sudo for a single-rule policy, and its execution time grew approximately 40% slower than sudo's as the policy grew.
Do you see these very straight lines compared to the previous ones? This is because we changed the way to way to plot the data. Instead of using a some arbitrary increase (e.g., 100, 200, 300), we used a logarithmic increase.
+## From 3.1.0 to 4.1.0
+
+These numbers were not measured again for the releases that followed. In October 2026, every version was built again and measured on the same virtual machine, with the same policy:
+
+| version | one rule | each additional rule |
+|---|---|---|
+| 3.1.0 | 5.4 ms | 1.9 µs |
+| 4.0.0 | 13.9 ms | 2.5 µs |
+| 4.0.2 | 14.4 ms | 2.5 µs |
+| 4.1.0 | 11.7 ms | 13.4 µs |
+| 4.2.0 | 9.4 ms | 2.3 µs |
+
+
+
+In this figure, `rar-dev` is 4.2.0, just before its release.
+
+**The time of a single rule grew with PAM.** Since 4.0.0, dosr runs the whole PAM transaction even for a task that skips the authentication: it starts PAM, checks the account, sets the credentials, then opens and closes a session. 3.1.0 did not call PAM at all for such a task. The account check is what refuses an account that has expired, or that may not log in at this hour; sudo runs it too for a `NOPASSWD` rule. This cost is therefore kept, as a security choice. Measured commit by commit, it added about 5 ms to a run. Part of it comes from `pam_unix`, which starts its `unix_chkpwd` helper to read the account, because dosr runs as the user with file capabilities instead of being setuid root. The rest of the difference, about 1.5 ms during 3.2 and 1 ms at the end of 4.0, was not traced to a single commit.
+
+**The cost of each rule grew with patterns.** In 4.1.0, the command paths of the policy that contain wildcards were fixed: they never matched before (commit `1c3084f`). The fix matches them with globset patterns. But it compiled every command path of the policy as a pattern, even a path without any wildcard, which costs about 10 µs for each command of each task. In the same release, other changes made a run with a single rule about 2.7 ms faster.
+
+## RootAsRole 4.2.0
+
+4.2.0 now implement performance indicators unit tests, ignored by default, that print the cost of one task, so that a change can be measured before and after it:
+
+```sh
+cargo test --release --all-features --lib -- --ignored --nocapture --test-threads=1 finder::tests::perf
+```
+
+With 4.2.0, a run with one rule takes 9.4 ms, and each additional rule costs 2.3 µs. More than half of a run is the PAM transaction, which a timing probe measured at about 5.6 ms.
+
+The other tools, measured the same way on the same machine:
+
+
+
+| tool | one rule | 10,000 rules | each additional rule |
+|---|---|---|---|
+| dosr 4.2.0 | 9.4 ms | 30.1 ms | 2.1 µs |
+| sudo 1.9.17 | 10.5 ms | 41.1 ms | 3.0 µs |
+| doas 6.4 | 2.0 ms | 186 ms | 18.4 µs |
+| please 0.5.6 | 5.3 ms | 1,675 ms | 167 µs |
+
+`/usr/bin/true` alone takes 0.7 ms. With one rule, most of the time of dosr and sudo is the PAM transaction, which doas and please programs do not run when they ask no password. As the policy grows, the cost of each rule depends on how the tool reads and compares its rules. The policy of this figure is written by confgen, and the one of the versions by genpol, which is why the numbers of dosr differ slightly between the two.
+
## Large-Scale Automation
This scalability improvement is particularly important in automation environments.
Tools such as Ansible frequently invoke privilege escalation through `become: true` configuration option. With RaR, every role and task can introduce additional access control rules, potentially resulting in thousands of policy entries on a single system.
-The benchmark results show that this additional security granularity does not introduce a proportional performance cost. The optimized dosr engine can reach the execution time of a single sudo rule while evaluating approximately 4,000 RaR rules.
+The benchmark results show that this additional security granularity does not introduce a proportional performance cost. With 4.2.0, a policy of 10,000 tasks adds about 21 ms to a run.
This enables administrators to define significantly more precise privilege policies while maintaining operational efficiency.
@@ -57,5 +113,4 @@ For comparison, **sudo-rs** provides performance comparable to sudo in standard
One potential direction is the integration of a SQLite-based policy backend. The RBAC model of RaR map to relational data structures. A relational database could provide additional advantages as we could do efficient indexing as an example.
-The benchmarks presented above were performed in July 2025. Since then, additional performance optimisations have been implemented across the codebase, improving internal execution paths and reducing overhead further.
- @@billo2025
+The figures of the first engine were measured in 2025, with the scripts of that time. @@billo2025
diff --git a/book/src/cli/chsr.md b/book/src/cli/chsr.md
index 7af0d828..d8992965 100644
--- a/book/src/cli/chsr.md
+++ b/book/src/cli/chsr.md
@@ -1,102 +1,335 @@
# `chsr`
-This section needs rewriting.
+This section is AI-generated and reviewed by human.
-`chsr` is the policy administration tool for RaR.
+`chsr` (change switch role) is the tool that writes the policy that `dosr` reads. It manages roles, tasks, commands, credentials and options, and it refuses the values that `dosr` would not accept, instead of writing them.
-Use it to manage roles, tasks, command rules, credentials, and options.
+The policy is protected: its files belong to root, and when the file system supports it, they are immutable. So `chsr` needs privileges too, and you run it through `dosr`. At the installation, the default policy gives the installer a role for that:
-## Usage
+```bash
+dosr chsr list
+```
-```text
-chsr [COMMAND] [ARGS...]
+
+Whoever can run chsr can give themselves any privilege. The role that allows it is root-equivalent: grant it to as few people as possible, and keep it away from the most exposed accounts with a separation of duty.
+
+
+In the rest of this chapter, we omit `dosr` before `chsr`.
+
+## How a `chsr` command reads
+
+A `chsr` command walks down the policy, like a path in a file system. Each word narrows where the change applies:
+
+```bash
+chsr role ops task reboot options root user
```
-## Main command families
+reads as: in the role `ops`, in its task `reboot`, set the option `root` to `user`. The same command without `task reboot` applies to the whole role, and without `role ops`, to the whole policy.
-- `role`: create/delete roles, grant/revoke actors
-- `role ... task`: create/delete tasks
-- `role ... task ... cmd`: manage command allow/deny rules
-- `role ... task ... cred`: manage `setuid`, `setgid`, and capabilities
-- `options`: manage global/role/task execution options
-- `convert`: convert policy storage between JSON and CBOR
-- `editor`: open interactive editor mode (when enabled)
+Most keywords have a short form. The following commands are the same:
-## `chsr editor`
+```bash
+chsr role ops task reboot options root user
+chsr r ops t reboot o root user
+```
-`chsr editor` opens an interactive policy editing mode.
+| Long form | Short form |
+|-----------|------------|
+| `role` | `r` |
+| `task` | `t` |
+| `options` | `o` |
+| `command` | `cmd` |
+| `credentials` | `cred` |
+| `list`, `show` | `l` |
+| `whitelist`, `blacklist` | `wl`, `bl` |
+| `checklist`, `setlist` | `cl`, `sl` |
+| `add` | `create` |
+| `delete` | `del`, `d`, `rm`, `unset` |
-When the edit session is applied, RaR validates the policy before saving.
-Invalid content is rejected with explicit errors.
+When you are lost, `chsr --help` shows the main commands, and `-h` after an option shows its own help, like `chsr options path -h`. When a command is wrong, `chsr` tells which word it did not expect, and shows the help of that part. The complete grammar is available at .
-Use `chsr editor` for multi-field edits when you want immediate validation.
+## Look before you change
-For policy field reference, see [Configuration File Format](../configuration.md).
-For storage migration, see [File Format Conversion](#file-format-conversion).
+Before modifying anything, look at what is there. Every view is printed in JSON, exactly as it is stored:
-## File Format Conversion
+```bash
+chsr list # the whole policy, file by file
+chsr r caddy-audit l all # a role: its actors, tasks, or all
+chsr r caddy-audit t read-logs l cmd # a task: its cmd, cred, or all
+chsr o l env # an option of the whole policy
+chsr r sys-admin o l timeout # an option of a role
+```
+
+The options that you can show are `all`, `path`, `env`, `root`, `bounding`, `timeout`, `password`, `authentication`, `execinfo`, `umask` and `workdir`.
+
+## Building the WebOps policy
+
+The [Configuration](../configuration.md) chapter writes the WebOps policy in JSON. Here is the same policy, built with `chsr`, one need at a time.
+
+### 1. Safe defaults for the whole policy
+
+The team starts with the options that every role inherits: a fixed `PATH`, an environment that keeps nothing by default, root as a regular user, a strict bounding set, and no disclosure of the policy to users.
+
+```bash
+chsr o path setpolicy delete-all
+chsr o path wl set /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
+chsr o env setpolicy delete-all
+chsr o root user
+chsr o bounding strict
+chsr o execinfo hide
+chsr o auth perform
+```
+
+Most of these values are already the defaults of RaR. Setting them explicitly is a good practice anyway: a reader of the policy does not have to know the defaults of the version that was installed.
+
+### 2. Roles and actors
+
+Each responsibility becomes a role, granted to a group rather than to a person:
+
+```bash
+chsr r webadmin add
+chsr r webadmin grant -g webadmin
+chsr r caddy-audit add
+chsr r caddy-audit grant -g caddy-audit
+chsr r caddy-admin add
+chsr r caddy-admin grant -g caddy-admin
+chsr r sys-admin add
+chsr r sys-admin grant -g sys-admin
+```
+
+`grant -u alice` would grant a user instead. `-g` also accepts a combination of groups, `-g webadmin,staff`: the user must then be a member of all of them. `revoke` takes the same arguments.
+
+### 3. Tasks and their purpose
+
+A task is created inside its role. Give it a purpose: in six months, the purpose is what tells you why this task exists.
+
+```bash
+chsr r webadmin t web-push add
+chsr r webadmin t web-push purpose set Publish website content from the staging area
+```
+
+
+Keep your tasks narrow! One operational action per task is the golden rule.
+
+
+### 4. Commands
+
+The `cmd` of a task is a whitelist of command lines, and a blacklist that takes precedence over it. The command is written after `add`, as you would type it, with its absolute path:
+
+```bash
+chsr r caddy-audit t read-logs cmd wl add /usr/bin/journalctl --no-pager -u caddy
+```
+
+When the arguments are a regular expression, quote the whole command line, so that your shell does not interpret `(`, `|`, `[` or `$`:
+
+```bash
+chsr r caddy-audit t read-logs cmd wl add '/usr/bin/tail ^(-n [0-9]+|-f) /var/log/caddy/[^/ ]+$'
+chsr r caddy-audit t read-logs cmd wl add '/usr/bin/less ^/var/log/caddy/[^/ ]+$'
+```
+
+A regular expression always applies to the whole arguments: `^start|stop$` allows `start` or `stop` alone, not any arguments that start with `start`. A command written without arguments allows no argument. To allow any of them, write it explicitly with `^.*$`. `del` removes a command, `purge` empties the list, and `cmd setpolicy allow-all` or `deny-all` sets what is allowed by default.
-This section needs rewriting.
+Do not forget that setting cmd setpolicy allow-all allows every command of the system to this task. You never know what a user is able to run, and what it can do with it. The default deny-all is the safe choice.
-`chsr convert` converts RootAsRole policy storage between JSON and CBOR.
+### 5. Credentials
-### Supported formats
+The credentials say with which user, groups and capabilities the command runs. `cred set` forces them:
-- `json`
-- `cbor`
+```bash
+chsr r webadmin t web-push cred set --setuid www-data --setgid www-data
+chsr r webadmin t web-push cred caps setpolicy deny-all
+chsr r caddy-audit t read-logs cred set --setgid adm,systemd-journal
+chsr r caddy-audit t read-logs cred caps setpolicy deny-all
+```
-### Command forms
+The capabilities have their own whitelist and blacklist. `caddy-probe` only needs to capture packets:
-```text
-chsr convert [--from ]
-chsr convert -r [--from ]
+```bash
+chsr r caddy-admin t caddy-probe add
+chsr r caddy-admin t caddy-probe cmd wl add /usr/local/bin/caddy-probe
+chsr r caddy-admin t caddy-probe cred set --setgid www-data
+chsr r caddy-admin t caddy-probe cred caps wl add CAP_NET_RAW
```
-- `--from`: explicitly select source format and source file.
-- `-r` / `--reconfigure`: update `/etc/security/rootasrole.json` so storage points to the new file.
+The `CAP_` prefix is optional, and `cred unset` removes what `cred set` wrote. `cred set` forces one user and one set of groups. To let users choose with `dosr -u` or `dosr -g` among several ones, write a [selector](../configuration.md#selector-forms) with `chsr editor`.
-### Common convert examples
+A task that changes the user without `--setgid` gives the command the groups of that user, as a login does. To keep the groups of the person who runs `dosr` instead, say it:
-Convert current policy storage to CBOR and reconfigure runtime:
+```bash
+chsr r ops t backup cred set --setuid backup --setgid-from caller
+```
+
+`--setgid-from target` comes back to the default, and `cred unset --setgid-from target` removes it.
+
+### 6. Options of a role or a task
+
+Options are set at the level where they apply, and inherited below. `less` must not open other files or a shell, and the pagers of systemd must be disabled:
```bash
-chsr convert -r cbor /etc/security/rootasrole.bin
+chsr r caddy-audit t read-logs o env set LESSSECURE=1,SYSTEMD_PAGER=
```
-Convert back to JSON and reconfigure runtime:
+The root-equivalent `sys-admin` role requires a fresh authentication for each command:
```bash
-chsr convert -r json /etc/security/rootasrole.json
+chsr r sys-admin o timeout set --type tty --duration 00:01:00 --max-usage 1
```
-Explicit source/target conversion:
+### 7. Hierarchy and separation of duty
+
+`caddy-admin` inherits the tasks of `caddy-audit`, and `sys-admin` must never be held together with `caddy-admin`:
```bash
-chsr convert --from json /etc/security/rootasrole.json cbor /etc/security/rootasrole.bin
+chsr r caddy-admin parents add caddy-audit
+chsr r sys-admin ssd add caddy-admin
```
-## Operational notes
+Both take a list of roles separated by commas, with `add`, `del`, `set` or `purge`.
+
+### 8. Check the result
+
+```bash
+chsr r caddy-audit l all
+```
+
+```json
+{
+ "name": "caddy-audit",
+ "actors": [
+ {
+ "type": "group",
+ "groups": "caddy-audit"
+ }
+ ],
+ "tasks": [
+ {
+ "name": "read-logs",
+ "options": {
+ "env": {
+ "set": {
+ "LESSSECURE": "1",
+ "SYSTEMD_PAGER": ""
+ }
+ }
+ },
+ "purpose": "Read Caddy access logs and journal",
+ "cred": {
+ "setgid": [
+ "adm",
+ "systemd-journal"
+ ],
+ "capabilities": "none"
+ },
+ "commands": [ … ]
+ }
+ ]
+}
+```
+
+`chsr` writes the long form of the actors, `{"type": "group", …}`, and `"none"` for an empty set of capabilities. `dosr` reads both forms, so you can keep the short ones in the files that you write by hand.
+
+## Options reference
+
+Each option can be set on the whole policy (`chsr o …`), on a role (`chsr r o …`) or on a task (`chsr r t o …`). `unset` (or `del`) removes the value of this level, so that the level above applies again.
+
+| Option | Commands | |
+|--------|----------|---|
+| `path` | `setpolicy delete-all\|keep-safe\|keep-unsafe\|inherit`, `set`, `wl`/`bl` `add\|del\|set\|purge` | The `PATH` of the command. Entries are separated by `:`. |
+| `env` | `setpolicy delete-all\|keep-all\|inherit`, `wl`/`bl`/`cl` `add\|del\|set\|purge`, `keep-only`, `delete-only`, `set`, `sl`, `override allow\|deny\|unset` | The environment of the command. `override` decides whether `dosr -E` is accepted. |
+| `workdir` | `setpolicy all\|none\|inherit`, `set`, `wl`/`bl` | The working directory. |
+| `root` | `user\|privileged\|unset` | Whether uid 0 keeps its special privileges. |
+| `bounding` | `strict\|ignore\|unset` | Whether the capabilities that are not granted are removed from the bounding set. |
+| `authentication` | `perform\|skip\|unset` | Whether the user must authenticate. |
+| `timeout` | `set\|unset` `--type tty\|ppid\|uid` `--duration HH:MM:SS` `--max-usage N` | How long, and how many times, an authentication is reused. |
+| `password` | `set\|unset` `--tries 1-255` `--delay HH:MM:SS` | How many times the password is asked, and the delay after a wrong one. |
+| `execinfo` | `show\|hide\|unset` | Whether `dosr -i` may show the execution context. |
+| `umask` | `022`, `del` | The file creation mask, in octal. |
+
+For the meaning of each value, see [Options and inheritance](../configuration.md#options-and-inheritance).
+
+## Policy directories: `chsr file`
+
+When the policy is a directory, like `/etc/security/rootasrole.d/`, a command edits one file of it: `default.json`, or the only file when there is one. To work on another file of the directory, here `monitoring.json`, name it with `chsr file`, by its path or by its name. `chsr` refuses a file that is not part of the policy. It also refuses, with the reason, a file that it cannot load, like a file written by hand without its `version`: `chsr upgrade --check` lists them all.
+
+```bash
+chsr file list
+chsr file monitoring.json r prometheus add
+chsr f /etc/security/rootasrole.d/monitoring.json o l
+```
+
+`chsr list` shows every file of the policy, with its path. When `chsr` saves, it writes only the files that changed. The files that it creates are readable and writable by their owner only.
+
+## `chsr editor`
+
+`chsr editor` opens a policy file in a text editor, for the changes that are easier to write in JSON, like the selectors:
+
+```bash
+chsr editor # default.json, like the other commands
+chsr editor monitoring.json # another file of the policy
+```
+
+Without a path, it opens the same file as the other commands: `default.json` of a policy directory, or the only file. With a path or a name, it opens that file of the policy, like `chsr file`. To change where the policy is stored, give the path of the root file, `/etc/security/rootasrole.json`.
+
+`chsr` copies the file into a private temporary folder in `/tmp` and starts the editor on the copy. When you quit the editor, `chsr` reads the result and validates it. If it is invalid, `chsr` tells why, and you continue editing or abort. If it is valid, `chsr` shows the resulting configuration and asks for a confirmation before saving it.
+
+The editor is one of the editors chosen when RootAsRole was built (`/usr/bin/vim` by default). The `RAR_EDITOR` variable can select another one, but only among them. `vim` is started without your `vimrc` and without plugins, since they could run anything with the privileges of `chsr`.
-- Keep target files in protected system paths.
-- If policy content changes during migration prep, prefer `chsr editor` to catch invalid policies before write.
-- Validate policy execution after conversion with `dosr -i `.
-- Use version control and backups before large migrations.
+Before starting the editor, `chsr` restricts itself with Landlock: the editor can only reach the policy files, its temporary folder, and the files that it needs to run. On a kernel without Landlock, `chsr` refuses to start the editor.
-For policy field details, see [Configuration File Format](../configuration.md).
+
+Read the resulting configuration before you answer yes: what chsr saves is what it understood, not what you typed.
+
-## Common general examples
+## `chsr upgrade`
+
+A new version of RootAsRole may change how the policy is written. After an upgrade, `chsr upgrade` rewrites the policy files in the format of the installed version. Packages run it for you during their updates.
```bash
-chsr role ops add
-chsr role ops grant -g ops
-chsr role ops task reboot add
-chsr role ops task reboot cmd whitelist add reboot
-chsr role ops task reboot cred caps whitelist add CAP_SYS_BOOT
+chsr upgrade --check
```
-For complete policy fields, see [Configuration File Format](../configuration.md).
+`--check` only shows what would be upgraded, and fails if any file needs it: it is the right command for a script or a monitoring check.
+
+## File Format Conversion
+
+`chsr convert` converts the policy between JSON and CBOR. JSON is easy to read and to review. CBOR is a binary format, much faster to load: when your policy is ready, you can convert it.
+
+```text
+chsr convert [-r] [--from ]
+```
+
+- `` is `json` or `cbor`, and `` is the file to write.
+- `--from` reads another file than the current policy, with its format.
+- `-r` (or `--reconfigure`) also updates `/etc/security/rootasrole.json`, so that `dosr` uses the new file.
+
+Convert the current policy to CBOR, and use it:
+
+```bash
+chsr convert -r cbor /etc/security/rootasrole.bin
+```
+
+Come back to JSON:
+
+```bash
+chsr convert -r json /etc/security/rootasrole.json
+```
+
+Convert a file without changing what `dosr` uses:
+
+```bash
+chsr convert --from json /etc/security/rootasrole.json cbor /etc/security/rootasrole.bin
+```
+
+
+Keep the converted files in a protected system path, owned by root, and make them immutable. Also if you keep a copy on your working directory, do not make it readable by anyone else. Letting those files allows an attacker to discover your own policy.
+
+
+## Operational notes
+- Keep the policy under version control, or at least keep a backup, before large changes.
+- Check what a user can do with `dosr -i `, on a task where `execinfo` is `show`, or read the policy with `chsr list`.
+- After a conversion or an upgrade, run a command of each critical task once.
diff --git a/book/src/cli/dosr.md b/book/src/cli/dosr.md
index 06d3108b..8837e01a 100644
--- a/book/src/cli/dosr.md
+++ b/book/src/cli/dosr.md
@@ -1,32 +1,179 @@
# `dosr`
-`dosr` executes commands through RaR policy checks.
+
+This section is AI-generated and reviewed by human.
+
-## Usage
+`dosr` (do switch role) runs a command with the privileges of one task of the policy, and nothing more. You do not become root: you get the user, the groups and the Linux capabilities that this task gives to this command.
-
-Execute privileged commands with a role-based access control system
+This chapter explains what `dosr` does when you run it, how it chooses a task, and what you can control from the command line. It follows the WebOps team of the [Configuration](../configuration.md) chapter: alice, bob and charly now use `dosr` every day.
-Usage: dosr [OPTIONS] [COMMAND]...
+## Usage
-Arguments:
- [COMMAND]... Command to execute
+
+Usage: dosr [OPTIONS] [--] [COMMAND]...
Options:
- -r, --role <ROLE> Role to select
- -t, --task <TASK> Task to select (--role required)
- -u, --user <USER> User to execute the command as
- -g, --group <GROUP<,GROUP...>> Group(s) to execute the command as
- -E, --preserve-env Keep environment variables from the current process
- -D, --chdir <DIR> Change working directory before executing the command
- -p, --prompt <PROMPT> Prompt to display
- -K Remove timestamp file
- -i, --info Print the execution context of a command if allowed by a matching task
- -h, --help Print help (see more with '--help')
- -V, --version Print version
+ -r, --role <ROLE> Role to use
+ -t, --task <TASK> Task to use in the selected role (--role required)
+ -u, --user <USER> User to execute the command as
+ -g, --group <GROUP>(,<GROUP>...) Group(s) to execute the command as
+ -E, --preserve-env Keep your environment variables, if a matching task allows it
+ -D, --chdir <DIR> Working directory of the command
+ -p, --prompt <PROMPT> Prompt to display instead of "Password: "
+ -S, --stdin Read the password from the standard input
+ -K, --remove-timestamp Remove your timestamp cookies before authentication
+ -i, --info Print the execution context of a command, if a matching task allows it
+ -h, --help Print help
+ -v, --version Print version
-If you're accustomed to utilizing the sudo tool and find it difficult to break that habit, consider creating an alias :
+## What happens when you run `dosr`
+
+Let's follow bob, who wants to read the last lines of the Caddy access log:
+
+```bash
+dosr tail -n 50 /var/log/caddy/access.log
+```
+
+1. **Options first.** `dosr` reads its own options until the first word that is not an option: this is the command. Everything after it belongs to the command, even when it starts with `-`. Here, `-n 50` is given to `tail`, not to `dosr`. Like with sudo, `--` also ends the options of `dosr`.
+2. **Finding the tasks.** `dosr` reads the policy, keeps the roles where bob is an actor, and looks for the tasks whose commands match `tail -n 50 /var/log/caddy/access.log`. `tail` is resolved with the `path` option of the policy, never with the current directory.
+3. **Authentication.** `dosr` asks for bob's password through PAM. It asks it even when no task matches: before bob is authenticated, `dosr` reveals nothing about the policy. Someone who stole bob's account cannot list what the policy allows without his password.
+4. **The decision.** If no task matches, `dosr` answers "You are not allowed to execute this command, this incident will be reported." and logs the attempt.
+5. **The execution context.** `dosr` prepares the user, the groups and the capabilities of the task, filters the environment, sets the working directory and the umask. If the task needs a capability that this session can never obtain, `dosr` stops here and tells which ones.
+6. **The execution.** `dosr` logs the execution, runs the command and waits for it. When the command ends, `dosr` ends the same way.
+
+
+dosr does not run a shell, like sudo does as well. When you are doing dosr echo hello > /etc/file, the redirection is done by your own shell, with your own privileges, before dosr even starts. So, you can use dosr sh -c 'echo hello > /etc/file' to run the redirection with the privileges of the task.
+
+
+## How `dosr` chooses a task
+
+A user may hold several roles, and several tasks may match the same command. In that case, `dosr` selects the most precise and least privileged task.
+
+The precision comes first. It is about how closely the policy matches you and your command: a command line written in full is more precise than a regular expression on the arguments, which is more precise than a task that accepts any arguments. Then, only when two tasks are equally precise, the one with fewer privileges wins: fewer capabilities, no change to root, and so on.
+
+Despite this selection, confusion can still arise. When two tasks are entirely equivalent but configure the command differently (for example with different environment variables), `dosr` cannot decide for you. It answers "Permission denied", and logs a warning saying that the policy must be fixed.
+
+When you know which task you want, say it:
+
+```bash
+dosr -r caddy-audit -t read-logs tail -n 50 /var/log/caddy/access.log
+```
+
+This is the best practice in automation (Ansible, scripts): the selection is then deterministic, and a change in the policy cannot make your script use another task without you noticing.
+
+## Choosing the user and the groups: `-u` and `-g`
+
+By default, the task decides who the command runs as. With `-u` and `-g`, you ask for a specific user or group, but only among those that the task allows (see the [selector forms](../configuration.md#selector-forms)). A task that does not allow your request is not selected:
+
+```bash
+# Only accepted by a task whose setgid selector allows www-data
+dosr -g www-data /usr/local/bin/caddy-probe
+```
+
+`-u` and `-g` do not give you a user: they choose one within the policy.
+
+When the task changes the user and gives no groups, the command gets the groups of that user, its primary group and its supplementary groups, as a login gives them. It never mixes the new user with your own groups, unless the task asks for it with `"from": "caller"` in its `setgid`. A task that keeps your user keeps your groups.
+
+## The environment of the command
+
+The command does not inherit your environment. Only the variables allowed by the `env` option of the task cross the privilege boundary, and the variables that PAM modules set (`pam_env`, `pam_systemd`) go through the same filter. This is what stopped the `APT_CONFIG` hook of the WebOps scenario.
+
+`dosr` then sets a few variables itself:
+
+- `USER`, `LOGNAME`, `HOME` and `SHELL`, of the user the command runs as;
+- `PATH`, from the `path` option of the task;
+- `RAR_USER`, `RAR_UID`, `RAR_GID` and `RAR_COMMAND`, who asked for what;
+- `SUDO_USER`, `SUDO_UID`, `SUDO_GID` and `SUDO_COMMAND`, the same values, for the scripts written for sudo;
+- `TERM`, set to `unknown` if you had none.
+
+With `-E`, you ask to keep your own variables. It is only accepted by a task that allows it, and even then, the `delete` and `check` lists still apply. See [Preserving the environment with `dosr -E`](../configuration.md#preserving-the-environment-with-dosr--e).
+
+## The working directory: `-D`
+
+The command runs in your current directory, entered as the user of the command, or in the directory that the `workdir` option forces. With `-D`, you ask for another directory, among those that the `workdir` option allows.
+
+## Authentication
+
+`dosr` authenticates you through PAM, with the `dosr` service: `/etc/pam.d/dosr`. A task may skip it with the `authentication` option, but think twice: this is the only step that proves the person typing is really the owner of the account.
+
+A wrong password is asked again, after "Sorry, try again.", up to 3 times by default, with a delay of 5 seconds after each failure. The delay makes guessing slow. Any other PAM error stops at once. The `password` option changes both values, at any level of the policy:
+
+```bash
+chsr options password set --tries 3 --delay 00:00:05
+```
+
+`-p` changes the prompt, and `-S` reads the password from the standard input, when there is no terminal. When two `dosr` of the same pipeline need a password, like in `dosr cat a | dosr tee b`, the second one waits until the first is authenticated, and then reuses its cookie when the timeout allows it. They do not fight for your keystrokes anymore.
+
+### Timestamps
+
+Typing your password for every command is tiring, so `dosr` remembers a successful authentication for a while, according to the `timeout` option:
+
+- `type` is the scope of the cookie. `ppid` reuses it for the same parent process (your shell) in the same session, `tty` for the same terminal, and `uid` for any process of your user, which is the widest;
+- `duration` is how long it stays valid;
+- `max_usage` is how many commands it can serve.
+
+For their `sys-admin` role, the WebOps team chose a cookie per terminal, valid for one minute and one command only: this role is root-equivalent, so its authentication must stay fresh.
+
+`dosr -K` removes your cookies, so the next command asks for your password again. Without a command, it only removes them.
+
+
+If you are using cookies for uid, be warned about this feature. If anyone has access to your user account, they can then use your cookie, performing privileged operations without needing your password. This all matters of your own security policy.
+
+
+## While the command runs
+
+When you run `dosr` from a terminal, the command gets its own pseudo-terminal: it cannot push characters into your terminal to make your shell run them. `dosr` forwards the signals it receives to the command.
+
+- **Ctrl+Z** suspends the command like any shell job, and `fg` or `bg` resumes it. Your unsaved work in an editor is not lost.
+- **When `dosr` has to stop the command itself**, it sends SIGHUP and SIGTERM first, then SIGKILL two seconds later. A package manager or a database has time to remove its lock or to finish a write.
+- **The exit status** is the one of the command. If the command was killed by a signal, `dosr` ends with the same signal, so your shell sees the usual 128 + N: 130 for Ctrl+C, 143 for SIGTERM. The errors of `dosr` itself end with status 1.
+
+## Logs
+
+Every execution, every refusal and every authentication failure is written to syslog, with the `auth` facility. An execution looks like this:
+
+```text
+bob : TTY=/dev/pts/0 ; PWD=/home/bob ; ROLE=caddy-audit ; TASK=read-logs ; USER=bob ; GROUPS=adm,systemd-journal ; CAPS=none ; COMMAND=/usr/bin/tail -n 50 /var/log/caddy/access.log
+```
+
+With systemd, you can read them with:
+
+```bash
+journalctl -t dosr
+```
+
+## Checking your rights: `-i`
+
+`-i` shows what `dosr` would do, without running the command:
+
+```text
+$ dosr -i tail -n 50 /var/log/caddy/access.log
+Role: caddy-audit
+Task: read-logs
+Execute as user: Your current user and group(s): adm (4), systemd-journal (999)
+With capabilities: None
+Command: /usr/bin/tail ["-n", "50", "/var/log/caddy/access.log"]
+```
+
+It only works with tasks whose `execinfo` option is `show`. Listing what the policy allows is valuable information for an attacker, so the WebOps team hides it everywhere: for them, `dosr -i` always answers that it is not allowed. An administrator can still read the whole policy with `chsr`.
+
+## Coming from sudo
+
+We kept most of the sudo command line, so that your habits and your scripts keep working. However, a few options differ:
+
+| sudo | dosr | Note |
+|------|------|------|
+| `-u`, `-g`, `-E`, `-D`, `-p`, `-S`, `-K` | same | Only within what the task allows. |
+| `-r` (SELinux role) | `-r` | The RootAsRole role. |
+| `-i` (login shell) | `-i` | Prints the execution context instead. |
+| `-l` | `-i` | For one command, if `execinfo` is `show`. |
+| `-k` | `-K` | Removes the cookies. |
+| `-s`, `-b`, `-n`, `-A`, `sudoedit` | none | Not available. |
+
+If you're accustomed to utilizing the sudo tool and find it difficult to break that habit, consider creating an alias:
+
```sh
alias sudo="dosr"
alias sr="dosr"
@@ -34,13 +181,18 @@ alias sr="dosr"
### Future Design Considerations
-Today, we copied completely the CLI of sudo, thus ensuring old scripts compatibility.
+Today, the CLI of `dosr` follows the one of sudo, to keep old scripts working. We might follow a new design in the future, with retro-compatibility enabled. For example, we could detect when the command name is `sudo` and then use the sudo CLI design, otherwise fall back to the new design.
-We might following a new design in the future but with retro-compatibility enabled. For example, we could detect when the command name is sudo then using the sudo CLI design, otherwise fallback to a new design.
+## When something goes wrong
-## Examples
+`You are not allowed to execute this command, this incident will be reported.` --> No task of your roles matches this command, with these options.
-```bash
-dosr reboot
-dosr -r ops -t reboot reboot
-```
\ No newline at end of file
+`Permission denied` --> Two equivalent tasks match: the policy must be fixed. The log has the details.
+
+`dosr: the task needs capabilities that this session cannot obtain anymore: …` --> The task needs capabilities that are out of the bounding set of your session, for example in a container.
+
+`This incident is reported.` --> The authentication failed.
+
+`You need at least dac_read_search or dac_override, setpcap and setuid capabilities to run dosr` --> `dosr` itself lacks its file capabilities: check the installation.
+
+In every case, the log says more than the terminal, on purpose.
diff --git a/book/src/configuration.md b/book/src/configuration.md
index 0c2136a3..fe3c24a8 100644
--- a/book/src/configuration.md
+++ b/book/src/configuration.md
@@ -88,7 +88,7 @@ The data is split into two parts: the **root file**, which tells RaR where the p
```json
{
- "version": "4.1.0",
+ "version": "4.2.0",
"storage": {
"method": "json",
"settings": {
@@ -109,7 +109,7 @@ For the WebOps team, the policy will be stored in `/etc/security/rootasrole.d/de
```json
{
- "version": "4.1.0",
+ "version": "4.2.0",
"options": { },
"roles": [ ]
}
@@ -230,11 +230,21 @@ Arguments can be matched in two ways:
/usr/bin/install ^-d -m 0?[0-7]{3} /srv/www/[^ ]+$
```
-Regular expressions follow the syntax of the Rust [`regex`](https://docs.rs/regex/latest/regex/#syntax) crate. Note that this syntax does not support look-around nor backreferences.
+Regular expressions follow the syntax of the Rust [`regex`](https://docs.rs/regex/latest/regex/#syntax) crate. Note that this syntax does not support look-around nor backreferences. For a safer implementation of regex, we encapsulated the regex inside a non-capturing group, so the regex looks like `^(?:your_regex_here)$`.
-
-We are still working on the way to express a regular expression safely on the arguments.
-
+Note that when arguments hold special characters, the regex must take into account the quotes needed by the shell to express them. Before matching, `dosr` joins the arguments the way a shell would quote them, with single quotes only:
+
+- an argument that contains a space, a tab, a newline, or one of `'`, `"`, `\`, `|`, `&`, `;`, `<`, `>`, `(`, `)`, `$`, `` ` ``, `*`, `?`, `[`, `#`, `~`, `=` or `%` is wrapped in single quotes;
+- an empty argument becomes `''`;
+- a single quote inside an argument is written `'\''`.
+
+So `ls -l "My Documents"` is matched by `/usr/bin/ls ^-l 'My Documents'$`, and never by `^-l "My Documents"$`. The `=` of an option is the most common trap: bob's `journalctl --unit=caddy --no-pager` is seen as `'--unit=caddy' --no-pager`, so the regex must be `^'--unit=caddy' --no-pager$`. For `ls -l "My Doc'uments"`, the backslash of `'\''` must be escaped in the regex, `^-l 'My Doc'\\''uments'$`, and once more in the JSON file (`\\\\`).
+
+Exact matches are not concerned: the arguments written in the policy are quoted the same way before the comparison.
+
+For example, the following command `ls -l "My Doc'uments"`, must be translated in the policy in a single quote form: `/usr/bin/ls ^-l 'My Doc'\\\\''uments'$`. This amount of escaping is needed because the policy is a JSON file, and the regex is a string inside it. The shell will remove the quotes and the backslashes, and the regex engine will see `My Doc'uments` as expected.
+
+An argument is enclosed in single quotes whenever it contains any of the following characters: space, tab, newline, ‘ " \ | & ; < > ( ) $ ` * ? [ # ~ = %. An empty argument becomes ''. Non-regex arguments does not have this escaping requirement.
### Applying it to WebOps
@@ -277,10 +287,18 @@ The `setuid` field sets the user the command runs as. In security terms, this is
Most administrators want to be a single user: root, because the system's discretionary access control (DAC) is built around it. RaR suggests another approach: when impersonating root is needed, grant it **without capabilities**. Root then only has the rights given by file permissions, like a regular user. Going further, system configuration that administrators need should have a segregated DAC, owned by a dedicated user, so that bypassing it requires an explicit Linux capability.
+One note is : If you don't specify a `setgid`, the command is taking groups from the `setuid` user, aligning with DAC policy. If you want to reduce/edit the groups, you must specify a `setgid` to override the default.
+
+Also, if the user or group in the task does not exist, the task is left out, like a task that does not match: another task may still be chosen. The missing user or group is written in the log.
+
### `setgid`
The `setgid` field makes DAC configuration easier. You can define a single group, or a list of groups. If a list is defined, the first group is the primary group, and the others are supplementary groups.
+Now, let's discuss about subtleties. the `setgid` field can start from the caller groups, or from the `setuid` (target) user groups, with the `from` field of its [selector form](#setgid-1). If you don't specify a `setgid`, the command is taking groups from the target user, aligning with DAC policy. If you want to reduce/edit the groups, you must specify a `setgid` to override the default.
+
+Same as `setuid`, if a group of the task does not exist, the task is left out, like a task that does not match. The missing group is written in the log.
+
### `capabilities`
Linux divides the privileges of the superuser into distinct units called capabilities. Unlike DAC permissions tied to files, capabilities are assigned to processes. They allow granting a specific privilege to a task without running it as root.
@@ -377,6 +395,22 @@ Gives no capability, except `CAP_NET_BIND_SERVICE`. This is a common use case fo
}
```
+`fallback` is the group set used when the caller does not specify `-g`. Without `fallback`, the `from` field says whose groups the command gets:
+
+```json
+"setgid": {
+ "default": "none",
+ "from": "caller"
+}
+```
+
+- `target` (the default): the groups of the user the command runs as, its primary group and the groups that list it as a member, as a login gives them;
+- `caller`: the groups of the user who runs `dosr`.
+
+Without `fallback` nor `from`, the `none` default requires `-g`, and the `all` default gives the groups of the target user. With chsr, `--setgid-from` sets it, for example `chsr role admin task backup credentials set --setuid backup --setgid-from caller`.
+
+Keep `caller` for the tasks that really need it: the command then runs as the target user with every group of the caller (`adm`, `docker`…), which neither of them has alone.
+
`setuid` and `setgid` can also be written in their compact mandatory form (a single user, or a group/group list), as in the WebOps tasks.
## Options and inheritance
@@ -474,6 +508,10 @@ For bob's `read-logs` task, the `set` directive disables the shell escape of `le
Since no `default` is declared at the task level, the global mode and lists are inherited, and these values are added on top.
+#### PAM management of environment variables
+
+In the case where PAM is used to set environment variables through `pam_env` or `pam_env.so`. The `env` option of RaR will still filter these variables according to the policy, ensuring that only allowed variables are passed to the command.
+
### The `path` option
The `PATH` variable is a colon-separated list of directories searched for executables. The `path` option controls it and must be carefully configured to avoid privilege escalation.
@@ -484,9 +522,11 @@ This option works, again, like mathematical sets, with `add` and `sub` (alias `d
- `delete`: start from an empty list and add directories. This is the recommended mode.
- `keep-safe`: start from the caller's `PATH`, and remove relative paths (such as `.` or `bin`). Absolute directories are kept as is, even if they are writable by the caller.
-- `keep-unsafe`: start from the caller's `PATH` as is. Avoid it.
+- `keep-unsafe`: start from the caller's `PATH` as is. Avoid it. We use it only for development purposes.
- `inherit`: use the mode of the level above.
+Some details: in `delete` mode, the caller's `PATH` is not used at all. A relative directory is the working directory of the command, which the caller chooses, so except for `keep-unsafe`, relative directories are removed, from the caller's `PATH` as from the `add` lists of the policy (the ones of the policy are written in the log). The directories of the policy come first, then the ones of the caller; the `sub` list is removed, and each directory appears once. Whether a directory exists is not checked. If the resulting list is empty, then the PATH variable is not set at all, meaning that the user must type the full path of the command to run it.
+
For WebOps, the global level only allows system directories, including `/usr/local/bin` where `caddy-probe` lives:
```json
@@ -503,17 +543,21 @@ These directories must only be writable by root; otherwise, anyone able to write
| Option | Values | Description |
|------------------|---------------------------------|-------------------------------------------------------------------------------------------------------------------|
| `authentication` | `perform` \| `skip` | Whether the user must authenticate before running the command. |
-| `timeout` | `type`, `duration`, `max_usage` | How long, and how many times, an authentication remains valid, and its scope (`ppid`, `tty` or `uid`). |
+| `timeout` | `type`, `duration` (hh:mm:ss format), `max_usage` | How long, and how many times, an authentication remains valid, and its scope (`ppid`, `tty` or `uid`). |
| `root` | `privileged` \| `user` | `user` sets the `SECBIT_*` securebits that disable the special privileges of uid 0: root becomes a regular user, and gets only the capabilities granted by the task. `privileged` keeps the legacy root behavior. |
| `bounding` | `strict` \| `ignore` | `strict` enforces the granted capabilities on the bounding set: the command and its children can never regain any other capability. |
| `execinfo` | `show` \| `hide` | Whether `dosr --info` may disclose the existence of the matching policy element to the user. |
| `workdir` | path \| selector | Working directory of the command, or the set of directories the user may choose. |
| `umask` | string | File creation mask of the command. |
+| `password` | `tries`, `delay` (hh:mm:ss format) | Number of attempts and delay between them for password authentication. |
+| `tty` | `owner`: `caller` \| `target` | Who owns the terminal of a command run as another user, when `dosr` runs from a terminal: the caller (the default) or the target user. Generally, the `caller` option is safer, but sudo uses the `target` option behavior. The security of this option depends on the context of use. |
The `root` option deserves attention. On Linux, a process running as uid 0 that executes a binary automatically obtains all capabilities. Without `"root": "user"`, a task with `"setuid": "root"` and `"capabilities": []` would get them back as soon as the command runs another program. This is what makes the WebOps `caddy-service` and `reboot` tasks really unprivileged, so the team sets `root` to `user` and `bounding` to `strict` globally.
`execinfo` also has a security impact: listing what the policy allows is valuable information for an attacker who stole an account. The team sets it to `hide` globally.
+`tty` is written `"tty": { "owner": "target" }`. With `caller`, the user the command runs as cannot reopen its terminal by its name: a program that does, like pinentry through `GPG_TTY`, fails. With `target`, the other processes of that user can open the terminal while the command runs, and read what is typed into it. The default comes from the build (`RAR_TTY_OWNER`, see [Build](install/build.md)).
+
For `sys-admin`, the most critical role, the team requires authentication on every use, with a short and non-reusable timeout:
```json
@@ -535,6 +579,15 @@ This utility addresses differences in pattern syntax and list inheritance:
Tasks still using unmigrated regular expressions are rejected by `dosr` at runtime until the upgrade has been completed.
+### Migrating configurations from version 4.1
+
+No migration is needed from version 4.1 to 4.2.0. However, downgrading from 4.2.0 to 4.1:
+
+* removes the new `password` and `tty` options, and the `from` field of `setgid`;
+* writes a regular expression with a top-level alternation, like `^start|stop$`, as `^(?:start|stop)$`, so that 4.1 does not allow more.
+
+Note that 4.1 gives the caller's groups to a task that changes the user, whatever the policy says.
+
## Hierarchy and separation of duty
Hierarchy and separation of duty (SoD) are designed as plugins on top of the core RBAC-0 model. They are not part of the core model, but they are implemented in the RaR project.
@@ -542,6 +595,10 @@ Hierarchy and separation of duty (SoD) are designed as plugins on top of the cor
- The **hierarchy** plugin lets a role declare its parent roles, through the `parents` field. The role inherits the tasks of its parents.
- The **static separation of duty** plugin declares conflicting roles, through the `ssd` field: a user should never be an actor of two conflicting roles.
+When the lookup of an actor fails, for example a directory service that does not answer, `dosr` cannot know whether the user holds the conflicting role: it then disables every role with an `ssd` field in that policy file, and writes the failure in the log.
+
+Note that A role that is in conflict with one of its parents is invalid, because it would inherit the parent's tasks and thus be in conflict with itself.
+
### Applying it to WebOps
Bob operates Caddy, so he also needs to read its logs. Instead of duplicating the `read-logs` task, `caddy-admin` declares `caddy-audit` as its parent, and inherits its tasks:
@@ -605,7 +662,7 @@ Here is the resulting `/etc/security/rootasrole.d/default.json`:
```json
{
- "version": "4.1.0",
+ "version": "4.2.0",
"options": {
"path": {
"default": "delete",
diff --git a/book/src/install/build.md b/book/src/install/build.md
index c61f773a..2ba6e913 100644
--- a/book/src/install/build.md
+++ b/book/src/install/build.md
@@ -18,13 +18,17 @@ RAR_CHSR_EDITOR_PATH = "/usr/bin/vim"
RAR_TIMEOUT_TYPE = "ppid"
RAR_TIMEOUT_DURATION = "00:05:00"
RAR_TIMEOUT_MAX_USAGE = ""
+RAR_PASSWORD_TRIES = "3"
+RAR_PASSWORD_DELAY = "00:00:05"
+# Who owns the terminal of a command run as another user: caller, or target like sudo.
+RAR_TTY_OWNER = "caller"
RAR_PATH_DEFAULT = "delete"
RAR_PATH_ADD_LIST = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin"
RAR_PATH_REMOVE_LIST = ""
RAR_ENV_DEFAULT = "delete"
-RAR_ENV_KEEP_LIST = "HOME,USER,LOGNAME,COLORS,DISPLAY,HOSTNAME,KRB5CCNAME,LS_COLORS,PS1,PS2,XAUTHORY,XAUTHORIZATION,XDG_CURRENT_DESKTOP"
-RAR_ENV_CHECK_LIST = "COLORTERM,LANG,LANGUAGE,LC_.*,LINGUAS,TERM,TZ"
-RAR_ENV_DELETE_LIST = "PS4,SHELLOPTS,PERLLIB,PERL5LIB,PERL5OPT,PYTHONINSPECT"
+RAR_ENV_KEEP_LIST = "HOME,USER,LOGNAME,COLORS,DISPLAY,HOSTNAME,KRB5CCNAME,LS_COLORS,PS1,PS2,XAUTHORITY,XAUTHORIZATION,XDG_CURRENT_DESKTOP"
+RAR_ENV_CHECK_LIST = "COLORTERM,LANG,LANGUAGE,LC_*,LINGUAS,TERM,TZ"
+RAR_ENV_DELETE_LIST = "IFS,CDPATH,LOCALDOMAIN,RES_OPTIONS,HOSTALIASES,NLSPATH,PATH_LOCALE,LD_*,_RLD*,TERMINFO,TERMINFO_DIRS,TERMPATH,TERMCAP,ENV,BASH_ENV,PS4,GLOBIGNORE,BASHOPTS,SHELLOPTS,JAVA_TOOL_OPTIONS,PERLIO_DEBUG,PERLLIB,PERL5LIB,PERL5OPT,PERL5DB,FPATH,NULLCMD,READNULLCMD,ZDOTDIR,TMPPREFIX,PYTHONHOME,PYTHONPATH,PYTHONINSPECT,PYTHONUSERBASE,RUBYLIB,RUBYOPT"
RAR_ENV_SET_LIST = ""
RAR_ENV_OVERRIDE_BEHAVIOR = "false"
RAR_AUTHENTICATION = "perform"
@@ -83,6 +87,7 @@ Malformed values strings create compile-time errors.
| `RAR_WORKDIR_ADD_LIST`| Comma-separated (`,`) | Allowed system directories where users are explicitly authorized to execute commands. |
| `RAR_WORKDIR_REMOVE_LIST`| Comma-separated (`,`) | Denylist of forbidden working directories where execution requests are rejected. |
| `RAR_WORKDIR_FALLBACK`| Optional File Path | Forces a static fallback working directory for the executed command if not defined. Warning! This will *always* set the working directory for *every* commands. |
+| `RAR_TTY_OWNER` | String (`"caller"` \| `"target"`) | Determines whether the controlling terminal of the executed command is owned by the calling user or the target user. Note that sudo uses the "target" option by default, but a safer approach is to use "caller", so RaR uses "caller" by default. |
Be very careful with PATH management, "keep-unsafe" is insecure. And I strongly not reccommend it.
Working directory management is somewhat useful for some cases, such as allowing restricted commands only in a specific restricted directory. Of course that means that allowed commands shouldn't change internally the working directory. For example, if you set "/bin/ls" command, and setting fallback workdir to the "/tmp", then all "dosr ls" command will only show "/tmp" content, no matter where the user is located. Otherwise, if you set "/bin/ls .*" for the command, then the user still be able to retrieve folder contents everywhere on the system.
@@ -101,6 +106,8 @@ Be very careful with PATH management, "keep-unsafe" is insecure. And I strongly
| `RAR_TIMEOUT_MAX_USAGE`| Integer (`u64`) | Absolute limits on how many times a given token context can be reused. |
| `RAR_TIMEOUT_STORAGE` | Folder Path | Filesystem path used by the timeout engine to persist execution timestamp tokens. |
| `RAR_EXEC_INFO_DISPLAY`| String (`"hide"` \| `"show"`) | Give the ability to users to know their allowed commands and configuration applied. |
+| `RAR_PASSWORD_TRIES` | Integer (`u8`) | Maximum number of password attempts allowed before aborting the authentication sequence. |
+| `RAR_PASSWORD_DELAY` | Duration `"hh:mm:ss"` | The time to wait between password attempts. |
Showing to a user their policy can help attackers in their enumeration phase. A good policy knows what command a user is allowed to do in a such way that the user don't know that there is a strict policy applied.
diff --git a/book/src/install/installation.md b/book/src/install/installation.md
index 1fecb555..89eb2915 100644
--- a/book/src/install/installation.md
+++ b/book/src/install/installation.md
@@ -32,6 +32,25 @@ This is a complete process for compiling and installing `dosr` and `chsr` binari
- Rust toolchain
- Administrative rights (`sudo` or equivalent)
+## Dependencies installation
+
+Since 4.2.0, we removed the auto dependencies installation through `cargo xtask` because it takes responsibilities that are not the purpose of the project. So we now require you to install dependencies yourself:
+
+| Distribution | Dependencies |
+|---|---|
+| Debian, Ubuntu | `apt install clang pkg-config libpam0g-dev libseccomp-dev pandoc` |
+| Fedora, RHEL family | `dnf install clang pkgconfig pam-devel libseccomp-devel pandoc` |
+| openSUSE | `zypper install clang pkg-config pam-devel libseccomp-devel pandoc` |
+| Arch Linux | `pacman -S clang pkgconf pam libseccomp pandoc-cli` |
+
+Note that `clang` is selected in the case you want to compile the capable tool. If you prefer using `gcc`, feel free to use it instead.
+
+Also, `pandoc` is only required if you want to build the man pages from source.
+
+If you are using another distribution, you can find their names on these websites [pkgs.org](https://pkgs.org/) or [repology.org](https://repology.org/).
+
+If you miss any dependency, you can use the `cargo xtask doctor` command.
+
### Retrieve the source code
```bash
@@ -39,7 +58,7 @@ git clone https://github.com/LeChatP/RootAsRole
cd RootAsRole
```
-## Dependencies installation, compiling and installing
+## Compiling and installing
Before installing the tool, you might want to define some default behaviors. You'll find them in this page [Build-time default configuration](build.md).
@@ -48,12 +67,11 @@ It might be trivial, but in order to install RootAsRole, you needs full administ
To manage that, we developed an installation helper tool
```bash
-cargo xtask install -bip sudo
+cargo xtask install -bp sudo
```
This command performs:
-- dependency installation when required (privileged)
- project build
- deployment of `dosr` and `chsr` in `RAR_BIN_PATH` (privileged)
- capability/ownership setup for `dosr` (privileged)
diff --git a/book/src/security.md b/book/src/security.md
index 8b4b7696..6aae875b 100644
--- a/book/src/security.md
+++ b/book/src/security.md
@@ -14,7 +14,11 @@ In the RaR context, it describes the security mechanisms that secures the progra
- Capability-oriented privilege model
- Optional timestamp/timeout behavior
- Immutable policy file workflow
-- Hardened policy editor path in `chsr editor` (landlock/seccomp)
+- Hardened policy editor path in `chsr editor` (landlock)
+
+
+The hardening of the policy editor is complex to do with a generic editor tool. For now, only landlock is enforced. We need to look at seccomp protection in the editor mode.
+
## Operational safeguards
diff --git a/core/src/database/actor.rs b/core/src/database/actor.rs
index 9c9205ca..65795e5d 100644
--- a/core/src/database/actor.rs
+++ b/core/src/database/actor.rs
@@ -1,6 +1,9 @@
use std::{
borrow::Cow,
+ cell::RefCell,
+ collections::HashMap,
fmt::{self, Display, Formatter, Write},
+ thread::LocalKey,
};
use bon::bon;
@@ -77,17 +80,49 @@ impl SUserType {
}
}
+thread_local! {
+ /// The ids of the names that this thread looked up. A policy names the same users and groups
+ /// in many of its tasks (root above all), and each lookup goes through NSS, which may query
+ /// systemd-userdb or a directory service: about 20 µs, for each task of a policy. A failed
+ /// lookup is not kept, so that it is seen each time.
+ static USER_IDS: RefCell>> = RefCell::new(HashMap::new());
+ static GROUP_IDS: RefCell>> = RefCell::new(HashMap::new());
+}
+
+/// The id of `name` in `cache`, else from `lookup`, kept when the lookup succeeds.
+fn cached_id(
+ cache: &'static LocalKey>>>,
+ name: &str,
+ lookup: impl FnOnce() -> nix::Result