Módulo 4: Permisos, procesos y entorno

8. Proyecto: clínica de un entorno roto

Descripción

Al terminar este proyecto vas a poder diagnosticar y reparar, con evidencia y no con superstición, tres fallas que cualquier desarrollador se topa en su primera semana en un equipo real: un comando que existe en el disco pero que tu shell no encuentra, un script que se niega a correr junto con un directorio que se niega a dejarte entrar, y un puerto que alguien —probablemente tú, hace veinte minutos— dejó ocupado con un proceso en segundo plano. El entregable no es solo "que ya funcione": es una bitácora de los tres incidentes, con el mismo formato que usa un equipo de operaciones real —síntoma, hipótesis, comando de diagnóstico, causa raíz, arreglo y verificación.

Este es exactamente el ritual del primer día en cualquier equipo. Te entregan una laptop, clonas un repositorio, y algo —casi siempre algo pequeño— no funciona a la primera. Un compañero con experiencia no reinstala el sistema operativo ni copia un sudo chmod 777 de un foro que encontró a las dos de la mañana: abre una terminal, aísla la variable que falla y lo arregla en minutos porque sabe exactamente qué preguntarle al sistema. Esa diferencia —entre adivinar y diagnosticar— es la que separa a alguien a quien el equipo puede poner frente a un servidor de producción sin supervisión de alguien que todavía no.

Conexión con el módulo: cada lección anterior de este módulo te dio una pieza suelta —usuarios y grupos, permisos y chmod, cuándo sudo es la herramienta correcta y cuándo es el síntoma de otro error, procesos y señales, el PATH, y los archivos rc que hacen persistente un cambio. Hoy no aprendes ninguna pieza nueva: combinas las seis contra tres fallas reales, reproducibles, que vas a provocar tú mismo y luego reparar con el mismo rigor con el que las repararías en un servidor que no es tuyo.

Antes de tocar nada: el runbook de un incidente

Un médico frente a un síntoma —fiebre, dolor— no receta el primer tratamiento que se le ocurre. Separa el síntoma (lo que el paciente reporta) de la hipótesis (su sospecha inicial), pide un examen que confirme o descarte esa hipótesis, y solo entonces trata la causa real, no el síntoma. Un ingeniero frente a un Permission denied o un command not found tiene exactamente el mismo trabajo por delante, y la tentación exactamente opuesta: probar sudo, luego chmod 777, luego reiniciar la laptop, hasta que algo "funcione" sin haber entendido nunca qué estaba roto.

La alternativa profesional es un runbook: una plantilla fija que llenas para cada incidente, en este orden, sin saltarte pasos.

CampoQué responde
SíntomaQué ves literalmente en la pantalla, palabra por palabra.
HipótesisTu mejor sospecha de la causa, antes de confirmarla con un comando.
DiagnósticoEl comando exacto que corriste para confirmar o descartar la hipótesis, y su salida real.
Causa raízQué está pasando de verdad —no el síntoma, la razón detrás de él.
ArregloEl comando que resuelve la causa raíz, no el que silencia el síntoma.
VerificaciónCómo confirmas, con un comando, que quedó resuelto.
PersistenciaSi el arreglo sobrevive a cerrar la terminal, o si necesita quedar guardado en tu archivo rc para no repetirlo cada sesión.

Vas a llenar esta plantilla tres veces —una por incidente— y al final vas a tener la bitácora completa del proyecto.

Incidente 1: el comando fantasma

Provoca la falla instalando una herramienta propia en una carpeta que tú controlas, algo que vas a repetir muchas veces en tu carrera real:

mkdir -p ~/bin
cat > ~/bin/greet <<'EOF'
#!/usr/bin/env bash
# Saluda usando el usuario real del sistema
echo "Hola, $(whoami). Todo listo."
EOF
chmod +x ~/bin/greet

Ejemplo trabajado

Síntoma. Escribes el nombre del comando que acabas de instalar y la shell te dice que no existe:

$ greet
zsh: command not found: greet

Hipótesis. Tu primera sospecha, razonable, es que el archivo no se creó bien o que le falta permiso de ejecución.

Diagnóstico.

$ ls -l ~/bin/greet
-rwxr-xr-x 1 ana staff 96 Jul 21 09:40 /Users/ana/bin/greet

Las tres x (una por cada clase: propietario, grupo, otros) descartan la hipótesis: el archivo existe y sí tiene permiso de ejecución. El problema es otro. Confirmas con el comando que resuelve exactamente esta pregunta:

