From ff871f4a07a2ac1092ae1193eef60ce252a47b79 Mon Sep 17 00:00:00 2001 From: Sergio Date: Fri, 18 Sep 2026 15:20:55 +0000 Subject: [PATCH] =?UTF-8?q?dar=20de=20alta=20una=20persona:=20casi=20es=20?= =?UTF-8?q?`adduser`,=20y=20lo=20que=20falta=20es=20lo=20que=20tumb=C3=B3?= =?UTF-8?q?=20la=20caja?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Respuesta medida a «¿agregar un usuario es más complicado que un simple adduser?». `adduser` está —es el de busybox— y crea la cuenta. Lo que no alcanza es DÓNDE queda la contraseña: esta caja NO USA /etc/shadow, el hash vive en el campo 2 de /etc/passwd, y el shadow que hubo está aparcado como /etc/shadow.el-que-bloqueo-root. El nombre no es broma: crear un /etc/shadow con las cuentas bloqueadas dejó a sshd sin aceptar ni la clave de root, y la caja se quedó sin administración con los ocho dominios caídos ~20 minutos. Un adduser a secas puede crear ese fichero sin avisar. Así que el guion crea la cuenta, deja el hash donde el login lo lee, y APARTA cualquier /etc/shadow que el alta haya creado donde no había. El control es el que importa: fotografía quién podía entrar ANTES y falla si alguien perdió su contraseña. Y hace lo que hace falta para tener poder de verdad: sudoers.d (el sudo de acá sí es setuid; doas no funciona), rango en /etc/sub{u,g}id con los ayudantes newuidmap/newgidmap en setuid —sin eso un userns anidado no mapea uid 0 y takana build no corre dentro de la jaula—, ssh, y la instancia claude- con `root = false`, porque si no el CLI se ve uid 0 y rechaza --dangerously-skip-permissions. Probado de punta a punta con un usuario desechable y borrado después sin dejar residuo. Co-Authored-By: Claude Opus 5 (1M context) --- scripts/servidor/alta-usuario.sh | 102 +++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100755 scripts/servidor/alta-usuario.sh diff --git a/scripts/servidor/alta-usuario.sh b/scripts/servidor/alta-usuario.sh new file mode 100755 index 00000000..ca7d21ae --- /dev/null +++ b/scripts/servidor/alta-usuario.sh @@ -0,0 +1,102 @@ +#!/bin/sh +# Da de alta una PERSONA en la caja: cuenta, clave, sudo, subids y su jaula de Claude. +# +# ── ¿POR QUÉ NO ALCANZA CON `adduser`? Porque casi alcanza, y lo que falta tumbó la caja ──────── +# `adduser` está (es el de busybox) y crea la cuenta. El problema es **dónde queda la contraseña**: +# esta caja **NO USA `/etc/shadow`** — el hash vive en el campo 2 de `/etc/passwd`, y el `shadow` +# que hubo está aparcado como `/etc/shadow.el-que-bloqueo-root`. El nombre no es broma: al crear un +# `/etc/shadow` con todas las cuentas bloqueadas (`!`), **sshd dejó de aceptar hasta la clave de +# root** y la caja se quedó sin administración con los ocho dominios caídos ~20 min (SDD 28 §6.51). +# Un `adduser` a secas puede crear ese fichero sin avisar. +# +# Entonces el alta correcta acá es: crear la cuenta, dejar el hash donde el login lo lee, y +# **comprobar que quien podía entrar sigue pudiendo** antes de darse por satisfecho. +# +# Lo demás es lo que hace falta para «tener poder sobre la máquina, a mano y con claude»: +# · `/etc/sudoers.d/` — root a mano (el `sudo` de acá SÍ es setuid; `doas` no funciona) +# · `/etc/sub{u,g}id` — sin rango, un userns anidado no puede mapear uid 0 y `takana build` +# no corre dentro de la jaula +# · instancia `claude-` — con `root = false`, o el CLI se ve uid 0 y RECHAZA +# `--dangerously-skip-permissions` +# +# Uso: alta-usuario.sh [clave] (sin clave: la cuenta queda SIN contraseña, sólo ssh) +set -eu + +U=${1:?uso: alta-usuario.sh [clave]} +CLAVE=${2:-} +HOME_U=/home/$U + +[ "$(id -u)" = "0" ] || { echo "esto se corre como root" >&2; exit 1; } + +# Foto de quién puede entrar ANTES de tocar nada: es contra esto que se compara al final. +ANTES=$(awk -F: '$2 != "" && $2 != "!" && $2 != "*" {print $1}' /etc/passwd | sort | tr '\n' ' ') +HABIA_SHADOW=no; [ -f /etc/shadow ] && HABIA_SHADOW=si + +if id "$U" >/dev/null 2>&1; then + echo "==> $U ya existe (uid $(id -u "$U")) — sigo con el resto" +else + echo "==> creando $U" + adduser -D -h "$HOME_U" -s /bin/bash "$U" +fi + +# ── la contraseña, donde el login la lee ──────────────────────────────────────────────────────── +if [ -n "$CLAVE" ]; then + H=$(openssl passwd -6 "$CLAVE" 2>/dev/null || busybox cryptpw -m sha512 "$CLAVE") + # En passwd, campo 2. Se usa `awk` y no `sed` porque el hash trae `/` y `$` a montones. + awk -F: -v u="$U" -v h="$H" 'BEGIN{OFS=":"} $1==u {$2=h} {print}' /etc/passwd > /etc/passwd.nuevo + # Nunca dejar un passwd a medias: se comprueba que el fichero nuevo tenga las mismas cuentas. + [ "$(wc -l < /etc/passwd.nuevo)" -eq "$(wc -l < /etc/passwd)" ] || { echo "passwd nuevo con distinto número de cuentas — aborto" >&2; rm -f /etc/passwd.nuevo; exit 1; } + mv /etc/passwd.nuevo /etc/passwd; chmod 644 /etc/passwd + echo " clave puesta en /etc/passwd (esta caja no usa shadow)" +fi + +# ⚠ GUARDIÁN DEL BLOQUEO: si `adduser` creó un /etc/shadow que no había, cualquiera que tuviera su +# hash en passwd queda con `x` y bloqueado. Es literalmente cómo se perdió la caja. +if [ "$HABIA_SHADOW" = "no" ] && [ -f /etc/shadow ]; then + mv /etc/shadow /etc/shadow.creado-por-el-alta-$(date +%s) + echo " ⚠ adduser creó /etc/shadow donde no había: lo aparté (así se perdió la caja el 2026-09-17)" +fi + +echo "==> poder: sudo, subids" +[ -f "/etc/sudoers.d/$U" ] || { echo "$U ALL=(ALL:ALL) ALL" > "/etc/sudoers.d/$U"; chmod 440 "/etc/sudoers.d/$U"; } +for f in /etc/subuid /etc/subgid; do + grep -q "^$U:" "$f" 2>/dev/null && continue + # Rango propio, detrás del último declarado, sin solaparse. + ult=$(awk -F: '{print $2+$3}' "$f" 2>/dev/null | sort -n | tail -1); : "${ult:=100000}" + echo "$U:$ult:65536" >> "$f" +done +# Sin estos dos setuid, el rango de arriba no se puede aplicar: son los que escriben el uid_map. +for b in /usr/bin/newuidmap /usr/bin/newgidmap; do + [ -u "$b" ] || { chmod 4755 "$b"; echo " setuid puesto en $b"; } +done + +echo "==> ssh" +mkdir -p "$HOME_U/.ssh"; touch "$HOME_U/.ssh/authorized_keys" +chmod 700 "$HOME_U/.ssh"; chmod 600 "$HOME_U/.ssh/authorized_keys" +chown -R "$U:$U" "$HOME_U/.ssh" +[ -s "$HOME_U/.ssh/authorized_keys" ] || echo " (authorized_keys vacío: poné la clave pública o no entra por ssh)" + +echo "==> jaula de claude" +BASE=/var/lib/hammer/qorpa/instances +if [ -d "$BASE/claude" ] && [ ! -d "$BASE/claude-$U" ]; then + mkdir -p "$BASE/claude-$U" + sed -e "s|^root = true$|root = false|" \ + -e "s|/home/sergio|$HOME_U|g" -e "s|/work/sergio|/work/$U|g" \ + "$BASE/claude/instance.toml" > "$BASE/claude-$U/instance.toml" + mkdir -p "/work/$U" + chown -R "$U:$U" "$BASE/claude-$U" "/work/$U" + echo " instancia claude-$U creada (root = false: si no, el CLI se ve uid 0 y rechaza la bandera)" +else + echo " (ya existía, o no hay instancia `claude` de la que copiar)" +fi + +# ── CONTROLES ─────────────────────────────────────────────────────────────────────────────────── +echo "==> control" +id "$U" | sed 's/^/ /' +DESPUES=$(awk -F: '$2 != "" && $2 != "!" && $2 != "*" {print $1}' /etc/passwd | sort | tr '\n' ' ') +perdidos="" +for a in $ANTES; do case " $DESPUES " in *" $a "*) ;; *) perdidos="$perdidos $a";; esac; done +[ -z "$perdidos" ] || { echo " ✗ estas cuentas PERDIERON su contraseña:$perdidos" >&2; exit 1; } +echo " ✓ nadie perdió el acceso que tenía (antes:$ANTES)" +[ -f /etc/shadow ] && echo " ⚠ hay /etc/shadow: revisá que root NO esté bloqueado ahí" || echo " ✓ sin /etc/shadow (como esta caja espera)" +echo "==> $U dado de alta"