SSD Nodes Learn 🎉 VPS desde $5.50/mes
Guías Matt ConnorPor Matt Connor

Auditar los comandos ejecutados por usuarios en Linux

El historial del shell no es una auditoría. Compare sudo, registro de sesiones, hooks del shell y reglas auditd execve, y envíe los logs fuera del servidor.

Qué registra realmente los comandos ejecutados por los usuarios en el servidor

Para auditar los comandos que los usuarios ejecutan en el servidor, necesita un registro que el usuario no pueda modificar. El historial del shell no sirve para eso. Es un archivo de conveniencia, propiedad de la cuenta que lo escribió, y cualquiera que pueda escribir en ese shell puede desactivarlo o eliminarlo.

Hay cuatro capas que conservan un registro real, y cada una tiene un coste. sudo escribe una línea por comando en syslog. El registro de E/S de sudo captura una sesión completa de una cuenta. Un hook del shell, como PROMPT_COMMAND, registra lo que escribe un usuario interactivo de bash. El subsistema de auditoría del kernel registra la llamada al sistema execve, por lo que es la única capa que ve todos los procesos. Esta guía recorre esas capas, explica dónde termina cada una y termina con el aspecto que determina si todo esto sirve de algo: sacar los registros de la máquina antes de que la persona auditada pueda acceder a ellos.

Una advertencia antes de empezar. El subsistema de auditoría funciona en el kernel, por lo que no se puede probar dentro de un contenedor que comparta el kernel del host. Ejecute estos comandos en una VPS de KVM cuyo kernel controle usted.

Por qué el historial del shell no es un registro de auditoría

~/.bash_history no sirve como evidencia por cuatro motivos habituales, y ninguno requiere un atacante especialmente hábil.

Pertenece al usuario. El archivo tiene permisos 600 y pertenece a esa cuenta, por lo que rm ~/.bash_history no necesita ningún privilegio. Tampoco hace falta abrirlo en un editor y eliminar las veinte líneas relevantes.

Se escribe cuando el shell termina. Una sesión que finaliza con kill -9 $$ o con una conexión interrumpida no escribe nada. Ejecutar history -c antes de exit produce el mismo efecto y parece que no ocurrió nada.

Se desactiva con una sola palabra. unset HISTFILE impide que se escriba el archivo durante esa sesión. set +o history detiene el registro inmediatamente. HISTCONTROL=ignorespace oculta todos los comandos escritos con un espacio inicial. Todo esto se configura en man bash, porque está diseñado para quedar bajo el control del usuario.

Registra lo que se escribió, no lo que se ejecutó. Un alias o una función del shell hacen que el texto del archivo no sea el programa que ejecutó el kernel.

Tampoco hay marcas de tiempo, salvo que HISTTIMEFORMAT estuviera definido cuando se escribió la entrada, porque bash sólo escribe sus líneas de marcador #1755043200 cuando esa variable está definida.

En un inicio de sesión compartido tampoco puede indicar quién ejecutó la acción. Tres personas que usan una cuenta deploy generan un único archivo intercalado bajo un solo uid. Ninguna capa de registro puede atribuir una acción a una persona cuando dos personas comparten un uid. Este es el argumento práctico para usar una cuenta sin privilegios por persona en lugar de un inicio de sesión compartido.

El historial del shell es útil para su propósito real: volver a escribir el comando de ayer. Úselo como indicio. Nunca lo presente como prueba.

Qué registra sudo y dónde termina su trazabilidad

sudo envía una línea por cada comando que ejecuta a la instalación de syslog authpriv.

sudo grep 'sudo:' /var/log/auth.log | tail -5
journalctl -t sudo -n 5

Cada línea indica el usuario, el terminal, el directorio de trabajo, el usuario de destino y el comando:

sudo:    alice : TTY=pts/0 ; PWD=/home/alice ; USER=root ; COMMAND=/usr/bin/apt update

Si no existe /var/log/auth.log, rsyslog no está instalado en esa imagen y los mismos registros sólo están disponibles en el journal. Compruebe que el journal no sea volátil antes de depender de él:

journalctl --list-boots

Que sólo aparezca el arranque actual significa que /var/log/journal no existe. Por tanto, el journal reside en /run y todas las líneas desaparecen en el siguiente reinicio. Hágalo persistente:

sudo mkdir -p /var/log/journal
sudo systemd-tmpfiles --create --prefix /var/log/journal
sudo systemctl restart systemd-journald

Ahora, el límite. sudo registra el comando que se le pidió ejecutar. No registra lo que ese comando hace después. Por tanto, una línea termina la trazabilidad:

sudo -i

El registro contiene un único registro para el shell. Todos los comandos escritos dentro de ese shell de root son invisibles para sudo, porque sudo ya no está en la ruta de ejecución. sudo su -, sudo bash y sudo vim /etc/shadow seguidos de :!bash tienen la misma estructura. Una regla de sudoers que permita cualquier programa con una salida al shell, como vim o find, es una regla que concede acceso a root sin registro. Compruebe a qué puede acceder realmente una cuenta antes de confiar en sus líneas de registro:

sudo -l -U alice

Registrar una sesión completa para una cuenta

Primero averigüe qué implementación de sudo tiene, porque esta función no existe en la reescritura en Rust:

sudo --version | head -1

Si la salida indica sudo-rs, omita esta sección y use el subsistema de auditoría. La documentación de Ubuntu para las versiones 25.10 y 26.04 indica que el registro de E/S y sudoreplay no son compatibles, y esto seguía siendo así en agosto de 2026. Es importante porque sudo-rs es el sudo predeterminado en esas versiones, por lo que una actualización puede eliminar un control que creía tener. La lista completa de cambios de comportamiento de sudo-rs merece una lectura antes de planificar cualquier registro basado en sudo.

Con el sudo original, que Ubuntu 24.04 LTS todavía incluye, active el registro de E/S para una cuenta:

sudo visudo -f /etc/sudoers.d/iolog
Defaults:deploy log_output
Defaults!/usr/bin/sudoreplay !log_output

Use visudo en lugar de un editor, porque no permite guardar un archivo con errores de sintaxis. Un archivo sudoers defectuoso impide que todos usen sudo. Después, reproduzca una sesión:

sudo sudoreplay -l user=deploy
sudo sudoreplay 000001

sudoreplay -l muestra las sesiones con sus identificadores y no imprime nada si log_output nunca se aplicó a ese usuario. El coste es el siguiente: cada byte que atraviesa el terminal se almacena en /var/log/sudo-io, por lo que una sesión con mucha salida ocupa bastante espacio. La segunda línea de sudoers impide que una reproducción se registre a sí misma. El coste real son los secretos, porque un registro de E/S conserva todo lo que se escribió y se mostró, incluida una contraseña introducida en un indicador dentro de la sesión, por lo que necesita la misma protección que un almacén de contraseñas. La cobertura también es limitada. El registro sólo ve los comandos ejecutados mediante sudo. Si alguien inicia sesión y trabaja siempre con su propia cuenta, no se registra en absoluto.

Hooks del shell y cómo se eluden exactamente

La receta que circula para «registrar cada comando» consiste en añadir un hook PROMPT_COMMAND en /etc/profile.d/:

# /etc/profile.d/00-cmdlog.sh
PROMPT_COMMAND='logger -p local6.info -t cmdlog "$(whoami) $$ $(history 1 | sed "s/^ *[0-9]* *//")"'

bash ejecuta PROMPT_COMMAND antes de mostrar cada prompt, por lo que la línea llega a syslog mientras se escribe y no al salir. Además, logger escribe a través del daemon de registro del sistema, por lo que los permisos del propio archivo del usuario no intervienen. Abra un nuevo shell de inicio de sesión y compruébelo con sudo tail -f /var/log/syslog, o con journalctl -t cmdlog -f en una imagen que no tenga rsyslog.

Después deja de funcionar de cinco formas que puede reproducir en un minuto.

  • Los shells no interactivos nunca muestran un prompt. ssh you@server 'id' ejecuta el comando y termina, y no se registra nada porque PROMPT_COMMAND nunca se evaluó.
  • Es una variable. unset PROMPT_COMMAND la desactiva durante el resto de la sesión y no requiere privilegios.
  • El archivo se lee mediante los shells de inicio de sesión. bash --noprofile --norc no obtiene el contenido de /etc/profile.d/ en ningún momento.
  • Es específico de bash. zsh, sh, python3 -c 'import os; os.system("id")' y :!id dentro de vim ejecutan programas que ningún hook del prompt de bash verá.
  • Registra la línea tal como se escribió, por lo que un alias o una función sigue ocultando el comando que se ejecutó realmente.