$ echo $PATH
/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin

$ which greet
$ echo $?
1

which no imprimió nada, y el código de salida 1 confirma que ninguna carpeta de $PATH contiene ese ejecutable.

Causa raíz. El archivo existe y tiene permiso de ejecución, pero vive en ~/bin, una carpeta que no aparece en ningún punto de $PATH. La shell resuelve un comando sin barras buscando únicamente en esas carpetas, en ese orden exacto — no busca en todo el disco, ni "adivina" dónde podría estar algo que instalaste.

Arreglo (temporal, dura solo esta sesión).

$ export PATH="$HOME/bin:$PATH"
$ greet
Hola, ana. Todo listo.

Persistencia. Un cambio de PATH hecho con export a mano vive solo en la memoria de esta shell — desaparece en cuanto cierras la terminal. Igual que aprendiste en la lección anterior, lo dejas escrito en el archivo rc correcto:

echo 'export PATH="$HOME/bin:$PATH"' >> ~/.zshrc
source ~/.zshrc

Verificación. Abres una pestaña nueva (o simplemente confirmas después de source) y corres which greet — ahora sí devuelve /Users/ana/bin/greet, sin que tuvieras que exportar nada a mano.

Incidente 2: el permiso que falta (script y directorio)

Este incidente tiene una trampa: dos causas raíz completamente distintas producen el mismo mensaje superficial, Permission denied. Provócalo con un script de despliegue guardado en una carpeta a la que, sin querer, le quitaste el permiso de entrar:

mkdir -p ~/deploy_tools
cat > ~/deploy_tools/deploy.sh <<'EOF'
#!/usr/bin/env bash
# Simula un paso de despliegue
echo "Desplegando la aplicación..."
EOF
chmod 600 ~/deploy_tools

Esa última línea simula un error real y frecuente: alguien, tratando de "asegurar" la carpeta de despliegue, le quitó el bit de ejecución al directorio sin darse cuenta de la consecuencia.

Ejemplo trabajado

Síntoma (primera falla).

$ cd ~/deploy_tools
cd: permission denied: /Users/ana/deploy_tools

Hipótesis. Que la carpeta no existe, o que se corrompió.

Diagnóstico.

$ ls -ld ~/deploy_tools
drw------- 2 ana staff 64 Jul 21 09:52 /Users/ana/deploy_tools

Causa raíz. Descarta la hipótesis: la carpeta existe perfectamente. Lo que falta es el bit x del propietario. En un directorio, x no significa "ejecutar" como en un archivo — significa atravesar: sin él no puedes entrar con cd, ni acceder a los metadatos de lo que hay adentro, aunque tengas permiso de lectura (r) sobre la carpeta misma.

Arreglo.

$ chmod u+x ~/deploy_tools
$ ls -ld ~/deploy_tools
drwx------ 2 ana staff 64 Jul 21 09:52 /Users/ana/deploy_tools
$ cd ~/deploy_tools

Con el directorio abierto, intentas correr el script y aparece la segunda falla, con un mensaje casi idéntico al anterior:

$ ./deploy.sh
zsh: permission denied: ./deploy.sh

Diagnóstico.

$ ls -l deploy.sh
-rw-r--r-- 1 ana staff 52 Jul 21 09:51 deploy.sh

Causa raíz (distinta a la anterior). Ahora es el archivo, no el directorio, al que le falta su propio bit x. El de deploy_tools que acabas de arreglar solo controlaba si podías entrar a la carpeta — nunca controló si el archivo de adentro era ejecutable. Son dos permisos completamente independientes, aunque el mensaje de error luzca igual.

Arreglo.

$ chmod u+x deploy.sh
$ ./deploy.sh
Desplegando la aplicación...

Verificación. ls -l deploy.sh ahora muestra -rwxr--r--, con la x del propietario presente, y el script corre sin error.

Persistencia. Este incidente es distinto al anterior: los bits de permiso ya son persistentes por sí solos —viven en el disco, no en la memoria de la shell— así que no hay nada que agregar a un archivo rc. Lo que sí conviene dejar por escrito es la causa en la bitácora del equipo, para que nadie repita el error de "asegurar" una carpeta quitándole el bit de tránsito. Y si deploy.sh vive en un repositorio de git, vale la pena confirmar que el permiso de ejecución quedó registrado en el propio repositorio con git ls-files -s deploy.sh (debería mostrar el modo 100755, no 100644) — así, cuando un compañero clone el proyecto, el script llega ya ejecutable, sin que tenga que descubrir este mismo incidente por su cuenta.

