Cualquier crate en tu árbol de dependencias puede abrir /etc/passwd, escribir en tu directorio ~/.ssh, o enumerar cada archivo en tu proyecto. La biblioteca estándar de Rust no pide permiso. Asume que cualquier código con la capacidad de llamar a std::fs::File::open está autorizado para tocar cualquier ruta que el sistema operativo permita.

cap-std cambia esa suposición. Es un reemplazo directo de los modules de E/S de std de Rust que reemplaza la autoridad ambiental con capabilities explícitas. Si quieres abrir un archivo, primero necesitas una capability que demuestre que tienes acceso al directorio que lo contiene.

Esto importa más de lo que podrías pensar. Los ataques a la cadena de suministro no siempre necesitan exploits sofisticados. A veces solo necesitan un script de compilación o una dependencia transitiva que exfiltre archivos en silencio mientras tus pruebas se ejecutan. Aislar ese riesgo mediante sandboxing a nivel de biblioteca, sin containers ni máquinas virtuales, es lo que cap-std permite.

Cómo se ve realmente el acceso al sistema de archivos basado en capabilities

En Rust estándar, abrir un archivo es una línea sin prerequisitos:

use std::fs::File;

// Any code, anywhere, can do this
let file = File::open("/etc/passwd")?;

La ruta es absoluta, y la autoridad es ambiental. El sistema operativo verifica los permisos, pero el programa nunca tiene que demostrar que debería haber podido construir esa ruta en primer lugar.

cap-std reemplaza esto con un modelo de dos pasos. Empiezas con una capability, típicamente un handle de directorio, y solo puedes abrir rutas relativas a ese directorio:

use cap_std::fs::Dir;
use std::path::Path;

// Open the current working directory as a capability
let cwd = Dir::open_ambient_dir(".", cap_std::ambient_authority())?;

// Now we can only open files inside this directory tree
let file = cwd.open("config/app.toml")?;

Observa la llamada a open_ambient_dir. Esa es la salida de emergencia. Convierte una ruta ambiental en una capability, y es deliberadamente verbosa y fácil de auditar. Una vez que tienes un Dir, no existe un método open que acepte una ruta absoluta. La API simplemente no lo permite.

Cómo cap-std refleja std sin copiar su modelo de seguridad

cap-std está estructurado como reemplazos directos de std::fs, std::net, y std::os::unix::net. Los tipos son intencionalmente familiares. cap_std::fs::File envuelve a std::fs::File. cap_std::fs::Dir es el nuevo punto de entrada, más o menos análogo a trabajar dentro de un chroot excepto que se aplica mediante el sistema de tipos en lugar de una llamada al sistema operativo con privilegios.

El crate logra esto mediante una abstracción de más bajo nivel llamada cap-primitives. Bajo el capó, cap-std usa llamadas al sistema de estilo openat en Unix y operaciones restringidas relativas similares en Windows. Nunca llama al File::open sin restricciones de la biblioteca estándar. En Linux, usa openat2 con RESOLVE_BENEATH cuando está disponible para prevenir escapes por symlinks. En Windows, usa NtCreateFile con flags restringidas.

Esto no es un wrapper alrededor de containers o seccomp. Es una reimplementación de las APIs estándar del sistema de archivos que simplemente omite las operaciones peligrosas.

El tipo Dir soporta la mayoría de lo que esperarías: open, create_dir, rename, remove_file, read_dir, y así sucesivamente. La diferencia clave es que cada operación es relativa a ese handle de directorio. Si quieres mover un archivo fuera del árbol, necesitas dos capabilities Dir, una para el origen y otra para el destino. La API te obliga a llevar contigo la prueba de acceso.

Los trucos de portabilidad que usa cap-std

Los modelos de capabilities del sistema de archivos varían entre sistemas operativos. Linux tiene openat2. FreeBSD tiene cap_rights_limit y O_RESOLVE_BENEATH. Windows tiene control de acceso basado en handles pero no un equivalente directo de RESOLVE_BENEATH. macOS tiene el soporte más limitado de las plataformas principales.

cap-std maneja esto mediante una capa de portabilidad. En sistemas con soporte fuerte del kernel, usa los mecanismos de restricción nativos. En sistemas sin ellos, recurre a una implementación en userspace que resuelve symlinks manualmente y valida que cada componente de una ruta relativa se mantenga dentro del límite de la capability.

Este fallback es más costoso, pero significa que tu modelo de seguridad es portable. Un sandbox WASI, un servidor Linux, y la MacBook de un desarrollador pueden todos aplicar los mismos límites de capabilities sin código específico de la plataforma en tu aplicación.

Cómo se ve usar cap-std en un proyecto real

Convertir un proyecto existente es menos doloroso de lo que podrías esperar, porque las APIs son deliberadamente cercanas a std. El cambio principal es que dejas de pasar &Path o &str como tu unidad de direccionamiento del sistema de archivos. Pasas &Dir en su lugar.

