mudados tawasuyu.net y gioser.net — y los padres que inventa bwrap eran 0700
Los dos sitios que quedaban con trafico real sirven desde la caja `takana`, con TLS publico y
verificados desde fuera. Los servicios viejos SIGUEN corriendo en gioser a proposito (decision del
usuario: mover el DNS y dejar el origen vivo, para poder volver en un minuto).
· `tawasuyu.net` + `www`: la raiz en gioser era EL MONOREPO ENTERO (145 G) servido por HTTP. El log
de accesos dice que las unicas rutas con trafico son `/`, `/descargas` y `/web`, asi que se
replicaron solo los dos subarboles que el sitio usa (1,2 M + 3,8 G) con la MISMA estructura, para
que cada `rewrite` siga siendo el mismo.
· `gioser.net` + `www`: 428 M de estaticos + `/reencuentro`, que necesita PHP. `/hooks/*` NO se
muda: lo atiende `webhook-deploy.py`, que redespliega aura/sigma/summa/brahman — los cuatro
FOSILES del §6.19 — y tiene cero peticiones. Muere con la caja vieja.
· `sergio` dejo de ser `CNAME -> www` y tiene su A propio (apuntando a gioser, sin cambio visible).
Sin eso, mover `www` se lo llevaba puesto: en esa zona casi todo cuelga de `www`.
PHP no entra al corpus y no hace falta: `php-fpm 8.5.10` corre en la instancia `gioser-php` (ADR
0015), supervisado por arje. Control contra el original en los dos sentidos: POST con la trampa
anti-robots da `{"ok":true}` 200 y GET da 405, igual que en gioser.
EL MURO, que es general y no de PHP: para montar en `/work/www/gioser-web`, bwrap crea `/work` y
`/work/www` —que la imagen no trae— y los crea **0700 root**. Adentro somos root y a mano todo
funciona; pero un servicio que BAJA de privilegio (php-fpm a `http`, o cualquier instancia con
`run_as`) no puede ni atravesarlos, y el sintoma es un «File not found» sobre un fichero que ESTA.
Medido con el control que lo separa de un problema de permisos del anfitrion: como `http`, leer un
fichero de la IMAGEN funciona y leer el directorio CONCEDIDO da Permission denied.
`grants_to_args` crea ahora los ancestros que faltan con `--perms 0755 --dir`, y SOLO los que la
imagen no trae: hacerlo sobre `/etc` o `/home` le cambiaria los modos a la imagen. Con test, y
probado rompiendolo a proposito (sale `["/etc","/etc/php"]` en vez de `["/etc/php"]`).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1211,9 +1211,30 @@ fn provision(root: &Path, id: &str, force: bool, dry_run: bool) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Los directorios INTERMEDIOS de un punto de montaje que la imagen no trae, de arriba hacia abajo.
|
||||
///
|
||||
/// No incluye el punto de montaje en sí: ése lo tapa el `--bind` y hereda los modos del anfitrión.
|
||||
/// Sólo devuelve los que faltan en la imagen, porque `--dir` sobre uno existente le cambiaría los
|
||||
/// permisos a la imagen. Ver el comentario largo en `grants_to_args`.
|
||||
fn ancestros_a_crear(dentro: &str, tree: &Path) -> Vec<String> {
|
||||
let mut out = Vec::new();
|
||||
let mut acc = String::new();
|
||||
let partes: Vec<&str> = dentro.trim_matches('/').split('/').collect();
|
||||
// `partes.len() - 1`: el último es el punto de montaje, no un ancestro.
|
||||
for p in partes.iter().take(partes.len().saturating_sub(1)) {
|
||||
if p.is_empty() { continue; }
|
||||
acc.push('/');
|
||||
acc.push_str(p);
|
||||
if !tree.join(acc.trim_start_matches('/')).is_dir() {
|
||||
out.push(acc.clone());
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Traduce las concesiones a argumentos de bwrap. Devuelve también las advertencias que el usuario
|
||||
/// tiene que LEER, no descubrir: el ADR pide que el socket de Wayland se diga en la cara.
|
||||
fn grants_to_args(g: &Grants, destino_xdg: &str, wl: Option<&Path>) -> (Vec<String>, Vec<String>) {
|
||||
fn grants_to_args(g: &Grants, destino_xdg: &str, wl: Option<&Path>, tree: &Path) -> (Vec<String>, Vec<String>) {
|
||||
let mut a: Vec<String> = Vec::new();
|
||||
let mut warn: Vec<String> = Vec::new();
|
||||
// Sin dep de libc: el dueño de /proc/self ES el uid efectivo.
|
||||
@@ -1255,6 +1276,22 @@ fn grants_to_args(g: &Grants, destino_xdg: &str, wl: Option<&Path>) -> (Vec<Stri
|
||||
let host = if let Some(rest) = d.host.strip_prefix('~') {
|
||||
format!("{}{}", std::env::var("HOME").unwrap_or_default(), rest)
|
||||
} else { d.host.clone() };
|
||||
// ── LOS PADRES QUE INVENTA bwrap SON 0700, Y ESO DEJA LA CONCESIÓN FUERA DE ALCANCE ─────
|
||||
// MEDIDO el 2026-09-15 con `php-fpm` en la instancia `gioser-php`: la concesión estaba
|
||||
// montada, visible y con sus modos correctos, y el servicio contestaba «File not found».
|
||||
// La causa no está en el bind sino en el CAMINO: para montar en `/work/www/gioser-web`,
|
||||
// bwrap crea `/work` y `/work/www` —que no existen en la imagen— y los crea **0700 root**.
|
||||
// Adentro somos root, así que a mano todo se lee; pero un servicio que BAJA de privilegio
|
||||
// (php-fpm a `http`, o cualquier instancia con `run_as`) no puede ni atravesarlos. El
|
||||
// síntoma no se parece a la causa: dice que falta el fichero, y el fichero está.
|
||||
//
|
||||
// Se crean explícitamente a 0755 **sólo los que la imagen no trae**: hacerlo sobre un
|
||||
// directorio que ya existe (`/etc`, `/usr`, `/home`) le cambiaría los modos a la imagen,
|
||||
// que es justo lo que no se quiere. Misma familia que el `--perms 1777` del `--tmpfs /tmp`.
|
||||
for ancestro in ancestros_a_crear(&d.inside, tree) {
|
||||
a.push("--perms".into()); a.push("0755".into());
|
||||
a.push("--dir".into()); a.push(ancestro);
|
||||
}
|
||||
a.push(if d.mode == "rw" { "--bind".into() } else { "--ro-bind".into() });
|
||||
a.push(host); a.push(d.inside.clone());
|
||||
}
|
||||
@@ -1498,7 +1535,7 @@ fn run_instance(
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let (grant_args, warns) = grants_to_args(&inst.grants, &runtime_dir, proxy_sock.as_deref());
|
||||
let (grant_args, warns) = grants_to_args(&inst.grants, &runtime_dir, proxy_sock.as_deref(), &tree);
|
||||
|
||||
// El orden importa: `--overlay-src` describe la capa BAJA del `--overlay` que viene justo
|
||||
// después. Y `--clearenv` va primero porque el entorno del host también es una concesión: lo
|
||||
@@ -2742,21 +2779,42 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn sin_concesiones_la_red_queda_fuera_y_con_red_entra_el_resolv_conf() {
|
||||
let (a, w) = grants_to_args(&Grants::default(), "/run/user/0", None);
|
||||
let (a, w) = grants_to_args(&Grants::default(), "/run/user/0", None, Path::new("/nonexistent"));
|
||||
assert!(a.contains(&"--unshare-net".to_string()));
|
||||
assert!(w.is_empty());
|
||||
|
||||
let g = Grants { network: true, ..Default::default() };
|
||||
let (a, _) = grants_to_args(&g, "/run/user/0", None);
|
||||
let (a, _) = grants_to_args(&g, "/run/user/0", None, Path::new("/nonexistent"));
|
||||
assert!(!a.contains(&"--unshare-net".to_string()));
|
||||
// El segundo fallo clásico de la primera corrida, según el ADR.
|
||||
assert!(a.iter().any(|x| x == "/etc/resolv.conf"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn los_padres_que_faltan_se_crean_y_los_de_la_imagen_no_se_tocan() {
|
||||
// El control que importa es el NEGATIVO: si esto devolviera los directorios que la imagen
|
||||
// ya trae, `--dir` les cambiaría los permisos a `/etc` y `/home` de la imagen.
|
||||
let d = tempfile::tempdir().unwrap();
|
||||
let tree = d.path();
|
||||
std::fs::create_dir_all(tree.join("etc")).unwrap();
|
||||
std::fs::create_dir_all(tree.join("home")).unwrap();
|
||||
|
||||
// `/work` y `/work/www` no existen en la imagen ⇒ hay que crearlos, de arriba hacia abajo.
|
||||
assert_eq!(
|
||||
ancestros_a_crear("/work/www/gioser-web", tree),
|
||||
vec!["/work".to_string(), "/work/www".to_string()]
|
||||
);
|
||||
// `/etc` SÍ existe ⇒ sólo el que falta.
|
||||
assert_eq!(ancestros_a_crear("/etc/php/php-fpm.d", tree), vec!["/etc/php".to_string()]);
|
||||
// Montar directamente bajo un directorio de la imagen no necesita crear NADA.
|
||||
assert!(ancestros_a_crear("/home/sergio", tree).is_empty());
|
||||
assert!(ancestros_a_crear("/srv", tree).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn el_socket_de_wayland_avisa_que_es_un_borde_de_privilegio() {
|
||||
let g = Grants { sockets: vec!["wayland".into()], ..Default::default() };
|
||||
let (_, w) = grants_to_args(&g, "/run/user/0", None);
|
||||
let (_, w) = grants_to_args(&g, "/run/user/0", None, Path::new("/nonexistent"));
|
||||
assert_eq!(w.len(), 1);
|
||||
assert!(w[0].contains("CAPTURAR LA PANTALLA"), "el aviso no puede ser tibio: {}", w[0]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user