Incidente 3: el puerto ocupado

Provoca la falla lanzando un servidor de prueba en segundo plano, algo que harás constantemente al desarrollar:

$ python3 -m http.server 8000 &
[1] 41213
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...

Veinte minutos después, ya olvidado ese servidor, intentas levantar otro en el mismo puerto:

$ python3 -m http.server 8000
Traceback (most recent call last):
  ...
OSError: [Errno 48] Address already in use

(En Linux el mismo error aparece como [Errno 98] en vez de [Errno 48] — el número de errno cambia entre macOS/BSD y Linux, pero el mensaje y la causa son idénticos.)

Ejemplo trabajado

Síntoma. El segundo servidor no arranca; Python termina con un OSError señalando que la dirección ya está en uso.

Hipótesis. Un proceso previo sigue vivo y sigue escuchando en el puerto 8000.

Diagnóstico.

$ lsof -i :8000
COMMAND   PID USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
Python  41213  ana    3u  IPv4 0x5f3a9c8b2b1e4a2f      0t0  TCP *:8000 (LISTEN)

Causa raíz. El PID 41213 es el mismo número que la shell imprimió como job [1] cuando lanzaste el primer servidor con &. Ese proceso nunca se cerró — quedó vivo en segundo plano, invisible en cuanto dejaste de mirarlo, reteniendo el puerto.

Arreglo.

$ kill 41213
[1]  + terminated  python3 -m http.server 8000
$ lsof -i :8000
$

lsof -i :8000 ya no imprime nada: el puerto quedó libre. kill, sin ningún número de señal, envía por defecto SIGTERM (15) — le pide al proceso que se cierre en orden, dándole la oportunidad de liberar sus recursos correctamente. Si un proceso no responde a SIGTERM (por ejemplo, porque quedó colgado o ignora la señal), el último recurso es kill -9 41213, que envía SIGKILL: el kernel termina el proceso de inmediato, sin darle ninguna oportunidad de cerrar en orden. Por eso -9 es el último paso, no el primero — puedes dejar archivos temporales o conexiones a medio cerrar si lo usas de entrada.

Verificación. lsof -i :8000 sin salida confirma que nada sigue escuchando ahí; el segundo python3 -m http.server 8000 ahora arranca sin error.

Persistencia. Aquí no hay un valor que guardar en el archivo rc, como en el Incidente 1 — lo que conviene persistir es la herramienta que evita repetir manualmente el diagnóstico completo cada vez que te vuelva a pasar:

# ~/.zshrc (o ~/.bashrc)
killport() {
  local pid
  pid=$(lsof -ti ":$1")
  if [ -n "$pid" ]; then
    kill "$pid"
    echo "Terminado el proceso $pid que ocupaba el puerto $1."
  else
    echo "Nada escuchando en el puerto $1."
  fi
}
$ source ~/.zshrc
$ killport 8000
Nada escuchando en el puerto 8000.

La próxima vez que un puerto quede ocupado, killport 8000 reemplaza los tres comandos que acabas de ejecutar a mano.

La bitácora final del proyecto

Los tres incidentes, resumidos en el formato que entregarías a tu equipo:

#SíntomaCausa raízArregloPersistencia
1command not found: greet~/bin no está en $PATHexport PATH="$HOME/bin:$PATH"Línea agregada a ~/.zshrc
2permission denied al entrar y al ejecutarFalta x en el directorio y, por separado, en el archivochmod u+x ~/deploy_tools y chmod u+x deploy.shYa persistente en el disco; documentado para el equipo
3Address already in use en el puerto 8000Proceso previo en segundo plano nunca se cerrókill 41213 (o kill -9 si no responde)Función killport agregada a ~/.zshrc

Errores comunes

"sudo lo arregla todo" (conceptual). Qué pasa: ante el command not found: greet del Incidente 1, la reacción instintiva es escribir sudo greet, razonando que "sudo" resuelve cualquier problema de permisos. El error persiste, idéntico. Por qué ocurre: sudo eleva el usuario con el que corre un comando — no cambia en absoluto cómo la shell busca ese comando en $PATH. De hecho, muchas configuraciones de sudo usan un secure_path propio, distinto al tuyo, que puede esconder el ejecutable todavía más. El problema nunca fue de privilegios; fue de dónde busca la shell. Cómo detectarlo: si sudo comando sigue devolviendo command not found, ya sabes que el problema no es de permisos de superusuario — nunca lo fue. Cómo corregirlo: antes de escalar privilegios, diagnostica con which, type o echo $PATH si el problema es de resolución de rutas; reserva sudo para cuando el diagnóstico confirme que de verdad es un permiso de sistema el que falta.

