Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v3.0.0-alpha.5 #50

Merged
merged 32 commits into from
Jun 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
fc1e1d8
bump version in readme
LeChatP May 27, 2024
9137a3b
chore: Remove unnecessary explaination in no-root.md
LeChatP May 27, 2024
2588b0c
docs + upgrade addition and dependencies script fix
LeChatP May 29, 2024
0e4dd43
refactor: Fix and add logging in capable-ebpf module
LeChatP May 29, 2024
65eb64e
chore: fix typo in man-db package name
LeChatP May 29, 2024
7290a7c
Add bpftool on ubuntu
LeChatP May 30, 2024
8ae25bc
rh9 ok, testing centos9
LeChatP May 31, 2024
a9a4acf
Tested capable on centos, ubuntu, redhat
LeChatP Jun 1, 2024
2fc9e64
feat: Add FAQ page with known issues and solutions
LeChatP Jun 2, 2024
8354d3c
chore: Add Vagrantfiles for Debian 11 and Fedora 37 tests
LeChatP Jun 2, 2024
9b8270d
Make dependencies work on Debian 11
LeChatP Jun 2, 2024
e9c2b72
Update compatibility information in README.md
LeChatP Jun 2, 2024
575da2e
bump version to 3.0.0-alpha.5
LeChatP Jun 2, 2024
d299dbc
chore: Update capabilities in rootasrole.json for Capable tool
LeChatP Jun 2, 2024
eeea41a
fix: Correct path resolution in final_path function
LeChatP Jun 3, 2024
519d01a
Update capabilities for Capable tool
LeChatP Jun 3, 2024
a1db6fa
chore: Update Vagrantfiles to manage capabilities
LeChatP Jun 3, 2024
a4708b8
Add comparison table
LeChatP Jun 3, 2024
8e15240
Update tokio dependency to version 1.38.0 and nix dependency to versi…
LeChatP Jun 4, 2024
e91b83b
Finally a functionnal capable
LeChatP Jun 4, 2024
886e23a
miss to readd unshare lib
LeChatP Jun 4, 2024
35c270f
cargo fmt
LeChatP Jun 7, 2024
614f81b
Apply more least privilege on eBPF. Also unload it when program ends.
LeChatP Jun 8, 2024
bacd026
cargo fix fmt
LeChatP Jun 8, 2024
62c5fdf
ebpf fix and fmt
LeChatP Jun 8, 2024
63c196a
some tests
LeChatP Jun 8, 2024
98a5535
Add tests privileged
LeChatP Jun 10, 2024
b3892a9
Format Rust code using rustfmt
github-actions[bot] Jun 10, 2024
3859991
chore: Update tests.yml to install dependencies as root for admin tests
LeChatP Jun 10, 2024
c0cbe98
Merge branch 'develop' of github.com:LeChatP/RootAsRole into develop
LeChatP Jun 10, 2024
feb6cc0
Use absolute path instead of reinstalling rust as root
LeChatP Jun 10, 2024
25cae43
fix path for cargo
LeChatP Jun 10, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,14 @@ jobs:
with:
file: cobertura.xml
flags: unittests

- name: run tests with coverage as Admin
run: sudo -E /usr/local/cargo/bin/cargo +nightly tarpaulin --verbose --all-features --workspace --timeout 120 --bin chsr --bin sr --exclude-files capable* capable-ebpf/src/vmlinux.rs capable/src/main.rs build.rs --out Xml

- name: Upload coverage reports to Codecov as Admin
uses: codecov/codecov-action@v3
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
file: cobertura.xml
flags: admin-unittests
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,8 @@ Cargo.lock

# Html results
*.html
*.xml
*.xml

# Vagrant
*.env
*.vagrant/
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ members = ["xtask", "capable", "capable-common"]

