Skip to content

Commit

Permalink
v3.0.0-alpha.5
Browse files Browse the repository at this point in the history
v3.0.0-alpha.5
  • Loading branch information
LeChatP authored Jun 10, 2024
2 parents 91a8fc8 + 25cae43 commit bb52064
Show file tree
Hide file tree
Showing 37 changed files with 36,682 additions and 35,593 deletions.
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 @@ -150,6 +150,9 @@ fn main() {
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

0 comments on commit bb52064

Please sign in to comment.