La desviación de configuración (configuration drift) aparece cuando el estado real de un servidor deja de coincidir con el que define tu código: alguien edita un archivo por SSH para apagar un incendio, un paquete se actualiza fuera del pipeline o un recurso se modifica desde el panel del proveedor. En este tutorial detectarás esas desviaciones en tres niveles: la configuración del sistema operativo con Ansible en modo check, la infraestructura con terraform plan, y los archivos críticos con AIDE. Después programarás la comprobación con un temporizador de systemd y verás cómo corregir lo que encuentre.

Requisitos previos

  • Un nodo de control con Ubuntu 24.04 LTS desde el que ejecutarás Ansible (puede ser tu equipo o un VPS de CubePath).
  • Uno o más servidores gestionados con Ubuntu 24.04, accesibles por SSH con clave desde el nodo de control, con un usuario con sudo.
  • Para la parte de Terraform, un proyecto de Terraform ya desplegado con su estado accesible (local o en un backend remoto).
  • Conocimientos básicos de Ansible y Terraform.

Paso 1: Instalar Ansible y definir el inventario

En el nodo de control, instala Ansible desde los repositorios de Ubuntu y jq, que usarás más adelante para leer los resultados:

sudo apt update
sudo apt install ansible jq

Crea un directorio para el proyecto y el inventario con tus servidores:

sudo mkdir -p /opt/drift
sudo chown "$USER": /opt/drift
nano /opt/drift/inventory.ini
[web]
web1 ansible_host=203.0.113.10
web2 ansible_host=203.0.113.11

[all:vars]
ansible_user=your_user

Sustituye las IP por las de tus servidores y your_user por el usuario remoto. Comprueba que Ansible llega a todos ellos:

cd /opt/drift
ansible -i inventory.ini all -m ping
web1 | SUCCESS => {
    "changed": false,
    "ping": "pong"
}
web2 | SUCCESS => {
    "changed": false,
    "ping": "pong"
}

Paso 2: Describir el estado deseado en un playbook

Ansible solo puede detectar desviaciones en lo que gestiona. Este playbook de ejemplo define tres cosas habituales: paquetes que deben estar instalados, una configuración de SSH endurecida y un servicio que debe estar activo. Adáptalo a lo que realmente gestiones.

nano /opt/drift/site.yml
- name: Estado base de los servidores
  hosts: all
  become: true

  tasks:
    - name: Paquetes base instalados
      ansible.builtin.apt:
        name:
          - chrony
          - unattended-upgrades
        state: present

    - name: Configuración de SSH endurecida
      ansible.builtin.copy:
        dest: /etc/ssh/sshd_config.d/50-hardening.conf
        owner: root
        group: root
        mode: "0644"
        content: |
          PermitRootLogin no
          PasswordAuthentication no
          MaxAuthTries 3
      notify: Reload ssh

    - name: chrony activo y habilitado
      ansible.builtin.service:
        name: chrony
        state: started
        enabled: true

  handlers:
    - name: Reload ssh
      ansible.builtin.service:
        name: ssh
        state: reloaded

Aplica el playbook una vez para que los servidores partan del estado deseado:

ansible-playbook -i inventory.ini site.yml

Vuelve a ejecutarlo. Un playbook idempotente no debe cambiar nada en la segunda pasada:

PLAY RECAP *********************************************************************
web1                       : ok=4    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
web2                       : ok=4    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Esta propiedad es la base de todo lo que sigue: si una ejecución en modo check informa de changed mayor que cero, el servidor se ha desviado.

Paso 3: Detectar desviaciones con --check y --diff

Simula un cambio manual en web1, como haría alguien que entra por SSH para depurar un problema:

ssh -t [email protected] "echo 'MaxAuthTries 10' | sudo tee /etc/ssh/sshd_config.d/50-hardening.conf"

Desde el nodo de control, ejecuta el playbook en modo check. --check no modifica nada en los servidores y --diff muestra la diferencia exacta entre lo que hay y lo que debería haber:

ansible-playbook -i inventory.ini site.yml --check --diff
TASK [Configuración de SSH endurecida] *****************************************
--- before: /etc/ssh/sshd_config.d/50-hardening.conf
+++ after: /etc/ssh/sshd_config.d/50-hardening.conf
@@ -1 +1,3 @@
-MaxAuthTries 10
+PermitRootLogin no
+PasswordAuthentication no
+MaxAuthTries 3

changed: [web1]
ok: [web2]

