La petición que respondiste es ahora un registro que guardas tú
Todo lo demás en esta serie apunta hacia fuera. Ocultar el origen, cifrar el disco, elegir la jurisdicción, impedir que otro guarde un log sobre ti. Esta guía apunta a la máquina misma, porque un servidor que responde peticiones anota quién las hizo — por defecto, en varios sitios a la vez, con una retención que no eligió nadie.
Mira lo que conserva una imagen estándar dos semanas después del despliegue. /var/log/nginx/access.log tiene una línea por petición: dirección, marca de tiempo, ruta, referrer y user agent, con rotación diaria y catorce días de conservación. /var/log/auth.log tiene cada sesión SSH, incluida la dirección de origen de cada una de las tuyas. El journal tiene los mismos eventos otra vez, más lo que tus servicios hayan impreso en stderr. Si Docker está en marcha, cada contenedor tiene un archivo de log JSON que, de fábrica, no tiene ningún límite de tamaño. Si fail2ban está en marcha, tiene un log de cada dirección que ha bloqueado alguna vez y una base de datos SQLite que dice lo mismo, y las direcciones de ambos están completas.
Nada de eso es malicioso, y la mayor parte es genuinamente útil — durante aproximadamente una hora después de que algo se rompa. El problema es la forma: fidelidad total, conservada durante mucho tiempo, por accidente y no por decisión. La pregunta que merece la pena hacerse sobre cada uno de esos archivos no es "¿esto es sensible?", sino "¿qué haría yo de verdad con la línea que escribí hace tres semanas?" Para la inmensa mayoría de las líneas en la inmensa mayoría de los servidores la respuesta es nada, y un registro que nunca vas a leer es puro riesgo — ante un compromiso, ante una copia de seguridad que sobrevive a la máquina, ante quien acabe pidiéndolo.
La minimización de datos es el nombre formal de la solución, y es la idea menos controvertida de toda la protección de datos: recoger lo que el trabajo necesita, conservarlo mientras lo necesita, y parar ahí. Lo que sigue aplica eso a una máquina que administras de verdad, asumiendo que ya has hecho la primera hora tras el despliegue y que hay algo en la máquina que merece la pena proteger.
SP·02Seis logs, y los dos que identifican a personas
Antes de cambiar nada, conoce el inventario. Un VPS Debian o Ubuntu pequeño que ejecuta un servicio web suele escribir seis flujos, y se solapan más de lo que la gente espera — el mismo evento acaba a menudo en tres archivos con tres retenciones distintas.
El access log de nginx es el que identifica a tus visitantes. Una línea por petición, con la dirección del cliente, la ruta exacta, el referrer y una cadena de user agent lo bastante detallada como para ser, por sí sola, una huella digital débil. El error log de nginx es el que la gente olvida: registra client: 203.0.113.9 en cada timeout de upstream, cada 403, cada petición malformada — y, a diferencia del access log, su formato es fijo y no se puede definir con plantillas.
El auth log — /var/log/auth.log en la familia Debian, /var/log/secure en la familia RHEL — te identifica a ti. Cada línea de sesión aceptada por clave pública lleva tu dirección de origen y la huella de la clave que abrió la sesión. Cualquiera que lea un mes entero de ese archivo aprende desde qué redes administras, a qué horas y cuántas claves distintas usas. En una máquina de propiedad anónima, ese archivo suele ser más revelador que cualquier cosa que generen tus visitantes.
El journal guarda una copia de casi todo lo anterior, más el stdout y el stderr de cada unidad, y en una instalación por defecto puede crecer hasta el 10% del sistema de archivos, con un tope de 4 GB, antes de empezar a descartar las entradas más antiguas. En cualquier disco de 40 GB o más ese tope son los 4 GB completos, que, a los volúmenes que produce un servidor pequeño, son muchos meses.
Los logs de la aplicación son el comodín. Un framework en modo debug registra URLs completas, incluidas las query strings, y las query strings suelen llevar tokens de sesión, enlaces de restablecimiento de contraseña y términos de búsqueda. PHP-FPM se puede configurar para escribir su propio access log, que duplica la línea de petición que nginx ya escribió, en un archivo que tu configuración de nginx nunca toca.
Los logs de los contenedores son los silenciosos. El driver json-file que Docker usa por defecto no tiene rotación a menos que la configures, así que /var/lib/docker/containers/*/*-json.log conserva todo lo que ha dicho el contenedor desde que se creó. Es una forma habitual de descubrir que una política de retención de "14 días" en realidad guarda once meses, y es el mismo tipo de sorpresa que las reglas de firewall que Docker escribe por detrás de ufw.
De los seis, dos llevan identificadores de personas que merece la pena minimizar a propósito: el access log de nginx (tus visitantes) y el auth log (tú). El resto, en general, solo necesita un tope de tamaño y un reloj más corto.
SP·03Trunca al escribir, no al rotar
El instinto es seguir registrando con normalidad y limpiar después — un cron nocturno que reescribe el archivo de ayer con las direcciones eliminadas. No construyas eso. Un trabajo de limpieza implica que las direcciones completas existieron de verdad en el disco hasta un día entero, y durante ese día quedaron capturadas por tu copia de seguridad, posiblemente copiadas por el snapshot a nivel de bloque de tu proveedor, y abandonadas en lo que sea que el sistema de archivos hiciera con los bloques antiguos. Peor aún, el trabajo es una pieza móvil más: falla en silencio la semana en que se llena el disco, y nada te avisa de que el archivo de ayer sigue completo.
La única reducción con la que puedes contar de verdad es la que ocurre antes de escribir la línea. En nginx eso es un bloque map evaluado en el momento de registrar, que reescribe la dirección en una variable nueva, y un log_format que usa esa variable nueva en lugar de $remote_addr. La dirección completa nunca se serializa. No hay nada que limpiar después, nada que programar, y nada que se pueda estropear el día en que no estás mirando.
# /etc/nginx/conf.d/00-privacy-log.conf -- http context, loaded before the sites
map $remote_addr $ip_trunc {
# IPv4: keep the /24, zero the host part
~(?<v4>\d+\.\d+\.\d+)\. "${v4}.0";
# IPv6: keep the first two groups, drop the rest
~(?<v6>[^:]+:[^:]+): "${v6}::";
# anything the two patterns cannot parse -- including compressed forms
# like ::1 -- falls through here, i.e. fails closed rather than open
default "0.0.0.0";
}
log_format privacy '$ip_trunc - - [$time_local] "$request" $status '
'$body_bytes_sent "$http_referer" "$http_user_agent" '
'rid=$request_id rt=$request_time';
Tres detalles deciden si esto funciona de verdad. Primero, el map tiene que vivir en el contexto http — dentro de un bloque server nginx se niega a arrancar. Dejarlo en conf.d/ con un nombre que ordene alfabéticamente pronto es la forma más simple de asegurarlo.
Segundo, si estás detrás de Cloudflare o de cualquier otro edge, comprueba qué contiene $remote_addr. El módulo realip lo sustituye por la dirección real del cliente al principio del procesamiento de la petición, y guarda la dirección de conexión en $realip_remote_addr. Ese orden juega a tu favor: el map se ejecuta en el momento de registrar, así que trunca al visitante real y no al edge. Pero también significa que si no has configurado realip, estás truncando la dirección de Cloudflare y no aprendiendo nada, mientras la dirección real se queda en una cabecera.
Tercero, audita el resto de la cadena de formato. Truncar $remote_addr no sirve absolutamente de nada si la línea sigue terminando en "$http_x_forwarded_for" o lleva $http_cf_connecting_ip — y muchísimos formatos de fábrica y de paneles de control incluyen uno de los dos. La dirección completa está en las cabeceras de la petición; solo se queda fuera del archivo si dejas fuera del formato cada cabecera que la lleva.
Fíjate en lo que la sustituyó: $request_id, una cadena hexadecimal aleatoria de 32 caracteres que nginx genera por petición. Ese es el giro de la siguiente sección.
Lo que rompe el truncado, y los tres trabajos que un log hace de verdad
Siempre hay quien objeta que los logs anonimizados no sirven para nada, y tiene razón a medias — porque "los logs" son en realidad tres trabajos sin relación entre sí disfrazados de un solo nombre de archivo, y solo uno de ellos necesita la dirección.
Trabajo uno: bloquear a quien te está machacando ahora mismo. Esto necesita la dirección completa, y la necesita en segundos. No la necesita mañana. fail2ban es la herramienta habitual, y de verdad no puede funcionar con un archivo truncado — banear 203.0.113.0 banea a un host inocente y deja conectado al atacante. Pero este trabajo lo satisface un archivo que existe durante un día, o directamente no usar ningún archivo: limit_req y limit_conn, propios de nginx, guardan su estado en memoria compartida, actúan en microsegundos en lugar de al ritmo del intervalo de sondeo de fail2ban, y no escriben nada a disco.
Trabajo dos: averiguar por qué esa petición devolvió un 500. Esto necesita correlación, no identidad. Un ID de petición que va de nginx a la aplicación une la línea de access log, el error de upstream y el stack trace de la aplicación en un solo evento — que es lo que de verdad intentabas hacer cuando recurrías a la dirección. En la práctica el ID es mejor: sobrevive a un cliente en una red móvil cuya dirección cambia a mitad de sesión, y no caduca cuando cuatro visitantes comparten una misma dirección CGNAT.
Trabajo tres: entender el tráfico a lo largo del tiempo. Volumen, mezcla de códigos de estado, qué rutas están calientes, si el crawler se ha descontrolado. Aquí una dirección truncada va perfectamente, y el /24 que sobrevive al truncado basta para ver que una sola red es responsable del 40% de tus peticiones.
Así que el diseño no es "registrar menos", sino dividir por reloj: un flujo de fidelidad completa que vive un día y alimenta a las herramientas de bloqueo, y un flujo truncado que vive tanto como quieras tener estadísticas. nginx escribe los dos en el mismo instante, así que no hay ningún paso de procesamiento intermedio ni ninguna ventana en la que lo que no debería estar en disco lo esté más tiempo del previsto.
# inside the server block, or in a snippet included by it
access_log /var/log/nginx/access.log privacy; # truncated, keep for weeks
access_log /var/log/nginx/security.log secip; # full address, keep for a day
# static assets are pure noise in a privacy log -- drop them entirely
location ~* \.(?:css|js|svg|png|jpe?g|webp|woff2?|ico)$ {
access_log off;
expires 30d;
}
El formato secip es una sola línea al lado del otro — $remote_addr, la marca de tiempo, la petición y el estado, nada más. Es el único archivo de la máquina donde se permite que repose una dirección de visitante completa, lo que convierte su retención en una sola decisión en un solo sitio, y no en una propiedad que tienes que razonar a través de seis archivos.
journald, auth.log, y el rastro que lleva hasta ti
La privacidad de los visitantes es la parte de la que todo el mundo escribe. El rastro del administrador es la parte que importa en una máquina cuya razón de ser es que tu nombre no está vinculado a ella, y ese rastro está casi por completo en dos sitios.
/var/log/auth.log registra una línea Accepted publickey por cada sesión que abres, con tu dirección de origen y la huella de tu clave. En un mes es un horario de tus hábitos de trabajo y una lista de las redes que usas. Si siempre te conectas a través del mismo túnel, es una única dirección repetida y relativamente aburrida. Si te conectas desde donde sea que estés en cada momento, es un historial de viajes.
El journal guarda los mismos eventos, más todo lo que imprimieron tus unidades, y sus valores por defecto son generosos: SystemMaxUse= es el 10% del sistema de archivos, y MaxRetentionSec= no está definido, lo que significa que no hay ningún límite de tiempo — solo uno de tamaño. En un servidor tranquilo, esa combinación retiene meses.
Aquí hay una disyuntiva real, y merece la pena decirla claramente en lugar de despacharla con un gesto. Los logs que te describen a ti son los mismos que te dicen cómo entró alguien. Configura Storage=volatile y el journal vive solo en RAM, y desaparece al reiniciar — genuinamente privado, y genuinamente inútil la mañana en que encuentres un proceso que tú no arrancaste, porque el primer reinicio de tu atacante borró la prueba. Para la mayoría, el término medio sensato es almacenamiento persistente con un tope duro y un reloj corto: el tiempo suficiente para investigar un incidente que notas en la primera semana, lo bastante corto para que el archivo no sea un diario.
Dos notas de implementación que suelen pillar a la gente. Las imágenes de Debian y Ubuntu difieren en si rsyslog está instalado o no; si /var/log/auth.log existe en tu máquina, es rsyslog quien lo escribe, y los topes de journald no gobiernan ese archivo en absoluto — de eso se encarga logrotate. Y ForwardToSyslog= es lo que alimenta a rsyslog desde el journal, así que desactivarlo en una máquina que tiene los dos evita que acabes guardando dos copias de todo bajo dos políticas de retención distintas.
Tu política de retención es la que digan tus copias de seguridad
Esta es la que deshace todo el trabajo cuidadoso de más arriba, y es invisible a menos que vayas a buscarla.
Supongamos que logrotate conserva catorce días de logs de nginx, y estás contento con eso. Ahora añade la copia de seguridad fuera de sitio que hiciste bien en montar: una ejecución diaria de Borg o restic, con una retención de siete diarios, cuatro semanales y seis mensuales. Cada uno de esos archivos contiene /var/log tal y como estaba el día en que se ejecutó. El archivo mensual más antiguo tiene seis meses y guarda los catorce días de logs que eran los vigentes entonces. Tu retención de logs efectiva no es de catorce días. Son seis meses, en un repositorio cifrado que no puedes recorrer con grep sin restaurarlo antes, en una segunda máquina, en un país distinto.
Hay exactamente dos soluciones honestas. Excluir los directorios de logs de la copia de seguridad — son de las pocas cosas en un servidor que casi siempre puedes reconstruir o de las que puedes prescindir, y una restauración que omite /var/log no es una restauración peor. O incluirlos a propósito y aceptar que tu retención real es la del repositorio, en cuyo caso dilo así en cualquier política que publiques, porque la alternativa es una política declarada que tu propia infraestructura contradice.
# exclude logs from the backup, and prove it took
borg create --stats \
--exclude '/var/log' \
--exclude '/var/lib/docker/containers' \
::'{hostname}-{now:%Y-%m-%d}' /
# the check that matters: can you still find an address in the newest archive?
borg list ::"$(borg list --short --last 1)" | grep -c '^var/log/' || echo "clean"
Ya que estás en este estado de ánimo, dos primos del mismo problema. Los snapshots de tu proveedor no son tuyos. Un snapshot a nivel de hipervisor captura el disco tal como estaba, incluidos los logs que ya has rotado desde entonces, y vive en el almacenamiento del proveedor bajo la retención del proveedor — una razón más para que las direcciones nunca debieran haberse escrito completas, no una razón para hacer algo ingenioso después.
Y no recurras a shred en un VPS. Sobrescribir un archivo en un disco virtual con aprovisionamiento fino, sobre un sistema de archivos copy-on-write, encima de un SSD que remapea bloques para repartir el desgaste, no sobrescribe de forma fiable las celdas físicas que lo contenían. El borrado seguro en almacenamiento alquilado y virtualizado es puro teatro. La reducción que funciona es la que hiciste en el momento de escribir; todo lo que viene después es un mejor esfuerzo que no puedes verificar. Cifrar el volumen cambia esta ecuación — pero la cambia antes de que se escriban los datos, que es la misma lección otra vez.
Las copias que no controlas
La minimización en tu propia máquina es una capa entre varias, y tener claras las demás es lo que evita que esto se convierta en una falsa sensación de estar completo.
La red de tu proveedor de hosting ve el registro de flujo. Origen, destino, puertos, bytes, tiempos — de cada conexión de entrada y salida de la máquina, registres algo tú o no. Ninguna configuración en el servidor cambia eso. Es buena parte de la razón por la que la jurisdicción en la que está la máquina es una variable real y no de marketing; lo que la ley exige a la red difiere enormemente según el país.
Tu CDN o tu edge registra en el borde. Si Cloudflare termina el TLS por ti, tiene la línea de la petición y la dirección del cliente antes de que tu servidor entre en juego, con su propio calendario de retención, sujeto a su propio proceso legal. Truncar tu log de origen no llega hacia atrás a través de él. Montar un edge propio es la versión de esto que sí puedes configurar de verdad — y las reglas de logging de esta guía se aplican primero a esa máquina edge, porque es donde llegan las direcciones sin truncar.
Los rastreadores de errores y los analytics se la llevan fuera de la máquina por ti. Sentry y la mayoría de sus equivalentes adjuntan la IP del cliente a cada evento por defecto; el ajuste suele llamarse algo como send_default_pii, y merece la pena comprobarlo en lugar de darlo por hecho. Cualquier analytics alojado es, por construcción, una copia en manos de un tercero del mismo access log que acabas de pasarte una tarde truncando.
El correo es el que más filtra de todos. Si algo en la máquina envía correo, las cabeceras llevan el host y la dirección de origen, y cada relay del camino guarda una copia del sobre con sus marcas de tiempo. Un formulario de contacto que te manda un correo es un log que tú no administras.
Nada de esto hace inútil el trabajo local — la copia local es la que se incauta junto con la máquina, la que se exfiltra en una brecha, o la que entregas tú mismo. Es sencillamente la única capa que controlas por completo, y el error está en tratarla como si fuera todo el cuadro.
SP·08Minimización por diseño, no borrado al recibir un aviso
Merece la pena ser preciso aquí, porque las dos cosas se confunden, y la diferencia entre ellas es toda la diferencia que hay entre una práctica de ingeniería estándar y algo que no deberías hacer.
Decidir de antemano qué recoge tu servicio y cuánto tiempo lo conserva es una práctica normal, documentada y recomendada. Bajo el RGPD son dos de los principios centrales — minimización de datos y limitación del plazo de conservación — y una retención de logs más corta es un control que los auditores piden, no uno al que objeten. No existe una obligación general para el operador de un sitio web o un cliente de hosting en la UE de conservar logs de tráfico; la directiva de retención generalizada que una vez sugirió lo contrario fue anulada por el Tribunal de Justicia en 2014, y las leyes nacionales que sobreviven a esa anulación vinculan sobre todo a los proveedores de telecomunicaciones, no a quien administra un servidor web. Estados Unidos tampoco tiene un mandato general de retención para operadores de sitios.
Destruir registros concretos después de que te hayan notificado sobre ellos es un acto completamente distinto. Una solicitud de conservación, una orden de retención por litigio, una orden judicial o una investigación policial cambian lo que puedes hacer con los datos que existen en ese momento, y "mi política de retención lo borró" no es una defensa válida si aceleraste el borrado a causa del aviso. Nada de esta guía trata sobre eso. Una política de retención es algo que fijas un martes cualquiera y después dejas en paz; si solo se acorta cuando pasa algo, no era una política.
La misma distinción recorre el resto de este sitio: hay una línea real entre la ingeniería de privacidad y la postura "bulletproof" que se vende a sí misma como inmunidad. Diseñar un servicio que nunca acumula un historial de visitantes queda cómodamente del lado correcto de esa línea, en la misma categoría que cifrar tus discos o no pedirle a los usuarios una dirección de correo que no necesitas.
Dos consecuencias prácticas. Si publicas una política de privacidad, haz que sus cifras de retención coincidan con lo que hay de verdad en el disco — unos catorce días declarados y seis meses reales en un repositorio de copias de seguridad son el tipo de brecha que convierte sobre el papel a un operador de buena fe en uno de mala fe. Y anota la decisión para ti mismo, aunque sea en un comentario al principio del archivo de logrotate si no hay otro sitio, porque quien tenga que justificar estas cifras dentro de dieciocho meses vas a ser tú, y no recordarás por qué el número era siete. Nada de lo anterior es asesoría legal; si operas en algún sitio con obligaciones de retención específicas de tu sector, contrástalas con tu propia situación antes de acortar nada.
SP·09Paso a paso
-
01
Averigua qué está guardando ya la máquina
No configures nada hasta que hayas medido. El objetivo de esta pasada es encontrar el archivo que se te olvidó, que en la mayoría de las máquinas es un log de contenedor o un log de aplicación que nadie ha mirado desde el despliegue.
# biggest log files anywhere on the box, largest last sudo du -ah /var/log /var/lib/docker/containers 2>/dev/null \ | sort -h | tail -20 # how much disk the journal holds, and how far back it goes journalctl --disk-usage journalctl --output=short-iso | head -1 # how old is the oldest nginx line still on disk? zcat -f /var/log/nginx/access.log* 2>/dev/null | head -1
Después mira una línea de cada archivo y pregúntate qué identifica. El comando de abajo cuenta cuántas direcciones completas y distintas se pueden recuperar ahora mismo de tus logs web — suele ser el número que justifica el resto de esta guía.
zcat -f /var/log/nginx/access.log* 2>/dev/null \ | grep -oE '^([0-9]{1,3}\.){3}[0-9]{1,3}' | sort -u | wc -lApunta lo que encuentres. Ejecutarás estos mismos comandos al final para demostrar que el cambio surtió efecto.
-
02
Trunca la dirección del cliente antes de que nginx escriba la línea
Crea el map y los dos formatos en un archivo que se cargue dentro del contexto
http. En Debian y Ubuntu,/etc/nginx/conf.d/se incluye desdenginx.confantes que las configuraciones de los sitios, que es exactamente donde encaja esto.sudo tee /etc/nginx/conf.d/00-privacy-log.conf > /dev/null <<'EOF' # The client address is reduced here, at log time, and never written in full # to the long-retention file. $realip_remote_addr still holds the connecting # address if you need it while debugging a proxy problem. map $remote_addr $ip_trunc { ~(?<v4>\d+\.\d+\.\d+)\. "${v4}.0"; ~(?<v6>[^:]+:[^:]+): "${v6}::"; # anything the two patterns cannot parse -- including compressed forms # like ::1 -- falls through here, i.e. fails closed rather than open default "0.0.0.0"; } # Long retention: no full address, no forwarded-for header, request id instead. log_format privacy '$ip_trunc - - [$time_local] "$request" $status ' '$body_bytes_sent "$http_referer" "$http_user_agent" ' 'rid=$request_id rt=$request_time'; # Short retention: the one file allowed to hold a complete address. log_format secip '$remote_addr [$time_local] "$request" $status'; EOF sudo nginx -t && sudo systemctl reload nginxAhora apunta el sitio hacia ellos. En tu bloque server, sustituye la línea
access_logexistente por el par, y aprovecha para desactivar el logging de los recursos estáticos.access_log /var/log/nginx/access.log privacy; access_log /var/log/nginx/security.log secip;
Recarga, carga una página y lee el resultado. El primer campo debería terminar en
.0, y la línea debería llevar un valorrid=.sudo nginx -t && sudo systemctl reload nginx curl -s -o /dev/null https://your-domain.example/ sudo tail -1 /var/log/nginx/access.log
Si la dirección sigue completa, el bloque server está sobrescribiendo el formato en algún punto más abajo —
grep -rn access_log /etc/nginx/encuentra la línea que gana. -
03
Conserva direcciones completas solo donde algo actúe sobre ellas
Si fail2ban está en marcha, ahora mismo está leyendo el archivo que acabas de truncar. Apúntalo en su lugar al log de seguridad, y dale a ese log una vida de un día para que las direcciones completas caduquen por sí solas.
sudo tee /etc/fail2ban/jail.d/nginx-privacy.local > /dev/null <<'EOF' [nginx-http-auth] enabled = true logpath = /var/log/nginx/error.log [nginx-botsearch] enabled = true logpath = /var/log/nginx/security.log maxretry = 6 findtime = 10m bantime = 1h EOF sudo fail2ban-client reload
Después ocúpate de la propia memoria de fail2ban, que casi nadie toca nunca: guarda su propio log de baneos bajo el
rotate 4 weeklypor defecto, y una base de datos SQLite de cada baneo que ha impuesto.dbpurgeagees lo que expira filas de esa base de datos — ajústalo a algo cercano a tu baneo más largo, en lugar de dejarlo en el valor por defecto.sudo tee /etc/fail2ban/fail2ban.d/retention.local > /dev/null <<'EOF' [Definition] dbpurgeage = 2d loglevel = NOTICE EOF sudo systemctl restart fail2ban
Mejor todavía, para inundaciones simples, haz el trabajo en nginx, donde no se escribe absolutamente nada.
limit_reqguarda sus contadores en memoria compartida, responde en microsegundos en lugar de al ritmo de un intervalo de sondeo, y no deja ningún registro de a quién se limitó — consulta el runbook de la primera hora ante un DDoS para dimensionar las zonas bajo carga real.# http context: keyed on the truncated address, so nothing complete is # held in memory either. 10m of shared state is plenty for a small site. limit_req_zone $ip_trunc zone=perip:10m rate=20r/s; # in the location you want protected limit_req zone=perip burst=40 nodelay;
Indexar la zona por
$ip_truncen lugar de por$binary_remote_addres una concesión deliberada: el límite ahora se aplica a todo un/24a la vez, así que una oficina con mucho tráfico detrás de un mismo bloque comparte presupuesto. Para un sitio pequeño eso suele estar bien, y a veces incluso es una mejora; si no lo está, indexa la zona por la dirección completa — el estado de un limitador de tasa vive en memoria compartida y nunca se escribe a disco, así que no forma parte de lo que estás minimizando aquí. -
04
Limita el journal y evita la segunda copia
journald acepta un drop-in, que sobrevive a las actualizaciones del paquete de una forma que editar
journald.confno consigue. Los valores de abajo conservan cosa de una semana — suficiente para investigar algo que notas el lunes y que empezó el viernes — dentro de un tope duro de 200 MB.sudo mkdir -p /etc/systemd/journald.conf.d sudo tee /etc/systemd/journald.conf.d/retention.conf > /dev/null <<'EOF' [Journal] Storage=persistent SystemMaxUse=200M SystemMaxFileSize=20M MaxRetentionSec=7day MaxFileSec=1day ForwardToSyslog=no EOF sudo systemctl restart systemd-journald journalctl --disk-usage
El reinicio aplica los topes de tamaño de inmediato; el reloj de retención se impone a medida que se rotan archivos nuevos, así que un journal ya sobredimensionado se encoge en la siguiente rotación, no al instante.
journalctl --vacuum-time=7dlo fuerza ahora mismo si quieres recuperar el disco hoy.ForwardToSyslog=noimporta en cualquier imagen que incluya rsyslog: sin él, cada entrada del journal también se añade a/var/log/syslog, bajo el calendario de logrotate en lugar del de journald, y acabas con dos copias con dos fechas de caducidad distintas. Comprueba en qué situación estás antes de dar nada por hecho:systemctl is-active rsyslog 2>/dev/null || echo "rsyslog not running" ls -la /var/log/auth.log /var/log/syslog 2>/dev/null
Si esos archivos existen, son cosa de rsyslog, y su retención se fija en el paso seis. Si no existen, el journal es la única copia, y acabas de limitarla.
-
05
Impide que la aplicación vuelva a registrar lo que eliminaste
Nada de lo anterior toca tu aplicación, y una aplicación en el modo equivocado escribirá alegremente la dirección completa, la URL completa y el token de sesión en un archivo propio. Tres cosas que comprobar.
PHP-FPM incluye una directiva
access.logen la configuración de su pool, comentada por defecto pero activada por muchísimos paneles de control. Si está activa, es una segunda copia de cada línea de petición, en un archivo que tu trabajo en nginx nunca tocó.grep -rn '^access.log' /etc/php/*/fpm/pool.d/ || echo "fpm access log off"
El nivel de log de tu framework decide si las query strings y los cuerpos de las peticiones acaban en disco. El modo debug de la mayoría de los frameworks registra la URL completa, y un enlace de restablecimiento de contraseña es una URL completa. Fija el nivel de producción y confirma que es de verdad el que está cargado, y no el que está en el archivo que crees que se está leyendo.
El driver por defecto de Docker nunca rota. Arréglalo a nivel del daemon para que todos los contenedores futuros hereden el tope. Ten en cuenta que esto se aplica a los contenedores creados después del reinicio — los que ya existen conservan su archivo actual, sin límite, hasta que se recreen.
sudo tee /etc/docker/daemon.json > /dev/null <<'EOF' { "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" } } EOF sudo systemctl restart docker docker inspect --format '{{.HostConfig.LogConfig}}' $(docker ps -q) 2>/dev/nullSi un contenedor ya tiene un archivo grande, recrearlo con
docker compose up -d --force-recreatees lo que de verdad trunca el historial — reiniciarlo sin más conserva el mismo archivo de log. -
06
Fija la retención a propósito, en un solo sitio por archivo
logrotate es donde vive el reloj de todo lo que escriben rsyslog y nginx. Edita el bloque que viene de fábrica en lugar de añadir uno nuevo: dos bloques que nombran la misma ruta hacen que logrotate falle con un error de entrada duplicada y deje de rotar ese archivo por completo, que es la forma más habitual en que un cambio de retención se convierte, en silencio, en uno infinito.
# check for duplicates BEFORE editing, then again after sudo logrotate --debug /etc/logrotate.conf 2>&1 | grep -i 'duplicate\|error'
Para nginx, los dos archivos quieren relojes distintos: el truncado puede vivir semanas, el que guarda direcciones completas no debería sobrevivir al día. Añade un bloque aparte para el log de seguridad — una ruta distinta, así que no hay duplicado — y acorta el que viene de fábrica.
sudo tee /etc/logrotate.d/nginx-security > /dev/null <<'EOF' # The only file on this box that holds complete client addresses. # One day, uncompressed so fail2ban can read it. Do not lengthen without # a reason you would be happy to write down here. /var/log/nginx/security.log { daily rotate 1 maxage 1 missingok notifempty nocompress create 0640 www-data adm sharedscripts postrotate [ -f /var/run/nginx.pid ] && kill -USR1 `cat /var/run/nginx.pid` endscript } EOF sudo sed -i 's/^\trotate 14$/\trotate 7/' /etc/logrotate.d/nginx sudo logrotate --debug /etc/logrotate.d/nginx-securityHaz la misma cuenta para
/etc/logrotate.d/rsyslogsi rsyslog está instalado — sus valores por defecto conservan cuatro semanas deauth.log, que son cuatro semanas de tus propias sesiones SSH. Y ejecuta la pasada de debug una última vez: imprime exactamente qué archivos rotaría y borraría, que es la única forma de confirmar que los números que acabas de escribir son los números que están realmente en vigor. -
07
Demuéstralo, incluso a través de la copia de seguridad
Vuelve a ejecutar las mediciones del paso uno. El recuento de direcciones completas y distintas en el log de retención larga debería dejar de crecer ahora, y después de un ciclo de rotación debería ser cero.
# should print 0 once the pre-change files have rotated out zcat -f /var/log/nginx/access.log* 2>/dev/null \ | grep -oE '^([0-9]{1,3}\.){3}[0-9]{1,3}' \ | grep -v '\.0$' | sort -u | wc -l # and the journal should now be bounded journalctl --disk-usageDespués, la comprobación que casi nadie hace: mira dentro de la copia de seguridad. Un repositorio que todavía lleva
/var/logconserva la fidelidad y la retención que acabas de pasarte una tarde eliminando, y va a seguir conservándolas mientras sobreviva tu archivo más antiguo.borg list | head -3 # borg lists oldest first borg list ::"$(borg list --short | head -1)" 2>/dev/null \ | grep -c '^var/log/' || echo "no logs in oldest archive"
Si el recuento no es cero, o bien añade la exclusión de antes y deja que los archivos viejos caduquen solos, o bien pódalos a propósito. Hasta que ocurra una de las dos cosas, tu retención real es la del repositorio — que es la frase más útil de toda esta guía, y la más fácil de olvidar.
Por último, deja los números apuntados donde la siguiente persona los encuentre: la retención que elegiste, el motivo y la fecha. Basta un comentario al principio del archivo de logrotate. La configuración de más arriba es una decisión, y una decisión que nadie puede reconstruir dentro de un año vuelve a convertirse en un valor por defecto.


