Si llevas tiempo en Linux haciendo las mismas cosas a mano —lanzar escaneos, rotar logs, ejecutar backups, revisar servicios— estás perdiendo tiempo que podrías invertir en aprender algo que realmente importe. La automatización no es solo comodidad: en hacking ético es la diferencia entre un reconocimiento manual que tarda horas y un pipeline que corre solo mientras duermes.

Este artículo es para quien ya sabe moverse en terminal pero todavía no ha conectado los puntos entre bash, cron, ansible y el resto del ecosistema. No voy a listarte herramientas sin contexto. Voy a mostrarte cómo las uso en escenarios reales —reconocimiento, administración de labs, automatización de reportes— con los outputs que obtengo y los errores que me costaron tiempo.

Qué necesitas antes de empezar:

  • Linux funcional (Kali, Parrot, Ubuntu, lo que uses)
  • Acceso a terminal con permisos de usuario normal y sudo cuando sea necesario
  • Conocimiento básico de comandos: ls, cd, cat, grep, chmod
  • Ganas de dejar de hacer lo mismo dos veces

Herramientas para Automatizar Tareas en Linux

Por Qué la Automatización es Crítica en Linux (y en Hacking Ético)

Cuando empecé en CTFs, ejecutaba nmap a mano cada vez. Luego copiaba los puertos, lanzaba otro escaneo de versiones, abría el navegador para el servicio web, todo manual. En una máquina de HackTheBox eso son 15 minutos de trabajo mecánico antes de siquiera pensar en vulnerabilidades.

La automatización en Linux no es un lujo de sysadmin. Es una habilidad ofensiva. Los mejores scripts de reconocimiento que existen —AutoRecon, reconftw— son exactamente esto: bash y Python pegando herramientas que ya conoces. Cuando entiendes cómo funcionan por dentro, puedes modificarlos, depurarlos y construir los tuyos.

Desde el lado defensivo, un servidor sin tareas programadas para rotar logs, verificar integridad de archivos o limpiar temporales es un servidor que eventualmente explota por disco lleno o que nadie nota cuando algo cambia. He visto máquinas en CTFs donde el vector de entrada era un script mal configurado que corría como root. Conocer estas herramientas también te enseña a encontrar ese tipo de configuraciones erróneas.


Bash y Shell Scripting: La Base de Todo

Bash es el pegamento de Linux. Antes de tocar cualquier herramienta de automatización, necesitas entender que todo lo demás —cron, ansible, systemd timers— en algún punto ejecuta comandos de shell.

Un script básico de reconocimiento que uso como punto de partida en labs:

#!/bin/bash
# recon_basic.sh — reconocimiento inicial para CTFs/labs autorizados
# Uso: ./recon_basic.sh <IP_OBJETIVO>

TARGET=$1
OUTPUT_DIR="./recon_$TARGET"

if [ -z "$TARGET" ]; then
    echo "[!] Uso: $0 <IP>"
    exit 1
fi

mkdir -p "$OUTPUT_DIR"
echo "[*] Iniciando reconocimiento en $TARGET"
echo "[*] Resultados en: $OUTPUT_DIR"

# Ping check
if ping -c 1 "$TARGET" &>/dev/null; then
    echo "[+] Host activo"
else
    echo "[-] Host no responde a ping (puede seguir activo)"
fi

# Escaneo rápido de puertos
echo "[*] Escaneo rápido de puertos..."
nmap -T4 --open -p- "$TARGET" -oN "$OUTPUT_DIR/ports_fast.txt" 2>/dev/null

# Extraer puertos abiertos y lanzar escaneo de versiones
PORTS=$(grep "^[0-9]" "$OUTPUT_DIR/ports_fast.txt" | cut -d'/' -f1 | tr '\n' ',' | sed 's/,$//')

if [ -n "$PORTS" ]; then
    echo "[+] Puertos encontrados: $PORTS"
    echo "[*] Escaneo de versiones y scripts..."
    nmap -sV -sC -p "$PORTS" "$TARGET" -oN "$OUTPUT_DIR/services.txt" 2>/dev/null
    echo "[+] Completado. Revisa $OUTPUT_DIR/"
else
    echo "[-] No se encontraron puertos abiertos"
fi

Lo que hace este script es lo que harías a mano, pero encadenado. El truco está en extraer los puertos del primer escaneo y pasarlos al segundo: así no pierdes tiempo escaneando versiones en los 65535 puertos.

Estructura fundamental que debes dominar:

# Variables
NOMBRE="valor"
echo $NOMBRE

# Condicionales
if [ -f "/etc/passwd" ]; then
    echo "archivo existe"
fi

# Bucles
for IP in 192.168.1.{1..254}; do
    ping -c 1 -W 1 $IP &>/dev/null && echo "$IP activo"
done

# Funciones
check_port() {
    nc -zv "$1" "$2" 2>&1 | grep -q "succeeded" && echo "Puerto $2 abierto en $1"
}
check_port 10.10.10.1 80

# Redirección de errores
comando 2>/dev/null        # suprimir errores
comando > salida.txt 2>&1  # todo a archivo
comando &>/dev/null        # todo al vacío

Un error que cometí al principio: no validar inputs. Un script que recibe una IP y no verifica el formato puede hacer cosas raras si le pasas un string vacío o un nombre de host con caracteres especiales. Siempre valida lo que entra.


Cron Jobs: Programar Tareas en Linux sin Excusas

Cron es el scheduler por defecto en prácticamente cualquier distribución Linux. La sintaxis intimida al principio pero se aprende en 10 minutos.

# Formato: minuto hora día_mes mes día_semana comando
# *        *    *        *    *           comando

# Ejemplos:
0 2 * * *     /usr/bin/backup.sh          # cada día a las 2:00 AM
*/15 * * * *  /usr/bin/check_services.sh  # cada 15 minutos
0 9 * * 1     /usr/bin/weekly_report.sh   # lunes a las 9:00 AM
0 */6 * * *   /usr/bin/sync_logs.sh       # cada 6 horas

Para editar tu crontab:

crontab -e    # editar crontab del usuario actual
crontab -l    # listar tareas programadas
crontab -r    # eliminar todo el crontab (cuidado)
sudo crontab -e  # crontab de root

Output real de crontab -l en una de mis máquinas de lab:

# Crontab del usuario pentester — lab local
*/5 * * * * /home/pentester/scripts/check_vpn.sh >> /tmp/vpn_status.log 2>&1
0 3 * * * /home/pentester/scripts/backup_notes.sh
0 * * * * /usr/bin/find /tmp -name "*.tmp" -mtime +1 -delete
30 8 * * 1-5 /home/pentester/scripts/daily_summary.sh | mail -s "Lab Summary" pentester@local

La advertencia que casi nadie menciona: cuando cron ejecuta un script, el PATH no es el mismo que tienes en tu sesión interactiva. Cron usa un PATH mínimo: /usr/bin:/bin. Si tu script llama a nmap, python3, o cualquier herramienta que está en /usr/local/bin o en un virtualenv, va a fallar silenciosamente.

La solución es siempre usar rutas absolutas o definir el PATH al inicio del script:

#!/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
# resto del script...

Esto me costó dos horas una vez que un script de backup «funcionaba» cuando lo ejecutaba a mano pero nunca corría bien desde cron. El log estaba vacío porque el script moría en el primer comando que no encontraba.


Scripts Shell Avanzados: Más Allá del Básico

Una vez que dominas la estructura básica, el siguiente nivel es hacer scripts que sean reutilizables y que manejen errores correctamente.

#!/bin/bash
# monitor_web.sh — monitoreo de disponibilidad HTTP
# Útil para labs: verificar si un servicio web sigue activo durante un pentest

set -euo pipefail  # exit on error, undefined vars, pipe failures

TARGETS_FILE="${1:-targets.txt}"
LOG_FILE="/tmp/web_monitor_$(date +%Y%m%d).log"
TIMEOUT=5

log() {
    echo "[$(date '+%H:%M:%S')] $1" | tee -a "$LOG_FILE"
}

check_http() {
    local url="$1"
    local code
    code=$(curl -s -o /dev/null -w "%{http_code}" --max-time "$TIMEOUT" "$url" 2>/dev/null)
    
    case "$code" in
        200|301|302) log "[+] $url — HTTP $code" ;;
        000)         log "[-] $url — Sin respuesta (timeout o down)" ;;
        *)           log "[!] $url — HTTP $code (inesperado)" ;;
    esac
}

if [ ! -f "$TARGETS_FILE" ]; then
    log "[!] Archivo $TARGETS_FILE no encontrado"
    exit 1
fi

