[{"content":" El problema # Tenía contenedores Docker corriendo servicios críticos en mi servidor doméstico: Nextcloud, bases de datos, aplicaciones personalizadas. Necesitaba backups, pero no quería enviar datos sin cifrar a internet. Tampoco quería instalar agentes adicionales o configuraciones complicadas. Decidí usar restic con Wireguard.\nLa solución que implementé # La idea es simple: restic hace backup del volumen Docker cifrado, lo envía a través de un túnel Wireguard (red privada) hacia un NAS o servidor remoto que también tengo en otra ubicación.\nRequisitos # Docker y Docker Compose funcionando Wireguard ya configurado entre servidor y almacenamiento remoto restic instalado en el servidor principal Acceso SSH o local al almacenamiento remoto Paso 1: Preparar el almacenamiento remoto # En el servidor remoto, cree un directorio para los backups:\nmkdir -p /mnt/backups/docker-restic chmod 700 /mnt/backups/docker-restic Inicialicé el repositorio restic:\nrestic init --repo /mnt/backups/docker-restic Esto me pidió una contraseña. La guardé en un gestor de contraseñas.\nPaso 2: Configurar credenciales en el servidor principal # En el servidor con Docker, creé un archivo de configuración para restic:\ncat \u0026gt; ~/.restic-env \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; export RESTIC_REPOSITORY=\u0026#34;rest:http://wg-storage.local/backups/docker-restic\u0026#34; export RESTIC_PASSWORD=\u0026#34;tu-contraseña-fuerte-aqui\u0026#34; export RESTIC_CACHE_DIR=\u0026#34;/var/cache/restic\u0026#34; EOF chmod 600 ~/.restic-env Usé rest:http:// porque configuré un servidor REST en el almacenamiento remoto usando rest-server. Si prefieres sftp:\nexport RESTIC_REPOSITORY=\u0026#34;sftp:wg-storage.local:/mnt/backups/docker-restic\u0026#34; Paso 3: Script de backup # Creé un script que backupea los volúmenes de Docker:\n#!/bin/bash set -e source ~/.restic-env BACKUP_PATHS=( \u0026#34;/var/lib/docker/volumes/nextcloud-data/_data\u0026#34; \u0026#34;/var/lib/docker/volumes/postgres-data/_data\u0026#34; \u0026#34;/opt/docker-apps\u0026#34; ) echo \u0026#34;[$(date)] Iniciando backup de Docker...\u0026#34; \u0026gt;\u0026gt; /var/log/docker-backup.log for path in \u0026#34;${BACKUP_PATHS[@]}\u0026#34;; do if [ -d \u0026#34;$path\u0026#34; ]; then restic backup \u0026#34;$path\u0026#34; \\ --tag \u0026#34;docker-backup\u0026#34; \\ --tag \u0026#34;$(date +%Y-%m-%d)\u0026#34; \\ \u0026gt;\u0026gt; /var/log/docker-backup.log 2\u0026gt;\u0026amp;1 fi done # Limpiar snapshots antiguos (mantener últimas 30) restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 12 --prune \\ \u0026gt;\u0026gt; /var/log/docker-backup.log 2\u0026gt;\u0026amp;1 echo \u0026#34;[$(date)] Backup completado\u0026#34; \u0026gt;\u0026gt; /var/log/docker-backup.log Guardé el script en /opt/backup-docker.sh y le di permisos:\nchmod 755 /opt/backup-docker.sh Paso 4: Automatizar con cron # Agregué una entrada a crontab para ejecutar el backup cada día a las 2 AM:\n0 2 * * * /opt/backup-docker.sh Paso 5: Verificar cifrado y conectividad # Ejecuté el backup manualmente la primera vez:\n/opt/backup-docker.sh Verifiqué que los datos llegaron cifrados al almacenamiento remoto:\nls -lah /mnt/backups/docker-restic/ Comprobé la conectividad Wireguard durante el backup:\nwg show restic snapshots Consideraciones importantes # Cifrado: restic cifra todo por defecto. Los datos viajan encriptados por Wireguard y se almacenan cifrados. Bandwidth: El primer backup es grande. En mi caso tardó horas. Los incrementales son mucho más rápidos. Monitoreo: Agregué alertas en caso de fallos del backup verificando el exit code del script. Recuperación: Probé restaurar un archivo para asegurarme que funciona: restic restore latest --target /tmp/test Resultado # Ahora tengo backups automáticos cifrados ejecutándose cada noche. Si algo falla en mi servidor, puedo restaurar los contenedores Docker desde el almacenamiento remoto sin preocuparme por datos expuestos. La combinación de restic + Wireguard + Docker es simple, segura y automatizada.\n","date":"29 juin 2026","externalUrl":null,"permalink":"/posts/backups-automaticos-y-cifrados-de-docker-con-restic-hacia-almacenamiento-remoto-via-wireguard/","section":"Posts","summary":"El problema # Tenía contenedores Docker corriendo servicios críticos en mi servidor doméstico: Nextcloud, bases de datos, aplicaciones personalizadas. Necesitaba backups, pero no quería enviar datos sin cifrar a internet. Tampoco quería instalar agentes adicionales o configuraciones complicadas. Decidí usar restic con Wireguard.\nLa solución que implementé # La idea es simple: restic hace backup del volumen Docker cifrado, lo envía a través de un túnel Wireguard (red privada) hacia un NAS o servidor remoto que también tengo en otra ubicación.\n","title":"Backups automáticos y cifrados de Docker con restic hacia almacenamiento remoto vía Wireguard","type":"posts"},{"content":"Llevo tiempo queriendo mejorar mi estrategia de backups en el servidor doméstico. Tener todo en Docker está bien, pero necesitaba algo más robusto que simplemente copiar archivos. Restic + MinIO resultó ser la combinación perfecta: backups incrementales, deduplicación y un S3 local sin depender de servicios en la nube.\nPor qué esta combinación # Restic es incremental por defecto, solo almacena lo que cambió. MinIO corre en el mismo servidor y expone una API S3 compatible, lo que evita dependencias externas. PostgreSQL se respalda en volúmenes Docker que también necesitan protección. Todo automatizado con cron.\nInstalación base # Primero, restic:\nsudo apt-get install restic MinIO ya está corriendo en Docker. Si no lo tienes:\ndocker run -d \\ --name minio \\ -p 9000:9000 \\ -p 9001:9001 \\ -e MINIO_ROOT_USER=minioadmin \\ -e MINIO_ROOT_PASSWORD=tupassword \\ -v /data/minio:/data \\ minio/minio server /data --console-address \u0026#34;:9001\u0026#34; Luego creo un usuario y bucket específicos para backups en la consola de MinIO (puerto 9001).\nConfigurar restic con MinIO # Inicializo el repositorio:\nexport AWS_ACCESS_KEY_ID=backup-user export AWS_SECRET_ACCESS_KEY=tu-clave-secreta export RESTIC_REPOSITORY=s3:http://localhost:9000/backups export RESTIC_PASSWORD=tu-password-restic restic init Guardo estas variables en un archivo seguro que solo el usuario root puede leer:\n# /root/.restic-env export AWS_ACCESS_KEY_ID=backup-user export AWS_SECRET_ACCESS_KEY=tu-clave-secreta export RESTIC_REPOSITORY=s3:http://localhost:9000/backups export RESTIC_PASSWORD=tu-password-restic chmod 600 /root/.restic-env Backup de PostgreSQL # PostgreSQL en Docker requiere un dump antes de respaldar. Creo un script:\n#!/bin/bash # /root/backup-postgres.sh set -e source /root/.restic-env BACKUP_DIR=\u0026#34;/tmp/postgres-backup\u0026#34; mkdir -p $BACKUP_DIR # Dump de la base de datos docker exec postgres-container pg_dump -U postgres -d tu_database \u0026gt; \\ $BACKUP_DIR/database-$(date +%s).sql # Backup con restic restic backup $BACKUP_DIR --tag postgres # Limpio rm -rf $BACKUP_DIR Backup de volúmenes Docker # Para los volúmenes de datos:\n#!/bin/bash # /root/backup-volumes.sh set -e source /root/.restic-env # Respalda volúmenes específicos montándolos docker run --rm -v mi_volumen:/data \\ -v /root:/root:ro \\ -w /data \\ alpine/restic:latest backup /data --tag docker-volumes Mejor aún, respaldo directamente la ruta donde Docker almacena los volúmenes:\nrestic backup /var/lib/docker/volumes/mi_volumen/_data --tag docker-volumes Automatización con cron # Creo un script maestro que ejecuta ambos backups:\n#!/bin/bash # /root/backup-all.sh source /root/.restic-env /root/backup-postgres.sh 2\u0026gt;\u0026amp;1 | logger -t restic-pg /root/backup-volumes.sh 2\u0026gt;\u0026amp;1 | logger -t restic-vol # Purgar snapshots antiguos (mantener 7 días) restic forget --keep-daily 7 --keep-monthly 3 --prune chmod +x /root/backup-all.sh En crontab:\n# Cron - ejecuta a las 2 AM diariamente 0 2 * * * /root/backup-all.sh Verificación y mantenimiento # Para ver el estado de los backups:\nrestic snapshots restic stats Para probar una restauración (sin tocar nada):\nrestic restore latest --target /tmp/test-restore Lo que aprendí # La deduplicación de restic es potente; después del primer backup, los incrementales son mínimos. MinIO ocupa poco espacio si configurás bien su limpieza de versiones antiguas. El dump de PostgreSQL debe hacerse antes del backup, no hay forma de respaldar la base activa sin riesgo de corrupción.\nEste setup me da tranquilidad. Si algo falla en el servidor, tengo backups automáticos, versionados y recuperables en cuestión de minutos.\n","date":"26 juin 2026","externalUrl":null,"permalink":"/posts/automatizar-backups-incrementales-de-postgresql-y-docker-con-restic-hacia-minio/","section":"Posts","summary":"Llevo tiempo queriendo mejorar mi estrategia de backups en el servidor doméstico. Tener todo en Docker está bien, pero necesitaba algo más robusto que simplemente copiar archivos. Restic + MinIO resultó ser la combinación perfecta: backups incrementales, deduplicación y un S3 local sin depender de servicios en la nube.\nPor qué esta combinación # Restic es incremental por defecto, solo almacena lo que cambió. MinIO corre en el mismo servidor y expone una API S3 compatible, lo que evita dependencias externas. PostgreSQL se respalda en volúmenes Docker que también necesitan protección. Todo automatizado con cron.\n","title":"Automatizar backups incrementales de PostgreSQL y Docker con restic hacia MinIO","type":"posts"},{"content":"","date":"26 juin 2026","externalUrl":null,"permalink":"/tags/minio/","section":"Tags","summary":"","title":"Minio","type":"tags"},{"content":" El problema # Tenía varios contenedores Docker corriendo en mi servidor doméstico con datos que no podía perder. Los backups manuales no escalan, así que decidí automatizar todo con rsync y cron. Aquí documento lo que funcionó.\nLa arquitectura # Mi setup:\nServidor local: Host Docker con volúmenes en /var/lib/docker/volumes/ Almacenamiento remoto: NAS en red con SSH habilitado Herramientas: rsync para sincronización incremental, cron para programación, sha256sum para verificación Preparación # Primero, configura acceso SSH sin contraseña desde el host Docker al NAS:\nssh-keygen -t ed25519 -f ~/.ssh/backup_key -N \u0026#34;\u0026#34; ssh-copy-id -i ~/.ssh/backup_key usuario@nas.local Verifica que funciona:\nssh -i ~/.ssh/backup_key usuario@nas.local \u0026#34;ls -la\u0026#34; Script de backup incremental # Crea /usr/local/bin/docker-backup.sh:\n#!/bin/bash BACKUP_USER=\u0026#34;usuario\u0026#34; BACKUP_HOST=\u0026#34;nas.local\u0026#34; BACKUP_PATH=\u0026#34;/mnt/backups/docker\u0026#34; SSH_KEY=\u0026#34;/root/.ssh/backup_key\u0026#34; LOG_FILE=\u0026#34;/var/log/docker-backup.log\u0026#34; MANIFEST=\u0026#34;/var/log/docker-backup-manifest.txt\u0026#34; # Volúmenes a respaldar (ajusta según tus necesidades) VOLUMES=(\u0026#34;postgres_data\u0026#34; \u0026#34;nextcloud_data\u0026#34; \u0026#34;app_config\u0026#34;) # Función para logging log() { echo \u0026#34;[$(date \u0026#39;+%Y-%m-%d %H:%M:%S\u0026#39;)] $1\u0026#34; \u0026gt;\u0026gt; \u0026#34;$LOG_FILE\u0026#34; } log \u0026#34;=== Iniciando backup incremental ===\u0026#34; # Crear directorio remoto si no existe ssh -i \u0026#34;$SSH_KEY\u0026#34; \u0026#34;$BACKUP_USER@$BACKUP_HOST\u0026#34; \u0026#34;mkdir -p $BACKUP_PATH\u0026#34; # Función para respaldar volumen backup_volume() { local volume=$1 local volume_path=\u0026#34;/var/lib/docker/volumes/${volume}/_data\u0026#34; local remote_path=\u0026#34;$BACKUP_USER@$BACKUP_HOST:$BACKUP_PATH/$volume\u0026#34; if [ ! -d \u0026#34;$volume_path\u0026#34; ]; then log \u0026#34;ERROR: Volumen $volume no encontrado en $volume_path\u0026#34; return 1 fi log \u0026#34;Respaldando volumen: $volume\u0026#34; # rsync incremental rsync -av --delete \\ -e \u0026#34;ssh -i $SSH_KEY\u0026#34; \\ \u0026#34;$volume_path/\u0026#34; \\ \u0026#34;$remote_path/\u0026#34; \u0026gt;\u0026gt; \u0026#34;$LOG_FILE\u0026#34; 2\u0026gt;\u0026amp;1 if [ $? -eq 0 ]; then log \u0026#34;✓ Backup completado: $volume\u0026#34; return 0 else log \u0026#34;✗ Error en backup: $volume\u0026#34; return 1 fi } # Respaldar todos los volúmenes FAILED=0 for volume in \u0026#34;${VOLUMES[@]}\u0026#34;; do backup_volume \u0026#34;$volume\u0026#34; || ((FAILED++)) done # Generar manifiesto con checksums log \u0026#34;Generando manifiesto de integridad...\u0026#34; { echo \u0026#34;Manifiesto de Backup - $(date \u0026#39;+%Y-%m-%d %H:%M:%S\u0026#39;)\u0026#34; echo \u0026#34;==========================================\u0026#34; for volume in \u0026#34;${VOLUMES[@]}\u0026#34;; do echo \u0026#34;Volumen: $volume\u0026#34; find \u0026#34;/var/lib/docker/volumes/${volume}/_data\u0026#34; -type f -exec sha256sum {} \\; 2\u0026gt;/dev/null echo \u0026#34;\u0026#34; done } \u0026gt; \u0026#34;$MANIFEST\u0026#34; # Respaldar el manifiesto también scp -i \u0026#34;$SSH_KEY\u0026#34; \u0026#34;$MANIFEST\u0026#34; \u0026#34;$BACKUP_USER@$BACKUP_HOST:$BACKUP_PATH/manifest-$(date +%Y%m%d).txt\u0026#34; if [ $FAILED -eq 0 ]; then log \u0026#34;=== Backup completado exitosamente ===\u0026#34; exit 0 else log \u0026#34;=== Backup completado con $FAILED errores ===\u0026#34; exit 1 fi Hazlo ejecutable:\nchmod +x /usr/local/bin/docker-backup.sh Verificación de integridad # Crea /usr/local/bin/docker-backup-verify.sh:\n#!/bin/bash BACKUP_USER=\u0026#34;usuario\u0026#34; BACKUP_HOST=\u0026#34;nas.local\u0026#34; BACKUP_PATH=\u0026#34;/mnt/backups/docker\u0026#34; SSH_KEY=\u0026#34;/root/.ssh/backup_key\u0026#34; LOG_FILE=\u0026#34;/var/log/docker-backup-verify.log\u0026#34; log() { echo \u0026#34;[$(date \u0026#39;+%Y-%m-%d %H:%M:%S\u0026#39;)] $1\u0026#34; \u0026gt;\u0026gt; \u0026#34;$LOG_FILE\u0026#34; } log \u0026#34;=== Iniciando verificación de integridad ===\u0026#34; # Descargar manifiesto más reciente LATEST_MANIFEST=$(ssh -i \u0026#34;$SSH_KEY\u0026#34; \u0026#34;$BACKUP_USER@$BACKUP_HOST\u0026#34; \\ \u0026#34;ls -t $BACKUP_PATH/manifest-*.txt | head -1\u0026#34;) if [ -z \u0026#34;$LATEST_MANIFEST\u0026#34; ]; then log \u0026#34;ERROR: No se encontró manifiesto remoto\u0026#34; exit 1 fi scp -i \u0026#34;$SSH_KEY\u0026#34; \u0026#34;$BACKUP_USER@$BACKUP_HOST:$LATEST_MANIFEST\u0026#34; /tmp/manifest-verify.txt # Extraer y verificar checksums while IFS= read -r line; do if [[ $line =~ ^[a-f0-9]{64} ]]; then hash=$(echo \u0026#34;$line\u0026#34; | awk \u0026#39;{print $1}\u0026#39;) file=$(echo \u0026#34;$line\u0026#34; | awk \u0026#39;{print $2}\u0026#39;) if [ -f \u0026#34;$file\u0026#34; ]; then local_hash=$(sha256sum \u0026#34;$file\u0026#34; | awk \u0026#39;{print $1}\u0026#39;) if [ \u0026#34;$hash\u0026#34; != \u0026#34;$local_hash\u0026#34; ]; then log \u0026#34;ALERTA: Checksum inconsistente en $file\u0026#34; fi fi fi done \u0026lt; /tmp/manifest-verify.txt log \u0026#34;=== Verificación completada ===\u0026#34; Programación con cron # Edita el crontab:\ncrontab -e Añade estas líneas:\n# Backup diario a las 2:00 AM 0 2 * * * /usr/local/bin/docker-backup.sh # Verificación semanal los domingos a las 3:00 AM 0 3 * * 0 /usr/local/bin/docker-backup-verify.sh # Enviar log por correo si hay errores 0 4 * * * [ -f /var/log/docker-backup.log ] \u0026amp;\u0026amp; tail -20 /var/log/docker-backup.log | mail -s \u0026#34;Backup Docker - Resumen\u0026#34; admin@example.com Monitoreo # Revisa los logs regularmente:\ntail -f /var/log/docker-backup.log Para investigar problemas de rsync específicos:\nrsync -av --dry-run /var/lib/docker/volumes/postgres_data/_data usuario@nas.local:/mnt/backups/docker/postgres_data/ Resultado # Ahora tengo backups incrementales automáticos cada noche. rsync solo transfiere cambios, lo que ahorra ancho de banda. La verificación de integridad me alerta si algo se corrompe. Es simple, func\n","date":"24 juin 2026","externalUrl":null,"permalink":"/posts/automatizar-backups-incrementales-de-volumenes-docker-a-almacenamiento-remoto-con-rsync-y-verificacion-de-integridad/","section":"Posts","summary":"El problema # Tenía varios contenedores Docker corriendo en mi servidor doméstico con datos que no podía perder. Los backups manuales no escalan, así que decidí automatizar todo con rsync y cron. Aquí documento lo que funcionó.\nLa arquitectura # Mi setup:\nServidor local: Host Docker con volúmenes en /var/lib/docker/volumes/ Almacenamiento remoto: NAS en red con SSH habilitado Herramientas: rsync para sincronización incremental, cron para programación, sha256sum para verificación Preparación # Primero, configura acceso SSH sin contraseña desde el host Docker al NAS:\n","title":"Automatizar backups incrementales de volúmenes Docker a almacenamiento remoto con rsync y verificación de integridad","type":"posts"},{"content":" El problema # Después de perder datos por un fallo de almacenamiento, aprendí que los contenedores Docker son efímeros. Los volúmenes que albergan bases de datos, configuraciones y archivos importantes necesitan protección. Backups manuales no escalan. Necesitaba algo automatizado, eficiente y verificable.\nElegí restic porque hace backups incrementales (solo guarda cambios), es agnóstico del destino (local, S3, B2, etc.) y verifica integridad con hash. Combinado con cron, tengo una solución sólida.\nInstalación # En mi servidor Debian, instalo restic:\nsudo apt-get update \u0026amp;\u0026amp; sudo apt-get install -y restic Para backups locales, creo el directorio de destino:\nsudo mkdir -p /mnt/backup-storage sudo chmod 700 /mnt/backup-storage Si usas almacenamiento remoto (recomendado), configura credenciales. Yo uso un bucket S3 local con Minio, pero la sintaxis es similar.\nInicializar repositorio # Restic necesita un repositorio inicializado. Lo hago una sola vez:\nrestic -r /mnt/backup-storage init Me pide una contraseña. La guardo en un gestor seguro. Sin ella, los backups son inútiles.\nScript de backup # Creo /usr/local/bin/backup-docker-volumes.sh:\n#!/bin/bash # Variables RESTIC_REPO=\u0026#34;/mnt/backup-storage\u0026#34; RESTIC_PASSWORD=\u0026#34;tu_contraseña_aqui\u0026#34; LOG_FILE=\u0026#34;/var/log/docker-backup.log\u0026#34; DOCKER_VOLUMES_PATH=\u0026#34;/var/lib/docker/volumes\u0026#34; # Timestamp TIMESTAMP=$(date \u0026#39;+%Y-%m-%d %H:%M:%S\u0026#39;) # Exportar variables para restic export RESTIC_REPOSITORY=\u0026#34;$RESTIC_REPO\u0026#34; export RESTIC_PASSWORD=\u0026#34;$RESTIC_PASSWORD\u0026#34; # Función de logging log() { echo \u0026#34;[$TIMESTAMP] $1\u0026#34; \u0026gt;\u0026gt; \u0026#34;$LOG_FILE\u0026#34; } log \u0026#34;=== Iniciando backup de volúmenes Docker ===\u0026#34; # Backup de volúmenes restic backup \u0026#34;$DOCKER_VOLUMES_PATH\u0026#34; \\ --tag docker-volumes \\ --exclude=\u0026#39;lost+found\u0026#39; \\ --exclude=\u0026#39;.git\u0026#39; \\ \u0026gt;\u0026gt; \u0026#34;$LOG_FILE\u0026#34; 2\u0026gt;\u0026amp;1 if [ $? -eq 0 ]; then log \u0026#34;✓ Backup completado exitosamente\u0026#34; else log \u0026#34;✗ Error durante el backup\u0026#34; exit 1 fi # Limpiar snapshots antiguos (mantener últimos 30 días) restic forget --keep-daily 30 --prune \u0026gt;\u0026gt; \u0026#34;$LOG_FILE\u0026#34; 2\u0026gt;\u0026amp;1 log \u0026#34;=== Limpieza de snapshots antiguos completada ===\u0026#34; # Verificar integridad restic check \u0026gt;\u0026gt; \u0026#34;$LOG_FILE\u0026#34; 2\u0026gt;\u0026amp;1 if [ $? -eq 0 ]; then log \u0026#34;✓ Verificación de integridad exitosa\u0026#34; else log \u0026#34;✗ Error en verificación de integridad\u0026#34; exit 1 fi Hago el script ejecutable:\nsudo chmod +x /usr/local/bin/backup-docker-volumes.sh Programar con cron # Edito la tabla cron del root:\nsudo crontab -e Agrego esta línea para ejecutar backups diariamente a las 2 AM:\n0 2 * * * /usr/local/bin/backup-docker-volumes.sh Para backups cada 6 horas:\n0 */6 * * * /usr/local/bin/backup-docker-volumes.sh Verifico que se ejecute:\nsudo grep CRON /var/log/syslog | tail -5 Recuperación ante desastres # Cuando necesito restaurar:\n# Listar snapshots disponibles restic snapshots # Restaurar un snapshot específico restic restore [snapshot-id] --target /mnt/restore-point Si un contenedor se corrompió, detengo el stack, restauro el volumen y reinicio:\ndocker-compose down restic restore [snapshot-id] --target /var/lib/docker/volumes/mi-volumen docker-compose up -d Buenas prácticas aprendidas # Nunca guardes la contraseña en texto plano en cron. Yo uso un archivo /root/.restic-pass con permisos 600. Monitorea los logs. Configura alertas si los backups fallan. Prueba restauraciones regularmente. Un backup no probado es un backup que no funciona. Guarda backups fuera de tu servidor. La redundancia local no te protege de hardware fallido. Cifra los backups si los envías a cloud. Restic lo hace por defecto. Resultado # Ahora duermo tranquilo. Mis volúmenes Docker se respaldan cada 6 horas, solo guardo cambios (restic es eficiente), y puedo recuperar cualquier cosa en minutos. El destino de mis backups es un NAS en otra ubicación, así que aunque el servidor explote, mis datos están seguros.\n","date":"22 juin 2026","externalUrl":null,"permalink":"/posts/automatizar-backups-incrementales-de-volumenes-docker-con-restic-y-cron/","section":"Posts","summary":"El problema # Después de perder datos por un fallo de almacenamiento, aprendí que los contenedores Docker son efímeros. Los volúmenes que albergan bases de datos, configuraciones y archivos importantes necesitan protección. Backups manuales no escalan. Necesitaba algo automatizado, eficiente y verificable.\nElegí restic porque hace backups incrementales (solo guarda cambios), es agnóstico del destino (local, S3, B2, etc.) y verifica integridad con hash. Combinado con cron, tengo una solución sólida.\n","title":"Automatizar backups incrementales de volúmenes Docker con restic y cron","type":"posts"},{"content":"","date":"22 juin 2026","externalUrl":null,"permalink":"/tags/recuperaci%C3%B3n-desastres/","section":"Tags","summary":"","title":"Recuperación-Desastres","type":"tags"},{"content":" El problema # Tenía PostgreSQL corriendo en Docker con volúmenes que contenían datos críticos. Los backups manuales no escalan y respaldar todo cada vez es ineficiente. Necesitaba algo automatizado, incremental y que verificara que los datos llegaran bien al NAS.\nPor qué restic # Restic maneja snapshots incrementales nativamente, comprime, cifra y permite verificar la integridad de los backups remotos sin descargar todo. Perfecto para un servidor doméstico donde el ancho de banda importa.\nPrerequisitos # Docker con PostgreSQL corriendo NAS accesible por SSH o SMB restic instalado en el host pg_dump o acceso al contenedor PostgreSQL Script de automatización (systemd timer o cron) Paso 1: Preparar el volumen de datos # Primero, identifico dónde está el volumen de PostgreSQL:\ndocker volume inspect postgres_data Esto me muestra la ruta. En mi caso: /var/lib/docker/volumes/postgres_data/_data\nPaso 2: Configurar acceso al NAS # Monto el NAS en el host. Uso SSH con restic:\nexport RESTIC_REPOSITORY=\u0026#34;sftp:usuario@192.168.1.100:/backups/postgres\u0026#34; export RESTIC_PASSWORD=\u0026#34;contraseña-fuerte\u0026#34; Guardo estas variables en un archivo seguro:\ncat \u0026gt; /etc/restic/env.sh \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; export RESTIC_REPOSITORY=\u0026#34;sftp:usuario@192.168.1.100:/backups/postgres\u0026#34; export RESTIC_PASSWORD=\u0026#34;tu-password\u0026#34; EOF chmod 600 /etc/restic/env.sh Paso 3: Crear script de backup # El script hace tres cosas: dump de la BD, backup con restic y verificación:\n#!/bin/bash set -e source /etc/restic/env.sh POSTGRES_CONTAINER=\u0026#34;postgres\u0026#34; BACKUP_DIR=\u0026#34;/tmp/pg_backups\u0026#34; LOG_FILE=\u0026#34;/var/log/restic-postgres.log\u0026#34; # Crear directorio temporal mkdir -p $BACKUP_DIR # Dump de PostgreSQL docker exec $POSTGRES_CONTAINER pg_dump -U postgres nombrebd | \\ gzip \u0026gt; $BACKUP_DIR/db_$(date +%Y%m%d_%H%M%S).sql.gz # Backup incremental con restic echo \u0026#34;[$(date)] Iniciando backup...\u0026#34; \u0026gt;\u0026gt; $LOG_FILE restic backup $BACKUP_DIR \\ --tag postgres \\ --tag incremental \\ 2\u0026gt;\u0026amp;1 \u0026gt;\u0026gt; $LOG_FILE # Verificar integridad echo \u0026#34;[$(date)] Verificando integridad...\u0026#34; \u0026gt;\u0026gt; $LOG_FILE restic check --read-data 2\u0026gt;\u0026amp;1 \u0026gt;\u0026gt; $LOG_FILE if [ $? -eq 0 ]; then echo \u0026#34;[$(date)] Backup verificado correctamente\u0026#34; \u0026gt;\u0026gt; $LOG_FILE rm -rf $BACKUP_DIR else echo \u0026#34;[$(date)] ERROR: Verificación fallida\u0026#34; \u0026gt;\u0026gt; $LOG_FILE exit 1 fi Lo guardo en /usr/local/bin/backup-postgres.sh y lo hago ejecutable:\nchmod +x /usr/local/bin/backup-postgres.sh Paso 4: Automatizar con systemd timer # Creo un servicio y un timer para ejecutar diariamente:\ncat \u0026gt; /etc/systemd/system/restic-postgres.service \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; [Unit] Description=Restic Backup PostgreSQL After=docker.service Wants=restic-postgres.timer [Service] Type=oneshot ExecStart=/usr/local/bin/backup-postgres.sh StandardOutput=journal StandardError=journal EOF cat \u0026gt; /etc/systemd/system/restic-postgres.timer \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; [Unit] Description=Restic PostgreSQL Timer Requires=restic-postgres.service [Timer] OnCalendar=daily OnCalendar=03:00 Persistent=true [Install] WantedBy=timers.target EOF Activo los timers:\nsystemctl daemon-reload systemctl enable --now restic-postgres.timer Paso 5: Monitorear y verificar # Chequeo el estado:\nsystemctl status restic-postgres.timer journalctl -u restic-postgres.service -n 50 Para ver snapshots disponibles en el NAS:\nrestic snapshots Verificación periódica # Cada mes ejecuto una verificación completa de lectura:\nrestic check --read-data-subset=10% Conclusión # Con esto tengo backups incrementales diarios, cifrados, verificados automáticamente y almacenados en el NAS. El overhead es mínimo porque restic solo sube lo que cambió. Si algo falla, systemd me avisa en el log y puedo investigar.\nEl punto crítico: restic verifica que los datos se escribieron bien sin descargar todo. Eso vale oro en un backup.\n","date":"19 juin 2026","externalUrl":null,"permalink":"/posts/automatizar-backups-incrementales-de-volumenes-docker-en-postgresql-con-restic-y-verificacion-en-nas/","section":"Posts","summary":"El problema # Tenía PostgreSQL corriendo en Docker con volúmenes que contenían datos críticos. Los backups manuales no escalan y respaldar todo cada vez es ineficiente. Necesitaba algo automatizado, incremental y que verificara que los datos llegaran bien al NAS.\nPor qué restic # Restic maneja snapshots incrementales nativamente, comprime, cifra y permite verificar la integridad de los backups remotos sin descargar todo. Perfecto para un servidor doméstico donde el ancho de banda importa.\n","title":"Automatizar backups incrementales de volúmenes Docker en PostgreSQL con restic y verificación en NAS","type":"posts"},{"content":"","date":"19 juin 2026","externalUrl":null,"permalink":"/tags/nas/","section":"Tags","summary":"","title":"Nas","type":"tags"},{"content":"","date":"18 juin 2026","externalUrl":null,"permalink":"/tags/802.1x/","section":"Tags","summary":"","title":"802.1x","type":"tags"},{"content":"","date":"18 juin 2026","externalUrl":null,"permalink":"/tags/active-directory/","section":"Tags","summary":"","title":"Active-Directory","type":"tags"},{"content":"","date":"18 juin 2026","externalUrl":null,"permalink":"/tags/adcs/","section":"Tags","summary":"","title":"Adcs","type":"tags"},{"content":"","date":"18 juin 2026","externalUrl":null,"permalink":"/categories/administraci%C3%B3n-de-sistemas/","section":"Categories","summary":"","title":"Administración De Sistemas","type":"categories"},{"content":"","date":"18 juin 2026","externalUrl":null,"permalink":"/categories/an%C3%A1lisis-de-incidentes/","section":"Categories","summary":"","title":"Análisis De Incidentes","type":"categories"},{"content":"","date":"18 juin 2026","externalUrl":null,"permalink":"/tags/certificados/","section":"Tags","summary":"","title":"Certificados","type":"tags"},{"content":" El problema: ese aviso que aparece cada vez # Si administras un FortiGate y tienes usuarios conectándose por VPN SSL, ya conoces el escenario: el cliente FortiClient o el navegador muestra un aviso de \u0026ldquo;certificado no confiable\u0026rdquo; antes de establecer la conexión. Los usuarios hacen clic en \u0026ldquo;Continuar de todas formas\u0026rdquo; sin leerlo, y el área de seguridad pierde credibilidad cada vez que alguien les pregunta si eso es normal.\nEl motivo es simple: FortiGate viene de fábrica con un certificado autofirmado (Fortinet_CA_SSL o similar). Los navegadores y sistemas operativos no incluyen esa CA en su almacén de confianza, así que lo marcan como no confiable. No es un fallo del appliance; es el comportamiento correcto del cliente ante una CA desconocida.\nLa solución permanente es reemplazar ese certificado autofirmado por uno emitido por una autoridad en la que los clientes ya confíen.\nTres opciones para resolver el problema # Antes de entrar en detalle, conviene conocer las tres vías disponibles:\nOpción 1 — CA interna con ADCS (recomendada si tienes dominio Windows): usas tu propia infraestructura de clave pública. Los equipos unidos al dominio ya confían en tu CA automáticamente. Sin coste adicional y con control total sobre la renovación.\nOpción 2 — Let\u0026rsquo;s Encrypt con ACME nativo (FortiGate 6.4 en adelante): válida si el FortiGate tiene un FQDN público resolvible desde internet. FortiOS implementa el protocolo ACME y renueva el certificado automáticamente. No sirve para appliances detrás de NAT sin nombre público.\nOpción 3 — Certificado comercial: compras un certificado a una CA reconocida (DigiCert, Sectigo, etc.). El proceso de importación es idéntico a la opción 1, pero sin necesitar ADCS. Tiene coste anual y depende de un tercero para la renovación.\nEn la mayoría de entornos corporativos con Active Directory, la opción 1 es la más limpia: aprovecha infraestructura ya existente, los equipos del dominio confían en la CA sin configuración adicional y tienes control total.\nOpción 1 paso a paso: ADCS como CA firmante # Requisitos previos # Windows Server con el rol Active Directory Certificate Services (ADCS) instalado y configurado como CA empresarial (Enterprise CA). Acceso de administrador al FortiGate. El FortiGate debe tener un FQDN o nombre DNS interno que resuelva correctamente desde los clientes. Puede ser solo un nombre interno, no necesita ser público. Paso 1 — Generar el CSR en FortiGate # Un CSR (Certificate Signing Request) es la solicitud que el FortiGate genera para pedirle a la CA que emita un certificado. Contiene la clave pública y los datos de identidad del appliance.\nEn la interfaz de FortiGate:\nVe a System \u0026gt; Certificates \u0026gt; Generate. Configura los campos: Campo Valor recomendado Certificate Name fortigate-vpn (nombre interno, sin espacios) Key Type RSA Key Size 2048 bits (mínimo; 4096 si la CA lo soporta) Digest Algorithm SHA-256 Common Name (CN) FQDN del FortiGate, p. ej. vpn.empresa.local Organization (O) Nombre de tu organización Locality / State / Country Datos reales (la CA puede requerirlos) En Subject Alternative Names añade:\nTipo DNS: el FQDN que usan los clientes para conectarse. Tipo IP: la IP del interfaz SSL-VPN, si los clientes se conectan por IP. Los SAN son obligatorios para que Chrome y navegadores modernos acepten el certificado; el CN solo ya no es suficiente.\nHaz clic en OK. FortiGate descarga automáticamente un archivo .csr.\nPaso 2 — Firmar el CSR en ADCS # Transfiere el archivo .csr a un servidor con acceso a la CA. Hay dos métodos:\nMétodo A — línea de comandos (recomendado para automatizar):\ncertreq -submit -attrib \u0026#34;CertificateTemplate:WebServer\u0026#34; fortigate.csr fortigate.cer Si tienes varias CAs en el entorno, especifica cuál usar:\ncertreq -submit -config \u0026#34;CA-SERVER\\Nombre-CA\u0026#34; -attrib \u0026#34;CertificateTemplate:WebServer\u0026#34; fortigate.csr fortigate.cer El resultado es el archivo fortigate.cer con el certificado firmado.\nMétodo B — interfaz web de ADCS:\nAbre http://CA-SERVER/certsrv en un navegador. Haz clic en Solicitar un certificado. Elige Enviar una solicitud de certificado avanzada. Selecciona Enviar una solicitud de certificado mediante un archivo PKCS#10 codificado en Base 64. Pega el contenido del .csr y en Plantilla de certificado elige Servidor web. Descarga el certificado en formato Base 64 (.cer). Si el botón de descarga pide reiniciar la solicitud, usa el método de línea de comandos; es más fiable con CSRs generados fuera de Windows.\nPaso 3 — Importar el certificado firmado en FortiGate # En FortiGate, ve a System \u0026gt; Certificates \u0026gt; Import \u0026gt; Local Certificate. Selecciona el archivo fortigate.cer que generó la CA. FortiGate lo asocia automáticamente con el CSR que generaste antes porque las claves coinciden. Verifica que el nuevo certificado aparece en la lista con el nombre que configuraste y que el campo Issuer muestra el nombre de tu CA interna. Paso 4 — Asignar el certificado a SSL-VPN # El certificado importado no entra en servicio hasta que lo asignas explícitamente:\nVe a VPN \u0026gt; SSL-VPN Settings. En el campo Server Certificate, selecciona el certificado que acabas de importar (fortigate-vpn en el ejemplo). Guarda los cambios. FortiGate recarga el servicio SSL; los clientes conectados en ese momento pueden notar una desconexión breve. Distribución del certificado raíz via GPO # El certificado del FortiGate está firmado por tu CA interna, pero si los equipos cliente no tienen el certificado raíz de esa CA en su almacén de confianza, seguirán viendo el aviso. La distribución automática via Directiva de Grupo (GPO) resuelve esto de forma centralizada.\nEn el Controlador de Dominio o desde la consola GPMC:\nCrea o edita una GPO aplicada a los equipos que usan la VPN. Navega a: Computer Configuration \u0026gt; Windows Settings \u0026gt; Security Settings \u0026gt; Public Key Policies \u0026gt; Trusted Root Certification Authorities Haz clic derecho \u0026gt; Import e importa el certificado raíz de tu CA (.cer de la CA, no del FortiGate). Aplica y cierra. Los equipos recibirán el certificado raíz en el siguiente ciclo de actualización de directivas (normalmente 90 minutos, o puedes forzarlo con gpupdate /force). Para los equipos que no están en el dominio (dispositivos personales, móviles), tendrás que distribuir el certificado raíz manualmente o mediante MDM.\nVerificación # Una vez completado el proceso, verifica que todo funciona correctamente:\nDesde FortiClient:\nAbre FortiClient e intenta conectarte a la VPN SSL. Si el proceso fue correcto, la conexión se establece sin ningún aviso de certificado. La diferencia respecto al comportamiento anterior es inmediata.\nDesde el navegador:\nAccede a la interfaz de administración del FortiGate o al portal web SSL-VPN por HTTPS. El candado del navegador debe aparecer cerrado y sin advertencias. Haz clic en él para confirmar que el emisor es tu CA interna.\nValidación con certutil:\nEn cualquier equipo Windows del dominio:\ncertutil -verify fortigate.cer La salida debe terminar con CertUtil: -verify command completed successfully. Si hay errores de cadena, el problema está en la distribución del certificado raíz.\nPara comprobar la cadena completa:\ncertutil -verify -urlfetch fortigate.cer Esto verifica también los puntos de distribución CRL (listas de revocación) configurados en la CA.\nPor qué ADCS es la opción más limpia en entornos con dominio # La ventaja real no está en la firma del certificado en sí, sino en la distribución de confianza. Cuando una organización tiene un dominio Windows, los equipos unidos al dominio reciben el certificado raíz de la CA interna de forma automática desde el momento en que se unen al dominio. No hay que hacer nada adicional: la GPO que distribuye el certificado raíz ya existe por defecto en la mayoría de instalaciones de ADCS empresarial.\nComparado con las alternativas:\nLet\u0026rsquo;s Encrypt: requiere que el FortiGate tenga un FQDN público y responda al desafío ACME. En redes corporativas con FortiGates detrás de NAT o con nombre solo interno, esto no es viable. Además, la renovación cada 90 días depende de que el proceso automático funcione sin interrupciones. Certificado comercial: resuelve el problema para cualquier dispositivo (incluso los que no están en el dominio), pero tiene coste anual y el ciclo de renovación es manual o semi-manual. Útil cuando hay clientes externos que no controlas. Con ADCS, el ciclo de vida completo (emisión, renovación, revocación) queda dentro de tu infraestructura. La renovación antes de la expiración sigue el mismo proceso: nuevo CSR desde FortiGate, firma en ADCS, reimportación y reasignación. Algunos equipos automatizan este paso con scripts que invocan certreq contra la CA y suben el certificado resultado via la API de FortiGate.\nConclusión # El aviso de certificado no confiable no es un problema estético: erosiona la confianza de los usuarios en los controles de seguridad y, en el peor caso, los entrena a ignorar avisos SSL. Resolverlo con ADCS en entornos con Active Directory es directo, sin coste y deja la infraestructura en mejor estado que antes.\nEl proceso tiene cuatro pasos reales: generar el CSR en FortiGate, firmarlo en ADCS, importar el certificado firmado de vuelta y asignarlo a SSL-VPN. La distribución via GPO hace que la experiencia del usuario sea transparente desde el primer ciclo de actualización de directivas.\n","date":"18 juin 2026","externalUrl":null,"permalink":"/posts/certificados-ssl-fortigate-adcs-windows-server/","section":"Posts","summary":"El problema: ese aviso que aparece cada vez # Si administras un FortiGate y tienes usuarios conectándose por VPN SSL, ya conoces el escenario: el cliente FortiClient o el navegador muestra un aviso de “certificado no confiable” antes de establecer la conexión. Los usuarios hacen clic en “Continuar de todas formas” sin leerlo, y el área de seguridad pierde credibilidad cada vez que alguien les pregunta si eso es normal.\n","title":"Certificados SSL en FortiGate con ADCS: elimina el aviso de certificado no confiable","type":"posts"},{"content":"","date":"18 juin 2026","externalUrl":null,"permalink":"/tags/cve/","section":"Tags","summary":"","title":"Cve","type":"tags"},{"content":"","date":"18 juin 2026","externalUrl":null,"permalink":"/categories/diagn%C3%B3stico/","section":"Categories","summary":"","title":"Diagnóstico","type":"categories"},{"content":"","date":"18 juin 2026","externalUrl":null,"permalink":"/tags/diagn%C3%B3stico/","section":"Tags","summary":"","title":"Diagnóstico","type":"tags"},{"content":"","date":"18 juin 2026","externalUrl":null,"permalink":"/tags/eap-tls/","section":"Tags","summary":"","title":"Eap-Tls","type":"tags"},{"content":"Cuando un cliente pregunta si puede añadir TOTP al WiFi corporativo para cumplir con su política de MFA, la respuesta técnicamente correcta es \u0026ldquo;sí, pero no deberías\u0026rdquo;. Este artículo explica por qué TOTP es prácticamente inutilizable en 802.1X, cómo comparar los métodos EAP más comunes y cuál es la ruta recomendada hacia una autenticación WiFi robusta.\nEl problema de TOTP en redes inalámbricas # La intuición detrás de querer TOTP en WiFi es razonable: si lo usas en VPN y en aplicaciones web, ¿por qué no en la red inalámbrica? El problema está en cómo funciona el supplicant WiFi.\nEn cualquier dispositivo —Windows, iOS, Android— el cliente WiFi guarda las credenciales para reconectar automáticamente. Cada vez que el dispositivo pierde señal, cambia de punto de acceso o sale de suspensión, el supplicant intenta la autenticación sin intervención del usuario. Ese comportamiento es el que hace que el WiFi empresarial sea usable.\nAhora añade un OTP con validez de 30 segundos a esa ecuación:\nEl usuario introduce su usuario, contraseña y el código TOTP del momento. La autenticación funciona. El dispositivo se desconecta (ascensor, parking, suspensión). El supplicant intenta reconectar automáticamente con las mismas credenciales. El código TOTP ya ha caducado. La autenticación falla. El usuario ve que no tiene WiFi sin entender por qué. El helpdesk empieza a recibir llamadas. Los usuarios aprenden a desactivar el WiFi y usar datos móviles. La política de seguridad ha conseguido exactamente lo contrario de lo que pretendía.\nEl método que haría esto posible técnicamente es PEAP/GTC (Generic Token Card), que transmite la contraseña en texto plano dentro del túnel TLS externo. A diferencia de MSCHAPv2, GTC no aplica hash a las credenciales, por lo que el servidor RADIUS puede verificar un OTP en tiempo real. Pero sin caché de credenciales, cada reconexión requiere intervención manual del usuario. En la práctica, es inaceptable para cualquier entorno de producción.\nComparativa de métodos EAP # Antes de elegir un método EAP hay que entender qué hace cada uno con las credenciales y qué implica para la infraestructura:\nMétodo Autenticación interna Requiere cert. cliente Compatible con TOTP Experiencia de usuario PEAP/MSCHAPv2 NTLM hash No No Buena PEAP/GTC Texto plano (en túnel TLS) No Teóricamente sí Mala (sin caché) EAP-TLS Certificado X.509 Sí N/A Excelente PEAP/MSCHAPv2 es el punto de partida habitual. El dispositivo abre un túnel TLS con el servidor RADIUS usando el certificado del servidor, y dentro negocia MSCHAPv2 con el hash NTLMv2 de la contraseña. No requiere certificado en el cliente, solo un par usuario/contraseña. La reconexión automática funciona perfectamente porque las credenciales son estáticas.\nLa debilidad de MSCHAPv2 es que el hash NTLMv2 es vulnerable a ataques offline si alguien captura el intercambio y el diccionario de contraseñas es débil. Además, si el dispositivo no valida correctamente el certificado del servidor RADIUS, es posible un ataque man-in-the-middle con un RADIUS falso.\nEAP-TLS elimina estas debilidades por diseño: no hay contraseña que capturar ni hash que atacar.\nEAP-TLS: el certificado como factor de autenticación # En EAP-TLS, ambos lados presentan un certificado: el servidor RADIUS demuestra al cliente que es legítimo, y el dispositivo demuestra al RADIUS que está autorizado. La autenticación es mutua por construcción.\nEl certificado de dispositivo cumple la función de \u0026ldquo;algo que tienes\u0026rdquo; de forma nativa. No necesitas añadir un segundo factor porque el certificado ya es ese segundo factor: está vinculado a un dispositivo concreto, fue emitido por tu CA interna, y solo puede usarse desde ese dispositivo (la clave privada nunca sale del TPM o del almacén de certificados del sistema operativo).\nVentajas prácticas:\nNo hay credenciales que phishear. Un atacante no puede engañar a un usuario para que \u0026ldquo;introduzca su contraseña WiFi\u0026rdquo; porque no existe tal contraseña. No hay credenciales que rotar. Cuando un empleado se va, revocas su certificado. El dispositivo queda excluido de la red en el siguiente intento de autenticación, sin necesidad de cambiar contraseñas de grupo ni tocar la GPO de WiFi. La reconexión automática funciona perfectamente. El certificado no caduca en 30 segundos. El coste es la infraestructura: necesitas una CA interna que emita certificados a los dispositivos. En entornos con Active Directory, esto significa ADCS (Active Directory Certificate Services), y el proceso de distribución se automatiza con autoenrollment via GPO.\nImplementación básica con FreeRADIUS + ADCS # Certificados en ADCS # El primer paso es crear una plantilla de certificado en ADCS para autenticación de equipos. La plantilla debe tener:\nPropósito: Autenticación de cliente (OID 1.3.6.1.5.5.7.3.2) Nombre del sujeto: Construido desde Active Directory (nombre DNS del equipo) Permisos de inscripción: Equipos del dominio → Leer + Inscribir automáticamente Con esta plantilla publicada, el autoenrollment vía GPO distribuye los certificados sin intervención del usuario. Los equipos del dominio solicitan y renuevan sus propios certificados automáticamente.\nConfiguración de FreeRADIUS # El módulo EAP de FreeRADIUS necesita apuntar a los ficheros de certificado del servidor y activar la verificación del cliente:\neap { default_eap_type = tls tls { private_key_file = /etc/ssl/private/radius.key certificate_file = /etc/ssl/certs/radius.pem CA_file = /etc/ssl/certs/ca.pem dh_file = /etc/ssl/certs/dh.pem verify { client = yes } } } El parámetro CA_file debe apuntar a la cadena completa de tu CA interna —raíz e intermedias si las hay—. FreeRADIUS verificará que el certificado presentado por el cliente esté firmado por esa CA y no haya sido revocado. Para la revocación en tiempo real puedes configurar CRL o OCSP en el mismo bloque tls.\nEl certificado del servidor (radius.pem) debe estar firmado por la misma CA o una CA de confianza que los clientes conozcan. Si usas ADCS, lo más limpio es emitir también el certificado del servidor RADIUS desde allí.\nGPO para distribución automática # Dos objetos de directiva de grupo cubren el despliegue completo:\nGPO de autoenrollment (Computer Configuration → Windows Settings → Security Settings → Public Key Policies):\nActivar el autoenrollment de certificados de equipo Renovar automáticamente los certificados caducados Actualizar los certificados que usan plantillas de certificado GPO del perfil WiFi (Computer Configuration → Windows Settings → Security Settings → Wireless Network Policies):\nSSID corporativo Tipo de seguridad: WPA2-Enterprise Método EAP: Microsoft: Smart Card u otro certificado CA de validación: tu CA interna (marcada como confianza) Sin prompt al usuario (silent authentication) Con ambas GPO aplicadas, un equipo que se une al dominio recibe su certificado y su perfil WiFi automáticamente. El usuario no configura nada: la red aparece y conecta sola.\nRuta de migración recomendada # No todos los entornos pueden pasar a EAP-TLS de un día para otro. La infraestructura PKI requiere planificación: definir la jerarquía de CA, decidir si la raíz es online u offline, establecer el ciclo de vida de los certificados y preparar el proceso de revocación.\nUna ruta pragmática:\nFase 1 — Hoy: Desplegar WPA2-Enterprise con PEAP/MSCHAPv2 si aún no lo tienes. Es infinitamente mejor que WPA2-PSK (contraseña compartida). Asegúrate de que los clientes validan el certificado del servidor RADIUS para evitar ataques MitM.\nFase 2 — Medio plazo: Montar ADCS si no existe o auditar la CA existente. Publicar la plantilla de autoenrollment para equipos. Hacer una prueba piloto con EAP-TLS en una VLAN separada.\nFase 3 — Objetivo: Migrar el SSID corporativo a EAP-TLS. Mantener temporalmente un SSID secundario con PEAP/MSCHAPv2 para dispositivos de invitados o equipos no gestionados.\nLo que nunca deberías hacer: activar PEAP/GTC con TOTP en producción. El impacto en la experiencia de usuario garantiza que la política no se cumplirá —los usuarios buscarán alternativas— y el helpdesk pagará las consecuencias.\nConclusión # TOTP es una herramienta excelente para autenticación web y VPN, donde el usuario inicia la sesión de forma explícita. En WiFi empresarial, el modelo de reconexión automática lo hace incompatible con la realidad operativa.\nLa respuesta correcta al \u0026ldquo;quiero MFA en el WiFi\u0026rdquo; no es añadir TOTP: es migrar a EAP-TLS, donde el certificado de dispositivo ya incorpora el segundo factor de forma transparente para el usuario y robusta para el equipo de seguridad.\n","date":"18 juin 2026","externalUrl":null,"permalink":"/posts/eap-tls-vs-peap-wifi-empresarial-802-1x/","section":"Posts","summary":"Cuando un cliente pregunta si puede añadir TOTP al WiFi corporativo para cumplir con su política de MFA, la respuesta técnicamente correcta es “sí, pero no deberías”. Este artículo explica por qué TOTP es prácticamente inutilizable en 802.1X, cómo comparar los métodos EAP más comunes y cuál es la ruta recomendada hacia una autenticación WiFi robusta.\nEl problema de TOTP en redes inalámbricas # La intuición detrás de querer TOTP en WiFi es razonable: si lo usas en VPN y en aplicaciones web, ¿por qué no en la red inalámbrica? El problema está en cómo funciona el supplicant WiFi.\n","title":"EAP-TLS vs PEAP para WiFi empresarial: cuándo usar certificados y por qué TOTP no encaja en 802.1X","type":"posts"},{"content":"Configurar FreeRADIUS 3.x con autenticación PAM —especialmente combinando pam_google_authenticator, pam_winbind y fail2ban— genera un conjunto de errores que aparecen una y otra vez. Este artículo los recoge con causa exacta y solución directa, sin rodeos.\nTabla resumen # # Síntoma Causa raíz Solución rápida 1 Failed to change user id to \u0026quot;usuario\u0026quot; user=root en pam conf, freerad no puede hacer setuid user=freerad + chown freerad /etc/google-authenticator/ 2 user not found / getpwnam() failed Winbind sin idmap RID, los usuarios AD no tienen UID Unix Añadir bloques idmap config en smb.conf + reiniciar winbind 3 Secret file permissions (0644) are more permissive than 0600 Permisos demasiado abiertos en el fichero TOTP chmod 600 /etc/google-authenticator/* 4 Failed to create tempfile: Permission denied Directorio TOTP propiedad de root, freerad no puede escribir chown freerad:freerad /etc/google-authenticator/ 5 Contraseñas visibles en el log tras desactivarlas auth_badpass requiere reinicio, no reload; auth_log loguea independientemente systemctl restart freeradius + revisar módulo auth_log 6 No Auth-Type found: rejecting via Post-Auth-Type = Reject El virtual server no fuerza Auth-Type a PAP para rutas PAM Añadir update control { \u0026amp;Auth-Type := PAP } en authorize 7 fail2ban no arranca: Jail 'sshd' is not valid logpath duplicado en jail.local o jails de servicios inexistentes fail2ban-client -t + eliminar duplicados y jails inactivos Error 1: Failed to change user id to \u0026quot;usuario\u0026quot; # Síntoma # (0) pam: pam_authenticate: Failed to change user id to \u0026#34;usuario\u0026#34; (0) pam: ERROR: PAM auth for user \u0026#34;usuario\u0026#34; failed Causa # pam_google_authenticator.so se carga con el parámetro user=root, lo que obliga a PAM a hacer setuid(root) antes de leer el fichero TOTP. El proceso freeradius corre como usuario freerad y no tiene permiso para asumir la identidad de root.\nSolución # En /etc/pam.d/radiusd, cambiar user=root por user=freerad:\n# /etc/pam.d/radiusd auth required pam_google_authenticator.so secret=/etc/google-authenticator/${USER} user=freerad Asegurarse de que los ficheros TOTP son legibles por freerad:\nchown freerad /etc/google-authenticator/ ls -la /etc/google-authenticator/ Verificación # id freerad # uid=116(freerad) gid=125(freerad) ... ls -la /etc/google-authenticator/ # drwxr-xr-x freerad freerad . # -rw------- freerad freerad usuario Error 2: user not found / getpwnam() failed # Síntoma # pam_winbind(radiusd:auth): user \u0026#39;DOMINIO\\usuario\u0026#39; not found getpwnam() failed: No such user Causa # Winbind puede autenticar al usuario contra Active Directory (wbinfo -a funciona), pero si smb.conf no tiene una configuración idmap correcta para el dominio, getpwnam() no puede asignar un UID Unix al usuario AD. El sistema operativo no lo encuentra aunque las credenciales sean válidas.\nSolución # Añadir los bloques idmap config en /etc/samba/smb.conf:\n[global] # ... configuración existente ... # Backend por defecto para dominios no especificados idmap config * : backend = tdb idmap config * : range = 3000-7999 # Backend RID para el dominio corporativo idmap config DOMINIO : backend = rid idmap config DOMINIO : range = 10000-99999 Reiniciar winbind (no solo reload):\nsystemctl restart winbind systemctl status winbind Diagnóstico # # Listar usuarios del dominio wbinfo -u | grep usuario # Resolver usuario AD a entidad Unix getent passwd \u0026#39;DOMINIO\\usuario\u0026#39; # DOMINIO\\usuario:*:12345:10001:Nombre Apellido:/home/DOMINIO/usuario:/bin/bash Si getent no devuelve nada, el problema está en el rango idmap o en el backend. Si wbinfo -u tampoco funciona, el problema es previo: la unión al dominio o la conectividad con el DC.\nError 3: Secret file permissions (0644) are more permissive than 0600 # Síntoma # pam_google_authenticator(radiusd:auth): Secret file permissions (0644) are more permissive than 0600. (0) pam: ERROR: PAM auth for user \u0026#34;usuario\u0026#34; failed Causa # pam_google_authenticator comprueba los permisos del fichero TOTP antes de leerlo. Si son 0644 o más abiertos, rechaza el fichero por razones de seguridad. El error puede aparecer como autenticación fallida sin mayor detalle en los logs de freeradius, lo que dificulta el diagnóstico si no se ejecuta freeradius -X.\nSolución # chmod 600 /etc/google-authenticator/* ls -la /etc/google-authenticator/ # -rw------- freerad freerad usuario Consecuencia si no se corrige # pam_google_authenticator rechaza la autenticación silenciosamente desde la perspectiva de FreeRADIUS. El usuario recibe un Access-Reject sin causa aparente. Solo freeradius -X muestra el mensaje de permisos.\nError 4: Failed to create tempfile: Permission denied # Síntoma # pam_google_authenticator(radiusd:auth): Failed to create tempfile \u0026#34;/etc/google-authenticator/.usuario.XXXXXX\u0026#34;: Permission denied Causa # pam_google_authenticator escribe un fichero temporal en el mismo directorio donde está el secreto TOTP antes de actualizar el contador de uso único (para evitar replay attacks). Si el directorio /etc/google-authenticator/ pertenece a root y el proceso corre como freerad, la escritura falla.\nEste error es distinto del error 1: el setuid puede estar correctamente configurado con user=freerad, pero el directorio sigue siendo propiedad de root.\nSolución # chown freerad:freerad /etc/google-authenticator/ Verificar que los ficheros individuales siguen siendo 0600 después del cambio:\nls -la /etc/google-authenticator/ # drwxr-xr-x freerad freerad . # -rw------- freerad freerad usuario El directorio necesita permisos de escritura para freerad; los ficheros individuales solo necesitan lectura.\nError 5: Contraseñas visibles en el log tras configurar auth_badpass = no # Síntoma # Se configura auth_badpass = no en radiusd.conf esperando que las contraseñas fallidas dejen de aparecer en los logs, pero siguen siendo visibles.\nCausa # Hay dos causas independientes que pueden ocurrir simultáneamente:\nReinicio incompleto: el cambio en radiusd.conf requiere restart, no reload. Con reload, el proceso mantiene la configuración anterior en memoria. Módulo auth_log: si está habilitado en la sección authorize del virtual server, registra la petición —incluyendo atributos sensibles— independientemente de la configuración de auth_badpass. Solución # Reinicio completo:\nsystemctl restart freeradius # No usar: systemctl reload freeradius Verificar el módulo auth_log en el virtual server (normalmente /etc/freeradius/3.0/sites-enabled/default):\nauthorize { # Si este módulo está aquí, loguea independientemente de auth_badpass # auth_log ← comentar o eliminar si no se necesita ... } Confirmar que el cambio surtió efecto:\ngrep -n \u0026#39;auth_badpass\u0026#39; /etc/freeradius/3.0/radiusd.conf freeradius -X 2\u0026gt;\u0026amp;1 | grep -i \u0026#34;bad.*pass\\|log.*pass\u0026#34; Error 6: No Auth-Type found: rejecting via Post-Auth-Type = Reject # Síntoma # (0) No Auth-Type found: rejecting the user via Post-Auth-Type = Reject (0) Failed to authenticate the user La autenticación PAM funciona en pruebas directas (pamtester), pero FreeRADIUS rechaza sin llegar a invocarla.\nCausa # FreeRADIUS necesita saber qué método de autenticación usar. Si ningún módulo en la sección authorize establece Auth-Type, el servidor no sabe que debe delegar en PAM y rechaza la petición. Esto ocurre habitualmente cuando se migra de autenticación local a PAM sin actualizar el virtual server.\nSolución # En el virtual server activo (/etc/freeradius/3.0/sites-enabled/default), añadir en la sección authorize:\nauthorize { preprocess update control { \u0026amp;Auth-Type := PAP } pam ... } Y asegurarse de que el módulo pam está habilitado en la sección authenticate:\nauthenticate { Auth-Type PAP { pam } } Verificar que el módulo PAM está habilitado:\nls /etc/freeradius/3.0/mods-enabled/pam Si no existe, crear el enlace:\ncd /etc/freeradius/3.0/mods-enabled \u0026amp;\u0026amp; ln -s ../mods-available/pam pam Error 7: fail2ban no arranca — Jail 'sshd' is not valid # Síntoma # ERROR Failed during configuration: Have not found \u0026#39;logpath\u0026#39; option in \u0026#39;sshd\u0026#39; jail # o bien: ERROR Jail \u0026#39;sshd\u0026#39; is not valid systemctl start fail2ban falla o el servicio se inicia pero sin jails activos.\nCausa # Dos variantes comunes:\nlogpath duplicado: jail.local define logpath dos veces en la misma sección [sshd], lo que invalida la configuración del jail. Jails de servicios no instalados: si jail.local habilita jails para apache, vsftpd u otros servicios que no están presentes, fail2ban no encuentra los ficheros de log y falla. Solución # Validar la configuración antes de (re)iniciar el servicio:\nfail2ban-client -t # Si hay errores, los muestra con fichero y línea Editar /etc/fail2ban/jail.local y eliminar entradas duplicadas:\n[sshd] enabled = true port = ssh # Solo un logpath: logpath = %(sshd_log)s maxretry = 5 Comentar jails de servicios no instalados:\n# [apache-auth] # enabled = true # ... # [vsftpd] # enabled = true # ... Reiniciar tras validar:\nfail2ban-client -t \u0026amp;\u0026amp; systemctl restart fail2ban fail2ban-client status Herramientas de diagnóstico # Antes de revisar código o configuración, estas herramientas reducen el tiempo de diagnóstico a minutos:\nfreeradius -X — modo debug interactivo. Detener el servicio y ejecutar en primer plano para ver cada paso del procesamiento, incluyendo los mensajes de PAM que no aparecen en los logs normales:\nsystemctl stop freeradius freeradius -X radtest — enviar una petición de autenticación de prueba directamente desde la línea de comandos:\nradtest usuario contraseña+totp 127.0.0.1 0 secreto-compartido journalctl — seguir los logs del servicio en tiempo real:\njournalctl -u freeradius -f wbinfo -P — verificar que Winbind tiene conectividad con el controlador de dominio:\nwbinfo -P # checking the NETLOGON for domain[DOMINIO] dc connection ... succeeded fail2ban-client -t — validar la configuración completa de fail2ban sin iniciar el servicio:\nfail2ban-client -t Conclusión # La mayoría de estos errores comparten un patrón: el proceso freerad no tiene los permisos correctos sobre los recursos que necesita, o la configuración requiere un reinicio completo para surtir efecto. El orden de diagnóstico recomendado es:\nEjecutar freeradius -X para ver el error completo Verificar propietario y permisos con ls -la sobre los directorios afectados Confirmar que Winbind resuelve usuarios con getent passwd antes de depurar FreeRADIUS Validar configuraciones antes de aplicarlas (fail2ban-client -t) Reiniciar, no recargar, cuando se cambia configuración que afecta al proceso en memoria Con estos siete errores cubiertos y las herramientas de diagnóstico adecuadas, la mayoría de los problemas habituales en entornos FreeRADIUS + PAM se resuelven sin necesidad de escalar.\n","date":"18 juin 2026","externalUrl":null,"permalink":"/posts/errores-frecuentes-freeradius-pam-diagnostico/","section":"Posts","summary":"Configurar FreeRADIUS 3.x con autenticación PAM —especialmente combinando pam_google_authenticator, pam_winbind y fail2ban— genera un conjunto de errores que aparecen una y otra vez. Este artículo los recoge con causa exacta y solución directa, sin rodeos.\nTabla resumen # # Síntoma Causa raíz Solución rápida 1 Failed to change user id to \"usuario\" user=root en pam conf, freerad no puede hacer setuid user=freerad + chown freerad /etc/google-authenticator/ 2 user not found / getpwnam() failed Winbind sin idmap RID, los usuarios AD no tienen UID Unix Añadir bloques idmap config en smb.conf + reiniciar winbind 3 Secret file permissions (0644) are more permissive than 0600 Permisos demasiado abiertos en el fichero TOTP chmod 600 /etc/google-authenticator/* 4 Failed to create tempfile: Permission denied Directorio TOTP propiedad de root, freerad no puede escribir chown freerad:freerad /etc/google-authenticator/ 5 Contraseñas visibles en el log tras desactivarlas auth_badpass requiere reinicio, no reload; auth_log loguea independientemente systemctl restart freeradius + revisar módulo auth_log 6 No Auth-Type found: rejecting via Post-Auth-Type = Reject El virtual server no fuerza Auth-Type a PAP para rutas PAM Añadir update control { \u0026Auth-Type := PAP } en authorize 7 fail2ban no arranca: Jail 'sshd' is not valid logpath duplicado en jail.local o jails de servicios inexistentes fail2ban-client -t + eliminar duplicados y jails inactivos Error 1: Failed to change user id to \"usuario\" # Síntoma # (0) pam: pam_authenticate: Failed to change user id to \"usuario\" (0) pam: ERROR: PAM auth for user \"usuario\" failed Causa # pam_google_authenticator.so se carga con el parámetro user=root, lo que obliga a PAM a hacer setuid(root) antes de leer el fichero TOTP. El proceso freeradius corre como usuario freerad y no tiene permiso para asumir la identidad de root.\n","title":"Errores frecuentes en FreeRADIUS 3.x con PAM: guía de diagnóstico","type":"posts"},{"content":" Por qué proteger RADIUS con fail2ban # FreeRADIUS es el pilar de autenticación en muchas redes empresariales: VPNs, switches con 802.1X, Wi-Fi corporativo. El puerto 1812 UDP recibe constantemente intentos de credential stuffing, especialmente si la IP del servidor tiene alguna exposición pública. A diferencia de SSH, donde el atacante necesita llegar a un servicio TCP con negociación, RADIUS sobre UDP no tiene overhead de conexión: enviar miles de paquetes de autenticación cuesta casi nada al atacante.\nfail2ban resuelve esto de forma elegante: lee los logs de autenticación, detecta patrones de fallo repetidos y añade reglas de firewall temporales para bloquear el origen. El resultado es que un atacante que falla cinco veces queda bloqueado durante horas sin intervención manual.\nEste artículo asume FreeRADIUS 3.x sobre Ubuntu/Debian y fail2ban 0.11+. Para SSH, también se cubre el gotcha del logpath duplicado que impide arrancar el servicio.\nInstalación # fail2ban está en los repositorios oficiales:\napt update \u0026amp;\u0026amp; apt install fail2ban ufw -y systemctl enable fail2ban Si UFW no está activo aún, actívalo antes de continuar. fail2ban necesita que el backend de firewall esté operativo para añadir reglas:\nufw allow OpenSSH ufw enable Estructura de configuración: jail.local es la clave # Nunca editar /etc/fail2ban/jail.conf. Ese archivo lo sobreescribe el paquete en cada actualización. La práctica correcta es crear /etc/fail2ban/jail.local, que se fusiona con jail.conf en tiempo de carga y tiene precedencia sobre cualquier valor que defina.\nEl mismo principio aplica a los filtros: /etc/fail2ban/filter.d/ contiene los archivos .conf originales del paquete. Si necesitas modificar un filtro existente, crea un archivo .local con el mismo nombre junto al original.\nJail para SSH # La jail de SSH viene incluida y activa por defecto en muchas distribuciones, pero hay un error frecuente que hace que fail2ban no arranque: el parámetro logpath definido dos veces cuando se sobreescribe la jail en jail.local.\nEl bloque correcto para SSH es el siguiente:\n[sshd] enabled = true logpath = /var/log/auth.log Si en tu jail.local tienes una sección [sshd] y además la jail por defecto en jail.conf también define logpath, fail2ban 0.11+ trata la clave duplicada como error de configuración y se niega a arrancar. La solución es definir logpath solo en jail.local y dejar que sobreescriba el valor de jail.conf.\nPara verificar que la configuración es válida antes de reiniciar:\nfail2ban-client -t Si no aparece ningún error, la sintaxis es correcta.\nJail para FreeRADIUS # FreeRADIUS escribe los fallos de autenticación en /var/log/freeradius/radius.log. La jail personalizada usa un findtime más corto que la de SSH porque los ataques de credential stuffing son más agresivos en ráfagas cortas:\n[radius-login] enabled = true port = 1812 protocol = udp logpath = /var/log/freeradius/radius.log maxretry = 5 findtime = 300 bantime = 7200 filter = radius-login Parámetros destacados:\nprotocol = udp — fail2ban delega a UFW, que sí puede bloquear UDP por IP de origen. findtime = 300 — ventana de detección de 5 minutos: cinco fallos en ese periodo activan el bloqueo. bantime = 7200 — el origen queda baneado 2 horas. Más agresivo que el SSH porque un servidor RADIUS no tiene usuarios legítimos haciendo diez intentos seguidos. Filtro personalizado para RADIUS # fail2ban no incluye un filtro para FreeRADIUS por defecto. Hay que crearlo:\nnano /etc/fail2ban/filter.d/radius-login.conf [Definition] failregex = Login incorrect.*client \u0026lt;HOST\u0026gt; Auth: .*Login incorrect.*\\[\u0026lt;HOST\u0026gt;\\] ignoreregex = El placeholder \u0026lt;HOST\u0026gt; es la notación de fail2ban para capturar la IP del origen. Las dos expresiones cubren los dos formatos que usa FreeRADIUS 3.x en función de la versión y configuración del módulo pap/chap:\nLogin incorrect.*client \u0026lt;HOST\u0026gt; captura la forma más común donde la IP aparece en el campo client. Auth: .*Login incorrect.*\\[\u0026lt;HOST\u0026gt;\\] cubre el formato alternativo con la IP entre corchetes. Para verificar que el filtro funciona contra entradas reales del log antes de activar la jail:\nfail2ban-regex /var/log/freeradius/radius.log /etc/fail2ban/filter.d/radius-login.conf La salida muestra cuántas líneas coinciden. Si el contador es cero con un log que tiene fallos conocidos, hay que ajustar las expresiones.\nConfiguración completa de jail.local # El archivo resultante integra todo lo anterior con UFW como backend de baneo:\n# /etc/fail2ban/jail.local [DEFAULT] bantime = 3600 findtime = 600 maxretry = 5 banaction = ufw [sshd] enabled = true logpath = /var/log/auth.log [radius-login] enabled = true port = 1812 protocol = udp logpath = /var/log/freeradius/radius.log maxretry = 5 findtime = 300 bantime = 7200 filter = radius-login La sección [DEFAULT] establece los valores base para todas las jails. Cada jail puede sobrescribir los que necesite, como hace radius-login con findtime y bantime.\nbanaction = ufw indica a fail2ban que gestione los bloqueos mediante UFW en lugar de iptables directamente. Esto mantiene la coherencia: UFW es la herramienta de gestión de firewall en el sistema, y los baneos de fail2ban aparecen en ufw status junto con el resto de reglas.\nJails que no hay que activar sin verificar # Un error habitual al configurar fail2ban es activar jails para servicios que no están instalados. Si jail.local tiene [apache-auth] enabled = true pero Apache no está en el sistema, fail2ban buscará el logpath de Apache y fallará al arrancar porque el archivo no existe.\nLas jails problemáticas más habituales en un servidor sin esos servicios:\napache-auth, apache-badbots, apache-noscript vsftpd dovecot, postfix La regla es simple: solo activar jails de servicios que estén instalados y en ejecución en el sistema.\nAplicar la configuración y verificar # Tras guardar jail.local y el filtro radius-login.conf:\nsystemctl restart fail2ban systemctl status fail2ban Para ver el estado general de todas las jails activas:\nfail2ban-client status La salida muestra la lista de jails en ejecución. Para ver el detalle de una jail específica, incluyendo los IPs actualmente baneados:\nfail2ban-client status sshd fail2ban-client status radius-login La salida incluye el número total de fallos detectados, IPs baneadas en este momento y el histórico de baneos desde el último reinicio.\nPara desbanear manualmente una IP (por ejemplo, si un administrador se bloquea a sí mismo):\nfail2ban-client set radius-login unbanip 192.168.1.100 fail2ban-client set sshd unbanip 192.168.1.100 Seguimiento en los logs # fail2ban escribe su propia actividad en /var/log/fail2ban.log. Las entradas relevantes tienen este aspecto:\n2026-06-18 10:23:41,452 fail2ban.actions [1234]: NOTICE [radius-login] Ban 203.0.113.45 2026-06-18 10:23:41,460 fail2ban.actions [1234]: NOTICE [sshd] Ban 198.51.100.72 Para seguir los baneos en tiempo real:\ntail -f /var/log/fail2ban.log | grep Ban En infraestructura con volumen alto de intentos, es habitual ver docenas de baneos por hora en el puerto RADIUS. Eso confirma que el sistema funciona y cuantifica la presión que estaba recibiendo el servidor sin protección.\nConclusión # La combinación fail2ban + UFW cubre los dos vectores de autenticación más atacados en infraestructura de red: SSH para administración y RADIUS para acceso de usuarios. La configuración descrita en este artículo añade una capa de protección activa con un coste de mantenimiento prácticamente nulo una vez desplegada.\nEl punto más importante es el que más se pasa por alto: usar jail.local para toda la configuración propia y validar siempre con fail2ban-client -t antes de reiniciar el servicio. Los errores de configuración silenciosos —jails con logpath duplicado o servicios inexistentes— son la causa más habitual de que fail2ban arranque pero no proteja nada.\n","date":"18 juin 2026","externalUrl":null,"permalink":"/posts/fail2ban-freeradius-ssh-proteccion-fuerza-bruta-radius/","section":"Posts","summary":"Por qué proteger RADIUS con fail2ban # FreeRADIUS es el pilar de autenticación en muchas redes empresariales: VPNs, switches con 802.1X, Wi-Fi corporativo. El puerto 1812 UDP recibe constantemente intentos de credential stuffing, especialmente si la IP del servidor tiene alguna exposición pública. A diferencia de SSH, donde el atacante necesita llegar a un servicio TCP con negociación, RADIUS sobre UDP no tiene overhead de conexión: enviar miles de paquetes de autenticación cuesta casi nada al atacante.\n","title":"fail2ban para FreeRADIUS y SSH: bloqueo de fuerza bruta en infraestructura de red","type":"posts"},{"content":"","date":"18 juin 2026","externalUrl":null,"permalink":"/tags/flask/","section":"Tags","summary":"","title":"Flask","type":"tags"},{"content":"El 7 de junio de 2026 Fortinet confirmó lo que los investigadores de seguridad llevaban semanas documentando: una campaña de compromiso masivo contra dispositivos FortiGate que dejó 75.000 equipos afectados en 194 países. El ataque combinó una vulnerabilidad de autenticación SAML (CVE-2026-24858, CVSS 9.8) con un fallo en cómo FortiOS almacenaba las contraseñas durante el proceso de actualización. El resultado: 1.160 millones de intentos de autenticación en 29 días, credenciales de administrador en manos de actores de amenaza y acceso directo a redes corporativas a través de las mismas VPN diseñadas para protegerlas.\nEste artículo analiza qué falló técnicamente, por qué el vector de ataque era predecible y cómo un stack de autenticación bien configurado —en particular FreeRADIUS con MFA— reduce drásticamente el radio de explosión de este tipo de incidente.\nQué ocurrió: el ataque explicado # CVE-2026-24858 es un bypass de autenticación en FortiCloud SSO a través del canal SAML. Un atacante con acceso de red al portal de gestión puede construir una aserción SAML manipulada que el dispositivo acepta como válida sin verificar la firma correctamente. El resultado es sesión de administrador sin credenciales.\nEl segundo vector, más sutil, explica por qué la campaña tuvo tanta escala de extracción de credenciales. Los archivos de configuración de FortiGate incluyen un campo old-password que conserva el hash de la contraseña anterior durante el proceso de migración entre versiones. Los dispositivos actualizados a FortiOS 7.2.11+, 7.4.8+ o 7.6.1+ —que cambiaron de SHA-256 a PBKDF2— guardaban el hash SHA-256 en ese campo hasta el siguiente cambio de contraseña manual. Un atacante que extrae el backup de configuración obtiene un hash SHA-256 sin sal, craqueable offline con hardware moderno en minutos para contraseñas débiles o comunes.\n# Extracto típico de config comprometida (estructura anonimizada) config system admin edit \u0026#34;admin\u0026#34; set password ENC \u0026lt;hash-PBKDF2\u0026gt; set old-password \u0026lt;SHA256-sin-sal\u0026gt; ← vector de craqueo offline next end La cadena de ataque completa:\nExplotación de CVE-2026-24858 → acceso administrador vía SAML bypass Extracción del backup de configuración Craqueo offline del hash SHA-256 del campo old-password Reutilización de credenciales: VPN SSL, FortiCloud, Active Directory si el administrador reutilizaba contraseña Escala real: entre el 19 de mayo y el 7 de junio, se registraron ataques contra más de 320.000 dispositivos FortiGate expuestos a internet. El 23% de ellos —75.000— quedaron comprometidos.\nDónde falla el modelo \u0026ldquo;VPN como perímetro\u0026rdquo; # FortiBleed no es excepcional. Es la demostración más reciente de un patrón conocido: los dispositivos de perímetro concentran privilegio de acceso y son objetivos de alto valor. Un dispositivo FortiGate comprometido equivale a llaves de la red entera.\nEl problema estructural es la autenticación de factor único en la VPN. Si el acceso VPN SSL solo requiere usuario y contraseña, comprometer esa contraseña (ya sea por craqueo de hash, phishing o credential stuffing) es suficiente para acceder a la red interna. La VPN se convierte en una puerta con una sola llave.\nMFA en el acceso VPN convierte este ataque en un proceso de dos pasos que requiere comprometer simultáneamente la contraseña y el factor temporal. Para la mayoría de los atacantes, el segundo factor rompe la cadena de ataque aunque las credenciales estén expuestas.\nFreeRADIUS + TOTP como backend MFA para FortiGate # La solución no requiere licencias adicionales de Fortinet. FortiGate soporta autenticación RADIUS nativa y puede delegar el factor adicional a un servidor FreeRADIUS que combine pam_winbind (credenciales AD) con pam_google_authenticator (TOTP de 6 dígitos).\nLa arquitectura resultante:\nUsuario → FortiGate SSL-VPN ↓ RADIUS (PAP) FreeRADIUS 3.x ↓ PAM pam_google_authenticator ← verifica TOTP pam_winbind ← verifica contraseña AD ↓ Active Directory El usuario introduce ContraseñaAD123456 —contraseña seguida del OTP de 6 dígitos— como un string único. FreeRADIUS separa los factores y valida cada uno independientemente. Si el hash SHA-256 de la contraseña AD queda expuesto en un ataque similar a FortiBleed, el atacante aún necesita el código TOTP rotativo para acceder a la VPN.\nConfiguración PAM mínima funcional # # /etc/pam.d/radiusd auth required pam_google_authenticator.so \\ forward_pass \\ secret=/etc/google-authenticator/${USER} \\ user=freerad auth required pam_winbind.so use_first_pass account required pam_winbind.so forward_pass pasa la contraseña AD (sin el OTP ya consumido) al módulo siguiente. use_first_pass evita pedir la contraseña de nuevo. user=freerad es crítico: sin él, pam_google_authenticator intentará hacer setuid y el proceso fallará con el usuario freerad que corre FreeRADIUS.\nActivar Auth-Type PAP en el virtual server # # /etc/freeradius/3.0/sites-enabled/default authorize { preprocess update control { \u0026amp;Auth-Type := PAP } pam } authenticate { Auth-Type PAP { pam } } Sin el bloque update control, FreeRADIUS no sabrá que debe delegar en PAM y rechazará todas las peticiones con No Auth-Type found: rejecting via Post-Auth-Type = Reject.\nLos errores que aparecen durante la implementación # Configurar este stack genera errores específicos que aparecen sistemáticamente. Los siete más frecuentes —con causa exacta y solución— están documentados en detalle en el artículo Errores frecuentes en FreeRADIUS 3.x con PAM: guía de diagnóstico.\nResumen de los puntos más críticos relacionados con la seguridad:\nPermisos del directorio TOTP: si /etc/google-authenticator/ pertenece a root, el proceso freerad no puede crear el fichero temporal que previene replay attacks. El síntoma es Failed to create tempfile: Permission denied. La solución es chown freerad:freerad /etc/google-authenticator/.\nPermisos de los ficheros de secreto: pam_google_authenticator rechaza ficheros con permisos 0644 por razones de seguridad. La autenticación falla silenciosamente desde la perspectiva de FreeRADIUS. Solo visible con freeradius -X. Solución: chmod 600 /etc/google-authenticator/*.\nContraseñas en logs: el módulo auth_log registra atributos sensibles independientemente de auth_badpass = no. Si está habilitado en la sección authorize, las contraseñas aparecerán en los logs aunque la configuración diga lo contrario.\nMedidas de remediación inmediata ante FortiBleed # Si tienes dispositivos FortiGate en producción, las acciones prioritarias son:\n1. Actualizar FortiOS\nFortiOS 7.2.x → 7.2.11 o superior FortiOS 7.4.x → 7.4.8 o superior FortiOS 7.6.x → 7.6.1 o superior 2. Rotar credenciales de administrador después de actualizar\nEl campo old-password se limpia en el siguiente cambio de contraseña. Sin este paso, el hash SHA-256 vulnerable permanece en el backup de configuración incluso después del parche.\n3. Habilitar protección contra hashes débiles\nconfig system global set login-lockout-upon-weaker-encryption enable end 4. Verificar si el dispositivo fue comprometido\n# Buscar accesos SAML anómalos en los logs grep \u0026#34;SAML\u0026#34; /var/log/fortios-forticloud.log | grep -E \u0026#34;login|session\u0026#34; # Verificar cambios de configuración recientes get system status | grep \u0026#34;Last configuration change\u0026#34; 5. Implementar MFA en la VPN SSL\nEste es el cambio estructural que convierte futuras vulnerabilidades de contraseña en ataques incompletos. Con FreeRADIUS + TOTP, un atacante con la contraseña AD no puede acceder a la VPN sin el código temporal del segundo factor.\nEl patrón de fondo # FortiBleed sigue el mismo patrón que CVE-2024-21762 (Fortinet SSL-VPN path traversal), Citrix Bleed (CVE-2023-4966) y el compromiso masivo de Pulse Secure de 2021: los dispositivos de perímetro concentran acceso y son objetivos prioritarios precisamente por eso.\nLa respuesta técnica es doble: mantener el firmware actualizado (reducir la ventana de exposición de vulnerabilidades conocidas) e implementar defensa en profundidad en la autenticación (garantizar que comprometer una capa no equivale a comprometer el acceso completo).\nFreeRADIUS con MFA es una capa de defensa que no depende de que el fabricante del perímetro publique el parche a tiempo. Existe el stack, está documentado y es gratuito.\nPara la implementación completa del stack FreeRADIUS + PAM + pam_google_authenticator + pam_winbind, consulta la serie de artículos sobre infraestructura MFA empresarial en este blog.\n","date":"18 juin 2026","externalUrl":null,"permalink":"/posts/fortibleed-fortigate-freeradius-mfa-hardening/","section":"Posts","summary":"El 7 de junio de 2026 Fortinet confirmó lo que los investigadores de seguridad llevaban semanas documentando: una campaña de compromiso masivo contra dispositivos FortiGate que dejó 75.000 equipos afectados en 194 países. El ataque combinó una vulnerabilidad de autenticación SAML (CVE-2026-24858, CVSS 9.8) con un fallo en cómo FortiOS almacenaba las contraseñas durante el proceso de actualización. El resultado: 1.160 millones de intentos de autenticación en 29 días, credenciales de administrador en manos de actores de amenaza y acceso directo a redes corporativas a través de las mismas VPN diseñadas para protegerlas.\n","title":"FortiBleed 2026: cuando la VPN se convierte en la puerta de entrada","type":"posts"},{"content":"","date":"18 juin 2026","externalUrl":null,"permalink":"/tags/fortigate/","section":"Tags","summary":"","title":"Fortigate","type":"tags"},{"content":"","date":"18 juin 2026","externalUrl":null,"permalink":"/tags/fortinet/","section":"Tags","summary":"","title":"Fortinet","type":"tags"},{"content":"","date":"18 juin 2026","externalUrl":null,"permalink":"/tags/freeradius/","section":"Tags","summary":"","title":"Freeradius","type":"tags"},{"content":" El problema # Un FortiGate configurado con SSL-VPN autenticando contra Active Directory solo pide usuario y contraseña. Si esas credenciales se filtran —un phishing, un password reutilizado, un volcado de NTDS— cualquiera entra a la red corporativa desde internet. El segundo factor elimina ese vector: aunque el atacante tenga la contraseña, sin el código TOTP del momento no pasa.\nLa solución que describo aquí conecta tres piezas:\nFortiGate: termina la VPN y delega la autenticación a RADIUS FreeRADIUS: recibe la petición y la procesa vía PAM PAM: encadena Google Authenticator (TOTP) + Winbind (AD) El usuario introduce ContraseñaAD + 123456 como un solo string. FreeRADIUS separa los factores internamente.\nArquitectura del stack # [Usuario VPN] | | ContraseñaAD123456 (PAP sobre TLS) v [FortiGate SSL-VPN] | | RADIUS Access-Request (PAP) v [FreeRADIUS] | | /etc/pam.d/radiusd v [PAM] ├── pam_google_authenticator ← valida últimos 6 dígitos (OTP) └── pam_winbind ← valida el resto contra AD El flujo PAP es obligatorio aquí: RADIUS necesita el password en claro para que PAM pueda separarlo y validar cada factor. CHAP o MS-CHAPv2 no funcionan con esta arquitectura.\nInstalación # En Ubuntu 22.04 / Debian 12:\napt update apt install freeradius freeradius-config \\ winbind samba libnss-winbind \\ libpam-winbind libpam-google-authenticator Verificar que FreeRADIUS arranca antes de tocar configuración:\nsystemctl stop freeradius freeradius -X # modo debug, ver output limpio Unir el servidor Linux a Active Directory # Editar /etc/samba/smb.conf. La sección [global] debe quedar así:\n[global] workgroup = DOMINIO realm = DOMINIO.LOCAL security = ads encrypt passwords = yes winbind use default domain = yes winbind enum users = yes winbind enum groups = yes winbind offline logon = no winbind separator = + idmap config * : backend = tdb idmap config * : range = 3000-7999 idmap config DOMINIO : backend = rid idmap config DOMINIO : range = 10000-99999 El bloque idmap config DOMINIO es el que más problemas causa cuando falta (ver sección de errores). Sin él, getpwnam() no resuelve los usuarios del dominio y FreeRADIUS falla con \u0026ldquo;user not found\u0026rdquo; aunque el usuario exista en AD.\nUnirse al dominio:\nnet ads join -U Administrador systemctl enable --now winbind Verificar que los usuarios del dominio son visibles:\nwbinfo -u | head getent passwd DOMINIO\\\\usuario.prueba Si getent devuelve la línea del usuario, la integración AD funciona.\nConfiguración de PAM # Crear o editar /etc/pam.d/radiusd:\nauth required pam_google_authenticator.so \\ forward_pass \\ secret=/etc/google-authenticator/${USER} \\ user=freerad auth required pam_winbind.so use_first_pass account required pam_winbind.so Tres directivas clave:\nforward_pass en pam_google_authenticator: el módulo extrae los últimos 6 caracteres del password recibido, los valida como TOTP, y pasa el resto hacia adelante en la pila PAM. Sin esta directiva, el módulo consumiría el password entero y pam_winbind no tendría nada que validar.\nuse_first_pass en pam_winbind: fuerza al módulo a usar el password que quedó en la pila (el que pasó forward_pass) en lugar de pedirlo de nuevo. Si se omite, intenta leer desde stdin y cuelga o falla.\nuser=freerad: pam_google_authenticator lee el fichero de secreto del usuario que hace la petición. El problema es que por defecto intenta leerlo como ese usuario de sistema, y el usuario del dominio no tiene permisos en /etc/google-authenticator/. Fijando user=freerad, el módulo siempre lee como el usuario freerad (proceso de FreeRADIUS), que sí tiene acceso.\nFicheros TOTP por usuario # Crear el directorio de secretos:\nmkdir -p /etc/google-authenticator chown freerad:freerad /etc/google-authenticator chmod 700 /etc/google-authenticator Para cada usuario del dominio, generar el secreto y dejar el fichero donde PAM lo buscará:\n# Ejecutar como el propio usuario, o con google-authenticator y mover el fichero su -s /bin/bash -c \u0026#34;google-authenticator -t -d -f -r 3 -R 30 -w 3\u0026#34; freerad # O generarlo manualmente y colocarlo: mv ~/.google_authenticator /etc/google-authenticator/usuario.dominio chown freerad:freerad /etc/google-authenticator/usuario.dominio chmod 600 /etc/google-authenticator/usuario.dominio Los permisos 0600 son obligatorios. pam_google_authenticator rechaza ficheros con permisos más permisivos como medida de seguridad.\nConfiguración de FreeRADIUS # Declarar el cliente FortiGate # En /etc/freeradius/3.0/clients.conf, añadir:\nclient fortigate-vpn { ipaddr = IP-FORTIGATE secret = secreto-compartido-seguro shortname = fortigate nastype = other } Activar autenticación PAP en el virtual server # El virtual server por defecto está en /etc/freeradius/3.0/sites-enabled/default. En la sección authorize, añadir antes del bloque pap:\nauthorize { ... update control { \u0026amp;Auth-Type := PAP } pap ... } Esto fuerza a FreeRADIUS a usar PAM/PAP para todos los usuarios. Si hay otros métodos activos (LDAP, SQL), asegurarse de que este bloque tiene prioridad o va en un virtual server dedicado.\nActivar el módulo PAM # En /etc/freeradius/3.0/mods-enabled/, comprobar que existe el enlace simbólico pam:\nls -la /etc/freeradius/3.0/mods-enabled/pam # Si no existe: ln -s /etc/freeradius/3.0/mods-available/pam \\ /etc/freeradius/3.0/mods-enabled/pam En la sección authenticate del virtual server:\nauthenticate { Auth-Type PAP { pam } ... } Configuración del FortiGate # En la consola FortiGate:\nServidor RADIUS: User \u0026amp; Authentication → RADIUS Servers → Create New\nIP/Name: IP-SERVIDOR-RADIUS Secret: el mismo secreto-compartido-seguro Authentication method: PAP Grupo de usuarios: crear un grupo que use el servidor RADIUS como fuente.\nSSL-VPN: en la política SSL-VPN, asociar el grupo RADIUS.\nPAP debe estar explícito en FortiGate. Si queda en \u0026ldquo;auto\u0026rdquo;, FortiGate puede intentar MS-CHAP y la autenticación falla silenciosamente.\nPruebas # Antes de tocar el FortiGate, probar localmente con radtest:\n# Iniciar FreeRADIUS en modo debug systemctl stop freeradius freeradius -X \u0026amp; # Probar: password = ContraseñaAD + OTP del momento radtest nombre.usuario \u0026#34;MiPassword123456\u0026#34; 127.0.0.1 0 secreto-local # Respuesta esperada: # Received Access-Accept Id 0 from 127.0.0.1:1812 Si aparece Access-Reject, el modo debug de FreeRADIUS muestra exactamente en qué paso PAM falló. Leer el output completo antes de buscar en otra parte.\nPara probar winbind por separado:\nwbinfo -a \u0026#39;DOMINIO\\usuario.prueba%ContraseñaAD\u0026#39; # Respuesta esperada: plaintext password authentication succeeded Errores frecuentes # Failed to change user id to \u0026quot;nombre.usuario\u0026quot; # FreeRADIUS intenta ejecutar pam_google_authenticator como el usuario del dominio, que no existe como usuario Unix local.\nSolución: añadir user=freerad en la línea de pam_google_authenticator en /etc/pam.d/radiusd.\nuser(\u0026quot;nombre.usuario\u0026quot;) not found # Winbind no resuelve el usuario del dominio. Casi siempre falta la configuración de idmap para el dominio específico en smb.conf.\nSolución: añadir en [global]:\nidmap config DOMINIO : backend = rid idmap config DOMINIO : range = 10000-99999 Reiniciar winbind después: systemctl restart winbind. Confirmar con getent passwd DOMINIO\\\\usuario.\nSecret file permissions (0644) are more permissive than 0600 # El fichero de secreto TOTP tiene permisos demasiado abiertos.\nSolución:\nchmod 600 /etc/google-authenticator/nombre.usuario Failed to create tempfile: Permission denied # pam_google_authenticator intenta escribir un fichero temporal en el directorio de secretos (para el rate limiting y los códigos de un solo uso) y no tiene permiso.\nSolución: el directorio /etc/google-authenticator/ debe ser propiedad de freerad:\nchown freerad:freerad /etc/google-authenticator chmod 700 /etc/google-authenticator Auth-Type already set o autenticación en bucle # Hay un módulo LDAP o SQL activo que también intenta establecer Auth-Type. El bloque update control { \u0026amp;Auth-Type := PAP } usa := (asignación forzada) precisamente para sobreescribir lo que otros módulos hayan puesto. Verificar que no hay otro bloque estableciendo Auth-Type después.\nConclusión # El stack FreeRADIUS + PAM + Google Authenticator + Winbind cubre el requisito de MFA en VPN sin necesidad de licencias adicionales ni appliances dedicados. El coste operativo real está en la gestión de los ficheros TOTP por usuario: cada incorporación requiere generar el secreto, compartir el QR con el usuario y asegurarse de que el fichero tiene los permisos correctos.\nPara entornos con más de 20-30 usuarios, tiene sentido automatizar ese proceso de onboarding con un script que genere el fichero, lo ubique en /etc/google-authenticator/ y envíe el QR por correo cifrado. La base técnica que describe este artículo es la misma independientemente de la escala.\nEl punto más frágil de la arquitectura es el fichero de secreto en disco: si alguien con acceso root al servidor RADIUS extrae esos ficheros, puede clonar los tokens TOTP. Limitar el acceso SSH al servidor, activar auditoría y rotar los secretos periódicamente son las medidas de higiene mínimas para mantener el segundo factor real.\n","date":"18 juin 2026","externalUrl":null,"permalink":"/posts/freeradius-totp-active-directory-mfa-vpn-empresarial/","section":"Posts","summary":"El problema # Un FortiGate configurado con SSL-VPN autenticando contra Active Directory solo pide usuario y contraseña. Si esas credenciales se filtran —un phishing, un password reutilizado, un volcado de NTDS— cualquiera entra a la red corporativa desde internet. El segundo factor elimina ese vector: aunque el atacante tenga la contraseña, sin el código TOTP del momento no pasa.\n","title":"FreeRADIUS + TOTP + Active Directory: doble factor para VPN empresarial desde cero","type":"posts"},{"content":"","date":"18 juin 2026","externalUrl":null,"permalink":"/tags/hardening/","section":"Tags","summary":"","title":"Hardening","type":"tags"},{"content":"","date":"18 juin 2026","externalUrl":null,"permalink":"/tags/mfa/","section":"Tags","summary":"","title":"Mfa","type":"tags"},{"content":"","date":"18 juin 2026","externalUrl":null,"permalink":"/categories/operaciones/","section":"Categories","summary":"","title":"Operaciones","type":"categories"},{"content":"","date":"18 juin 2026","externalUrl":null,"permalink":"/tags/operaciones/","section":"Tags","summary":"","title":"Operaciones","type":"tags"},{"content":"","date":"18 juin 2026","externalUrl":null,"permalink":"/tags/pam/","section":"Tags","summary":"","title":"Pam","type":"tags"},{"content":"","date":"18 juin 2026","externalUrl":null,"permalink":"/tags/peap/","section":"Tags","summary":"","title":"Peap","type":"tags"},{"content":"","date":"18 juin 2026","externalUrl":null,"permalink":"/tags/pki/","section":"Tags","summary":"","title":"Pki","type":"tags"},{"content":"Configurar TOTP (Time-based One-Time Password) en un entorno con varios usuarios tiene un problema de distribución: el secret TOTP tiene que llegar al dispositivo del usuario de forma segura. La solución habitual —que el administrador escanee el QR en el teléfono del usuario— no escala, no es cómoda y rompe el modelo de privacidad. Este artículo documenta un microservicio Python que resuelve el problema generando URLs de un solo uso con TTL que el usuario consume desde su propio dispositivo.\nEl problema de distribución # RFC 6238 define TOTP como un algoritmo que comparte un secret entre el servidor y el cliente. El servidor lo almacena en una base de datos o en /etc/users.totp. El cliente lo introduce en Google Authenticator, Aegis o cualquier app compatible. El vector de ataque más común no está en el algoritmo, sino en el momento de la entrega del secret.\nLas alternativas habituales tienen problemas obvios:\nMostrar el secret en texto plano durante el registro: el secret queda en logs del navegador, historial, pantallas observadas. Enviar el QR como imagen adjunta en el email: la imagen puede quedar en servidores de correo intermedios, en caché de clientes de correo, en previews de notificaciones. Que el administrador configure el dispositivo: no escala, rompe la cadena de custodia, y en entornos con rotación de personal es un problema de auditoría. La solución: generar un enlace de un solo uso con TTL de 48 horas que apunte a una página HTML servida localmente. El usuario hace clic, ve el QR, escanea, y el enlace queda invalidado. Si el enlace caduca o ya fue consumido, muestra un error.\nDiseño del sistema # El flujo tiene tres componentes:\nGenerador de tokens: crea un token aleatorio de 32 bytes codificado como URL-safe base64, lo persiste en un JSON con metadatos y TTL. Servidor HTTP mínimo (Flask): recibe el token en la URL, valida TTL y estado used, genera el QR en memoria, sirve la página HTML con el QR embebido como data URI. Notificador SMTP: envía el enlace al email del usuario con autenticación manual para manejar contraseñas con caracteres no-ASCII. Los tokens se almacenan en un directorio tokens/ como ficheros JSON individuales. Sin base de datos, sin dependencias externas más allá de Flask y qrencode.\nEstructura del token # import secrets, json, time from pathlib import Path def generate_token(username): token = secrets.token_urlsafe(32) data = { \u0026#34;user\u0026#34;: username, \u0026#34;created\u0026#34;: time.time(), \u0026#34;ttl\u0026#34;: 172800, # 48h en segundos \u0026#34;used\u0026#34;: False } Path(f\u0026#34;tokens/{token}.json\u0026#34;).write_text(json.dumps(data)) return token secrets.token_urlsafe(32) genera 32 bytes de entropía criptográfica, codificados en base64 URL-safe: 43 caracteres sin padding, sin / ni +, seguros para usarlos directamente en URLs sin encoding adicional. La entropía efectiva es 256 bits —suficiente para que la probabilidad de colisión sea irrelevante en cualquier escenario real.\nValidación del token en el servidor # from flask import Flask, abort, render_template_string import json, time from pathlib import Path app = Flask(__name__) @app.route(\u0026#34;/setup/\u0026lt;token\u0026gt;\u0026#34;) def setup(token): token_file = Path(f\u0026#34;tokens/{token}.json\u0026#34;) if not token_file.exists(): abort(404) data = json.loads(token_file.read_text()) if data[\u0026#34;used\u0026#34;]: abort(410) # Gone — ya fue consumido if time.time() - data[\u0026#34;created\u0026#34;] \u0026gt; data[\u0026#34;ttl\u0026#34;]: abort(410) # Gone — expirado # Marcar como usado ANTES de servir la respuesta data[\u0026#34;used\u0026#34;] = True token_file.write_text(json.dumps(data)) username = data[\u0026#34;user\u0026#34;] secret = get_totp_secret(username) # Recuperar de almacén seguro return render_totp_page(username, secret) El orden importa: se marca used: true antes de servir la respuesta, no después. Si el servidor falla entre el marcado y el envío, el usuario tendrá que solicitar un nuevo enlace, pero no hay ventana donde un atacante pueda consumir el enlace en paralelo.\nGeneración del QR en servidor # El QR no se genera en el cliente. Se genera en el servidor con qrencode, se convierte a base64 y se embebe directamente en el HTML como data URI. Esto evita que la imagen salga del servidor hacia un servicio externo de generación de QR.\nURI otpauth # El formato estándar que entienden todas las apps TOTP es:\notpauth://totp/ISSUER:usuario?secret=XXX\u0026amp;issuer=ISSUER\u0026amp;algorithm=SHA1\u0026amp;digits=6\u0026amp;period=30 ISSUER aparece dos veces: como prefijo del nombre de cuenta y como parámetro issuer. Algunas apps solo leen uno de los dos; incluir ambos garantiza compatibilidad. secret en base32 (RFC 4648), sin padding. algorithm=SHA1 es el único que soportan la mayoría de apps TOTP en la práctica; SHA256 y SHA512 están en el RFC pero el soporte es inconsistente. digits=6 y period=30 son los valores por defecto de Google Authenticator. Generación del PNG # import subprocess, base64 def generate_qr_base64(otpauth_uri: str) -\u0026gt; str: result = subprocess.run( [\u0026#34;qrencode\u0026#34;, \u0026#34;-o\u0026#34;, \u0026#34;-\u0026#34;, \u0026#34;-t\u0026#34;, \u0026#34;PNG\u0026#34;, \u0026#34;-s\u0026#34;, \u0026#34;6\u0026#34;, otpauth_uri], capture_output=True, check=True ) return base64.b64encode(result.stdout).decode() def render_totp_page(username: str, secret: str) -\u0026gt; str: issuer = \u0026#34;MiServicio\u0026#34; uri = f\u0026#34;otpauth://totp/{issuer}:{username}?secret={secret}\u0026amp;issuer={issuer}\u0026amp;algorithm=SHA1\u0026amp;digits=6\u0026amp;period=30\u0026#34; qr_b64 = generate_qr_base64(uri) # qr_b64 se embebe en el HTML como data:image/png;base64,... return HTML_TEMPLATE.format(username=username, secret=secret, qr_b64=qr_b64) El tamaño -s 6 genera celdas de 6 píxeles: un QR legible en pantalla sin ser exageradamente grande. qrencode está disponible en los repositorios de cualquier distribución Debian/Ubuntu/RHEL.\nEnvío del enlace por SMTP: el problema con contraseñas no-ASCII # smtplib es la librería estándar de Python para SMTP. Su método login() falla con UnicodeEncodeError si la contraseña contiene caracteres fuera de ASCII —como la letra Ñ, tildes, o cualquier carácter con código \u0026gt; 127. Esto ocurre porque smtplib intenta codificar la contraseña en latin-1 para ciertos servidores, y si el carácter no existe en ese charset, lanza una excepción que no documenta claramente el motivo.\nLa solución es autenticar manualmente con AUTH PLAIN construyendo el payload en base64 desde bytes UTF-8:\nimport smtplib, base64, ssl def send_setup_email(to_email: str, username: str, setup_url: str): smtp_host = \u0026#34;smtp.ejemplo.com\u0026#34; smtp_port = 465 smtp_user = \u0026#34;notificaciones@ejemplo.com\u0026#34; smtp_pass = \u0026#34;contraseña_con_Ñ\u0026#34; # Leída desde variable de entorno subject = f\u0026#34;Configura tu autenticador para {username}\u0026#34; body = f\u0026#34;\u0026#34;\u0026#34;Hola {username}, Accede al siguiente enlace desde tu dispositivo móvil para configurar Google Authenticator o Aegis. El enlace es válido durante 48 horas y solo puede usarse una vez: {setup_url} Si no solicitaste este enlace, ignora este email. \u0026#34;\u0026#34;\u0026#34; msg = f\u0026#34;From: {smtp_user}\\r\\nTo: {to_email}\\r\\nSubject: {subject}\\r\\n\\r\\n{body}\u0026#34; context = ssl.create_default_context() with smtplib.SMTP_SSL(smtp_host, smtp_port, context=context) as smtp: smtp.ehlo() # AUTH PLAIN manual: evita UnicodeEncodeError con contraseñas no-ASCII auth_payload = base64.b64encode( b\u0026#34;\\x00\u0026#34; + smtp_user.encode(\u0026#34;utf-8\u0026#34;) + b\u0026#34;\\x00\u0026#34; + smtp_pass.encode(\u0026#34;utf-8\u0026#34;) ).decode(\u0026#34;ascii\u0026#34;) smtp.docmd(\u0026#34;AUTH PLAIN\u0026#34;, auth_payload) smtp.sendmail(smtp_user, [to_email], msg.encode(\u0026#34;utf-8\u0026#34;)) El protocolo AUTH PLAIN define el payload como \\x00authzid\\x00authcid\\x00password o en su forma más común sin authzid: \\x00usuario\\x00contraseña. Los tres campos se concatenan en bytes UTF-8, se codifican en base64 y se envían como argumento del comando AUTH PLAIN. El servidor lo decodifica en UTF-8 en su extremo. smtplib.login() hace exactamente esto internamente, pero con una codificación que falla antes de llegar al servidor cuando hay caracteres no-ASCII.\nInterfaz HTML embebida # La página que ve el usuario no tiene dependencias externas. Sin CDN, sin JavaScript de terceros, sin tracking. Todo en una sola respuesta HTTP:\n\u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;es\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;UTF-8\u0026#34;\u0026gt; \u0026lt;meta name=\u0026#34;viewport\u0026#34; content=\u0026#34;width=device-width, initial-scale=1\u0026#34;\u0026gt; \u0026lt;title\u0026gt;Configurar autenticador — {username}\u0026lt;/title\u0026gt; \u0026lt;style\u0026gt; body {{ font-family: system-ui, sans-serif; max-width: 480px; margin: 2rem auto; padding: 1rem; }} .qr {{ text-align: center; margin: 2rem 0; }} .secret {{ font-family: monospace; background: #f4f4f4; padding: .5rem 1rem; border-radius: 4px; word-break: break-all; letter-spacing: .1em; }} .warning {{ background: #fff3cd; border-left: 4px solid #ffc107; padding: .75rem 1rem; }} \u0026lt;/style\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;h1\u0026gt;Configurar Google Authenticator\u0026lt;/h1\u0026gt; \u0026lt;p\u0026gt;Escanea el código QR con tu app de autenticación (Google Authenticator, Aegis, etc.).\u0026lt;/p\u0026gt; \u0026lt;div class=\u0026#34;qr\u0026#34;\u0026gt; \u0026lt;img src=\u0026#34;data:image/png;base64,{qr_b64}\u0026#34; alt=\u0026#34;QR TOTP\u0026#34; width=\u0026#34;250\u0026#34; height=\u0026#34;250\u0026#34;\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;p\u0026gt;Si no puedes escanear el QR, introduce el secret manualmente:\u0026lt;/p\u0026gt; \u0026lt;p class=\u0026#34;secret\u0026#34;\u0026gt;{secret}\u0026lt;/p\u0026gt; \u0026lt;div class=\u0026#34;warning\u0026#34;\u0026gt; \u0026lt;strong\u0026gt;Este enlace ya no es válido.\u0026lt;/strong\u0026gt; Si necesitas reconfigurarlo, contacta con el administrador para obtener un nuevo enlace. \u0026lt;/div\u0026gt; \u0026lt;h2\u0026gt;Códigos de emergencia\u0026lt;/h2\u0026gt; \u0026lt;p\u0026gt;Guarda estos códigos en un lugar seguro. Cada uno es de un solo uso:\u0026lt;/p\u0026gt; \u0026lt;ul\u0026gt; {emergency_codes} \u0026lt;/ul\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; Los códigos de emergencia se generan en el mismo paso (secrets.token_hex(4) repetido 8 veces), se almacenan hasheados junto al registro TOTP del usuario y se muestran una única vez en esta página. Después no son recuperables.\nServicio systemd y comandos de gestión # # /etc/systemd/system/totp-provisioning.service [Unit] Description=TOTP Provisioning Service After=network.target [Service] Type=simple User=totp-svc WorkingDirectory=${BASE_DIR}/totp-provisioning EnvironmentFile=${BASE_DIR}/totp-provisioning/.env ExecStart=/usr/bin/python3 server.py Restart=on-failure RestartSec=5 [Install] WantedBy=multi-user.target Los comandos de administración son dos subcomandos del mismo script:\n# Crear token y enviar email al usuario python3 manage.py send usuario@dominio.com usuario # Generar token sin enviar email (para depuración o entrega alternativa) python3 manage.py token usuario send llama a generate_token(), construye la URL completa y llama a send_setup_email(). token solo imprime la URL en stdout para que el administrador la entregue por otro canal si lo prefiere.\nConclusión # Este patrón resuelve el problema de distribución de secrets TOTP sin comprometer la seguridad ni requerir intervención manual en el dispositivo del usuario. Los puntos clave del diseño:\nEntropía suficiente: 256 bits hacen que la enumeración de tokens sea computacionalmente inviable. Un solo uso: el token queda invalidado tras el primer acceso, independientemente de si el usuario completa la configuración. TTL corto: 48 horas reduce la ventana de exposición si el email es interceptado. Sin dependencias externas en el cliente: la página HTML es autocontenida; el QR se genera y embebe en servidor. Autenticación SMTP robusta: el workaround con AUTH PLAIN manual elimina una clase de errores silenciosos que aparecen solo en producción cuando las contraseñas contienen caracteres no-ASCII. El servicio es deliberadamente mínimo —menos de 300 líneas incluyendo la plantilla HTML— porque en seguridad, el código que no existe no tiene vulnerabilidades.\n","date":"18 juin 2026","externalUrl":null,"permalink":"/posts/provisioning-totp-urls-un-solo-uso-qr-codes-email/","section":"Posts","summary":"Configurar TOTP (Time-based One-Time Password) en un entorno con varios usuarios tiene un problema de distribución: el secret TOTP tiene que llegar al dispositivo del usuario de forma segura. La solución habitual —que el administrador escanee el QR en el teléfono del usuario— no escala, no es cómoda y rompe el modelo de privacidad. Este artículo documenta un microservicio Python que resuelve el problema generando URLs de un solo uso con TTL que el usuario consume desde su propio dispositivo.\n","title":"Provisioning TOTP con URLs de un solo uso: entrega segura de QR codes por email","type":"posts"},{"content":"","date":"18 juin 2026","externalUrl":null,"permalink":"/categories/python/","section":"Categories","summary":"","title":"Python","type":"categories"},{"content":"","date":"18 juin 2026","externalUrl":null,"permalink":"/tags/qr-code/","section":"Tags","summary":"","title":"Qr-Code","type":"tags"},{"content":"","date":"18 juin 2026","externalUrl":null,"permalink":"/tags/radius/","section":"Tags","summary":"","title":"Radius","type":"tags"},{"content":"","date":"18 juin 2026","externalUrl":null,"permalink":"/tags/rotaci%C3%B3n/","section":"Tags","summary":"","title":"Rotación","type":"tags"},{"content":" Por qué rotar el shared secret RADIUS # El shared secret RADIUS no es una contraseña de usuario: es la clave que cifra el campo User-Password en cada paquete RADIUS entre el NAS (FortiGate, AP, switch) y el servidor FreeRADIUS. El protocolo PAP transporta la contraseña del usuario XOR-cifrada con un hash MD5 derivado del shared secret y un vector aleatorio del paquete.\nEl problema es que ese cifrado es débil por diseño: si alguien captura tráfico RADIUS durante meses y después consigue el shared secret, puede desencriptar retroactivamente todas esas contraseñas. No hay perfect forward secrecy en RADIUS clásico.\nA esto se suma que muchas instalaciones nunca cambiaron el secret del ejemplo de configuración. Si en tu clients.conf ves secret = testing123, no estás solo. Y si llevas años con el mismo valor, cualquier empleado que haya tenido acceso a la configuración lo conoce indefinidamente.\nDesde el punto de vista regulatorio, tanto ISO 27001 como el ENS (Esquema Nacional de Seguridad) exigen rotación periódica de credenciales de infraestructura. El shared secret RADIUS entra en esa categoría.\nEl riesgo central: la coordinación atómica # Aquí está el problema operativo real. FortiGate y FreeRADIUS deben tener el mismo secret en el mismo instante. No hay handshake de negociación, no hay período de gracia, no hay solapamiento posible: un paquete con el secret incorrecto simplemente produce un Access-Reject y el usuario no se autentica.\nSi cambias FreeRADIUS primero y tardas 30 segundos en actualizar el FortiGate, durante esos 30 segundos nadie puede conectarse por VPN ni autenticarse en WiFi corporativo. Si lo haces al revés, igual.\nEsta naturaleza atómica es lo que hace que muchos administradores pospongan la rotación indefinidamente. El objetivo de este proceso es reducir esa ventana de impacto a unos pocos segundos y tener un plan de reversión claro si algo falla.\nLimitaciones antes de empezar # Antes de generar el nuevo secret, hay restricciones del hardware que debes conocer:\nLongitud máxima: algunos modelos FortiGate más antiguos aceptan secrets de hasta 32 caracteres. Otros permiten más, pero la interoperabilidad entre NAS y servidor puede verse afectada con secrets muy largos. El rango seguro es 20-32 caracteres.\nCaracteres permitidos: ciertos NAS tienen problemas con caracteres especiales. Los caracteres @, # y ! son los más conflictivos, especialmente en firmwares antiguos o implementaciones que no escapan correctamente el valor antes de pasarlo al stack RADIUS. Usar únicamente caracteres alfanuméricos elimina esa clase de problemas.\nVerificar antes: si tienes dudas sobre tu modelo específico de FortiGate, genera un secret de 28 caracteres alfanuméricos y pruébalo en un entorno de laboratorio antes de tocarlo en producción.\nGeneración del nuevo secret # Un secret generado con entropía criptográfica real, sin caracteres problemáticos y de longitud adecuada:\nopenssl rand -base64 24 | tr -d \u0026#39;=+/\u0026#39; | head -c 28 Esto genera 24 bytes aleatorios, los codifica en base64, elimina los caracteres =, + y /, y toma los primeros 28 caracteres. El resultado es alfanumérico puro con alta entropía.\nGuarda este valor en tu gestor de contraseñas corporativo antes de continuar. Lo necesitarás en tres lugares distintos durante el proceso.\nEl proceso paso a paso # 1. Elegir la ventana de mantenimiento # Trabaja en horario de baja actividad: entre semana a las 2-4h, o fin de semana por la mañana. Notifica con antelación si hay usuarios con VPN activa. El impacto real si todo va bien es de 5-15 segundos; si hay que revertir, puede ser de 2-3 minutos.\n2. Preparar FreeRADIUS (sin aplicar todavía) # Edita /etc/freeradius/3.0/clients.conf con el nuevo secret pero no recargues el servicio aún:\nclient fortigate { ipaddr = IP-FORTIGATE secret = NuevoSecretAqui nastype = other require_message_authenticator = no } Ten el archivo abierto en un editor o el cambio listo con sed preparado. El objetivo es que entre el cambio en FortiGate y el reload de FreeRADIUS pasen los menos segundos posibles.\n3. Cambiar el secret en FortiGate # Desde la CLI de FortiGate:\nconfig user radius edit \u0026#34;servidor-radius\u0026#34; set server IP-RADIUS set secret NuevoSecretAqui next end O desde la interfaz web: User \u0026amp; Authentication → RADIUS Servers → editar el servidor → campo Secret.\nEn el momento en que aplicas este cambio, FortiGate empieza a enviar paquetes con el nuevo secret. FreeRADIUS todavía espera el antiguo: hay impacto desde este instante.\n4. Recargar FreeRADIUS inmediatamente # systemctl reload freeradius reload es suficiente si FreeRADIUS soporta recarga de configuración en caliente (versiones \u0026gt;= 3.0). Si tienes dudas o el reload no funciona en tu versión:\nsystemctl restart freeradius El restart añade 1-2 segundos adicionales de indisponibilidad, pero garantiza que se carga la configuración limpia.\n5. Verificar con radtest # Antes de dar la rotación por finalizada, valida que el nuevo secret funciona de extremo a extremo:\nradtest usuario contraseña+OTP IP-RADIUS 0 NuevoSecretAqui Una respuesta exitosa devuelve Access-Accept. Cualquier otra respuesta indica un problema.\n6. Plan de reversión # Si radtest falla o ves Access-Reject en los logs, revierte en el orden inverso:\nRestaura el secret antiguo en FortiGate (CLI o web) Restaura el clients.conf con el secret antiguo systemctl reload freeradius Confirma con radtest usando el secret antiguo Por eso es fundamental tener el secret anterior a mano durante toda la operación. No lo elimines del gestor de contraseñas hasta que la verificación post-rotación esté completa.\nGestión de múltiples clientes RADIUS # Si tienes varios NAS (múltiples FortiGate, APs con autenticación 802.1X, switches con TACACS+ extendido a RADIUS), no los cambies todos a la vez.\nEl enfoque correcto:\nRotar un cliente cada vez Completar la verificación de ese cliente antes de pasar al siguiente Si los clientes usan secrets diferentes (lo cual es recomendable), documenta qué secret usa cada uno En clients.conf puedes tener entradas separadas por cliente con secrets distintos:\nclient fortigate-sede-madrid { ipaddr = IP-FORTIGATE-MAD secret = SecretMadrid28Chars nastype = other } client ap-controller { ipaddr = IP-WLAN-CONTROLLER secret = SecretAPs28Chars nastype = other } Esto te permite rotar cada cliente de forma independiente y reduce el radio de impacto si algo sale mal.\nAlmacenamiento seguro del secret # El clients.conf contiene secrets en claro, lo que lo convierte en un archivo crítico:\nchown freerad:freerad /etc/freeradius/3.0/clients.conf chmod 640 /etc/freeradius/3.0/clients.conf Estos permisos aseguran que solo el proceso FreeRADIUS y root pueden leerlo. Verifica que tu sistema de backup no está exportando este archivo a un destino sin cifrado.\nPara entornos con mayor madurez de seguridad, las alternativas son:\nHashiCorp Vault: el script de despliegue recupera el secret de Vault en tiempo de ejecución y escribe clients.conf con vault kv get. El archivo nunca se persiste en disco sin cifrado. Bitwarden o KeePass corporativo: para equipos pequeños, guardar el secret en una bóveda compartida con acceso auditado es suficiente y mucho mejor que una hoja de cálculo o un archivo de texto. Lo que no es aceptable: el secret en un documento de Word compartido por email, en un comentario de un ticket de soporte, o en el historial de bash de un administrador.\nVerificación post-rotación # Una vez completada la rotación, el proceso de verificación no termina con radtest. Los pasos completos:\nInmediatos (primeros 2 minutos):\n# Verificar que no hay errores en el log de FreeRADIUS tail -f /var/log/freeradius/radius.log Busca líneas con Access-Accept para autenticaciones reales. Cualquier Access-Reject inesperado puede indicar un cliente que todavía usa el secret antiguo.\nPrueba funcional:\nConectar un usuario de prueba a la VPN y verificar que autentica correctamente Si tienes WiFi 802.1X, conectar un dispositivo de prueba a la red corporativa Verificar que las autenticaciones aparecen en los logs con el timestamp correcto Monitorización los primeros 15 minutos:\n# Si usas fail2ban para proteger RADIUS tail -f /var/log/fail2ban.log Un pico de Access-Reject seguido de bloqueos en fail2ban puede indicar que algún dispositivo (impresora, teléfono corporativo, servicio automatizado) no se actualizó con las nuevas credenciales y está bloqueando IPs legítimas.\nConclusión # La rotación del shared secret RADIUS no es complicada técnicamente, pero requiere coordinación y velocidad en los pasos críticos. El riesgo no está en el proceso en sí, sino en hacerlo de forma improvisada o sin tener claro el plan de reversión.\nCon este procedimiento: ventana de impacto de menos de 15 segundos si todo va bien, reversión en menos de 3 minutos si algo falla, y el proceso documentado para repetirlo cada 6-12 meses como parte del calendario de rotación de credenciales de infraestructura.\nEl secret testing123 que llevas años usando puede esperar hasta esta noche. Pero no más.\n","date":"18 juin 2026","externalUrl":null,"permalink":"/posts/rotacion-shared-secrets-radius-sin-corte-servicio/","section":"Posts","summary":"Por qué rotar el shared secret RADIUS # El shared secret RADIUS no es una contraseña de usuario: es la clave que cifra el campo User-Password en cada paquete RADIUS entre el NAS (FortiGate, AP, switch) y el servidor FreeRADIUS. El protocolo PAP transporta la contraseña del usuario XOR-cifrada con un hash MD5 derivado del shared secret y un vector aleatorio del paquete.\n","title":"Rotación de shared secrets RADIUS: proceso coordinado sin cortar el servicio","type":"posts"},{"content":"","date":"18 juin 2026","externalUrl":null,"permalink":"/tags/secrets/","section":"Tags","summary":"","title":"Secrets","type":"tags"},{"content":"","date":"18 juin 2026","externalUrl":null,"permalink":"/tags/ssl/","section":"Tags","summary":"","title":"Ssl","type":"tags"},{"content":"","date":"18 juin 2026","externalUrl":null,"permalink":"/tags/totp/","section":"Tags","summary":"","title":"Totp","type":"tags"},{"content":"","date":"18 juin 2026","externalUrl":null,"permalink":"/tags/ufw/","section":"Tags","summary":"","title":"Ufw","type":"tags"},{"content":"","date":"18 juin 2026","externalUrl":null,"permalink":"/tags/wifi/","section":"Tags","summary":"","title":"Wifi","type":"tags"},{"content":"","date":"18 juin 2026","externalUrl":null,"permalink":"/categories/wifi/","section":"Categories","summary":"","title":"WiFi","type":"categories"},{"content":"","date":"18 juin 2026","externalUrl":null,"permalink":"/tags/winbind/","section":"Tags","summary":"","title":"Winbind","type":"tags"},{"content":"","date":"18 juin 2026","externalUrl":null,"permalink":"/categories/windows/","section":"Categories","summary":"","title":"Windows","type":"categories"},{"content":"","date":"18 juin 2026","externalUrl":null,"permalink":"/tags/windows-server/","section":"Tags","summary":"","title":"Windows-Server","type":"tags"},{"content":"","date":"18 juin 2026","externalUrl":null,"permalink":"/tags/wpa-enterprise/","section":"Tags","summary":"","title":"Wpa-Enterprise","type":"tags"},{"content":"","date":"17 juin 2026","externalUrl":null,"permalink":"/tags/almacenamiento-remoto/","section":"Tags","summary":"","title":"Almacenamiento-Remoto","type":"tags"},{"content":" El problema # Tengo varios contenedores Docker con datos críticos en volúmenes. Un día se corrompió una base de datos y perdí días de información. Después de eso decidí automatizar los backups incrementales a almacenamiento remoto. Aquí comparto cómo lo hice.\nPreparación # Primero instalé duplicity en el servidor anfitrión:\napt-get install duplicity python3-pip pip3 install boto3 # si usas S3 Necesitaba acceso a almacenamiento remoto. En mi caso usé B2 de Backblaze, que es económico. También puedes usar S3, Google Cloud Storage o incluso un servidor SFTP propio.\nGeneré las credenciales necesarias y las guardé en un archivo seguro:\ncat \u0026gt; /root/.duplicity_env \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; export B2_ACCOUNT_ID=\u0026#34;tu_account_id\u0026#34; export B2_APP_KEY=\u0026#34;tu_app_key\u0026#34; EOF chmod 600 /root/.duplicity_env Estructura de backups # Decidí hacer backup de los volúmenes por separado. Para un contenedor con PostgreSQL, primero detenía el contenedor para garantizar consistencia:\ndocker stop mi_postgres Luego ejecutaba duplicity:\nsource /root/.duplicity_env duplicity \\ --full-if-older-than 30D \\ --include=/var/lib/docker/volumes/postgres_data/_data \\ --exclude=\u0026#39;**\u0026#39; \\ /var/lib/docker/volumes/postgres_data/_data \\ b2://bucket_name/postgres_backup docker start mi_postgres Este comando hace backups completos cada 30 días e incrementales el resto del tiempo. Sin --full-if-older-than, los incrementales se acumulan y la restauración es más lenta.\nAutomatización con cron # Creé un script de backup completo:\ncat \u0026gt; /usr/local/bin/backup-docker-volumes.sh \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; #!/bin/bash source /root/.duplicity_env TIMESTAMP=$(date +%Y%m%d_%H%M%S) LOG_FILE=\u0026#34;/var/log/docker-backups-$TIMESTAMP.log\u0026#34; backup_volume() { local volume_name=$1 local b2_path=$2 local container_name=$3 echo \u0026#34;[$(date)] Iniciando backup de $volume_name\u0026#34; \u0026gt;\u0026gt; $LOG_FILE docker stop $container_name 2\u0026gt;/dev/null sleep 5 duplicity \\ --full-if-older-than 30D \\ --include=/var/lib/docker/volumes/${volume_name}/_data \\ --exclude=\u0026#39;**\u0026#39; \\ /var/lib/docker/volumes/${volume_name}/_data \\ b2://$b2_path \u0026gt;\u0026gt; $LOG_FILE 2\u0026gt;\u0026amp;1 docker start $container_name echo \u0026#34;[$(date)] Backup de $volume_name completado\u0026#34; \u0026gt;\u0026gt; $LOG_FILE } backup_volume \u0026#34;postgres_data\u0026#34; \u0026#34;mi_bucket/postgres\u0026#34; \u0026#34;postgres\u0026#34; backup_volume \u0026#34;appdata\u0026#34; \u0026#34;mi_bucket/app\u0026#34; \u0026#34;webapp\u0026#34; duplicity cleanup --force b2://mi_bucket/postgres 2\u0026gt;/dev/null duplicity cleanup --force b2://mi_bucket/app 2\u0026gt;/dev/null EOF chmod +x /usr/local/bin/backup-docker-volumes.sh Lo agregué a crontab para que corra cada noche:\n0 2 * * * /usr/local/bin/backup-docker-volumes.sh Restauración ante fallos # Cuando un contenedor falla, la restauración es sencilla:\n# Crear directorio temporal mkdir -p /tmp/restore cd /tmp/restore source /root/.duplicity_env # Restaurar desde b2 duplicity restore b2://mi_bucket/postgres . # Los datos están en /tmp/restore ls -la Luego puedo copiar los datos al volumen:\ndocker stop postgres cp -r /tmp/restore/* /var/lib/docker/volumes/postgres_data/_data/ docker start postgres Verificación # Ejecuto verificaciones mensuales:\nduplicity verify b2://mi_bucket/postgres /var/lib/docker/volumes/postgres_data/_data Esto comprueba que los backups sean válidos sin restaurarlos completamente.\nLecciones aprendidas # Los backups sin verificación no sirven. He encontrado problemas en restauraciones de prueba. Detener el contenedor antes de backup es crítico para bases de datos. Los backups incrementales después de 30 días se vuelven difíciles de restaurar. Por eso limito el intervalo de completos. Guarda credenciales con chmod 600 siempre. Esta configuración me ha salvado varias veces. La automatización es la clave: sin cron, nunca hubiera tenido backups consistentes.\n","date":"17 juin 2026","externalUrl":null,"permalink":"/posts/automatizar-backups-incremental-de-volumenes-docker-a-almacenamiento-remoto-con-duplicity/","section":"Posts","summary":"El problema # Tengo varios contenedores Docker con datos críticos en volúmenes. Un día se corrompió una base de datos y perdí días de información. Después de eso decidí automatizar los backups incrementales a almacenamiento remoto. Aquí comparto cómo lo hice.\nPreparación # Primero instalé duplicity en el servidor anfitrión:\n","title":"Automatizar backups incremental de volúmenes Docker a almacenamiento remoto con duplicity","type":"posts"},{"content":"","date":"17 juin 2026","externalUrl":null,"permalink":"/tags/duplicity/","section":"Tags","summary":"","title":"Duplicity","type":"tags"},{"content":" El problema # Hace tiempo me pasó: un volumen Docker corrupto y sin backup limpio. Desde entonces, automatizo backups incrementales de mis volúmenes críticos. Te muestro cómo lo hago.\nRequisitos # Servidor Docker en Linux (Debian/Ubuntu) Acceso SSH a almacenamiento remoto (NAS, VPS, etc.) rsync instalado Bot de Telegram configurado Permisos de sudo o acceso directo a volúmenes Paso 1: Preparar el script de backup # Creo /opt/docker-backup/backup.sh:\n#!/bin/bash BACKUP_SOURCE=\u0026#34;/var/lib/docker/volumes\u0026#34; BACKUP_DEST=\u0026#34;user@remote-storage:/backups/docker-volumes\u0026#34; LOG_FILE=\u0026#34;/var/log/docker-backup.log\u0026#34; CHECKSUM_FILE=\u0026#34;/var/log/docker-backup.checksum\u0026#34; TELEGRAM_TOKEN=\u0026#34;YOUR_BOT_TOKEN\u0026#34; TELEGRAM_CHAT=\u0026#34;YOUR_CHAT_ID\u0026#34; # Función para enviar alertas por Telegram send_alert() { local message=$1 local status=$2 if [ \u0026#34;$status\u0026#34; = \u0026#34;error\u0026#34; ]; then curl -s -X POST \u0026#34;https://api.telegram.org/bot${TELEGRAM_TOKEN}/sendMessage\u0026#34; \\ -d chat_id=\u0026#34;${TELEGRAM_CHAT}\u0026#34; \\ -d text=\u0026#34;❌ Docker Backup Error: ${message}\u0026#34; else curl -s -X POST \u0026#34;https://api.telegram.org/bot${TELEGRAM_TOKEN}/sendMessage\u0026#34; \\ -d chat_id=\u0026#34;${TELEGRAM_CHAT}\u0026#34; \\ -d text=\u0026#34;✅ Docker Backup: ${message}\u0026#34; fi } # Crear directorio de log si no existe mkdir -p $(dirname \u0026#34;$LOG_FILE\u0026#34;) echo \u0026#34;[$(date \u0026#39;+%Y-%m-%d %H:%M:%S\u0026#39;)] Iniciando backup incremental\u0026#34; \u0026gt;\u0026gt; \u0026#34;$LOG_FILE\u0026#34; # Ejecutar rsync incremental rsync -av --delete \\ --backup-dir=\u0026#34;/backups/docker-volumes/daily-$(date +%Y%m%d)\u0026#34; \\ \u0026#34;$BACKUP_SOURCE/\u0026#34; \u0026#34;$BACKUP_DEST/\u0026#34; \u0026gt;\u0026gt; \u0026#34;$LOG_FILE\u0026#34; 2\u0026gt;\u0026amp;1 if [ $? -eq 0 ]; then echo \u0026#34;[$(date \u0026#39;+%Y-%m-%d %H:%M:%S\u0026#39;)] Backup completado exitosamente\u0026#34; \u0026gt;\u0026gt; \u0026#34;$LOG_FILE\u0026#34; BACKUP_STATUS=\u0026#34;exitoso\u0026#34; else echo \u0026#34;[$(date \u0026#39;+%Y-%m-%d %H:%M:%S\u0026#39;)] ERROR en backup\u0026#34; \u0026gt;\u0026gt; \u0026#34;$LOG_FILE\u0026#34; send_alert \u0026#34;Fallo en transferencia rsync\u0026#34; \u0026#34;error\u0026#34; exit 1 fi Paso 2: Validación de integridad # Agrego a backup.sh:\n# Generar checksum local find \u0026#34;$BACKUP_SOURCE\u0026#34; -type f -exec md5sum {} \\; | sort \u0026gt; /tmp/local.checksum # Verificar checksum remoto ssh user@remote-storage \u0026#34;find /backups/docker-volumes -type f -exec md5sum {} \\; | sort \u0026gt; /tmp/remote.checksum\u0026#34; # Comparar rsync --checksum \u0026#34;$BACKUP_SOURCE/\u0026#34; \u0026#34;$BACKUP_DEST/\u0026#34; --dry-run \u0026gt; /tmp/verify.log 2\u0026gt;\u0026amp;1 DIFF_COUNT=$(grep -c \u0026#34;would be transferred\u0026#34; /tmp/verify.log) if [ \u0026#34;$DIFF_COUNT\u0026#34; -gt 0 ]; then send_alert \u0026#34;Advertencia: ${DIFF_COUNT} archivos no sincronizados\u0026#34; \u0026#34;error\u0026#34; else send_alert \u0026#34;Integridad verificada: $(du -sh $BACKUP_SOURCE | cut -f1)\u0026#34; \u0026#34;ok\u0026#34; fi Paso 3: Configurar cron # Edito /etc/cron.d/docker-backup:\n# Backup completo cada domingo a las 2 AM 0 2 * * 0 root /opt/docker-backup/backup.sh # Backup incremental diario a las 3 AM 0 3 * * 1-6 root /opt/docker-backup/backup.sh # Limpieza de backups antiguos (más de 30 días) cada viernes 0 4 * * 5 root find /backups/docker-volumes/daily-* -mtime +30 -delete Paso 4: Configurar Telegram # Creo un bot en BotFather y obtengo el token. Luego:\n# Probar conexión curl -s -X POST \u0026#34;https://api.telegram.org/botTOKEN/sendMessage\u0026#34; \\ -d chat_id=\u0026#34;CHAT_ID\u0026#34; \\ -d text=\u0026#34;Test de conexión\u0026#34; Paso 5: Permisos y prueba # chmod 750 /opt/docker-backup/backup.sh chmod 640 /etc/cron.d/docker-backup # Ejecutar manualmente para verificar /opt/docker-backup/backup.sh En producción # Reviso logs semanalmente:\ntail -f /var/log/docker-backup.log He detectado problemas temprano gracias a las alertas de Telegram. El script se ejecuta sin intervención manual y rsync solo transfiere lo que cambió. Con esto dormía más tranquilo.\n","date":"15 juin 2026","externalUrl":null,"permalink":"/posts/automatizar-backups-incrementales-de-volumenes-docker-con-rsync-validacion-y-alertas-por-telegram/","section":"Posts","summary":"El problema # Hace tiempo me pasó: un volumen Docker corrupto y sin backup limpio. Desde entonces, automatizo backups incrementales de mis volúmenes críticos. Te muestro cómo lo hago.\nRequisitos # Servidor Docker en Linux (Debian/Ubuntu) Acceso SSH a almacenamiento remoto (NAS, VPS, etc.) rsync instalado Bot de Telegram configurado Permisos de sudo o acceso directo a volúmenes Paso 1: Preparar el script de backup # Creo /opt/docker-backup/backup.sh:\n","title":"Automatizar backups incrementales de volúmenes Docker con rsync, validación y alertas por Telegram","type":"posts"},{"content":"","date":"15 juin 2026","externalUrl":null,"permalink":"/tags/telegram/","section":"Tags","summary":"","title":"Telegram","type":"tags"},{"content":" Automatizar backups incrementales de volúmenes Docker con rsync y systemd timers # Después de perder datos por un fallo de disco, decidí implementar un sistema robusto de backups para mis volúmenes Docker. La solución que uso hoy es simple, confiable y se ejecuta sin intervención manual.\nEl problema # Tener contenedores con datos persistentes sin respaldo es un riesgo que no me puedo permitir. Los backups deben ser:\nIncrementales (no duplicar datos) Automatizados (sin recordarlos) Verificables (saber si son confiables) Remotos (proteger contra fallos locales) La solución: rsync + systemd timers # Uso rsync porque es eficiente con cambios incrementales y systemd timers porque no necesito cron ni servicios adicionales.\n1. Preparar directorios y credenciales # Primero, identifico mis volúmenes Docker:\ndocker volume ls En mi caso, los volúmenes están en /var/lib/docker/volumes/. Creo un directorio local de staging:\nmkdir -p /mnt/backups/docker-volumes chmod 700 /mnt/backups Para el almacenamiento remoto, configuro SSH sin contraseña. Genero una clave dedicada:\nssh-keygen -t ed25519 -f /root/.ssh/backup_key -N \u0026#34;\u0026#34; -C \u0026#34;docker-backup\u0026#34; La añado al servidor remoto en .ssh/authorized_keys del usuario de backup.\n2. Script de backup con verificación # Creo /usr/local/bin/docker-backup.sh:\n#!/bin/bash set -e BACKUP_DIR=\u0026#34;/mnt/backups/docker-volumes\u0026#34; REMOTE_USER=\u0026#34;backup\u0026#34; REMOTE_HOST=\u0026#34;backup.example.com\u0026#34; REMOTE_PATH=\u0026#34;/backups/docker\u0026#34; SSH_KEY=\u0026#34;/root/.ssh/backup_key\u0026#34; CHECKSUM_FILE=\u0026#34;${BACKUP_DIR}/.checksums\u0026#34; # Función para loguear log() { echo \u0026#34;[$(date +\u0026#39;%Y-%m-%d %H:%M:%S\u0026#39;)] $1\u0026#34; | tee -a /var/log/docker-backup.log } log \u0026#34;Iniciando backup de volúmenes Docker\u0026#34; # Copiar volúmenes locales docker volume ls -q | while read volume; do SOURCE=\u0026#34;/var/lib/docker/volumes/${volume}/_data\u0026#34; DEST=\u0026#34;${BACKUP_DIR}/${volume}\u0026#34; if [ -d \u0026#34;$SOURCE\u0026#34; ]; then mkdir -p \u0026#34;$DEST\u0026#34; rsync -av --delete \u0026#34;$SOURCE/\u0026#34; \u0026#34;$DEST/\u0026#34; || log \u0026#34;Error en $volume\u0026#34; fi done log \u0026#34;Generando checksums\u0026#34; find \u0026#34;$BACKUP_DIR\u0026#34; -type f -exec md5sum {} \\; \u0026gt; \u0026#34;$CHECKSUM_FILE\u0026#34; 2\u0026gt;/dev/null || true log \u0026#34;Sincronizando con servidor remoto\u0026#34; rsync -avz \\ --delete \\ -e \u0026#34;ssh -i $SSH_KEY -o StrictHostKeyChecking=no\u0026#34; \\ \u0026#34;$BACKUP_DIR/\u0026#34; \\ \u0026#34;${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_PATH}/\u0026#34; if [ $? -eq 0 ]; then log \u0026#34;Backup completado exitosamente\u0026#34; # Limpiar backups locales más antiguos de 7 días find \u0026#34;$BACKUP_DIR\u0026#34; -type f -mtime +7 -delete else log \u0026#34;ERROR: Fallo en la sincronización remota\u0026#34; exit 1 fi Hago el script ejecutable:\nchmod +x /usr/local/bin/docker-backup.sh 3. Crear systemd service y timer # Creo /etc/systemd/system/docker-backup.service:\n[Unit] Description=Docker Volumes Backup Service After=docker.service [Service] Type=oneshot ExecStart=/usr/local/bin/docker-backup.sh StandardOutput=journal StandardError=journal Y /etc/systemd/system/docker-backup.timer:\n[Unit] Description=Daily Docker Backup Timer Requires=docker-backup.service [Timer] OnCalendar=daily OnCalendar=*-*-* 02:00:00 Persistent=true [Install] WantedBy=timers.target 4. Activar y verificar # systemctl daemon-reload systemctl enable docker-backup.timer systemctl start docker-backup.timer systemctl status docker-backup.timer Ver logs:\njournalctl -u docker-backup.service -f 5. Verificar integridad remota # En el servidor remoto, creo un script para validar:\n#!/bin/bash REMOTE_PATH=\u0026#34;/backups/docker\u0026#34; cd \u0026#34;$REMOTE_PATH\u0026#34; md5sum -c .checksums \u0026gt; /tmp/backup-verify.log 2\u0026gt;\u0026amp;1 if [ $? -eq 0 ]; then echo \u0026#34;Backups íntegros\u0026#34; | mail -s \u0026#34;Backup OK\u0026#34; admin@example.com else echo \u0026#34;ADVERTENCIA: Fallos en checksums\u0026#34; | mail -s \u0026#34;Backup FALLIDO\u0026#34; admin@example.com fi Lo ejecuto como cron diario.\nResultado # Llevo 6 meses con este sistema. Los backups se ejecutan automáticamente cada noche sin intervención. Los checksums me dan confianza en que los datos remoto están intactos. He recuperado volúmenes tres veces sin problemas.\nLo mejor: no he tenido que pensar en ello desde que lo configuré.\n","date":"12 juin 2026","externalUrl":null,"permalink":"/posts/automatizar-backups-incrementales-de-volumenes-docker-con-rsync-y-systemd-timers/","section":"Posts","summary":"Automatizar backups incrementales de volúmenes Docker con rsync y systemd timers # Después de perder datos por un fallo de disco, decidí implementar un sistema robusto de backups para mis volúmenes Docker. La solución que uso hoy es simple, confiable y se ejecuta sin intervención manual.\nEl problema # Tener contenedores con datos persistentes sin respaldo es un riesgo que no me puedo permitir. Los backups deben ser:\n","title":"Automatizar backups incrementales de volúmenes Docker con rsync y systemd timers","type":"posts"},{"content":" El problema # Tenía PostgreSQL y Grafana corriendo en Docker con volúmenes persistentes. La idea de perder esos datos me mantenía despierto. Backups manuales no son confiables. Necesitaba algo automático, eficiente y verificable.\nDespués de probar varias opciones, me decidí por restic + S3 + cron jobs. Es directo y funciona.\nPor qué restic # Restic hace backups incrementales deduplicados. Solo envía lo que cambió. Es perfecto para volúmenes Docker que típicamente tienen pequeños cambios día a día. Además verifica integridad automáticamente.\nSetup básico # 1. Instalar restic # sudo apt-get update sudo apt-get install restic 2. Crear credenciales S3 # Necesitas acceso a un bucket S3 (o MinIO si usas self-hosted). Crea las credenciales con permisos limitados:\nexport AWS_ACCESS_KEY_ID=\u0026#34;tu_access_key\u0026#34; export AWS_SECRET_ACCESS_KEY=\u0026#34;tu_secret_key\u0026#34; export AWS_DEFAULT_REGION=\u0026#34;us-east-1\u0026#34; Guarda esto en un archivo seguro (/root/.restic-env):\nexport AWS_ACCESS_KEY_ID=\u0026#34;...\u0026#34; export AWS_SECRET_ACCESS_KEY=\u0026#34;...\u0026#34; export RESTIC_REPOSITORY=\u0026#34;s3:https://s3.amazonaws.com/tu-bucket/backups\u0026#34; export RESTIC_PASSWORD=\u0026#34;tu_password_restic_fuerte\u0026#34; Permisos: chmod 600 /root/.restic-env\n3. Inicializar el repositorio # source /root/.restic-env restic init Script de backup # Crea /opt/backup-docker.sh:\n#!/bin/bash source /root/.restic-env BACKUP_DIRS=( \u0026#34;/var/lib/docker/volumes/postgres_data/_data\u0026#34; \u0026#34;/var/lib/docker/volumes/grafana_data/_data\u0026#34; ) TIMESTAMP=$(date +%Y-%m-%d_%H:%M:%S) LOG_FILE=\u0026#34;/var/log/restic-backup.log\u0026#34; echo \u0026#34;=== Backup iniciado: $TIMESTAMP ===\u0026#34; \u0026gt;\u0026gt; $LOG_FILE for dir in \u0026#34;${BACKUP_DIRS[@]}\u0026#34;; do if [ -d \u0026#34;$dir\u0026#34; ]; then echo \u0026#34;Backupeando: $dir\u0026#34; \u0026gt;\u0026gt; $LOG_FILE restic backup \u0026#34;$dir\u0026#34; --tag \u0026#34;docker\u0026#34; --tag \u0026#34;$(date +%A)\u0026#34; \u0026gt;\u0026gt; $LOG_FILE 2\u0026gt;\u0026amp;1 if [ $? -eq 0 ]; then echo \u0026#34;✓ Backup exitoso: $dir\u0026#34; \u0026gt;\u0026gt; $LOG_FILE else echo \u0026#34;✗ Error en backup: $dir\u0026#34; \u0026gt;\u0026gt; $LOG_FILE # Aquí podrías enviar una notificación fi fi done echo \u0026#34;=== Backup completado ===\u0026#34; \u0026gt;\u0026gt; $LOG_FILE Permisos:\nchmod +x /opt/backup-docker.sh Automatizar con cron # Edita la crontab:\ncrontab -e Agrega:\n# Backup diario a las 2 AM 0 2 * * * /opt/backup-docker.sh # Verificación de integridad cada domingo a las 3 AM 0 3 * * 0 source /root/.restic-env \u0026amp;\u0026amp; restic check --with-cache \u0026gt;\u0026gt; /var/log/restic-check.log 2\u0026gt;\u0026amp;1 # Limpieza de snapshots viejos cada mes 0 4 1 * * source /root/.restic-env \u0026amp;\u0026amp; restic forget --keep-daily 30 --keep-weekly 12 --prune \u0026gt;\u0026gt; /var/log/restic-prune.log 2\u0026gt;\u0026amp;1 Validación programada # La línea de restic check verifica que todos los datos estén íntegros. Es lo que duermes más tranquilo:\nsource /root/.restic-env restic check --with-cache Si hay corrupción, lo sabrás. Si todo está bien, ves esto:\nusing backend repository stored in s3:... checking integrity of repository [...] 100.00% checking snapshots, keys, buckets check successful, no errors were found Restaurar # Cuando lo necesites (espero que no):\nsource /root/.restic-env restic restore latest --target /tmp/restore O un snapshot específico:\nrestic snapshots # lista todos restic restore \u0026lt;snapshot-id\u0026gt; --target /tmp/restore En producción # Monitorea los logs:\ntail -f /var/log/restic-backup.log Considera agregar alertas si los backups fallan. Yo tengo un script que envía notificación a Discord si detecta errores.\nResultados # Después de tres meses:\nPostgreSQL: 2.3 GB comprimido Grafana: 150 MB comprimido Almacenamiento total en S3: 1.8 GB (deduplicación en acción) Los backups incrementales tardan entre 30 segundos y 2 minutos. Las verificaciones de integridad unos 5 minutos.\nEs confiable. Dormimos mejor.\nActualizado: 2026-06-10\n","date":"10 juin 2026","externalUrl":null,"permalink":"/posts/automatizar-backups-incrementales-de-docker-con-restic-hacia-s3/","section":"Posts","summary":"El problema # Tenía PostgreSQL y Grafana corriendo en Docker con volúmenes persistentes. La idea de perder esos datos me mantenía despierto. Backups manuales no son confiables. Necesitaba algo automático, eficiente y verificable.\nDespués de probar varias opciones, me decidí por restic + S3 + cron jobs. Es directo y funciona.\nPor qué restic # Restic hace backups incrementales deduplicados. Solo envía lo que cambió. Es perfecto para volúmenes Docker que típicamente tienen pequeños cambios día a día. Además verifica integridad automáticamente.\n","title":"Automatizar backups incrementales de Docker con restic hacia S3","type":"posts"},{"content":"","date":"10 juin 2026","externalUrl":null,"permalink":"/tags/s3/","section":"Tags","summary":"","title":"S3","type":"tags"},{"content":" El problema # Tenía varios contenedores Docker con datos importantes: bases de datos, archivos de configuración, datos de aplicaciones. Necesitaba backups automáticos, incrementales y con retención inteligente. No quería gastar mucho en almacenamiento externo.\nLa solución: restic + S3. Restic es pequeño, eficiente y maneja incrementales nativamente. S3 es barato y confiable.\nSetup inicial # Primero instalé restic en el host:\ncurl -b /tmp/restic.bz2 -L https://github.com/restic/restic/releases/download/v0.16.0/restic_0.16.0_linux_amd64.bz2 cd /tmp \u0026amp;\u0026amp; bunzip2 restic.bz2 \u0026amp;\u0026amp; mv restic /usr/local/bin/ chmod +x /usr/local/bin/restic Creé credenciales de AWS IAM con permisos solo para S3:\n{ \u0026#34;Version\u0026#34;: \u0026#34;2012-10-17\u0026#34;, \u0026#34;Statement\u0026#34;: [ { \u0026#34;Effect\u0026#34;: \u0026#34;Allow\u0026#34;, \u0026#34;Action\u0026#34;: [ \u0026#34;s3:ListBucket\u0026#34;, \u0026#34;s3:GetBucketVersioning\u0026#34;, \u0026#34;s3:ListBucketVersions\u0026#34; ], \u0026#34;Resource\u0026#34;: \u0026#34;arn:aws:s3:::mi-bucket-backups\u0026#34; }, { \u0026#34;Effect\u0026#34;: \u0026#34;Allow\u0026#34;, \u0026#34;Action\u0026#34;: [ \u0026#34;s3:GetObject\u0026#34;, \u0026#34;s3:PutObject\u0026#34;, \u0026#34;s3:DeleteObject\u0026#34; ], \u0026#34;Resource\u0026#34;: \u0026#34;arn:aws:s3:::mi-bucket-backups/*\u0026#34; } ] } Inicializar repositorio restic # export AWS_ACCESS_KEY_ID=\u0026#34;tu_access_key\u0026#34; export AWS_SECRET_ACCESS_KEY=\u0026#34;tu_secret_key\u0026#34; export RESTIC_PASSWORD=\u0026#34;contraseña_muy_segura\u0026#34; restic -r s3:s3.amazonaws.com/mi-bucket-backups init Guardé estas variables en /etc/restic/env con permisos 600.\nScript de backup # Creé /usr/local/bin/backup-docker-volumes.sh:\n#!/bin/bash source /etc/restic/env TIMESTAMP=$(date +%Y%m%d_%H%M%S) LOG_FILE=\u0026#34;/var/log/restic-backup-${TIMESTAMP}.log\u0026#34; # Array de volúmenes a respaldar VOLUMES=( \u0026#34;postgres_data\u0026#34; \u0026#34;nginx_config\u0026#34; \u0026#34;app_data\u0026#34; ) # Montar volúmenes temporalmente MOUNT_POINT=\u0026#34;/mnt/backup_volumes\u0026#34; mkdir -p ${MOUNT_POINT} for VOLUME in \u0026#34;${VOLUMES[@]}\u0026#34;; do VOLUME_PATH=\u0026#34;${MOUNT_POINT}/${VOLUME}\u0026#34; mkdir -p ${VOLUME_PATH} docker run --rm -v ${VOLUME}:/data -v ${MOUNT_POINT}:/mnt alpine \\ cp -r /data/* /mnt/${VOLUME}/ 2\u0026gt;/dev/null || true done # Ejecutar backup incremental restic -r s3:s3.amazonaws.com/mi-bucket-backups backup ${MOUNT_POINT} \\ --tag docker-volumes \\ --tag $(date +%Y-%m-%d) \\ \u0026gt;\u0026gt; ${LOG_FILE} 2\u0026gt;\u0026amp;1 BACKUP_STATUS=$? # Limpiar rm -rf ${MOUNT_POINT} # Rotación: mantener últimos 30 días diarios, 12 meses semanales restic -r s3:s3.amazonaws.com/mi-bucket-backups forget \\ --keep-daily 30 \\ --keep-weekly 12 \\ --keep-monthly 6 \\ --prune \\ \u0026gt;\u0026gt; ${LOG_FILE} 2\u0026gt;\u0026amp;1 if [ $BACKUP_STATUS -eq 0 ]; then echo \u0026#34;Backup exitoso\u0026#34; \u0026gt;\u0026gt; ${LOG_FILE} exit 0 else echo \u0026#34;Error en backup\u0026#34; \u0026gt;\u0026gt; ${LOG_FILE} exit 1 fi Lo hice ejecutable:\nchmod +x /usr/local/bin/backup-docker-volumes.sh Automatizar con systemd # Creé /etc/systemd/system/docker-backup.service:\n[Unit] Description=Docker Volumes Backup to S3 After=docker.service Requires=docker.service [Service] Type=oneshot ExecStart=/usr/local/bin/backup-docker-volumes.sh StandardOutput=journal StandardError=journal Y el timer en /etc/systemd/system/docker-backup.timer:\n[Unit] Description=Daily Docker Backup Timer [Timer] OnCalendar=daily OnCalendar=*-*-* 02:00:00 Persistent=true [Install] WantedBy=timers.target Activé el timer:\nsystemctl daemon-reload systemctl enable docker-backup.timer systemctl start docker-backup.timer Verificación y monitoreo # Para ver el estado:\nsystemctl status docker-backup.timer journalctl -u docker-backup.service -f Para listar snapshots:\nrestic -r s3:s3.amazonaws.com/mi-bucket-backups snapshots Para restaurar un volumen específico:\nrestic -r s3:s3.amazonaws.com/mi-bucket-backups restore latest \\ --target /tmp/restore \\ --path /mnt/backup_volumes/postgres_data Resultado final # Los backups corren automáticamente cada madrugada. Restic comprime y deduplica, así que el almacenamiento en S3 es mínimo. La rotación automática mantiene 30 días de backups diarios sin acumular basura.\nLlevó una tarde configurarlo, pero ahora duermo tranquilo.\n","date":"8 juin 2026","externalUrl":null,"permalink":"/posts/automatizar-backups-incrementales-de-volumenes-docker-con-restic-y-s3/","section":"Posts","summary":"El problema # Tenía varios contenedores Docker con datos importantes: bases de datos, archivos de configuración, datos de aplicaciones. Necesitaba backups automáticos, incrementales y con retención inteligente. No quería gastar mucho en almacenamiento externo.\nLa solución: restic + S3. Restic es pequeño, eficiente y maneja incrementales nativamente. S3 es barato y confiable.\nSetup inicial # Primero instalé restic en el host:\n","title":"Automatizar backups incrementales de volúmenes Docker con restic y S3","type":"posts"},{"content":"Tenía un problema real: mis datos en volúmenes Docker estaban solo en el servidor local. Un fallo de disco y perdía todo. Necesitaba backups automáticos, incrementales y notificaciones cuando algo fallara. Aquí está lo que implementé.\nEl plan # Usar rsync para copiar solo lo que cambió, ejecutarlo cada noche con cron, y recibir un email si algo falla. Simple y sin dependencias raras.\nPaso 1: Preparar el almacenamiento remoto # Acceso SSH al servidor remoto configurado sin contraseña (con claves). En mi caso, un NAS en la red.\nssh-keygen -t ed25519 -f ~/.ssh/backup_key -N \u0026#34;\u0026#34; ssh-copy-id -i ~/.ssh/backup_key user@nas.local Crear el directorio destino:\nssh -i ~/.ssh/backup_key user@nas.local mkdir -p /backups/docker-volumes Paso 2: Crear el script de backup # Archivo: /opt/backup/backup-docker-volumes.sh\n#!/bin/bash BACKUP_HOST=\u0026#34;user@nas.local\u0026#34; BACKUP_PATH=\u0026#34;/backups/docker-volumes\u0026#34; SSH_KEY=\u0026#34;/root/.ssh/backup_key\u0026#34; DOCKER_VOLUMES_PATH=\u0026#34;/var/lib/docker/volumes\u0026#34; LOG_FILE=\u0026#34;/var/log/docker-backup.log\u0026#34; EMAIL=\u0026#34;admin@example.local\u0026#34; # Función para registrar log() { echo \u0026#34;[$(date \u0026#39;+%Y-%m-%d %H:%M:%S\u0026#39;)] $1\u0026#34; \u0026gt;\u0026gt; \u0026#34;$LOG_FILE\u0026#34; } # Función para enviar alertas send_alert() { local subject=\u0026#34;$1\u0026#34; local message=\u0026#34;$2\u0026#34; echo \u0026#34;$message\u0026#34; | mail -s \u0026#34;$subject\u0026#34; \u0026#34;$EMAIL\u0026#34; } log \u0026#34;Iniciando backup de volúmenes Docker\u0026#34; # Listar y respaldar cada volumen for volume in $(docker volume ls --quiet); do VOLUME_PATH=\u0026#34;$DOCKER_VOLUMES_PATH/$volume/_data\u0026#34; if [ ! -d \u0026#34;$VOLUME_PATH\u0026#34; ]; then log \u0026#34;ADVERTENCIA: No se encontró $VOLUME_PATH\u0026#34; continue fi log \u0026#34;Respaldando volumen: $volume\u0026#34; rsync -avz --delete \\ -e \u0026#34;ssh -i $SSH_KEY -o StrictHostKeyChecking=no\u0026#34; \\ \u0026#34;$VOLUME_PATH/\u0026#34; \\ \u0026#34;$BACKUP_HOST:$BACKUP_PATH/$volume/\u0026#34; \\ \u0026gt;\u0026gt; \u0026#34;$LOG_FILE\u0026#34; 2\u0026gt;\u0026amp;1 if [ $? -eq 0 ]; then log \u0026#34;✓ Volumen $volume respaldado correctamente\u0026#34; else log \u0026#34;✗ ERROR respaldando $volume\u0026#34; send_alert \u0026#34;FALLO: Backup Docker - $volume\u0026#34; \\ \u0026#34;El backup del volumen $volume falló. Revisa $LOG_FILE\u0026#34; exit 1 fi done log \u0026#34;Backup completado\u0026#34; send_alert \u0026#34;OK: Backup Docker completado\u0026#34; \u0026#34;Todos los volúmenes se respaldaron correctamente en $(date)\u0026#34; Dar permisos:\nchmod 755 /opt/backup/backup-docker-volumes.sh Paso 3: Configurar cron # Abre el crontab root:\ncrontab -e Añade esta línea para ejecutar cada noche a las 3 AM:\n0 3 * * * /opt/backup/backup-docker-volumes.sh Paso 4: Configurar el correo # Instala un MTA ligero. En Debian/Ubuntu:\napt-get install msmtp msmtp-mta Archivo de configuración: ~/.msmtprc\ndefaults auth on tls on tls_trust_file /etc/ssl/certs/ca-certificates.crt account gmail host smtp.gmail.com port 587 from docker-backup@example.local user tu_correo@gmail.com password tu_contraseña_app account default : gmail Permisos restringidos:\nchmod 600 ~/.msmtprc Paso 5: Prueba # /opt/backup/backup-docker-volumes.sh Verifica el log:\ntail -f /var/log/docker-backup.log Consideraciones reales # Espacio: Los primeros backups pesan. Monitorea el almacenamiento remoto.\nAncho de banda: rsync es eficiente, pero con volúmenes grandes nocturno es mejor.\nSeguridad: La clave SSH sin contraseña funciona, pero está en el servidor. Usa permisos restrictivos (600 en la clave).\nRotación: Después de 30 días, los incrementales se acumulan. Considera crear snapshots mensuales completos.\nVerificación: Una vez al mes, restaura algo de verdad. Un backup sin testar es un backup que no funciona.\nHe estado usando esto 8 meses. Solo ha fallado por timeout SSH (solución: aumenté el timeout en rsync). Los emails de confirmación diaria me dan paz mental.\n","date":"5 juin 2026","externalUrl":null,"permalink":"/posts/automatizar-backups-incrementales-de-volumenes-docker-a-almacenamiento-remoto-con-rsync-y-cron/","section":"Posts","summary":"Tenía un problema real: mis datos en volúmenes Docker estaban solo en el servidor local. Un fallo de disco y perdía todo. Necesitaba backups automáticos, incrementales y notificaciones cuando algo fallara. Aquí está lo que implementé.\nEl plan # Usar rsync para copiar solo lo que cambió, ejecutarlo cada noche con cron, y recibir un email si algo falla. Simple y sin dependencias raras.\n","title":"Automatizar backups incrementales de volúmenes Docker a almacenamiento remoto con rsync y cron","type":"posts"},{"content":" Por qué necesitas backups remotos # Tenía dos contenedores críticos corriendo en mi servidor doméstico: Grafana con históricos de métricas y Listmonk manejando mi lista de correos. La idea de perder esos datos me mantenía despierto. Los backups locales no son suficientes. Necesitaba algo remoto, automatizado y que no me costara una fortuna.\nRestic + S3 (o compatible con S3) es la combinación que elegí. Restic es eficiente, soporta deduplicación y cifrado. S3 es accesible y barato. Aquí va lo que aprendí implementándolo.\nInstalación de restic # Primero, instalé restic en la máquina host:\nwget https://github.com/restic/restic/releases/download/v0.16.0/restic_0.16.0_linux_amd64.bz2 bzip2 -d restic_0.16.0_linux_amd64.bz2 mv restic_0.16.0_linux_amd64 /usr/local/bin/restic chmod +x /usr/local/bin/restic restic version Configurar credenciales de S3 # Creé un archivo de configuración con las credenciales. Usé Minio en mi caso (compatible con S3), pero funciona igual con AWS S3:\ncat \u0026gt; /root/.restic-env \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; export AWS_ACCESS_KEY_ID=\u0026#34;tu_access_key\u0026#34; export AWS_SECRET_ACCESS_KEY=\u0026#34;tu_secret_key\u0026#34; export RESTIC_REPOSITORY=\u0026#34;s3:https://minio.dominio.local/backups/docker\u0026#34; export RESTIC_PASSWORD=\u0026#34;contraseña_fuerte_para_restic\u0026#34; EOF chmod 600 /root/.restic-env Inicializar el repositorio # source /root/.restic-env restic init Restic crea la estructura necesaria en S3. Solo se hace una vez.\nEstrategia de backup para Docker # Para Grafana y Listmonk necesitaba extraer los volúmenes. Creé un script que:\nDetiene temporalmente los contenedores Copia los volúmenes a un directorio temporal Ejecuta restic Reinicia los contenedores cat \u0026gt; /usr/local/bin/docker-backup.sh \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; #!/bin/bash source /root/.restic-env BACKUP_DIR=\u0026#34;/tmp/docker-backup\u0026#34; CONTAINERS=(\u0026#34;grafana\u0026#34; \u0026#34;listmonk\u0026#34;) mkdir -p $BACKUP_DIR for container in \u0026#34;${CONTAINERS[@]}\u0026#34;; do echo \u0026#34;[$(date)] Deteniendo $container...\u0026#34; docker stop $container echo \u0026#34;[$(date)] Backupeando volúmenes de $container...\u0026#34; docker run --rm -v ${container}-storage:/data -v $BACKUP_DIR:/backup \\ alpine tar -czf /backup/${container}.tar.gz -C /data . echo \u0026#34;[$(date)] Iniciando $container...\u0026#34; docker start $container done echo \u0026#34;[$(date)] Ejecutando restic backup...\u0026#34; restic backup $BACKUP_DIR --tag=\u0026#34;docker-containers\u0026#34; --cleanup-cache echo \u0026#34;[$(date)] Backup completado\u0026#34; echo \u0026#34;[$(date)] Limpiando archivos temporales...\u0026#34; rm -rf $BACKUP_DIR restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 12 --prune EOF chmod +x /usr/local/bin/docker-backup.sh Automatizar con cron # # Backup diario a las 2 AM 0 2 * * * /usr/local/bin/docker-backup.sh \u0026gt;\u0026gt; /var/log/docker-backup.log 2\u0026gt;\u0026amp;1 Verificar y restaurar # Para estar seguro, verifico regularmente:\nsource /root/.restic-env restic snapshots restic check Para restaurar en caso de desastre:\nsource /root/.restic-env restic restore latest --target /tmp/restore-point Después extraería los tar.gz en los volúmenes correspondientes.\nLo que aprendí # No confíes solo en backups locales. Un fallo del disco afecta todo. Cifra los datos en tránsito. Restic lo hace automáticamente. Verifica los backups. He visto casos donde fallaban sin aviso. Ten política de retención. No necesito backups de hace 2 años. Monitorea los logs. Agrego alertas si el backup falla. Consideraciones finales # Esta configuración me permite dormir tranquilo. Los backups corren automaticamente, están cifrados, deduplicados y almacenados remotamente. El único punto débil es confiar en que S3 no falla, pero eso es aceptable para un servidor doméstico.\nSi usas AWS S3 puro, el costo es mínimo. Si usas Minio o Backblaze B2, aún mejor.\n","date":"3 juin 2026","externalUrl":null,"permalink":"/posts/backups-automatizados-con-restic-y-s3-protegiendo-grafana-y-listmonk-en-docker/","section":"Posts","summary":"Por qué necesitas backups remotos # Tenía dos contenedores críticos corriendo en mi servidor doméstico: Grafana con históricos de métricas y Listmonk manejando mi lista de correos. La idea de perder esos datos me mantenía despierto. Los backups locales no son suficientes. Necesitaba algo remoto, automatizado y que no me costara una fortuna.\nRestic + S3 (o compatible con S3) es la combinación que elegí. Restic es eficiente, soporta deduplicación y cifrado. S3 es accesible y barato. Aquí va lo que aprendí implementándolo.\n","title":"Backups automatizados con restic y S3: Protegiendo Grafana y Listmonk en Docker","type":"posts"},{"content":" El problema # Tenía varios contenedores Docker corriendo servicios críticos en mi servidor doméstico. La idea de perder datos por un fallo de disco me mantenía despierto. Necesitaba una solución que:\nFuera incremental (no copiar todo cada vez) Se ejecutara automáticamente sin intervención Permitiera rotación de copias antiguas Guardara los datos en almacenamiento remoto Después de probar varias opciones, restic fue la respuesta. Es rápido, eficiente y se integra bien con cron.\nInstalación de restic # Lo primero es instalar restic en el servidor:\ncurl -sL https://api.github.com/repos/restic/restic/releases/latest | grep -oP \u0026#39;\u0026#34;browser_download_url\u0026#34;: \u0026#34;\\K.*linux_amd64\\.bz2\u0026#39; | head -1 | xargs wget -O - | bunzip2 \u0026gt; /usr/local/bin/restic chmod +x /usr/local/bin/restic restic version También necesito jq para procesar JSON en los scripts:\napt-get install jq Configurar almacenamiento remoto # Usé un servidor SFTP remoto, pero restic soporta S3, B2, Google Cloud, etc.\nPrimero creo el repositorio:\nexport RESTIC_REPOSITORY=\u0026#34;sftp:usuario@servidor.remoto:/ruta/backups/docker\u0026#34; export RESTIC_PASSWORD=\u0026#34;contraseña_super_segura\u0026#34; restic init Guardo las credenciales en un archivo protegido:\ncat \u0026gt; /root/.restic_env \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; export RESTIC_REPOSITORY=\u0026#34;sftp:usuario@servidor.remoto:/ruta/backups/docker\u0026#34; export RESTIC_PASSWORD=\u0026#34;contraseña_super_segura\u0026#34; export RESTIC_CACHE_DIR=\u0026#34;/var/cache/restic\u0026#34; EOF chmod 600 /root/.restic_env Script de backup # Creo el script principal en /usr/local/bin/docker-backup.sh:\n#!/bin/bash source /root/.restic_env # Exportar volúmenes de contenedores específicos CONTAINERS=(\u0026#34;contenedor1\u0026#34; \u0026#34;contenedor2\u0026#34; \u0026#34;contenedor3\u0026#34;) BACKUP_DIR=\u0026#34;/tmp/docker-backup\u0026#34; mkdir -p \u0026#34;$BACKUP_DIR\u0026#34; for container in \u0026#34;${CONTAINERS[@]}\u0026#34;; do echo \u0026#34;[$(date +\u0026#39;%Y-%m-%d %H:%M:%S\u0026#39;)] Exportando $container...\u0026#34; docker export \u0026#34;$container\u0026#34; | gzip \u0026gt; \u0026#34;$BACKUP_DIR/$container.tar.gz\u0026#34; done # Backup de directorios de volúmenes echo \u0026#34;[$(date +\u0026#39;%Y-%m-%d %H:%M:%S\u0026#39;)] Iniciando backup con restic...\u0026#34; restic backup \u0026#34;$BACKUP_DIR\u0026#34; \\ --tag \u0026#34;docker-backup\u0026#34; \\ --tag \u0026#34;$(date +\u0026#39;%Y-%m-%d\u0026#39;)\u0026#34; \\ --verbose if [ $? -eq 0 ]; then echo \u0026#34;[$(date +\u0026#39;%Y-%m-%d %H:%M:%S\u0026#39;)] Backup exitoso\u0026#34; rm -rf \u0026#34;$BACKUP_DIR\u0026#34; else echo \u0026#34;[$(date +\u0026#39;%Y-%m-%d %H:%M:%S\u0026#39;)] Error en backup\u0026#34; \u0026gt;\u0026amp;2 exit 1 fi Lo hago ejecutable:\nchmod +x /usr/local/bin/docker-backup.sh Script de rotación # Creo otro script para limpiar backups antiguos en /usr/local/bin/docker-backup-prune.sh:\n#!/bin/bash source /root/.restic_env echo \u0026#34;[$(date +\u0026#39;%Y-%m-%d %H:%M:%S\u0026#39;)] Iniciando limpieza de backups antiguos...\u0026#34; # Mantener últimos 30 backups diarios y 12 mensuales restic forget \\ --keep-daily 30 \\ --keep-monthly 12 \\ --keep-yearly 2 \\ --tag \u0026#34;docker-backup\u0026#34; \\ --prune \\ --verbose echo \u0026#34;[$(date +\u0026#39;%Y-%m-%d %H:%M:%S\u0026#39;)] Limpieza completada\u0026#34; chmod +x /usr/local/bin/docker-backup-prune.sh Configurar cron # Edito la crontab del root:\ncrontab -e Agrego las siguientes líneas:\n# Backup diario a las 2 AM 0 2 * * * /usr/local/bin/docker-backup.sh \u0026gt;\u0026gt; /var/log/docker-backup.log 2\u0026gt;\u0026amp;1 # Rotación de backups cada domingo a las 3 AM 0 3 * * 0 /usr/local/bin/docker-backup-prune.sh \u0026gt;\u0026gt; /var/log/docker-backup.log 2\u0026gt;\u0026amp;1 Monitoreo # Para saber qué está pasando, reviso los logs regularmente:\ntail -f /var/log/docker-backup.log También puedo verificar el estado del repositorio:\nsource /root/.restic_env restic snapshots restic stats Resultado # Después de una semana funcionando, tengo:\nBackups incrementales diarios sin intervención manual 30 días de historiales disponibles Rotación automática de copias antiguas Datos replicados en servidor remoto seguro Esto me quitó la preocupación. Los contenedores están protegidos.\n","date":"1 juin 2026","externalUrl":null,"permalink":"/posts/automatizar-backups-incrementales-de-docker-con-restic-y-cron/","section":"Posts","summary":"El problema # Tenía varios contenedores Docker corriendo servicios críticos en mi servidor doméstico. La idea de perder datos por un fallo de disco me mantenía despierto. Necesitaba una solución que:\nFuera incremental (no copiar todo cada vez) Se ejecutara automáticamente sin intervención Permitiera rotación de copias antiguas Guardara los datos en almacenamiento remoto Después de probar varias opciones, restic fue la respuesta. Es rápido, eficiente y se integra bien con cron.\n","title":"Automatizar backups incrementales de Docker con restic y cron","type":"posts"},{"content":"","date":"29 mai 2026","externalUrl":null,"permalink":"/tags/almacenamiento/","section":"Tags","summary":"","title":"Almacenamiento","type":"tags"},{"content":" El problema real # Tenía PostgreSQL corriendo en Docker y los backups manuales no escalan. Necesitaba algo automático, que verificara que los datos estuvieran bien y que limpiara lo viejo sin intervención.\nLa solución que implementé # Creé un script bash que:\nGenera backups incrementales en formato custom (comprimido) Verifica la integridad de cada copia Mantiene solo los últimos N backups Corre vía cron en horarios específicos Paso 1: Preparar el almacenamiento externo # Monté un disco externo en /mnt/backups-postgres/:\nsudo mkdir -p /mnt/backups-postgres sudo chown $USER:$USER /mnt/backups-postgres chmod 700 /mnt/backups-postgres Cada día un subdirectorio separado:\nmkdir -p /mnt/backups-postgres/$(date +%Y-%m-%d) Paso 2: El script de backup # Creé /home/rogelio/scripts/backup-postgres.sh:\n#!/bin/bash BACKUP_DIR=\u0026#34;/mnt/backups-postgres/$(date +%Y-%m-%d)\u0026#34; CONTAINER_NAME=\u0026#34;postgres-prod\u0026#34; DB_USER=\u0026#34;postgres\u0026#34; TIMESTAMP=$(date +%H-%M-%S) BACKUP_FILE=\u0026#34;$BACKUP_DIR/backup-$TIMESTAMP.custom\u0026#34; LOG_FILE=\u0026#34;$BACKUP_DIR/backup-$TIMESTAMP.log\u0026#34; # Crear directorio del día mkdir -p \u0026#34;$BACKUP_DIR\u0026#34; # Ejecutar dump incrementa (custom format) docker exec $CONTAINER_NAME pg_dump \\ -U $DB_USER \\ -Fc \\ --verbose \\ --compress=9 \\ postgres \u0026gt; \u0026#34;$BACKUP_FILE\u0026#34; 2\u0026gt; \u0026#34;$LOG_FILE\u0026#34; if [ $? -eq 0 ]; then echo \u0026#34;✓ Backup exitoso: $BACKUP_FILE\u0026#34; \u0026gt;\u0026gt; \u0026#34;$LOG_FILE\u0026#34; # Verificación de integridad docker exec $CONTAINER_NAME pg_restore \\ -U $DB_USER \\ --list \u0026#34;$BACKUP_FILE\u0026#34; \u0026gt; /dev/null 2\u0026gt;\u0026amp;1 if [ $? -eq 0 ]; then echo \u0026#34;✓ Verificación OK\u0026#34; \u0026gt;\u0026gt; \u0026#34;$LOG_FILE\u0026#34; echo \u0026#34;$(date \u0026#39;+%Y-%m-%d %H:%M:%S\u0026#39;) - Backup exitoso y verificado\u0026#34; \u0026gt;\u0026gt; \u0026#34;$BACKUP_DIR/resumen.log\u0026#34; else echo \u0026#34;✗ Verificación fallida\u0026#34; \u0026gt;\u0026gt; \u0026#34;$LOG_FILE\u0026#34; exit 1 fi else echo \u0026#34;✗ Error en backup\u0026#34; \u0026gt;\u0026gt; \u0026#34;$LOG_FILE\u0026#34; exit 1 fi Paso 3: Rotación de backups antiguos # Agregué al script una función de limpieza:\n# Mantener solo 7 días de backups RETENTION_DAYS=7 find /mnt/backups-postgres -maxdepth 1 -type d -mtime +$RETENTION_DAYS -exec rm -rf {} \\; 2\u0026gt;/dev/null echo \u0026#34;Rotación completada: directorios mayores a $RETENTION_DAYS días eliminados\u0026#34; Paso 4: Automatizar con cron # crontab -e Agregué estas líneas:\n# Backup cada 6 horas 0 0,6,12,18 * * * /home/rogelio/scripts/backup-postgres.sh # Verificación semanal de integridad 0 3 * * 0 /home/rogelio/scripts/verify-backups.sh Paso 5: Script de verificación adicional # Creé /home/rogelio/scripts/verify-backups.sh para validar todos los backups de una vez:\n#!/bin/bash BACKUP_DIR=\u0026#34;/mnt/backups-postgres\u0026#34; FAILED=0 for backup_file in $(find $BACKUP_DIR -name \u0026#34;*.custom\u0026#34; -type f); do docker exec postgres-prod pg_restore \\ -U postgres \\ --list \u0026#34;$backup_file\u0026#34; \u0026gt; /dev/null 2\u0026gt;\u0026amp;1 if [ $? -ne 0 ]; then echo \u0026#34;✗ Fallo verificación: $backup_file\u0026#34; FAILED=$((FAILED + 1)) fi done if [ $FAILED -eq 0 ]; then echo \u0026#34;✓ Todos los backups verificados correctamente\u0026#34; else echo \u0026#34;✗ $FAILED backups fallaron verificación\u0026#34; # Aquí podrías enviar alerta por correo exit 1 fi Monitoreo real # Los logs quedan en cada directorio. Reviso el resumen:\ntail -f /mnt/backups-postgres/$(date +%Y-%m-%d)/resumen.log Lo que aprendí # El formato custom es mejor que SQL plano: comprime más y permite restaurar tablas específicas Validar con pg_restore --list es rápido y confiable No confíes solo en el cron: agrega logs para verificar que corre El almacenamiento externo montado directamente es más rápido que NAS Llevo 6 meses con esto sin problemas. Los backups incrementales con rotación automática funcionan sin intervención y la verificación de integridad te da tranquilidad real.\n","date":"29 mai 2026","externalUrl":null,"permalink":"/posts/automatizar-backups-incrementales-de-postgresql-en-docker-con-verificacion-y-rotacion/","section":"Posts","summary":"El problema real # Tenía PostgreSQL corriendo en Docker y los backups manuales no escalan. Necesitaba algo automático, que verificara que los datos estuvieran bien y que limpiara lo viejo sin intervención.\nLa solución que implementé # Creé un script bash que:\nGenera backups incrementales en formato custom (comprimido) Verifica la integridad de cada copia Mantiene solo los últimos N backups Corre vía cron en horarios específicos Paso 1: Preparar el almacenamiento externo # Monté un disco externo en /mnt/backups-postgres/:\n","title":"Automatizar backups incrementales de PostgreSQL en Docker con verificación y rotación","type":"posts"},{"content":" El problema # Tenía contenedores Docker corriendo en el servidor doméstico con datos que no podía perder. Los backups manuales no escalan y dejar todo en el almacenamiento local es arriesgado. Necesitaba automatizar backups incrementales a un NAS remoto y poder verificar que todo estuviera bien desde el móvil sin exponer el servidor directamente a Internet.\nLa solución: restic + cron + Wireguard # Restic es perfecto para esto. Hace backups incrementales comprimidos, verifica integridad automáticamente y soporta múltiples backends remotos. Lo configuré así:\n1. Instalación y preparación # sudo apt install restic Cré un repositorio restic en el NAS remoto accesible vía Wireguard:\nrestic -r sftp:usuario@nas-interno:/backups/docker init Generé una contraseña fuerte y la guardé en /root/.restic/password:\nmkdir -p /root/.restic echo \u0026#34;tu_contraseña_super_segura\u0026#34; \u0026gt; /root/.restic/password chmod 600 /root/.restic/password 2. Script de backup de volúmenes # Creé /usr/local/bin/backup-docker-volumes.sh:\n#!/bin/bash export RESTIC_REPOSITORY=\u0026#34;sftp:usuario@nas-interno:/backups/docker\u0026#34; export RESTIC_PASSWORD_FILE=\u0026#34;/root/.restic/password\u0026#34; # Array con los volúmenes a respaldar VOLUMES=(\u0026#34;postgres_data\u0026#34; \u0026#34;mongodb_data\u0026#34; \u0026#34;app_config\u0026#34;) for volume in \u0026#34;${VOLUMES[@]}\u0026#34;; do echo \u0026#34;[$(date +\u0026#39;%Y-%m-%d %H:%M:%S\u0026#39;)] Respaldando volumen: $volume\u0026#34; # Obtener la ruta de montaje del volumen VOLUME_PATH=$(docker volume inspect \u0026#34;$volume\u0026#34; \\ --format=\u0026#39;{{.Mountpoint}}\u0026#39;) # Realizar backup con restic restic backup \u0026#34;$VOLUME_PATH\u0026#34; \\ --tag \u0026#34;docker_$volume\u0026#34; \\ --exclude=\u0026#34;/lost+found\u0026#34; \\ --exclude=\u0026#34;*.tmp\u0026#34; if [ $? -eq 0 ]; then echo \u0026#34;✓ Backup de $volume completado\u0026#34; else echo \u0026#34;✗ Error en backup de $volume\u0026#34; # Aquí puedes agregar notificación por email fi done # Limpiar snapshots antiguos (mantener 7 días) restic forget --keep-daily 7 --prune Permisos:\nchmod +x /usr/local/bin/backup-docker-volumes.sh 3. Automatización con cron # En el crontab del root (crontab -e):\n# Backup diario a las 2:00 AM 0 2 * * * /usr/local/bin/backup-docker-volumes.sh \u0026gt;\u0026gt; /var/log/docker-backups.log 2\u0026gt;\u0026amp;1 4. Verificación desde el móvil vía Wireguard # Instalé la app de Wireguard en el móvil y configuré acceso al servidor doméstico. Una vez conectado a la VPN:\nCreé un script de verificación en /usr/local/bin/check-backups.sh:\n#!/bin/bash export RESTIC_REPOSITORY=\u0026#34;sftp:usuario@nas-interno:/backups/docker\u0026#34; export RESTIC_PASSWORD_FILE=\u0026#34;/root/.restic/password\u0026#34; echo \u0026#34;=== Estado de Backups ===\u0026#34; echo \u0026#34;Snapshots disponibles:\u0026#34; restic snapshots --json | jq -r \u0026#39;.[] | \u0026#34;\\(.time | split(\u0026#34;T\u0026#34;)[0]) - \\(.paths[0] | split(\u0026#34;/\u0026#34;)[-1]) (\\(.id[0:8]))\u0026#34;\u0026#39; echo \u0026#34;\u0026#34; echo \u0026#34;Verificando integridad...\u0026#34; restic check --with-cache echo \u0026#34;\u0026#34; echo \u0026#34;Estadísticas:\u0026#34; restic stats Accesible vía SSH desde cualquier cliente Wireguard:\nssh usuario@servidor-local \u0026#39;/usr/local/bin/check-backups.sh\u0026#39; 5. Monitoreo básico # Agregué un pequeño endpoint HTTP para monitoreo remoto. Script Python simple en /opt/backup-monitor.py:\n#!/usr/bin/env python3 from flask import Flask import subprocess import os app = Flask(__name__) @app.route(\u0026#39;/status\u0026#39;) def status(): try: result = subprocess.run( [\u0026#39;restic\u0026#39;, \u0026#39;snapshots\u0026#39;, \u0026#39;--json\u0026#39;], env={**os.environ, \u0026#39;RESTIC_REPOSITORY\u0026#39;: \u0026#39;sftp:...\u0026#39;, \u0026#39;RESTIC_PASSWORD_FILE\u0026#39;: \u0026#39;/root/.restic/password\u0026#39;}, capture_output=True ) return {\u0026#39;status\u0026#39;: \u0026#39;ok\u0026#39;, \u0026#39;backups\u0026#39;: len(result.stdout.decode().split(\u0026#39;\\n\u0026#39;))} except: return {\u0026#39;status\u0026#39;: \u0026#39;error\u0026#39;}, 500 if __name__ == \u0026#39;__main__\u0026#39;: app.run(host=\u0026#39;127.0.0.1\u0026#39;, port=5000) Lo ejecuto como servicio systemd accesible solo vía Wireguard.\nResultados en producción # Después de 3 meses:\nBackups consistentes: Sin fallos. Los logs están limpios. Espacio optimizado: La deduplicación de restic mantiene el almacenamiento en ~200GB para 6 meses de datos. Verificación desde móvil: Tardo 15 segundos en confirmar que todo está bien, desde cualquier lugar. Tranquilidad: Sé exactamente qué respaldar y no dejo nada a la improvisación. La combinación restic + Wireguard es muy potente. Sin exponer el servidor, tengo backups automatizados y verificables desde cualquier dispositivo. Vale la pena configurarlo bien desde el principio.\n","date":"27 mai 2026","externalUrl":null,"permalink":"/posts/automatizar-backups-incrementales-de-volumenes-docker-a-almacenamiento-remoto-con-restic-y-verificar-integridad-desde-el-movil-via-wireguard/","section":"Posts","summary":"El problema # Tenía contenedores Docker corriendo en el servidor doméstico con datos que no podía perder. Los backups manuales no escalan y dejar todo en el almacenamiento local es arriesgado. Necesitaba automatizar backups incrementales a un NAS remoto y poder verificar que todo estuviera bien desde el móvil sin exponer el servidor directamente a Internet.\nLa solución: restic + cron + Wireguard # Restic es perfecto para esto. Hace backups incrementales comprimidos, verifica integridad automáticamente y soporta múltiples backends remotos. Lo configuré así:\n","title":"Automatizar backups incrementales de volúmenes Docker a almacenamiento remoto con restic y verificar integridad desde el móvil vía Wireguard","type":"posts"},{"content":" El problema # Tengo un PostgreSQL corriendo en Docker en mi servidor doméstico. Los datos son importantes. Los backups manuales son un desastre garantizado. Necesitaba algo automatizado, incremental y verificable sin exponer el servidor a internet.\nLa solución # Combiné cron, Docker, pg_dump y Wireguard para crear un sistema de backups que se ejecuta solo, verifica su integridad y me avisa si algo falla.\nPaso 1: Script de backup incremental # Primero, creé un script que genera backups con marca de tiempo y usa hard links para no duplicar datos innecesarios:\n#!/bin/bash BACKUP_DIR=\u0026#34;/mnt/backups/postgresql\u0026#34; DB_CONTAINER=\u0026#34;postgres-container\u0026#34; DB_NAME=\u0026#34;tu_base_datos\u0026#34; FULL_BACKUP_DAY=0 # Domingo RETENTION_DAYS=30 mkdir -p \u0026#34;$BACKUP_DIR\u0026#34; CURRENT_DAY=$(date +%u) BACKUP_DATE=$(date +%Y%m%d_%H%M%S) if [ \u0026#34;$CURRENT_DAY\u0026#34; -eq \u0026#34;$FULL_BACKUP_DAY\u0026#34; ]; then BACKUP_FILE=\u0026#34;$BACKUP_DIR/full_backup_$BACKUP_DATE.sql.gz\u0026#34; docker exec $DB_CONTAINER pg_dump -U postgres $DB_NAME | gzip \u0026gt; \u0026#34;$BACKUP_FILE\u0026#34; echo \u0026#34;Full backup creado: $BACKUP_FILE\u0026#34; else LAST_FULL=$(ls -t \u0026#34;$BACKUP_DIR\u0026#34;/full_backup_*.sql.gz 2\u0026gt;/dev/null | head -1) BACKUP_FILE=\u0026#34;$BACKUP_DIR/incr_backup_$BACKUP_DATE.sql.gz\u0026#34; docker exec $DB_CONTAINER pg_dump -U postgres $DB_NAME | gzip \u0026gt; \u0026#34;$BACKUP_FILE\u0026#34; echo \u0026#34;Backup incremental creado: $BACKUP_FILE\u0026#34; fi # Calcular hash para verificación sha256sum \u0026#34;$BACKUP_FILE\u0026#34; \u0026gt; \u0026#34;$BACKUP_FILE.sha256\u0026#34; # Limpiar backups antiguos find \u0026#34;$BACKUP_DIR\u0026#34; -name \u0026#34;*.sql.gz\u0026#34; -mtime +$RETENTION_DAYS -delete echo \u0026#34;Backup completado exitosamente\u0026#34; Guardé esto en /opt/backup-scripts/pg_backup.sh con permisos de ejecución:\nchmod +x /opt/backup-scripts/pg_backup.sh Paso 2: Configurar cron # Agregué la tarea a crontab para ejecutarse diariamente a las 2 AM:\ncrontab -e 0 2 * * * /opt/backup-scripts/pg_backup.sh \u0026gt;\u0026gt; /var/log/pg_backup.log 2\u0026gt;\u0026amp;1 Paso 3: Script de verificación con Wireguard # Ahora necesitaba verificar la integridad de los backups desde otra máquina a través de Wireguard. Creé un script de verificación:\n#!/bin/bash BACKUP_DIR=\u0026#34;/mnt/backups/postgresql\u0026#34; LOG_FILE=\u0026#34;/var/log/backup_verify.log\u0026#34; { echo \u0026#34;=== Verificación de backups - $(date) ===\u0026#34; for backup in \u0026#34;$BACKUP_DIR\u0026#34;/*.sql.gz; do if [ -f \u0026#34;$backup.sha256\u0026#34; ]; then if sha256sum -c \u0026#34;$backup.sha256\u0026#34; \u0026gt; /dev/null 2\u0026gt;\u0026amp;1; then echo \u0026#34;[OK] $backup\u0026#34; else echo \u0026#34;[ERROR] Checksum fallido: $backup\u0026#34; # Aquí podrías enviar una alerta fi fi done } \u0026gt;\u0026gt; \u0026#34;$LOG_FILE\u0026#34; Lo dejé en /opt/backup-scripts/verify_backups.sh y lo ejecuto semanalmente:\n0 3 * * 0 /opt/backup-scripts/verify_backups.sh Paso 4: Acceso seguro vía Wireguard # En mi servidor doméstico, solo la VPN de Wireguard tiene acceso a la ruta de backups. En el cliente Wireguard:\nMe conecto a la VPN Puedo acceder al servidor por su IP de Wireguard Ejecuto la verificación de forma segura sin exponer nada a internet ssh usuario@10.0.0.2 \u0026#34;sudo /opt/backup-scripts/verify_backups.sh\u0026#34; Monitoreo # Configuré alertas básicas editando el script de backup para notificar errores:\nif [ $? -ne 0 ]; then echo \u0026#34;Backup fallido\u0026#34; | mail -s \u0026#34;Error en backup PostgreSQL\u0026#34; mi@email.com fi Lo que aprendí # Los backups automáticos sin verificación son apenas mejores que nada Hard links ahorran espacio pero complican las cosas. Acabé usando compresión simple Wireguard es extremadamente util para acceso seguro sin VPN tradicionales Ejecutar backups a las 2 AM evita picos de carga He estado usando esta setup 6 meses. Funciona sin intervención. Los backups se crean, se verifican y se limpian solos. Eso es exactamente lo que quería.\n","date":"25 mai 2026","externalUrl":null,"permalink":"/posts/automatizar-backups-incrementales-de-postgresql-en-docker-con-cron-y-verificacion-en-wireguard/","section":"Posts","summary":"El problema # Tengo un PostgreSQL corriendo en Docker en mi servidor doméstico. Los datos son importantes. Los backups manuales son un desastre garantizado. Necesitaba algo automatizado, incremental y verificable sin exponer el servidor a internet.\nLa solución # Combiné cron, Docker, pg_dump y Wireguard para crear un sistema de backups que se ejecuta solo, verifica su integridad y me avisa si algo falla.\n","title":"Automatizar backups incrementales de PostgreSQL en Docker con cron y verificación en Wireguard","type":"posts"},{"content":"Un contenedor en estado running no significa que el servicio dentro funcione correctamente. He tenido casos en los que Nginx arrancaba, Docker lo veía verde, pero la aplicación devolvía 502 en cada petición. Sin health checks, eso pasa desapercibido hasta que lo detecta un usuario. Con health checks, Docker lo detecta en segundos y puede reiniciar el contenedor automáticamente.\nEl problema: running no es healthy # Por defecto, Docker solo sabe si el proceso principal del contenedor sigue vivo. Si tu aplicación arranca, entra en un bucle de error interno y sigue corriendo, Docker no tiene forma de saberlo.\n$ docker ps CONTAINER ID IMAGE STATUS PORTS a3f9c2b1d4e5 mi-app:1.0 Up 3 minutes 0.0.0.0:8080-\u0026gt;8080/tcp Up 3 minutes solo dice que el proceso no ha muerto. No dice nada sobre si responde peticiones HTTP, si la base de datos está accesible desde dentro del contenedor, o si la aplicación está en medio de un deadlock.\nHEALTHCHECK en el Dockerfile # La forma más portable de definir un health check es directamente en el Dockerfile. Así el check viaja con la imagen y no depende de cómo se despliegue:\nFROM python:3.12-slim WORKDIR /app COPY . . RUN pip install -r requirements.txt EXPOSE 8080 HEALTHCHECK --interval=30s --timeout=10s --start-period=20s --retries=3 \\ CMD curl -f http://localhost:8080/health || exit 1 CMD [\u0026#34;python\u0026#34;, \u0026#34;-m\u0026#34;, \u0026#34;uvicorn\u0026#34;, \u0026#34;main:app\u0026#34;, \u0026#34;--host\u0026#34;, \u0026#34;0.0.0.0\u0026#34;, \u0026#34;--port\u0026#34;, \u0026#34;8080\u0026#34;] Los parámetros que importan:\nParámetro Valor por defecto Qué hace --interval 30s Cada cuánto ejecuta el check --timeout 30s Tiempo máximo para que el check responda --start-period 0s Tiempo de gracia al arrancar (no cuenta fallos) --retries 3 Fallos consecutivos antes de marcar como unhealthy El --start-period es importante: si tu aplicación tarda en arrancar (carga de modelos, migraciones de base de datos, precalentamiento de caché), necesitas darle ese margen o Docker la marcará como unhealthy antes de que esté lista.\nEl endpoint /health # Para que el health check sea útil, tu aplicación debería exponer un endpoint específico que compruebe más que \u0026ldquo;estoy vivo\u0026rdquo;. Un /health mínimo en FastAPI:\nfrom fastapi import FastAPI from sqlalchemy import text app = FastAPI() @app.get(\u0026#34;/health\u0026#34;) async def health_check(): # Comprueba que la base de datos responde try: db.execute(text(\u0026#34;SELECT 1\u0026#34;)) except Exception: return {\u0026#34;status\u0026#34;: \u0026#34;unhealthy\u0026#34;, \u0026#34;detail\u0026#34;: \u0026#34;db unreachable\u0026#34;}, 503 return {\u0026#34;status\u0026#34;: \u0026#34;ok\u0026#34;} Si el endpoint devuelve cualquier código que no sea 2xx, curl -f falla y el check falla.\nHEALTHCHECK en docker-compose.yml # Si no controlas el Dockerfile (imagen de terceros) o quieres sobrescribir el health check definido en la imagen, puedes hacerlo directamente en docker-compose.yml:\nservices: servidor-web: image: nginx:alpine ports: - \u0026#34;80:80\u0026#34; healthcheck: test: [\u0026#34;CMD\u0026#34;, \u0026#34;curl\u0026#34;, \u0026#34;-f\u0026#34;, \u0026#34;http://localhost/nginx_status\u0026#34;] interval: 30s timeout: 5s start_period: 10s retries: 3 base-de-datos: image: postgres:16 environment: POSTGRES_DB: mi_base_datos POSTGRES_USER: usuario POSTGRES_PASSWORD: TU_PASSWORD_AQUI healthcheck: test: [\u0026#34;CMD-SHELL\u0026#34;, \u0026#34;pg_isready -U usuario -d mi_base_datos\u0026#34;] interval: 10s timeout: 5s start_period: 30s retries: 5 Nota la diferencia entre CMD y CMD-SHELL:\nCMD: ejecuta el comando directamente, sin shell. Más seguro y predecible. CMD-SHELL: ejecuta el comando a través de /bin/sh -c. Necesario cuando usas tuberías, variables o lógica shell. depends_on con condición de salud # Aquí está el verdadero poder de los health checks. Si tu aplicación necesita la base de datos para arrancar, puedes indicarle a Docker Compose que espere a que la base de datos esté healthy antes de iniciar la aplicación:\nservices: base-de-datos: image: postgres:16 environment: POSTGRES_DB: mi_app_db POSTGRES_USER: usuario POSTGRES_PASSWORD: TU_PASSWORD_AQUI healthcheck: test: [\u0026#34;CMD-SHELL\u0026#34;, \u0026#34;pg_isready -U usuario -d mi_app_db\u0026#34;] interval: 10s timeout: 5s start_period: 30s retries: 5 mi-aplicacion: image: mi-app:latest ports: - \u0026#34;8080:8080\u0026#34; depends_on: base-de-datos: condition: service_healthy environment: DATABASE_URL: postgresql://usuario:TU_PASSWORD_AQUI@base-de-datos:5432/mi_app_db Con condition: service_healthy, Docker Compose no arrancará mi-aplicacion hasta que base-de-datos devuelva un health check exitoso. Sin esto, si la base de datos tarda 20 segundos en estar lista, la aplicación puede fallar al arrancar porque intenta conectarse antes de tiempo.\nLas tres condiciones disponibles son:\nservice_started → el contenedor simplemente está en marcha (comportamiento por defecto) service_healthy → el health check pasa service_completed_successfully → el contenedor ha terminado con código de salida 0 (para tareas de inicialización) Ver el estado de salud # Una vez que los contenedores tienen health check configurado, docker ps muestra el estado:\n$ docker ps CONTAINER ID IMAGE STATUS PORTS a3f9c2b1d4e5 mi-app:1.0 Up 5 minutes (healthy) 0.0.0.0:8080-\u0026gt;8080/tcp b1c4d5e6f7a8 postgres:16 Up 5 minutes (healthy) 5432/tcp c9d0e1f2a3b4 nginx:alpine Up 2 minutes (unhealthy) 0.0.0.0:80-\u0026gt;80/tcp Para ver el historial de los últimos checks de un contenedor:\ndocker inspect --format=\u0026#39;{{json .State.Health}}\u0026#39; mi-aplicacion | python3 -m json.tool Esto muestra los últimos 5 resultados con timestamp, código de salida y salida del comando. Muy útil para diagnosticar por qué un contenedor está marcado como unhealthy.\nReinicio automático con restart policies # Los health checks cobran su máximo valor combinados con la política de reinicio:\nservices: mi-aplicacion: image: mi-app:latest restart: unless-stopped healthcheck: test: [\u0026#34;CMD\u0026#34;, \u0026#34;curl\u0026#34;, \u0026#34;-f\u0026#34;, \u0026#34;http://localhost:8080/health\u0026#34;] interval: 30s timeout: 10s start_period: 20s retries: 3 Con restart: unless-stopped, Docker reiniciará el contenedor si el proceso muere. Pero no lo reinicia automáticamente si el health check falla. Para eso necesitas una herramienta adicional como Autoheal:\nservices: autoheal: image: willfarrell/autoheal:latest restart: unless-stopped volumes: - /var/run/docker.sock:/var/run/docker.sock environment: AUTOHEAL_CONTAINER_LABEL: autoheal AUTOHEAL_INTERVAL: 10 AUTOHEAL_START_PERIOD: 0 mi-aplicacion: image: mi-app:latest restart: unless-stopped labels: - \u0026#34;autoheal=true\u0026#34; healthcheck: test: [\u0026#34;CMD\u0026#34;, \u0026#34;curl\u0026#34;, \u0026#34;-f\u0026#34;, \u0026#34;http://localhost:8080/health\u0026#34;] interval: 30s timeout: 10s start_period: 20s retries: 3 Autoheal monitoriza los contenedores marcados con la etiqueta autoheal=true y los reinicia cuando pasan a estado unhealthy.\nHealth checks para servicios sin HTTP # No todo es HTTP. Algunos ejemplos habituales:\nRedis:\nhealthcheck: test: [\u0026#34;CMD\u0026#34;, \u0026#34;redis-cli\u0026#34;, \u0026#34;ping\u0026#34;] interval: 10s timeout: 3s retries: 3 MySQL/MariaDB:\nhealthcheck: test: [\u0026#34;CMD\u0026#34;, \u0026#34;mysqladmin\u0026#34;, \u0026#34;ping\u0026#34;, \u0026#34;-h\u0026#34;, \u0026#34;localhost\u0026#34;, \u0026#34;-u\u0026#34;, \u0026#34;root\u0026#34;, \u0026#34;--password=TU_PASSWORD_AQUI\u0026#34;] interval: 10s timeout: 5s start_period: 30s retries: 5 Verificación de puerto con netcat (cuando no hay cliente disponible en la imagen):\nhealthcheck: test: [\u0026#34;CMD-SHELL\u0026#34;, \u0026#34;nc -z localhost 8080 || exit 1\u0026#34;] interval: 15s timeout: 5s retries: 3 Script personalizado (para lógica compleja):\nhealthcheck: test: [\u0026#34;CMD\u0026#34;, \u0026#34;/usr/local/bin/check-health.sh\u0026#34;] interval: 30s timeout: 15s retries: 3 El script puede hacer múltiples comprobaciones: conectividad de red, disponibilidad de ficheros, espacio en disco, o cualquier condición que defina \u0026ldquo;sano\u0026rdquo; para ese servicio específico.\nDeshabilitar el health check de una imagen # Si una imagen base define un health check que no quieres, puedes desactivarlo:\nhealthcheck: disable: true Útil cuando heredas una imagen con un health check que no aplica a tu caso de uso o que genera falsos positivos.\nQué he aprendido en producción # start_period demasiado corto: el error más común. Una base de datos con muchos datos puede tardar más de lo esperado en arrancar. Empieza con un valor generoso y ajusta observando los logs. Health checks que consumen demasiado: un curl a un endpoint ligero cuesta muy poco. Un health check que lanza una query pesada cada 10 segundos puede añadir carga innecesaria. Mantén los checks simples y rápidos. No confundir readiness con liveness: en Kubernetes existe la distinción entre readiness (¿listo para recibir tráfico?) y liveness (¿sigue vivo?). En Docker puro, el health check cubre ambos, así que diseña el endpoint /health pensando en ambas preguntas. Logs del health check: cuando un contenedor está unhealthy y no sabes por qué, docker inspect con el filtro de Health te da los últimos resultados con la salida exacta del comando. Es la primera herramienta que busco. Los health checks son una de esas cosas que tardas diez minutos en configurar y te ahorran horas de diagnóstico. Si tienes servicios en producción sin ellos, este fin de semana es buen momento para añadirlos.\n","date":"25 mai 2026","externalUrl":null,"permalink":"/posts/docker-health-checks-contenedores-resilientes/","section":"Posts","summary":"Un contenedor en estado running no significa que el servicio dentro funcione correctamente. He tenido casos en los que Nginx arrancaba, Docker lo veía verde, pero la aplicación devolvía 502 en cada petición. Sin health checks, eso pasa desapercibido hasta que lo detecta un usuario. Con health checks, Docker lo detecta en segundos y puede reiniciar el contenedor automáticamente.\nEl problema: running no es healthy # Por defecto, Docker solo sabe si el proceso principal del contenedor sigue vivo. Si tu aplicación arranca, entra en un bucle de error interno y sigue corriendo, Docker no tiene forma de saberlo.\n","title":"Docker health checks: cómo detectar contenedores rotos antes de que te afecten","type":"posts"},{"content":"","date":"24 mai 2026","externalUrl":null,"permalink":"/tags/action1/","section":"Tags","summary":"","title":"Action1","type":"tags"},{"content":"Gestionar varios equipos Windows desde un único panel, sin instalar agentes pesados ni pagar licencias de RMM corporativo, es exactamente lo que resuelve Action1. Es gratuito para hasta 100 endpoints, corre en la nube de su proveedor —sin infraestructura propia que mantener— y el agente en el endpoint pesa menos de 5 MB.\nEste artículo no cubre la instalación: eso se hace en diez minutos siguiendo su asistente. Me centro en lo que realmente importa cuando ya lo tienes corriendo: cómo organizar y controlar los endpoints, cómo automatizar tareas repetitivas con scripts y cómo explotar el inventario para saber exactamente qué hay en cada máquina.\nGestión de endpoints # Una vez instalado el agente en cada equipo, todos aparecen en el panel bajo Endpoints. El estado (online/offline), el sistema operativo, la versión de Windows y el usuario activo se actualizan en tiempo real.\nLo más útil del panel de gestión:\nGrupos de endpoints. Puedes organizar los equipos por ubicación, cliente o función (servidores, portátiles, TPVs). Las políticas y los scripts programados se aplican por grupo, lo que es la base de todo lo demás.\nAcceso remoto integrado. Sin VPN ni herramientas externas. Desde el panel abres una sesión remota directamente en el navegador, con la latencia esperada según la conexión del endpoint.\nAlertas de salud. Disco lleno, servicio caído, sistema sin actualizaciones críticas. Las alertas llegan por email y se registran en el panel con historial.\nUsuarios activos en tiempo real. Útil para saber si un equipo está en uso antes de lanzar un script que interrumpa la sesión del usuario.\nLa lógica de grupos es la que da potencia al resto de funciones: cualquier automatización o política que configures se puede apuntar a un grupo entero en lugar de endpoint por endpoint.\nAutomatización de scripts # Esta es la parte que más tiempo ahorra en el día a día. Action1 tiene una biblioteca de scripts predefinidos (PowerShell y batch) para las tareas más comunes, pero lo interesante es crear los tuyos y programarlos sobre grupos de endpoints.\nEl flujo de trabajo # Creas o importas un script en la biblioteca de Action1 (.ps1 o .cmd) Lo asignas a un grupo de endpoints o a máquinas concretas Eliges si se ejecuta inmediatamente, a una hora fija o de forma recurrente (diario, semanal\u0026hellip;) Los resultados —salida estándar y código de error— quedan registrados en el historial por endpoint Parámetros dinámicos # Cada script puede declarar variables que se rellenan desde el panel antes de lanzarlo. Así reutilizas el mismo .ps1 para distintos grupos sin duplicar código: por ejemplo, el mismo script de limpieza con una ruta de destino diferente por cliente o sede.\nPermisos de ejecución # El agente de Action1 corre como servicio de sistema (SYSTEM), así que los scripts tienen privilegios elevados por defecto sin necesitar UAC ni credenciales adicionales. En entornos con ExecutionPolicy: Restricted, Action1 invoca PowerShell con -ExecutionPolicy Bypass internamente, de forma que no necesitas tocar las GPOs del dominio para que funcione.\nEjecución offline-safe # Si el endpoint está apagado o sin conexión cuando llega la hora programada, el script se encola y se ejecuta en cuanto el agente recupera contacto. El historial registra tanto la hora de programación como la de ejecución real, para que quede trazabilidad completa.\nEjemplos de automatizaciones reales # Limpiar carpetas temporales y papelera en todos los equipos cada domingo a las 2:00 Forzar gpupdate /force en endpoints de dominio tras cambiar una GPO Verificar que un servicio crítico está corriendo y levantarlo si no lo está Desplegar una actualización de software en silencio, sin ventanas ni interrupciones al usuario Recopilar logs de una aplicación y subirlos a una ruta UNC compartida Inventario de hardware y software # Sin ejecutar nada manualmente, Action1 construye automáticamente el inventario completo de cada endpoint en cuanto el agente se conecta. Se actualiza de forma periódica y siempre refleja el estado real de la máquina.\nQué recoge por endpoint # Categoría Datos Hardware CPU (modelo, núcleos, frecuencia), RAM por ranura, discos (modelo, capacidad, espacio libre, tipo SSD/HDD), GPU, placa base, número de serie Red Adaptadores, MACs, IPs, nombre de dominio o workgroup Sistema Versión de Windows, build, fecha de instalación, último arranque Software Nombre, versión, editor y fecha de instalación de cada aplicación Parches Actualizaciones instaladas y pendientes con KB y fecha de publicación Para qué sirve en la práctica # Lo más valioso no es tener los datos sino poder filtrarlos. Puedes hacer consultas como \u0026ldquo;endpoints con menos de 20 GB libres en disco\u0026rdquo; o \u0026ldquo;equipos que aún tienen instalada la versión X de una aplicación\u0026rdquo; y actuar sobre ese grupo directamente desde el mismo panel: lanzar un script de limpieza, programar una desinstalación o exportar el listado.\nEl inventario de software también sirve como auditoría de licencias rápida: ves exactamente qué está instalado en cada máquina sin conectarte de forma remota ni preguntar al usuario.\nEn resumen # Action1 resuelve bien el triángulo habitual de los RMM: visibilidad, control y automatización, sin coste hasta los 100 endpoints. No reemplaza un SIEM ni una solución EDR, pero para el escenario típico de un administrador que gestiona un parque Windows heterogéneo —en una empresa, para varios clientes, o en un homelab con máquinas virtuales— cubre el 80% de las necesidades del día a día sin complejidad de infraestructura.\nLo que más uso: la combinación de grupos + scripts programados. Una vez definidos los grupos por función, las automatizaciones se despliegan en segundos sobre todos los equipos que correspondan, con historial de ejecución y alertas si algo falla.\n","date":"24 mai 2026","externalUrl":null,"permalink":"/posts/action1-rmm-gestion-endpoints-scripts-inventario/","section":"Posts","summary":"Gestionar varios equipos Windows desde un único panel, sin instalar agentes pesados ni pagar licencias de RMM corporativo, es exactamente lo que resuelve Action1. Es gratuito para hasta 100 endpoints, corre en la nube de su proveedor —sin infraestructura propia que mantener— y el agente en el endpoint pesa menos de 5 MB.\nEste artículo no cubre la instalación: eso se hace en diez minutos siguiendo su asistente. Me centro en lo que realmente importa cuando ya lo tienes corriendo: cómo organizar y controlar los endpoints, cómo automatizar tareas repetitivas con scripts y cómo explotar el inventario para saber exactamente qué hay en cada máquina.\n","title":"Action1 RMM: gestiona endpoints remotos, automatiza scripts e inventaría hardware sin coste","type":"posts"},{"content":"","date":"24 mai 2026","externalUrl":null,"permalink":"/categories/automatizaci%C3%B3n/","section":"Categories","summary":"","title":"Automatización","type":"categories"},{"content":"","date":"24 mai 2026","externalUrl":null,"permalink":"/tags/endpoints/","section":"Tags","summary":"","title":"Endpoints","type":"tags"},{"content":"","date":"24 mai 2026","externalUrl":null,"permalink":"/tags/inventario/","section":"Tags","summary":"","title":"Inventario","type":"tags"},{"content":"","date":"24 mai 2026","externalUrl":null,"permalink":"/tags/powershell/","section":"Tags","summary":"","title":"Powershell","type":"tags"},{"content":"","date":"24 mai 2026","externalUrl":null,"permalink":"/tags/rmm/","section":"Tags","summary":"","title":"Rmm","type":"tags"},{"content":"","date":"24 mai 2026","externalUrl":null,"permalink":"/tags/sysadmin/","section":"Tags","summary":"","title":"Sysadmin","type":"tags"},{"content":"","date":"24 mai 2026","externalUrl":null,"permalink":"/tags/windows/","section":"Tags","summary":"","title":"Windows","type":"tags"},{"content":"","date":"22 mai 2026","externalUrl":null,"permalink":"/fr/categories/","section":"Categories","summary":"","title":"Categories","type":"categories"},{"content":"","date":"22 mai 2026","externalUrl":null,"permalink":"/fr/tags/cumplimiento/","section":"Tags","summary":"","title":"Cumplimiento","type":"tags"},{"content":"","date":"22 mai 2026","externalUrl":null,"permalink":"/fr/categories/docker/","section":"Categories","summary":"","title":"Docker","type":"categories"},{"content":"","date":"22 mai 2026","externalUrl":null,"permalink":"/fr/tags/docker/","section":"Tags","summary":"","title":"Docker","type":"tags"},{"content":"","date":"22 mai 2026","externalUrl":null,"permalink":"/fr/tags/monitorizaci%C3%B3n/","section":"Tags","summary":"","title":"Monitorización","type":"tags"},{"content":"","date":"22 mai 2026","externalUrl":null,"permalink":"/fr/posts/","section":"Posts","summary":"","title":"Posts","type":"posts"},{"content":"","date":"22 mai 2026","externalUrl":null,"permalink":"/fr/categories/seguridad/","section":"Categories","summary":"","title":"Seguridad","type":"categories"},{"content":"","date":"22 mai 2026","externalUrl":null,"permalink":"/fr/tags/seguridad/","section":"Tags","summary":"","title":"Seguridad","type":"tags"},{"content":"","date":"22 mai 2026","externalUrl":null,"permalink":"/fr/","section":"Servicios Rogeliowar","summary":"","title":"Servicios Rogeliowar","type":"page"},{"content":"","date":"22 mai 2026","externalUrl":null,"permalink":"/fr/tags/siem/","section":"Tags","summary":"","title":"Siem","type":"tags"},{"content":"","date":"22 mai 2026","externalUrl":null,"permalink":"/fr/tags/","section":"Tags","summary":"","title":"Tags","type":"tags"},{"content":"","date":"22 mai 2026","externalUrl":null,"permalink":"/fr/tags/wazuh/","section":"Tags","summary":"","title":"Wazuh","type":"tags"},{"content":" Qu\u0026rsquo;est-ce que l\u0026rsquo;ENS et pourquoi c\u0026rsquo;est important # L\u0026rsquo;Schéma National de Sécurité (ENS) est le cadre normatif obligatoire en matière de cybersécurité pour les Administrations Publiques espagnoles et pour les entreprises privées qui fournissent des services à ces dernières. Il est réglementé par le Real Decreto 311/2022 et établit les principes, exigences et mesures de sécurité que doivent appliquer les systèmes d\u0026rsquo;information qui traitent des données ou des services publics.\nEn pratique, l\u0026rsquo;ENS classe les systèmes selon l\u0026rsquo;impact qu\u0026rsquo;aurait un incident de sécurité sur l\u0026rsquo;organisation et les citoyens. Cette classification détermine le niveau de mesures à mettre en œuvre.\nLes trois catégories de l\u0026rsquo;ENS # L\u0026rsquo;ENS définit trois niveaux de catégorisation :\nCatégorie Basique Pour les systèmes dont la compromission aurait un impact limité. S\u0026rsquo;applique généralement aux sites web informatifs, services internes à faible risque ou systèmes contenant des données non sensibles. Les mesures requises sont les minimales du cadre.\nCatégorie Moyenne Pour les systèmes dont la compromission causerait un préjudice considérable à l\u0026rsquo;organisation ou à des tiers. C\u0026rsquo;est la catégorie la plus courante dans les environnements de gestion interne : ERP, portails employés, systèmes RH, plateformes de gestion documentaire. Elle requiert des contrôles actifs de surveillance, gestion des incidents et contrôle d\u0026rsquo;accès.\nCatégorie Élevée Pour les systèmes critiques dont la compromission pourrait causer un dommage grave ou très grave. S\u0026rsquo;applique aux systèmes gérant des infrastructures critiques, données de santé, systèmes judiciaires ou de défense. Exige les mesures les plus strictes du schéma.\nLa catégorie moyenne en pratique # Un système classé en catégorie moyenne doit avoir, entre autres contrôles :\nSurveillance continue des événements de sécurité Détection et gestion active des incidents Contrôle d\u0026rsquo;accès privilégié et traçabilité des actions Intégrité des fichiers système Enregistrement et analyse centralisée des logs C\u0026rsquo;est précisément là que Wazuh intervient.\nWazuh comme plateforme de conformité # Wazuh est une plateforme SIEM (Security Information and Event Management) open source qui couvre de manière native la plupart des contrôles de surveillance exigés par la catégorie moyenne : analyse des logs en temps réel, détection des intrusions, surveillance de l\u0026rsquo;intégrité des fichiers, inventaire logiciel et réponse active aux incidents.\nDans cet article, je déploie un nœud unique avec Docker, je génère les certificats TLS nécessaires et j\u0026rsquo;ajoute des règles personnalisées orientées vers les contrôles de sécurité les plus courants dans les environnements de catégorie moyenne.\nArchitecture de la pile # La pile Wazuh single-node comporte trois composants :\nwazuh.manager — moteur d\u0026rsquo;analyse et de corrélation d\u0026rsquo;événements wazuh.indexer — stockage basé sur OpenSearch wazuh.dashboard — interface web (OpenSearch Dashboards) La communication entre les composants utilise TLS mutuel avec des certificats propres, que nous générons avant le premier démarrage.\nStructure du projet # wazuh/ ├── docker-compose.yml ├── generate-certs.yml ├── gen-certs.sh ├── deploy.sh ├── .env └── config/ ├── certs.yml ├── wazuh_manager/ │ ├── wazuh_manager.conf │ └── local_rules.xml ├── wazuh_indexer/ │ ├── wazuh.indexer.yml │ └── internal_users.yml └── wazuh_dashboard/ ├── opensearch_dashboards.yml └── wazuh.yml Variables d\u0026rsquo;environnement # Créez le fichier .env à partir de l\u0026rsquo;exemple :\ncp .env.example .env Contenu minimal :\nINDEXER_ADMIN_PASSWORD=TU_PASSWORD_SEGURO KIBANA_PASSWORD=TU_PASSWORD_KIBANA API_PASSWORD=TU_PASSWORD_API SMTP_HOST=mail.tudominio.com SMTP_FROM=alertas@tudominio.com SMTP_TO=admin@tudominio.com Utilisez des mots de passe d\u0026rsquo;au moins 12 caractères contenant des majuscules, des chiffres et des symboles. L\u0026rsquo;indexer les valide au démarrage.\nGénérer les certificats TLS # Wazuh nécessite des certificats TLS pour la communication interne entre le manager, l\u0026rsquo;indexer et le dashboard. La pile inclut un conteneur générateur :\ndocker compose -f generate-certs.yml run --rm generator Cela crée config/wazuh_indexer_ssl_certs/ avec :\nroot-ca.pem — CA racine auto-signée wazuh.manager.pem / wazuh.manager-key.pem wazuh.indexer.pem / wazuh.indexer-key.pem wazuh.dashboard.pem / wazuh.dashboard-key.pem docker-compose.yml # services: wazuh.manager: image: wazuh/wazuh-manager:4.9.2 hostname: wazuh.manager restart: always ulimits: memlock: soft: -1 hard: -1 nofile: soft: 655360 hard: 655360 ports: - \u0026#34;1514:1514/tcp\u0026#34; # agentes TCP - \u0026#34;1515:1515/tcp\u0026#34; # enrollment - \u0026#34;5140:5140/udp\u0026#34; # syslog externo environment: INDEXER_URL: https://wazuh.indexer:9200 INDEXER_USERNAME: admin INDEXER_PASSWORD: ${INDEXER_ADMIN_PASSWORD} FILEBEAT_SSL_VERIFICATION_MODE: full SSL_CERTIFICATE_AUTHORITIES: /etc/ssl/root-ca.pem SSL_CERTIFICATE: /etc/ssl/filebeat.pem SSL_KEY: /etc/ssl/filebeat.key API_USERNAME: wazuh-wui API_PASSWORD: ${API_PASSWORD} volumes: - wazuh_api_configuration:/var/ossec/api/configuration - wazuh_etc:/var/ossec/etc - wazuh_logs:/var/ossec/logs - ./config/wazuh_indexer_ssl_certs/root-ca.pem:/etc/ssl/root-ca.pem - ./config/wazuh_indexer_ssl_certs/wazuh.manager.pem:/etc/ssl/filebeat.pem - ./config/wazuh_indexer_ssl_certs/wazuh.manager-key.pem:/etc/ssl/filebeat.key - ./config/wazuh_manager/local_rules.xml:/var/ossec/etc/rules/local_rules.xml - ./config/wazuh_manager/wazuh_manager.conf:/wazuh-config-mount/etc/ossec.conf wazuh.indexer: image: wazuh/wazuh-indexer:4.9.2 hostname: wazuh.indexer restart: always ulimits: memlock: soft: -1 hard: -1 nofile: soft: 65536 hard: 65536 environment: OPENSEARCH_JAVA_OPTS: \u0026#34;-Xms1g -Xmx1g\u0026#34; volumes: - wazuh-indexer-data:/var/lib/wazuh-indexer - ./config/wazuh_indexer/wazuh.indexer.yml:/usr/share/wazuh-indexer/opensearch.yml - ./config/wazuh_indexer_ssl_certs/root-ca.pem:/usr/share/wazuh-indexer/certs/root-ca.pem - ./config/wazuh_indexer_ssl_certs/wazuh.indexer.pem:/usr/share/wazuh-indexer/certs/wazuh.indexer.pem - ./config/wazuh_indexer_ssl_certs/wazuh.indexer-key.pem:/usr/share/wazuh-indexer/certs/wazuh.indexer-key.pem wazuh.dashboard: image: wazuh/wazuh-dashboard:4.9.2 hostname: wazuh.dashboard restart: always ports: - \u0026#34;8443:5601\u0026#34; environment: INDEXER_USERNAME: admin INDEXER_PASSWORD: ${INDEXER_ADMIN_PASSWORD} WAZUH_API_URL: https://wazuh.manager DASHBOARD_USERNAME: kibanaserver DASHBOARD_PASSWORD: ${KIBANA_PASSWORD} API_USERNAME: wazuh-wui API_PASSWORD: ${API_PASSWORD} volumes: - ./config/wazuh_dashboard/opensearch_dashboards.yml:/usr/share/wazuh-dashboard/config/opensearch_dashboards.yml - ./config/wazuh_dashboard/wazuh.yml:/usr/share/wazuh-dashboard/data/wazuh/config/wazuh.yml - ./config/wazuh_indexer_ssl_certs/wazuh.dashboard.pem:/usr/share/wazuh-dashboard/certs/wazuh-dashboard.pem - ./config/wazuh_indexer_ssl_certs/wazuh.dashboard-key.pem:/usr/share/wazuh-dashboard/certs/wazuh-dashboard-key.pem - ./config/wazuh_indexer_ssl_certs/root-ca.pem:/usr/share/wazuh-dashboard/certs/root-ca.pem depends_on: - wazuh.indexer volumes: wazuh_api_configuration: wazuh_etc: wazuh_logs: wazuh_queue: wazuh_var_multigroups: wazuh_integrations: wazuh_active_response: wazuh_agentless: wazuh_wodles: filebeat_etc: filebeat_var: wazuh-indexer-data: Script de déploiement # #!/usr/bin/env bash set -euo pipefail echo \u0026#34;=== [1/4] Generando certificados SSL ===\u0026#34; docker compose -f generate-certs.yml run --rm generator echo \u0026#34;=== [2/4] Abriendo puertos en UFW ===\u0026#34; ufw allow 1514/tcp comment \u0026#34;Wazuh agentes TCP\u0026#34; ufw allow 1515/tcp comment \u0026#34;Wazuh enrollment\u0026#34; ufw allow 5140/udp comment \u0026#34;Wazuh syslog\u0026#34; echo \u0026#34;=== [3/4] Iniciando stack ===\u0026#34; docker compose up -d echo \u0026#34;=== [4/4] Esperando indexer (2-3 min) ===\u0026#34; for i in $(seq 1 30); do if docker compose exec wazuh.indexer \\ curl -ks https://localhost:9200/_cat/health 2\u0026gt;/dev/null | grep -qE \u0026#39;green|yellow\u0026#39;; then echo \u0026#34;Indexer OK\u0026#34; break fi echo \u0026#34;Esperando... ($i/30)\u0026#34; sleep 10 done docker compose ps echo \u0026#34;Dashboard disponible en https://TU-IP:8443\u0026#34; Configuration du manager # Le fichier wazuh_manager.conf définit le comportement global. Points clés :\n\u0026lt;ossec_config\u0026gt; \u0026lt;global\u0026gt; \u0026lt;jsonout_output\u0026gt;yes\u0026lt;/jsonout_output\u0026gt; \u0026lt;alerts_log\u0026gt;yes\u0026lt;/alerts_log\u0026gt; \u0026lt;email_notification\u0026gt;no\u0026lt;/email_notification\u0026gt; \u0026lt;smtp_server\u0026gt;mail.tudominio.com\u0026lt;/smtp_server\u0026gt; \u0026lt;email_from\u0026gt;alertas@tudominio.com\u0026lt;/email_from\u0026gt; \u0026lt;email_to\u0026gt;admin@tudominio.com\u0026lt;/email_to\u0026gt; \u0026lt;email_maxperhour\u0026gt;12\u0026lt;/email_maxperhour\u0026gt; \u0026lt;agents_disconnection_time\u0026gt;10m\u0026lt;/agents_disconnection_time\u0026gt; \u0026lt;white_list\u0026gt;127.0.0.1\u0026lt;/white_list\u0026gt; \u0026lt;white_list\u0026gt;192.168.0.0/16\u0026lt;/white_list\u0026gt; \u0026lt;/global\u0026gt; \u0026lt;alerts\u0026gt; \u0026lt;log_alert_level\u0026gt;3\u0026lt;/log_alert_level\u0026gt; \u0026lt;email_alert_level\u0026gt;12\u0026lt;/email_alert_level\u0026gt; \u0026lt;/alerts\u0026gt; \u0026lt;!-- Agentes via TCP --\u0026gt; \u0026lt;remote\u0026gt; \u0026lt;connection\u0026gt;secure\u0026lt;/connection\u0026gt; \u0026lt;port\u0026gt;1514\u0026lt;/port\u0026gt; \u0026lt;protocol\u0026gt;tcp\u0026lt;/protocol\u0026gt; \u0026lt;queue_size\u0026gt;131072\u0026lt;/queue_size\u0026gt; \u0026lt;/remote\u0026gt; \u0026lt;!-- Syslog UDP para firewalls y dispositivos de red --\u0026gt; \u0026lt;remote\u0026gt; \u0026lt;connection\u0026gt;syslog\u0026lt;/connection\u0026gt; \u0026lt;port\u0026gt;5140\u0026lt;/port\u0026gt; \u0026lt;protocol\u0026gt;udp\u0026lt;/protocol\u0026gt; \u0026lt;allowed-ips\u0026gt;0.0.0.0/0\u0026lt;/allowed-ips\u0026gt; \u0026lt;/remote\u0026gt; \u0026lt;syscheck\u0026gt; \u0026lt;disabled\u0026gt;no\u0026lt;/disabled\u0026gt; \u0026lt;frequency\u0026gt;43200\u0026lt;/frequency\u0026gt; \u0026lt;scan_on_start\u0026gt;yes\u0026lt;/scan_on_start\u0026gt; \u0026lt;alert_new_files\u0026gt;yes\u0026lt;/alert_new_files\u0026gt; \u0026lt;directories check_all=\u0026#34;yes\u0026#34; report_changes=\u0026#34;yes\u0026#34;\u0026gt;/etc,/usr/bin,/usr/sbin\u0026lt;/directories\u0026gt; \u0026lt;directories check_all=\u0026#34;yes\u0026#34;\u0026gt;/bin,/sbin,/boot\u0026lt;/directories\u0026gt; \u0026lt;/syscheck\u0026gt; \u0026lt;/ossec_config\u0026gt; Règles de sécurité personnalisées # Wazuh inclut des milliers de règles prédéfinies. Les vôtres vont dans local_rules.xml avec des IDs à partir de 100000. Ce bloc implémente les contrôles de surveillance les plus courants dans les environnements avec des exigences de conformité de catégorie moyenne : détection des attaques, contrôle d\u0026rsquo;accès privilégié, intégrité des fichiers et disponibilité des services.\n\u0026lt;group name=\u0026#34;cumplimiento_custom,\u0026#34;\u0026gt; \u0026lt;!-- DETECCIÓN DE ATAQUES — autenticación --\u0026gt; \u0026lt;!-- Fuerza bruta SSH: 8 intentos fallidos en 120 segundos --\u0026gt; \u0026lt;rule id=\u0026#34;100001\u0026#34; level=\u0026#34;10\u0026#34; frequency=\u0026#34;8\u0026#34; timeframe=\u0026#34;120\u0026#34;\u0026gt; \u0026lt;if_matched_sid\u0026gt;5760\u0026lt;/if_matched_sid\u0026gt; \u0026lt;description\u0026gt;Posible ataque de fuerza bruta SSH — $(attempts) intentos fallidos\u0026lt;/description\u0026gt; \u0026lt;group\u0026gt;authentication_failures,\u0026lt;/group\u0026gt; \u0026lt;/rule\u0026gt; \u0026lt;!-- Cuenta de usuario bloqueada --\u0026gt; \u0026lt;rule id=\u0026#34;100002\u0026#34; level=\u0026#34;10\u0026#34;\u0026gt; \u0026lt;if_sid\u0026gt;5503\u0026lt;/if_sid\u0026gt; \u0026lt;description\u0026gt;Cuenta bloqueada por múltiples intentos fallidos de autenticación\u0026lt;/description\u0026gt; \u0026lt;group\u0026gt;authentication_failures,\u0026lt;/group\u0026gt; \u0026lt;/rule\u0026gt; \u0026lt;!-- CONTROL DE ACCESO PRIVILEGIADO --\u0026gt; \u0026lt;!-- Escalada de privilegios con sudo --\u0026gt; \u0026lt;rule id=\u0026#34;100003\u0026#34; level=\u0026#34;9\u0026#34;\u0026gt; \u0026lt;if_sid\u0026gt;5402\u0026lt;/if_sid\u0026gt; \u0026lt;description\u0026gt;Escalada de privilegios detectada mediante sudo\u0026lt;/description\u0026gt; \u0026lt;group\u0026gt;priv_escalation,\u0026lt;/group\u0026gt; \u0026lt;/rule\u0026gt; \u0026lt;!-- Nuevo usuario creado en el sistema --\u0026gt; \u0026lt;rule id=\u0026#34;100010\u0026#34; level=\u0026#34;8\u0026#34;\u0026gt; \u0026lt;if_sid\u0026gt;5902\u0026lt;/if_sid\u0026gt; \u0026lt;description\u0026gt;Nuevo usuario creado en el sistema — revisión recomendada\u0026lt;/description\u0026gt; \u0026lt;group\u0026gt;account_changes,\u0026lt;/group\u0026gt; \u0026lt;/rule\u0026gt; \u0026lt;!-- INTEGRIDAD DE FICHEROS --\u0026gt; \u0026lt;!-- Modificación de fichero crítico del sistema --\u0026gt; \u0026lt;rule id=\u0026#34;100020\u0026#34; level=\u0026#34;10\u0026#34;\u0026gt; \u0026lt;if_sid\u0026gt;550\u0026lt;/if_sid\u0026gt; \u0026lt;description\u0026gt;Fichero crítico del sistema modificado — $(file)\u0026lt;/description\u0026gt; \u0026lt;group\u0026gt;syscheck,integrity_check_host,\u0026lt;/group\u0026gt; \u0026lt;/rule\u0026gt; \u0026lt;!-- DISPONIBILIDAD DE SERVICIOS --\u0026gt; \u0026lt;!-- Servicio detenido inesperadamente --\u0026gt; \u0026lt;rule id=\u0026#34;100030\u0026#34; level=\u0026#34;8\u0026#34;\u0026gt; \u0026lt;if_sid\u0026gt;2904\u0026lt;/if_sid\u0026gt; \u0026lt;description\u0026gt;Servicio detenido de forma inesperada — $(service)\u0026lt;/description\u0026gt; \u0026lt;group\u0026gt;service_control,\u0026lt;/group\u0026gt; \u0026lt;/rule\u0026gt; \u0026lt;/group\u0026gt; Ce que couvre chaque bloc # Règles Zone de contrôle Description 100001–100002 Détection des attaques Force brute et verrouillage de comptes 100003, 100010 Accès privilégié Sudo et création d\u0026rsquo;utilisateurs 100020 Intégrité du système Modifications de fichiers critiques 100030 Disponibilité Arrêt inattendu de services Les niveaux (8-10) déterminent quelles alertes génèrent une notification par email selon le seuil configuré dans email_alert_level.\nEnrôler un agent Linux # Depuis le serveur où installer l\u0026rsquo;agent :\n# Descargar e instalar (ajusta versión y arquitectura) wget https://packages.wazuh.com/4.x/apt/pool/main/w/wazuh-agent/wazuh-agent_4.9.2-1_amd64.deb dpkg -i wazuh-agent_4.9.2-1_amd64.deb # Apuntar al manager sed -i \u0026#39;s/MANAGER_IP/192.168.X.X/\u0026#39; /var/ossec/etc/ossec.conf # Registrar y arrancar /var/ossec/bin/agent-auth -m 192.168.X.X systemctl enable wazuh-agent \u0026amp;\u0026amp; systemctl start wazuh-agent Accès au dashboard # Une fois la pile en cours d\u0026rsquo;exécution (l\u0026rsquo;indexer prend 2-3 minutes pour initialiser) :\nhttps://TU-IP:8443 Usuario: admin Contraseña: (la definida en INDEXER_ADMIN_PASSWORD) Important : changez tous les mots de passe par défaut avant d\u0026rsquo;exposer le service sur le réseau.\nConclusion # Avec cette pile, vous disposez d\u0026rsquo;un SIEM complet sur un seul serveur capable de couvrir les contrôles de surveillance exigés par les cadres de conformité les plus courants. L\u0026rsquo;étape suivante naturelle consiste à ajouter plus d\u0026rsquo;agents, à configurer des alertes par email ou webhook pour les événements de haut niveau, et à consulter les tableaux de bord de conformité inclus par défaut dans Wazuh pour PCI-DSS, GDPR, HIPAA et ENS. Pour vérifier le niveau de conformité d\u0026rsquo;un système à l\u0026rsquo;ENS, le CCN-CERT publie les guides de sécurité (séries CCN-STIC) de référence.\n","date":"22 mai 2026","externalUrl":null,"permalink":"/fr/posts/wazuh-siem-docker-ssl-cumplimiento/","section":"Posts","summary":"Qu’est-ce que l’ENS et pourquoi c’est important # L’Schéma National de Sécurité (ENS) est le cadre normatif obligatoire en matière de cybersécurité pour les Administrations Publiques espagnoles et pour les entreprises privées qui fournissent des services à ces dernières. Il est réglementé par le Real Decreto 311/2022 et établit les principes, exigences et mesures de sécurité que doivent appliquer les systèmes d’information qui traitent des données ou des services publics.\n","title":"Wazuh SIEM avec Docker : déploiement complet avec SSL et règles de conformité","type":"posts"},{"content":"","date":"19 mai 2026","externalUrl":null,"permalink":"/fr/tags/bases-de-datos/","section":"Tags","summary":"","title":"Bases-De-Datos","type":"tags"},{"content":" Le vrai problème # Récemment, j\u0026rsquo;ai réalisé que j\u0026rsquo;avais pusheé un fichier .env contenant des identifiants d\u0026rsquo;API dans un référentiel privé. Bien qu\u0026rsquo;il soit privé, ce n\u0026rsquo;est pas une excuse. Un accès compromis, un référentiel devenant public, ou simplement un audit de sécurité aurait exposé mes tokens. J\u0026rsquo;ai appris que je ne peux pas faire confiance à la suppression de fichiers dans les commits ultérieurs—Git conserve tout l\u0026rsquo;historique.\nPourquoi git-filter-repo # Il y a des années, j\u0026rsquo;aurais utilisé git filter-branch, mais c\u0026rsquo;est lent et sujet aux erreurs. git-filter-repo est l\u0026rsquo;outil moderne recommandé par les mainteneurs de Git. C\u0026rsquo;est rapide, précis et dispose de meilleures options pour ce travail.\nInstallation # Sur mon serveur Debian :\napt-get install git-filter-repo Ou avec pip :\npip3 install git-filter-repo Étape 1 : Identifier les dégâts # D\u0026rsquo;abord, je dois savoir quels commits contiennent des identifiants. Je recherche des modèles suspects :\ngit log --all --oneline | head -20 git log -p --all | grep -i \u0026#34;token\\|password\\|api_key\u0026#34; | head -10 Je vérifie également quels fichiers sensibles se trouvent dans l\u0026rsquo;historique :\ngit log --all --full-history -- \u0026#34;.env\u0026#34; git log --all --full-history -- \u0026#34;config.yml\u0026#34; Dans mon cas, j\u0026rsquo;ai découvert que .env avait été commitié 3 fois et un fichier credentials.json 2 fois.\nÉtape 2 : Faire une sauvegarde # Je ne fais jamais cela sans sauvegarde :\ncd /path/to/my/repo git clone --mirror . backup-mirror.git Si quelque chose se passe mal, j\u0026rsquo;ai une copie complète du référentiel avec tout son historique.\nÉtape 3 : Nettoyer les fichiers spécifiques # J\u0026rsquo;exécute git-filter-repo pour supprimer les fichiers sensibles de l\u0026rsquo;historique complet :\ngit-filter-repo --invert-paths --path .env --path credentials.json Le paramètre --invert-paths signifie que cela conserve tout SAUF ce que je spécifie. C\u0026rsquo;est l\u0026rsquo;inverse de ce que cela semble être, mais cela fonctionne parfaitement.\nLe processus prend quelques secondes et réécrit tout l\u0026rsquo;historique. À la fin, je vois :\nProcessed 47 commits New history has 47 commits Étape 4 : Forcer le push (avec prudence) # Puisque j\u0026rsquo;ai réécrit l\u0026rsquo;historique, j\u0026rsquo;ai besoin de faire un push forcé. Sur un serveur personnel où je suis le seul dev, c\u0026rsquo;est sûr :\ngit push origin --force --all git push origin --force --tags Si c\u0026rsquo;est un référentiel partagé, je me coordonne avec l\u0026rsquo;équipe pour que tout le monde fasse git reset --hard origin/main après.\nÉtape 5 : Renouveler les identifiants compromis # Les identifiants qui étaient dans Git ne s\u0026rsquo;y trouvent plus, mais je dois supposer qu\u0026rsquo;ils ont été compromis. Je renouvelle tous les tokens :\nClés d\u0026rsquo;API : Je révoque les anciennes dans le panneau de l\u0026rsquo;API et en génère de nouvelles Mots de passe : Je change le mot de passe de tout service utilisant ces identifiants Tokens de BD : Je régénère les identifiants des bases de données Clés SSH : S\u0026rsquo;ils étaient exposés, je génère de nouvelles paires Je documente ces changements dans un fichier privé (pas dans Git) :\n2026-05-19 - Rotación de credenciales post-exposición - API key antigua: revocada, nueva generada - Token DB: regenerado - Contraseña servicio X: cambiada Étape 6 : Prévention future # J\u0026rsquo;ajoute des règles à .gitignore (qui est maintenant propre) :\n.env .env.local credentials.json secrets/ Je configure également un hook pre-commit pour détecter les modèles dangereux :\ngit config core.hooksPath .githooks Et je crée .githooks/pre-commit :\n#!/bin/bash if git diff --cached | grep -iE \u0026#34;(password|token|api_key|secret)\u0026#34; \u0026amp;\u0026amp; \\ ! git diff --cached | grep \u0026#34;.gitignore\u0026#34;; then echo \u0026#34;⚠️ Posible credencial detectada. Abortando.\u0026#34; exit 1 fi Conclusion # Le nettoyage prend 10 minutes. Le renouvellement des identifiants, un peu plus. Mais c\u0026rsquo;est du temps bien investi. Sur un serveur personnel, je n\u0026rsquo;ai aucune excuse pour être négligent avec les secrets. Maintenant j\u0026rsquo;utilise des variables d\u0026rsquo;environnement et des fichiers locaux qui ne vont jamais dans Git.\nLeçon apprise : Les secrets ne vont jamais dans le contrôle de version, même pas \u0026ldquo;privé\u0026rdquo;. Point final.\nÉquipement recommandé # YubiKey 5 NFC — Clé de sécurité physique pour SSH et GitLab — élimine le risque de tokens volés Disque dur externe 2 To — Sauvegarde des référentiels avant les opérations destructrices comme git-filter-repo Liens d\u0026rsquo;affiliation. Pas de coût supplémentaire pour toi.\n","date":"19 mai 2026","externalUrl":null,"permalink":"/fr/posts/como-limpiar-credenciales-expuestas-en-git-con-git-filter-repo-y-rotar-tokens/","section":"Posts","summary":"Le vrai problème # Récemment, j’ai réalisé que j’avais pusheé un fichier .env contenant des identifiants d’API dans un référentiel privé. Bien qu’il soit privé, ce n’est pas une excuse. Un accès compromis, un référentiel devenant public, ou simplement un audit de sécurité aurait exposé mes tokens. J’ai appris que je ne peux pas faire confiance à la suppression de fichiers dans les commits ultérieurs—Git conserve tout l’historique.\n","title":"Comment nettoyer les identifiants exposés dans Git avec git-filter-repo et faire tourner les tokens","type":"posts"},{"content":" Le problème # J\u0026rsquo;avais besoin d\u0026rsquo;accéder à mon serveur domestique par SSH depuis mon téléphone sans l\u0026rsquo;exposer directement à internet. Les options évidentes étaient mauvaises : ouvrir le port 22 au monde entier est un suicide, et faire confiance à des apps tierces avec accès root ne me convenait pas. La solution qui a fonctionné : WireGuard + Termius.\nPourquoi cette combinaison # WireGuard est léger, rapide et consomme peu de batterie sur les téléphones. Termius est un client SSH soigné qui gère bien les clés privées. Ensemble, tu as un accès sécurisé sans complications.\nÉtape 1 : Installation et configuration de WireGuard sur le serveur # J\u0026rsquo;ai installé WireGuard sur mon serveur (Debian 12) :\nsudo apt update sudo apt install wireguard wireguard-tools J\u0026rsquo;ai généré les clés publique et privée du serveur :\ncd /etc/wireguard sudo wg genkey | tee privatekey | wg pubkey \u0026gt; publickey J\u0026rsquo;ai créé le fichier de configuration /etc/wireguard/wg0.conf :\n[Interface] Address = 10.0.0.1/24 ListenPort = 51820 PrivateKey = [CONTENIDO DE privatekey] PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE [Peer] PublicKey = [CLAVE PÚBLICA DEL MÓVIL - GENERARLA DESPUÉS] AllowedIPs = 10.0.0.2/32 J\u0026rsquo;ai activé le service :\nsudo systemctl enable wg-quick@wg0 sudo systemctl start wg-quick@wg0 J\u0026rsquo;ai ouvert le port UDP 51820 dans le pare-feu (dans mon cas, le routeur) :\nsudo ufw allow 51820/udp Étape 2 : Configuration du client sur le téléphone # J\u0026rsquo;ai installé WireGuard depuis le Play Store (Android) ou l\u0026rsquo;App Store (iOS).\nJ\u0026rsquo;ai généré les clés du téléphone sur le serveur :\nwg genkey | tee mobile_privatekey | wg pubkey \u0026gt; mobile_publickey J\u0026rsquo;ai créé le fichier de configuration pour le téléphone :\n[Interface] Address = 10.0.0.2/24 PrivateKey = [CONTENIDO DE mobile_privatekey] DNS = 8.8.8.8 [Peer] PublicKey = [CLAVE PÚBLICA DEL SERVIDOR] Endpoint = [IP_PÚBLICA_SERVIDOR]:51820 AllowedIPs = 10.0.0.0/24 PersistentKeepalive = 25 J\u0026rsquo;ai exporté ce fichier en QR ou je l\u0026rsquo;ai passé par USB au téléphone. WireGuard l\u0026rsquo;importe directement.\nJ\u0026rsquo;ai activé la connexion dans WireGuard du téléphone et j\u0026rsquo;ai vérifié la connectivité :\nwg show Étape 3 : Configuration de SSH dans Termius # Dans Termius j\u0026rsquo;ai créé une nouvelle connexion :\nHôte : 10.0.0.1 (l\u0026rsquo;adresse IP interne du serveur dans WireGuard) Port : 22 (SSH standard, n\u0026rsquo;a pas besoin d\u0026rsquo;être ouvert à l\u0026rsquo;extérieur) Utilisateur : mon utilisateur habituel Authentification : Clé privée SSH J\u0026rsquo;ai importé ma clé privée SSH depuis les fichiers du téléphone. Termius la gère sans exposer les fichiers.\nÉtape 4 : Tests et ajustements # Je me suis connecté à WireGuard depuis le téléphone. J\u0026rsquo;ai ouvert Termius et je me suis connecté au serveur. Ça a marché du premier coup.\nLa latence est imperceptible. La consommation de batterie de WireGuard est minime (à peine 2-3 % en 8 heures en veille).\nDétails de sécurité qui importent # Le serveur SSH n\u0026rsquo;est jamais exposé à internet WireGuard utilise une cryptographie moderne (protocole Noise) Les clés privées ne voyagent jamais sur le réseau Le trafic SSH à l\u0026rsquo;intérieur du tunnel est doublement chiffré Ce que je changerais # Rien. Cette configuration fonctionne depuis des mois sans problème. La seule amélioration serait d\u0026rsquo;utiliser des adresses DNS dynamiques si mon adresse IP publique change, mais c\u0026rsquo;est un autre article.\nMise à jour : Cette même méthode fonctionne pour connecter d\u0026rsquo;autres appareils (ordinateur portable, tablette). Il suffit de générer de nouvelles clés et d\u0026rsquo;ajouter plus de peers dans WireGuard.\nÉquipement recommandé # TECLAST T65 Tablette 13.4\u0026quot; Android 16 avec clavier et stylet — Tablette avec 4G LTE comme client SSH/VPN portable de n\u0026rsquo;importe où Routeur GL.iNet MT3000 — Routeur avec WireGuard intégré pour installer le tunnel VPN en quelques minutes Support pliable pour ordinateur portable en aluminium avec angle ajustable — Ergonomie essentielle si tu utilises une tablette ou un ordinateur portable pour gérer ton serveur Liens d\u0026rsquo;affiliation. Pas de coût supplémentaire pour toi.\n","date":"19 mai 2026","externalUrl":null,"permalink":"/fr/posts/conectar-el-movil-a-ssh-desde-cualquier-lugar-con-wireguard-y-termius/","section":"Posts","summary":"Le problème # J’avais besoin d’accéder à mon serveur domestique par SSH depuis mon téléphone sans l’exposer directement à internet. Les options évidentes étaient mauvaises : ouvrir le port 22 au monde entier est un suicide, et faire confiance à des apps tierces avec accès root ne me convenait pas. La solution qui a fonctionné : WireGuard + Termius.\n","title":"Connecter le mobile à SSH de n'importe où avec WireGuard et Termius","type":"posts"},{"content":"","date":"19 mai 2026","externalUrl":null,"permalink":"/fr/tags/credenciales/","section":"Tags","summary":"","title":"Credenciales","type":"tags"},{"content":"","date":"19 mai 2026","externalUrl":null,"permalink":"/fr/tags/devops/","section":"Tags","summary":"","title":"Devops","type":"tags"},{"content":"","date":"19 mai 2026","externalUrl":null,"permalink":"/fr/tags/git/","section":"Tags","summary":"","title":"Git","type":"tags"},{"content":"","date":"19 mai 2026","externalUrl":null,"permalink":"/fr/tags/grafana/","section":"Tags","summary":"","title":"Grafana","type":"tags"},{"content":"","date":"19 mai 2026","externalUrl":null,"permalink":"/fr/tags/listmonk/","section":"Tags","summary":"","title":"Listmonk","type":"tags"},{"content":" Le problème initial # J\u0026rsquo;avais Grafana et Listmonk s\u0026rsquo;exécutant dans des conteneurs Docker, chacun avec sa propre instance PostgreSQL embarquée. Cela fonctionnait, mais c\u0026rsquo;était inefficace : deux moteurs de BD consommant des ressources et sans moyen centralisé de faire des sauvegardes. J\u0026rsquo;ai décidé de tout consolider dans une seule instance PostgreSQL partagée.\nPréparation : démarrer PostgreSQL central # La première chose a été de créer le serveur PostgreSQL qui serait le point central. Je l\u0026rsquo;ai fait avec un docker-compose dédié :\nversion: \u0026#39;3.8\u0026#39; services: postgres: image: postgres:15-alpine container_name: postgres-central environment: POSTGRES_USER: admin POSTGRES_PASSWORD: tu_contraseña_segura POSTGRES_DB: default_db ports: - \u0026#34;5432:5432\u0026#34; volumes: - postgres_data:/var/lib/postgresql/data networks: - central-net volumes: postgres_data: networks: central-net: driver: bridge J\u0026rsquo;ai exécuté docker-compose up -d et vérifié que cela fonctionne avec docker exec postgres-central psql -U admin -d default_db -c \u0026quot;\\l\u0026quot;.\nCréer des bases de données pour chaque service # J\u0026rsquo;ai accédé au conteneur PostgreSQL et créé les BD dont j\u0026rsquo;aurais besoin :\ndocker exec -it postgres-central psql -U admin -d default_db Une fois à l\u0026rsquo;intérieur :\nCREATE DATABASE grafana; CREATE DATABASE listmonk; CREATE USER grafana_user WITH PASSWORD \u0026#39;grafana_pass\u0026#39;; CREATE USER listmonk_user WITH PASSWORD \u0026#39;listmonk_pass\u0026#39;; GRANT ALL PRIVILEGES ON DATABASE grafana TO grafana_user; GRANT ALL PRIVILEGES ON DATABASE listmonk TO listmonk_user; Exporter les données des anciennes BD # Avant de déplacer quoi que ce soit, j\u0026rsquo;ai fait un dump des BD existantes. Pour Grafana :\ndocker exec grafana-container pg_dump -U grafana -d grafana \u0026gt; grafana_backup.sql Pour Listmonk :\ndocker exec listmonk-container pg_dump -U listmonk -d listmonk \u0026gt; listmonk_backup.sql Importer les données dans PostgreSQL central # J\u0026rsquo;ai importé les sauvegardes dans les nouvelles BD :\ndocker exec -i postgres-central psql -U grafana_user -d grafana \u0026lt; grafana_backup.sql docker exec -i postgres-central psql -U listmonk_user -d listmonk \u0026lt; listmonk_backup.sql J\u0026rsquo;ai vérifié que les tables étaient présentes avec \\dt dans chaque BD.\nMettre à jour Grafana # J\u0026rsquo;ai modifié le docker-compose de Grafana pour qu\u0026rsquo;il pointe vers PostgreSQL central :\nservices: grafana: image: grafana/grafana:latest container_name: grafana environment: GF_DATABASE_TYPE: postgres GF_DATABASE_HOST: postgres-central:5432 GF_DATABASE_NAME: grafana GF_DATABASE_USER: grafana_user GF_DATABASE_PASSWORD: grafana_pass networks: - central-net - grafana-net networks: central-net: external: true grafana-net: driver: bridge Important : le réseau central-net doit être external: true car il existe déjà dans le docker-compose de PostgreSQL.\nMettre à jour Listmonk # La même chose pour Listmonk. Sa configuration dans docker-compose :\nservices: listmonk: image: listmonk/listmonk:latest container_name: listmonk environment: LISTMONK_db__host: postgres-central LISTMONK_db__port: \u0026#34;5432\u0026#34; LISTMONK_db__user: listmonk_user LISTMONK_db__password: listmonk_pass LISTMONK_db__database: listmonk networks: - central-net - listmonk-net networks: central-net: external: true listmonk-net: driver: bridge Tests et nettoyage # J\u0026rsquo;ai démarré les deux conteneurs : docker-compose up -d. J\u0026rsquo;ai vérifié que Grafana et Listmonk démarrent correctement et que leurs données sont intactes.\nUne fois que tout a été confirmé comme fonctionnel, j\u0026rsquo;ai supprimé les volumes des anciennes BD :\ndocker volume rm grafana_postgres_data listmonk_postgres_data Avantages réels # J\u0026rsquo;ai maintenant un seul point de sauvegarde, une consommation de RAM réduite, et je peux évoluer plus facilement. Un problème : assurez-vous que PostgreSQL central est sur le bon réseau ou que les services peuvent communiquer par hôte externe.\nLa migration m\u0026rsquo;a pris une heure. Sans stress.\nÉquipement recommandé # SSD NVMe 1TB — Améliore les performances du serveur avec base de données Mini PC Intel N100 — Serveur domestique silencieux et efficace pour exécuter Docker et PostgreSQL 24/7 SAI/UPS 600VA — Protège le serveur et la base de données contre les coupures de courant Liens d\u0026rsquo;affiliation. Aucun frais supplémentaire pour toi.\n","date":"19 mai 2026","externalUrl":null,"permalink":"/fr/posts/migrar-grafana-y-listmonk-a-postgresql-centralizado-en-docker/","section":"Posts","summary":"Le problème initial # J’avais Grafana et Listmonk s’exécutant dans des conteneurs Docker, chacun avec sa propre instance PostgreSQL embarquée. Cela fonctionnait, mais c’était inefficace : deux moteurs de BD consommant des ressources et sans moyen centralisé de faire des sauvegardes. J’ai décidé de tout consolider dans une seule instance PostgreSQL partagée.\nPréparation : démarrer PostgreSQL central # La première chose a été de créer le serveur PostgreSQL qui serait le point central. Je l’ai fait avec un docker-compose dédié :\n","title":"Migrer Grafana et Listmonk vers PostgreSQL centralisé dans Docker","type":"posts"},{"content":"","date":"19 mai 2026","externalUrl":null,"permalink":"/fr/tags/m%C3%B3vil/","section":"Tags","summary":"","title":"Móvil","type":"tags"},{"content":"","date":"19 mai 2026","externalUrl":null,"permalink":"/fr/tags/postgresql/","section":"Tags","summary":"","title":"Postgresql","type":"tags"},{"content":"","date":"19 mai 2026","externalUrl":null,"permalink":"/fr/tags/ssh/","section":"Tags","summary":"","title":"Ssh","type":"tags"},{"content":"","date":"19 mai 2026","externalUrl":null,"permalink":"/fr/tags/vpn/","section":"Tags","summary":"","title":"Vpn","type":"tags"},{"content":"","date":"19 mai 2026","externalUrl":null,"permalink":"/fr/tags/wireguard/","section":"Tags","summary":"","title":"Wireguard","type":"tags"},{"content":"","date":"11 mai 2026","externalUrl":null,"permalink":"/fr/tags/automatizaci%C3%B3n/","section":"Tags","summary":"","title":"Automatización","type":"tags"},{"content":"","date":"11 mai 2026","externalUrl":null,"permalink":"/fr/tags/backups/","section":"Tags","summary":"","title":"Backups","type":"tags"},{"content":"","date":"11 mai 2026","externalUrl":null,"permalink":"/fr/categories/infraestructura/","section":"Categories","summary":"","title":"Infraestructura","type":"categories"},{"content":"","date":"11 mai 2026","externalUrl":null,"permalink":"/fr/tags/linux/","section":"Tags","summary":"","title":"Linux","type":"tags"},{"content":"","date":"11 mai 2026","externalUrl":null,"permalink":"/fr/tags/restic/","section":"Tags","summary":"","title":"Restic","type":"tags"},{"content":"J\u0026rsquo;avais déjà des backups avec rsync et cron, mais rsync copie des fichiers, pas des snapshots. Si tu supprimes accidentellement un fichier et que la sauvegarde se synchronise avant que tu ne t\u0026rsquo;en aperçoives, tu le perds. Restic résout ce problème et ajoute quelque chose que rsync ne donnera jamais : chiffrement AES-256, déduplication et snapshots avec historique navigable.\nCe qui rend Restic différent # Caractéristique rsync Restic Chiffrement AES-256 Non Oui Déduplication Non Oui (au niveau des blocs) Snapshots navigables Non Oui Plusieurs backends Non SFTP, S3, Backblaze, rclone… Vérification d\u0026rsquo;intégrité Non restic check Politique de rétention Manuel restic forget --prune La déduplication est particulièrement utile pour les sauvegardes de bases de données et les répertoires de configuration qui changent peu : un référentiel Restic qui a 6 mois de sauvegardes quotidiennes occupe généralement beaucoup moins qu'180 copies complètes.\nInstallation # Sur Ubuntu/Debian, la version du référentiel officiel a généralement du retard. Il est préférable de télécharger le binaire directement depuis les releases officielles GitHub :\n# Descarga la última versión (ajusta la versión si hay una más reciente) wget https://github.com/restic/restic/releases/download/v0.17.3/restic_0.17.3_linux_amd64.bz2 bunzip2 restic_0.17.3_linux_amd64.bz2 chmod +x restic_0.17.3_linux_amd64 sudo mv restic_0.17.3_linux_amd64 /usr/local/bin/restic # Verificar restic version Ou avec les référentiels système si la version ne te préoccupe pas :\nsudo apt install restic # Debian/Ubuntu sudo dnf install restic # Fedora/RHEL Concepts clés avant de commencer # Référentiel : la destination où Restic stocke les sauvegardes. Il peut s\u0026rsquo;agir d\u0026rsquo;un dossier local, d\u0026rsquo;un serveur SFTP, d\u0026rsquo;un bucket S3, etc. Snapshot : chaque fois que tu exécutes restic backup, Restic enregistre un snapshot ponctuel. Les snapshots partagent des blocs dédupliqués, ils ne multiplient donc pas l\u0026rsquo;espace utilisé. Mot de passe : le référentiel est chiffré avec un mot de passe. Sans lui, les données sont illisibles. Stocke-le dans un gestionnaire de mots de passe ou dans un fichier séparé de la sauvegarde. Initialiser un référentiel # Option A : référentiel local # restic init --repo /opt/backups/mi-servidor Option B : référentiel sur serveur distant via SFTP # restic init --repo sftp:usuario@servidor-backup:/opt/restic/mi-servidor Option C : référentiel sur S3 (ou compatible : Backblaze B2, MinIO…) # export AWS_ACCESS_KEY_ID=\u0026#34;TU_ACCESS_KEY_AQUI\u0026#34; export AWS_SECRET_ACCESS_KEY=\u0026#34;TU_SECRET_KEY_AQUI\u0026#34; restic init --repo s3:s3.eu-west-1.amazonaws.com/mi-bucket-backups/mi-servidor Dans les trois cas, Restic te demandera un mot de passe pour chiffrer le référentiel. Garde-le bien — sans lui, tu ne peux rien restaurer.\nFichier de variables d\u0026rsquo;environnement # Pour ne pas taper le mot de passe à chaque commande, crée un fichier de variables :\n# /etc/restic/env.sh (solo root puede leerlo) sudo mkdir -p /etc/restic sudo chmod 700 /etc/restic sudo tee /etc/restic/env.sh \u0026gt; /dev/null \u0026lt;\u0026lt;\u0026#39;EOF\u0026#39; export RESTIC_REPOSITORY=\u0026#34;/opt/backups/mi-servidor\u0026#34; export RESTIC_PASSWORD=\u0026#34;TU_CONTRASENA_AQUI\u0026#34; EOF sudo chmod 600 /etc/restic/env.sh À partir de maintenant, avant toute commande Restic :\nsource /etc/restic/env.sh Ou avec les variables d\u0026rsquo;environnement en ligne pour les scripts :\nenv $(cat /etc/restic/env.sh | grep export | sed \u0026#39;s/export //\u0026#39;) restic snapshots Première sauvegarde # source /etc/restic/env.sh # Backup de los datos de Docker y configuraciones restic backup \\ /opt/app/ \\ /etc/nginx/ \\ /home/usuario/ \\ --exclude=\u0026#39;/opt/app/logs\u0026#39; \\ --exclude=\u0026#39;*.tmp\u0026#39; \\ --tag servidor-web La sortie affiche combien de fichiers ont été traités, combien sont nouveaux et l\u0026rsquo;espace économisé par déduplication :\nFiles: 1234 new, 567 changed, 8901 unmodified Dirs: 45 new, 12 changed, 89 unmodified Added to the repo: 234.567 MiB processed 10702 files, 1.234 GiB in 0:23 snapshot a1b2c3d4 saved Voir et naviguer les snapshots # # Listar todos los snapshots restic snapshots # Listar ficheros dentro de un snapshot específico restic ls a1b2c3d4 # Buscar un fichero en todos los snapshots restic find --tag servidor-web nombre-fichero.conf Sortie de restic snapshots :\nID Time Host Tags Paths ──────────────────────────────────────────────────────────────────── a1b2c3d4 2026-05-11 03:00:02 mi-servidor servidor-web /opt/app, /etc/nginx, /home/usuario e5f6a7b8 2026-05-10 03:00:01 mi-servidor servidor-web /opt/app, /etc/nginx, /home/usuario Restaurer des données # Restauration complète d\u0026rsquo;un snapshot # # Restaura todo en /tmp/restauracion para revisar antes de mover restic restore a1b2c3d4 --target /tmp/restauracion Restaurer un seul fichier ou répertoire # # Restaura solo nginx.conf del snapshot más reciente restic restore latest --target /tmp/restauracion \\ --include /etc/nginx/nginx.conf Monter le référentiel comme système de fichiers (utile pour explorer) # # Requiere FUSE instalado: sudo apt install fuse mkdir -p /mnt/restic-backups restic mount /mnt/restic-backups \u0026amp; # Ahora puedes navegar por todos los snapshots ls /mnt/restic-backups/snapshots/ # → a1b2c3d4/ e5f6a7b8/ latest/ # Cuando acabes fusermount -u /mnt/restic-backups Politique de rétention automatique # Sans politique de rétention, le référentiel grandit indéfiniment. C\u0026rsquo;est la configuration que j\u0026rsquo;utilise pour les sauvegardes quotidiennes :\n# 7 días diarios, 4 semanas, 6 meses, 1 año restic forget \\ --keep-daily 7 \\ --keep-weekly 4 \\ --keep-monthly 6 \\ --keep-yearly 1 \\ --tag servidor-web \\ --prune --prune supprime physiquement les données non référencées. Sans lui, forget supprime uniquement les métadonnées du snapshot mais ne libère pas d\u0026rsquo;espace.\nVérifier l\u0026rsquo;intégrité du référentiel # # Verificación rápida de metadatos restic check # Verificación completa leyendo todos los datos (lento, hazlo mensualmente) restic check --read-data Si restic check échoue, le référentiel est corrompu. C\u0026rsquo;est pourquoi il est toujours recommandé d\u0026rsquo;avoir au moins deux référentiels dans des destinations différentes (la célèbre règle 3-2-1).\nAutomatiser avec systemd timer # Restic s\u0026rsquo;intègre parfaitement avec systemd timers, qui permettent de capturer la sortie dans journald et d\u0026rsquo;exécuter la tâche même si le serveur était éteint à l\u0026rsquo;heure programmée.\n/etc/systemd/system/restic-backup.service :\n[Unit] Description=Backup diario con Restic After=network.target [Service] Type=oneshot User=root EnvironmentFile=/etc/restic/env.sh ExecStart=/usr/local/bin/restic backup \\ /opt/app/ \\ /etc/nginx/ \\ /home/usuario/ \\ --exclude=\u0026#39;/opt/app/logs\u0026#39; \\ --tag servidor-web ExecStartPost=/usr/local/bin/restic forget \\ --keep-daily 7 \\ --keep-weekly 4 \\ --keep-monthly 6 \\ --tag servidor-web \\ --prune StandardOutput=journal StandardError=journal /etc/systemd/system/restic-backup.timer :\n[Unit] Description=Ejecuta backup Restic diariamente [Timer] OnCalendar=*-*-* 03:00:00 Persistent=true RandomizedDelaySec=10min [Install] WantedBy=timers.target Activer :\nsudo systemctl daemon-reload sudo systemctl enable --now restic-backup.timer # Verificar que está activo sudo systemctl list-timers restic-backup.timer RandomizedDelaySec=10min distribue les sauvegardes dans des fenêtres aléatoires pour éviter que tous les serveurs ne frappent la destination SFTP ou S3 en même temps.\nNotifications par email à la fin # En combinant avec le système de notifications par email avec msmtp, nous pouvons recevoir un résumé de la sauvegarde. Modifiez ExecStart par un script wrapper :\n/usr/local/bin/restic-backup.sh :\n#!/bin/bash set -euo pipefail source /etc/restic/env.sh LOG=$(mktemp) STATUS=0 restic backup \\ /opt/app/ \\ /etc/nginx/ \\ /home/usuario/ \\ --exclude=\u0026#39;/opt/app/logs\u0026#39; \\ --tag servidor-web \u0026gt; \u0026#34;$LOG\u0026#34; 2\u0026gt;\u0026amp;1 || STATUS=$? restic forget \\ --keep-daily 7 \\ --keep-weekly 4 \\ --keep-monthly 6 \\ --tag servidor-web \\ --prune \u0026gt;\u0026gt; \u0026#34;$LOG\u0026#34; 2\u0026gt;\u0026amp;1 || STATUS=$? SUBJECT=\u0026#34;[Backup] $(hostname) - $(date \u0026#39;+%Y-%m-%d\u0026#39;)\u0026#34; [ $STATUS -ne 0 ] \u0026amp;\u0026amp; SUBJECT=\u0026#34;[ERROR Backup] $(hostname) - $(date \u0026#39;+%Y-%m-%d\u0026#39;)\u0026#34; cat \u0026#34;$LOG\u0026#34; | mail -s \u0026#34;$SUBJECT\u0026#34; usuario@ejemplo.com rm -f \u0026#34;$LOG\u0026#34; exit $STATUS chmod +x /usr/local/bin/restic-backup.sh Et dans le .service, remplacez ExecStart et ExecStartPost par une seule ligne :\nExecStart=/usr/local/bin/restic-backup.sh Vérification hebdomadaire de l\u0026rsquo;intégrité # Ajoutez un deuxième timer pour la vérification hebdomadaire, qui est plus coûteuse :\n/etc/systemd/system/restic-check.service :\n[Unit] Description=Verificación semanal de integridad Restic After=network.target [Service] Type=oneshot User=root EnvironmentFile=/etc/restic/env.sh ExecStart=/usr/local/bin/restic check StandardOutput=journal StandardError=journal /etc/systemd/system/restic-check.timer :\n[Unit] Description=Verifica integridad del repositorio Restic cada semana [Timer] OnCalendar=Sun *-*-* 04:00:00 Persistent=true [Install] WantedBy=timers.target sudo systemctl enable --now restic-check.timer Voir l\u0026rsquo;état dans le journal du système # # Último backup journalctl -u restic-backup.service -n 50 # Seguir en tiempo real durante la ejecución manual journalctl -u restic-backup.service -f # Historial de ejecuciones del timer systemctl status restic-backup.timer Conclusion # Restic ne remplace pas complètement rsync pour les cas de synchronisation de répertoires en direct, mais pour les sauvegardes avec historique, il est clairement supérieur : chiffrement par défaut, déduplication transparente et snapshots navigables sans scripts supplémentaires. L\u0026rsquo;intégration avec systemd timers et l\u0026rsquo;envoi de notifications par email ferme la boucle : vous savez que la sauvegarde s\u0026rsquo;est exécutée, quand elle a échoué, et vous pouvez restaurer n\u0026rsquo;importe quel point dans le temps en minutes.\nLa véritable clé de toute stratégie de sauvegarde n\u0026rsquo;est pas le logiciel que vous choisissez, mais vérifier que vous pouvez restaurer. Testez restic restore dans un répertoire temporaire avant d\u0026rsquo;en avoir vraiment besoin.\n","date":"11 mai 2026","externalUrl":null,"permalink":"/fr/posts/restic-backups-cifrados-deduplicacion/","section":"Posts","summary":"J’avais déjà des backups avec rsync et cron, mais rsync copie des fichiers, pas des snapshots. Si tu supprimes accidentellement un fichier et que la sauvegarde se synchronise avant que tu ne t’en aperçoives, tu le perds. Restic résout ce problème et ajoute quelque chose que rsync ne donnera jamais : chiffrement AES-256, déduplication et snapshots avec historique navigable.\nCe qui rend Restic différent # Caractéristique rsync Restic Chiffrement AES-256 Non Oui Déduplication Non Oui (au niveau des blocs) Snapshots navigables Non Oui Plusieurs backends Non SFTP, S3, Backblaze, rclone… Vérification d’intégrité Non restic check Politique de rétention Manuel restic forget --prune La déduplication est particulièrement utile pour les sauvegardes de bases de données et les répertoires de configuration qui changent peu : un référentiel Restic qui a 6 mois de sauvegardes quotidiennes occupe généralement beaucoup moins qu'180 copies complètes.\n","title":"Restic: sauvegardes chiffrées avec déduplication pour ton serveur Linux","type":"posts"},{"content":" El problema de la IA de usar y tirar # La mayoría de los flujos con asistentes de IA tienen el mismo patrón: abres el chat, explicas el contexto desde cero, obtienes la respuesta y cierras. La próxima vez, repites.\nEso funciona para tareas puntuales, pero no escala si quieres integrar la IA en tu flujo de trabajo diario. Cada sesión empieza en cero: sin memoria de tu infraestructura, sin conocimiento de tus convenciones, sin contexto de lo que estabas haciendo.\nLa solución que monté combina dos piezas: los skills de Claude Code y un bot de Telegram como canal de comunicación.\nQué son los skills de Claude Code # Claude Code es la CLI oficial de Anthropic para interactuar con Claude desde el terminal. Una de sus funcionalidades más potentes es el sistema de skills: scripts personalizados que se invocan como comandos dentro de la sesión.\nUn skill es básicamente un fichero Markdown con instrucciones que el asistente ejecuta al llamar a /nombre-skill. Lo importante es que esas instrucciones incluyen todo el contexto necesario: dónde están los archivos, qué convenciones seguir, qué evitar, qué herramientas usar.\n~/.claude/ └── skills/ ├── revisar-infra.md # /revisar-infra → analiza logs y estado Docker ├── publicar.md # /publicar → pipeline LinkedIn + blog └── deploy.md # /deploy → git push + CI/CD En lugar de explicar cada vez \u0026ldquo;mira en /home/tellme/CLAUDE/agente/, el .env tiene las credenciales, no uses rutas absolutas\u0026hellip;\u0026rdquo;, defines el skill una vez y lo invocas con un comando.\nEjemplo real # El skill /publicar de mi setup hace esto al invocarse:\nComprueba la cola de LinkedIn (social/linkedin-queue.json) Verifica que el token no esté próximo a caducar Genera el texto del post según el último artículo del blog Aplica las reglas de seguridad (sin IPs reales, sin rutas absolutas) Lo añade a la cola para publicación Sin el skill, eso requeriría explicar el flujo completo en cada sesión.\nTelegram como canal de acceso # El segundo ingrediente es conectar Claude Code a un bot de Telegram. Esto te permite interactuar con el asistente desde el móvil sin necesidad de estar en el terminal.\nLa integración funciona así:\nMóvil → Telegram Bot → Claude Code session → skill/tool → respuesta El control de acceso es por ID de usuario de Telegram. Solo los IDs en la lista blanca pueden activar el bot. El fichero access.json define la política:\n{ \u0026#34;dmPolicy\u0026#34;: \u0026#34;allowlist\u0026#34;, \u0026#34;allowFrom\u0026#34;: [\u0026#34;TU_ID_TELEGRAM\u0026#34;] } Con dmPolicy: \u0026quot;allowlist\u0026quot; nadie puede hacer pairing sin que tú lo apruebes explícitamente, y los IDs no autorizados simplemente no reciben respuesta.\nFlujo práctico # Desde el móvil puedo enviarle al bot:\n\u0026quot;¿Hay posts pendientes en LinkedIn?\u0026quot; → comprueba la cola y responde \u0026ldquo;Lanza el publisher\u0026rdquo; → ejecuta la rutina programada \u0026ldquo;Estado de los contenedores Docker\u0026rdquo; → revisa y resume El asistente tiene acceso al contexto del proyecto porque la sesión de Claude Code está corriendo en el servidor con acceso a todos los ficheros.\nRutinas programadas: la capa de autonomía # La tercera pieza son las rutinas remotas: agentes que se ejecutan en la nube de Anthropic según un cron, sin que tengas que estar delante.\n0 8 * * * → LinkedIn publisher 0 6 * * * → revisión de seguridad 0 8 * * 1 → borrador de post semanal Cada rutina clona el repositorio, ejecuta su script y hace push del resultado. El agente no tiene acceso a tu máquina local — trabaja contra el repo en GitLab.\nLa combinación de los tres elementos crea un sistema coherente:\nComponente Qué hace Skills Define qué puede hacer el asistente y cómo Telegram Acceso desde el móvil con control de acceso Rutinas Autonomía programada sin intervención El hardware # Todo esto corre en un servidor doméstico 24/7. Un Mini PC con procesador N100 es suficiente para Docker, el agente y el bot de Telegram con un consumo de unos 8-10W.\n→ Mini PC Intel N100 en Amazon\nNo necesitas nada en la nube para la parte local del agente. Las rutinas remotas sí corren en la infraestructura de Anthropic, pero el acceso al repo y los scripts es tuyo.\nConclusión # El valor no está en cada pieza por separado sino en la composabilidad:\nSkills → el asistente conoce tu contexto sin que lo repitas Telegram → acceso desde cualquier sitio con política de acceso clara Rutinas → autonomía programada para las tareas repetitivas El resultado es un asistente que funciona como un colaborador técnico: conoce tu infraestructura, puede actuar sobre ella y está disponible desde el móvil.\nToda la configuración está documentada en el repo de infraestructura.\n","date":"10 mai 2026","externalUrl":null,"permalink":"/posts/skills-y-agentes-claude-code-asistente-tecnico-desde-movil/","section":"Posts","summary":"El problema de la IA de usar y tirar # La mayoría de los flujos con asistentes de IA tienen el mismo patrón: abres el chat, explicas el contexto desde cero, obtienes la respuesta y cierras. La próxima vez, repites.\nEso funciona para tareas puntuales, pero no escala si quieres integrar la IA en tu flujo de trabajo diario. Cada sesión empieza en cero: sin memoria de tu infraestructura, sin conocimiento de tus convenciones, sin contexto de lo que estabas haciendo.\n","title":"Skills y agentes en Claude Code: tu asistente técnico accesible desde el móvil","type":"posts"},{"content":"","date":"6 mai 2026","externalUrl":null,"permalink":"/fr/tags/servidor-dom%C3%A9stico/","section":"Tags","summary":"","title":"Servidor-Doméstico","type":"tags"},{"content":" Pourquoi tu as besoin de cela # Il y a quelques mois, j\u0026rsquo;ai fait face à un problème courant : je voulais accéder à mes services internes (Jellyfin, Home Assistant, etc.) de l\u0026rsquo;extérieur de la maison, mais je ne voulais pas les exposer directement sur internet. Ouvrir des ports est un risque inutile. La solution a été de mettre en place un VPN avec Wireguard dans Docker. C\u0026rsquo;était la meilleure décision que j\u0026rsquo;ai prise pour mon infrastructure domestique.\nAvantages de Wireguard # Léger : consomme moins de ressources qu\u0026rsquo;OpenVPN Rapide : protocole moderne et efficace Facile à configurer : comparé à d\u0026rsquo;autres alternatives Sécurisé : cryptographie de dernière génération Docker-friendly : il existe d\u0026rsquo;excellentes images officielles Préparation # Tu as besoin de :\nUn serveur avec Docker installé Le fichier docker-compose.yml Un domaine ou une adresse IP publique (pour te connecter de l\u0026rsquo;extérieur) Les clients Wireguard sur tes appareils Installation étape par étape # 1. Créer le répertoire de configuration # mkdir -p ~/wireguard/config cd ~/wireguard 2. Docker Compose # Crée le fichier docker-compose.yml :\nversion: \u0026#39;3.8\u0026#39; services: wireguard: image: linuxserver/wireguard:latest container_name: wireguard cap_add: - NET_ADMIN - SYS_MODULE environment: - PUID=1000 - PGID=1000 - TZ=Europe/Madrid - SERVERURL=tu-dominio-o-ip-publica.com - SERVERPORT=51820 - PEERS=telefono,laptop,tablet - PEERDNS=auto ports: - \u0026#34;51820:51820/udp\u0026#34; volumes: - ./config:/config - /lib/modules:/lib/modules:ro networks: - mi-red restart: unless-stopped networks: mi-red: driver: bridge ipam: config: - subnet: 10.0.0.0/24 Remplace :\ntu-dominio-o-ip-publica.com par ton adresse réelle Les PEERS par les noms de tes appareils Le fuseau horaire selon ta localisation 3. Démarrer le conteneur # docker-compose up -d Les fichiers de configuration seront générés automatiquement dans ./config. Attends quelques secondes et vérifie :\nls -la config/peer_*/ 4. Obtenir les codes QR # Pour connecter tes appareils :\ndocker exec wireguard cat /config/peer_telefono/peer_telefono.conf Ou directement les QR :\ndocker exec wireguard qrencode -t ansiutf8 \u0026lt; /config/peer_telefono/peer_telefono.conf Scanne avec ton client Wireguard sur chaque appareil.\nConnecter les services internes # C\u0026rsquo;est ici que ça devient important. Je veux accéder à des services sur mon réseau interne. Pour cela, je modifie le docker-compose.yml et j\u0026rsquo;ajoute des routes :\nenvironment: - ALLOWEDIPS=10.0.0.0/24,192.168.1.0/24 Cela te permet d\u0026rsquo;accéder de la VPN au réseau 192.168.1.0/24 (ton réseau local).\nSur le serveur, active le forwarding :\necho \u0026#34;net.ipv4.ip_forward=1\u0026#34; | sudo tee -a /etc/sysctl.conf sudo sysctl -p Accès depuis les clients # Une fois connecté au VPN, tu accèdes à tes services en utilisant leurs adresses IP internes :\nhttp://192.168.1.100:8096 pour Jellyfin http://192.168.1.50:8123 pour Home Assistant Ce dont tu as besoin sur ton réseau Maintenance # Renouveler les certificats (tous les 6 mois environ) :\ndocker exec wireguard /app/wireguard-tools/show-peer peer_nombre Ajouter un nouvel appareil :\ndocker-compose down # Edita PEERS en docker-compose.yml docker-compose up -d Notes finales # Ouvre seulement le port 51820/UDP sur ton routeur Utilise un pare-feu sur le serveur pour bloquer l\u0026rsquo;accès inutile Vérifie que le forwarding d\u0026rsquo;IP est actif Surveille régulièrement le trafic du VPN Depuis plusieurs mois avec cette configuration et elle est totalement stable. J\u0026rsquo;accède à mes services de n\u0026rsquo;importe où sans soucis de sécurité. Je recommande définitivement cette configuration à quiconque veut maintenir son infrastructure domestique privée mais accessible.\nÉquipement recommandé # Raspberry Pi 3 B+ — Serveur léger à faible consommation pour démarrer ton homelab Raspberry Pi 4 (4GB) — La base parfaite pour homelab, Docker et surveillance TECLAST T65 Tablet 13.4\u0026quot; Android 16 avec clavier et stylet — Client WireGuard portable : gère tes services de n\u0026rsquo;importe où Liens d\u0026rsquo;affiliation. Aucun coût supplémentaire pour toi.\n","date":"6 mai 2026","externalUrl":null,"permalink":"/fr/posts/vpn-con-wireguard-en-docker-acceso-seguro-a-servicios-internos-sin-exponer-puertos/","section":"Posts","summary":"Pourquoi tu as besoin de cela # Il y a quelques mois, j’ai fait face à un problème courant : je voulais accéder à mes services internes (Jellyfin, Home Assistant, etc.) de l’extérieur de la maison, mais je ne voulais pas les exposer directement sur internet. Ouvrir des ports est un risque inutile. La solution a été de mettre en place un VPN avec Wireguard dans Docker. C’était la meilleure décision que j’ai prise pour mon infrastructure domestique.\n","title":"VPN avec Wireguard dans Docker : Accès sécurisé aux services internes sans exposition de ports","type":"posts"},{"content":"","date":"5 mai 2026","externalUrl":null,"permalink":"/fr/tags/alertas/","section":"Tags","summary":"","title":"Alertas","type":"tags"},{"content":"","date":"5 mai 2026","externalUrl":null,"permalink":"/fr/tags/homelab/","section":"Tags","summary":"","title":"Homelab","type":"tags"},{"content":"","date":"5 mai 2026","externalUrl":null,"permalink":"/fr/tags/prometheus/","section":"Tags","summary":"","title":"Prometheus","type":"tags"},{"content":" Le problème # Après avoir passé des mois à exécuter des conteneurs sur mon serveur domestique, j\u0026rsquo;en ai eu assez de découvrir des problèmes quand les choses étaient déjà cassées. Un conteneur consommant toute la mémoire. Un volume plein sans avertissement. J\u0026rsquo;avais besoin d\u0026rsquo;une vraie visibilité sur ce qui se passait dans mon infrastructure.\nJ\u0026rsquo;ai décidé de mettre en place une stack de monitoring avec Prometheus et Grafana. Je documente ici exactement comment j\u0026rsquo;ai procédé.\nArchitecture choisie # Prometheus : collecte les métriques de Docker cAdvisor : expose les métriques des conteneurs Grafana : visualise tout dans les dashboards Alertmanager : notifie quand quelque chose échoue Étape 1 : Docker Compose avec la stack complète # J\u0026rsquo;ai créé un fichier docker-compose.yml qui démarre le tout :\nversion: \u0026#39;3.8\u0026#39; services: prometheus: image: prom/prometheus:latest container_name: prometheus volumes: - ./prometheus.yml:/etc/prometheus/prometheus.yml - ./alertas.yml:/etc/prometheus/alertas.yml - prometheus_data:/prometheus ports: - \u0026#34;9090:9090\u0026#34; command: - \u0026#39;--config.file=/etc/prometheus/prometheus.yml\u0026#39; - \u0026#39;--storage.tsdb.path=/prometheus\u0026#39; networks: - monitoring cadvisor: image: gcr.io/cadvisor/cadvisor:latest container_name: cadvisor volumes: - /:/rootfs:ro - /var/run:/var/run:ro - /sys:/sys:ro - /var/lib/docker/:/var/lib/docker:ro ports: - \u0026#34;8080:8080\u0026#34; networks: - monitoring grafana: image: grafana/grafana:latest container_name: grafana ports: - \u0026#34;3000:3000\u0026#34; volumes: - grafana_data:/var/lib/grafana environment: - GF_SECURITY_ADMIN_PASSWORD=admin networks: - monitoring alertmanager: image: prom/alertmanager:latest container_name: alertmanager volumes: - ./alertmanager.yml:/etc/alertmanager/alertmanager.yml - alertmanager_data:/alertmanager ports: - \u0026#34;9093:9093\u0026#34; networks: - monitoring volumes: prometheus_data: grafana_data: alertmanager_data: networks: monitoring: driver: bridge Étape 2 : Configurer Prometheus # Fichier prometheus.yml :\nglobal: scrape_interval: 15s evaluation_interval: 15s alerting: alertmanagers: - static_configs: - targets: - alertmanager:9093 rule_files: - \u0026#39;/etc/prometheus/alertas.yml\u0026#39; scrape_configs: - job_name: \u0026#39;cadvisor\u0026#39; static_configs: - targets: [\u0026#39;cadvisor:8080\u0026#39;] - job_name: \u0026#39;prometheus\u0026#39; static_configs: - targets: [\u0026#39;localhost:9090\u0026#39;] Étape 3 : Définir les alertes # Fichier alertas.yml :\ngroups: - name: docker_alerts interval: 10s rules: - alert: HighCPUUsage expr: \u0026#39;rate(container_cpu_usage_seconds_total[5m]) \u0026gt; 0.8\u0026#39; for: 2m annotations: summary: \u0026#34;CPU alta en contenedor {{ $labels.name }}\u0026#34; description: \u0026#34;{{ $labels.name }} está usando {{ $value | humanizePercentage }} de CPU\u0026#34; - alert: HighMemoryUsage expr: \u0026#39;container_memory_usage_bytes / container_spec_memory_limit_bytes \u0026gt; 0.85\u0026#39; for: 2m annotations: summary: \u0026#34;Memoria alta en {{ $labels.name }}\u0026#34; description: \u0026#34;Uso de memoria: {{ $value | humanizePercentage }}\u0026#34; - alert: DiskSpaceRunningOut expr: \u0026#39;node_filesystem_avail_bytes{mountpoint=\u0026#34;/\u0026#34;} / node_filesystem_size_bytes{mountpoint=\u0026#34;/\u0026#34;} \u0026lt; 0.1\u0026#39; for: 5m annotations: summary: \u0026#34;Espacio en disco por debajo del 10%\u0026#34; Étape 4 : Configurer Alertmanager # Fichier alertmanager.yml :\nglobal: resolve_timeout: 5m route: receiver: \u0026#39;console\u0026#39; group_by: [\u0026#39;alertname\u0026#39;] group_wait: 10s group_interval: 10s repeat_interval: 1h receivers: - name: \u0026#39;console\u0026#39; webhook_configs: - url: \u0026#39;http://localhost:5001/\u0026#39; Étape 5 : Démarrer et vérifier # docker-compose up -d Accès :\nPrometheus : http://localhost:9090 Grafana : http://localhost:3000 cAdvisor : http://localhost:8080 Étape 6 : Créer des dashboards dans Grafana # Dans Grafana j\u0026rsquo;ai importé le dashboard public 893 (Docker and Host Monitoring) qui fonctionne directement avec cAdvisor.\nRésultat # J\u0026rsquo;ai maintenant une visibilité complète. Je reçois des alertes quand :\nUn conteneur consomme plus de 80% de CPU pendant 2 minutes La mémoire dépasse 85% de la limite Le disque tombe en dessous de 10% La setup complète occupe moins de 500MB de RAM au repos et m\u0026rsquo;a déjà évité plusieurs frayeurs. Ça en vaut la peine.\nÉquipement recommandé # Raspberry Pi 3 B+ — Serveur léger à faible consommation pour commencer ton homelab Raspberry Pi 4 (4GB) — La base parfaite pour homelab, Docker et monitoring Liens d\u0026rsquo;affiliation. Aucun coût supplémentaire pour toi.\n","date":"5 mai 2026","externalUrl":null,"permalink":"/fr/posts/monitorizacion-de-contenedores-docker-con-prometheus-y-grafana-alertas-automaticas-en-casa/","section":"Posts","summary":"Le problème # Après avoir passé des mois à exécuter des conteneurs sur mon serveur domestique, j’en ai eu assez de découvrir des problèmes quand les choses étaient déjà cassées. Un conteneur consommant toute la mémoire. Un volume plein sans avertissement. J’avais besoin d’une vraie visibilité sur ce qui se passait dans mon infrastructure.\nJ’ai décidé de mettre en place une stack de monitoring avec Prometheus et Grafana. Je documente ici exactement comment j’ai procédé.\n","title":"Surveillance des conteneurs Docker avec Prometheus et Grafana : alertes automatiques à domicile","type":"posts"},{"content":"","date":"4 mai 2026","externalUrl":null,"permalink":"/fr/tags/auth/","section":"Tags","summary":"","title":"Auth","type":"tags"},{"content":" Le problème # Il y a quelques jours, j\u0026rsquo;ai tenté d\u0026rsquo;automatiser l\u0026rsquo;envoi de courriels depuis mon serveur domestique. Rien de compliqué : un script Python avec smtplib pour envoyer des notifications. Le problème est arrivé quand j\u0026rsquo;ai configuré un mot de passe avec des caractères spéciaux : MiPasw0rd$Ñ.\nimport smtplib server = smtplib.SMTP(\u0026#39;mail.example.com\u0026#39;, 587) server.starttls() server.login(\u0026#39;usuario@example.com\u0026#39;, \u0026#39;MiPasw0rd$Ñ\u0026#39;) # Error 535 Résultat : SMTPAuthenticationError: (535, b'5.7.8 Authentication credentials invalid')\nL\u0026rsquo;étrange, c\u0026rsquo;est que le mot de passe était correct. J\u0026rsquo;y accédais manuellement sans problème. L\u0026rsquo;erreur 535 suggérait des échecs d\u0026rsquo;authentification, mais le vrai problème était dans l\u0026rsquo;encodage.\nPourquoi ça échoue # C\u0026rsquo;est la faute de la gestion de l\u0026rsquo;encodage dans smtplib. La méthode login() utilise par défaut UTF-8, mais applique ensuite des transformations qui ne respectent pas toujours correctement les caractères spéciaux, particulièrement quand le serveur ou la libraire ont des configurations héritées.\nDans mon cas, le serveur SMTP s\u0026rsquo;attendait à un encodage UTF-8 cohérent. Quand smtplib traitait le mot de passe avec $ et Ñ, quelque chose se corrompait en chemin.\nLa solution : AUTH LOGIN manuel # Le protocole AUTH LOGIN est simple : on encode l\u0026rsquo;utilisateur et le mot de passe en base64 séparément et on les envoie par étapes manuelles. Cela te donne un contrôle total sur l\u0026rsquo;encodage.\nimport smtplib import base64 def auth_login_manual(server, usuario, contraseña): \u0026#34;\u0026#34;\u0026#34;Implementa AUTH LOGIN manualmente con UTF-8\u0026#34;\u0026#34;\u0026#34; # Respuesta inicial del servidor code, response = server.docmd(\u0026#34;AUTH LOGIN\u0026#34;) # Codificar usuario en base64 UTF-8 usuario_b64 = base64.b64encode(usuario.encode(\u0026#39;utf-8\u0026#39;)).decode(\u0026#39;ascii\u0026#39;) code, response = server.docmd(usuario_b64) # Codificar contraseña en base64 UTF-8 contraseña_b64 = base64.b64encode(contraseña.encode(\u0026#39;utf-8\u0026#39;)).decode(\u0026#39;ascii\u0026#39;) code, response = server.docmd(contraseña_b64) # Verificar éxito (código 235 es autenticación exitosa) if code != 235: raise Exception(f\u0026#34;Autenticación fallida: {code} {response}\u0026#34;) return True # Uso server = smtplib.SMTP(\u0026#39;mail.example.com\u0026#39;, 587) server.starttls() auth_login_manual(server, \u0026#39;usuario@example.com\u0026#39;, \u0026#39;MiPasw0rd$Ñ\u0026#39;) print(\u0026#34;Autenticado exitosamente\u0026#34;) Détail du protocole # AUTH LOGIN : Le client demande d\u0026rsquo;utiliser le mécanisme AUTH LOGIN Utilisateur en base64 : Le serveur répond avec 334, attend l\u0026rsquo;utilisateur encodé Mot de passe en base64 : Le serveur répond avec 334, attend le mot de passe encodé Réponse 235 : Indique une authentification réussie La méthode docmd() envoie des commandes SMTP brutes et retourne le code de réponse et le message.\nTest sur mon serveur # J\u0026rsquo;ai implémenté cela dans mon infrastructure domestique avec Postfix. La différence est notable :\n# Antes: falla con caracteres especiales server.login(\u0026#39;admin@local.home\u0026#39;, \u0026#39;P@ssw0rdÑ\u0026#39;) # Error 535 # Después: funciona perfectamente auth_login_manual(server, \u0026#39;admin@local.home\u0026#39;, \u0026#39;P@ssw0rdÑ\u0026#39;) # OK Considérations de sécurité # Base64 n\u0026rsquo;est pas du chiffrement : utilise toujours STARTTLS ou une connexion directe au port 465 avec SSL L\u0026rsquo;encodage UTF-8 est sûr pour les caractères spéciaux Cette méthode est compatible avec n\u0026rsquo;importe quel serveur SMTP qui supporte AUTH LOGIN Script complet # import smtplib import base64 class SMTPConnection: def __init__(self, host, port=587): self.server = smtplib.SMTP(host, port) self.server.starttls() def login(self, usuario, contraseña): self.server.docmd(\u0026#34;AUTH LOGIN\u0026#34;) self.server.docmd(base64.b64encode(usuario.encode(\u0026#39;utf-8\u0026#39;)).decode(\u0026#39;ascii\u0026#39;)) code, resp = self.server.docmd(base64.b64encode(contraseña.encode(\u0026#39;utf-8\u0026#39;)).decode(\u0026#39;ascii\u0026#39;)) if code != 235: raise Exception(f\u0026#34;Auth failed: {code}\u0026#34;) def send(self, de, para, asunto, cuerpo): msg = f\u0026#34;From: {de}\\nTo: {para}\\nSubject: {asunto}\\n\\n{cuerpo}\u0026#34; self.server.sendmail(de, para, msg) def close(self): self.server.quit() # Uso smtp = SMTPConnection(\u0026#39;mail.example.com\u0026#39;) smtp.login(\u0026#39;usuario@example.com\u0026#39;, \u0026#39;MiPasw0rd$Ñ\u0026#39;) smtp.send(\u0026#39;usuario@example.com\u0026#39;, \u0026#39;destino@example.com\u0026#39;, \u0026#39;Test\u0026#39;, \u0026#39;Funciona!\u0026#39;) smtp.close() Conclusion # Si ton serveur SMTP rejette les mots de passe avec des caractères spéciaux, ce n\u0026rsquo;est pas un mystère. C\u0026rsquo;est un problème d\u0026rsquo;encodage. Implémenter AUTH LOGIN manuellement te donne un contrôle total et fonctionne avec n\u0026rsquo;importe quel mot de passe.\nJe l\u0026rsquo;ai appliqué en production sur mon serveur domestique et je n\u0026rsquo;ai eu aucun problème depuis.\nÉquipement recommandé # Raspberry Pi 3 B+ — Serveur léger et peu gourmand en énergie pour débuter ton homelab Raspberry Pi 4 (4GB) — La base parfaite pour homelab, Docker et surveillance Liens d\u0026rsquo;affiliation. Aucun coût supplémentaire pour toi.\n","date":"4 mai 2026","externalUrl":null,"permalink":"/fr/posts/auth-login-manual-en-python-con-smtplib-caracteres-especiales-y-error-535/","section":"Posts","summary":"Le problème # Il y a quelques jours, j’ai tenté d’automatiser l’envoi de courriels depuis mon serveur domestique. Rien de compliqué : un script Python avec smtplib pour envoyer des notifications. Le problème est arrivé quand j’ai configuré un mot de passe avec des caractères spéciaux : MiPasw0rd$Ñ.\nimport smtplib server = smtplib.SMTP('mail.example.com', 587) server.starttls() server.login('usuario@example.com', 'MiPasw0rd$Ñ') # Error 535 Résultat : SMTPAuthenticationError: (535, b'5.7.8 Authentication credentials invalid')\n","title":"AUTH LOGIN manual en Python con smtplib: caracteres especiales y error 535","type":"posts"},{"content":"","date":"4 mai 2026","externalUrl":null,"permalink":"/fr/tags/cron/","section":"Tags","summary":"","title":"Cron","type":"tags"},{"content":"","date":"4 mai 2026","externalUrl":null,"permalink":"/fr/tags/disaster-recovery/","section":"Tags","summary":"","title":"Disaster-Recovery","type":"tags"},{"content":"","date":"4 mai 2026","externalUrl":null,"permalink":"/fr/tags/docker-compose/","section":"Tags","summary":"","title":"Docker-Compose","type":"tags"},{"content":"","date":"4 mai 2026","externalUrl":null,"permalink":"/fr/tags/encoding/","section":"Tags","summary":"","title":"Encoding","type":"tags"},{"content":"","date":"4 mai 2026","externalUrl":null,"permalink":"/fr/tags/excel/","section":"Tags","summary":"","title":"Excel","type":"tags"},{"content":"","date":"4 mai 2026","externalUrl":null,"permalink":"/fr/tags/fastapi/","section":"Tags","summary":"","title":"Fastapi","type":"tags"},{"content":"J\u0026rsquo;ai travaillé avec des feuilles de calcul Excel provenant de différents départements. Chacune utilise des noms de colonne distincts, les données sont sales (téléphones avec notes, NIFs mélangés avec du texte), et les codes ont des formats incohérents. Je documente ici la solution que j\u0026rsquo;ai construite.\nLe problème réel # Je recevais des fichiers Excel où :\nLes colonnes s\u0026rsquo;appelaient \u0026ldquo;NIF\u0026rdquo; dans l\u0026rsquo;un, \u0026ldquo;CIF\u0026rdquo; dans un autre, \u0026ldquo;Identification\u0026rdquo; dans le troisième Les téléphones ressemblaient à \u0026ldquo;123-456-7890 (ext 5)\u0026rdquo;, \u0026ldquo;9876543210 - non disponible\u0026rdquo; Les NIFs avec des tirets, des espaces et des lettres variées Les codes de produit avec des préfixes incohérents Je ne pouvais pas attendre que chaque personne formate de la même façon. J\u0026rsquo;avais besoin d\u0026rsquo;un système flexible.\nArchitecture de la solution # Mon approche utilise trois couches :\nDétection de colonnes par regex (trouve \u0026ldquo;nif\u0026rdquo;, \u0026ldquo;cif\u0026rdquo;, \u0026ldquo;identificaci\u0026rdquo; avec fuzzy matching) Nettoyeurs spécialisés pour chaque type de données Validation et logging pour audit Implémentation étape par étape # 1. Charger le fichier et détecter les colonnes # from openpyxl import load_workbook import re def detectar_columnas(archivo_excel): wb = load_workbook(archivo_excel) ws = wb.active encabezados = [cell.value for cell in ws[1]] mapa_columnas = { \u0026#39;nif\u0026#39;: detectar_columna(encabezados, r\u0026#39;nif|cif|identificaci\u0026#39;), \u0026#39;telefono\u0026#39;: detectar_columna(encabezados, r\u0026#39;telef|phone|contacto\u0026#39;), \u0026#39;nombre\u0026#39;: detectar_columna(encabezados, r\u0026#39;nombre|name|razón\u0026#39;), \u0026#39;codigo\u0026#39;: detectar_columna(encabezados, r\u0026#39;código|code|articulo\u0026#39;) } return wb, ws, mapa_columnas def detectar_columna(encabezados, patron): for idx, encabezado in enumerate(encabezados): if encabezado and re.search(patron, str(encabezado).lower()): return idx return None 2. Nettoyer et valider NIF/CIF # class LimpiadorNIF: PATRON_NIF = r\u0026#39;^([0-9]{8}[A-Z]|[XYZ][0-9]{7}[A-Z])$\u0026#39; @staticmethod def limpiar(valor): if not valor: return None # Extraer solo números y letras limpio = re.sub(r\u0026#39;[^A-Z0-9]\u0026#39;, \u0026#39;\u0026#39;, str(valor).upper()) # Si tiene más de 9 caracteres, puede ser NIF + texto match = re.search(r\u0026#39;([0-9]{8}[A-Z]|[XYZ][0-9]{7}[A-Z])\u0026#39;, limpio) if match: return match.group(1) return None if not re.match(LimpiadorNIF.PATRON_NIF, limpio) else limpio 3. Extraire les téléphones \u0026ldquo;propres\u0026rdquo; # class LimpiadorTelefono: @staticmethod def limpiar(valor): if not valor: return None, None texto = str(valor) # Extraer solo números (mínimo 9 dígitos) numeros = re.sub(r\u0026#39;\\D\u0026#39;, \u0026#39;\u0026#39;, texto) telefono = numeros[-9:] if len(numeros) \u0026gt;= 9 else None # Detectar notas (texto entre paréntesis o después de guiones) notas = re.search(r\u0026#39;(\\([^)]+\\)|-.+)\u0026#39;, texto) nota = notas.group(1).strip() if notas else None return telefono, nota 4. Normaliser les codes avec préfixes variables # class LimpiadorCodigo: @staticmethod def limpiar(valor, prefijo_esperado=\u0026#39;PRD\u0026#39;): if not valor: return None # Convertir a mayúsculas y eliminar espacios limpio = str(valor).upper().strip() # Extraer la parte numérica match = re.search(r\u0026#39;([A-Z]*)?(\\d+)\u0026#39;, limpio) if match: numero = match.group(2) # Garantizar formato consistente return f\u0026#34;{prefijo_esperado}{numero.zfill(6)}\u0026#34; return None 5. Traiter le fichier complet # def importar_excel(ruta_archivo): wb, ws, columnas = detectar_columnas(ruta_archivo) registros = [] errores = [] for fila_idx, fila in enumerate(ws.iter_rows(min_row=2, values_only=False), start=2): registro = {} try: if columnas[\u0026#39;nif\u0026#39;] is not None: nif_raw = fila[columnas[\u0026#39;nif\u0026#39;]].value registro[\u0026#39;nif\u0026#39;] = LimpiadorNIF.limpiar(nif_raw) if columnas[\u0026#39;telefono\u0026#39;] is not None: tel_raw = fila[columnas[\u0026#39;telefono\u0026#39;]].value tel, nota = LimpiadorTelefono.limpiar(tel_raw) registro[\u0026#39;telefono\u0026#39;] = tel registro[\u0026#39;nota_telefono\u0026#39;] = nota if columnas[\u0026#39;codigo\u0026#39;] is not None: cod_raw = fila[columnas[\u0026#39;codigo\u0026#39;]].value registro[\u0026#39;codigo\u0026#39;] = LimpiadorCodigo.limpiar(cod_raw) registros.append(registro) except Exception as e: errores.append(f\u0026#34;Fila {fila_idx}: {str(e)}\u0026#34;) return registros, errores Utilisation en production # registros, errores = importar_excel(\u0026#39;ventas_marzo.xlsx\u0026#39;) if errores: print(f\u0026#34;⚠️ {len(errores)} errores encontrados:\u0026#34;) for error in errores[:5]: print(f\u0026#34; - {error}\u0026#34;) print(f\u0026#34;✓ {len(registros)} registros procesados correctamente\u0026#34;) Leçons apprises # Les regex sont le modèle d\u0026rsquo;or pour les données sales Toujours retourner la donnée originale + nettoyée pour audit Le log des erreurs spécifiques par ligne facilite le débogage La détection flexible des colonnes a économisé des heures de support Ce système est en production depuis 6 mois. J\u0026rsquo;ai dû ajouter seulement 2 nettoyeurs supplémentaires. La clé est de garder chaque nettoyeur indépendant.\nÉquipement recommandé # Raspberry Pi 3 B+ — Serveur léger et économe pour débuter votre homelab Raspberry Pi 4 (4GB) — La base parfaite pour homelab, Docker et monitoring Liens d\u0026rsquo;affiliation. Aucun coût supplémentaire pour vous.\n","date":"4 mai 2026","externalUrl":null,"permalink":"/fr/posts/importacion-inteligente-de-excel-en-python-deteccion-flexible-de-columnas-y-limpieza-de-datos-heterogeneos/","section":"Posts","summary":"J’ai travaillé avec des feuilles de calcul Excel provenant de différents départements. Chacune utilise des noms de colonne distincts, les données sont sales (téléphones avec notes, NIFs mélangés avec du texte), et les codes ont des formats incohérents. Je documente ici la solution que j’ai construite.\nLe problème réel # Je recevais des fichiers Excel où :\nLes colonnes s’appelaient “NIF” dans l’un, “CIF” dans un autre, “Identification” dans le troisième Les téléphones ressemblaient à “123-456-7890 (ext 5)”, “9876543210 - non disponible” Les NIFs avec des tirets, des espaces et des lettres variées Les codes de produit avec des préfixes incohérents Je ne pouvais pas attendre que chaque personne formate de la même façon. J’avais besoin d’un système flexible.\n","title":"Importation intelligente d'Excel en Python : Détection flexible des colonnes et nettoyage des données hétérogènes","type":"posts"},{"content":"","date":"4 mai 2026","externalUrl":null,"permalink":"/fr/tags/infraestructura/","section":"Tags","summary":"","title":"Infraestructura","type":"tags"},{"content":" Le problème que personne ne voit # Il y a deux mois, j\u0026rsquo;ai déployé une API FastAPI en production et le serveur se comportait étrangement. CPU stable à 40-50% sans raison apparente. J\u0026rsquo;ai pensé que c\u0026rsquo;était une fuite mémoire, que c\u0026rsquo;étaient les logs, que c\u0026rsquo;était la base de données. C\u0026rsquo;était --reload.\nIl s\u0026rsquo;avère que copier-coller la commande de développement directement dans le conteneur Docker est plus courant que cela ne devrait l\u0026rsquo;être. Et oui, uvicorn avec --reload fonctionne. Le serveur répond. Les requêtes vont vite. Mais il y a un coût que tu ne vois pas jusqu\u0026rsquo;à ce que tu aies 10K requêtes par jour.\nQu\u0026rsquo;est-ce que le file-watcher exactement # Le flag --reload d\u0026rsquo;uvicorn lance un processus supplémentaire qui surveille tous les fichiers Python de ton projet. Pas seulement ton code. Tous.\nQuand tu actives --reload, uvicorn :\nDémarre un gestionnaire de processus (watchfiles par défaut) Chaque X secondes (par défaut 0.4s) scanne tous les .py du répertoire Calcule les checksums ou hashs de chaque fichier S\u0026rsquo;il détecte des changements, redémarre le worker complet Pendant ce temps, continue à scanner à chaque cycle, même sans changements Ce scan n\u0026rsquo;est pas gratuit. Dans un projet moyen avec 200 fichiers Python distribués dans vendor, les librairies et les modules propres, chaque cycle de watchfiles touche le disque et le CPU.\nComment il consomme du CPU à chaque requête # La partie criminelle est que le file-watcher ne s\u0026rsquo;interrompt pas pendant les requêtes. Tandis que ton API traite une demande :\nLe moniteur continue à scanner les fichiers en arrière-plan Il entre en concurrence pour l\u0026rsquo;I/O du disque avec ton application Dans les conteneurs, si tu n\u0026rsquo;as pas de limites de ressources, il peut finir par consommer plus de CPU que la logique même de la requête J\u0026rsquo;ai mesuré cela sur mon serveur. Avec une requête simple à un endpoint qui prend 50ms :\nSans --reload :\nCPU: 2-3% por request I/O wait: \u0026lt;1% Avec --reload :\nCPU: 8-12% por request I/O wait: 3-5% Cela ne semble pas beaucoup sur une requête isolée. Mais avec 100 requêtes concurrentes, cet overhead se multiplie.\nComment le détecter sur ton serveur # 1. Regarde les processus en exécution # ps aux | grep uvicorn Si tu vois deux processus uvicorn (ou un uvicorn + watchfiles), tu as --reload actif.\n2. Vérifie ta commande de démarrage # # MAL - esto es lo que probablemente tienes uvicorn main:app --host 0.0.0.0 --port 8000 --reload # BIEN - así debe estar en producción uvicorn main:app --host 0.0.0.0 --port 8000 3. Surveille le CPU pendant un test de charge # # Terminal 1: corre tu servidor docker run -it tu-contenedor # Terminal 2: genera carga ab -n 1000 -c 10 http://localhost:8000/endpoint Observe top ou docker stats. Si tu vois des pics inexplicables, soupçonne --reload.\n4. Vérifie les logs d\u0026rsquo;uvicorn # Avec --reload actif, tu verras des messages comme :\nINFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit) INFO: Will watch for changes in these directories: ... Si tu vois \u0026ldquo;Will watch for changes\u0026rdquo;, tu as un problème.\nLa solution (c\u0026rsquo;est évident, mais important) # Dans Docker, assure-toi que ton Dockerfile n\u0026rsquo;utilise PAS --reload :\nFROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install -r requirements.txt COPY . . # Aquí no va --reload CMD [\u0026#34;uvicorn\u0026#34;, \u0026#34;main:app\u0026#34;, \u0026#34;--host\u0026#34;, \u0026#34;0.0.0.0\u0026#34;, \u0026#34;--port\u0026#34;, \u0026#34;8000\u0026#34;, \u0026#34;--workers\u0026#34;, \u0026#34;4\u0026#34;] Pour le développement local, utilise --reload sans culpabilité :\nuvicorn main:app --reload Ce que j\u0026rsquo;ai appris # Le file-watcher d\u0026rsquo;uvicorn est excellent pour le développement. C\u0026rsquo;est transparent, cela fonctionne bien et cela accélère le cycle. Mais en production, c\u0026rsquo;est comme laisser l\u0026rsquo;ordinateur scanner avec un antivirus en continu.\nJ\u0026rsquo;ai examiné mes autres conteneurs après cela. J\u0026rsquo;en ai trouvé trois autres avec --reload actif. Après l\u0026rsquo;avoir supprimé, la consommation de CPU a baissé entre 30-45%.\nC\u0026rsquo;est l\u0026rsquo;un de ces bugs qui n\u0026rsquo;en est pas vraiment un. Ton application fonctionne. Les requêtes sont traitées. Mais ton serveur effectue un travail invisible dont il n\u0026rsquo;a pas besoin.\nLa prochaine fois avant de déployer en production, grep pour --reload. Cela t\u0026rsquo;évitera une session de dépannage.\nÉquipement recommandé # Raspberry Pi 3 B+ — Serveur léger à faible consommation pour démarrer ton homelab Raspberry Pi 4 (4GB) — La base parfaite pour homelab, Docker et monitoring Liens d\u0026rsquo;affiliation. Aucun coût supplémentaire pour toi.\n","date":"4 mai 2026","externalUrl":null,"permalink":"/fr/posts/el-coste-oculto-de---reload-en-uvicorn-que-consume-cpu-realmente-en-produccion/","section":"Posts","summary":"Le problème que personne ne voit # Il y a deux mois, j’ai déployé une API FastAPI en production et le serveur se comportait étrangement. CPU stable à 40-50% sans raison apparente. J’ai pensé que c’était une fuite mémoire, que c’étaient les logs, que c’était la base de données. C’était --reload.\nIl s’avère que copier-coller la commande de développement directement dans le conteneur Docker est plus courant que cela ne devrait l’être. Et oui, uvicorn avec --reload fonctionne. Le serveur répond. Les requêtes vont vite. Mais il y a un coût que tu ne vois pas jusqu’à ce que tu aies 10K requêtes par jour.\n","title":"Le coût caché de --reload dans uvicorn : ce qui consomme vraiment du CPU en production","type":"posts"},{"content":" Pourquoi j\u0026rsquo;ai abandonné cron # J\u0026rsquo;utilise cron sur mes serveurs depuis des années. C\u0026rsquo;est simple, fiable et ça marche. Mais récemment j\u0026rsquo;ai découvert systemd timers et je ne reviens pas en arrière. La raison principale : logs intégrés dans journald, sans fichiers .log qui traînent partout dans le système, et un meilleur contrôle sur ce qui se passe quand le serveur démarre ou redémarre.\nDans mon cas spécifique, j\u0026rsquo;avais une sauvegarde qui ne s\u0026rsquo;exécutait pas si le serveur était arrêté à l\u0026rsquo;heure programmée. Avec cron, elle était simplement perdue. Avec systemd timers et Persistent=true, la tâche s\u0026rsquo;exécute dès que le serveur s\u0026rsquo;allume.\nStructure basique : .service + .timer # Systemd nécessite deux fichiers :\nLe service (/etc/systemd/system/mibackup.service) :\n[Unit] Description=Backup diario de datos After=network.target [Service] Type=oneshot User=backup ExecStart=/usr/local/bin/backup.sh StandardOutput=journal StandardError=journal Le timer (/etc/systemd/system/mibackup.timer) :\n[Unit] Description=Ejecuta backup diariamente [Timer] OnCalendar=daily OnCalendar=*-*-* 03:00:00 Persistent=true Accuracy=1min [Install] WantedBy=timers.target Puis :\nsudo systemctl daemon-reload sudo systemctl enable --now mibackup.timer OnCalendar : la syntaxe dont tu as besoin # OnCalendar est le cron de systemd, mais plus lisible :\ndaily → chaque jour à minuit weekly → chaque lundi à minuit hourly → chaque heure *-*-* 03:00:00 → chaque jour à 3 h du matin Mon *-*-* 14:30:00 → chaque lundi à 14:30 *-01,04,07,10-01 00:00:00 → premier jour de chaque trimestre Tu peux combiner plusieurs lignes OnCalendar :\nOnCalendar=*-*-* 03:00:00 OnCalendar=*-*-* 15:00:00 Ceci exécute la tâche deux fois par jour.\nPersistent=true : le changement qui m\u0026rsquo;a convaincu # Par défaut, si ton serveur est arrêté quand une tâche est programmée, systemd l\u0026rsquo;ignore simplement. Avec Persistent=true, systemd se souvient et exécute la tâche la prochaine fois qu\u0026rsquo;il démarre.\nSur mon serveur domestique, c\u0026rsquo;est critique. Il n\u0026rsquo;est pas toujours allumé, et j\u0026rsquo;ai besoin de garantir que mes sauvegardes s\u0026rsquo;exécutent même si des heures ont passé.\n[Timer] OnCalendar=daily Persistent=true Type=oneshot : pour les tâches qui se terminent # Le paramètre Type=oneshot dans le .service indique que le processus se terminera. C\u0026rsquo;est normal pour les scripts de sauvegarde, synchronisation, etc.\nSi tu utilises Type=simple (la valeur par défaut), systemd s\u0026rsquo;attend à ce que le processus continue à s\u0026rsquo;exécuter. Ce n\u0026rsquo;est pas ce que nous voulons ici.\nVoir les logs sans fichiers externes # Voilà le meilleur : oublie \u0026gt;\u0026gt; /var/log/mibackup.log.\nLes logs vont directement dans journald :\n# Ver los últimos logs del timer journalctl -u mibackup.service -n 50 # Ver en tiempo real journalctl -u mibackup.service -f # Los últimos 2 horas journalctl -u mibackup.service --since \u0026#34;2 hours ago\u0026#34; # Con niveles de prioridad journalctl -u mibackup.service -p err Dans ton script tu peux logger avec echo ou logger :\n#!/bin/bash logger \u0026#34;Iniciando backup...\u0026#34; /backup/script.sh logger \u0026#34;Backup completado\u0026#34; Tout est capturé automatiquement.\nListe de contrôle de ce que nous avons fait # ✅ Tu as créé un .service avec Type=oneshot ✅ Tu as créé un .timer avec OnCalendar et Persistent=true ✅ Tu as rechargé systemd et activé le timer ✅ Tu vérifies les logs avec journalctl, sans fichiers de log supplémentaires Conseil final # Avant de faire confiance à un timer, teste-le manuellement :\n# Ejecutar el servicio ahora sudo systemctl start mibackup.service # Ver qué pasó journalctl -u mibackup.service -n 20 Voilà. Systemd timers n\u0026rsquo;est pas la panacée, mais pour les tâches programmées sur les serveurs modernes, c\u0026rsquo;est supérieur à cron sur presque tous les aspects. Les logs centralisés dans journald, combinés avec Persistent=true, font qu\u0026rsquo;il est impossible de ne pas le recommander.\nSur mon serveur domestique, toutes les sauvegardes, nettoyages de cache et synchronisations de données utilisent des timers. Zéro fichiers de log qui traînent. Tout intégré.\nÉquipement recommandé # Raspberry Pi 3 B+ — Serveur léger à faible consommation pour commencer ton homelab Mini PC Intel N100 — Mini PC silencieux et efficace pour serveur domestique 24/7 Liens d\u0026rsquo;affiliation. Aucun coût supplémentaire pour toi.\n","date":"4 mai 2026","externalUrl":null,"permalink":"/fr/posts/systemd-timers-la-alternativa-moderna-a-cron-que-necesitabas/","section":"Posts","summary":"Pourquoi j’ai abandonné cron # J’utilise cron sur mes serveurs depuis des années. C’est simple, fiable et ça marche. Mais récemment j’ai découvert systemd timers et je ne reviens pas en arrière. La raison principale : logs intégrés dans journald, sans fichiers .log qui traînent partout dans le système, et un meilleur contrôle sur ce qui se passe quand le serveur démarre ou redémarre.\n","title":"Les minuteurs Systemd : l'alternative moderne à cron dont tu avais besoin","type":"posts"},{"content":"","date":"4 mai 2026","externalUrl":null,"permalink":"/fr/tags/ntlm/","section":"Tags","summary":"","title":"Ntlm","type":"tags"},{"content":"","date":"4 mai 2026","externalUrl":null,"permalink":"/fr/tags/openpyxl/","section":"Tags","summary":"","title":"Openpyxl","type":"tags"},{"content":"","date":"4 mai 2026","externalUrl":null,"permalink":"/fr/tags/performance/","section":"Tags","summary":"","title":"Performance","type":"tags"},{"content":"","date":"4 mai 2026","externalUrl":null,"permalink":"/fr/tags/procesamiento-datos/","section":"Tags","summary":"","title":"Procesamiento-Datos","type":"tags"},{"content":"","date":"4 mai 2026","externalUrl":null,"permalink":"/fr/tags/producci%C3%B3n/","section":"Tags","summary":"","title":"Producción","type":"tags"},{"content":"","date":"4 mai 2026","externalUrl":null,"permalink":"/fr/tags/python/","section":"Tags","summary":"","title":"Python","type":"tags"},{"content":"","date":"4 mai 2026","externalUrl":null,"permalink":"/fr/tags/regex/","section":"Tags","summary":"","title":"Regex","type":"tags"},{"content":"","date":"4 mai 2026","externalUrl":null,"permalink":"/fr/tags/rsync/","section":"Tags","summary":"","title":"Rsync","type":"tags"},{"content":" Le problème # J\u0026rsquo;ai récemment perdu un disque dur sans avertissement. Ce n\u0026rsquo;était pas catastrophique car j\u0026rsquo;avais des sauvegardes, mais cela m\u0026rsquo;a rendu conscient que beaucoup de passionnés avec des serveurs domestiques n\u0026rsquo;ont aucune stratégie de protection des données. Si ton serveur Docker s\u0026rsquo;arrête demain, combien de temps te faudrait-il pour le récupérer ?\nDans cet article, je partage comment j\u0026rsquo;ai automatisé les sauvegardes de mon infrastructure Docker en utilisant rsync et cron. C\u0026rsquo;est simple, efficace et ça fonctionne.\nLa stratégie # Mon approche est straightforward :\nrsync pour synchroniser de manière incrémentale uniquement ce qui a changé cron pour automatiser l\u0026rsquo;exécution quotidienne Un disque USB externe comme destination de sauvegarde La rétention de plusieurs snapshots pour une récupération granulaire Ce n\u0026rsquo;est pas une sauvegarde en cloud. C\u0026rsquo;est une sauvegarde locale, rapide et sous mon contrôle.\nConfiguration étape par étape # 1. Préparer le stockage # J\u0026rsquo;ai connecté un disque USB et l\u0026rsquo;ai monté sur /mnt/backup. Vérifiez qu\u0026rsquo;il soit disponible :\nlsblk mount | grep backup 2. Script de sauvegarde # J\u0026rsquo;ai créé /usr/local/bin/docker-backup.sh :\n#!/bin/bash BACKUP_DEST=\u0026#34;/mnt/backup\u0026#34; DOCKER_DATA=\u0026#34;/var/lib/docker\u0026#34; COMPOSE_DIR=\u0026#34;/home/user/docker-compose\u0026#34; TIMESTAMP=$(date +%Y%m%d_%H%M%S) BACKUP_PATH=\u0026#34;$BACKUP_DEST/backups/$TIMESTAMP\u0026#34; # Crear directorio de backup mkdir -p \u0026#34;$BACKUP_PATH\u0026#34; # Backup de volúmenes Docker echo \u0026#34;[$(date)] Iniciando backup de Docker volumes...\u0026#34; rsync -av --delete \u0026#34;$DOCKER_DATA/volumes/\u0026#34; \u0026#34;$BACKUP_PATH/volumes/\u0026#34; \u0026gt;\u0026gt; /var/log/docker-backup.log 2\u0026gt;\u0026amp;1 # Backup de configuraciones docker-compose echo \u0026#34;[$(date)] Iniciando backup de docker-compose...\u0026#34; rsync -av \u0026#34;$COMPOSE_DIR/\u0026#34; \u0026#34;$BACKUP_PATH/compose/\u0026#34; \u0026gt;\u0026gt; /var/log/docker-backup.log 2\u0026gt;\u0026amp;1 # Backup incremental de datos de aplicaciones echo \u0026#34;[$(date)] Iniciando backup de datos...\u0026#34; rsync -av --delete \u0026#34;/home/user/app-data/\u0026#34; \u0026#34;$BACKUP_PATH/app-data/\u0026#34; \u0026gt;\u0026gt; /var/log/docker-backup.log 2\u0026gt;\u0026amp;1 # Limpiar backups más antiguos (mantener 7 últimos) echo \u0026#34;[$(date)] Limpiando backups antiguos...\u0026#34; ls -t \u0026#34;$BACKUP_DEST/backups\u0026#34; | tail -n +8 | xargs -I {} rm -rf \u0026#34;$BACKUP_DEST/backups/{}\u0026#34; echo \u0026#34;[$(date)] Backup completado\u0026#34; \u0026gt;\u0026gt; /var/log/docker-backup.log Le rendre exécutable :\nchmod +x /usr/local/bin/docker-backup.sh 3. Configurer cron # J\u0026rsquo;ai édité la table cron de l\u0026rsquo;utilisateur root :\nsudo crontab -e J\u0026rsquo;ai ajouté cette ligne pour l\u0026rsquo;exécuter à 2 AM tous les jours :\n0 2 * * * /usr/local/bin/docker-backup.sh Pour vérifier qu\u0026rsquo;il est enregistré :\nsudo crontab -l 4. Surveillance # J\u0026rsquo;ai créé un deuxième script pour me alerter si quelque chose échoue. Dans /usr/local/bin/check-backup.sh :\n#!/bin/bash LAST_BACKUP=$(ls -t /mnt/backup/backups | head -1) BACKUP_TIME=$(date -d \u0026#34;$(stat -c %y /mnt/backup/backups/$LAST_BACKUP | cut -d\u0026#39; \u0026#39; -f1)\u0026#34; +%s) CURRENT_TIME=$(date +%s) DIFF=$((($CURRENT_TIME - $BACKUP_TIME) / 3600)) if [ $DIFF -gt 25 ]; then echo \u0026#34;ALERTA: No hay backup desde hace $DIFF horas\u0026#34; else echo \u0026#34;Último backup: $DIFF horas atrás - OK\u0026#34; fi Je l\u0026rsquo;exécute manuellement chaque semaine ou via cron si je le souhaite :\nchmod +x /usr/local/bin/check-backup.sh /usr/local/bin/check-backup.sh Considérations importantes # Espace disque : rsync avec --delete synchronise exactement la source. Je vérifiez que la destination dispose d\u0026rsquo;au moins 1,5 fois la taille des données Docker.\nPermissions : Le script s\u0026rsquo;exécute en tant que root, donc il peut accéder à /var/lib/docker. Si tu utilises un utilisateur régulier, tu auras besoin de permissions spéciales.\nTests : Une fois par mois, je simule une récupération en restaurant un fichier aléatoire sur une machine de test. Une sauvegarde qui n\u0026rsquo;a jamais été testée n\u0026rsquo;existe pas.\nChiffrement : Mon disque USB est chez moi avec moi, donc je ne le chiffre pas. Si tu le conservais ailleurs, considère --backup-dir avec synchronisation vers une destination chiffrée.\nRésultat # Je dors mieux maintenant. Chaque nuit à 2 AM, Docker, les configurations et les données se synchronisent automatiquement. Si le serveur meurt, je récupère tout en 30 minutes.\nLa clé est : automatisation simple, vérification manuelle. N\u0026rsquo;attends pas que quelque chose échoue pour tester ta sauvegarde.\nÉquipement recommandé # Raspberry Pi 3 B+ — Serveur léger et peu consommateur d\u0026rsquo;énergie pour commencer ton homelab Raspberry Pi 4 (4GB) — La base parfaite pour homelab, Docker et surveillance Liens d\u0026rsquo;affiliation. Aucun coût supplémentaire pour toi.\n","date":"4 mai 2026","externalUrl":null,"permalink":"/fr/posts/backups-automaticos-con-rsync-y-cron-para-docker-domestico/","section":"Posts","summary":"Le problème # J’ai récemment perdu un disque dur sans avertissement. Ce n’était pas catastrophique car j’avais des sauvegardes, mais cela m’a rendu conscient que beaucoup de passionnés avec des serveurs domestiques n’ont aucune stratégie de protection des données. Si ton serveur Docker s’arrête demain, combien de temps te faudrait-il pour le récupérer ?\nDans cet article, je partage comment j’ai automatisé les sauvegardes de mon infrastructure Docker en utilisant rsync et cron. C’est simple, efficace et ça fonctionne.\n","title":"Sauvegardes automatiques avec rsync et cron pour Docker domestique","type":"posts"},{"content":"","date":"4 mai 2026","externalUrl":null,"permalink":"/fr/tags/servidor/","section":"Tags","summary":"","title":"Servidor","type":"tags"},{"content":"","date":"4 mai 2026","externalUrl":null,"permalink":"/fr/tags/sincronizaci%C3%B3n/","section":"Tags","summary":"","title":"Sincronización","type":"tags"},{"content":"","date":"4 mai 2026","externalUrl":null,"permalink":"/fr/tags/smb/","section":"Tags","summary":"","title":"Smb","type":"tags"},{"content":"","date":"4 mai 2026","externalUrl":null,"permalink":"/fr/tags/smbprotocol/","section":"Tags","summary":"","title":"Smbprotocol","type":"tags"},{"content":"","date":"4 mai 2026","externalUrl":null,"permalink":"/fr/tags/smtp/","section":"Tags","summary":"","title":"Smtp","type":"tags"},{"content":"","date":"4 mai 2026","externalUrl":null,"permalink":"/fr/tags/smtplib/","section":"Tags","summary":"","title":"Smtplib","type":"tags"},{"content":"","date":"4 mai 2026","externalUrl":null,"permalink":"/fr/tags/sqlite/","section":"Tags","summary":"","title":"Sqlite","type":"tags"},{"content":" Le problème # J\u0026rsquo;avais récemment besoin de synchroniser un partage SMB depuis un NAS avec mon serveur Linux. La solution évidente serait smbclient ou mount -t cifs, mais je voulais :\nSynchronisation incrémentale (uniquement les fichiers nouveaux ou modifiés) Détecter les fichiers supprimés du partage Contrôler l\u0026rsquo;authentification NTLM directement depuis le code Réduire la quantité obscène de logs que génère smbprotocol La librairie smbprotocol de Python résolvait tout cela, mais il n\u0026rsquo;y a pas de documentation sur comment bien le faire. Voici ma solution.\nConfiguration initiale # Installe les dépendances :\npip install smbprotocol sqlalchemy pydantic python-dotenv L\u0026rsquo;idée de base : maintenir une base de données SQLite avec un enregistrement de tous les fichiers synchronisés (nom, hash MD5, timestamp). À chaque exécution, on compare le partage actuel avec la base de données et on traite uniquement les changements.\nRéduire les logs de smbprotocol # C\u0026rsquo;est critique. Sans le contrôler, la librairie remplit ta console de messages de débogage :\nimport logging # Silenciar smbprotocol logging.getLogger(\u0026#39;smbprotocol\u0026#39;).setLevel(logging.WARNING) logging.getLogger(\u0026#39;smbprotocol.connection\u0026#39;).setLevel(logging.WARNING) logging.getLogger(\u0026#39;smbprotocol.session\u0026#39;).setLevel(logging.WARNING) # Tu logger logger = logging.getLogger(\u0026#39;sync_smb\u0026#39;) logger.setLevel(logging.INFO) handler = logging.StreamHandler() handler.setFormatter(logging.Formatter(\u0026#39;%(asctime)s - %(levelname)s - %(message)s\u0026#39;)) logger.addHandler(handler) Cela réduit les logs à un niveau raisonnable. Sans cela, chaque opération génère 50 lignes de charabia.\nStructure de la base de données SQLite # from datetime import datetime from sqlalchemy import create_engine, Column, String, DateTime, Integer from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker Base = declarative_base() class SyncedFile(Base): __tablename__ = \u0026#39;synced_files\u0026#39; filename = Column(String, primary_key=True) md5_hash = Column(String) file_size = Column(Integer) last_modified = Column(DateTime) sync_timestamp = Column(DateTime, default=datetime.utcnow) engine = create_engine(\u0026#39;sqlite:///smb_sync.db\u0026#39;) Base.metadata.create_all(engine) Session = sessionmaker(bind=engine) Connexion avec NTLM # from smbprotocol.session import Session from smbprotocol.tree import TreeConnect import socket username = \u0026#34;DOMINIO\\\\usuario\u0026#34; # Formato NETBIOS\\usuario password = \u0026#34;contraseña\u0026#34; host = \u0026#34;192.168.x.x\u0026#34; share = \u0026#34;compartido\u0026#34; # Conexión básica connection = smbprotocol.connection.Connection( uuid.uuid4(), host, 445, ) connection.connect() session = Session(connection, username, password) session.connect() tree = TreeConnect(session, f\u0026#34;\\\\\\\\{host}\\\\{share}\u0026#34;) tree.connect() NTLM est négocié automatiquement. Tu n\u0026rsquo;as rien de spécial à faire, mais assure-toi d\u0026rsquo;utiliser le format DOMINIO\\usuario correct.\nSynchronisation incrémentale # import hashlib from pathlib import Path def get_file_hash(file_data): \u0026#34;\u0026#34;\u0026#34;Calcula MD5 de contenido en bytes\u0026#34;\u0026#34;\u0026#34; return hashlib.md5(file_data).hexdigest() def sync_smb_share(local_path: Path): session = Session() remote_files = {} # Listar archivos del share directory = tree.open_file(share, FileAttributes.DIRECTORY, CreateOptions.FILE_DIRECTORY_FILE) for file_info in directory.query_directory(): if file_info.file_attributes \u0026amp; FileAttributes.DIRECTORY: continue # Ignorar carpetas por ahora filename = file_info.file_name remote_files[filename] = { \u0026#39;size\u0026#39;: file_info.end_of_file, \u0026#39;modified\u0026#39;: file_info.change_time.timestamp() } # Leer archivos nuevos o modificados local_db = session.query(SyncedFile).all() local_files = {f.filename: f for f in local_db} for filename, info in remote_files.items(): # Nuevo o modificado if filename not in local_files or local_files[filename].file_size != info[\u0026#39;size\u0026#39;]: logger.info(f\u0026#34;Descargando: {filename}\u0026#34;) file_obj = tree.open_file(filename) content = b\u0026#34;\u0026#34; for chunk in file_obj: content += chunk md5 = get_file_hash(content) (local_path / filename).write_bytes(content) # Actualizar BD sync_record = local_files.get(filename) or SyncedFile() sync_record.filename = filename sync_record.md5_hash = md5 sync_record.file_size = info[\u0026#39;size\u0026#39;] sync_record.last_modified = datetime.fromtimestamp(info[\u0026#39;modified\u0026#39;]) session.merge(sync_record) session.commit() # Detectar eliminados for filename in local_files: if filename not in remote_files: logger.warning(f\u0026#34;Archivo eliminado en remoto: {filename}\u0026#34;) (local_path / filename).unlink(missing_ok=True) session.query(SyncedFile).filter_by(filename=filename).delete() session.commit() tree.close() session.close() if __name__ == \u0026#34;__main__\u0026#34;: sync_smb_share(Path(\u0026#34;/mnt/sync\u0026#34;)) Automatisation avec cron # 0 */4 * * * /usr/bin/python3 /opt/sync_smb/sync.py \u0026gt;\u0026gt; /var/log/smb_sync.log 2\u0026gt;\u0026amp;1 Cela synchronise toutes les 4 heures.\nConclusion # Avec cette configuration, tu traites uniquement les changements, tu contrôles l\u0026rsquo;authentification NTLM sans trucs louches, et tu as des logs lisibles. La base de données SQLite est efficace même avec des milliers de fichiers.\nJ\u0026rsquo;ai utilisé ce setup en production pendant des mois sans problèmes.\nÉquipement recommandé # Mini PC Intel N100 — Mini PC silencieux et efficace pour serveur personnel 24/7 Raspberry Pi 3 B+ — Serveur léger à faible consommation pour débuter ton homelab Liens d\u0026rsquo;affiliation. Aucun coût supplémentaire pour toi.\n","date":"4 mai 2026","externalUrl":null,"permalink":"/fr/posts/sincronizacion-incremental-desde-smb-con-smbprotocol-en-linux-autenticacion-ntlm-y-control-de-logs/","section":"Posts","summary":"Le problème # J’avais récemment besoin de synchroniser un partage SMB depuis un NAS avec mon serveur Linux. La solution évidente serait smbclient ou mount -t cifs, mais je voulais :\nSynchronisation incrémentale (uniquement les fichiers nouveaux ou modifiés) Détecter les fichiers supprimés du partage Contrôler l’authentification NTLM directement depuis le code Réduire la quantité obscène de logs que génère smbprotocol La librairie smbprotocol de Python résolvait tout cela, mais il n’y a pas de documentation sur comment bien le faire. Voici ma solution.\n","title":"Synchronisation incrémentale depuis SMB avec smbprotocol sur Linux : authentification NTLM et contrôle des logs","type":"posts"},{"content":"","date":"4 mai 2026","externalUrl":null,"permalink":"/fr/tags/systemd/","section":"Tags","summary":"","title":"Systemd","type":"tags"},{"content":"","date":"4 mai 2026","externalUrl":null,"permalink":"/fr/tags/troubleshooting/","section":"Tags","summary":"","title":"Troubleshooting","type":"tags"},{"content":"","date":"4 mai 2026","externalUrl":null,"permalink":"/fr/tags/uvicorn/","section":"Tags","summary":"","title":"Uvicorn","type":"tags"},{"content":" Le vrai problème # Je fais tourner des services sur mon serveur domestique avec Docker Compose depuis plusieurs mois. Récemment, j\u0026rsquo;ai tenté de configurer un mot de passe avec des caractères spéciaux dans mon fichier .env. Le mot de passe ressemblait à Pass$word123!@. Au démarrage des conteneurs, la variable arrivait vide ou malformée. Après investigation, j\u0026rsquo;ai découvert que Docker Compose interprétait le $ comme référence à une autre variable.\nPourquoi c\u0026rsquo;est arrivé : interpolation de variables # Docker Compose interprète le fichier .env de façon spéciale. Quand il trouve un $ suivi d\u0026rsquo;un nom de variable valide, il essaie de le substituer par sa valeur. Si cette variable n\u0026rsquo;existe pas, il la laisse vide ou génère une erreur silencieuse.\nExemple du problème :\n# .env DB_PASSWORD=Pass$word123 API_KEY=sk_test_$random_key SECRET=$UNDEFINED_VAR Dans ces cas, Docker Compose cherchera des variables appelées word123, random_key et UNDEFINED_VAR. Évidemment il ne les trouvera pas, et tes valeurs seront corrompues.\nLa solution : guillemets simples # La solution la plus fiable est d\u0026rsquo;entourer les valeurs avec des guillemets simples. Les guillemets simples empêchent l\u0026rsquo;interpolation de variables dans Docker Compose, exactement comme ça fonctionne dans bash.\n# .env - FORMA CORRECTA DB_PASSWORD=\u0026#39;Pass$word123\u0026#39; API_KEY=\u0026#39;sk_test_$random_key\u0026#39; SECRET=\u0026#39;$UNDEFINED_VAR\u0026#39; COMPLEX=\u0026#39;!@#$%^\u0026amp;*()\u0026#39; Avec des guillemets simples, Docker Compose traite tout le contenu comme du texte littéral. Il ne tente pas de résoudre les références à des variables.\nUtiliser les variables dans docker-compose.yml # Une fois définies correctement dans .env, les utiliser dans ton docker-compose.yml est normal :\nversion: \u0026#39;3.8\u0026#39; services: database: image: postgres:15 environment: POSTGRES_PASSWORD: ${DB_PASSWORD} POSTGRES_USER: ${DB_USER} api: image: mi-api:latest environment: API_KEY: ${API_KEY} DB_SECRET: ${SECRET} Docker Compose chargera les valeurs depuis .env et injectera les variables correctement dans les conteneurs.\nLe second problème : restart ne recharge pas les variables # C\u0026rsquo;est là que c\u0026rsquo;est frustrant. Après avoir modifié ton fichier .env, tu exécutes :\ndocker-compose restart Et tu découvres que les conteneurs utilisent toujours les anciennes valeurs. Cela arrive parce que restart ne redémarre que les conteneurs existants sans les recréer. Il ne relit pas le fichier .env.\nLa solution : \u0026ndash;force-recreate # Pour que Docker Compose relise le fichier .env et applique les nouvelles variables, tu dois recréer les conteneurs. La commande correcte est :\ndocker-compose up -d --force-recreate Ou si tu préfères une séquence plus explicite :\ndocker-compose down docker-compose up -d L\u0026rsquo;option --force-recreate force la recréation même si l\u0026rsquo;image n\u0026rsquo;a pas changé. Sans elle, Docker Compose pourrait réutiliser des conteneurs existants.\nMon flux de travail actuel # Après expérimenter avec ça, voici comment je gère les variables sur mon serveur :\nJe définis tout dans .env avec des guillemets simples :\nMYSQL_ROOT_PASSWORD=\u0026#39;R00t!$pecial#Pass\u0026#39; MYSQL_USER=\u0026#39;admin\u0026#39; MYSQL_PASSWORD=\u0026#39;Pass$word@123\u0026#39; Référence dans docker-compose.yml :\nenvironment: MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} MYSQL_PASSWORD: ${MYSQL_PASSWORD} Après modification de .env, j\u0026rsquo;utilise toujours :\ndocker-compose up -d --force-recreate Je vérifie que les changements se sont appliqués :\ndocker-compose exec servicio env | grep MI_VAR Leçons apprises # Les caractères spéciaux $, !, @, # dans les valeurs nécessitent des guillemets simples restart redémarre seulement, ne recharge pas la configuration --force-recreate est indispensable après modification de .env Vérifie toujours que les variables ont été correctement chargées à l\u0026rsquo;intérieur du conteneur Ces détails m\u0026rsquo;ont épargné de nombreuses heures de debugging sur mon setup domestique. J\u0026rsquo;espère qu\u0026rsquo;ils t\u0026rsquo;en épargneront aussi.\nÉquipement recommandé # Raspberry Pi 3 B+ — Serveur léger à faible consommation pour démarrer ton homelab Raspberry Pi 4 (4GB) — La base parfaite pour homelab, Docker et monitoring Liens d\u0026rsquo;affiliation. Sans coût supplémentaire pour toi.\n","date":"4 mai 2026","externalUrl":null,"permalink":"/fr/posts/variables-de-entorno-con-caracteres-especiales-en-docker-compose-el-problema-del-dollar-y-como-recrear-contenedores/","section":"Posts","summary":"Le vrai problème # Je fais tourner des services sur mon serveur domestique avec Docker Compose depuis plusieurs mois. Récemment, j’ai tenté de configurer un mot de passe avec des caractères spéciaux dans mon fichier .env. Le mot de passe ressemblait à Pass$word123!@. Au démarrage des conteneurs, la variable arrivait vide ou malformée. Après investigation, j’ai découvert que Docker Compose interprétait le $ comme référence à une autre variable.\n","title":"Variables d'environnement avec caractères spéciaux dans Docker Compose: le problème du dollar et comment recréer les conteneurs","type":"posts"},{"content":"","date":"4 mai 2026","externalUrl":null,"permalink":"/fr/tags/variables-entorno/","section":"Tags","summary":"","title":"Variables-Entorno","type":"tags"},{"content":"En cumplimiento de lo dispuesto en el artículo 10 de la Ley 34/2002, de 11 de julio, de Servicios de la Sociedad de la Información y de Comercio Electrónico (LSSI-CE), se informa de los siguientes datos:\nTitular del sitio web # Nombre: Rogelio Guerra Riverón Email de contacto: serviciosrogeliowar@gmail.com Sitio web: serviciosrogeliowar.com / blog.serviciosrogeliowar.com Objeto y ámbito de aplicación # El presente aviso legal regula el uso del sitio web serviciosrogeliowar.com y blog.serviciosrogeliowar.com (en adelante, \u0026ldquo;el Sitio\u0026rdquo;), del que es titular Rogelio Guerra Riverón.\nLa utilización del Sitio implica la aceptación plena y sin reservas de todas las disposiciones incluidas en este Aviso Legal. El titular se reserva el derecho a modificar el presente aviso legal en cualquier momento, siendo responsabilidad del usuario su consulta periódica.\nPropiedad intelectual e industrial # Los contenidos del Sitio —incluyendo textos, imágenes, código fuente y diseño— son propiedad del titular o de sus legítimos licenciantes, y están protegidos por las leyes españolas e internacionales de propiedad intelectual e industrial.\nQueda expresamente prohibida la reproducción, distribución, comunicación pública o transformación de los contenidos sin autorización expresa del titular, salvo que se indique expresamente lo contrario o se trate de usos permitidos por la ley.\nResponsabilidad # El titular no garantiza la ausencia de errores en los contenidos ni su actualización permanente. El acceso al Sitio y el uso de la información contenida en él es responsabilidad exclusiva del usuario.\nEl titular no será responsable de los daños o perjuicios que pudieran derivarse del acceso al Sitio o del uso de la información que en él se contiene.\nPolítica de enlaces # El Sitio puede contener enlaces a páginas web de terceros. El titular no asume ninguna responsabilidad sobre los contenidos, informaciones o servicios que aparezcan en dichos sitios.\nLegislación aplicable y jurisdicción # El presente Aviso Legal se rige por la legislación española vigente. Para la resolución de cualquier controversia que pudiera surgir en relación con el Sitio, las partes se someten a la jurisdicción de los Juzgados y Tribunales competentes conforme a derecho.\nÚltima actualización: mayo de 2026\n","date":"1 mai 2026","externalUrl":null,"permalink":"/legal/aviso-legal/","section":"Legals","summary":"En cumplimiento de lo dispuesto en el artículo 10 de la Ley 34/2002, de 11 de julio, de Servicios de la Sociedad de la Información y de Comercio Electrónico (LSSI-CE), se informa de los siguientes datos:\nTitular del sitio web # Nombre: Rogelio Guerra Riverón Email de contacto: serviciosrogeliowar@gmail.com Sitio web: serviciosrogeliowar.com / blog.serviciosrogeliowar.com Objeto y ámbito de aplicación # El presente aviso legal regula el uso del sitio web serviciosrogeliowar.com y blog.serviciosrogeliowar.com (en adelante, “el Sitio”), del que es titular Rogelio Guerra Riverón.\n","title":"Aviso Legal","type":"legal"},{"content":"Avoir un serveur exposé à Internet sans une configuration de sécurité minimale, c\u0026rsquo;est laisser la porte entrouverte. Dans cet article, je rassemble les étapes que j\u0026rsquo;applique sur mes propres serveurs pour réduire la surface d\u0026rsquo;attaque sans compliquer la gestion du quotidien.\nSSH : la première ligne de défense # Le service SSH est le point d\u0026rsquo;entrée le plus attaqué sur tout serveur Linux. Voici les réglages les plus importants dans /etc/ssh/sshd_config :\n# Deshabilitar acceso root PermitRootLogin no # Solo autenticación por clave pública PasswordAuthentication no ChallengeResponseAuthentication no # Limitar intentos de autenticación MaxAuthTries 3 # Desactivar forwarding innecesario X11Forwarding no AllowTcpForwarding no Après chaque modification :\nsudo systemctl reload ssh # Ubuntu/Debian sudo systemctl reload sshd # CentOS/RHEL Pourquoi PermitRootLogin no et non prohibit-password ? Bien que prohibit-password bloque déjà l\u0026rsquo;accès root par mot de passe, laisser actif le login root par clé reste un risque : si cette clé est compromise, l\u0026rsquo;attaquant a accès total au système sans avoir besoin d\u0026rsquo;escalader les privilèges.\nChanger le port SSH (sécurité par l\u0026rsquo;obscurité) # Changer le port par défaut (22) n\u0026rsquo;est pas une véritable sécurité, mais cela élimine le bruit des bots Internet qui scannent continuellement ce port :\nPort 2222 # cualquier número entre 1024 y 65535 Sur le routeur ou le pare-feu, créez la règle de NAT pour rediriger le port externe vers le 22 interne, ou configurez UFW pour accepter le nouveau port.\nPare-feu avec UFW # UFW (Uncomplicated Firewall) simplifie la gestion d\u0026rsquo;iptables :\n# Política por defecto: denegar todo sudo ufw default deny incoming sudo ufw default allow outgoing # Permitir solo lo necesario sudo ufw allow 2222/tcp # SSH en puerto personalizado sudo ufw allow 80/tcp # HTTP sudo ufw allow 443/tcp # HTTPS # Activar sudo ufw enable sudo ufw status verbose Mises à jour de sécurité automatiques # Les serveurs qui ne sont pas mis à jour finissent par être vulnérables. unattended-upgrades applique les correctifs de sécurité sans intervention manuelle :\nsudo apt install unattended-upgrades sudo dpkg-reconfigure unattended-upgrades Pour vérifier que c\u0026rsquo;est actif :\nsystemctl is-active unattended-upgrades Le fichier de configuration se trouve dans /etc/apt/apt.conf.d/50unattended-upgrades. Par défaut, il n\u0026rsquo;applique que les mises à jour de sécurité, ce qui est le comportement correct pour la production.\nGestion des utilisateurs # Principe du moindre privilège # Chaque utilisateur ne doit avoir que les permissions dont il a besoin. Examinez régulièrement quels utilisateurs ont un shell actif :\ngrep -v nologin /etc/passwd | grep -v false Pour désactiver un utilisateur sans le supprimer :\nsudo usermod -s /usr/sbin/nologin usuario sudo passwd -l usuario # Bloquear contraseña sudo sans mot de passe — avec discernement # Il est tentant de mettre NOPASSWD dans sudoers pour éviter de taper le mot de passe, mais limitez-le aux commandes spécifiques qui en ont besoin :\n# Bien: solo para comandos concretos usuario ALL=(ALL) NOPASSWD: /usr/bin/docker, /usr/bin/systemctl restart nginx # Mal: acceso total sin contraseña usuario ALL=(ALL) NOPASSWD: ALL Protection contre les attaques par force brute avec fail2ban # fail2ban surveille les logs du système et bloque automatiquement les adresses IP qui effectuent trop de tentatives infructueuses :\nsudo apt install fail2ban Configuration basique dans /etc/fail2ban/jail.local :\n[DEFAULT] bantime = 1h findtime = 10m maxretry = 5 [sshd] enabled = true port = 2222 logpath = %(sshd_log)s backend = %(sshd_backend)s sudo systemctl enable fail2ban sudo systemctl start fail2ban # Ver IPs baneadas sudo fail2ban-client status sshd Audit : ce qu\u0026rsquo;il faut examiner régulièrement # Une fois le serveur configuré, il est conseillé d\u0026rsquo;examiner ces points avec une certaine fréquence :\n# Intentos de acceso fallidos sudo lastb | head -20 # Puertos escuchando (detectar servicios inesperados) ss -tlnp # Binarios con SUID (posibles vectores de escalada de privilegios) find / -perm -4000 -type f 2\u0026gt;/dev/null # Actualizaciones pendientes apt list --upgradable 2\u0026gt;/dev/null | grep -i security Chiffrement entre serveurs avec WireGuard # Si vous avez plusieurs serveurs qui doivent communiquer (rsync, bases de données, APIs internes), évitez d\u0026rsquo;exposer ces services à Internet. WireGuard offre un tunnel VPN rapide et simple avec chiffrement ChaCha20-Poly1305 :\nsudo apt install wireguard wg genkey | tee privatekey | wg pubkey \u0026gt; publickey Le trafic entre serveurs voyage chiffré à travers le tunnel (10.10.0.x) au lieu d\u0026rsquo;utiliser les adresses IP publiques. Ainsi, des services comme rsync ou PostgreSQL ne sont jamais exposés même si quelqu\u0026rsquo;un intercepte le trafic réseau.\nJ\u0026rsquo;ai un article spécifique sur la réplication d\u0026rsquo;urgence avec rsync et WireGuard avec la configuration complète.\nRésumé : checklist de durcissement # Action Priorité PermitRootLogin no en sshd_config Élevée PasswordAuthentication no Élevée Firewall UFW actif et règles minimales Élevée unattended-upgrades actif Élevée Changer le port SSH Moyenne AllowTcpForwarding no Moyenne MaxAuthTries 3 Moyenne fail2ban installé et configuré Moyenne Examiner les utilisateurs avec shell actif Moyenne Audit périodique des ports et SUID Basse La sécurité parfaite n\u0026rsquo;existe pas, mais appliquer ces étapes réduit drastiquement la probabilité d\u0026rsquo;être la cible facile que les bots automatisés recherchent.\nÉquipement recommandé # Mini PC Intel N100 — Mini PC silencieux et efficace pour serveur domestique 24/7 YubiKey 5 NFC — Clé de sécurité physique pour 2FA et accès SSH sécurisé Liens d\u0026rsquo;affiliation. Aucun coût supplémentaire pour toi.\n","date":"1 mai 2026","externalUrl":null,"permalink":"/fr/posts/hardening-servidores-linux/","section":"Posts","summary":"Avoir un serveur exposé à Internet sans une configuration de sécurité minimale, c’est laisser la porte entrouverte. Dans cet article, je rassemble les étapes que j’applique sur mes propres serveurs pour réduire la surface d’attaque sans compliquer la gestion du quotidien.\nSSH : la première ligne de défense # Le service SSH est le point d’entrée le plus attaqué sur tout serveur Linux. Voici les réglages les plus importants dans /etc/ssh/sshd_config :\n","title":"Durcissement de serveurs Linux : guide pratique","type":"posts"},{"content":"","date":"1 mai 2026","externalUrl":null,"permalink":"/legal/","section":"Legals","summary":"","title":"Legals","type":"legal"},{"content":"En cumplimiento del artículo 22.2 de la Ley 34/2002, de Servicios de la Sociedad de la Información (LSSI-CE) y el Reglamento (UE) 2016/679 (RGPD), te informamos sobre las cookies utilizadas en este sitio web.\n¿Qué son las cookies? # Las cookies son pequeños archivos de texto que los sitios web almacenan en tu dispositivo cuando los visitas. Sirven para recordar tus preferencias, mejorar la experiencia de navegación y, en algunos casos, recopilar información sobre el uso del sitio.\nCookies que utilizamos # Cookies necesarias # Son imprescindibles para el funcionamiento básico del sitio. No requieren tu consentimiento y no pueden desactivarse.\nCookie Proveedor Finalidad Duración cookie_consent Este sitio Almacena tus preferencias de cookies 1 año Cookies funcionales # Mejoran la funcionalidad del sitio. Solo se activan si das tu consentimiento.\nCookie Proveedor Finalidad Duración _gh_sess, user_session GitHub (Giscus) Sistema de comentarios y reacciones Sesión onesignal-* OneSignal Gestión de notificaciones push Persistente Sin cookies de analítica ni publicidad: Este sitio no utiliza Google Analytics ni ninguna otra herramienta de análisis de comportamiento. No existen cookies publicitarias.\nGestionar tus preferencias # Puedes modificar tus preferencias de cookies en cualquier momento haciendo clic aquí:\nTambién puedes controlar las cookies desde la configuración de tu navegador:\nGoogle Chrome Mozilla Firefox Safari Microsoft Edge Más información # Para información sobre cómo tratamos tus datos personales, consulta nuestra Política de Privacidad.\nÚltima actualización: mayo de 2026\n","date":"1 mai 2026","externalUrl":null,"permalink":"/legal/politica-cookies/","section":"Legals","summary":"En cumplimiento del artículo 22.2 de la Ley 34/2002, de Servicios de la Sociedad de la Información (LSSI-CE) y el Reglamento (UE) 2016/679 (RGPD), te informamos sobre las cookies utilizadas en este sitio web.\n¿Qué son las cookies? # Las cookies son pequeños archivos de texto que los sitios web almacenan en tu dispositivo cuando los visitas. Sirven para recordar tus preferencias, mejorar la experiencia de navegación y, en algunos casos, recopilar información sobre el uso del sitio.\n","title":"Política de Cookies","type":"legal"},{"content":"En cumplimiento del Reglamento (UE) 2016/679 General de Protección de Datos (RGPD) y la Ley Orgánica 3/2018, de 5 de diciembre, de Protección de Datos Personales y garantía de los derechos digitales (LOPD-GDD), se informa de lo siguiente:\nResponsable del tratamiento # Nombre: Rogelio Guerra Riverón Email: serviciosrogeliowar@gmail.com Sitio web: serviciosrogeliowar.com / blog.serviciosrogeliowar.com Datos que recopilamos y finalidades # Suscripción al newsletter # Cuando te suscribes al newsletter a través del formulario del blog, recopilamos:\nNombre (opcional) Dirección de correo electrónico Finalidad: Envío de comunicaciones informativas sobre nuevos artículos publicados en el blog.\nBase legal: Consentimiento del interesado (art. 6.1.a RGPD).\nPlazo de conservación: Hasta que el suscriptor solicite la baja o retire su consentimiento.\nComentarios y reacciones (Giscus) # El sistema de comentarios y reacciones utiliza Giscus, basado en GitHub Discussions. Si interactúas con los comentarios, deberás disponer de una cuenta de GitHub. El tratamiento de datos en este contexto está sujeto a la Política de privacidad de GitHub.\nNotificaciones push (OneSignal) # Si aceptas las notificaciones push, OneSignal almacena un identificador de dispositivo para el envío de notificaciones. El tratamiento está sujeto a la Política de privacidad de OneSignal.\nCookies # Consulta nuestra Política de Cookies para información detallada.\nDerechos del interesado # Puedes ejercer en cualquier momento los siguientes derechos:\nAcceso: Obtener confirmación sobre si tratamos tus datos y, en su caso, acceder a ellos. Rectificación: Solicitar la corrección de datos inexactos. Supresión: Solicitar la eliminación de tus datos cuando ya no sean necesarios. Oposición: Oponerte al tratamiento de tus datos. Limitación: Solicitar la restricción del tratamiento. Portabilidad: Recibir tus datos en formato estructurado y de uso común. Retirada del consentimiento: En cualquier momento, sin que ello afecte a la licitud del tratamiento previo. Para ejercer estos derechos, envía un email a serviciosrogeliowar@gmail.com indicando el derecho que deseas ejercer y adjuntando una copia de tu documento de identidad.\nTienes derecho a presentar una reclamación ante la Agencia Española de Protección de Datos (AEPD) en www.aepd.es si consideras que el tratamiento no es conforme al RGPD.\nTransferencias internacionales # OneSignal y GitHub (Giscus) son empresas estadounidenses que pueden transferir datos fuera del Espacio Económico Europeo. Estas transferencias se realizan bajo garantías adecuadas conforme al RGPD (cláusulas contractuales tipo o marcos de adecuación).\nSeguridad # Aplicamos medidas técnicas y organizativas adecuadas para garantizar la seguridad de los datos personales, incluyendo cifrado TLS en todas las comunicaciones y alojamiento en servidores bajo control del responsable.\nÚltima actualización: mayo de 2026\n","date":"1 mai 2026","externalUrl":null,"permalink":"/legal/politica-privacidad/","section":"Legals","summary":"En cumplimiento del Reglamento (UE) 2016/679 General de Protección de Datos (RGPD) y la Ley Orgánica 3/2018, de 5 de diciembre, de Protección de Datos Personales y garantía de los derechos digitales (LOPD-GDD), se informa de lo siguiente:\nResponsable del tratamiento # Nombre: Rogelio Guerra Riverón Email: serviciosrogeliowar@gmail.com Sitio web: serviciosrogeliowar.com / blog.serviciosrogeliowar.com Datos que recopilamos y finalidades # Suscripción al newsletter # Cuando te suscribes al newsletter a través del formulario del blog, recopilamos:\n","title":"Política de Privacidad","type":"legal"},{"content":"","date":"1 mai 2026","externalUrl":null,"permalink":"/fr/tags/vps/","section":"Tags","summary":"","title":"Vps","type":"tags"},{"content":" Pourquoi automatiser la génération de contenu # Écrire des articles techniques prend du temps. Entre la configuration de serveurs, le troubleshooting et la documentation, je trouve peu de temps pour rédiger. J\u0026rsquo;ai donc décidé de créer un agent IA pour m\u0026rsquo;aider à structurer et générer des brouillons depuis le terminal.\nClaude Haiku est parfait pour cela : il est rapide, bon marché et fonctionne bien pour les tâches de génération de texte. Il ne nécessite pas de GPUs puissants. Je lance simplement un script et j\u0026rsquo;obtiens un article prêt à éditer.\nPrérequis # Compte Anthropic avec accès à l\u0026rsquo;API Claude Token API configuré dans une variable d\u0026rsquo;environnement Python 3.10+ Librairie anthropic installée pip install anthropic L\u0026rsquo;agent en pratique # J\u0026rsquo;ai créé un script qui reçoit en entrée :\nUn sujet ou concept à documenter Le nombre de sections souhaité Le ton (technique, didactique, etc.) Et génère un article en format Markdown prêt à publier.\nImplémentation # Voici le script de base que j\u0026rsquo;utilise :\n#!/usr/bin/env python3 import anthropic import sys from datetime import datetime def generate_article(topic: str, sections: int = 5, tone: str = \u0026#34;técnico\u0026#34;) -\u0026gt; str: \u0026#34;\u0026#34;\u0026#34; Genera un artículo de blog usando Claude Haiku \u0026#34;\u0026#34;\u0026#34; client = anthropic.Anthropic() prompt = f\u0026#34;\u0026#34;\u0026#34;Eres un escritor técnico especializado en servidores, Docker y automatización. Genera un artículo de blog sobre: {topic} Requisitos: - Número de secciones principales: {sections} - Tono: {tone} - Incluye ejemplos de código cuando sea relevante - Usa formato Markdown - Longitud: 600-800 palabras - Sé práctico y directo, sin florituras - Primera persona cuando sea apropiado Estructura recomendada: 1. Introducción (por qué es importante) 2. Requisitos previos 3. Pasos o explicación principal (puede dividirse en subsecciones) 4. Configuración o ejemplo práctico 5. Conclusión Genera el artículo completo en Markdown, listo para publicar.\u0026#34;\u0026#34;\u0026#34; message = client.messages.create( model=\u0026#34;claude-3-5-haiku-20241022\u0026#34;, max_tokens=2000, messages=[ {\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: prompt} ] ) return message.content[0].text def save_article(content: str, filename: str) -\u0026gt; None: \u0026#34;\u0026#34;\u0026#34; Guarda el artículo en un archivo con frontmatter \u0026#34;\u0026#34;\u0026#34; frontmatter = f\u0026#34;\u0026#34;\u0026#34;--- title: \u0026#34;{filename.replace(\u0026#39;-\u0026#39;, \u0026#39; \u0026#39;).title()}\u0026#34; date: {datetime.now().strftime(\u0026#39;%Y-%m-%d\u0026#39;)} draft: false tags: [\u0026#34;técnica\u0026#34;, \u0026#34;servidor\u0026#34;, \u0026#34;automatización\u0026#34;] description: \u0026#34;Artículo generado con asistencia de IA\u0026#34; --- \u0026#34;\u0026#34;\u0026#34; with open(f\u0026#34;{filename}.md\u0026#34;, \u0026#34;w\u0026#34;, encoding=\u0026#34;utf-8\u0026#34;) as f: f.write(frontmatter + content) print(f\u0026#34;✓ Artículo guardado en {filename}.md\u0026#34;) def main(): if len(sys.argv) \u0026lt; 2: print(\u0026#34;Uso: python blog_agent.py \u0026#39;\u0026lt;tema\u0026gt;\u0026#39; [secciones] [tono]\u0026#34;) print(\u0026#34;Ejemplo: python blog_agent.py \u0026#39;Docker en producción\u0026#39; 5 técnico\u0026#34;) sys.exit(1) topic = sys.argv[1] sections = int(sys.argv[2]) if len(sys.argv) \u0026gt; 2 else 5 tone = sys.argv[3] if len(sys.argv) \u0026gt; 3 else \u0026#34;técnico\u0026#34; print(f\u0026#34;Generando artículo sobre: {topic}...\u0026#34;) print(\u0026#34;Esto puede tomar 30-60 segundos...\\n\u0026#34;) content = generate_article(topic, sections, tone) filename = topic.lower().replace(\u0026#34; \u0026#34;, \u0026#34;-\u0026#34;).replace(\u0026#34;\u0026#39;\u0026#34;, \u0026#34;\u0026#34;) save_article(content, filename) print(\u0026#34;\\nPrimeras líneas del artículo:\u0026#34;) print(\u0026#34;-\u0026#34; * 50) print(content[:300] + \u0026#34;...\\n\u0026#34;) if __name__ == \u0026#34;__main__\u0026#34;: main() Utilisation en pratique # J\u0026rsquo;exécute le script ainsi :\npython blog_agent.py \u0026#39;Configurar Nginx con SSL en Docker\u0026#39; 5 técnico En moins d\u0026rsquo;une minute, j\u0026rsquo;obtiens un fichier .md avec un brouillon complet. Je le révise ensuite, corrige les détails spécifiques et le publie.\nAvantages réels # Vitesse : Du sujet au brouillon en 1-2 minutes Cohérence : Le format est toujours homogène Point de départ : Je ne pars pas d\u0026rsquo;une page blanche Économique : Haiku est très bon marché comparé aux autres modèles Limitations # L\u0026rsquo;agent génère du contenu générique. Je dois toujours ajouter :\nLes détails spécifiques de mes configurations réelles Les commandes exactes que j\u0026rsquo;ai utilisées Les erreurs que j\u0026rsquo;ai rencontrées et comment je les ai résolu Ma perspective personnelle C\u0026rsquo;est un assistant, pas un remplacement. Mais cela économise beaucoup de temps dans la structure et la rédaction initiale.\nConclusion # Utiliser l\u0026rsquo;IA pour automatiser la rédaction technique a du sens si vous la combinez avec une édition humaine. Cet agent me permet de documenter les expériences plus rapidement sans sacrifier la qualité. Si vous écrivez fréquemment sur un blog technique, cela vaut la peine d\u0026rsquo;expérimenter.\nÉquipement recommandé # Raspberry Pi 3 B+ — Serveur léger à faible consommation pour débuter votre homelab Raspberry Pi 4 (4GB) — La base parfaite pour homelab, Docker et monitoring Liens d\u0026rsquo;affiliation. Sans coût supplémentaire pour vous.\n","date":"30 avril 2026","externalUrl":null,"permalink":"/fr/posts/agente-ia-con-claude-haiku-para-generar-articulos-de-blog-desde-el-terminal/","section":"Posts","summary":"Pourquoi automatiser la génération de contenu # Écrire des articles techniques prend du temps. Entre la configuration de serveurs, le troubleshooting et la documentation, je trouve peu de temps pour rédiger. J’ai donc décidé de créer un agent IA pour m’aider à structurer et générer des brouillons depuis le terminal.\nClaude Haiku est parfait pour cela : il est rapide, bon marché et fonctionne bien pour les tâches de génération de texte. Il ne nécessite pas de GPUs puissants. Je lance simplement un script et j’obtiens un article prêt à éditer.\n","title":"Agent IA avec Claude Haiku pour générer des articles de blog depuis le terminal","type":"posts"},{"content":"","date":"30 avril 2026","externalUrl":null,"permalink":"/fr/tags/autenticaci%C3%B3n/","section":"Tags","summary":"","title":"Autenticación","type":"tags"},{"content":" Pourquoi passer à l\u0026rsquo;authentification par clé # Après des mois de maintenance d\u0026rsquo;un serveur domestique avec accès SSH ouvert, j\u0026rsquo;en ai eu assez des tentatives de force brute contre le mot de passe. Passer à l\u0026rsquo;authentification par clé publique a été la meilleure décision de sécurité que j\u0026rsquo;ai prise. Les clés sont mathématiquement impossibles à craquer par force brute, alors que les mots de passe sont toujours une cible.\nGénération de la clé SSH # La première chose est de générer une paire de clés sur ta machine locale (pas sur le serveur) :\nssh-keygen -t ed25519 -C \u0026#34;tu_email@ejemplo.com\u0026#34; Il te demandera où enregistrer la clé. Appuie sur Entrée pour utiliser l\u0026rsquo;emplacement par défaut (~/.ssh/id_ed25519). Ensuite, il te demandera une phrase de passe (passphrase). J\u0026rsquo;utilise un mot de passe fort ici, car il protège ta clé privée localement.\nAprès cela, tu auras deux fichiers :\n~/.ssh/id_ed25519 - Ta clé privée (ne la partage jamais) ~/.ssh/id_ed25519.pub - Ta clé publique (c\u0026rsquo;est celle-ci qui va sur le serveur) Copier la clé sur le serveur # La méthode la plus sécurisée est d\u0026rsquo;utiliser ssh-copy-id. Depuis ta machine locale :\nssh-copy-id -i ~/.ssh/id_ed25519.pub usuario@servidor Cela ajoutera ta clé publique au fichier ~/.ssh/authorized_keys sur le serveur. Tu auras toujours besoin de ton mot de passe pour cette étape.\nSi ssh-copy-id ne fonctionne pas, tu peux le faire manuellement :\ncat ~/.ssh/id_ed25519.pub | ssh usuario@servidor \u0026#34;mkdir -p ~/.ssh \u0026amp;\u0026amp; cat \u0026gt;\u0026gt; ~/.ssh/authorized_keys \u0026amp;\u0026amp; chmod 600 ~/.ssh/authorized_keys\u0026#34; Vérifier que cela fonctionne # Avant de désactiver les mots de passe, vérifie que l\u0026rsquo;accès par clé fonctionne :\nssh usuario@servidor Si tout va bien, tu devrais accéder sans qu\u0026rsquo;on te demande un mot de passe (ou seulement la passphrase de ta clé locale, si tu l\u0026rsquo;as configurée).\nConfiguration du serveur SSH # Maintenant, nous éditons /etc/ssh/sshd_config sur le serveur :\nsudo nano /etc/ssh/sshd_config Cherche ces lignes et ajuste-les (supprime le # s\u0026rsquo;il est commenté) :\nPubkeyAuthentication yes PasswordAuthentication no PermitEmptyPasswords no PermitRootLogin no Ce sont les lignes critiques :\nPubkeyAuthentication : Active l\u0026rsquo;authentification par clé (doit être sur yes) PasswordAuthentication : Tu la changes en no pour désactiver les mots de passe PermitEmptyPasswords : Assure qu\u0026rsquo;il n\u0026rsquo;y a pas d\u0026rsquo;accès sans mot de passe vide PermitRootLogin : C\u0026rsquo;est une bonne pratique de mettre ceci sur no Appliquer les changements # Avant de redémarrer le service SSH, vérifie que la configuration est valide :\nsudo sshd -t Si cela ne renvoie pas d\u0026rsquo;erreurs, redémarre le service :\nsudo systemctl restart ssh Test final # Voici le moment de vérité. Ouvre une nouvelle session SSH sans fermer la session actuelle :\nssh usuario@servidor Si tu accèdes sans problème, tout fonctionne. Si ce n\u0026rsquo;est pas le cas, garde la session précédente ouverte pour annuler les modifications.\nSauvegarde et checklist # Avant de faire cela, je sauvegarde sshd_config :\nsudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.backup Ma checklist avant de désactiver les mots de passe :\nClé SSH générée localement Clé publique copiée sur le serveur Accès par clé testé correctement sshd -t sans erreurs Sauvegarde de sshd_config effectuée Session de test ouverte avant redémarrage Résultat # Depuis que j\u0026rsquo;ai mis cela en place, les logs du serveur sont calmes. Zéro tentative de force brute réussie. Les clés SSH sont l\u0026rsquo;une de ces améliorations de sécurité qui semblent compliquées au début mais qui valent vraiment le coup.\nÉquipement recommandé # YubiKey 5 NFC — Clé de sécurité physique pour 2FA et accès SSH sécurisé Raspberry Pi 3 B+ — Serveur léger à faible consommation pour démarrer ton homelab Liens d\u0026rsquo;affiliation. Aucun coût supplémentaire pour toi.\n","date":"30 avril 2026","externalUrl":null,"permalink":"/fr/posts/autenticacion-ssh-por-clave-publica-desactivar-contrasenas-en-ubuntu-server/","section":"Posts","summary":"Pourquoi passer à l’authentification par clé # Après des mois de maintenance d’un serveur domestique avec accès SSH ouvert, j’en ai eu assez des tentatives de force brute contre le mot de passe. Passer à l’authentification par clé publique a été la meilleure décision de sécurité que j’ai prise. Les clés sont mathématiquement impossibles à craquer par force brute, alors que les mots de passe sont toujours une cible.\n","title":"Authentification SSH par clé publique : désactiver les mots de passe sur Ubuntu Server","type":"posts"},{"content":"","date":"30 avril 2026","externalUrl":null,"permalink":"/fr/tags/backup/","section":"Tags","summary":"","title":"Backup","type":"tags"},{"content":"","date":"30 avril 2026","externalUrl":null,"permalink":"/fr/tags/blog/","section":"Tags","summary":"","title":"Blog","type":"tags"},{"content":"Je documente depuis un moment mes projets d\u0026rsquo;infrastructure. Après avoir testé plusieurs options, j\u0026rsquo;ai décidé de créer un blog statique avec Hugo. La combinaison Hugo + Blowfish s\u0026rsquo;est avérée être exactement ce dont j\u0026rsquo;avais besoin : rapide, propre et facile à maintenir.\nPourquoi Hugo et Blowfish # Hugo est un générateur de sites statiques écrit en Go. Il est incroyablement rapide et ne nécessite ni base de données ni dépendances compliquées. Blowfish est un thème moderne, minimaliste et bien documenté. Les deux fonctionnent parfaitement sur un serveur domestique avec des ressources limitées.\nInstallation sur le serveur # La première étape a été d\u0026rsquo;installer Hugo. Dans mon cas, j\u0026rsquo;utilise Debian sur le serveur :\nsudo apt-get update sudo apt-get install hugo Vérifier l\u0026rsquo;installation :\nhugo version Créer le site # J\u0026rsquo;ai initialisé le projet dans un dossier à l\u0026rsquo;intérieur de /home :\nhugo new site mi-blog cd mi-blog Ajouter le thème Blowfish # J\u0026rsquo;ai cloné le référentiel du thème dans le dossier des thèmes :\ngit clone https://github.com/nunocoracao/blowfish.git themes/blowfish Ensuite, j\u0026rsquo;ai mis à jour le fichier de configuration hugo.toml :\nbaseURL = \u0026#34;https://mi-dominio.local/\u0026#34; languageCode = \u0026#34;es\u0026#34; title = \u0026#34;Servicios Rogeliowar\u0026#34; theme = \u0026#34;blowfish\u0026#34; [params] description = \u0026#34;Documentación técnica de infraestructura y servidores\u0026#34; author = \u0026#34;Rogelio\u0026#34; [menu] [[menu.main]] name = \u0026#34;Posts\u0026#34; pageRef = \u0026#34;/posts\u0026#34; weight = 10 [[menu.main]] name = \u0026#34;Sobre mí\u0026#34; pageRef = \u0026#34;/about\u0026#34; weight = 20 Créer du contenu # Les articles vont dans le dossier content/posts/. Chacun est un fichier Markdown :\nhugo new posts/mi-primer-articulo.md Le fichier généré inclut un frontmatter YAML prêt à être édité :\n--- title: \u0026#34;Mi Primer Artículo\u0026#34; date: 2026-04-28 draft: false --- Contenido del artículo aquí... Serveur local pour les tests # Avant de publier, j\u0026rsquo;ai tout testé localement :\nhugo server -D Le site sera disponible à http://localhost:1313/. Le paramètre -D inclut les brouillons.\nGénérer les fichiers statiques # Une fois prêt, j\u0026rsquo;ai généré les fichiers HTML finaux :\nhugo Cela crée le dossier public/ avec tout le contenu compilé.\nServir avec Nginx # J\u0026rsquo;ai copié les fichiers générés dans le dossier Nginx :\nsudo cp -r public/* /var/www/mi-blog/ J\u0026rsquo;ai configuré un bloc serveur dans Nginx :\nserver { listen 80; server_name mi-dominio.local; root /var/www/mi-blog; index index.html; location / { try_files $uri $uri/ =404; } } J\u0026rsquo;ai rechargé Nginx :\nsudo systemctl reload nginx Automatiser les compilations # Pour ne pas compiler manuellement chaque fois que j\u0026rsquo;écris un article, j\u0026rsquo;ai créé un script simple :\n#!/bin/bash cd /home/usuario/mi-blog hugo sudo cp -r public/* /var/www/mi-blog/ echo \u0026#34;Blog actualizado\u0026#34; Je l\u0026rsquo;ai enregistré sous actualizar-blog.sh et je lui ai donné les permissions d\u0026rsquo;exécution :\nchmod +x actualizar-blog.sh Réflexion finale # Après une semaine d\u0026rsquo;utilisation de cette configuration, je peux dire qu\u0026rsquo;elle est solide. Hugo compile tout en moins d\u0026rsquo;une seconde, Blowfish a l\u0026rsquo;air professionnel sans avoir besoin de personnalisation extrême, et le serveur domestique gère tout sans problème.\nLe meilleur : pas de base de données à sauvegarder, pas de plugins qui se cassent, pas de mises à jour de sécurité chaque semaine. Juste des fichiers statiques servis par Nginx. Exactement ce que je cherchais.\n--- ## Équipement recommandé - **[Raspberry Pi 3 B+](https://amzn.to/4upmmwn)** — Serveur léger à faible consommation pour démarrer votre homelab - **[Raspberry Pi 4 (4GB)](https://amzn.to/4utrPSX)** — La base parfaite pour homelab, Docker et monitoring *Liens d\u0026#39;affiliation. Aucun coût supplémentaire pour vous.* ","date":"30 avril 2026","externalUrl":null,"permalink":"/fr/posts/blog-estatico-con-hugo-y-tema-blowfish-en-un-servidor-domestico/","section":"Posts","summary":"Je documente depuis un moment mes projets d’infrastructure. Après avoir testé plusieurs options, j’ai décidé de créer un blog statique avec Hugo. La combinaison Hugo + Blowfish s’est avérée être exactement ce dont j’avais besoin : rapide, propre et facile à maintenir.\nPourquoi Hugo et Blowfish # Hugo est un générateur de sites statiques écrit en Go. Il est incroyablement rapide et ne nécessite ni base de données ni dépendances compliquées. Blowfish est un thème moderne, minimaliste et bien documenté. Les deux fonctionnent parfaitement sur un serveur domestique avec des ressources limitées.\n","title":"Blog statique avec Hugo et thème Blowfish sur un serveur domestique","type":"posts"},{"content":"","date":"30 avril 2026","externalUrl":null,"permalink":"/fr/tags/blowfish/","section":"Tags","summary":"","title":"Blowfish","type":"tags"},{"content":"","date":"30 avril 2026","externalUrl":null,"permalink":"/fr/tags/ci/cd/","section":"Tags","summary":"","title":"Ci/Cd","type":"tags"},{"content":" Introduction # Fatigué de faire des déploiements manuels de mon blog Hugo chaque fois que je publie un article. J\u0026rsquo;ai décidé de mettre en place un pipeline CI/CD local avec GitLab Runner. Le résultat : automatique, fiable et sans dépendre de services externes.\nPrérequis # Tu as besoin de :\nUn serveur avec Docker installé Un référentiel sur GitLab (peut être auto-hébergé ou gitlab.com) Hugo installé localement pour les tests Accès SSH configuré sur ton serveur Installation de GitLab Runner # D\u0026rsquo;abord, tu dois installer GitLab Runner sur ton serveur. Je l\u0026rsquo;ai fait dans Docker parce que j\u0026rsquo;avais déjà le démon en cours d\u0026rsquo;exécution.\ndocker pull gitlab/gitlab-runner:latest docker run -d --name gitlab-runner \\ --restart always \\ -v /var/run/docker.sock:/var/run/docker.sock \\ -v /srv/gitlab-runner/config:/etc/gitlab-runner \\ gitlab/gitlab-runner:latest Cela monte le socket Docker pour que le runner puisse exécuter des conteneurs imbriqués. Important pour construire des images.\nEnregistrer le Runner # Tu as besoin d\u0026rsquo;un token de ton projet GitLab. Tu le trouves à : Configuración del proyecto → CI/CD → Runners\nPuis tu exécutes :\ndocker exec -it gitlab-runner gitlab-runner register \\ --url https://gitlab.com/ \\ --registration-token TU_TOKEN_AQUI \\ --executor docker \\ --docker-image alpine:latest \\ --docker-volumes /var/run/docker.sock:/var/run/docker.sock \\ --description \u0026#34;Runner Local Hugo\u0026#34; Choisis Docker comme exécuteur. C\u0026rsquo;est le plus propre pour ce cas.\nConfigurer le pipeline # À la racine de ton référentiel, tu crées .gitlab-ci.yml :\nstages: - build - deploy variables: DEPLOY_PATH: /home/deploy/blog-hugo/public build: stage: build image: alpine:latest before_script: - apk add --no-cache hugo git script: - hugo --minify - echo \u0026#34;Build completado\u0026#34; artifacts: paths: - public/ expire_in: 1 week only: - main deploy: stage: deploy image: alpine:latest before_script: - apk add --no-cache openssh-client rsync - mkdir -p ~/.ssh - echo \u0026#34;$DEPLOY_KEY\u0026#34; | base64 -d \u0026gt; ~/.ssh/id_rsa - chmod 600 ~/.ssh/id_rsa - ssh-keyscan -H localhost \u0026gt;\u0026gt; ~/.ssh/known_hosts script: - rsync -avz --delete public/ deploy@localhost:${DEPLOY_PATH} - ssh deploy@localhost \u0026#39;sudo systemctl restart nginx\u0026#39; only: - main when: on_success Variables d\u0026rsquo;environnement # Dans Configuración del proyecto → CI/CD → Variables, ajoute :\nDEPLOY_KEY : Ta clé SSH privée en base64 (cat ~/.ssh/id_rsa | base64 -w0) DEPLOY_PATH : Chemin où tu veux les fichiers (j\u0026rsquo;utilise /home/deploy/blog-hugo/public) Utilisateur de déploiement # Sur ton serveur, tu crées un utilisateur spécifique :\nsudo useradd -m -s /bin/bash deploy sudo usermod -aG docker deploy sudo mkdir -p /home/deploy/blog-hugo/public sudo chown deploy:deploy /home/deploy/blog-hugo Configure la clé SSH publique du runner :\nsudo -u deploy ssh-keygen -t ed25519 -N \u0026#34;\u0026#34; -f /home/deploy/.ssh/id_rsa cat /home/deploy/.ssh/id_rsa.pub \u0026gt;\u0026gt; /home/deploy/.ssh/authorized_keys Vérifier que ça fonctionne # Fais un push sur la branche main :\ngit add . git commit -m \u0026#34;Test CI/CD\u0026#34; git push origin main Sur GitLab, tu vois le pipeline en temps réel. Si tout va bien, en quelques secondes ton blog sera déployé.\nsudo -u deploy cat /home/deploy/blog-hugo/public/index.html Notes finales # Le runner local ne quitte jamais ton réseau. Contrôle total. Les temps de build sont rapides parce que tout est sur la machine locale. Si tu as besoin de mettre en cache des dépendances, configure des volumes persistants dans Docker. J\u0026rsquo;ai mis des restrictions sur la branche main pour éviter les déploiements accidentels. Après trois mois de fonctionnement sans problèmes. C\u0026rsquo;est simple mais efficace.\nÉquipement recommandé # Raspberry Pi 3 B+ — Serveur léger à faible consommation pour commencer ton homelab Raspberry Pi 4 (4GB) — La base parfaite pour homelab, Docker et monitoring Liens d\u0026rsquo;affiliation. Aucun coût supplémentaire pour toi.\n","date":"30 avril 2026","externalUrl":null,"permalink":"/fr/posts/cicd-con-gitlab-runner-local-para-desplegar-automaticamente-un-blog-hugo/","section":"Posts","summary":"Introduction # Fatigué de faire des déploiements manuels de mon blog Hugo chaque fois que je publie un article. J’ai décidé de mettre en place un pipeline CI/CD local avec GitLab Runner. Le résultat : automatique, fiable et sans dépendre de services externes.\nPrérequis # Tu as besoin de :\n","title":"CI/CD avec GitLab Runner local pour déployer automatiquement un blog Hugo","type":"posts"},{"content":"","date":"30 avril 2026","externalUrl":null,"permalink":"/fr/tags/claude/","section":"Tags","summary":"","title":"Claude","type":"tags"},{"content":" Introduction # Il y a quelques mois, j\u0026rsquo;ai décidé d\u0026rsquo;arrêter d\u0026rsquo;utiliser des services cloud coûteux et de mettre en place ma propre infrastructure à la maison. La solution que j\u0026rsquo;ai trouvée a été de combiner Docker avec Traefik. Cela fonctionne bien et maintenant j\u0026rsquo;ai plusieurs services fonctionnant sous HTTPS sans toucher manuellement à un certificat. Je te raconte comment j\u0026rsquo;ai fait.\nCe dont tu as besoin # Un serveur avec Docker installé (n\u0026rsquo;importe quelle machine Linux avec 2GB de RAM suffit). Un domaine personnel. Un peu de patience avec DNS. C\u0026rsquo;est tout.\nSi tu n\u0026rsquo;as pas de serveur dédié, tu as des options selon le budget et la consommation : une Raspberry Pi 3 B+ (lien d\u0026rsquo;affiliation) est parfaite pour les services légers avec une consommation d\u0026rsquo;énergie minimale. Si tu as besoin de plus de puissance, un ordinateur portable comme le Lenovo V15 (lien d\u0026rsquo;affiliation) est une option très polyvalente : en plus de serveur domestique, il a la capacité de fonctionner avec des logiciels industriels de marques comme Siemens (TIA Portal, SIMATIC) ou d\u0026rsquo;autres environnements d\u0026rsquo;automatisation qui demandent des ressources réelles. Un équipement, deux usages.\nLe plan # Je vais utiliser Traefik comme reverse proxy. Il gère automatiquement les certificats Let\u0026rsquo;s Encrypt, achemine le trafic vers les bons conteneurs et sert HTTPS sans que tu aies à faire quoi que ce soit une fois configuré. C\u0026rsquo;est propre et ça fonctionne.\nÉtape 1 : Préparer Docker Compose # Crée un dossier pour ta stack :\nmkdir -p ~/docker/traefik cd ~/docker/traefik Ce sera ton fichier docker-compose.yml :\nversion: \u0026#39;3.8\u0026#39; services: traefik: image: traefik:v2.10 container_name: traefik restart: always ports: - \u0026#34;80:80\u0026#34; - \u0026#34;443:443\u0026#34; volumes: - /var/run/docker.sock:/var/run/docker.sock - ./traefik.yml:/traefik.yml - ./acme.json:/acme.json networks: - web networks: web: driver: bridge Crée le fichier traefik.yml :\napi: insecure: true dashboard: true entryPoints: web: address: \u0026#34;:80\u0026#34; http: redirections: entryPoint: to: websecure websecure: address: \u0026#34;:443\u0026#34; certificatesResolvers: letsencrypt: acme: email: tu-email@example.com storage: acme.json httpChallenge: entryPoint: web providers: docker: endpoint: unix:///var/run/docker.sock exposedByDefault: false file: filename: traefik.yml Crée le fichier acme.json avec des permissions restrictives :\ntouch acme.json chmod 600 acme.json Étape 2 : Lance Traefik # docker-compose up -d Vérifie que c\u0026rsquo;est en fonctionnement :\ndocker-compose logs traefik Étape 3 : Configure ton domaine # Chez ton fournisseur DNS, pointe ton domaine (et un wildcard) vers l\u0026rsquo;adresse IP publique de ton serveur :\nexample.com A TU_IP_PUBLICA *.example.com A TU_IP_PUBLICA Attends que ça se propage (environ 15 minutes).\nÉtape 4 : Ajoute ton premier service # Je vais ajouter un exemple simple. Modifie le docker-compose.yml :\nservices: traefik: # ... config anterior whoami: image: traefik/whoami restart: always labels: - \u0026#34;traefik.enable=true\u0026#34; - \u0026#34;traefik.http.routers.whoami.rule=Host(`whoami.example.com`)\u0026#34; - \u0026#34;traefik.http.routers.whoami.entrypoints=websecure\u0026#34; - \u0026#34;traefik.http.routers.whoami.tls.certresolver=letsencrypt\u0026#34; - \u0026#34;traefik.http.services.whoami.loadbalancer.server.port=80\u0026#34; networks: - web Redéploie :\ndocker-compose up -d Attends 30 secondes et accède à https://whoami.example.com. Le certificat est généré automatiquement.\nÉtape 5 : Ajoute plus de services # Pour chaque nouveau service, ajoute simplement des labels similaires à ceux du whoami. Traefik s\u0026rsquo;occupe du reste. C\u0026rsquo;est aussi simple que ça.\nConsidérations pratiques # Sauvegarde de acme.json : C\u0026rsquo;est ton fichier de certificats. Fais-en une sauvegarde régulièrement ou tu perdras les certificats.\nFirewall : Ouvre les ports 80 et 443 sur ton routeur en pointant vers le serveur.\nIP dynamique : Si ton fournisseur d\u0026rsquo;accès change ton IP (courant en résidentiel), utilise un service DDNS.\nDashboard : Traefik a un dashboard sur http://localhost:8080 (seulement depuis la machine locale pour la sécurité).\nProblèmes courants # Si les certificats ne se génèrent pas, vérifie les logs : docker-compose logs traefik. Généralement c\u0026rsquo;est un problème de DNS ou de firewall.\nSi un service ne répond pas, vérifie que le label port correspond au port interne du conteneur.\nConclusion # Avec cette setup, j\u0026rsquo;ai mis en place un blog, un wiki, nextcloud et d\u0026rsquo;autres services à la maison sans dépenser pour SSL ou pour un reverse proxy commercial. Traefik est une bête pour ça. Ça vaut vraiment le coup de consacrer une heure pour le configurer correctement.\nÉquipement recommandé # Raspberry Pi 3 B+ — Serveur léger à faible consommation pour démarrer ton homelab Raspberry Pi 4 (4GB) — La base parfaite pour homelab, Docker et monitoring Lenovo V15 — Ordinateur portable polyvalent comme serveur domestique ou pour logiciels industriels Support pliable pour ordinateur portable en aluminium avec angle réglable — Ergonomie essentielle si tu utilises l\u0026rsquo;ordinateur portable comme station de travail Liens d\u0026rsquo;affiliation. Aucun coût supplémentaire pour toi.\n","date":"30 avril 2026","externalUrl":null,"permalink":"/fr/posts/como-montar-tu-propia-infraestructura-web-en-casa-con-docker-y-traefik-desde-cero-hasta-https-automatico/","section":"Posts","summary":"Introduction # Il y a quelques mois, j’ai décidé d’arrêter d’utiliser des services cloud coûteux et de mettre en place ma propre infrastructure à la maison. La solution que j’ai trouvée a été de combiner Docker avec Traefik. Cela fonctionne bien et maintenant j’ai plusieurs services fonctionnant sous HTTPS sans toucher manuellement à un certificat. Je te raconte comment j’ai fait.\n","title":"Comment configurer votre propre infrastructure web à la maison avec Docker et Traefik : de zéro à HTTPS automatique","type":"posts"},{"content":"Quand j\u0026rsquo;ai mis mon premier site en production sur le serveur domestique, j\u0026rsquo;ai commis l\u0026rsquo;erreur de penser que Google le découvrirait tout seul. Ce ne fut pas le cas. Après une semaine sans trace dans les résultats de recherche, j\u0026rsquo;ai compris que je devais être plus proactif. Voici ce que j\u0026rsquo;ai appris en configurant Google Search Console à partir de zéro.\nPourquoi tu as besoin de Google Search Console # Google Search Console n\u0026rsquo;est pas optionnel. C\u0026rsquo;est ta communication directe avec Google concernant ton site. Il te montre les erreurs d\u0026rsquo;indexation, les problèmes de sécurité, et surtout : il te permet de dire à Google exactement quelles pages indexer et quand.\nSans lui, tu dépends de ce que le bot de Google découvre ton site de façon organique. Avec un site nouveau sur un serveur domestique, cela peut prendre des semaines ou des mois.\nÉtape 1 : Vérifier ton domaine # Entre dans Google Search Console avec ton compte Google. Si tu n\u0026rsquo;en as pas, crée-en un. C\u0026rsquo;est gratuit.\nClique sur « Ajouter une propriété » et sélectionne le type de propriété. Tu as deux options :\nDomaine : vérifie tout le domaine (recommandé) Préfixe d\u0026rsquo;URL : vérifie seulement une URL spécifique J\u0026rsquo;ai choisi domaine parce que je voulais couvrir tout : example.com, www.example.com, et n\u0026rsquo;importe quel sous-domaine futur.\nVérification par DNS # Google te donnera un enregistrement TXT à ajouter à ton fournisseur de domaine. Dans mon cas, j\u0026rsquo;ai utilisé Namecheap.\nL\u0026rsquo;enregistrement ressemble à ceci :\ngoogle-site-verification=ABC123XYZ... J\u0026rsquo;ai accédé au panneau de contrôle de mon registraire, suis allé dans les paramètres DNS, et j\u0026rsquo;ai ajouté un nouvel enregistrement TXT avec cette valeur. J\u0026rsquo;ai attendu quelques minutes pour que cela se propage.\nReviens à Google Search Console et clique sur « Vérifier ». Si tout est correct, tu verras le message de confirmation.\nConseil : la vérification par DNS est définitive. Google la détectera automatiquement pour les futures propriétés sur le même domaine.\nÉtape 2 : Créer et optimiser ton sitemap # Un sitemap est un fichier XML qui liste toutes tes pages. Google l\u0026rsquo;utilise pour découvrir le contenu qu\u0026rsquo;il pourrait manquer.\nSi tu utilises un CMS comme WordPress, Astro ou Next.js, tu as probablement déjà un plugin ou un générateur. Dans mon cas, avec un site statique, je l\u0026rsquo;ai généré manuellement.\nUn sitemap basique ressemble à ceci :\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;urlset xmlns=\u0026#34;http://www.sitemaps.org/schemas/sitemap/0.9\u0026#34;\u0026gt; \u0026lt;url\u0026gt; \u0026lt;loc\u0026gt;https://example.com/\u0026lt;/loc\u0026gt; \u0026lt;lastmod\u0026gt;2026-04-25\u0026lt;/lastmod\u0026gt; \u0026lt;changefreq\u0026gt;weekly\u0026lt;/changefreq\u0026gt; \u0026lt;priority\u0026gt;1.0\u0026lt;/priority\u0026gt; \u0026lt;/url\u0026gt; \u0026lt;url\u0026gt; \u0026lt;loc\u0026gt;https://example.com/articulos/\u0026lt;/loc\u0026gt; \u0026lt;lastmod\u0026gt;2026-04-20\u0026lt;/lastmod\u0026gt; \u0026lt;changefreq\u0026gt;daily\u0026lt;/changefreq\u0026gt; \u0026lt;priority\u0026gt;0.8\u0026lt;/priority\u0026gt; \u0026lt;/url\u0026gt; \u0026lt;/urlset\u0026gt; J\u0026rsquo;ai sauvegardé le fichier en tant que sitemap.xml à la racine du serveur web (dans /var/www/html/ dans mon cas).\nNote importante : inclus la balise \u0026lt;lastmod\u0026gt; avec la date réelle. Google l\u0026rsquo;utilise pour savoir si ton contenu a changé.\nÉtape 3 : Envoyer le sitemap à Google # Reviens à Google Search Console, va à « Sitemaps » dans le menu de gauche, et colle l\u0026rsquo;URL complète :\nhttps://example.com/sitemap.xml Clique sur « Envoyer ». Google le traitera en quelques minutes.\nTu devrais voir un statut « Réussi » avec le nombre d\u0026rsquo;URL trouvées. S\u0026rsquo;il y a des erreurs, Google te les affichera ici.\nÉtape 4 : Optimise ton fichier robots.txt # Pendant que tu y es, assure-toi que ton robots.txt pointe vers ton sitemap :\nUser-agent: * Allow: / Disallow: /admin/ Disallow: /private/ Sitemap: https://example.com/sitemap.xml Cela dit à Google où trouver ton sitemap sans qu\u0026rsquo;il ait à le deviner.\nRésultats # Après avoir envoyé mon sitemap, Google a indexé 80 % de mes pages en 24 heures. En une semaine, elles y étaient toutes. Les recherches ont commencé à afficher mes articles.\nCe n\u0026rsquo;est pas de la magie, mais c\u0026rsquo;est efficace. Google Search Console est un outil que tout propriétaire de site doit utiliser, sans exception.\nÉquipement recommandé # Raspberry Pi 3 B+ — Serveur léger à faible consommation pour démarrer ton homelab Raspberry Pi 4 (4GB) — La base parfaite pour homelab, Docker et monitoring Liens d\u0026rsquo;affiliation. Aucun coût supplémentaire pour toi.\n","date":"30 avril 2026","externalUrl":null,"permalink":"/fr/posts/como-indexar-tu-sitio-web-en-google-search-console-guia-practica-con-sitemap-y-verificacion-de-dominio/","section":"Posts","summary":"Quand j’ai mis mon premier site en production sur le serveur domestique, j’ai commis l’erreur de penser que Google le découvrirait tout seul. Ce ne fut pas le cas. Après une semaine sans trace dans les résultats de recherche, j’ai compris que je devais être plus proactif. Voici ce que j’ai appris en configurant Google Search Console à partir de zéro.\nPourquoi tu as besoin de Google Search Console # Google Search Console n’est pas optionnel. C’est ta communication directe avec Google concernant ton site. Il te montre les erreurs d’indexation, les problèmes de sécurité, et surtout : il te permet de dire à Google exactement quelles pages indexer et quand.\n","title":"Comment indexer votre site web dans Google Search Console : guide pratique avec sitemap et vérification de domaine","type":"posts"},{"content":" Introduction # Après des mois d\u0026rsquo;utilisation manuelle de nginx, j\u0026rsquo;ai décidé de passer à Traefik. La raison est simple : gérer les certificats SSL pour chaque nouveau service est fastidieux. Traefik automatise tout avec Let\u0026rsquo;s Encrypt intégré. Voici ma configuration réelle.\nPourquoi Traefik # Avec Traefik, tu n\u0026rsquo;as pas besoin de recharger nginx chaque fois que tu ajoutes un conteneur. Il détecte automatiquement les services Docker, génère les certificats SSL à la demande et redirige le trafic. Tout est déclaratif.\nStructure de base # Je crée un dossier pour Traefik :\nmkdir -p /home/usuario/docker/traefik cd /home/usuario/docker/traefik J\u0026rsquo;ai besoin de trois fichiers : docker-compose.yml, traefik.yml et acme.json.\nFichier acme.json # Ce fichier stocke les certificats. Il doit avoir des permissions restrictives :\ntouch acme.json chmod 600 acme.json Configuration de Traefik (traefik.yml) # api: insecure: true dashboard: true entryPoints: web: address: \u0026#34;:80\u0026#34; http: redirections: entrypoint: to: websecure scheme: https websecure: address: \u0026#34;:443\u0026#34; providers: docker: endpoint: \u0026#34;unix:///var/run/docker.sock\u0026#34; exposedByDefault: false file: filename: /traefik/traefik.yml watch: true certificatesResolvers: letsencrypt: acme: email: mi-email@example.com storage: acme.json httpChallenge: entryPoint: web Je remplace mi-email@example.com par mon vrai email. Let\u0026rsquo;s Encrypt l\u0026rsquo;utilise pour les notifications.\nDocker Compose # C\u0026rsquo;est le fichier qui lance tout :\nversion: \u0026#39;3.8\u0026#39; services: traefik: image: traefik:v2.11 container_name: traefik restart: unless-stopped security_opt: - no-new-privileges:true networks: - proxy ports: - \u0026#34;80:80\u0026#34; - \u0026#34;443:443\u0026#34; environment: - TZ=Europe/Madrid volumes: - /etc/localtime:/etc/localtime:ro - /var/run/docker.sock:/var/run/docker.sock:ro - ./traefik.yml:/traefik/traefik.yml:ro - ./acme.json:/acme.json labels: - \u0026#34;traefik.enable=true\u0026#34; - \u0026#34;traefik.http.routers.traefik.rule=Host(`traefik.midominio.com`)\u0026#34; - \u0026#34;traefik.http.routers.traefik.entrypoints=websecure\u0026#34; - \u0026#34;traefik.http.routers.traefik.tls.certresolver=letsencrypt\u0026#34; - \u0026#34;traefik.http.services.traefik.loadbalancer.server.port=8080\u0026#34; networks: proxy: driver: bridge Je remplace traefik.midominio.com par mon vrai domaine. Le port 8080 est celui du tableau de bord de Traefik.\nLancer Traefik # docker-compose up -d Je vérifie les logs :\ndocker-compose logs -f Si tout va bien, le tableau de bord sera sur https://traefik.midominio.com.\nAjouter des services # Voilà le meilleur. Pour ajouter un nouveau service, j\u0026rsquo;ai juste besoin de labels Docker. Exemple avec un conteneur simple :\nservices: mi-app: image: mi-imagen:latest container_name: mi-app restart: unless-stopped networks: - proxy labels: - \u0026#34;traefik.enable=true\u0026#34; - \u0026#34;traefik.http.routers.mi-app.rule=Host(`app.midominio.com`)\u0026#34; - \u0026#34;traefik.http.routers.mi-app.entrypoints=websecure\u0026#34; - \u0026#34;traefik.http.routers.mi-app.tls.certresolver=letsencrypt\u0026#34; - \u0026#34;traefik.http.services.mi-app.loadbalancer.server.port=3000\u0026#34; networks: proxy: external: true Je n\u0026rsquo;ai pas besoin de toucher à Traefik. Le certificat se génère automatiquement.\nProblèmes courants # Le domaine ne résout pas : Assure-toi que ton DNS pointe vers la bonne IP.\nACME challenge échoue : Vérifie que le port 80 est ouvert et accessible depuis internet. Let\u0026rsquo;s Encrypt en a besoin.\nTableau de bord lent : C\u0026rsquo;est normal avec beaucoup de services. Ce n\u0026rsquo;est pas un problème.\nConclusion # Traefik m\u0026rsquo;a fait gagner des heures de configuration manuelle. Chaque nouveau conteneur n\u0026rsquo;a besoin que de quatre labels. Les certificats se renouvellent automatiquement 30 jours avant leur expiration.\nSi tu as un serveur domestique avec plusieurs services, ça vaut le coup de migrer. La courbe d\u0026rsquo;apprentissage est courte et les bénéfices sont réels.\nÉquipement recommandé # Raspberry Pi 3 B+ — Serveur léger à faible consommation pour démarrer ton homelab Raspberry Pi 4 (4GB) — La base parfaite pour homelab, Docker et monitoring Liens d\u0026rsquo;affiliation. Pas de coût supplémentaire pour toi.\n","date":"30 avril 2026","externalUrl":null,"permalink":"/fr/posts/configurar-traefik-v211-como-reverse-proxy-con-docker-y-https-automatico-con-lets-encrypt/","section":"Posts","summary":"Introduction # Après des mois d’utilisation manuelle de nginx, j’ai décidé de passer à Traefik. La raison est simple : gérer les certificats SSL pour chaque nouveau service est fastidieux. Traefik automatise tout avec Let’s Encrypt intégré. Voici ma configuration réelle.\nPourquoi Traefik # Avec Traefik, tu n’as pas besoin de recharger nginx chaque fois que tu ajoutes un conteneur. Il détecte automatiquement les services Docker, génère les certificats SSL à la demande et redirige le trafic. Tout est déclaratif.\n","title":"Configurer Traefik v2.11 en tant que reverse proxy avec Docker et HTTPS automatique avec Let's Encrypt","type":"posts"},{"content":"","date":"30 avril 2026","externalUrl":null,"permalink":"/fr/tags/css/","section":"Tags","summary":"","title":"Css","type":"tags"},{"content":"CSS semble simple jusqu\u0026rsquo;à ce que quelque chose ne fonctionne pas comme prévu. Tu passes des heures à regarder l\u0026rsquo;inspecteur du navigateur, tu changes une propriété, puis une autre, et le problème persiste. Cet article recueille les erreurs les plus fréquentes que j\u0026rsquo;ai rencontrées lors de la création de ce blog, avec des solutions directes.\nLe fond qui disparaît au défilement # Le problème : La page s\u0026rsquo;affiche bien dans la partie visible, mais en bas apparaît un fond blanc ou différent de celui attendu.\nPourquoi cela se produit : La couleur de fond est définie dans un conteneur interne (comme main ou article), non dans html ou body. Quand le contenu est plus court que la fenêtre, le reste de la page reste sans couleur.\nLa solution :\n/* MAL: solo el body tiene color */ body { background-color: #0f172a; } /* BIEN: también html, para cubrir toda la página */ html, body { background-color: #0f172a; min-height: 100%; } Cela garantit que le fond couvre du premier pixel au dernier, quel que soit le contenu.\nDark mode qui ne s\u0026rsquo;applique qu\u0026rsquo;à moitié # Le problème : Tu actives le mode sombre et certains éléments changent, mais d\u0026rsquo;autres restent avec un fond clair.\nPourquoi cela se produit : Le dark mode dans des frameworks comme Tailwind ou Blowfish fonctionne en ajoutant la classe .dark à l\u0026rsquo;élément html. Si ton CSS personnalisé ne cible que body, il peut perdre la couleur dans certains cas.\n/* Puede fallar en algunos navegadores */ html.dark body { background-color: #0f172a; } /* Más robusto: apuntar también a html */ html.dark, html.dark body { background-color: #0f172a; } Règle pratique : lorsque tu travailles avec un dark mode basé sur des classes, applique toujours la couleur de fond à la fois dans html et dans body.\nbackground-image sans background-color # Le problème : Tu as un motif SVG ou une image de fond, mais dans les zones où l\u0026rsquo;image ne se charge pas ou tarde, apparaît un fond blanc.\nLa solution : Définis toujours un background-color de secours junto avec background-image :\nbody { background-color: #0f172a; /* fallback si la imagen no carga */ background-image: url(\u0026#34;/patron.svg\u0026#34;); background-size: 120px 120px; background-repeat: repeat; } La couleur agit comme un filet de sécurité. L\u0026rsquo;utilisateur ne voit jamais un scintillement blanc pendant le chargement du SVG.\nbackground-attachment: fixed et le problème sur mobile # Le problème : L\u0026rsquo;effet parallaxe avec background-attachment: fixed s\u0026rsquo;affiche bien sur ordinateur mais sur mobile le fond apparaît statique, coupé ou décalé de manière étrange.\nPourquoi cela se produit : La plupart des navigateurs mobiles ignorent fixed sur les éléments autres que html/body, et certains l\u0026rsquo;implémentent avec des bugs connus sur iOS Safari.\nLa solution : Désactiv-le sur mobile :\nbody { background-attachment: fixed; } @media (max-width: 768px) { body { background-attachment: scroll; /* o simplemente eliminar el patrón */ background-image: none; } } z-index et le contenu qui disparaît derrière le fond # Le problème : Tu ajoutes un fond décoratif et soudain le texte ou les éléments interactifs disparaissent ou deviennent inaccessibles.\nPourquoi cela se produit : Le fond a un z-index plus élevé que le contenu, ou le contenu n\u0026rsquo;a pas de position défini (nécessaire pour que z-index fonctionne).\n/* El fondo queda detrás */ .fondo-decorativo { position: fixed; z-index: 0; } /* El contenido queda encima */ main, article, .contenido { position: relative; z-index: 1; } Spécificité : quand ton CSS ne surcharge pas celui du framework # Le problème : Tu écris une règle CSS mais elle n\u0026rsquo;a pas d\u0026rsquo;effet. L\u0026rsquo;inspecteur montre qu\u0026rsquo;elle est surchargée par le framework (Tailwind, Bootstrap, etc.).\nPourquoi cela se produit : La spécificité CSS détermine quelle règle gagne. Un sélecteur de classe (.dark body) a moins de poids qu\u0026rsquo;un sélecteur composé du framework.\nSolutions par ordre de préférence :\n/* 1. Aumentar especificidad añadiendo el padre */ html.dark body { ... } /* 2. Usar :is() para agrupar sin perder especificidad */ :is(html.dark) body { ... } /* 3. !important — solo como último recurso */ body { background-color: #0f172a !important; } Évite !important autant que possible — cela crée une dette de spécificité qui s\u0026rsquo;accumule et rend le CSS impossible à maintenir.\nVariables CSS : l\u0026rsquo;ordre compte # Le problème : Tu définis des variables CSS (custom properties) mais dans certains contextes elles ne fonctionnent pas.\nPourquoi cela se produit : Les variables CSS (custom properties) ne sont accessibles que dans l\u0026rsquo;élément où elles sont définies et ses descendants. Si vous les définissez dans body, elles ne seront pas disponibles dans html.\n/* BIEN: definir en :root para disponibilidad global */ :root { --color-fondo: #0f172a; --color-texto: #f1f5f9; } /* Luego úsalas en cualquier elemento */ body { background-color: var(--color-fondo); color: var(--color-texto); } Débogage rapide avec l\u0026rsquo;inspecteur # Quand quelque chose ne fonctionne pas :\nOuvrez DevTools (F12) et sélectionnez l\u0026rsquo;élément problématique Dans l\u0026rsquo;onglet Styles, recherchez les propriétés barrées — elles sont en cours de remplacement Filtrez par :hov pour voir les styles des états (:hover, :focus) Activez/désactivez le mode sombre depuis DevTools : panneau Rendering → Emulate CSS media feature prefers-color-scheme Utilisez l\u0026rsquo;onglet Computed pour voir la valeur finale qui s\u0026rsquo;applique réellement La plupart de ces erreurs ont la même racine : supposer que le style se propage vers le haut dans le DOM alors qu\u0026rsquo;en réalité il ne descend que vers le bas. html et body sont le point de départ — assurez-vous qu\u0026rsquo;ils ont exactement l\u0026rsquo;apparence que vous voulez avant de vous préoccuper du reste.\nÉquipement recommandé # Raspberry Pi 3 B+ — Serveur léger à faible consommation pour démarrer votre homelab Raspberry Pi 4 (4GB) — La base parfaite pour homelab, Docker et surveillance Liens d\u0026rsquo;affiliation. Aucun coût supplémentaire pour vous.\n","date":"30 avril 2026","externalUrl":null,"permalink":"/fr/posts/css-errores-comunes-estilos/","section":"Posts","summary":"CSS semble simple jusqu’à ce que quelque chose ne fonctionne pas comme prévu. Tu passes des heures à regarder l’inspecteur du navigateur, tu changes une propriété, puis une autre, et le problème persiste. Cet article recueille les erreurs les plus fréquentes que j’ai rencontrées lors de la création de ce blog, avec des solutions directes.\nLe fond qui disparaît au défilement # Le problème : La page s’affiche bien dans la partie visible, mais en bas apparaît un fond blanc ou différent de celui attendu.\n","title":"CSS qui ne faille pas : erreurs courantes lors de la stylisation de pages web et comment les éviter","type":"posts"},{"content":"","date":"30 avril 2026","externalUrl":null,"permalink":"/fr/tags/dark-mode/","section":"Tags","summary":"","title":"Dark-Mode","type":"tags"},{"content":"","date":"30 avril 2026","externalUrl":null,"permalink":"/fr/categories/desarrollo-web/","section":"Categories","summary":"","title":"Desarrollo Web","type":"categories"},{"content":"","date":"30 avril 2026","externalUrl":null,"permalink":"/fr/categories/dise%C3%B1o/","section":"Categories","summary":"","title":"Diseño","type":"categories"},{"content":"","date":"30 avril 2026","externalUrl":null,"permalink":"/fr/tags/dise%C3%B1o/","section":"Tags","summary":"","title":"Diseño","type":"tags"},{"content":"","date":"30 avril 2026","externalUrl":null,"permalink":"/fr/tags/email/","section":"Tags","summary":"","title":"Email","type":"tags"},{"content":"","date":"30 avril 2026","externalUrl":null,"permalink":"/fr/tags/fail2ban/","section":"Tags","summary":"","title":"Fail2ban","type":"tags"},{"content":" Le problème : attaques par force brute # Après avoir exposé mon serveur Ubuntu à internet, j\u0026rsquo;ai passé une nuit à examiner les logs. SSH recevait des tentatives de connexion échouées chaque seconde. Nginx aussi avait des requêtes suspectes vers des chemins courants. J\u0026rsquo;avais besoin de quelque chose pour bloquer ces tentatives automatiquement. Fail2ban a été ma solution.\nInstallation # sudo apt update sudo apt install fail2ban sudo systemctl start fail2ban sudo systemctl enable fail2ban Vérifier qu\u0026rsquo;il est en cours d\u0026rsquo;exécution :\nsudo systemctl status fail2ban Structure de Fail2ban # Fail2ban fonctionne ainsi : il surveille les logs, détecte les modèles d\u0026rsquo;échec et crée des règles de firewall pour bloquer les IPs. Il a trois composants clés :\nJails : définissent quel service protéger Filters : modèles regex pour détecter les tentatives échouées Actions : que faire quand une attaque est détectée (blocage, email, etc) Configuration de SSH # Le jail SSH est préconfiguré, mais je l\u0026rsquo;ai personnalisé. Créer le fichier /etc/fail2ban/jail.local :\nsudo nano /etc/fail2ban/jail.local [DEFAULT] bantime = 3600 findtime = 600 maxretry = 5 [sshd] enabled = true port = ssh filter = sshd logpath = /var/log/auth.log maxretry = 3 Explication :\nbantime : secondes durant lesquelles dure le blocage (1 heure) findtime : fenêtre en secondes pour compter les tentatives (10 min) maxretry : tentatives échouées avant blocage (3 pour SSH, plus restrictif) Configuration de Nginx # Pour Nginx, j\u0026rsquo;ai dû créer un jail personnalisé. D\u0026rsquo;abord, le filtre dans /etc/fail2ban/filter.d/nginx-http-auth.conf :\nsudo nano /etc/fail2ban/filter.d/nginx-http-auth.conf [Definition] failregex = ^\u0026lt;HOST\u0026gt; - \\S+ \\[\\S+ \\S+\\] \u0026#34;.*?\u0026#34; (401|403) .*$ ignoreregex = Ensuite, ajouter le jail au fichier local :\n[nginx-http-auth] enabled = true port = http,https filter = nginx-http-auth logpath = /var/log/nginx/error.log maxretry = 5 findtime = 600 bantime = 3600 Appliquer les modifications # sudo systemctl restart fail2ban Vérifier que les jails sont actifs :\nsudo fail2ban-client status Voir le statut détaillé de SSH :\nsudo fail2ban-client status sshd Surveillance # Après quelques heures, j\u0026rsquo;ai vérifié l\u0026rsquo;activité :\nsudo fail2ban-client status sshd La sortie affiche les IPs bloquées. Pour voir les détails du jail :\nsudo tail -f /var/log/fail2ban.log Débloquer une IP (au cas où) # Si je me suis trompé et que j\u0026rsquo;ai bloqué ma propre IP :\nsudo fail2ban-client set sshd unbanip \u0026lt;IP\u0026gt; Notes importantes # Fail2ban ne remplace pas les clés SSH. J\u0026rsquo;ai continué à utiliser l\u0026rsquo;authentification par clé, pas par mot de passe. J\u0026rsquo;ai augmenté maxretry pour SSH à 3 parce que c\u0026rsquo;est plus restrictif que 5 pour les applications web. Les logs de Nginx doivent être au format combiné. Vérifier /etc/nginx/nginx.conf. Pour les modifications de filtres, redémarrer : sudo systemctl restart fail2ban. Résultat # Après avoir implémenté cela, les tentatives de force brute ont disparu. Les logs ont cessé d\u0026rsquo;être un chaos. Le serveur se sent plus tranquille.\nFail2ban n\u0026rsquo;est pas une balle magique, mais c\u0026rsquo;est un bouclier efficace contre l\u0026rsquo;automatisation basique. Ça vaut la peine de prendre le temps de le configurer correctement.\nÉquipement recommandé # YubiKey 5 NFC — Clé de sécurité physique pour l\u0026rsquo;authentification à deux facteurs et l\u0026rsquo;accès SSH sécurisé Raspberry Pi 3 B+ — Serveur léger à faible consommation pour débuter votre homelab Liens d\u0026rsquo;affiliation. Aucun coût supplémentaire pour vous.\n","date":"30 avril 2026","externalUrl":null,"permalink":"/fr/posts/fail2ban-para-proteger-ssh-y-nginx-configuracion-practica-en-ubuntu/","section":"Posts","summary":"Le problème : attaques par force brute # Après avoir exposé mon serveur Ubuntu à internet, j’ai passé une nuit à examiner les logs. SSH recevait des tentatives de connexion échouées chaque seconde. Nginx aussi avait des requêtes suspectes vers des chemins courants. J’avais besoin de quelque chose pour bloquer ces tentatives automatiquement. Fail2ban a été ma solution.\n","title":"Fail2ban pour protéger SSH et Nginx : configuration pratique sur Ubuntu","type":"posts"},{"content":"","date":"30 avril 2026","externalUrl":null,"permalink":"/fr/tags/failover/","section":"Tags","summary":"","title":"Failover","type":"tags"},{"content":"","date":"30 avril 2026","externalUrl":null,"permalink":"/fr/tags/frontend/","section":"Tags","summary":"","title":"Frontend","type":"tags"},{"content":"","date":"30 avril 2026","externalUrl":null,"permalink":"/fr/tags/gitlab/","section":"Tags","summary":"","title":"Gitlab","type":"tags"},{"content":"","date":"30 avril 2026","externalUrl":null,"permalink":"/fr/tags/gmail/","section":"Tags","summary":"","title":"Gmail","type":"tags"},{"content":"","date":"30 avril 2026","externalUrl":null,"permalink":"/fr/tags/google-search-console/","section":"Tags","summary":"","title":"Google-Search-Console","type":"tags"},{"content":"","date":"30 avril 2026","externalUrl":null,"permalink":"/fr/tags/https/","section":"Tags","summary":"","title":"Https","type":"tags"},{"content":"","date":"30 avril 2026","externalUrl":null,"permalink":"/fr/tags/hugo/","section":"Tags","summary":"","title":"Hugo","type":"tags"},{"content":"","date":"30 avril 2026","externalUrl":null,"permalink":"/fr/tags/ia/","section":"Tags","summary":"","title":"IA","type":"tags"},{"content":"","date":"30 avril 2026","externalUrl":null,"permalink":"/fr/tags/indexaci%C3%B3n/","section":"Tags","summary":"","title":"Indexación","type":"tags"},{"content":"","date":"30 avril 2026","externalUrl":null,"permalink":"/fr/tags/lets-encrypt/","section":"Tags","summary":"","title":"Let's-Encrypt","type":"tags"},{"content":"","date":"30 avril 2026","externalUrl":null,"permalink":"/fr/tags/loki/","section":"Tags","summary":"","title":"Loki","type":"tags"},{"content":"","date":"30 avril 2026","externalUrl":null,"permalink":"/fr/categories/monitoring/","section":"Categories","summary":"","title":"Monitoring","type":"categories"},{"content":"","date":"30 avril 2026","externalUrl":null,"permalink":"/fr/tags/monitoring/","section":"Tags","summary":"","title":"Monitoring","type":"tags"},{"content":"","date":"30 avril 2026","externalUrl":null,"permalink":"/fr/tags/msmtp/","section":"Tags","summary":"","title":"Msmtp","type":"tags"},{"content":"","date":"30 avril 2026","externalUrl":null,"permalink":"/fr/tags/nginx/","section":"Tags","summary":"","title":"Nginx","type":"tags"},{"content":"","date":"30 avril 2026","externalUrl":null,"permalink":"/fr/tags/node-exporter/","section":"Tags","summary":"","title":"Node-Exporter","type":"tags"},{"content":" Pourquoi tu as besoin de cela # Quand tu lances un serveur à la maison, tu dois savoir si quelque chose d\u0026rsquo;étrange se produit. Un script qui t\u0026rsquo;envoie un email quand il détecte une tentative d\u0026rsquo;accès échouée, un certificat sur le point d\u0026rsquo;expirer ou un disque presque plein est inestimable. Le problème est que ton FAI bloque le port 25, donc tu ne peux pas utiliser sendmail directement. C\u0026rsquo;est là que msmtp intervient.\nmsmtp est un client SMTP minimaliste. Ce n\u0026rsquo;est pas un serveur de courrier complet, il envoie juste des emails via des serveurs externes comme Gmail. Parfait pour des cas comme le nôtre.\nInstallation # Sur Debian/Ubuntu :\nsudo apt-get update sudo apt-get install msmtp msmtp-mta L\u0026rsquo;option msmtp-mta est importante car elle crée un lien symbolique qui fait croire aux autres programmes que tu utilises sendmail traditionnel.\nConfiguration basique avec Gmail # Gmail a deux options : mot de passe d\u0026rsquo;application ou utiliser le protocole SMTP direct. Je vais utiliser un mot de passe d\u0026rsquo;application parce que c\u0026rsquo;est plus sûr et ça fonctionne sans activer « applications moins sécurisées ».\nD\u0026rsquo;abord, crée un mot de passe d\u0026rsquo;application sur ton compte Google :\nVa sur myaccount.google.com Sécurité → Mots de passe d\u0026rsquo;application (tu dois avoir l\u0026rsquo;authentification à 2 facteurs activée) Sélectionne « Courrier » et « Autre (personnalisé) » Gmail génère un mot de passe de 16 caractères Maintenant crée ou édite ~/.msmtprc :\nnano ~/.msmtprc Ajoute ceci :\ndefaults auth on tls on tls_trust_file /etc/ssl/certs/ca-certificates.crt logfile ~/.msmtp.log account gmail host smtp.gmail.com port 587 from tu-email@gmail.com user tu-email@gmail.com password tu-contraseña-de-aplicacion account default : gmail Permissions critiques :\nchmod 600 ~/.msmtprc C\u0026rsquo;est important. Si d\u0026rsquo;autres utilisateurs peuvent lire le fichier, ils verront ton mot de passe.\nTest initial # Teste que ça fonctionne :\necho \u0026#34;Cuerpo del email\u0026#34; | msmtp tu-email@gmail.com -S from=tu-email@gmail.com Vérifie ta boîte de réception. Si tu reçois l\u0026rsquo;email, c\u0026rsquo;est fonctionnel.\nUtiliser msmtp depuis les scripts de sécurité # Maintenant intègre cela dans tes alertes. Voici un exemple simple qui surveille les tentatives SSH échouées :\n#!/bin/bash FAILED_ATTEMPTS=$(grep \u0026#34;Failed password\u0026#34; /var/log/auth.log | wc -l) THRESHOLD=10 if [ $FAILED_ATTEMPTS -gt $THRESHOLD ]; then { echo \u0026#34;Asunto: ALERTA - Múltiples intentos de acceso SSH fallidos\u0026#34; echo \u0026#34;\u0026#34; echo \u0026#34;Se detectaron $FAILED_ATTEMPTS intentos fallidos en las últimas 24 horas\u0026#34; echo \u0026#34;\u0026#34; echo \u0026#34;Últimos intentos:\u0026#34; grep \u0026#34;Failed password\u0026#34; /var/log/auth.log | tail -5 } | msmtp tu-email@gmail.com fi Enregistre-le dans /usr/local/bin/check-ssh-alerts.sh et rends-le exécutable :\nsudo chmod +x /usr/local/bin/check-ssh-alerts.sh Automatiser avec cron # Ajoute à crontab pour qu\u0026rsquo;il s\u0026rsquo;exécute chaque heure :\nsudo crontab -e 0 * * * * /usr/local/bin/check-ssh-alerts.sh Problèmes courants # « SMTP Error: 535 » → Mot de passe incorrect. Vérifie que tu as utilisé le mot de passe d\u0026rsquo;application, pas ton mot de passe Google normal.\n« TLS connection refused » → Vérifie que le certificat est dans le bon chemin. Utilise ls /etc/ssl/certs/ca-certificates.crt.\nLes emails n\u0026rsquo;arrivent pas → Vérifie le journal : cat ~/.msmtp.log. Gmail refuse parfois si il détecte une activité suspecte.\nSécurité supplémentaire # Si le serveur s\u0026rsquo;exécute avec un utilisateur ordinaire mais que les scripts doivent s\u0026rsquo;exécuter en tant que root, considère :\nsudo visudo Et ajoute :\nnobody ALL=(ALL) NOPASSWD: /usr/local/bin/check-ssh-alerts.sh Ainsi tu exécutes le script sans demander de mot de passe dans cron.\nConclusion # Avec msmtp tu as des alertes de sécurité automatiques en minutes, sans les complications de monter un serveur SMTP complet. Je l\u0026rsquo;utilise sur mon serveur domestique pour surveiller les modifications dans iptables, les certificats expirés et les pics de charge. Dormant tranquille en sachant que quelque chose m\u0026rsquo;avertira s\u0026rsquo;il y a un problème.\nÉquipement recommandé # Raspberry Pi 3 B+ — Serveur léger à faible consommation pour commencer ton homelab Mini PC Intel N100 — Mini PC silencieux et efficace pour serveur domestique 24/7 Liens d\u0026rsquo;affiliation. Aucun coût supplémentaire pour toi.\n","date":"30 avril 2026","externalUrl":null,"permalink":"/fr/posts/notificaciones-de-seguridad-por-email-desde-el-terminal-con-msmtp-y-gmail/","section":"Posts","summary":"Pourquoi tu as besoin de cela # Quand tu lances un serveur à la maison, tu dois savoir si quelque chose d’étrange se produit. Un script qui t’envoie un email quand il détecte une tentative d’accès échouée, un certificat sur le point d’expirer ou un disque presque plein est inestimable. Le problème est que ton FAI bloque le port 25, donc tu ne peux pas utiliser sendmail directement. C’est là que msmtp intervient.\n","title":"Notifications de sécurité par email depuis le terminal avec msmtp et Gmail","type":"posts"},{"content":"","date":"30 avril 2026","externalUrl":null,"permalink":"/fr/tags/productividad/","section":"Tags","summary":"","title":"Productividad","type":"tags"},{"content":"","date":"30 avril 2026","externalUrl":null,"permalink":"/fr/tags/red/","section":"Tags","summary":"","title":"Red","type":"tags"},{"content":"","date":"30 avril 2026","externalUrl":null,"permalink":"/fr/categories/redes/","section":"Categories","summary":"","title":"Redes","type":"categories"},{"content":"Avoir un serveur à la maison présente un point faible évident : s\u0026rsquo;il y a une coupure d\u0026rsquo;électricité, le routeur tombe en panne, ou le disque meurt, ton site disparaît. La solution est d\u0026rsquo;avoir une réplique dans le cloud prête à s\u0026rsquo;activer en quelques minutes.\nL\u0026rsquo;architecture # [servidor-casa] → rsync cada 6h → [VPS réplica] servicios activos réplica en espera TTL DNS: 5 min Uptime Kuma vigilando Le serveur de la maison envoie le contenu au VPS toutes les 6 heures. Si le serveur tombe en panne, je change le DNS et en 5 minutes le VPS diffuse le site.\nPourquoi push et pas pull ? # Le serveur de la maison se trouve derrière un routeur domestique (Digi). Le routeur n\u0026rsquo;a que les ports 80 et 443 ouverts. Le VPS ne peut pas se connecter en SSH au serveur de la maison directement.\nLa solution : le serveur de la maison envoie au VPS (il a la sortie SSH libre), le VPS reçoit uniquement.\nPréparation du VPS # Le VPS (Debian 12, 2 CPUs, 4 GB RAM, 30 GB disque) avait déjà Docker installé. D\u0026rsquo;abord, la sécurité :\n# UFW: solo lo necesario sudo ufw default deny incoming sudo ufw default allow outgoing sudo ufw allow 22/tcp sudo ufw allow 80/tcp sudo ufw allow 443/tcp sudo ufw enable # Fail2ban SSH sudo apt-get install -y fail2ban # /etc/fail2ban/jail.local [DEFAULT] bantime = 7d findtime = 1h maxretry = 3 ignoreip = 127.0.0.1/8 \u0026lt;IP_PUBLICA_CASA\u0026gt; [sshd] enabled = true # SSH: solo clave pública sudo sed -i \u0026#39;s/#PasswordAuthentication yes/PasswordAuthentication no/\u0026#39; /etc/ssh/sshd_config sudo systemctl reload sshd Traefik comme proxy inverse # Même Traefik v2.11 que sur le serveur de la maison, avec Let\u0026rsquo;s Encrypt automatique :\nservices: traefik: image: traefik:v2.11 command: - --providers.docker=true - --entrypoints.web.address=:80 - --entrypoints.websecure.address=:443 - --certificatesresolvers.letsencrypt.acme.email=tu@email.com - --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json ports: - \u0026#34;80:80\u0026#34; - \u0026#34;443:443\u0026#34; Uptime Kuma : surveillance externe # Uptime Kuma surveille le serveur de la maison de l\u0026rsquo;extérieur. S\u0026rsquo;il ne répond pas, alerte immédiate par email :\nuptime-kuma: image: louislam/uptime-kuma:1 volumes: - ./data:/app/data labels: - traefik.enable=true - traefik.http.routers.uptime.rule=Host(`uptime.serviciosrogeliowar.com`) - traefik.http.routers.uptime.entrypoints=websecure - traefik.http.routers.uptime.tls.certresolver=letsencrypt Répliques en attente # Les services web et blog s\u0026rsquo;exécutent sur le VPS mais avec traefik.enable=false — Traefik les ignore, ils ne sont pas accessibles depuis internet. Ils ne s\u0026rsquo;activent qu\u0026rsquo;en cas d\u0026rsquo;urgence :\nweb-replica: image: nginx:alpine labels: - traefik.enable=false # ← cambiar a true en emergencia Script rsync depuis le serveur de la maison # #!/bin/bash # ~/infra/sync-to-vps.sh VPS=\u0026#34;usuario@\u0026lt;IP_PUBLICA_VPS\u0026gt;\u0026#34; SSH_KEY=\u0026#34;~/.ssh/id_ed25519\u0026#34; rsync -az --delete \\ -e \u0026#34;ssh -i $SSH_KEY -o StrictHostKeyChecking=no\u0026#34; \\ ~/infra/blog/public/ \\ ${VPS}:~/infra/blog/public/ rsync -az --delete \\ -e \u0026#34;ssh -i $SSH_KEY -o StrictHostKeyChecking=no\u0026#34; \\ ~/infra/web/html/ \\ ${VPS}:~/infra/web/html/ Crontab sur le serveur de la maison :\n0 */6 * * * ~/infra/sync-to-vps.sh Procédure de basculement en cas d\u0026rsquo;urgence # Quand le serveur de la maison tombe en panne :\nÉditer sur le VPS : changer traefik.enable=false à traefik.enable=true dans web et blog docker compose up -d dans chaque répertoire Dans le panneau DNS, changer l\u0026rsquo;enregistrement A de ton domaine de l\u0026rsquo;IP du serveur de la maison à l\u0026rsquo;IP du VPS Avec un TTL de 5 minutes, en moins de 10 minutes le site est rétabli Quand le serveur de la maison se rétablit, processus inverse : restaurer le DNS, revenir à traefik.enable=false sur le VPS.\nRésultat # Uptime Kuma surveillant de l\u0026rsquo;extérieur sur uptime.serviciosrogeliowar.com rsync automatique toutes les 6 heures — maximum 6 heures de contenu perdu en cas de défaillance Temps de récupération (RTO) : ~5 minutes Perte maximale de données (RPO) : ~6 heures Coût supplémentaire : uniquement le VPS (je l\u0026rsquo;avais déjà) Pour un serveur domestique, cette architecture est plus que suffisante.\nÉquipement recommandé # Raspberry Pi 3 B+ — Serveur léger à faible consommation pour commencer ton homelab Raspberry Pi 4 (4 GB) — La base parfaite pour homelab, Docker et surveillance Liens d\u0026rsquo;affiliation. Aucun frais supplémentaire pour toi.\n","date":"30 avril 2026","externalUrl":null,"permalink":"/fr/posts/replica-emergencia-vps/","section":"Posts","summary":"Avoir un serveur à la maison présente un point faible évident : s’il y a une coupure d’électricité, le routeur tombe en panne, ou le disque meurt, ton site disparaît. La solution est d’avoir une réplique dans le cloud prête à s’activer en quelques minutes.\nL’architecture # [servidor-casa] → rsync cada 6h → [VPS réplica] servicios activos réplica en espera TTL DNS: 5 min Uptime Kuma vigilando Le serveur de la maison envoie le contenu au VPS toutes les 6 heures. Si le serveur tombe en panne, je change le DNS et en 5 minutes le VPS diffuse le site.\n","title":"Réplique d'urgence : comment avoir votre serveur domestique sauvegardé sur un VPS","type":"posts"},{"content":"","date":"30 avril 2026","externalUrl":null,"permalink":"/fr/tags/reverse-proxy/","section":"Tags","summary":"","title":"Reverse-Proxy","type":"tags"},{"content":"","date":"30 avril 2026","externalUrl":null,"permalink":"/fr/tags/seo/","section":"Tags","summary":"","title":"Seo","type":"tags"},{"content":"","date":"30 avril 2026","externalUrl":null,"permalink":"/fr/tags/sitemap/","section":"Tags","summary":"","title":"Sitemap","type":"tags"},{"content":"","date":"30 avril 2026","externalUrl":null,"permalink":"/fr/tags/sitios-est%C3%A1ticos/","section":"Tags","summary":"","title":"Sitios Estáticos","type":"tags"},{"content":"Un serveur sans surveillance est un serveur aveugle. Vous ne savez pas quand le disque se remplit, quel conteneur consomme trop de RAM, ou combien de requêtes 404 votre site génère. Cet article documente comment j\u0026rsquo;ai configuré la pile complète : Prometheus + Node Exporter + Grafana + Loki + Promtail.\nL\u0026rsquo;architecture # [Servidor doméstico] ├── node-exporter → métricas del sistema (CPU, RAM, disco, red) ├── docker-stats- → métricas de contenedores (textfile collector) │ collector ├── prometheus → recolecta y almacena métricas ├── loki → agrega y almacena logs ├── promtail → envía logs de Nginx y syslog a Loki └── grafana → dashboards de todo lo anterior Tous les services tournent sur Docker, coordonnés par le même docker-compose.yml.\nMétriques système : Node Exporter # Node Exporter expose les métriques du matériel et du SE. L\u0026rsquo;astuce : il doit tourner avec network_mode: host pour voir les vraies interfaces réseau du serveur. S\u0026rsquo;il tourne en réseau Docker, il ne voit que l\u0026rsquo;interface eth0 du conteneur.\nnode-exporter: image: prom/node-exporter:v1.8.2 network_mode: host pid: host volumes: - /proc:/host/proc:ro - /sys:/host/sys:ro - /:/rootfs:ro - ./textfile-collector:/textfile:ro command: - --path.procfs=/host/proc - --path.sysfs=/host/sys - --path.rootfs=/rootfs - --web.listen-address=127.0.0.1:9100 - --collector.textfile.directory=/textfile Il écoute sur 127.0.0.1:9100. Prometheus l\u0026rsquo;atteint par 172.17.0.1:9100 (l\u0026rsquo;IP de l\u0026rsquo;hôte depuis le réseau Docker).\nMétriques de conteneurs : docker stats + textfile collector # Le problème avec cAdvisor est qu\u0026rsquo;il ne fonctionne pas avec Docker 29 et le driver de stockage overlayfs en cgroupv2 — il échoue avec \u0026ldquo;failed to identify read-write layer ID\u0026rdquo;.\nLa solution : un conteneur léger qui exécute docker stats toutes les 30 secondes et écrit le résultat au format Prometheus dans un fichier que Node Exporter lit.\n#!/bin/bash # docker_stats.sh OUTFILE=\u0026#34;/textfile/docker_stats.prom\u0026#34; TMPFILE=\u0026#34;${OUTFILE}.tmp\u0026#34; { echo \u0026#34;# HELP docker_container_cpu_percent CPU usage percentage per container\u0026#34; echo \u0026#34;# TYPE docker_container_cpu_percent gauge\u0026#34; # ... más definiciones ... docker stats --no-stream --format \\ \u0026#39;{{.Name}}|{{.CPUPerc}}|{{.MemUsage}}|{{.NetIO}}\u0026#39; 2\u0026gt;/dev/null | \\ while IFS=\u0026#39;|\u0026#39; read -r name cpu mem net; do cpu_val=$(echo \u0026#34;$cpu\u0026#34; | tr -d \u0026#39;%\u0026#39; | tr \u0026#39;,\u0026#39; \u0026#39;.\u0026#39;) # ... conversión de unidades ... echo \u0026#34;docker_container_cpu_percent{name=\\\u0026#34;${name}\\\u0026#34;} ${cpu_val}\u0026#34; echo \u0026#34;docker_container_memory_bytes{name=\\\u0026#34;${name}\\\u0026#34;} ${mem_used_bytes}\u0026#34; echo \u0026#34;docker_container_running{name=\\\u0026#34;${name}\\\u0026#34;} 1\u0026#34; done # Contenedores parados docker ps -a --filter \u0026#34;status=exited\u0026#34; --format \u0026#39;{{.Names}}\u0026#39; 2\u0026gt;/dev/null | \\ while read -r name; do echo \u0026#34;docker_container_running{name=\\\u0026#34;${name}\\\u0026#34;} 0\u0026#34; done } \u0026gt; \u0026#34;$TMPFILE\u0026#34; \u0026amp;\u0026amp; mv \u0026#34;$TMPFILE\u0026#34; \u0026#34;$OUTFILE\u0026#34; L\u0026rsquo;écriture atomique (tmp → final) empêche Prometheus de lire un fichier à moitié.\ndocker-stats-collector: image: docker:27-cli volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - ./textfile-collector:/textfile - ./docker_stats.sh:/docker_stats.sh:ro entrypoint: sh -c \u0026#34;apk add --no-cache bc \u0026gt; /dev/null 2\u0026gt;\u0026amp;1; while true; do sh /docker_stats.sh; sleep 30; done\u0026#34; Prometheus : collecter et conserver # prometheus: image: prom/prometheus:v2.51.2 networks: - monitoring volumes: - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro - prometheus-data:/prometheus command: - --config.file=/etc/prometheus/prometheus.yml - --storage.tsdb.path=/prometheus - --storage.tsdb.retention.time=30d - --web.enable-lifecycle Configuration du scraping :\nglobal: scrape_interval: 15s scrape_configs: - job_name: prometheus static_configs: - targets: [localhost:9090] - job_name: node static_configs: - targets: [172.17.0.1:9100] relabel_configs: - target_label: host replacement: servidor-casa 172.17.0.1 est l\u0026rsquo;IP de l\u0026rsquo;hôte accessible depuis le réseau Docker bridge. Les données sont conservées 30 jours.\nLogs : Loki + Promtail # Loki stocke les logs sans indexer le contenu complet — uniquement les étiquettes (labels). Promtail les collecte et les envoie avec des étiquettes comme job, host, filename.\npromtail: image: grafana/promtail:3.3.2 user: root networks: - monitoring volumes: - ./promtail-config.yml:/etc/promtail/config.yml:ro - ./promtail-data:/tmp/promtail - ~/infra/web/logs:/logs/nginx:ro - /var/log:/logs/host:ro Il doit tourner en root pour lire /var/log.\nGrafana : dashboards # Grafana se connecte à Prometheus et Loki comme sources de données. Les dashboards les plus utiles :\nSystème (Node Exporter) :\nCPU total et par cœur RAM utilisée / libre / cache Disque : utilisation par partition, IOPS, débit Réseau : trafic entrant/sortant par interface Conteneurs (docker stats) :\nCPU % par conteneur RAM par conteneur vs limite État (running/stopped) Trafic réseau par conteneur Logs (Loki) :\nLogs Nginx en temps réel Requêtes par code de statut (200, 301, 404, 500) Top des IPs avec le plus de requêtes Top des routes les plus accédées Problème : [$__range] dans les requêtes instantanées de Loki # Lors de l\u0026rsquo;utilisation de panneaux de type \u0026ldquo;stat\u0026rdquo; ou \u0026ldquo;piechart\u0026rdquo; avec Loki, la variable [$__range] ne se résout pas — Grafana renvoie \u0026ldquo;empty duration string\u0026rdquo;. La solution est d\u0026rsquo;utiliser une durée fixe :\n# MAL (en paneles stat/piechart): sum by(status) (count_over_time({job=\u0026#34;nginx\u0026#34;} | pattern ... [$__range])) # BIEN: sum by(status) (count_over_time({job=\u0026#34;nginx\u0026#34;} | pattern ... [24h])) Les panneaux de type \u0026ldquo;time series\u0026rdquo; acceptent correctement [$__interval].\nSécurité de la pile # Prometheus et Loki n\u0026rsquo;ont pas d\u0026rsquo;accès externe — uniquement sur le réseau interne monitoring Grafana est le seul point d\u0026rsquo;accès, protégé avec Traefik et Let\u0026rsquo;s Encrypt GF_AUTH_ANONYMOUS_ENABLED=false et GF_USERS_ALLOW_SIGN_UP=false dans Grafana Node Exporter écoute uniquement sur 127.0.0.1, non exposé sur toutes les interfaces Résultat # Avec cette stack, tu as une visibilité complète du serveur : quels processus consomment des ressources, quels conteneurs échouent, quelles requêtes reçoit ton web et quelles erreurs il génère. Tout dans des dashboards accessibles depuis monitor.serviciosrogeliowar.com.\nÉquipement recommandé # Raspberry Pi 3 B+ — Serveur léger à faible consommation pour commencer ton homelab Raspberry Pi 4 (4GB) — La base parfaite pour homelab, Docker et monitoring Liens d\u0026rsquo;affiliation. Aucun coût supplémentaire pour toi.\n","date":"30 avril 2026","externalUrl":null,"permalink":"/fr/posts/monitoring-prometheus-grafana-loki/","section":"Posts","summary":"Un serveur sans surveillance est un serveur aveugle. Vous ne savez pas quand le disque se remplit, quel conteneur consomme trop de RAM, ou combien de requêtes 404 votre site génère. Cet article documente comment j’ai configuré la pile complète : Prometheus + Node Exporter + Grafana + Loki + Promtail.\nL’architecture # [Servidor doméstico] ├── node-exporter → métricas del sistema (CPU, RAM, disco, red) ├── docker-stats- → métricas de contenedores (textfile collector) │ collector ├── prometheus → recolecta y almacena métricas ├── loki → agrega y almacena logs ├── promtail → envía logs de Nginx y syslog a Loki └── grafana → dashboards de todo lo anterior Tous les services tournent sur Docker, coordonnés par le même docker-compose.yml.\n","title":"Surveillance complète avec Prometheus, Grafana et Loki : métriques, logs et conteneurs Docker","type":"posts"},{"content":"","date":"30 avril 2026","externalUrl":null,"permalink":"/fr/tags/terminal/","section":"Tags","summary":"","title":"Terminal","type":"tags"},{"content":"","date":"30 avril 2026","externalUrl":null,"permalink":"/fr/tags/traefik/","section":"Tags","summary":"","title":"Traefik","type":"tags"},{"content":"","date":"30 avril 2026","externalUrl":null,"permalink":"/fr/tags/ubuntu/","section":"Tags","summary":"","title":"Ubuntu","type":"tags"},{"content":"","date":"30 avril 2026","externalUrl":null,"permalink":"/fr/tags/web/","section":"Tags","summary":"","title":"Web","type":"tags"},{"content":"L\u0026rsquo;un des problèmes classiques d\u0026rsquo;avoir un serveur à la maison est l\u0026rsquo;accès à distance sécurisé. Ouvrir les ports SSH directement au monde est une mauvaise idée — vous le voyez dans les logs d\u0026rsquo;authentification : des centaines de tentatives par jour. La solution élégante est un VPN, et WireGuard est aujourd\u0026rsquo;hui la meilleure option disponible.\nPourquoi WireGuard ? # Comparé à OpenVPN ou IPSec :\nBeaucoup plus rapide — il est intégré dans le kernel Linux depuis la version 5.6 Configuration minimale — la config du serveur tient en 10 lignes Cryptographie moderne — ChaCha20, Curve25519, BLAKE2 (voir protocole WireGuard) Un seul port UDP — facile à ouvrir sur le routeur Architecture # [Móvil/Portátil] ←── WireGuard túnel UDP 51820 ──→ [Router casa] → [Servidor doméstico 192.168.1.X] 10.10.0.2 10.10.0.1 Le serveur agit comme concentrateur VPN. Quand je me connecte, j\u0026rsquo;obtiens l\u0026rsquo;IP 10.10.0.2 et je peux accéder à n\u0026rsquo;importe quel service du réseau local comme si j\u0026rsquo;étais à la maison.\nInstallation sur Ubuntu # sudo apt-get install -y wireguard WireGuard est disponible dans les dépôts d\u0026rsquo;Ubuntu depuis 20.04. Dans les versions plus récentes, le module du kernel est inclus par défaut.\nGénération de clés # WireGuard utilise la cryptographie à clé publique. Nous générons une paire pour le serveur et une autre pour chaque client :\n# Claves del servidor wg genkey | tee server_private.key | wg pubkey \u0026gt; server_public.key # Claves del cliente (móvil o portátil) wg genkey | tee client_private.key | wg pubkey \u0026gt; client_public.key Important : les clés privées ne quittent jamais le dispositif qui les génère. Seules les clés publiques sont échangées.\nConfiguration du serveur # Fichier /etc/wireguard/wg0.conf :\n[Interface] Address = 10.10.0.1/24 ListenPort = 51820 PrivateKey = \u0026lt;CLAVE_PRIVADA_SERVIDOR\u0026gt; PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o eno1 -j MASQUERADE PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o eno1 -j MASQUERADE [Peer] # Móvil / portátil PublicKey = \u0026lt;CLAVE_PUBLICA_CLIENTE\u0026gt; AllowedIPs = 10.10.0.2/32 Les règles de iptables dans PostUp/PostDown activent la redirection de paquets (NAT) pour que le client puisse accéder au réseau local, pas seulement au serveur.\nPermissions strictes sur le fichier :\nsudo chmod 600 /etc/wireguard/wg0.conf Activer la redirection IP # Sans cela, le serveur ne réachemine pas les paquets entre les interfaces :\n# Temporal (hasta reinicio) sudo sysctl -w net.ipv4.ip_forward=1 # Permanente sudo sed -i \u0026#39;s/#net.ipv4.ip_forward=1/net.ipv4.ip_forward=1/\u0026#39; /etc/sysctl.conf Démarrer WireGuard # sudo systemctl enable --now wg-quick@wg0 Vérifier que c\u0026rsquo;est actif :\nsudo wg show wg0 Cela doit afficher l\u0026rsquo;interface en écoute sur le port 51820 et le peer enregistré.\nPare-feu (UFW) # sudo ufw allow 51820/udp sudo ufw reload Configuration du client # Fichier wg-casa.conf pour l\u0026rsquo;ordinateur portable ou mobile :\n[Interface] Address = 10.10.0.2/24 PrivateKey = \u0026lt;CLAVE_PRIVADA_CLIENTE\u0026gt; DNS = 1.1.1.1 [Peer] PublicKey = \u0026lt;CLAVE_PUBLICA_SERVIDOR\u0026gt; Endpoint = \u0026lt;IP_PUBLICA_CASA\u0026gt;:51820 AllowedIPs = 10.10.0.0/24 PersistentKeepalive = 25 AllowedIPs = 10.10.0.0/24 signifie que seul le trafic vers le réseau VPN passe par le tunnel — le reste d\u0026rsquo;Internet continue de sortir directement. Si vous vouliez router tout le trafic (y compris la navigation web) par la maison, vous utiliseriez 0.0.0.0/0.\nPersistentKeepalive = 25 maintient la connexion active même s\u0026rsquo;il n\u0026rsquo;y a pas de trafic — utile sur les réseaux mobiles qui ferment les connexions UDP inactives.\nRouteur : redirection de port # Sur le routeur, il faut rediriger le port 51820 UDP vers l\u0026rsquo;IP locale du serveur (par ex. 192.168.1.X). C\u0026rsquo;est généralement dans Configuration → NAT → Redirection de ports.\nCode QR pour le mobile # Au lieu de taper la config sur le mobile, nous générons un code QR :\nsudo apt-get install -y qrencode qrencode -t ansiutf8 \u0026lt; cliente.conf L\u0026rsquo;application WireGuard (iOS/Android) le scanne directement.\nVérification # Pour tester que cela fonctionne réellement, il faut se connecter depuis un réseau différent de celui de la maison — par exemple, les données mobiles :\nDésactivez le wifi du mobile Activez le tunnel WireGuard dans l\u0026rsquo;app Accédez à un service du serveur par IP locale (ex. http://192.168.1.X) Si cela répond, le tunnel fonctionne correctement.\nSécurité supplémentaire # Les clés privées ne voyagent jamais sur le réseau — seules les clés publiques sont échangées Pas d\u0026rsquo;utilisateurs ni de mots de passe — authentification purement cryptographique Un peer = une clé publique — si tu perds un appareil, tu supprimes sa [Peer] du serveur et il n\u0026rsquo;a plus accès Fail2ban ne s\u0026rsquo;applique pas — WireGuard abandonne silencieusement les paquets invalides sans répondre Ajouter plus de clients # Pour chaque nouvel appareil, nous générons une nouvelle paire de clés et ajoutons un bloc [Peer] supplémentaire sur le serveur avec sa clé publique et une IP différente (10.10.0.3, 10.10.0.4\u0026hellip;):\nsudo wg set wg0 peer \u0026lt;NUEVA_CLAVE_PUBLICA\u0026gt; allowed-ips 10.10.0.3/32 sudo wg-quick save wg0 Il n\u0026rsquo;est pas nécessaire de redémarrer le service — WireGuard ajoute les peers à chaud.\nMode tunnel complet : tout le trafic par le VPN # Par défaut, le client n\u0026rsquo;achemine que le trafic du réseau local (10.10.0.0/24) par le tunnel. Si tu veux que toute la navigation de l\u0026rsquo;appareil passe par ton serveur — utile sur les réseaux publics, dans les hôtels ou sur des réseaux WiFi inconnus — change AllowedIPs sur le client :\n# Solo red local (por defecto) AllowedIPs = 10.10.0.0/24 # Todo el tráfico (modo privacidad total) AllowedIPs = 0.0.0.0/0, ::/0 Avec 0.0.0.0/0 toute la navigation sort par ton adresse IP domestique. Avantages : confidentialité sur les réseaux publics, ton adresse IP réelle à tout moment. Inconvénient : ta vitesse de remontée à la maison limite la navigation de l\u0026rsquo;appareil distant.\nDans l\u0026rsquo;application WireGuard (mobile ou bureau) tu peux changer ceci en éditant le tunnel sans avoir besoin de toucher au serveur.\nAvec cela, j\u0026rsquo;ai un accès complet à mon réseau domestique de n\u0026rsquo;importe où, sans exposer aucun port supplémentaire au monde et avec une cryptographie moderne. L\u0026rsquo;étape naturelle suivante est de mettre en place des sauvegardes automatiques du VPS vers le serveur de la maison, en utilisant ce tunnel comme canal sécurisé.\nÉquipement recommandé # Raspberry Pi 3 B+ — Serveur léger à faible consommation pour débuter ton homelab Raspberry Pi 4 (4GB) — La base parfaite pour homelab, Docker et monitoring Liens d\u0026rsquo;affiliation. Aucun frais supplémentaire pour toi.\n","date":"30 avril 2026","externalUrl":null,"permalink":"/fr/posts/wireguard-vpn-servidor-dom%C3%A9stico/","section":"Posts","summary":"L’un des problèmes classiques d’avoir un serveur à la maison est l’accès à distance sécurisé. Ouvrir les ports SSH directement au monde est une mauvaise idée — vous le voyez dans les logs d’authentification : des centaines de tentatives par jour. La solution élégante est un VPN, et WireGuard est aujourd’hui la meilleure option disponible.\nPourquoi WireGuard ? # Comparé à OpenVPN ou IPSec :\n","title":"WireGuard VPN : accédez à votre serveur domestique de n'importe où","type":"posts"},{"content":"Ce blog est né d\u0026rsquo;un processus réel.\nJ\u0026rsquo;avais un domaine, un serveur à la maison et l\u0026rsquo;envie de monter quelque chose par moi-même. Au lieu de suivre un tutoriel générique, j\u0026rsquo;ai décidé de documenter exactement ce que je faisais — erreurs comprises.\nCe que tu trouveras ici # Des articles sur ce que je construis et ce que j\u0026rsquo;apprends :\nConfiguration de serveurs Linux Déploiement de services avec Docker Réseaux, DNS et sécurité Automatisation et infrastructure Sans raccourcis, sans simplifications. Processus réel, documenté étape par étape.\nÉquipement recommandé # Raspberry Pi 3 B+ — Serveur léger à faible consommation pour démarrer ton homelab Raspberry Pi 4 (4GB) — La base parfaite pour homelab, Docker et monitoring Liens d\u0026rsquo;affiliation. Aucun coût supplémentaire pour toi.\n","date":"29 avril 2026","externalUrl":null,"permalink":"/fr/posts/bienvenida/","section":"Posts","summary":"Ce blog est né d’un processus réel.\nJ’avais un domaine, un serveur à la maison et l’envie de monter quelque chose par moi-même. Au lieu de suivre un tutoriel générique, j’ai décidé de documenter exactement ce que je faisais — erreurs comprises.\nCe que tu trouveras ici # Des articles sur ce que je construis et ce que j’apprends :\n","title":"Comment ce blog est né","type":"posts"},{"content":"","date":"29 avril 2026","externalUrl":null,"permalink":"/fr/tags/inicio/","section":"Tags","summary":"","title":"Inicio","type":"tags"},{"content":" Bonjour, je suis Rogelio # Je suis sysadmin passionné par l\u0026rsquo;infrastructure réelle : celle qui tourne sur du vrai matériel, pas seulement dans le cloud. Depuis des années, je construis et maintiens ma propre infrastructure web depuis chez moi, apprenant au cours du processus tout ce que les cours n\u0026rsquo;enseignent pas.\nCe que tu trouveras ici # Ce blog est mon carnet technique public. Je documente ce que je fais, ce que je casse et comment je le répare. Rien d\u0026rsquo;exemples génériques — tout provient de cas réels.\nLes sujets principaux :\nInfrastructure Docker — compose, réseaux, volumes, reverse proxy avec Traefik Surveillance — Prometheus, Grafana, Loki, alertes réelles Réseaux — WireGuard, fail2ban, hardening SSH, segmentation CI/CD — GitLab Runner, Hugo, déploiements automatiques Automatisation — scripts Python, crons, agents IA Ma stack actuelle # Serveur principal : machine domestique avec Linux Réplica : VPS sur Clouding avec basculement DNS automatique Services : Traefik, Grafana, Loki, Prometheus, Listmonk, Hugo Tunnel privé : WireGuard entre les deux serveurs Contact # Tu peux me trouver sur :\nLinkedIn : linkedin.com/in/rogeliowar GitLab : gitlab.com/rogeliowar Instagram : @rogeliowarr Email : serviciosrogeliowar@gmail.com ","date":"1 janvier 2026","externalUrl":null,"permalink":"/fr/sobre-mi/","section":"Servicios Rogeliowar","summary":"Bonjour, je suis Rogelio # Je suis sysadmin passionné par l’infrastructure réelle : celle qui tourne sur du vrai matériel, pas seulement dans le cloud. Depuis des années, je construis et maintiens ma propre infrastructure web depuis chez moi, apprenant au cours du processus tout ce que les cours n’enseignent pas.\nCe que tu trouveras ici # Ce blog est mon carnet technique public. Je documente ce que je fais, ce que je casse et comment je le répare. Rien d’exemples génériques — tout provient de cas réels.\n","title":"À propos de moi","type":"page"},{"content":"","externalUrl":null,"permalink":"/viajes/","section":"Viajes \u0026 Experiencias","summary":"","title":"Viajes \u0026 Experiencias","type":"viajes"}]