"Si chmod no arregla el problema, pruebo con 777" (conceptual). Qué pasa: frente al Incidente 2, en vez de identificar exactamente qué bit falta (x en el directorio, x en el archivo), el reflejo es correr chmod -R 777 sobre toda la carpeta "para que ya no dé más problemas". Y efectivamente deja de dar problemas — ese es justo el riesgo. Por qué ocurre: 777 le da permiso de lectura, escritura y ejecución a cualquier usuario del sistema, no solo al propietario, violando el criterio de menor privilegio que viste al inicio del módulo. En una laptop personal de un solo usuario el costo inmediato parece nulo; en un servidor compartido, cualquier otra cuenta del sistema puede ahora leer, modificar o reemplazar ese script. Cómo detectarlo: ls -l mostrando rwxrwxrwx en un archivo o carpeta que no tiene ninguna razón para ser accesible por todo el mundo. Cómo corregirlo: identifica el bit exacto que falta con ls -l/ls -ld, y aplica el permiso mínimo necesario (chmod u+x, o el octal equivalente) en vez del máximo disponible — la meta nunca es "que deje de fallar", es "que tenga exactamente el acceso que necesita".

Usar kill -9 como primer movimiento (procedimental). Qué pasa: en el Incidente 3, en vez de un kill simple, se escribe directamente kill -9 41213 porque "es más seguro, así se muere sí o sí". Por qué ocurre: se confunde "más agresivo" con "más correcto". SIGKILL (9) no le da al proceso ninguna oportunidad de cerrar archivos abiertos, liberar conexiones de red o guardar estado antes de morir — el kernel lo termina de inmediato, sin negociación. SIGTERM (15), la señal por defecto de kill, sí le permite al proceso atender la señal y cerrar en orden si fue programado para eso. Cómo detectarlo: archivos temporales que quedan a medias, locks que no se liberaron, o un proceso hijo que queda huérfano después de terminar el padre con -9 — señales de que algo no se cerró en orden. Cómo corregirlo: intenta primero kill <PID> sin ningún número (SIGTERM); dale unos segundos; escala a kill -9 <PID> únicamente si el proceso sigue vivo después de eso.

Ejercicios

1. Instalaste una herramienta cuyo ejecutable quedó en /opt/tools/lint-check, con permiso de ejecución ya confirmado (ls -l muestra -rwxr-xr-x). Al escribir lint-check en la terminal, obtienes command not found. Tu $PATH actual es:

/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin

¿Cuál es la causa raíz, y qué dos comandos necesitas —uno para esta sesión, otro para que sobreviva a cerrar la terminal— para resolverlo?

Ver solución

La causa raíz es que /opt/tools no aparece en ningún punto de $PATH, así que la shell nunca considera ese directorio al resolver lint-check, aunque el ejecutable exista ahí con permisos correctos. Para esta sesión: export PATH="/opt/tools:$PATH". Para que sobreviva a cerrar la terminal: agregar esa misma línea al archivo rc correspondiente y recargarlo, por ejemplo echo 'export PATH="/opt/tools:$PATH"' >> ~/.zshrc && source ~/.zshrc. Esto funciona porque la shell busca un comando sin barras únicamente en las carpetas listadas en $PATH, en orden, y un export sin persistir desaparece en cuanto cierras la sesión que lo definió.

2. Un compañero de equipo, que pertenece al mismo grupo staff que tú, no puede entrar a una carpeta compartida que tú sí puedes abrir sin problema. ls -ld sobre esa carpeta muestra:

drwxr----- 2 ana staff 96 Jul 21 10:15 team-notes

¿Qué le falta exactamente a tu compañero, y qué comando lo resuelve sin darle más acceso del necesario?

Ver solución

El bloque de grupo es r--: el grupo staff puede listar los nombres dentro de team-notes (r), pero no puede atravesar el directorio (x ausente) ni escribir en él (w ausente). Como tu compañero entra por la categoría "grupo" (no es el propietario), le falta específicamente el bit de tránsito. El comando mínimo es chmod g+x team-notes — agrega solo el permiso de entrar al grupo, sin tocar la categoría "otros" ni darle permiso de escritura que nadie pidió. Esto funciona porque x en un directorio es un permiso independiente de r: puedes ver los nombres de lo que hay adentro sin poder entrar, y viceversa, así que hay que otorgar exactamente el bit que falta, para la clase exacta a la que pertenece tu compañero.