log "[*] Iniciando monitoreo desde $TARGETS_FILE"
while IFS= read -r target || [ -n "$target" ]; do
    [[ "$target" =~ ^#|^$ ]] && continue  # ignorar comentarios y líneas vacías
    check_http "$target"
done < "$TARGETS_FILE"

log "[*] Completado. Log: $LOG_FILE"

El set -euo pipefail al inicio es algo que aprendí tarde. Sin eso, un script puede continuar ejecutándose después de que un comando falla, y terminas con resultados parciales que parecen completos. En pentesting eso es peligroso: podrías pensar que un escaneo terminó cuando en realidad murió a mitad.


Ansible: Automatización a Escala

Ansible es otra categoría. Mientras bash es para automatizar tareas en una máquina, Ansible es para automatizar tareas en muchas máquinas simultáneamente, sin agente, usando SSH.

En hacking ético, Ansible tiene dos usos principales:

  1. Gestión de laboratorios: configurar múltiples VMs con las mismas herramientas, usuarios, configuraciones
  2. Post-explotación documentada: cuando tienes acceso a múltiples sistemas en un pentest de red interna, Ansible puede ayudarte a ejecutar comandos de recolección de información de forma ordenada (en entornos autorizados)

Instalación:

sudo apt install ansible -y
# o con pip
pip3 install ansible

Estructura básica de un playbook para configurar un lab de pentesting:

# setup_pentest_lab.yml
---
- name: Configurar herramientas básicas de pentesting
  hosts: lab_vms
  become: yes
  
  vars:
    tools:
      - nmap
      - gobuster
      - nikto
      - sqlmap
      - python3-pip
      - curl
      - wget
      - netcat-openbsd

  tasks:
    - name: Actualizar repositorios
      apt:
        update_cache: yes
        cache_valid_time: 3600

    - name: Instalar herramientas
      apt:
        name: "{{ tools }}"
        state: present

    - name: Crear directorio de trabajo
      file:
        path: /opt/pentest
        state: directory
        mode: '0755'
        owner: "{{ ansible_user }}"

    - name: Clonar wordlists básicas
      git:
        repo: https://github.com/danielmiessler/SecLists.git
        dest: /opt/seclists
        depth: 1
      ignore_errors: yes

Inventario simple (hosts.ini):

[lab_vms]
192.168.56.10 ansible_user=kali ansible_ssh_private_key_file=~/.ssh/lab_key
192.168.56.11 ansible_user=kali ansible_ssh_private_key_file=~/.ssh/lab_key

[lab_vms:vars]
ansible_python_interpreter=/usr/bin/python3

Ejecutar:

ansible-playbook -i hosts.ini setup_pentest_lab.yml

Output real de una ejecución:

PLAY [Configurar herramientas básicas de pentesting] ***************************

TASK [Gathering Facts] *********************************************************
ok: [192.168.56.10]
ok: [192.168.56.11]

TASK [Actualizar repositorios] *************************************************
changed: [192.168.56.10]
changed: [192.168.56.11]

TASK [Instalar herramientas] ***************************************************
changed: [192.168.56.10]
changed: [192.168.56.11]

TASK [Crear directorio de trabajo] *********************************************
changed: [192.168.56.10]
changed: [192.168.56.11]

TASK [Clonar wordlists básicas] ************************************************
changed: [192.168.56.10]
changed: [192.168.56.11]

PLAY RECAP *********************************************************************
192.168.56.10              : ok=5    changed=4    unreachable=0    failed=0    skipped=0
192.168.56.11              : ok=5    changed=4    unreachable=0    failed=0    skipped=0

Lo que me gusta de Ansible sobre un script bash que hace SSH en bucle: la idempotencia. Si corres el playbook dos veces, no instala las herramientas dos veces ni rompe nada. Un script bash con apt install en bucle puede hacer cosas raras si algo ya está parcialmente instalado.


Systemd Timers: La Alternativa Moderna a Cron

Si usas una distro moderna (Ubuntu 20.04+, Debian 11+, cualquier Arch), systemd timers son una alternativa a cron con mejor logging y más control.

La ventaja real sobre cron: puedes ver exactamente cuándo corrió una tarea y qué salida produjo con journalctl. Con cron, si no configuras logging explícito, los errores desaparecen.

Crear un timer para ejecutar un script cada hora:

# 1. Crear el servicio
sudo nano /etc/systemd/system/mi-script.service
[Unit]
Description=Mi script de automatización
After=network.target

[Service]
Type=oneshot
User=pentester
ExecStart=/home/pentester/scripts/mi_script.sh
StandardOutput=journal
StandardError=journal
# 2. Crear el timer
sudo nano /etc/systemd/system/mi-script.timer
[Unit]
Description=Ejecutar mi-script cada hora

[Timer]
OnBootSec=5min
OnUnitActiveSec=1h
Persistent=true

[Install]
WantedBy=timers.target
# 3. Activar y arrancar
sudo systemctl daemon-reload
sudo systemctl enable mi-script.timer
sudo systemctl start mi-script.timer

# Ver estado
systemctl status mi-script.timer
systemctl list-timers --all

# Ver logs
journalctl -u mi-script.service -f

Persistent=true es importante: si el sistema estaba apagado cuando debía correr la tarea, la ejecuta al arrancar. Cron no hace esto por defecto.


Make y Taskfile: Automatización de Workflows

make no es solo para compilar código. Es un runner de tareas con dependencias. Lo uso para organizar workflows de pentesting donde hay pasos que dependen de otros.

# Makefile para workflow de reconocimiento
# Uso: make recon TARGET=10.10.10.1

TARGET ?= 10.10.10.1
OUTPUT := ./output/$(TARGET)

.PHONY: all recon ports services web clean

all: recon

$(OUTPUT):
	mkdir -p $(OUTPUT)

recon: $(OUTPUT) ports services
	@echo "[+] Reconocimiento completo para $(TARGET)"

ports: $(OUTPUT)
	@echo "[*] Escaneo de puertos..."
	nmap -T4 --open -p- $(TARGET) -oN $(OUTPUT)/ports.txt

services: ports
	@echo "[*] Escaneo de servicios..."
	$(eval PORTS := $(shell grep "^[0-9]" $(OUTPUT)/ports.txt | cut -d'/' -f1 | tr '\n' ',' | sed 's/,$$//'))
	nmap -sV -sC -p $(PORTS) $(TARGET) -oN $(OUTPUT)/services.txt

web: $(OUTPUT)
	@echo "[*] Enumeración web..."
	gobuster dir -u http://$(TARGET) -w /usr/share/wordlists/dirb/common.txt \
		-o $(OUTPUT)/gobuster.txt 2>/dev/null

clean:
	rm -rf ./output/$(TARGET)

Ejecutar:

make recon TARGET=10.10.10.5
make web TARGET=10.10.10.5
make clean TARGET=10.10.10.5

La dependencia services: ports garantiza que el escaneo de servicios nunca corre antes de tener los puertos. Si ports falla, services no se ejecuta. Con un script bash lineal, tendrías que manejar esto manualmente.


Python para Automatización: Cuando Bash No Alcanza

Bash es perfecto para encadenar comandos del sistema. Cuando necesitas lógica más compleja —parsear JSON, hacer requests HTTP, manipular datos estructurados— Python es la herramienta correcta.

Un script que uso para parsear resultados de nmap y generar un resumen:

#!/usr/bin/env python3
# parse_nmap.py — parsear XML de nmap y generar resumen
# Requiere: pip3 install python-libnmap

from libnmap.parser import NmapParser
import sys
import json
from datetime import datetime

def parse_nmap_xml(xml_file):
    try:
        report = NmapParser.parse_fromfile(xml_file)
    except Exception as e:
        print(f"[!] Error parseando {xml_file}: {e}")
        sys.exit(1)
    
    results = {
        "scan_date": datetime.now().isoformat(),
        "hosts": []
    }
    
    for host in report.hosts:
        if host.status != "up":
            continue
            
        host_data = {
            "ip": host.address,
            "hostname": host.hostnames[0] if host.hostnames else "N/A",
            "os": host.os_fingerprint if host.os_fingerprint else "Unknown",
            "services": []
        }
        
        for service in host.services:
            if service.state == "open":
                host_data["services"].append({
                    "port": service.port,
                    "protocol": service.protocol,
                    "service": service.service,
                    "version": service.banner
                })
        
        results["hosts"].append(host_data)
    
    return results

if __name__ == "__main__":
    if len(sys.argv) != 2:
        print(f"Uso: {sys.argv[0]} <nmap_output.xml>")
        sys.exit(1)
    
    data = parse_nmap_xml(sys.argv[1])
    
    # Mostrar resumen
    print(f"\n[+] Hosts activos: {len(data['hosts'])}")
    for host in data["hosts"]:
        print(f"\n  IP: {host['ip']} ({host['hostname']})")
        print(f"  OS: {host['os']}")
        print(f"  Puertos abiertos: {len(host['services'])}")
        for svc in host['services']:
            print(f"    {svc['port']}/{svc['protocol']} — {svc['service']} {svc['version']}")
    
    # Guardar JSON
    output_file = sys.argv[1].replace('.xml', '_parsed.json')
    with open(output_file, 'w') as f:
        json.dump(data, f, indent=2)
    print(f"\n[+] JSON guardado: {output_file}")

Para usar nmap con salida XML:

nmap -sV -sC -p 22,80,443,8080 10.10.10.1 -oX scan.xml
python3 parse_nmap.py scan.xml

Integrar Todo: Un Pipeline Real de Reconocimiento

Aquí está cómo combino estas herramientas en un flujo real. Este es el pipeline que uso cuando empiezo una máquina nueva en HackTheBox:

#!/bin/bash
# pipeline_recon.sh — pipeline completo de reconocimiento inicial
# Tiempo estimado: 5-20 minutos dependiendo del objetivo
# Solo para uso en entornos autorizados: HTB, THM, labs propios

set -uo pipefail

TARGET="${1:?'Uso: $0 <IP>'}"
WORKDIR="$HOME/htb/$(date +%Y%m%d)_$TARGET"
WORDLIST="/usr/share/wordlists/dirb/common.txt"

mkdir -p "$WORKDIR"/{nmap,web,notes}
exec > >(tee "$WORKDIR/notes/pipeline.log") 2>&1

timestamp() { date '+%H:%M:%S'; }
log_info()  { echo "[$(timestamp)] [*] $1"; }
log_ok()    { echo "[$(timestamp)] [+] $1"; }
log_warn()  { echo "[$(timestamp)] [!] $1"; }

log_info "Target: $TARGET"
log_info "Workdir: $WORKDIR"

# Fase 1: Descubrimiento de puertos (rápido)
log_info "Fase 1: Descubrimiento de puertos"
nmap -T4 --open -p- --min-rate 5000 "$TARGET" \
    -oN "$WORKDIR/nmap/allports.txt" \
    -oX "$WORKDIR/nmap/allports.xml" 2>/dev/null

OPEN_PORTS=$(grep "^[0-9]" "$WORKDIR/nmap/allports.txt" | \
    grep "open" | cut -d'/' -f1 | tr '\n' ',' | sed 's/,$//')

if [ -z "$OPEN_PORTS" ]; then
    log_warn "No se encontraron puertos abiertos. Verifica conectividad."
    exit 1
fi
log_ok "Puertos abiertos: $OPEN_PORTS"

# Fase 2: Servicios y scripts
log_info "Fase 2: Enumeración de servicios"
nmap -sV -sC -p "$OPEN_PORTS" "$TARGET" \
    -oN "$WORKDIR/nmap/services.txt" 2>/dev/null
log_ok "Servicios enumerados"

# Fase 3: Si hay HTTP/HTTPS, enumerar directorios
for PORT in $(echo "$OPEN_PORTS" | tr ',' '\n'); do
    case "$PORT" in
        80|8080|8000)
            log_info "Puerto web detectado: $PORT — lanzando gobuster"
            gobuster dir -u "http://$TARGET:$PORT" \
                -w "$WORDLIST" \
                -o "$WORKDIR/web/gobuster_$PORT.txt" \
                -q 2>/dev/null &
            ;;
        443|8443)
            log_info "Puerto HTTPS detectado: $PORT — lanzando gobuster"
            gobuster dir -u "https://$TARGET:$PORT" \
                -w "$WORDLIST" \
                -k \
                -o "$WORKDIR/web/gobuster_$PORT.txt" \
                -q 2>/dev/null &
            ;;
    esac
