extrasafe 0.5.1

repository·master·Indexed 42 days ago

https://github.com/boustrophedon/extrasafe

A Rust wrapper for Linux security features including seccomp, Landlock, and user namespaces. It provides a default-deny framework using SafetyContext and RuleSet to restrict syscalls and filesystem access, helping developers mitigate the impact of vulnerabilities like RCE by embedding security profiles directly into applications.

Tokens
11.9K
Snippets
33
Records
45
Agent score
53%

What's inside extrasafe

  1. How extrasafe works: SafetyContext, RuleSet, and SeccompRule

    master

    extrasafe operates on a default-deny principle. It uses a logical framework to group syscalls into manageable sets, allowing you to build a security policy without needing deep knowledge of seccomp internals.

    The core abstractions are:

    1. SafetyContext: The entry point. You create a context, gather rules via RuleSets, and then apply the resulting seccomp filters. Once applied via apply*() methods, the filters cannot be removed.
    2. RuleSet: A trait representing a facet of a security policy (e.g., allowing IO, network, or clock access). You can use built-in implementors or define your own.
    3. SeccompRule: Defines a specific syscall and optional SeccompArgumentFilter conditions.

    Logic Rules:

    • Argument Filters: Within a single SeccompRule, multiple conditions are AND-ed together (all must be true to allow the syscall).
    • Rulesets: If multiple SeccompRules are loaded for the same syscall, they are OR-ed together (the syscall is allowed if any rule allows it).
    • Limitations: Argument filters cannot dereference pointers; therefore, you cannot perform string comparisons on file paths.
  2. Understanding RuleSets and their restrictions

    master

    A RuleSet is a collection of SeccompRule and LandlockRules. It consists of:

    • Simple rules: Syscalls enabled without any restriction.
    • Conditional rules: Syscalls restricted by specific argument conditions.
    • Name: A string used for error reporting.
    • Landlock rules: (If feature enabled) Access control for specific paths/directories.

    Important Restrictions

    Overlapping Rules

    Because extrasafe is additive, you cannot enable a RuleSet that uses simple_rules (unrestricted) for a syscall if another enabled RuleSet uses conditional_rules (restricted) for that same syscall. This prevents silent failures where a restriction would be ignored by the kernel.

    Syscall Pointer Arguments

    Seccomp cannot compare arguments that are pointers. For example, you cannot filter the open syscall based on the file path string because the path is passed as a char * pointer.

  3. Best practices for library developers using extrasafe

    master

    If you are developing a library, you should avoid calling extrasafe directly because you cannot predict the requirements of your dependents. Instead, use one of these three patterns:

    1. Provide a RuleSet: Implement the extrasafe::RuleSet trait that covers the specific functionality your library requires.
    2. Provide an init function: Perform all necessary IO-related work (e.g., reading configuration files, loading SSL certificates) in an initialization phase before security restrictions are applied.
    3. Thread-level isolation: If your library uses independent worker threads, apply extrasafe restrictions specifically within those worker threads.
  4. Extrasafe platform support and limitations

    master

    Supported Architectures

    • Currently supports x86_64 only.

    Security Model

    • Seccomp: Used for syscall filtering. It is deny-by-default. It cannot filter by path name or pointer arguments.
    • Landlock: Used for fine-grained filesystem access control (path-based).
    • User Namespaces: Used for broader isolation (via the Isolate abstraction).
  5. How SafetyContext and Isolate work together

    master

    The extrasafe library provides two primary abstractions for securing a program from the inside:

    1. SafetyContext: Used to create seccomp and Landlock filters.
      • seccomp allows you to prohibit specific syscalls. It is somewhat coarse-grained and cannot filter syscalls with complex arguments (like pointers).
      • Landlock provides fine-grained access control over filesystem operations.
    2. Isolate: Used to launch and configure unprivileged user namespaces. This allows a program to run in an environment with isolated filesystems, networks, and process trees, making it suitable for running untrusted code like image or video processors.

    Once applied, these security features are permanent for the lifetime of the thread and all subsequent threads or subprocesses. Seccomp can also be applied to all existing threads at once.

  6. Use extrasafe to restrict syscalls with seccomp

    master

    You can use extrasafe::SafetyContext to apply seccomp filters to your process. The library uses a deny-by-default approach with pre-selected sets of syscalls to prevent misuse. You can restrict syscalls to a specific thread using .apply_to_current_thread() or to all threads in the process using .apply_to_all_threads().

    Common patterns include:

    • Disabling all syscalls except standard I/O (stdout/stderr).
    • Implementing the RuleSet trait to define a custom set of allowed syscalls.
    fn main() {
        println!("disabling syscalls...");
    
        extrasafe::SafetyContext::new()
            .enable(
               extrasafe::builtins::SystemIO::nothing()
                  .allow_stdout()
                  .allow_stderr()
       	    ).unwrap()
    	.apply_to_all_threads().unwrap();
    
        // Opening files now fails!
        let res = File::create("should_fail.txt");
        assert!(res.is_err());
    
        println!("but printing to stdout still works!");
        eprintln!("and so does stderr!");
    }
  7. Use Landlock for fine-grained filesystem access control

    master

    While seccomp is a blunt tool that cannot filter by path name, extrasafe supports the Landlock Linux Security Module to provide targeted filesystem access. This allows you to permit specific operations (like creating or writing to files) within specific directories while denying access to the rest of the system.

    fn main() {
        let tmp_dir = tempfile::tempdir().unwrap().into_path();
    
        extrasafe::SafetyContext::new()
            .enable(
               extrasafe::builtins::SystemIO::nothing()
                  .allow_create_in_dir(&tmp_dir)
                  .allow_write_file(&tmp_dir)
            ).unwrap()
    	.apply_to_current_thread().unwrap();
    
        // Opening arbitrary files now fails!
        assert!(File::open("/etc/passwd")
            .is_err());
    
        // But the directory we allowed works
        assert!(File::create(tmp_dir.join("my_output.txt"))
            .is_ok());
    
        // And other syscalls are still disallowed
        assert!(std::net::UdpSocket::bind("127.0.0.1:0")
            .is_err());
    }
  8. Use Landlock to restrict filesystem access

    master

    Landlock allows you to restrict filesystem access via access rights applied to files or directories.

    Requirements & Setup:

    • Requires the landlock crate feature to be active.
    • Requires Linux kernel 5.19+ (Landlock ABI V2).
    • Landlock can be applied independently of seccomp using SafetyContext::landlock_only() before calling apply_to_current_thread().

    Usage Patterns:

    • The easiest way to use Landlock is via the SystemIO ruleset, which provides helper methods like allow_create_in_dir, allow_read_path, and allow_write_file.
    • To implement custom rules, you can use the extrasafe::landlock::access module or the AccessFs enum from the landlock crate to create extrasafe::LandlockRules manually.

    Caveats:

    • allow_create_in_dir typically requires a corresponding allow_write_file call to function correctly.
    • SafetyContext::apply_to_all_threads does not work with Landlock. However, Landlock rules are inherited by child threads and processes.
    fn with_landlock() {
        let tmp_dir_allow = tempfile::tempdir().unwrap().into_path();
        let tmp_dir_deny = tempfile::tempdir().unwrap().into_path();
    
        extrasafe::SafetyContext::new()
            .enable(
               extrasafe::builtins::SystemIO::nothing()
                  .allow_create_in_dir(&tmp_dir_allow)
                  .allow_write_file(&tmp_dir_allow)
            ).unwrap()
        .apply_to_current_thread().unwrap();
    
        // Opening arbitrary files now fails!
        let res = File::create(tmp_dir_deny.join("evil.txt"));
        assert!(res.is_err());
    
        // But the directory we allowed works
        let res = File::create(tmp_dir_allow.join("my_output.txt"));
        assert!(res.is_ok());
    }
  9. Typical usage pattern for extrasafe

    master

    To minimize the attack surface, apply extrasafe filters after the initial startup phase but before entering the main application logic.

    Recommended Workflow:

    1. Perform startup tasks (e.g., reading configuration files, binding to network sockets, initializing file handles).
    2. Create and configure a SafetyContext with the necessary RuleSets.
    3. Call apply*() to lock down the process.

    This ensures that once the application is running, it cannot perform unauthorized actions like opening new files or binding to new sockets, even if an exploit occurs.

  10. Define a custom RuleSet

    master

    If you need syscalls not covered by built-in sets, you can implement the RuleSet trait. For very simple needs (one or two syscalls without conditions), you can pass a Sysno directly to .enable().

    To implement a full RuleSet with conditional logic, use the seccomp_arg_filter! macro to define argument conditions.

    use extrasafe::*;
    use syscalls::Sysno;
    use std::collections::HashMap;
    
    struct MyRuleSet;
    
    impl RuleSet for MyRuleSet {
        fn simple_rules(&self) -> Vec<Sysno> {
            // Example: allow reboot
            vec![Sysno::reboot]
        }
    
        fn conditional_rules(&self) -> HashMap<Sysno, Vec<SeccompRule>> {
            // Example: Only allow the creation of stream (tcp) sockets
            const SOCK_STREAM: u64 = libc::SOCK_STREAM as u64;
    
            let rule = SeccompRule::new(Sysno::socket)
                .and_condition(
                    seccomp_arg_filter!(arg0 & SOCK_STREAM == SOCK_STREAM));
            
            HashMap::from([(Sysno::socket, vec![rule])])
        }
    
        fn name(&self) -> &'static str {
            "MyRuleSet"
        }
    }
    
    // Usage:
    extrasafe::SafetyContext::new()
        .enable(MyRuleSet).unwrap()
        .apply_to_current_thread().unwrap();
  11. Use Extrasafe Isolates for namespace isolation

    master

    Extrasafe Isolates are wrappers around Linux user namespaces (mount, network, PID, and user namespaces) that allow you to isolate a subprocess from the rest of the network and filesystem.

    How it works:

    1. Isolate::run launches a subprocess by re-running the current executable with specific arguments.
    2. The child process must call Isolate::main_hook in its main() function to recognize the isolation request.
    3. main_hook performs the clone syscall, enters the namespace, sets up a private temporary filesystem, mounts specified directories, and uses pivot_root to change the root filesystem.

    Key Configuration Methods:

    • Isolate::new(name, setup_fn): Creates a new isolate instance. The name must match the name used in Isolate::run.
    • .add_bind_mount(source_path, target_path): Mounts a directory from the parent into the isolate's filesystem.
    • .set_rootfs_size(size): Limits the amount of data that can be written to the isolate's filesystem.

    Communication & Configuration:

    • Environment Variables: You can pass a HashMap<String, String> to Isolate::run to provide initial configuration to the isolate.
    • Inter-process Communication: By default, there is no communication channel. To enable communication, you can bind-mount Unix sockets or choose not to isolate the network namespace.

    Important Note: The name passed to Isolate::run and Isolate::main_hook must be identical, otherwise the program will not enter the isolate after re-executing.

    use extrasafe::isolate::Isolate;
    use std::collections::HashMap;
    use std::path::PathBuf;
    
    const EXAMPLE_ISOLATE: &str = "user guide isolate";
    
    fn do_cool_thing() {
        println!("I'm going to read some files from /cooldir and do cool stuff with it!");
    }
    
    fn setup_isolate(name: &'static str) -> Isolate {
        let path = std::env::var("COOL_DIRECTORY").unwrap();
        let path = PathBuf::from(path);
        Isolate::new(name, do_cool_thing)
            .add_bind_mount(path, "/cooldir")
            .set_rootfs_size(1)
    }
    
    fn main() {
        // Must be called in main to handle re-execution
        Isolate::main_hook(EXAMPLE_ISOLATE, setup_isolate);
    
        let env_vars = HashMap::from([("COOL_DIRECTORY".to_string(), "/".to_string())]);
    
        // Launches the isolated subprocess
        let output = Isolate::run(EXAMPLE_ISOLATE, &env_vars).unwrap();
    
        assert!(output.status.success());
    }
  12. Basic SafetyContext usage

    master

    To secure a thread using SafetyContext, you create a context, enable one or more RuleSets (collections of syscalls), and then apply the rules to the current thread or all threads.

    Note that extrasafe starts from a default-deny state. Every enable call is additive.

    let ctx = extrasafe::SafetyContext::new();
    
    use extrasafe::builtins::{SystemIO, Networking};
    
    let ctx = ctx
        .enable(
            SystemIO::nothing()
                .allow_open_readonly()
            ).expect("Failed to add systemio ruleset to context")
        .enable(
            Networking::nothing()
                .allow_start_tcp_clients()
                .allow_running_tcp_clients()
            ).expect("Failed to add networking ruleset to context");
    
    ctx.apply_to_current_thread()
        .expect("Failed to apply seccomp filters");
    
    // Any attempt to perform unauthorized actions (like writing to a file) will now fail
    assert!(std::fs::File::create("/tmp/a_file").is_err());