Si administras servidores Linux, Bash scripting no es opcional — es una habilidad fundamental. Cada tarea repetitiva que realizas manualmente es candidata a ser automatizada: verificar espacio en disco, rotar logs, crear respaldos, auditar cuentas de usuario, monitorear servicios. Un script de Bash bien escrito ejecuta en segundos lo que te tomaria minutos (u horas) de escribir y hacer clic. Esta guia te ensena Bash scripting desde cero, con enfoque en tareas practicas de sysadmin que puedes poner a trabajar inmediatamente.
Al final de este articulo, sabras como escribir scripts que usan variables, condicionales, bucles y funciones. Entenderas patrones de manejo de errores que previenen que los scripts causen danos cuando algo sale mal. Y tendras cuatro scripts completos, listos para produccion, que resuelven problemas reales de administracion de sistemas.
Por que Bash Scripting para Sysadmins?
Bash es el shell predeterminado en practicamente todas las distribuciones Linux. Viene preinstalado en Ubuntu, Debian, CentOS, RHEL, Fedora, Arch e incluso macOS (aunque macOS cambio a zsh como shell interactivo predeterminado, Bash sigue disponible). Esto significa que tus scripts se ejecutaran en cualquier servidor sin instalar software adicional.
Esto es lo que Bash scripting te ofrece:
- Automatizacion: Convierte cualquier secuencia de comandos de terminal en un script repetible
- Consistencia: Los scripts se ejecutan de la misma manera cada vez, eliminando el error humano
- Velocidad: Un script puede procesar miles de archivos o usuarios en segundos
- Programacion: Combinado con cron, los scripts se ejecutan automaticamente a cualquier intervalo
- Portabilidad: Los scripts de Bash se ejecutan en cualquier sistema Linux sin dependencias
- Auditabilidad: Los scripts sirven como documentacion de exactamente lo que se hizo y cuando
Cuando usar Bash vs. Python: Bash es ideal para tareas que encadenan herramientas de linea de comandos existentes (operaciones con archivos, procesamiento de texto, gestion de servicios). Si tu tarea requiere estructuras de datos complejas, llamadas a APIs o procesamiento de JSON/YAML, considera Python en su lugar.
Requisitos previos
Antes de comenzar, asegurate de tener:
- Un sistema Linux (Ubuntu 22.04 o 24.04 recomendado, pero cualquier distribucion funciona)
- Acceso a terminal con una cuenta de usuario regular que tenga privilegios
sudo - Un editor de texto (
nanopara principiantes,vimo VS Code para usuarios mas avanzados) - Familiaridad basica con la linea de comandos de Linux (navegar directorios, listar archivos, leer contenido de archivos)
Verifica tu version de Bash:
bash --version
Deberias ver la version 4.0 o mas reciente. Ubuntu 22.04 viene con Bash 5.1 y Ubuntu 24.04 con Bash 5.2.
Tu primer script
Todo script de Bash comienza con una linea shebang que le indica al sistema que interprete usar. Crea un archivo llamado hola.sh:
#!/bin/bash
# Mi primer script de Bash
echo "Hola, $(whoami)! Hoy es $(date +%A), $(date +%B\ %d,\ %Y)."
echo "Estas ejecutando Bash version: $BASH_VERSION"
echo "Tu directorio home es: $HOME"
Hazlo ejecutable y ejecutalo:
chmod +x hola.sh
./hola.sh
Salida:
Hola, jc! Hoy es lunes, enero 27, 2026.
Estas ejecutando Bash version: 5.2.21(1)-release
Tu directorio home es: /home/jc
Conceptos clave:
#!/bin/bash— el shebang le dice a Linux que use Bash para interpretar este archivochmod +x— establece el permiso de ejecucion para que puedas ejecutar el script directamente$(comando)— sustitucion de comando: ejecuta el comando e inserta su salida$VARIABLE— expansion de variable: inserta el valor de la variable
Buena practica: Siempre usa
#!/bin/bash(no#!/bin/sh) cuando tu script use caracteristicas especificas de Bash como arrays, pruebas[[ ]]o expansion de llaves{1..10}. Si necesitas maxima portabilidad entre diferentes shells, escribe scriptsshcompatibles con POSIX.
Variables y tipos de datos
Las variables de Bash almacenan cadenas de texto por defecto. No hay tipos separados de enteros, decimales o booleanos — todo es una cadena que puede interpretarse como numero en contextos aritmeticos.
Definir variables
#!/bin/bash
# Variables de cadena (sin espacios alrededor del =)
HOSTNAME="webserver01"
BACKUP_DIR="/var/backups"
LOG_FILE="/var/log/myscript.log"
# Aritmetica entera usando $(( ))
MAX_RETRIES=5
CURRENT_RETRY=0
TOTAL=$((MAX_RETRIES - CURRENT_RETRY))
# Sustitucion de comandos
CURRENT_DATE=$(date +%Y-%m-%d)
DISK_USAGE=$(df -h / | awk 'NR==2 {print $5}')
UPTIME=$(uptime -p)
echo "Servidor: $HOSTNAME"
echo "Directorio de respaldos: $BACKUP_DIR"
echo "Fecha: $CURRENT_DATE"
echo "Uso de disco: $DISK_USAGE"
echo "Tiempo activo: $UPTIME"
echo "Reintentos restantes: $TOTAL"
Variables especiales
#!/bin/bash
echo "Nombre del script: $0"
echo "Primer argumento: $1"
echo "Segundo argumento: $2"
echo "Todos los argumentos: $@"
echo "Numero de argumentos: $#"
echo "Estado de salida del ultimo comando: $?"
echo "ID de proceso de este script: $$"
echo "ID de proceso del ultimo comando en segundo plano: $!"
Arrays
#!/bin/bash
# Array indexado
SERVERS=("web01" "web02" "db01" "db02" "cache01")
echo "Primer servidor: ${SERVERS[0]}"
echo "Todos los servidores: ${SERVERS[@]}"
echo "Numero de servidores: ${#SERVERS[@]}"
# Recorrer el array
for server in "${SERVERS[@]}"; do
echo "Verificando $server..."
done
# Array asociativo (Bash 4+)
declare -A SERVICE_PORTS
SERVICE_PORTS[http]=80
SERVICE_PORTS[https]=443
SERVICE_PORTS[ssh]=22
SERVICE_PORTS[mysql]=3306
for service in "${!SERVICE_PORTS[@]}"; do
echo "$service -> puerto ${SERVICE_PORTS[$service]}"
done
Error comun: Nunca pongas espacios alrededor del signo
=al asignar variables.NAME="value"es correcto.NAME = "value"fallara porque Bash interpretaNAMEcomo un comando.
Condicionales: if, elif, else
Los condicionales controlan el flujo de tu script basandose en condiciones de prueba.
Sintaxis basica
#!/bin/bash
FILE="/etc/ssh/sshd_config"
if [[ -f "$FILE" ]]; then
echo "$FILE existe."
elif [[ -d "$FILE" ]]; then
echo "$FILE es un directorio, no un archivo."
else
echo "$FILE no existe."
fi
Operadores de prueba de archivos
#!/bin/bash
TARGET="/var/log/syslog"
# Pruebas de existencia y tipo de archivo
[[ -e "$TARGET" ]] && echo "Existe"
[[ -f "$TARGET" ]] && echo "Es un archivo regular"
[[ -d "$TARGET" ]] && echo "Es un directorio"
[[ -L "$TARGET" ]] && echo "Es un enlace simbolico"
[[ -r "$TARGET" ]] && echo "Es legible"
[[ -w "$TARGET" ]] && echo "Es escribible"
[[ -x "$TARGET" ]] && echo "Es ejecutable"
[[ -s "$TARGET" ]] && echo "Tiene tamano mayor que cero"
Comparaciones de cadenas
#!/bin/bash
ENVIRONMENT="production"
if [[ "$ENVIRONMENT" == "production" ]]; then
echo "Ejecutando en modo produccion -- precaucion extra!"
VERBOSE=false
elif [[ "$ENVIRONMENT" == "staging" ]]; then
echo "Ejecutando en modo staging."
VERBOSE=true
else
echo "Entorno desconocido: $ENVIRONMENT"
exit 1
fi
# Verificaciones de cadenas
[[ -z "$VAR" ]] && echo "VAR esta vacia o no definida"
[[ -n "$VAR" ]] && echo "VAR no esta vacia"
# Coincidencia de patrones con [[ ]]
if [[ "$HOSTNAME" == web* ]]; then
echo "Este es un servidor web."
fi
Comparaciones numericas
#!/bin/bash
DISK_PERCENT=$(df / | awk 'NR==2 {print $5}' | tr -d '%')
if [[ "$DISK_PERCENT" -gt 90 ]]; then
echo "CRITICO: Uso de disco al ${DISK_PERCENT}%"
elif [[ "$DISK_PERCENT" -gt 75 ]]; then
echo "ADVERTENCIA: Uso de disco al ${DISK_PERCENT}%"
else
echo "OK: Uso de disco al ${DISK_PERCENT}%"
fi
Consejo: Usa
[[ ]](corchetes dobles) en lugar de[ ](corchetes simples). Los corchetes dobles son una extension de Bash que maneja mejor los espacios en variables, soporta coincidencia de patrones con==y permite&&/||dentro de la expresion de prueba.
Bucles: for, while, until
Los bucles te permiten repetir operaciones sobre archivos, servidores, usuarios o cualquier lista de elementos.
Bucle for
#!/bin/bash
# Recorrer una lista
for PACKAGE in nginx curl wget htop; do
echo "Instalando $PACKAGE..."
sudo apt install -y "$PACKAGE" > /dev/null 2>&1
echo "$PACKAGE instalado."
done
# Recorrer un rango
for i in {1..10}; do
echo "Iteracion $i"
done
# Bucle for estilo C
for ((i = 0; i < 5; i++)); do
echo "Contador: $i"
done
# Recorrer archivos
for FILE in /var/log/*.log; do
SIZE=$(stat -c%s "$FILE" 2>/dev/null || echo 0)
echo "$FILE: $SIZE bytes"
done
Bucle while
#!/bin/bash
# Leer un archivo linea por linea
while IFS= read -r line; do
echo "Procesando: $line"
done < /etc/hosts
# Bucle basado en contador
COUNTER=0
MAX=5
while [[ $COUNTER -lt $MAX ]]; do
echo "Intento $((COUNTER + 1)) de $MAX"
COUNTER=$((COUNTER + 1))
done
# Esperar a que un servicio este disponible
RETRIES=0
MAX_RETRIES=30
while ! curl -s http://localhost:8080/health > /dev/null 2>&1; do
RETRIES=$((RETRIES + 1))
if [[ $RETRIES -ge $MAX_RETRIES ]]; then
echo "El servicio no inicio en $MAX_RETRIES intentos."
exit 1
fi
echo "Esperando servicio... (intento $RETRIES/$MAX_RETRIES)"
sleep 2
done
echo "El servicio esta listo!"
Bucle until
#!/bin/bash
# Bucle until -- se ejecuta hasta que la condicion sea verdadera
COUNT=0
until [[ $COUNT -ge 5 ]]; do
echo "Conteo es $COUNT"
COUNT=$((COUNT + 1))
done
Funciones
Las funciones te permiten organizar tus scripts en bloques reutilizables. Esto es critico para scripts mas grandes que realizan multiples tareas relacionadas.
#!/bin/bash
# Definicion de funcion
log_message() {
local LEVEL="$1"
local MESSAGE="$2"
local TIMESTAMP
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$TIMESTAMP] [$LEVEL] $MESSAGE"
}
# Funcion con valor de retorno
check_service() {
local SERVICE_NAME="$1"
if systemctl is-active --quiet "$SERVICE_NAME"; then
return 0 # exito
else
return 1 # fallo
fi
}
# Funcion que produce un valor (capturar con sustitucion de comando)
get_memory_usage() {
free -m | awk 'NR==2 {printf "%.1f", $3/$2 * 100}'
}
# Usando las funciones
log_message "INFO" "Iniciando verificacion del sistema..."
SERVICES=("nginx" "ssh" "cron")
for svc in "${SERVICES[@]}"; do
if check_service "$svc"; then
log_message "INFO" "$svc esta ejecutandose."
else
log_message "WARN" "$svc NO esta ejecutandose!"
fi
done
MEM_USAGE=$(get_memory_usage)
log_message "INFO" "Uso de memoria: ${MEM_USAGE}%"
Puntos clave sobre las funciones de Bash:
- Usa
localpara declarar variables con alcance de funcion (previene contaminar el alcance global) - Las funciones usan
returnpara codigos de salida (0-255), no para devolver datos - Para devolver datos, usa
echodentro de la funcion y captura con$(nombre_funcion) - Los argumentos se acceden con
$1,$2,$@dentro de la funcion
Entrada/Salida y redireccion
Entender la redireccion de E/S es esencial para escribir scripts que registren su salida, procesen archivos y manejen errores correctamente.
#!/bin/bash
# Redirigir stdout a un archivo (sobrescribir)
echo "Entrada de log" > /tmp/output.log
# Redirigir stdout a un archivo (agregar)
echo "Otra entrada" >> /tmp/output.log
# Redirigir stderr a un archivo
command_that_fails 2> /tmp/error.log
# Redirigir tanto stdout como stderr al mismo archivo
some_command > /tmp/all_output.log 2>&1
# Sintaxis moderna (Bash 4+) para redirigir ambos
some_command &> /tmp/all_output.log
# Descartar la salida completamente
noisy_command > /dev/null 2>&1
# Here document (heredoc) para entrada multilinea
cat << 'EOF' > /tmp/config.txt
server {
listen 80;
server_name example.com;
root /var/www/html;
}
EOF
# Canalizar salida a otro comando
ps aux | grep nginx | grep -v grep
# Tee: escribir a archivo Y stdout simultaneamente
echo "Verificacion del sistema aprobada" | tee -a /var/log/checks.log
# Sustitucion de proceso
diff <(ls /dir1) <(ls /dir2)
Leer entrada del usuario
#!/bin/bash
read -p "Ingresa el nombre del servidor: " SERVER_NAME
read -sp "Ingresa la contrasena: " PASSWORD
echo "" # nueva linea despues de entrada oculta
read -p "Proceder con el despliegue en $SERVER_NAME? (s/n): " CONFIRM
if [[ "$CONFIRM" != "s" ]]; then
echo "Despliegue cancelado."
exit 0
fi
Manejo de errores: set -euo pipefail y trap
El manejo adecuado de errores separa los scripts de calidad de produccion de los fragiles. Sin manejo de errores, un script puede fallar silenciosamente a mitad de camino, dejando tu sistema en un estado inconsistente.
El encabezado de modo estricto
#!/bin/bash
set -euo pipefail
IFS=$'\n\t'
Lo que hace cada opcion:
set -e— Termina inmediatamente si cualquier comando devuelve un estado de salida no ceroset -u— Trata las referencias a variables no definidas como erroresset -o pipefail— Si cualquier comando en un pipeline falla, todo el pipeline falla (no solo el ultimo comando)IFS=$'\n\t'— Establece el Separador de Campo Interno a nueva linea y tabulacion solamente (previene la division de palabras en espacios en nombres de archivos)
Usar trap para limpieza
#!/bin/bash
set -euo pipefail
TEMP_DIR=""
cleanup() {
local EXIT_CODE=$?
if [[ -n "$TEMP_DIR" && -d "$TEMP_DIR" ]]; then
rm -rf "$TEMP_DIR"
echo "Directorio temporal limpiado: $TEMP_DIR"
fi
if [[ $EXIT_CODE -ne 0 ]]; then
echo "El script fallo con codigo de salida: $EXIT_CODE"
fi
exit $EXIT_CODE
}
trap cleanup EXIT ERR
TEMP_DIR=$(mktemp -d)
echo "Trabajando en $TEMP_DIR"
# Tu logica de script aqui...
# Si algo falla, cleanup() se ejecuta automaticamente
cp /some/important/file "$TEMP_DIR/"
process_data "$TEMP_DIR"
Manejar fallos esperados
#!/bin/bash
set -euo pipefail
# Metodo 1: Usar || true para permitir que un comando falle
grep "pattern" /var/log/syslog || true
# Metodo 2: Usar if para verificar el resultado
if grep -q "error" /var/log/syslog; then
echo "Errores encontrados en syslog"
else
echo "No se encontraron errores"
fi
# Metodo 3: Capturar y verificar codigo de salida
set +e # deshabilitar temporalmente salida-ante-error
risky_command
EXIT_CODE=$?
set -e # re-habilitar
if [[ $EXIT_CODE -ne 0 ]]; then
echo "El comando fallo con codigo $EXIT_CODE"
fi
Trabajar con archivos y directorios
Los scripts de sysadmin frecuentemente necesitan crear, verificar, mover y procesar archivos. Aqui estan los patrones mas comunes:
#!/bin/bash
set -euo pipefail
# Crear una estructura de directorios
BACKUP_BASE="/var/backups/myapp"
BACKUP_DIR="${BACKUP_BASE}/$(date +%Y-%m-%d)"
mkdir -p "$BACKUP_DIR"
# Verificar si un archivo existe antes de operar con el
CONFIG_FILE="/etc/myapp/config.yml"
if [[ ! -f "$CONFIG_FILE" ]]; then
echo "ERROR: Archivo de configuracion no encontrado: $CONFIG_FILE"
exit 1
fi
# Encontrar archivos con mas de 30 dias
find /var/log/myapp -name "*.log" -mtime +30 -type f -print
# Encontrar y eliminar archivos viejos (con confirmacion)
find /tmp -name "*.tmp" -mtime +7 -type f -delete
# Obtener tamano de archivo en bytes
FILE_SIZE=$(stat -c%s "$CONFIG_FILE")
echo "Tamano del archivo de configuracion: $FILE_SIZE bytes"
# Contar lineas en un archivo
LINE_COUNT=$(wc -l < "$CONFIG_FILE")
echo "La configuracion tiene $LINE_COUNT lineas"
# Leer un archivo de configuracion, saltando comentarios y lineas vacias
while IFS='=' read -r key value; do
# Saltar comentarios y lineas vacias
[[ "$key" =~ ^#.*$ || -z "$key" ]] && continue
echo "Config: $key = $value"
done < "$CONFIG_FILE"
# Creacion segura de archivos temporales
TEMP_FILE=$(mktemp /tmp/myapp.XXXXXX)
echo "Usando archivo temporal: $TEMP_FILE"
# Siempre limpia archivos temporales (usa trap como se mostro antes)
Scripts practicos para sysadmins
Aqui hay cuatro scripts completos, listos para produccion, que resuelven problemas comunes de administracion de sistemas.
Script 1: Monitor de espacio en disco con alerta por correo
#!/bin/bash
set -euo pipefail
# Configuracion
THRESHOLD=80
ALERT_EMAIL="admin@example.com"
LOG_FILE="/var/log/disk-monitor.log"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
log "Iniciando verificacion de espacio en disco..."
ALERT_TRIGGERED=false
while IFS= read -r line; do
# Saltar la linea de encabezado
[[ "$line" == Filesystem* ]] && continue
USAGE=$(echo "$line" | awk '{print $5}' | tr -d '%')
MOUNT=$(echo "$line" | awk '{print $6}')
FILESYSTEM=$(echo "$line" | awk '{print $1}')
if [[ "$USAGE" -ge "$THRESHOLD" ]]; then
ALERT_TRIGGERED=true
log "ADVERTENCIA: $MOUNT esta al ${USAGE}% ($FILESYSTEM)"
else
log "OK: $MOUNT esta al ${USAGE}%"
fi
done < <(df -h --output=source,size,used,avail,pcent,target -x tmpfs -x devtmpfs)
if [[ "$ALERT_TRIGGERED" == true ]]; then
SUBJECT="[ALERTA] Advertencia de espacio en disco en $(hostname)"
BODY="Uno o mas sistemas de archivos excedieron ${THRESHOLD}% de uso en $(hostname) a las $(date).\n\n$(df -h)"
echo -e "$BODY" | mail -s "$SUBJECT" "$ALERT_EMAIL" 2>/dev/null || \
log "ADVERTENCIA: No se pudo enviar la alerta por correo"
fi
log "Verificacion de espacio en disco completada."
Script 2: Limpieza y rotacion de logs
#!/bin/bash
set -euo pipefail
# Configuracion
LOG_DIRS=("/var/log/myapp" "/var/log/nginx" "/home/*/logs")
MAX_AGE_DAYS=30
COMPRESS_AGE_DAYS=7
DRY_RUN=false
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}
# Analizar argumentos
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run) DRY_RUN=true; shift ;;
--max-age) MAX_AGE_DAYS="$2"; shift 2 ;;
*) echo "Opcion desconocida: $1"; exit 1 ;;
esac
done
TOTAL_FREED=0
for DIR_PATTERN in "${LOG_DIRS[@]}"; do
for DIR in $DIR_PATTERN; do
[[ -d "$DIR" ]] || continue
log "Procesando $DIR..."
# Comprimir logs mas antiguos que COMPRESS_AGE_DAYS
while IFS= read -r -d '' file; do
if [[ "$DRY_RUN" == true ]]; then
log " [SIMULACION] Comprimiria: $file"
else
gzip "$file"
log " Comprimido: $file"
fi
done < <(find "$DIR" -name "*.log" -mtime +"$COMPRESS_AGE_DAYS" -type f -print0)
# Eliminar logs comprimidos mas antiguos que MAX_AGE_DAYS
while IFS= read -r -d '' file; do
SIZE=$(stat -c%s "$file")
TOTAL_FREED=$((TOTAL_FREED + SIZE))
if [[ "$DRY_RUN" == true ]]; then
log " [SIMULACION] Eliminaria: $file ($SIZE bytes)"
else
rm -f "$file"
log " Eliminado: $file ($SIZE bytes)"
fi
done < <(find "$DIR" -name "*.gz" -mtime +"$MAX_AGE_DAYS" -type f -print0)
done
done
FREED_MB=$((TOTAL_FREED / 1024 / 1024))
log "Espacio total liberado: ${FREED_MB} MB"
Script 3: Script de respaldo automatizado
#!/bin/bash
set -euo pipefail
# Configuracion
BACKUP_SOURCE="/var/www /etc/nginx /etc/myapp"
BACKUP_DEST="/var/backups"
RETENTION_DAYS=14
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
HOSTNAME=$(hostname)
BACKUP_FILE="${BACKUP_DEST}/${HOSTNAME}_backup_${TIMESTAMP}.tar.gz"
LOG_FILE="/var/log/backup.log"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
cleanup() {
local EXIT_CODE=$?
if [[ $EXIT_CODE -ne 0 ]]; then
log "ERROR: El respaldo fallo con codigo de salida $EXIT_CODE"
rm -f "$BACKUP_FILE"
fi
}
trap cleanup EXIT
# Asegurar que el directorio de respaldos existe
mkdir -p "$BACKUP_DEST"
log "Iniciando respaldo de: $BACKUP_SOURCE"
log "Destino: $BACKUP_FILE"
# Crear tarball comprimido
# shellcheck disable=SC2086
tar -czf "$BACKUP_FILE" $BACKUP_SOURCE 2>/dev/null
BACKUP_SIZE=$(stat -c%s "$BACKUP_FILE")
BACKUP_SIZE_MB=$((BACKUP_SIZE / 1024 / 1024))
log "Respaldo creado: ${BACKUP_SIZE_MB} MB"
# Verificar el respaldo
if tar -tzf "$BACKUP_FILE" > /dev/null 2>&1; then
log "Verificacion de integridad del respaldo: APROBADA"
else
log "ERROR: Verificacion de integridad del respaldo FALLIDA"
exit 1
fi
# Eliminar respaldos antiguos
DELETED_COUNT=0
while IFS= read -r -d '' old_backup; do
rm -f "$old_backup"
DELETED_COUNT=$((DELETED_COUNT + 1))
log "Respaldo antiguo eliminado: $old_backup"
done < <(find "$BACKUP_DEST" -name "${HOSTNAME}_backup_*.tar.gz" -mtime +"$RETENTION_DAYS" -type f -print0)
log "Respaldo completado. Se eliminaron $DELETED_COUNT respaldo(s) antiguo(s)."
Script 4: Auditoria de cuentas de usuario
#!/bin/bash
set -euo pipefail
# Configuracion
OUTPUT_FILE="/tmp/user_audit_$(date +%Y%m%d).txt"
MIN_UID=1000
log() {
echo "$1" | tee -a "$OUTPUT_FILE"
}
# Limpiar salida anterior
> "$OUTPUT_FILE"
log "============================================"
log " REPORTE DE AUDITORIA DE CUENTAS DE USUARIO"
log " Host: $(hostname)"
log " Fecha: $(date '+%Y-%m-%d %H:%M:%S')"
log "============================================"
log ""
# Listar todos los usuarios humanos (UID >= 1000)
log "--- Cuentas de usuarios humanos (UID >= $MIN_UID) ---"
while IFS=: read -r username _ uid gid _ home shell; do
if [[ $uid -ge $MIN_UID ]]; then
LAST_LOGIN=$(lastlog -u "$username" 2>/dev/null | tail -1 | awk '{print $4, $5, $6, $7, $8, $9}')
GROUPS=$(groups "$username" 2>/dev/null | cut -d: -f2)
HAS_SUDO="No"
if groups "$username" 2>/dev/null | grep -qw "sudo\|wheel"; then
HAS_SUDO="Si"
fi
log " Usuario: $username (UID: $uid)"
log " Home: $home"
log " Shell: $shell"
log " Sudo: $HAS_SUDO"
log " Grupos:$GROUPS"
log " Ultimo inicio de sesion: $LAST_LOGIN"
log ""
fi
done < /etc/passwd
# Verificar usuarios con contrasenas vacias
log "--- Usuarios con contrasenas vacias ---"
EMPTY_PASS=$(sudo awk -F: '($2 == "") {print $1}' /etc/shadow 2>/dev/null || true)
if [[ -n "$EMPTY_PASS" ]]; then
log " ADVERTENCIA: $EMPTY_PASS"
else
log " Ninguno encontrado (bien)."
fi
log ""
# Verificar usuarios con UID 0 (equivalentes a root)
log "--- Usuarios con UID 0 (Privilegios de root) ---"
while IFS=: read -r username _ uid _; do
if [[ $uid -eq 0 ]]; then
log " $username (UID: 0)"
fi
done < /etc/passwd
log ""
# Listar llaves SSH autorizadas
log "--- Llaves SSH autorizadas ---"
for HOME_DIR in /home/*; do
USERNAME=$(basename "$HOME_DIR")
AUTH_KEYS="$HOME_DIR/.ssh/authorized_keys"
if [[ -f "$AUTH_KEYS" ]]; then
KEY_COUNT=$(wc -l < "$AUTH_KEYS")
log " $USERNAME: $KEY_COUNT llave(s) en $AUTH_KEYS"
fi
done
log ""
log "Reporte guardado en: $OUTPUT_FILE"
Programacion con cron
Cron es el programador de tareas estandar de Linux. Ejecuta scripts a intervalos definidos sin intervencion manual.
Editar el crontab
# Editar crontab para el usuario actual
crontab -e
# Editar crontab para un usuario especifico (requiere root)
sudo crontab -u www-data -e
# Listar entradas actuales del crontab
crontab -l
Sintaxis de cron
# ┌───────────── minuto (0 - 59)
# │ ┌───────────── hora (0 - 23)
# │ │ ┌───────────── dia del mes (1 - 31)
# │ │ │ ┌───────────── mes (1 - 12)
# │ │ │ │ ┌───────────── dia de la semana (0 - 7, 0 y 7 = Domingo)
# │ │ │ │ │
# * * * * * comando
Programaciones comunes de cron
# Ejecutar monitor de disco cada hora
0 * * * * /usr/local/bin/disk-monitor.sh >> /var/log/disk-monitor.log 2>&1
# Ejecutar respaldo diariamente a las 2 AM
0 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
# Ejecutar limpieza de logs cada domingo a las 3 AM
0 3 * * 0 /usr/local/bin/log-cleanup.sh >> /var/log/cleanup.log 2>&1
# Ejecutar auditoria de usuarios el 1ero de cada mes
0 9 1 * * /usr/local/bin/user-audit.sh >> /var/log/user-audit.log 2>&1
# Ejecutar verificacion de salud cada 5 minutos
*/5 * * * * /usr/local/bin/health-check.sh > /dev/null 2>&1
Reglas importantes de cron: Siempre usa rutas absolutas en los trabajos de cron (no rutas relativas). Cron se ejecuta con un entorno minimo, por lo que
PATHpuede no incluir/usr/local/bin. Siempre redirige la salida a un archivo de log o a/dev/nullpara prevenir que cron envie correo en cada ejecucion.
Usar variables de entorno en cron
# Establecer variables de entorno al inicio de tu crontab
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=admin@example.com
# Ahora tus trabajos tienen acceso a rutas comunes
0 2 * * * /usr/local/bin/backup.sh
Tabla de referencia de operadores de Bash
| Categoria | Operador | Descripcion | Ejemplo |
|---|---|---|---|
| Pruebas de archivos | -f | Archivo existe y es regular | [[ -f /etc/hosts ]] |
-d | Directorio existe | [[ -d /var/log ]] | |
-e | Archivo o directorio existe | [[ -e /tmp/lock ]] | |
-r | Archivo es legible | [[ -r config.yml ]] | |
-w | Archivo es escribible | [[ -w /var/log/app.log ]] | |
-x | Archivo es ejecutable | [[ -x script.sh ]] | |
-s | Tamano mayor que cero | [[ -s output.log ]] | |
-L | Es un enlace simbolico | [[ -L /usr/bin/python ]] | |
| Cadenas | == | Cadenas son iguales | [[ "$a" == "$b" ]] |
!= | Cadenas no son iguales | [[ "$a" != "$b" ]] | |
-z | Cadena esta vacia | [[ -z "$var" ]] | |
-n | Cadena no esta vacia | [[ -n "$var" ]] | |
=~ | Coincidencia regex | [[ "$s" =~ ^[0-9]+$ ]] | |
| Numerico | -eq | Igual | [[ $a -eq $b ]] |
-ne | No igual | [[ $a -ne $b ]] | |
-gt | Mayor que | [[ $a -gt $b ]] | |
-ge | Mayor o igual que | [[ $a -ge $b ]] | |
-lt | Menor que | [[ $a -lt $b ]] | |
-le | Menor o igual que | [[ $a -le $b ]] | |
| Logico | && | AND (dentro de [[ ]]) | [[ $a -gt 0 && $a -lt 100 ]] |
|| | OR (dentro de [[ ]]) | [[ $a -eq 0 || $a -eq 1 ]] | |
! | NOT | [[ ! -f /tmp/lock ]] |
Solucion de problemas y depuracion
Cuando un script no funciona como se espera, usa estas tecnicas para encontrar el problema.
Depurar con set -x
#!/bin/bash
set -x # Imprimir cada comando antes de ejecutarlo
NAME="world"
echo "Hello, $NAME"
# Salida:
# + NAME=world
# + echo 'Hello, world'
# Hello, world
Puedes habilitar la depuracion para secciones especificas:
#!/bin/bash
echo "Salida normal aqui"
set -x # Iniciar salida de depuracion
RESULT=$(some_complex_command)
process "$RESULT"
set +x # Detener salida de depuracion
echo "De vuelta a la salida normal"
Ejecutar un script en modo de depuracion
# Depurar el script completo sin modificarlo
bash -x ./myscript.sh
# Modo verboso (imprimir lineas al ser leidas)
bash -v ./myscript.sh
# Verboso + rastreo combinados
bash -xv ./myscript.sh
ShellCheck: Analisis estatico
ShellCheck es una herramienta esencial que detecta errores comunes de Bash, errores de entrecomillado y problemas de portabilidad.
# Instalar ShellCheck
sudo apt install -y shellcheck
# Analizar un script
shellcheck myscript.sh
# Salida de ejemplo:
# In myscript.sh line 5:
# echo $VARIABLE
# ^--------^ SC2086: Double quote to prevent globbing and word splitting.
Lista de verificacion de depuracion comun
- Verificar el shebang: Es
#!/bin/bash(no#!/bin/sh)? - Verificar permisos: Ejecutaste
chmod +x script.sh? - Verificar finales de linea: Los finales de linea de Windows (
\r\n) causan/bin/bash^M: bad interpreter. Corrige condos2unix script.sh. - Entrecomillar tus variables: Siempre usa
"$VARIABLE"no$VARIABLEpara manejar espacios y caracteres especiales. - Verificar codigos de salida: Despues de cada comando critico, verifica
$?o usaset -e. - Probar con valores fijos: Reemplaza variables con valores conocidos para aislar el problema.
- Verificar entorno de cron: Si un script funciona manualmente pero no en cron, el problema usualmente es
PATHo variables de entorno faltantes.
# Script rapido para volcar el entorno de cron
* * * * * env > /tmp/cron-env.txt 2>&1
# (eliminar despues de verificar)
Resumen
Bash scripting es una de las habilidades mas valiosas que un administrador de sistemas Linux puede desarrollar. En esta guia, aprendiste como:
- Escribir scripts con estructura adecuada (shebang, permisos, modo estricto)
- Usar variables, arrays y sustitucion de comandos
- Controlar el flujo con condicionales y bucles
- Organizar codigo con funciones
- Manejar errores de forma segura con
set -euo pipefailytrap - Construir scripts practicos para monitoreo de disco, limpieza de logs, respaldos y auditoria de usuarios
- Programar scripts con cron para ejecucion completamente automatizada
- Depurar scripts con
set -xy ShellCheck
Los cuatro scripts practicos de este articulo son puntos de partida. Personalizalos para tu entorno especifico, agrega notificaciones por correo, integralos con sistemas de monitoreo y construye sobre ellos a medida que crezcan tus necesidades.
Para temas relacionados de administracion de servidores, consulta nuestra Lista de verificacion de seguridad para servidores Linux: 20 pasos esenciales para asegurar los servidores donde se ejecutan tus scripts, y Hardening SSH: 12 pasos para asegurar tu servidor Linux para proteger el acceso SSH que usas para administrarlos.