PLAY RECAP *********************************************************************
web1                       : ok=4    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
web2                       : ok=4    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

La salida identifica el servidor (web1), el archivo y el contenido que alguien ha cambiado. Para limitar la comprobación a un grupo o a un servidor concreto, añade --limit web1.

Paso 4: Programar la comprobación con systemd

Revisar la salida a mano no escala. El callback json de Ansible devuelve el resultado en un formato que puedes procesar con jq: el objeto stats contiene, por servidor, cuántas tareas habrían cambiado. Crea un script que ejecute la comprobación y termine con error si hay desviaciones:

sudo nano /usr/local/bin/drift-check
#!/usr/bin/env bash
set -euo pipefail

cd /opt/drift

rc=0
report=$(ANSIBLE_STDOUT_CALLBACK=json ansible-playbook -i inventory.ini site.yml --check) || rc=$?

# rc 2 = alguna tarea falló, rc 4 = algún servidor inalcanzable
if [[ -z "$report" ]] || (( rc != 0 && rc != 2 && rc != 4 )); then
  echo "ansible-playbook terminó con código ${rc}" >&2
  exit 2
fi

unreachable=$(jq -r '.stats | to_entries[] | select(.value.unreachable > 0 or .value.failures > 0) | .key' <<<"$report")
drifted=$(jq -r '.stats | to_entries[] | select(.value.changed > 0) | "\(.key) (\(.value.changed) cambios)"' <<<"$report")

if [[ -n "$unreachable" ]]; then
  echo "Servidores con errores o inalcanzables: ${unreachable//$'\n'/, }" >&2
fi

if [[ -n "$drifted" ]]; then
  echo "Desviación detectada: ${drifted//$'\n'/, }"
  exit 1
fi

if [[ -n "$unreachable" ]]; then
  exit 2
fi

echo "Sin desviaciones"

Hazlo ejecutable y pruébalo. Como web1 sigue modificado, debe informar de la desviación y salir con código 1:

sudo chmod 755 /usr/local/bin/drift-check
drift-check; echo "exit=$?"
Desviación detectada: web1 (1 cambios)
exit=1

Crea un servicio de systemd que ejecute el script con tu usuario, que es el que tiene las claves SSH:

sudo nano /etc/systemd/system/drift-check.service
[Unit]
Description=Detección de configuration drift con Ansible
Wants=network-online.target
After=network-online.target

[Service]
Type=oneshot
User=your_user
ExecStart=/usr/local/bin/drift-check

Y el temporizador que lo lanza cada hora:

sudo nano /etc/systemd/system/drift-check.timer
[Unit]
Description=Comprobación horaria de configuration drift

[Timer]
OnCalendar=hourly
RandomizedDelaySec=5m
Persistent=true

[Install]
WantedBy=timers.target

Activa el temporizador y lanza una ejecución inmediata para comprobarlo:

sudo systemctl daemon-reload
sudo systemctl enable --now drift-check.timer
sudo systemctl start drift-check.service
journalctl -u drift-check.service -n 5 --no-pager
Sep 25 10:00:14 control drift-check[4121]: Desviación detectada: web1 (1 cambios)
Sep 25 10:00:14 control systemd[1]: drift-check.service: Main process exited, code=exited, status=1/FAILURE
Sep 25 10:00:14 control systemd[1]: drift-check.service: Failed with result 'exit-code'.

Una desviación deja el servicio en estado failed, así que cualquier sistema de monitorización que vigile unidades fallidas (systemctl --failed, node_exporter con el collector de systemd, etc.) la detectará sin configuración adicional.

Paso 5: Detectar desviaciones en la infraestructura con Terraform

Ansible cubre lo que hay dentro del servidor. Los cambios en la infraestructura en sí (una regla de firewall añadida desde el panel, un disco redimensionado a mano) se detectan con Terraform. Desde el directorio de tu proyecto, -refresh-only compara el estado guardado con la realidad sin proponer cambios:

terraform plan -refresh-only
Note: Objects have changed outside of Terraform

Terraform detected the following changes made outside of Terraform since the
last "terraform apply" which may have affected this plan:

Debajo aparece cada recurso modificado o borrado fuera de Terraform. Para automatizarlo, usa -detailed-exitcode, que cambia el código de salida de terraform plan:

CódigoSignificado
0La infraestructura coincide con el código
1Error al ejecutar el plan
2Hay diferencias entre el código y la realidad
terraform plan -detailed-exitcode -input=false -lock=false > /dev/null; echo "exit=$?"
exit=2