Use un hook del shell como comodidad. Sirve para responder «qué ejecuté el martes pasado» cuando los usuarios colaboran. No permita que una lista de comprobación lo presente como un control.

The kernel audit subsystem sees every execve

The Linux audit subsystem, driven by the auditd daemon, is the only layer here that a user cannot step around, because the record is made inside the kernel at the moment the syscall runs. If a process executes a program, there is an event. The shell, the language and the presence of a terminal make no difference.

sudo apt update && sudo apt install -y auditd audispd-plugins
sudo systemctl enable --now auditd
sudo auditctl -s

auditctl -s prints the daemon state. enabled 1 with a non-zero pid means it is running, and lost 0 means no records have been dropped yet. Remember that lost counter, it comes back later.

auid is the field that makes audit worth the trouble. PAM sets a login uid when a session starts, and the kernel carries it on every child process from then on. Check yours:

cat /proc/self/loginuid

An interactive SSH session prints your uid, because /etc/pam.d/sshd includes pam_loginuid.so. A value of 4294967295 means the loginuid was never set, which is normal for a process started by a system daemon at boot. The important part is that sudo -i does not change it: a root shell opened by alice still carries auid 1000, so every command inside it is attributable to alice. That is exactly the gap sudo leaves open. Changing a loginuid once set needs CAP_AUDIT_CONTROL, which ordinary users do not have, and sudo auditctl --loginuid-immutable closes it for root as well until the next reboot.

Check that /etc/pam.d/sshd, /etc/pam.d/login and /etc/pam.d/cron each include pam_loginuid.so, or events will arrive with nobody attached to them. That is the same file list you touch when hardening SSH access on a VPS, so do the two jobs together.

Un conjunto inicial de reglas para auditd