done

wait  # esperar todos los gobuster en background
log_ok "Pipeline completado. Resultados en: $WORKDIR"

El & al final de los gobuster los lanza en background y el wait al final espera a que todos terminen antes de continuar. Así los escaneos web corren en paralelo en vez de secuencial.


Errores Comunes y Cómo Evitarlos

1. Scripts sin manejo de errores que fallan silenciosamente

El error más frecuente: un script que corre desde cron, falla en el paso 2, y continúa ejecutando los pasos 3, 4 y 5 con datos incorrectos o sin datos. El output parece normal pero los resultados son basura.

Solución: set -euo pipefail al inicio de cada script bash. -e detiene el script si cualquier comando falla. -u trata variables no definidas como error. -o pipefail hace que un pipe falle si cualquier comando en él falla.

#!/bin/bash
set -euo pipefail
# Ahora si algo falla, el script para y puedes saber exactamente dónde

2. Cron jobs que funcionan a mano pero no en cron

Ya lo mencioné con el PATH, pero hay otro problema frecuente: rutas relativas. Si tu script hace cd /home/user/scripts && ./helper.sh, eso funciona en tu sesión porque el directorio existe. Cron ejecuta desde /, así que las rutas relativas se resuelven desde ahí.

Siempre usa rutas absolutas o añade esto al inicio:

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"