Aquí hay un ejemplo simplificado de leer configuración desde un directorio que el llamador controla:

use cap_std::fs::Dir;
use std::io::{self, Read};

pub fn load_config(dir: &Dir, name: &str) -> io::Result<String> {
    let mut file = dir.open(name)?;
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    Ok(contents)
}

La función load_config no puede acceder a archivos fuera del directorio que se le da. No necesita saber dónde está ese directorio en el disco. Esto la hace trivial de probar de forma aislada:

use cap_std::fs::Dir;
use cap_tempfile::TempDir;

#[test]
fn test_load_config() -> std::io::Result<()> {
    let tmp = TempDir::new(Default::default())?;
    let dir = tmp.dir();

    {
        let mut f = dir.create("app.toml")?;
        std::io::Write::write_all(&mut f, b"key = \"value\"")?;
    }

    let config = load_config(dir, "app.toml")?;
    assert!(config.contains("value"));
    Ok(())
}

cap-tempfile provee un directorio temporal como una capability, así que incluso tus pruebas nunca otorgan autoridad ambiental. La función load_config fallaría si intentara abrir ../etc/passwd o cualquier ruta absoluta, independientemente de lo que la prueba o el llamador proporcione.

Qué se rompe cuando cambias

La limitación más grande es la compatibilidad con el ecosistema. La mayoría de los crates de Rust que tocan el sistema de archivos aceptan un &Path o un PathBuf. Esperan autoridad ambiental. Si quieres usar cap-std con, digamos, un crate de análisis de configuración que lee archivos include desde el disco, ese crate necesita ser consciente de cap-std o necesitas leer los archivos tú mismo y pasar el contenido como strings al parser.

Esta es la misma historia de migration que async Rust. De la misma manera que una función que toma std::fs::File no puede ser llamada desde código async que espera un handle de archivo async, una función que toma &Path no puede ser llamada con un &Dir. El límite es claro pero real.

También hay operaciones que cap-std deliberadamente no soporta. No puedes crear hard links que escapen de un árbol de directorios. No puedes seguir symlinks arbitrarios que apunten fuera del límite de la capability. No puedes llamar a std::env::current_dir y asumir que significa algo en un programa basado en capabilities. Estos no son bugs. Son el modelo de seguridad.

El rendimiento usualmente no es un problema. En Linux con openat2, la sobrecarga es unos pocos flags extra a una syscall existente. En plataformas que usan el fallback de userspace, la resolución de rutas es más lenta, pero típicamente sigue siendo despreciable comparada con el I/O real.

Cuándo cap-std vale la fricción

Probablemente no necesites cap-std para cada herramienta CLI o servidor web. Si confías en tus dependencias y tu modelo de amenaza son atacantes externos en lugar de crates maliciosos, los containers y los permisos normales del sistema operativo están bien.

cap-std brilla en algunos escenarios específicos:

Sistemas de plugins. Si tu aplicación carga WASM modules no confiables o plugins nativos, cap-std te permite otorgar a cada plugin una capability Dir que representa su sandbox. El plugin no puede escapar sin un exploit del kernel, porque la API simplemente no expresa el concepto de “abrir cualquier archivo”.

Herramientas de compilación y package managers. Estos ejecutan código arbitrario de internet. Un script build.rs que usa cap-std no puede leer tus claves SSH a menos que le entregues explícitamente una capability a tu directorio home.

Objetivos WASI. La WebAssembly System Interface se construye sobre capabilities. El diseño de la API de cap-std influyó directamente en WASI, y usar cap-std hace que portar a WASI sea directo porque el modelo de seguridad ya está alineado.

Pruebas y reproducibilidad. Las pruebas que toman &Dir en lugar de depender del directorio de trabajo actual son herméticas por defecto. No necesitas hacer chdir y restaurar el estado. Solo le entregas a la prueba una capability de directorio temporal.

Empezar

Agrega cap-std a tu Cargo.toml:

[dependencies]
cap-std = "3.0"
cap-tempfile = "3.0"

Elige un module que lea archivos, cambia su API pública para que acepte &Dir en lugar de &Path, y actualiza los llamadores. No tienes que migrar todo de una vez. cap-std y std::fs pueden coexistir en el mismo binario. La migration es opt-in por module.

Si mantienes una biblioteca que lee archivos, considera agregar una API basada en cap_std::fs::Dir junto a tu existente basada en rutas. Tus usuarios de WASI te lo agradecerán, y tus usuarios conscientes de la seguridad tendrán una tarea más fácil aplicando sandboxing a sus aplicaciones.

El repository de cap-std está en github.com/bytecodealliance/cap-std. La documentación del crate incluye notas de soporte de plataforma y la superficie completa de la API. Empieza con Dir::open_ambient_dir como punto de entrada, luego intenta eliminar cada otra llamada a std::fs que acepte una ruta absoluta. Te sorprenderá cuánta autoridad ambiental llevaba tu código.