Las reglas se almacenan en /etc/audit/rules.d/*.rules. augenrules las concatena en un único listado, ordenadas por nombre de archivo, y el orden determina el comportamiento porque el kernel se detiene en la primera regla coincidente. Lea primero lo que ya existe antes de añadir nada, porque un -D en un archivo posterior elimina todo lo cargado anteriormente.

ls /etc/audit/rules.d/
cat /etc/audit/rules.d/audit.rules

Después, escriba /etc/audit/rules.d/50-exec.rules:

## Suppressions first: the kernel takes the first matching rule.
-a never,exit -F arch=b64 -S execve -F exe=/usr/bin/dpkg
-a never,exit -F arch=b64 -S execve -F exe=/usr/bin/dpkg-deb
-a never,exit -F arch=b64 -S execve -F exe=/usr/bin/dpkg-query
-a never,exit -F arch=b64 -S execve -F exe=/usr/bin/dpkg-split
-a never,exit -F arch=b64 -S execve -F exe=/usr/bin/dpkg-trigger

## Every program started by a logged-in human.
-a always,exit -F arch=b64 -S execve,execveat -F auid>=1000 -F auid!=unset -k exec
-a always,exit -F arch=b32 -S execve,execveat -F auid>=1000 -F auid!=unset -k exec

## Changes to who may become root.
-w /etc/sudoers -p wa -k sudoers
-w /etc/sudoers.d/ -p wa -k sudoers
-w /etc/passwd -p wa -k identity
-w /etc/shadow -p wa -k identity
-w /etc/group -p wa -k identity

## Changes to the audit configuration itself.
-w /etc/audit/ -p wa -k auditconfig

Cárguelo y confirme:

sudo augenrules --load
sudo auditctl -l

Que auditctl -l imprima de nuevo las reglas significa que están activas. No rules indica que la carga ha fallado, y journalctl -u auditd -n 20 identifica el archivo y la línea que el analizador rechazó. Las versiones antiguas del espacio de usuario de audit no entienden la palabra clave unset. Si el cargador se queja de ese campo, escriba -F auid!=4294967295, que expresa el mismo valor de forma explícita.

Ahora lea los eventos:

sudo ausearch -k exec -ts recent -i | tail -40
sudo ausearch -ul 1000 -ts today -i
sudo aureport -k --summary -i

-i convierte los UID y los números de syscall en nombres, y en la práctica es imprescindible. -ts recent cubre los últimos diez minutos. Cada ejecución llega como un grupo de registros: un registro SYSCALL que contiene uid, auid, el estado de salida y la clave; un registro EXECVE con la lista completa de argumentos; además de los registros CWD y PATH para el contexto.

Hay un límite importante que suele causar problemas. audit registra las syscalls, pero un builtin del shell no realiza ninguna syscall propia. cd /root no ejecuta ningún programa. echo evil >> /etc/passwd escrito en un prompt de bash tampoco ejecuta ningún programa, porque tanto echo como la redirección se realizan dentro del proceso del shell que ya está en ejecución. Por tanto, las reglas execve detectan los programas y las reglas -w detectan las escrituras. Ninguna de las dos es suficiente por sí sola.

Por último, bloquee la configuración:

## /etc/audit/rules.d/99-finalize.rules
-e 2

-e 2 hace que el conjunto de reglas sea inmutable hasta el siguiente reinicio. Después de cargarlo, auditctl -s informa de enabled 2, y cualquier intento de añadir o eliminar una regla falla con Operation not permitted, incluido root. Añada este archivo en último lugar y espere tener que reiniciar cada vez que quiera cambiar una regla. Ese es el objetivo de esta medida: un conjunto de reglas que cualquiera pueda desactivar discretamente no sirve como evidencia.

Un registro de auditoría que nadie lee es un artefacto de cumplimiento

El problema de auditd no es que omita eventos. El problema es que registra tanto que nadie lo revisa, y el registro termina existiendo para satisfacer una lista de comprobación en lugar de responder a una pregunta.

Haga los cálculos en su propio equipo antes de ajustar nada:

sudo aureport -k --summary -i
sudo du -sh /var/log/audit

Un solo sudo apt upgrade ejecuta miles de procesos de corta duración, y cada uno conserva su auid. Por eso, una actualización de paquetes puede generar más registros que una semana de actividad humana. Esa es la razón por la que las exclusiones anteriores mencionan dpkg y sus auxiliares. Excluya por ejecutable, nunca por usuario: una exclusión para /usr/bin/dpkg es un vacío que puede describir en una frase, mientras que una exclusión para una cuenta tiene exactamente la forma de aquello que intentaba detectar.

La clave -k de cada regla permite buscar en el registro un mes después. ausearch -k sudoers es una pregunta con respuesta. ausearch sin ningún filtro es un bloque de texto que le enseña a dejar de leer. Si su recopilador necesita JSON en lugar del formato nativo, laurel es un complemento de auditd que reescribe cada evento como un único objeto JSON con los argumentos decodificados. Se registra en /etc/audit/plugins.d/ como cualquier otro complemento, y auditd aplica los cambios de los complementos en sudo pkill -HUP auditd.

El coste real de auditd

Cada llamada al sistema que coincide se convierte en un registro que el kernel formatea y entrega al espacio de usuario. El coste aparece en dos puntos y ambos se pueden medir con su propia carga de trabajo, en lugar de intentar deducirlo a partir de una cifra publicada por otra persona.

  • CPU y latencia. Una máquina que crea procesos constantemente, un host de compilación o un ejecutor de CI genera un registro por cada ejecución. Cuando se llena la cola de espera del kernel, --backlog_wait_time hace que el kernel pause el proceso que generó el evento hasta que haya espacio. Por eso audit puede manifestarse como compilaciones lentas y no como un porcentaje de CPU. Supervise backlog y lost en sudo auditctl -s con carga real. Un lost creciente indica que se han descartado registros. Un registro con interrupciones silenciosas es peor que no tener registro, porque seguirá confiando en él.
  • Disco. Lea /etc/audit/auditd.conf y decida explícitamente qué debe ocurrir cuando se llene el disco, porque los valores incluidos de forma predeterminada son decisiones de diseño. max_log_file, num_logs y max_log_file_action controlan la rotación. space_left_action, admin_space_left_action y disk_full_action controlan la situación de emergencia. Algunas de las acciones disponibles, entre ellas halt y single, apagan la máquina en lugar de perder un registro.

La línea -f de /etc/audit/rules.d/audit.rules representa esa misma decisión en el nivel del kernel: -f 1 informa de un fallo de auditoría a syslog y -f 2 provoca un panic del kernel. Elija 2 sólo si realmente prefiere perder el servidor antes que perder un registro. En una VPS que ejecuta un servicio del que dependen otras personas, use la rotación y traslade el problema de almacenamiento fuera del equipo.

Enviar los registros fuera del servidor casi en tiempo real

Esta es la parte que los informes de incidentes siguen demostrando. Los registros que permanecen en el host comprometido pueden ser modificados por quien lo comprometió. root puede reescribir /var/log/auth.log, eliminar /var/log/audit/audit.log y detener el daemon. -e 2 impide descargar las reglas. No hace nada respecto a rm. Todas las capas superiores sólo producen evidencias si primero sale una copia de la máquina.

El transporte propio de Audit es el complemento audisp-remote de audispd-plugins. Actívelo en /etc/audit/plugins.d/au-remote.conf:

active = yes
direction = out
path = /usr/sbin/audisp-remote
type = always
format = string

Compruebe path frente a command -v audisp-remote antes de recargar, porque una ruta incorrecta no produce nada salvo una línea en el journal. Configure remote_server y port en /etc/audit/audisp-remote.conf, y en el colector configure tcp_listen_port = 60 en su propio auditd.conf. Recargue con sudo pkill -HUP auditd. En muchas imágenes se rechaza systemctl restart auditd porque el archivo de unidad establece RefuseManualStop=yes, por lo que la señal es el método fiable.

La otra opción incorpora Audit al flujo de syslog que ya reenvía. /etc/audit/plugins.d/syslog.conf se distribuye con active = no. Configúrelo como yes, recargue y los eventos de Audit se unirán a las líneas de sudo y al resto de los registros. Después, reenvíe todo con rsyslog mediante TLS (seguridad de la capa de transporte), que necesita el paquete rsyslog-gnutls:

# /etc/rsyslog.d/60-forward.conf
*.* action(type="omfwd"
    target="logs.example.net" port="6514" protocol="tcp"
    StreamDriver="gtls" StreamDriverMode="1"
    StreamDriverAuthMode="x509/name"
    StreamDriverPermittedPeers="logs.example.net"
    action.resumeRetryCount="-1"
    queue.type="linkedList" queue.filename="fwd" queue.saveOnShutdown="on")

La parte importante son los ajustes de la cola. action.resumeRetryCount="-1" reintenta indefinidamente, y la cola asistida por disco con queue.saveOnShutdown="on" conserva los registros mientras el colector no está disponible y los envía cuando vuelve a estarlo. Sin esas dos opciones, un reinicio del colector deja un vacío en las evidencias y no hay nada que indique que existe. Aplique los cambios con sudo systemctl restart rsyslog y confirme que los registros llegan realmente al colector antes de confiar en esta configuración.

Queda un bucle por cerrar: el colector debe ser una máquina en la que las personas auditadas no puedan iniciar sesión. Si el mismo grupo de administradores tiene root en el servidor de registros, ha copiado el archivo, pero no lo ha protegido. Use credenciales separadas, claves separadas e, idealmente, una cuenta de proveedor separada. Este es el mismo razonamiento que hace que un método centralizado para administrar muchos servidores Linux merezca la pena configurarlo antes de necesitarlo, y es la diferencia entre una primera hora útil y una inútil cuando está trabajando con un VPS comprometido.

Compruebe que un usuario normal no pueda reescribir el registro

Compruebe la afirmación en lugar de darla por supuesta. Desde una cuenta normal, sin sudo:

echo test >> /var/log/auth.log
cat /var/log/audit/audit.log
auditctl -D
id -nG

Espere, en este orden: Permission denied, porque auth.log pertenece a syslog, tiene el grupo adm y el modo 640; Permission denied de nuevo, porque el registro de auditoría tiene el modo 600 y pertenece a root; un error que impida la ejecución, porque cambiar las reglas de auditoría requiere CAP_AUDIT_CONTROL; y una lista de grupos que no contenga adm ni systemd-journal.

Esta última comprobación es la que suele fallar. Pertenecer a adm concede acceso de lectura a /var/log/auth.log, y pertenecer a systemd-journal concede acceso de lectura al journal completo. Ninguno concede acceso de escritura, por lo que ninguno permite manipularlo. Ambos permiten leer todas las líneas de autenticación del equipo. Es una decisión que debe tomarse de forma explícita, no copiando una línea usermod -aG de la respuesta de un foro.

Por último, confirme las dos condiciones que deben mantenerse después de reiniciar:

sudo auditctl -s
systemctl is-enabled auditd

enabled 2 significa que el conjunto de reglas queda bloqueado hasta el siguiente arranque. enabled, en la segunda orden, significa que auditd vuelve a iniciarse después de ese arranque. Un conjunto de reglas que sólo dura hasta la siguiente actualización del kernel tampoco es un registro de auditoría.

FAQ

¿Cómo puedo ver todos los comandos que ejecutó un usuario específico?

Busque su uid con id -u alice y, después, busque en el registro de auditoría por uid de inicio de sesión: sudo ausearch -ul 1000 -ts today -i. Añada -k exec para limitar la búsqueda a la regla execve. El uid de inicio de sesión se establece al iniciar sesión y se conserva durante su y sudo -i, por lo que esto también detecta los comandos ejecutados dentro de un shell de root que abrió esa cuenta. Sólo funciona con los comandos ejecutados después de cargar las reglas, porque audit no conserva un historial de los eventos que no estaba configurado para registrar. sudo aureport -k --summary -i muestra los recuentos por regla si primero quiere conocer la distribución de los datos.

¿Puede un usuario eliminar su historial de bash para ocultar lo que ejecutó?

Sí, y no necesita privilegios. ~/.bash_history pertenece a ese usuario y tiene el modo 600, por lo que puede editarlo, truncarlo o eliminarlo. También puede impedir que se escriba mediante unset HISTFILE, detener el registro durante la sesión con set +o history u ocultar comandos individuales escribiéndolos con un espacio inicial cuando HISTCONTROL=ignorespace está establecido. Bash escribe el archivo cuando el shell termina, por lo que una sesión finalizada con kill -9 $$ no registra nada. Considere el historial del shell como una pista, nunca como una prueba.

¿Registra sudo lo que ocurre dentro de sudo -i?

No. sudo registra el comando que se le pidió ejecutar, por lo que sudo -i genera una línea para el shell y nada más después de ella. Todos los comandos escritos en ese shell de root son invisibles para sudo, porque sudo ya no interviene. sudo su -, sudo bash y cualquier programa permitido que ofrezca una salida al shell se comportan de la misma forma. Dos medidas cierran esta brecha: reglas de audit sobre execve, que registran cada programa con el uid de inicio de sesión original asociado, y reglas de sudoers que no conceden un shell desde el principio.

¿Ralentizará auditd mi servidor?

Depende por completo de cuántos procesos inicie la carga de trabajo, así que mídalo en lugar de confiar en una cifra. Un servidor que principalmente responde a solicitudes ejecuta muy pocos procesos y no notará ningún efecto. Un host de compilación o un runner de CI ejecuta procesos constantemente y puede notarlo mucho, porque cuando se llena la cola de auditoría del kernel, el proceso que generó el evento se pausa hasta que haya espacio. Ejecute sudo auditctl -s con carga real y supervise backlog y lost. Cualquier lost superior a cero significa que se descartaron registros. Es el peor resultado, porque el registro contiene brechas invisibles.

¿Dónde deben almacenarse los registros de auditoría?

En otra máquina, con un retraso medido en segundos. Cualquiera que obtenga root en el host auditado puede eliminar /var/log/audit/audit.log y modificar /var/log/auth.log, por lo que las copias locales sólo permiten responder preguntas sobre incidentes que nadie intentó ocultar. Reenvíe los registros con el plugin audisp-remote a un auditd centralizado, o habilite el plugin de syslog de audit y reenvíe todo el flujo de syslog con rsyslog mediante TLS. Asigne al recopilador sus propias credenciales y asegúrese de que las cuentas auditadas no tengan acceso a él.