3. Ansible fallando por versiones de Python inconsistentes

En labs con múltiples VMs, es común tener mezcla de Ubuntu 20.04 (Python 3.8) y 22.04 (Python 3.10). Ansible puede conectar pero fallar en módulos específicos si no especificas el intérprete.

Solución en el inventario:

[lab_vms:vars] ansible_python_interpreter=/usr/bin/python3

O en el playbook:

- hosts: all
  vars:
    ansible_python_interpreter: /usr/bin/python3

4. Permisos incorrectos en scripts de cron

Un script que no tiene permisos de ejecución falla desde cron con un error críptico. Siempre:

chmod +x /ruta/al/script.sh
# Y verifica que el usuario de cron tiene acceso al directorio
ls -la /ruta/al/script.sh

5. No redirigir salida en cron y llenar el mailbox del sistema

Por defecto, cron envía la salida de cada tarea al mail del usuario. En muchos sistemas esto llena /var/spool/mail hasta que el disco se llena. Siempre redirige:

*/5 * * * * /usr/bin/mi_script.sh >> /var/log/mi_script.log 2>&1

Próximos Pasos

Si este artículo te abrió el apetito, estos son los caminos naturales para seguir:

1. Profundizar en bash scripting para pentesting

El siguiente nivel es aprender a procesar texto con awk, sed y grep de forma avanzada. La mayoría de outputs de herramientas de seguridad son texto plano y saber manipularlo te ahorra instalar dependencias innecesarias.

