Cuando algo falla en un servidor, la respuesta suele estar en los logs, y grep, awk y sed permiten encontrarla en segundos sin instalar nada. grep selecciona líneas, awk trabaja con columnas y hace cálculos, y sed transforma o recorta texto. En este tutorial usarás las tres herramientas sobre dos logs habituales en Ubuntu 24.04, el access log de Nginx y /var/log/auth.log, para responder preguntas reales: qué IP generan más tráfico, qué URL devuelven errores, cuándo empezó un problema y quién intenta entrar por SSH.
Requisitos previos
Para seguir esta guía necesitas:
- Un servidor con Ubuntu 24.04 LTS (o Debian 12) y un usuario con privilegios
sudo, por ejemplo un VPS de CubePath. - Conocimientos básicos de la terminal y de tuberías (
|).
grep, sed y awk vienen instalados. En Ubuntu, awk es mawk, rápido y compatible con todos los ejemplos de esta guía. Los logs de sistema de /var/log pertenecen al grupo adm; léelos con sudo o añade tu usuario al grupo con sudo usermod -aG adm your_user y vuelve a iniciar sesión.
Paso 1: Preparar logs de ejemplo
Para que puedas comprobar que obtienes los mismos resultados, crea dos archivos de ejemplo en un directorio de trabajo. El primero sigue el formato combined de Nginx y Apache:
mkdir -p ~/logs-lab && cd ~/logs-lab
cat > access.log <<'EOF'
203.0.113.10 - - [25/Sep/2026:10:01:12 +0000] "GET / HTTP/1.1" 200 5320 "-" "Mozilla/5.0 (X11; Linux x86_64)"
203.0.113.10 - - [25/Sep/2026:10:01:13 +0000] "GET /css/app.css HTTP/1.1" 200 18234 "https://example.com/" "Mozilla/5.0 (X11; Linux x86_64)"
198.51.100.23 - - [25/Sep/2026:10:02:40 +0000] "GET /wp-login.php HTTP/1.1" 404 162 "-" "python-requests/2.31"
198.51.100.23 - - [25/Sep/2026:10:02:41 +0000] "GET /.env HTTP/1.1" 404 162 "-" "python-requests/2.31"
198.51.100.23 - - [25/Sep/2026:10:02:41 +0000] "GET /admin/config.php HTTP/1.1" 404 162 "-" "python-requests/2.31"
192.0.2.55 - - [25/Sep/2026:10:15:03 +0000] "POST /api/orders HTTP/1.1" 201 412 "-" "curl/8.5.0"
192.0.2.55 - - [25/Sep/2026:10:15:09 +0000] "POST /api/orders HTTP/1.1" 502 166 "-" "curl/8.5.0"
203.0.113.10 - - [25/Sep/2026:11:20:44 +0000] "GET /products?id=42 HTTP/1.1" 200 7421 "https://example.com/" "Mozilla/5.0 (X11; Linux x86_64)"
192.0.2.55 - - [25/Sep/2026:11:21:02 +0000] "POST /api/orders HTTP/1.1" 502 166 "-" "curl/8.5.0"
203.0.113.77 - - [25/Sep/2026:11:40:19 +0000] "GET /products?id=7 HTTP/1.1" 200 7390 "-" "Mozilla/5.0 (Macintosh)"
EOF
El segundo imita /var/log/auth.log en Ubuntu 24.04, que usa marcas de tiempo ISO 8601:
cat > auth.log <<'EOF'
2026-09-25T09:58:01.120334+00:00 web01 sshd[2101]: Failed password for root from 198.51.100.23 port 40122 ssh2
2026-09-25T09:58:04.502113+00:00 web01 sshd[2101]: Failed password for root from 198.51.100.23 port 40122 ssh2
2026-09-25T09:59:10.884120+00:00 web01 sshd[2140]: Failed password for invalid user admin from 198.51.100.23 port 40388 ssh2
2026-09-25T10:03:22.004511+00:00 web01 sshd[2188]: Failed password for invalid user oracle from 203.0.113.200 port 51544 ssh2
2026-09-25T10:05:47.310022+00:00 web01 sshd[2203]: Accepted publickey for deploy from 192.0.2.10 port 53120 ssh2: ED25519 SHA256:q1w2e3r4t5y6u7i8o9p0
2026-09-25T10:05:47.412874+00:00 web01 sshd[2203]: pam_unix(sshd:session): session opened for user deploy(uid=1001) by deploy(uid=0)
2026-09-25T10:30:02.000118+00:00 web01 CRON[2301]: pam_unix(cron:session): session opened for user root(uid=0) by root(uid=0)
EOF
Cuando termines, aplica los mismos comandos a tus logs reales: /var/log/nginx/access.log y /var/log/auth.log.
Entender los campos del access log
awk divide cada línea en campos separados por espacios, numerados $1, $2... En el formato combined quedan así:
| Campo | Contenido | Ejemplo |
|---|---|---|
$1 | IP del cliente | 203.0.113.10 |
$4 | Fecha y hora (con [) | [25/Sep/2026:10:01:12 |
$6 | Método (con ") | "GET |
$7 | Ruta solicitada | /css/app.css |
$9 | Código de estado | 200 |
$10 | Bytes enviados | 18234 |
El user agent contiene espacios, así que no tiene un número de campo fijo. Para él usarás las comillas como separador, como verás en el paso 3.
Paso 2: Filtrar líneas con grep
grep muestra las líneas que coinciden con un patrón. Es el primer filtro en casi cualquier análisis.
Cuenta las respuestas 404. Los espacios alrededor evitan que coincida con un 404 dentro de una URL o de un tamaño:
grep -c ' 404 ' access.log
3
Con -E usas expresiones regulares extendidas. Esta cuenta todos los errores 4xx y 5xx, anclando el código justo después de las comillas que cierran la petición:
grep -E '" [45][0-9]{2} ' access.log | wc -l
5
-v invierte la selección. Es útil para quitar ruido, como los recursos estáticos:
grep -vE '\.(css|js|png|jpg|svg|ico)' access.log | wc -l
9
-F busca texto literal, sin interpretar caracteres especiales, y es más rápido. Combinado con un segundo grep, responde a "¿cuántas veces ha fallado con 502 la API de pedidos?":
grep -F '/api/orders' access.log | grep -c '" 502 '
2
-o imprime solo la parte que coincide, en lugar de la línea entera. Así extraes las IP y las cuentas:
grep -oE '^[0-9.]+' access.log | sort | uniq -c | sort -rn
3 203.0.113.10
3 198.51.100.23
3 192.0.2.55
1 203.0.113.77
sort | uniq -c | sort -rn es el patrón clásico para contar ocurrencias: uniq -c cuenta líneas iguales consecutivas (por eso hay que ordenar antes) y sort -rn ordena el resultado de mayor a menor.
Con -P (expresiones compatibles con Perl) puedes usar \K para descartar lo que va antes. Esto extrae los usuarios con los que se ha intentado entrar por SSH:
grep -oP 'Failed password for (invalid user )?\K\S+' auth.log | sort | uniq -c | sort -rn
2 root
1 oracle
1 admin
Otras opciones que usarás a menudo:
-iignora mayúsculas y minúsculas.-A 3,-B 3y-C 3muestran 3 líneas después, antes o alrededor de cada coincidencia, útil con trazas de error.-nmuestra el número de línea.zgrepbusca en logs rotados comprimidos (zgrep ' 502 ' /var/log/nginx/access.log.*.gz).
Paso 3: Trabajar con columnas en awk
awk procesa cada línea con la forma condición { acción }. Si omites la condición, la acción se aplica a todas las líneas.
Imprime solo IP, código de estado y ruta:
awk '{print $1, $9, $7}' access.log | head -n 3
203.0.113.10 200 /
203.0.113.10 200 /css/app.css
198.51.100.23 404 /wp-login.php
Una condición filtra por el valor de una columna, algo que con grep es mucho menos preciso. Esto muestra todas las peticiones con error de servidor:
awk '$9 >= 500 {print $4, $7, $9}' access.log
[25/Sep/2026:10:15:09 /api/orders 502
[25/Sep/2026:11:21:02 /api/orders 502
Distribución de códigos de estado:
awk '{print $9}' access.log | sort | uniq -c | sort -rn
4 200
3 404
2 502
1 201
awk también acumula valores. El bloque END se ejecuta al terminar de leer el archivo. Total de datos servidos:
awk '{bytes += $10} END {printf "%.1f KiB\n", bytes / 1024}' access.log
38.7 KiB
Con arrays asociativos agrupas por cualquier clave. Peticiones y bytes por IP, ordenados por bytes:
awk '{hits[$1]++; bytes[$1] += $10}
END {for (ip in hits) printf "%-15s %5d %8d\n", ip, hits[ip], bytes[ip]}' access.log | sort -k3 -rn
203.0.113.10 3 30975
203.0.113.77 1 7390
192.0.2.55 3 744
198.51.100.23 3 486
Peticiones por hora. split divide el campo de fecha por :, y la segunda parte es la hora:
awk '{split($4, t, ":"); hits[t[2] ":00"]++} END {for (h in hits) print h, hits[h]}' access.log | sort
10:00 7
11:00 3
Con -F cambias el separador de campos. Si separas por comillas, el user agent queda en el campo 6:
awk -F'"' '{print $6}' access.log | sort | uniq -c | sort -rn
3 python-requests/2.31
3 curl/8.5.0
3 Mozilla/5.0 (X11; Linux x86_64)
1 Mozilla/5.0 (Macintosh)
En auth.log la IP no está siempre en la misma columna, porque "invalid user" añade dos palabras. En lugar de un número de campo, recorre la línea y toma la palabra que sigue a from:
awk '/Failed password/ {for (i = 1; i <= NF; i++) if ($i == "from") print $(i + 1)}' auth.log | sort | uniq -c | sort -rn
3 198.51.100.23
1 203.0.113.200
Como las marcas de tiempo ISO 8601 se ordenan igual como texto que como fecha, puedes filtrar un intervalo comparando cadenas:
awk '$1 >= "2026-09-25T10:00" && $1 < "2026-09-25T10:10"' auth.log
2026-09-25T10:03:22.004511+00:00 web01 sshd[2188]: Failed password for invalid user oracle from 203.0.113.200 port 51544 ssh2
2026-09-25T10:05:47.310022+00:00 web01 sshd[2203]: Accepted publickey for deploy from 192.0.2.10 port 53120 ssh2: ED25519 SHA256:q1w2e3r4t5y6u7i8o9p0
2026-09-25T10:05:47.412874+00:00 web01 sshd[2203]: pam_unix(sshd:session): session opened for user deploy(uid=1001) by deploy(uid=0)
Paso 4: Recortar y transformar con sed
sed edita el texto al vuelo. Sus dos usos principales en logs son imprimir rangos de líneas y sustituir texto.
Con -n y la orden p, sed imprime solo las líneas que coinciden. Todas las peticiones de las 11:
sed -n '/25\/Sep\/2026:11:/p' access.log
203.0.113.10 - - [25/Sep/2026:11:20:44 +0000] "GET /products?id=42 HTTP/1.1" 200 7421 "https://example.com/" "Mozilla/5.0 (X11; Linux x86_64)"
192.0.2.55 - - [25/Sep/2026:11:21:02 +0000] "POST /api/orders HTTP/1.1" 502 166 "-" "curl/8.5.0"
203.0.113.77 - - [25/Sep/2026:11:40:19 +0000] "GET /products?id=7 HTTP/1.1" 200 7390 "-" "Mozilla/5.0 (Macintosh)"
Un rango /inicio/,/fin/ imprime desde la primera línea que coincide con el inicio hasta la primera que coincide con el fin. Sirve para extraer la ventana de un incidente:
sed -n '/25\/Sep\/2026:10:15/,/25\/Sep\/2026:11:21/p' access.log
192.0.2.55 - - [25/Sep/2026:10:15:03 +0000] "POST /api/orders HTTP/1.1" 201 412 "-" "curl/8.5.0"
192.0.2.55 - - [25/Sep/2026:10:15:09 +0000] "POST /api/orders HTTP/1.1" 502 166 "-" "curl/8.5.0"
203.0.113.10 - - [25/Sep/2026:11:20:44 +0000] "GET /products?id=42 HTTP/1.1" 200 7421 "https://example.com/" "Mozilla/5.0 (X11; Linux x86_64)"
192.0.2.55 - - [25/Sep/2026:11:21:02 +0000] "POST /api/orders HTTP/1.1" 502 166 "-" "curl/8.5.0"
La orden s/patrón/reemplazo/ sustituye texto, y con -E puedes usar grupos. Antes de compartir un log con terceros, anonimiza el último octeto de las IP:
sed -E 's/^([0-9]+\.[0-9]+\.[0-9]+)\.[0-9]+/\1.0/' access.log | head -n 2
203.0.113.0 - - [25/Sep/2026:10:01:12 +0000] "GET / HTTP/1.1" 200 5320 "-" "Mozilla/5.0 (X11; Linux x86_64)"
203.0.113.0 - - [25/Sep/2026:10:01:13 +0000] "GET /css/app.css HTTP/1.1" 200 18234 "https://example.com/" "Mozilla/5.0 (X11; Linux x86_64)"
Quitar la query string antes de contar agrupa las URL que solo cambian en sus parámetros:
sed -E 's/\?[^ ]*//' access.log | awk '{print $7}' | sort | uniq -c | sort -rn
3 /api/orders
2 /products
1 /wp-login.php
1 /css/app.css
1 /admin/config.php
1 /.env
1 /
Eliminar la marca de tiempo y el host de auth.log deja líneas más cortas y fáciles de leer:
sed -E 's/^\S+ \S+ //' auth.log | head -n 2
sshd[2101]: Failed password for root from 198.51.100.23 port 40122 ssh2
sshd[2101]: Failed password for root from 198.51.100.23 port 40122 ssh2
Advertencia
sed -imodifica el archivo en su sitio. No lo uses sobre logs en uso; trabaja siempre sobre la salida o sobre una copia.
Paso 5: Combinar las tres herramientas
La potencia real aparece al encadenarlas: cada herramienta hace una cosa y pasa el resultado a la siguiente.
Rutas que devuelven 404, de más a menos frecuentes. En un log real verás aquí enlaces rotos y escaneos automáticos:
awk '$9 == 404 {print $7}' access.log | sort | uniq -c | sort -rn | head -n 10
1 /wp-login.php
1 /admin/config.php
1 /.env
IP que han provocado 3 o más respuestas 404, candidatas a ser escáneres:
awk '$9 == 404 {print $1}' access.log | sort | uniq -c | awk '$1 >= 3 {print $2}'
198.51.100.23
IP con intentos fallidos de SSH, usando grep para extraer y awk para quedarse con la dirección:
grep 'Failed password' auth.log | grep -oE 'from [0-9.]+' | awk '{print $2}' | sort | uniq -c | sort -rn
3 198.51.100.23
1 203.0.113.200
Seguir un log en tiempo real
tail -F sigue un archivo aunque se rote. Añade --line-buffered a grep para que imprima cada coincidencia en cuanto llega en lugar de acumularlas:
sudo tail -F /var/log/nginx/access.log | grep --line-buffered -E '" 5[0-9]{2} '
Pulsa Ctrl+C para salir.
Leer los logs de journald
Muchos servicios escriben solo en el journal de systemd. journalctl puede volcar su salida como texto para que la proceses igual. En Ubuntu 24.04 el servicio de SSH se llama ssh:
sudo journalctl -u ssh --since today --no-pager | grep 'Failed password' | grep -oE 'from [0-9.]+' | sort | uniq -c | sort -rn
Consejos de rendimiento con logs grandes
- Filtra primero con
grep -Fy dejaawkpara el trabajo por columnas sobre menos líneas. - Anteponer
LC_ALL=Cagrepysortevita el coste de las reglas de idioma y acelera mucho los archivos de varios GB:LC_ALL=C grep -F ' 502 ' access.log. - Para logs rotados y comprimidos usa
zcatozgrepen lugar de descomprimirlos en disco:zcat /var/log/nginx/access.log.*.gz | awk '{print $9}' | sort | uniq -c.
Conclusión
Con grep para seleccionar líneas, awk para trabajar con columnas y agregar, y sed para recortar y transformar, puedes responder en segundos a la mayoría de preguntas sobre lo que ha pasado en un servidor. La clave es construir la tubería paso a paso y comprobar la salida de cada etapa antes de añadir la siguiente.
Como siguientes pasos puedes bloquear automáticamente las IP que fallan repetidamente en SSH con Fail2ban, centralizar los logs de varios servidores con rsyslog para analizarlos desde un único punto, o llevarlos a Elasticsearch cuando necesites búsquedas y paneles sobre grandes volúmenes.