[package]
name = "RootAsRole"
# The project version is managed on json file in resources/rootasrole.json
version = "3.0.0-alpha.5"
rust-version = "1.74.1"
authors = ["Eddie Billoir <[email protected]>"]
Expand Down
2 changes: 0 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,13 @@ ifneq (0, $(filter $(shell capsh --has-p=CAP_DAC_OVERRIDE,CAP_CHOWN &>/dev/null;
$(PRIV_EXE) chown root:root /usr/bin/sr /usr/bin/chsr /usr/bin/capable
$(PRIV_EXE) chmod 0555 /usr/bin/sr /usr/bin/chsr /usr/bin/capable
$(PRIV_EXE) setcap "=p" /usr/bin/sr
$(PRIV_EXE) setcap cap_dac_override,cap_sys_admin,cap_sys_ptrace+ep /usr/bin/capable
else ifneq (0, $(shell capsh --has-p=CAP_SETFCAP &>/dev/null; echo $?))
@echo "You must have CAP_SETFCAP privilege to perform installation."
else
cp -f $(BINS) /usr/bin
chown root:root /usr/bin/sr /usr/bin/chsr /usr/bin/capable
chmod 0555 /usr/bin/sr /usr/bin/chsr /usr/bin/capable
setcap "=p" /usr/bin/sr
setcap cap_dac_override,cap_sys_admin,cap_sys_ptrace+ep /usr/bin/capable
endif


Expand Down
35 changes: 31 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@
<img alt="GitHub" src="https://img.shields.io/github/license/LeChatP/RootAsRole">

</p>
<!-- The project version is managed on json file in resources/rootasrole.json -->
<!-- markdownlint-restore -->

# RootAsRole (V3.0.0-alpha.4) : A memory-safe and security-oriented alternative to sudo/su commands
# RootAsRole (V3.0.0-alpha.5) : A memory-safe and security-oriented alternative to sudo/su commands

**RootAsRole** is a project to allow Linux/Unix administrators to delegate their administrative tasks access rights to users. Its main features are :

Expand Down Expand Up @@ -81,6 +82,19 @@ However you won't find out exact same options as sudo, you can use the `--role`

## Why do you need this tool ?

| | setcap | sudo | sr |
|-------------------------------------|--------|------------------|----|
| Change user | | ✅ but mandatory | ✅ |
| Change groups | | ✅ but mandatory | ✅ |
| Manage environment variables | | ✅ | ✅ |
| Strict command matching | | ✅ with wildcards | ✅ with PCRE and glob |
| Interoperable configuration/policy | | ✅ only with LDAP | ✅ with JSON |
| Set capabilities | ✅ | | ✅ with Ambient set |
| Prevent direct privilege escalation | | | ✅ with Bounding set |
| Do not trust authorized users by default | | | ✅ |
| Evolvable configuration/policy | | | ✅ with JSON |
| Scalable access control | | | ✅ with RBAC |

Traditional Linux system administration relies on a single powerful user, the superuser (root), who holds all system privileges. This model does not adhere to the principle of least privilege, as any program executed with superuser rights gains far more privileges than necessary. For example, `tcpdump`, a tool for sniffing network packets, only needs network capabilities. However, when run as the superuser, tcpdump gains all system privileges, including the ability to reboot the system. This excessive privilege can be exploited by attackers to compromise the entire system if tcpdump has vulnerabilities or their developers performs a supply chain attack.

The RootAsRole project offers a role-based approach for managing Linux capabilities. It includes the sr (switch role) tool, which allows users to control the specific privileges assigned to programs.
Expand All @@ -93,7 +107,7 @@ Additionnally, `setcap` is applied to the binary file, which means that the capa

Furthermore, the `pam_cap` module is applied to the PAM user session, which means that the capabilities are fixed for every user's session. This is not ideal as administrator do not need these capabilities for every commands and every sessions.

The RootAsRole project is compatible with LSM (Linux Security Modules) such as SELinux and AppArmor, as well as pam_cap.so. Administrators can continue using pam_cap.so alongside our module. Additionally, the module includes the capable tool, which helps users identify the privileges required by an application.
The RootAsRole project is compatible with LSM (Linux Security Modules) such as SELinux and AppArmor, as well as pam_cap.so. Administrators can continue using pam_cap.so alongside our project. Additionally, the project includes the capable tool, which helps users identify the privileges required by an application.

### How to configure RootAsRole

Expand All @@ -113,14 +127,27 @@ To determine the privileges required for your command, you can use the capable p

By following these steps, you can identify and manage the necessary privileges for your command more effectively.

## Tested Platforms
## Compatibility

Our module has been tested on:
Our project has been manually tested on (tests in may 2023):

* Ubuntu>=16.04
* Debian>=10
* ArchLinux

In june 2024, we performed automated `capable` tests with Vagrant on the following distributions:

* ❌ Centos 7 → Kernel too old (3.1)
* ✅ Centos 8
* ❌ Debian 10 → Dev dependencies unavailable, it should work once compiled
* ✅ Debian 11
* ✅ Fedora 37
* ✅ RedHat 9
* ✅ Ubuntu 22.04
* ✅ ArchLinux

This doesn't mean that earlier versions of these distributions are incompatible; it simply indicates they haven't been tested yet. However, if you encounter issues during the compilation process, they are likely due to dependency problems. In theory, the RootAsRole project should work on any Linux distribution with a kernel version of 4.1 or higher. However, since BTF (BPF Type Format) is becoming a mandatory requirement, [the kernel must be compiled with many features enabled](https://github.com/iovisor/bcc/blob/master/INSTALL.md#kernel-configuration).

## Contributors

Ahmad Samer Wazan : <[email protected]>
Expand Down
1 change: 1 addition & 0 deletions book/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
- [Configure RootAsRole](chsr/file-config.md)
- [Continuous Integration](continuous-integration.md)
- [How to contribute](dev/CONTRIBUTE.md)
- [FAQ](faq.md)
- [Code of Conduct](dev/CODE_OF_CONDUCT.md)

-----------
Expand Down
9 changes: 9 additions & 0 deletions book/src/faq.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# FAQ

This page contains known issues and solutions for RootAsRole project.

## capable does not work on my OS, what can I do ?

capable is a tool based on eBPF features, so it requires a Linux kernel version 4.1 or later. Additionnally you need many kernel features enabled, [described here](https://github.com/iovisor/bcc/blob/master/INSTALL.md#kernel-configuration). It is also, possible that the program cannot allocate memory, in this case you may consider to add CAP_SYS_RESOURCE capability to the program, but this may not solve completely the issue.

Finally, if you want that capable works on your OS, you can 1) open an issue on the [GitHub repository](http://github.com/LeChatP/RootAsRole), 2) create a Vagrantfile in [test/capable/](https://github.com/LeChatP/RootAsRole/tree/develop/tests/capable) directory and a script to reproduce the issue/and or fix the problem. Note: Community Vagrant images may create more issues than they solve. For example, I never managed to make capable work on ArchLinux images, but my development machine is an ArchLinux.
6 changes: 1 addition & 5 deletions book/src/knowledge/no-root.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,4 @@ This should install apache2 configuration files owned by apache2 user and group.

```bash
sr systemctl start apache2
```

This should start apache2 with the apache2 user. You can also stop it with the apache2 user:


```
2 changes: 1 addition & 1 deletion book/src/knowledge/role_hierarchy.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# How does work role hierarchy feature

The role hierarchy feature allows to extend a role with another role. This feature is useful when you don't want to duplicate the same rights in different roles. The role hierarchy feature allows to create a role that inherits the rights of another role.
A role hierarchy allows roles to be organized in a tree-like structure where roles can inherit permissions from other roles. This means that a higher-level role, often called a parent role, can pass down its permissions to lower-level roles, known as child roles. For example, in a corporate environment, a role hierarchy might be set up so that a "Manager" role inherits all the permissions of an "Employee" role, plus additional managerial permissions. This hierarchical structuring simplifies the assignment and management of permissions because changes to a parent role automatically propagate to its child roles, reducing redundancy and the potential for errors. In RootAsRole this is possible by adding the `parent` array in a role definition.
10 changes: 7 additions & 3 deletions book/src/knowledge/sod.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@

## Static Separation of Duties

Static separation of duties (SSD) in RBAC is a security feature that restricts users from having conflicting roles. This means that a user cannot have two roles that are in conflict. In RootAsRole, if a user is still assigned to two roles that are in conflict, the user will not be able to execute any command of these roles.
Static Separation of Duties (SSD) within an RBAC ensures that no single user can hold conflicting administrative roles, enhancing security and operational integrity. For instance, SSD policies would prevent a user assigned as a "System Administrator" from also being a "Network Administrator" or "Backup Administrator," thereby mitigating the risk of entire control of a system and potential fraud.

For example, let's say we have two roles: `admin` and `user`. The `admin` role has the ability to create new users, while the `user` role does not. If a user is assigned to both the `admin` and `user` roles, the user will not be able to execute any command of these roles.
With RootAsRole, you can implement SSD by creating roles that are mutually exclusive by adding `ssd` array in a role definition. For example, you can create a role for a "System Administrator" and another for a "Network Administrator." You can then assign these roles to different users, ensuring that no single user has both roles at the same time. If a user obtains a new role that conflicts with an existing role, RootAsRole will prevent the user to use any conflicting role.

## Dynamic Separation of Duties

Dynamic separation of duties (DSD) in RBAC is a security feature that restricts users from having conflicting roles at the same time. This means that a user cannot have two roles that are in conflict at the same time. For now, RootAsRole does not support this feature.
Dynamic Separation of Duties (DSD) in RBAC ensures that no single user can perform conflicting roles within a system simultaneously, managed at runtime. It verifies users' or system sessions context before allowing them to activate roles, ensuring that they do not have conflicting permissions given the dynamic context of the system.

For example, In a very small business, it may have only one system administrator. In this case the small business can enforce a DSD feature that prevent the unique administrator to simultaneously activate roles that allow both system configuration and audit log management, and cannot perform system configuration if no audit is enforced, ensuring that the administrator cannot cover his tracks.

For now, RootAsRole does not support DSD.
3 changes: 3 additions & 0 deletions build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
Ok(())
}

fn write_doc(f: &mut File) -> Result<(), Box<dyn Error>> {

Check warning on line 41 in build.rs

View workflow job for this annotation

GitHub Actions / clippy

function `write_doc` is never used

warning: function `write_doc` is never used --> build.rs:41:4 | 41 | fn write_doc(f: &mut File) -> Result<(), Box<dyn Error>> { | ^^^^^^^^^ | = note: `#[warn(dead_code)]` on by default
let docresp = reqwest::blocking::get(
"https://git.kernel.org/pub/scm/docs/man-pages/man-pages.git/plain/man7/capabilities.7",
)
Expand Down Expand Up @@ -150,6 +150,9 @@
if let Err(err) = set_cargo_version(package_version, "capable-ebpf/Cargo.toml") {
eprintln!("cargo:warning={}", err);
}
if let Err(err) = set_cargo_version(package_version, "capable-common/Cargo.toml") {
eprintln!("cargo:warning={}", err);
}
if let Err(err) = set_cargo_version(package_version, "xtask/Cargo.toml") {
eprintln!("cargo:warning={}", err);
}
Expand Down
3 changes: 2 additions & 1 deletion capable-common/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[package]
name = "capable-common"
version = "0.1.0"
# The project version is managed on json file in resources/rootasrole.json
version = "3.0.0-alpha.5"
edition = "2021"

[features]
Expand Down
1 change: 1 addition & 0 deletions capable-ebpf/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[package]
name = "capable-ebpf"
# The project version is managed on json file in resources/rootasrole.json
version = "3.0.0-alpha.5"
edition = "2021"

Expand Down
86 changes: 56 additions & 30 deletions capable-ebpf/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,79 +1,105 @@
#![no_std]
#![no_main]


#[allow(non_upper_case_globals)]
#[allow(non_snake_case)]
#[allow(non_camel_case_types)]
#[allow(dead_code)]
mod vmlinux;

use aya_ebpf::{macros::{kprobe,map}, maps::HashMap, programs::ProbeContext, helpers::{bpf_get_current_task, bpf_get_current_uid_gid, bpf_probe_read_kernel}};
use vmlinux::{task_struct, nsproxy, pid_namespace, ns_common};
use aya_ebpf::{
helpers::{bpf_get_current_task, bpf_get_current_uid_gid, bpf_probe_read_kernel},
macros::{kprobe, map},
maps::HashMap,
programs::ProbeContext,
};
use aya_log_ebpf::{debug, info};
use vmlinux::{ns_common, nsproxy, pid_namespace, task_struct};

const MAX_PID : u32 = 4*1024*1024;
const MAX_PID: u32 = 4 * 1024 * 1024;

type Key = i32;
type TaskStructPtr = *mut task_struct;

#[map]
static mut KALLSYMS_MAP: HashMap<Key, u64> = HashMap::with_max_entries(MAX_PID,0);
static mut KALLSYMS_MAP: HashMap<Key, u64> = HashMap::with_max_entries(MAX_PID, 0);
#[map]
static mut CAPABILITIES_MAP: HashMap<Key, u64> = HashMap::with_max_entries(MAX_PID,0);
static mut CAPABILITIES_MAP: HashMap<Key, u64> = HashMap::with_max_entries(MAX_PID, 0);
#[map]
static mut UID_GID_MAP: HashMap<Key, u64> = HashMap::with_max_entries(MAX_PID,0);
static mut UID_GID_MAP: HashMap<Key, u64> = HashMap::with_max_entries(MAX_PID, 0);
#[map]
static mut PPID_MAP: HashMap<Key, i32> = HashMap::with_max_entries(MAX_PID,0);
static mut PPID_MAP: HashMap<Key, i32> = HashMap::with_max_entries(MAX_PID, 0);
#[map]
static mut PNSID_NSID_MAP: HashMap<Key, u64> = HashMap::with_max_entries(MAX_PID,0);
static mut PNSID_NSID_MAP: HashMap<Key, u64> = HashMap::with_max_entries(MAX_PID, 0);

#[kprobe]
pub fn capable(ctx: ProbeContext) -> u32 {
match try_capable(ctx) {
match try_capable(&ctx) {
Ok(ret) => ret,
Err(ret) => ret as u32,
}
}

fn try_capable(ctx: ProbeContext) -> Result<u32, i64> {
fn try_capable(ctx: &ProbeContext) -> Result<u32, i64> {
info!(ctx, "capable");
unsafe {
let task: TaskStructPtr = bpf_get_current_task() as TaskStructPtr;
debug!(ctx, "debug1");
let task = bpf_probe_read_kernel(&task)?;
debug!(ctx, "debug2");
let ppid: i32 = get_ppid(task)?;
let pid: i32 = bpf_probe_read_kernel(&(*task).pid)?;
let cap: u64 = (1 << ctx.arg::<u8>(2).expect("failed to get arg")) as u64;
debug!(ctx, "debug3");
let pid: i32 = bpf_probe_read_kernel(&(*task).pid)? as i32;
debug!(ctx, "debug4");
let cap: u64 = (1 << ctx.arg::<u8>(2).unwrap()) as u64;
debug!(ctx, "debug5");
let uid: u64 = bpf_get_current_uid_gid();
debug!(ctx, "debug6");
let zero = 0;
let capval: u64 = *CAPABILITIES_MAP.get(&pid).unwrap_or(&zero);
let pinum_inum :u64 = Into::<u64>::into(get_parent_ns_inode(task)?)<<32 | Into::<u64>::into(get_ns_inode(task)?);
UID_GID_MAP.insert(&pid, &uid,0).expect("failed to insert uid");
PNSID_NSID_MAP.insert(&pid, &pinum_inum,0).expect("failed to insert pnsid");
PPID_MAP.insert(&pid, &ppid,0).expect("failed to insert ppid");
CAPABILITIES_MAP.insert(&pid, &(capval|cap),0).expect("failed to insert cap");
debug!(ctx, "debug7");
let pinum_inum: u64 = Into::<u64>::into(get_parent_ns_inode(task)?) << 32
| Into::<u64>::into(get_ns_inode(task)?);
debug!(ctx, "debug8");
UID_GID_MAP
.insert(&pid, &uid, 0)
.expect("failed to insert uid");
debug!(ctx, "debug9");
PNSID_NSID_MAP
.insert(&pid, &pinum_inum, 0)
.expect("failed to insert pnsid");
debug!(ctx, "debug10");
PPID_MAP
.insert(&pid, &ppid, 0)
.expect("failed to insert ppid");
debug!(ctx, "debug11");
CAPABILITIES_MAP
.insert(&pid, &(capval | cap), 0)
.expect("failed to insert cap");
}
Ok(0)

}

unsafe fn get_ppid(task : TaskStructPtr) -> Result<i32, i64> {
unsafe fn get_ppid(task: TaskStructPtr) -> Result<i32, i64> {
let parent_task: TaskStructPtr = get_parent_task(task)?;
return bpf_probe_read_kernel(&(*parent_task).pid);
bpf_probe_read_kernel(&(*parent_task).pid)
}

unsafe fn get_parent_task(task : TaskStructPtr) -> Result<TaskStructPtr, i64> {
return bpf_probe_read_kernel(&(*task).real_parent);
unsafe fn get_parent_task(task: TaskStructPtr) -> Result<TaskStructPtr, i64> {
bpf_probe_read_kernel(&(*task).parent)
}

unsafe fn get_parent_ns_inode(task : TaskStructPtr) -> Result<u32, i64> {
unsafe fn get_parent_ns_inode(task: TaskStructPtr) -> Result<u32, i64> {
let parent_task: TaskStructPtr = get_parent_task(task)?;
return get_ns_inode(parent_task);
get_ns_inode(parent_task)
}

unsafe fn get_ns_inode(task : TaskStructPtr) -> Result<u32, i64> {
let nsp: *mut nsproxy = bpf_probe_read_kernel(&(*task).nsproxy)?;
let pns: *mut pid_namespace = bpf_probe_read_kernel(&(*nsp).pid_ns_for_children)?;
let nsc: ns_common = bpf_probe_read_kernel(&(*pns).ns)?;
return bpf_probe_read_kernel(&nsc.inum);
unsafe fn get_ns_inode(task: TaskStructPtr) -> Result<u32, i64> {
let nsp: *mut nsproxy = bpf_probe_read_kernel(&(*task).nsproxy).map_err(|e| e as u32)?;
let pns: *mut pid_namespace =
bpf_probe_read_kernel(&(*nsp).pid_ns_for_children).map_err(|e| e as u32)?;
let nsc: ns_common = bpf_probe_read_kernel(&(*pns).ns).map_err(|e| e as u32)?;
bpf_probe_read_kernel(&nsc.inum)
}

#[panic_handler]
Expand Down
Loading
Loading