→ Recurso: Bash Scripting para Hacking Ético en hackchest.net

2. Automatización de reconocimiento con Python

Una vez que dominas bash, Python te da acceso a APIs, parseo de XML/JSON, y librerías como scapy para manipulación de paquetes. El salto natural es escribir tus propias herramientas de reconocimiento.

→ Recurso: Python para Pentesting: Primeros Scripts en hackchest.net

3. Privilege Escalation a través de cron y scripts mal configurados

Conocer cómo funcionan estas herramientas te enseña a encontrar configuraciones erróneas. Un cron job que ejecuta un script escribible por usuarios sin privilegios, o un script que usa rutas relativas en un directorio donde puedes escribir, son vectores reales de escalada de privilegios.

→ Recurso: Linux Privilege Escalation: Cron y Wildcards en hackchest.net


FAQ

¿Cuánto tiempo lleva aprender a automatizar tareas en Linux de forma útil?

Con dedicación real —una hora diaria practicando— en 3-4 semanas puedes escribir scripts bash funcionales que resuelvan problemas concretos. El primer mes no vas a escribir nada elegante, pero vas a escribir cosas que funcionan. Ansible toma un poco más porque implica entender el modelo declarativo, pero un playbook básico lo puedes tener en un día.

El pipeline completo de este artículo —bash + cron + Python básico— lo puedes tener operativo en un fin de semana si te enfocas. Lo que toma tiempo es el refinamiento: manejar errores correctamente, hacer scripts reutilizables, no repetirte.

¿Es mejor usar cron jobs o systemd timers para tareas programadas en Linux?

Depende del contexto. Si estás en un sistema moderno con systemd (la mayoría de distros actuales), los timers son objetivamente superiores: mejor logging, más opciones de scheduling, integración con el sistema de servicios. El comando journalctl -u mi-servicio.service te da todo el historial de ejecuciones sin configurar nada extra.

Cron sigue siendo válido para scripts simples, para compatibilidad con sistemas más antiguos, o cuando necesitas algo que funcione en cualquier Linux sin pensar. Para un lab de pentesting personal, cron es perfectamente suficiente.

¿Puedo usar estas técnicas de automatización en un pentest real?

Sí, con matices importantes. Ansible para gestionar tu infraestructura de ataque (C2, redirectores, herramientas) es completamente estándar. Scripts bash para automatizar reconocimiento también. Lo que debes tener cuidado es con la velocidad y el ruido: un script que lanza nmap con --min-rate 10000 en