-lock=false evita que la comprobación periódica bloquee el estado mientras alguien aplica cambios. Puedes añadir esta llamada al mismo script del paso anterior o ejecutarla como job programado en tu CI, que ya tiene las credenciales del proveedor.

Paso 6: Vigilar archivos críticos con AIDE

Ansible y Terraform solo detectan cambios en lo que declaras. AIDE (Advanced Intrusion Detection Environment) cubre el resto: guarda hashes y permisos de los archivos del sistema y avisa de cualquier modificación, esté o no gestionada. Instálalo en cada servidor gestionado:

sudo apt install aide

Si apt te pide configurar Postfix para el envío de correo, elige Sin configuración si no vas a enviar los informes por email.

Genera la base de datos inicial. Tarda unos minutos porque recorre todo el sistema de archivos:

sudo aideinit
sudo cp /var/lib/aide/aide.db.new /var/lib/aide/aide.db

Provoca un cambio y ejecuta una comprobación:

echo "# prueba" | sudo tee -a /etc/hosts
sudo aide --check --config /etc/aide/aide.conf
AIDE found differences between database and filesystem!!

Summary:
  Total number of entries:	160358
  Added entries:		0
  Removed entries:		0
  Changed entries:		1

---------------------------------------------------
Changed entries:
---------------------------------------------------

f   ...    .C... : /etc/hosts

El paquete de Ubuntu ya programa una comprobación diaria; verifica cómo lo hace en tu sistema con systemctl list-timers | grep -i aide o ls /etc/cron.daily/. Cuando un cambio sea legítimo (tras aplicar tu playbook, por ejemplo), actualiza la base de datos para que deje de aparecer:

sudo aide --update --config /etc/aide/aide.conf
sudo cp /var/lib/aide/aide.db.new /var/lib/aide/aide.db

Elimina la línea de prueba de /etc/hosts antes de actualizar la base de datos.

Paso 7: Corregir la desviación

Detectar solo es la mitad del trabajo. Ante cada desviación hay dos decisiones posibles:

  • El cambio manual era un error o ya no hace falta: vuelve al estado declarado. Con Ansible, ejecuta el playbook sin --check limitado al servidor afectado. Con Terraform, terraform apply revierte el recurso a lo que dice el código.
  • El cambio manual era necesario: llévalo al código (el playbook, el módulo de Terraform), haz commit y aplica. En Terraform, si el cambio es solo en el estado (un recurso que ya no existe, atributos actualizados), terraform apply -refresh-only registra la realidad en el estado sin tocar nada.

Corrige la desviación de web1:

cd /opt/drift
ansible-playbook -i inventory.ini site.yml --limit web1
drift-check
Sin desviaciones

La regla que evita que el drift vuelva a aparecer es organizativa más que técnica: cualquier cambio en producción pasa por el repositorio, y los accesos por SSH para depurar terminan con el cambio llevado al código o revertido.

Solución de problemas

  • Una tarea aparece siempre como changed en modo check: el playbook no es idempotente (típico de command, shell o plantillas con marcas de tiempo). Añade creates, changed_when o sustitúyela por un módulo específico; si no, generará falsas alarmas cada hora.
  • drift-check funciona a mano pero falla desde systemd: el servicio corre como your_user sin tu entorno de shell. Comprueba que la clave SSH está en ~/.ssh de ese usuario y que los servidores están en su known_hosts.
  • terraform plan muestra diferencias que nadie ha provocado: algunos proveedores normalizan atributos (mayúsculas, orden de listas). Revisa el diff y, si el atributo lo gestiona otro sistema, usa lifecycle { ignore_changes = [...] } en ese recurso.
  • AIDE informa de cientos de cambios tras cada apt upgrade: es esperado. Actualiza la base de datos después de cada actualización planificada.

Conclusión

Tienes detección de configuration drift en tres capas: Ansible en modo check para la configuración del sistema, terraform plan -detailed-exitcode para la infraestructura y AIDE para cambios en archivos no gestionados, con una comprobación horaria que deja rastro en el journal. Como siguientes pasos, puedes enviar el resultado de drift-check a tu canal de alertas con OnFailure= en la unidad de systemd, ejecutar la comprobación de Terraform como job programado en tu CI y ampliar el playbook para cubrir usuarios, reglas de UFW y parámetros de sysctl.