3. Corres lsof -i :5432 y obtienes:

COMMAND    PID  USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
postgres  8842  ana    7u   IPv4 0x...      0t0   TCP *:5432 (LISTEN)

Necesitas liberar el puerto para levantar tu propia base de datos de prueba. ¿Qué comando escribes primero, y por qué no empiezas directamente con la señal más agresiva disponible?

Ver solución

El primer comando es kill 8842, sin ningún número de señal — eso envía SIGTERM (15) por defecto, la señal que le pide a postgres que se cierre en orden. Una base de datos como PostgreSQL, en particular, mantiene archivos de estado y transacciones a medio confirmar; cerrarla de golpe con kill -9 (SIGKILL) le quita la oportunidad de guardar ese estado correctamente y puede dejar la base en un estado inconsistente que tarda más en repararse que los segundos que ahorrarías. kill -9 8842 queda como último recurso, solo si después de unos segundos lsof -i :5432 sigue mostrando el proceso vivo. Esto funciona porque SIGTERM es una señal que un proceso bien escrito puede capturar y atender antes de morir, mientras que SIGKILL no le da al proceso ninguna oportunidad de reaccionar.

4. Un compañero te escribe en el chat del equipo: "tenía un problema de permisos con una carpeta del proyecto, le hice chmod 777 y ya funciona, quedó resuelto." ¿Qué le responderías, y qué harías en su lugar?

Ver solución

Le respondería que "funciona" y "está resuelto correctamente" no son lo mismo: chmod 777 le da permiso de lectura, escritura y ejecución a cualquier usuario del sistema, no solo a quien realmente necesitaba acceso — en una laptop de un solo usuario el riesgo inmediato es bajo, pero en cualquier máquina compartida (un servidor, un contenedor con varios servicios) cualquier otra cuenta puede ahora leer, modificar o reemplazar esos archivos. En su lugar, correría ls -ld sobre la carpeta para ver exactamente qué bit faltaba y para qué clase (propietario, grupo u otros), y aplicaría el permiso mínimo necesario con chmod simbólico (u+x, g+w, lo que corresponda) en vez del máximo disponible. Esto funciona porque el objetivo nunca es "que deje de fallar" sino "que tenga exactamente el acceso que necesita" — el criterio de menor privilegio que viste al principio del módulo aplica exactamente igual aquí, con chmod, que aplicó con usuarios y con sudo.

Resumen y siguiente paso

Hoy provocaste y reparaste tres fallas reales usando un runbook en vez de superstición: un command not found causado por un PATH mal armado, un Permission denied que en realidad eran dos causas distintas —un directorio sin bit de tránsito y un archivo sin bit de ejecución— disfrazadas del mismo mensaje, y un puerto ocupado por un proceso en segundo plano que localizaste con lsof y terminaste con la señal correcta. Para cada uno llenaste la misma plantilla —síntoma, hipótesis, diagnóstico, causa raíz, arreglo, verificación, persistencia— porque esa disciplina, no el comando específico, es lo que se transfiere de un incidente al siguiente.

Antes de avanzar deberías poder: diagnosticar un command not found distinguiendo si el problema es que el archivo no existe, que le falta permiso de ejecución, o que su carpeta no está en $PATH; explicar por qué el bit x de un directorio y el bit x de un archivo dentro de él son dos permisos independientes, aunque produzcan el mismo mensaje de error; encontrar el proceso dueño de cualquier puerto ocupado con lsof -i y elegir SIGTERM antes que SIGKILL; y dejar cualquier arreglo de entorno persistente en tu archivo rc en vez de repetirlo a mano cada sesión.

Lo que practicaste hoy —leer antes de actuar, diagnosticar antes de "arreglar", y dejar evidencia— es exactamente la disciplina que necesitas para el próximo módulo: configurar acceso SSH sin depender de contraseñas exige entender los permisos de carpetas como ~/.ssh con la misma precisión con la que hoy entendiste ~/deploy_tools, y cualquier automatización real que construyas de aquí en adelante va a fallar tarde o temprano por exactamente una de estas tres causas — un PATH mal armado, un bit de ejecución faltante, o un proceso que quedó vivo ocupando un recurso. Ya sabes cómo diagnosticar las tres.

Recursos