Al final de la lliçó anterior va quedar plantejat el problema, i convé mirar-lo de cara: tota la configuració de srv-tramontana viu a la memòria de l'administrador i en un manual d'operació en prosa. Els usuaris i grups de 05-01, les regles de sudoers de 05-02, la fixació de versions de paquets de 05-03, l'LVM de 05-04, les unitats i temporitzadors de 05-05, la rotació de registres de 05-06, la configuració de netplan de 06-01, l'sshd_config enfortit de 06-02, les regles d'ufw de 06-03, AIDE i auditd de 06-04, els secrets de 06-05, PAM i AppArmor de 06-06, i els sysctl de 07-03. Tot això es va aplicar a mà, ordre a ordre, al llarg de setmanes.

Les conseqüències són concretes:

  • L'RTO de 8 hores que la Marta va aprovar depèn que algú recordi l'ordre correcte de tots aquells passos, sota pressió i probablement de matinada.
  • La regla de 06-04 —«un servidor compromès es reinstal·la, no es neteja»— és fàcil d'enunciar i molt cara de complir. Si reinstal·lar costa un dia de feina, la temptació de «netejar» és enorme.
  • srv-tramontana-proves no és igual que producció, per molt acurat que fos el user-data de cloud-init. I un entorn de proves que difereix no prova el que et penses que prova.
  • Ningú més que tu no pot reconstruir el servidor.

Aquesta lliçó converteix tot això en codi versionat, revisable i executable. No és una eina més: és el canvi de «jo sé com està configurat el servidor» a «el repositori defineix com està configurat el servidor».

Contingut

  1. Estat desitjat davant d'instruccions
  2. Idempotència, i per què ho canvia tot
  3. Per què Ansible: sense agent, sobre SSH, en YAML
  4. Instal·lació, inventari i variables
  5. Mòduls i tasques
  6. Playbooks: manejadors, condicions, bucles i blocs
  7. Plantilles Jinja2
  8. Rols: organitzar per reutilitzar
  9. Ansible Vault i els secrets
  10. Execució: --check, --diff, --tags i ansible-lint
  11. Cas Tramontana: reconstruir el servidor i mesurar l'RTO

Estat desitjat davant d'instruccions

desplegar.sh és un bon script. Té mode de simulació, blocatge, còpia prèvia, verificació amb curl i reversió automàtica. I tot i així representa un model diferent del que cal aquí.

# Model IMPERATIU: una sequencia d instruccions
sudo useradd -r -u 997 -s /usr/sbin/nologin svc-tramontana
sudo groupadd -g 1002 tramontana
sudo usermod -aG tramontana operador

Executa això dues vegades i la segona falla: l'usuari ja existeix. Per fer-ho repetible cal escriure les comprovacions a mà:

getent passwd svc-tramontana >/dev/null || \
    sudo useradd -r -u 997 -s /usr/sbin/nologin svc-tramontana
getent group tramontana >/dev/null || sudo groupadd -g 1002 tramontana
id -nG operador | grep -qw tramontana || sudo usermod -aG tramontana operador

Funciona, i és el que vas aprendre a fer a 04-06. Però multiplica-ho per les dues-centes operacions que configuren srv-tramontana i tindràs dues mil línies de Bash plenes de comprovacions, cadascuna una oportunitat d'equivocar-se.

# Model DECLARATIU: descriure l estat desitjat
- name: Crear el grup tramontana
  ansible.builtin.group:
    name: tramontana
    gid: 1002
    state: present

- name: Crear el compte de servei
  ansible.builtin.user:
    name: svc-tramontana
    uid: 997
    group: tramontana
    system: true
    shell: /usr/sbin/nologin
    create_home: false
    state: present

La diferència no és de sintaxi: és de què s'escriu. Al primer model descrius com arribar-hi; al segon, on vols ser. L'eina comprova l'estat actual i fa només el que falti. I la conseqüència pràctica és enorme: el fitxer es llegeix com una descripció del servidor, no com un procediment. Algú que l'obri d'aquí a un any entén com està configurada la màquina sense executar-lo.

Imperatiu (Bash) Declaratiu (Ansible)
Què escrius Els passos El resultat
Executar dues vegades Falla, llevat que ho programis Sense efecte la segona vegada
Punt de partida Ha de ser conegut Qualsevol
Es llegeix com Un procediment Una descripció
Bo per a Operacions puntuals, desplegament Configuració

I un aclariment important: Ansible no substitueix desplegar.sh. Desplegar una versió concreta amb reversió és una operació imperativa i l'script ho fa bé. El que substitueix és la configuració del servidor.

Idempotència, i per què ho canvia tot

A 04-06 vas definir idempotència: una operació que es pot executar diverses vegades amb el mateix resultat. Allà era una bona pràctica; en gestió de configuració és la propietat que fa viable tot el model.

$ ansible-playbook -i inventari.yml lloc.yml

PLAY RECAP *******************************************************************
srv-tramontana  : ok=47  changed=12  unreachable=0  failed=0  skipped=3

# I executant-lo un altre cop, sense canviar res
$ ansible-playbook -i inventari.yml lloc.yml

PLAY RECAP *******************************************************************
srv-tramontana  : ok=47  changed=0   unreachable=0  failed=0  skipped=3

Aquell changed=0 de la segona execució és l'objectiu, i té tres conseqüències que convé enunciar:

  1. Es pot executar sense por. No cal preguntar-se si ja es va executar. Si el servidor està com ha d'estar, no passa res.
  2. Detecta la deriva. Si demà executes el playbook i surt changed=3, alguna cosa va canviar fora del codi: algú va editar un fitxer a mà, una actualització va sobreescriure una configuració. El mateix playbook és un control d'integritat, complementari a l'AIDE de 06-04.
  3. Es pot executar periòdicament perquè la configuració torni a convergir sola.

El changed que informa cada tasca és una afirmació sobre el món, no sobre el que va fer l'eina. I per això els mòduls que no ho poden garantir es marquen com a problemàtics, que és la raó de l'advertiment sobre command i shell que veuràs més endavant.

Per què Ansible: sense agent, sobre SSH, en YAML

Ansible Puppet / Chef Salt Terraform
Model Push, sense agent Pull, amb agent Tots dos Push, API
Transport SSH HTTPS propi ZeroMQ o SSH API del proveïdor
Llenguatge YAML DSL propi / Ruby YAML HCL
Què gestiona Configuració Configuració Configuració Aprovisionament
Corba d'entrada Suau Pronunciada Mitjana Mitjana
Escala gran Centenars amb ajustos Milers Milers N/A

Les tres raons per les quals Ansible encaixa aquí:

  • Sense agent. No cal instal·lar res al servidor gestionat ni mantenir un dimoni més. Menys superfície d'atac, en línia amb 06-06.
  • Sobre SSH. Fa servir exactament la infraestructura que vas enfortir a 06-02: claus ed25519, ~/.ssh/config, sshd_config sense contrasenyes. No afegeix un canal nou per assegurar, i l'autenticació i l'auditoria són les que ja tens.
  • YAML. Es llegeix sense conèixer el llenguatge, cosa que importa quan una altra persona ha d'entendre la configuració.

La distinció amb Terraform convé tenir-la clara perquè es confonen: Terraform aprovisiona —crea la màquina virtual, la xarxa, el disc— i Ansible configura el que hi ha a dins. Són complementaris: a 07-04 vas aprovisionar amb virt-install i cloud-init; ara configures amb Ansible.

Instal·lació, inventari i variables

Ansible s'instal·la a l'equip de control, no al servidor gestionat:

# A portatil-alumne
$ sudo apt install ansible ansible-lint
$ ansible --version | head -2
ansible [core 2.16.3]
  config file = /home/alumne/tramontana-infra/ansible.cfg

El repositori, versionat a git:

$ mkdir -p ~/tramontana-infra/{inventari,group_vars,host_vars,roles,plantilles}
$ cd ~/tramontana-infra && git init
# ansible.cfg
[defaults]
inventory = inventari/produccio.yml
roles_path = roles
host_key_checking = True
# Veure els canvis de fitxer a cada execucio: aplica la convencio
# del curs de "diff -u despres d editar"
diff = True
stdout_callback = yaml
callbacks_enabled = profile_tasks, timer
interpreter_python = auto_silent
retry_files_enabled = False

[ssh_connection]
# Reutilitza la connexio SSH entre tasques: redueix molt el temps total
pipelining = True
ssh_args = -o ControlMaster=auto -o ControlPersist=300s

host_key_checking = True és deliberat i va contra el que recomanen molts tutorials: desactivar-lo evita l'avís d'empremta desconeguda i, amb ell, la protecció contra intermediaris que vas estudiar a 06-02. Es deixa actiu i s'accepta l'empremta la primera vegada.

pipelining = True mereix esment: sense ell, Ansible copia un script al servidor i l'executa, per a cada tasca. Amb ell, l'envia per la connexió ja oberta. La diferència en un playbook de cinquanta tasques són minuts.

L'inventari

# inventari/produccio.yml
all:
  children:
    servidors_web:
      hosts:
        srv-tramontana:
          ansible_host: 10.0.2.15
          entorn: produccio
    servidors_proves:
      hosts:
        srv-tramontana-proves:
          ansible_host: 192.168.122.104
          entorn: proves
  vars:
    ansible_user: operador
    ansible_ssh_private_key_file: ~/.ssh/id_ed25519
    ansible_python_interpreter: /usr/bin/python3
$ ansible-inventory --graph
@all:
  |--@servidors_proves:
  |  |--srv-tramontana-proves
  |--@servidors_web:
  |  |--srv-tramontana

$ ansible all -m ping
srv-tramontana | SUCCESS => {"changed": false, "ping": "pong"}
srv-tramontana-proves | SUCCESS => {"changed": false, "ping": "pong"}

Variables per grup i per màquina

La precedència de variables permet tenir una definició comuna i diferències per entorn, que és el que fa possible que proves i producció siguin gairebé idèntics de manera controlada:

# group_vars/all.yml — comu a totes les maquines
tramontana_usuari_servei: svc-tramontana
tramontana_uid_servei: 997
tramontana_grup: tramontana
tramontana_gid: 1002
tramontana_port: 8080
tramontana_versio: "3.2.1"

tramontana_rutes:
  - { ruta: /opt/tramontana,                 mode: '0755', propietari: root,           grup: root }
  - { ruta: /opt/tramontana/releases,        mode: '0755', propietari: root,           grup: root }
  - { ruta: /opt/tramontana/shared/uploads,  mode: '2770', propietari: svc-tramontana, grup: tramontana }
  - { ruta: /etc/tramontana,                 mode: '0750', propietari: root,           grup: tramontana }
  - { ruta: /var/log/tramontana,             mode: '0750', propietari: svc-tramontana, grup: adm }
  - { ruta: /srv/tramontana/backups,         mode: '2770', propietari: operador,       grup: tramontana }

paquets_base:
  - postgresql-16
  - restic
  - ufw
  - fail2ban
  - auditd
  - aide
  - libpam-pwquality
  - needrestart
  - sysstat

sysctl_rendiment:
  net.core.default_qdisc: fq
  net.ipv4.tcp_congestion_control: bbr
  vm.swappiness: 10
  vm.dirty_background_ratio: 5
  vm.dirty_ratio: 10
  fs.inotify.max_user_watches: 524288

sysctl_seguretat:
  kernel.dmesg_restrict: 1
  kernel.kptr_restrict: 2
  kernel.randomize_va_space: 2
  fs.protected_hardlinks: 1
  fs.protected_symlinks: 1
  fs.protected_fifos: 2
  fs.protected_regular: 2
  kernel.unprivileged_bpf_disabled: 1
  kernel.yama.ptrace_scope: 1
# group_vars/servidors_web.yml — nomes produccio
tramontana_log_nivell: info
tramontana_max_connexions: 80
tramontana_xarxa_interna: 10.0.2.0/24
copia_hora: "02:30"
# group_vars/servidors_proves.yml — nomes proves
tramontana_log_nivell: debug
tramontana_max_connexions: 20
tramontana_xarxa_interna: 192.168.122.0/24
copia_hora: "22:00"
# Eines que en produccio no calen
paquets_extra:
  - strace
  - bpfcc-tools
  - bpftrace
  - linux-tools-generic

Aquí està el valor concret: les diferències entre entorns són explícites i caben en deu línies. A 07-04, amb cloud-init, calia mantenir dos fitxers paral·lels i confiar a no oblidar res. Aquí, el que no apareix a group_vars/servidors_proves.yml és idèntic per construcció.

Els fets (facts) són les variables que Ansible descobreix de cada màquina:

$ ansible srv-tramontana -m ansible.builtin.setup \
      -a 'filter=ansible_distribution*,ansible_memtotal_mb,ansible_processor_vcpus'
srv-tramontana | SUCCESS => {
    "ansible_facts": {
        "ansible_distribution": "Ubuntu",
        "ansible_distribution_version": "24.04",
        "ansible_memtotal_mb": 3891,
        "ansible_processor_vcpus": 2
    }
}

Mòduls i tasques

Un mòdul és una unitat de feina idempotent. Els que es fan servir de debò:

Mòdul Per a què Nota
ansible.builtin.apt Paquets a Debian/Ubuntu state: present, latest, absent
ansible.builtin.copy Copiar un fitxer Amb mode, owner, group, backup
ansible.builtin.template Copiar processant Jinja2 El que més es fa servir
ansible.builtin.file Directoris, enllaços, permisos state: directory, link, absent
ansible.builtin.lineinfile Una línia en un fitxer Últim recurs: millor template
ansible.builtin.user / group Identitats uid/gid explícits
ansible.builtin.systemd_service Serveis i unitats enabled, state, daemon_reload
ansible.posix.sysctl Paràmetres del nucli Amb sysctl_file
community.general.ufw Tallafocs
community.general.lvol / filesystem LVM i sistemes de fitxers
ansible.builtin.command / shell Executar una ordre Últim recurs

I l'advertiment sobre els dos últims, que és una de les coses que separa un playbook bo d'un de dolent:

# MALAMENT: no es idempotent. S executa sempre i sempre informa 'changed'.
- name: Inicialitzar la base de dades d AIDE
  ansible.builtin.command: aideinit

# BE: 'creates' fa que la tasca se salti si el fitxer ja existeix
- name: Inicialitzar la base de dades d AIDE
  ansible.builtin.command:
    cmd: aideinit -y -f
    creates: /var/lib/aide/aide.db

Les tres formes de fer idempotent una ordre:

Opció Quan
creates: <ruta> L'ordre produeix un fitxer: si existeix, no s'executa
removes: <ruta> Només s'executa si el fitxer existeix
changed_when: Ho decideixes tu, segons la sortida o el codi de retorn
- name: Comprovar si hi ha serveis pendents de reinici
  ansible.builtin.command: needrestart -r l -p
  register: pendents
  changed_when: false          # mai no modifica res: es una consulta
  failed_when: pendents.rc > 2

changed_when: false a les consultes és el que manté net el PLAY RECAP. Un playbook que sempre informa changed en cinc tasques perd el seu valor com a detector de deriva, perquè no es distingeix el soroll del senyal.

Playbooks: manejadors, condicions, bucles i blocs

# lloc.yml
- name: Configurar els servidors de Tramontana
  hosts: all
  become: true
  gather_facts: true

  tasks:
    # ---------- Identitats ----------
    - name: Crear el grup tramontana
      ansible.builtin.group:
        name: "{{ tramontana_grup }}"
        gid: "{{ tramontana_gid }}"
        state: present
      tags: [usuaris]

    - name: Crear el compte de servei
      ansible.builtin.user:
        name: "{{ tramontana_usuari_servei }}"
        uid: "{{ tramontana_uid_servei }}"
        group: "{{ tramontana_grup }}"
        system: true
        shell: /usr/sbin/nologin
        create_home: false
        state: present
      tags: [usuaris]

    # ---------- Rutes: un bucle sobre la llista de group_vars ----------
    - name: Crear l arbre de directoris amb els seus permisos
      ansible.builtin.file:
        path: "{{ item.ruta }}"
        state: directory
        mode: "{{ item.mode }}"
        owner: "{{ item.propietari }}"
        group: "{{ item.grup }}"
      loop: "{{ tramontana_rutes }}"
      loop_control:
        label: "{{ item.ruta }}"       # sortida llegible en lloc del dict sencer
      tags: [rutes]

    # ---------- Paquets ----------
    - name: Instal·lar els paquets base
      ansible.builtin.apt:
        name: "{{ paquets_base + (paquets_extra | default([])) }}"
        state: present
        update_cache: true
        cache_valid_time: 3600
      tags: [paquets]

    - name: Fixar la versio de PostgreSQL
      ansible.builtin.dpkg_selections:
        name: postgresql-16
        selection: hold
      tags: [paquets]

    # ---------- sysctl ----------
    - name: Aplicar els parametres de seguretat del nucli
      ansible.posix.sysctl:
        name: "{{ item.key }}"
        value: "{{ item.value }}"
        sysctl_file: /etc/sysctl.d/60-enfortiment.conf
        sysctl_set: true
        reload: true
      loop: "{{ sysctl_seguretat | dict2items }}"
      loop_control:
        label: "{{ item.key }}"
      tags: [nucli, seguretat]

    - name: Aplicar els parametres de rendiment
      ansible.posix.sysctl:
        name: "{{ item.key }}"
        value: "{{ item.value }}"
        sysctl_file: /etc/sysctl.d/70-rendiment.conf
        sysctl_set: true
        reload: true
      loop: "{{ sysctl_rendiment | dict2items }}"
      loop_control:
        label: "{{ item.key }}"
      tags: [nucli]

    # ---------- Configuracio des de plantilla ----------
    - name: Generar app.conf des de la plantilla
      ansible.builtin.template:
        src: plantilles/app.conf.j2
        dest: /etc/tramontana/app.conf
        owner: root
        group: "{{ tramontana_grup }}"
        mode: '0640'
        backup: true                 # deixa una copia amb data, com .bak-
        validate: "grep -q '^db_host=' %s"
      notify: Reiniciar tramontana
      tags: [config]

    # ---------- SSH, amb la xarxa de seguretat de 06-02 ----------
    - name: Enfortir la configuracio d SSH
      ansible.builtin.template:
        src: plantilles/sshd_tramontana.conf.j2
        dest: /etc/ssh/sshd_config.d/60-tramontana.conf
        owner: root
        group: root
        mode: '0600'
        # NO s aplica si la sintaxi es incorrecta: evita quedar-te fora
        validate: /usr/sbin/sshd -t -f %s
      notify: Recarregar ssh
      tags: [ssh, seguretat]

    # ---------- Tallafocs, en l ORDRE correcte ----------
    - name: Politica per defecte de denegar entrant
      community.general.ufw:
        direction: incoming
        policy: deny
      tags: [firewall]

    - name: Permetre SSH ABANS d habilitar el tallafocs
      community.general.ufw:
        rule: limit
        port: '22'
        proto: tcp
        comment: 'SSH amb limit de taxa'
      tags: [firewall]

    - name: Permetre PostgreSQL nomes des de la xarxa interna
      community.general.ufw:
        rule: allow
        port: '5432'
        proto: tcp
        src: "{{ tramontana_xarxa_interna }}"
      tags: [firewall]

    - name: Habilitar el tallafocs
      community.general.ufw:
        state: enabled
      tags: [firewall]

    # ---------- Servei ----------
    - name: Instal·lar la unitat de systemd
      ansible.builtin.template:
        src: plantilles/tramontana.service.j2
        dest: /etc/systemd/system/tramontana.service
        owner: root
        group: root
        mode: '0644'
      notify:
        - Recarregar systemd
        - Reiniciar tramontana
      tags: [servei]

    - name: Activar el servei
      ansible.builtin.systemd_service:
        name: tramontana.service
        enabled: true
        state: started
        daemon_reload: true
      tags: [servei]

    # ---------- Bloc amb gestio d errors ----------
    - name: Inicialitzar AIDE
      block:
        - name: Generar la base de dades d integritat
          ansible.builtin.command:
            cmd: aideinit -y -f
            creates: /var/lib/aide/aide.db.new
          register: aide_init

        - name: Posar la base de dades al seu lloc
          ansible.builtin.copy:
            src: /var/lib/aide/aide.db.new
            dest: /var/lib/aide/aide.db
            remote_src: true
            mode: '0600'
          when: aide_init.changed
      rescue:
        - name: Avisar que AIDE no s ha pogut inicialitzar
          ansible.builtin.debug:
            msg: "AIDE no s ha inicialitzat. Revisar a ma; no bloqueja la resta."
      always:
        - name: Registrar l intent
          ansible.builtin.lineinfile:
            path: /var/log/tramontana/ansible.log
            line: "{{ ansible_date_time.iso8601 }} AIDE: {{ aide_init.rc | default('sense executar') }}"
            create: true
            mode: '0640'
            owner: "{{ tramontana_usuari_servei }}"
            group: adm
      tags: [seguretat, aide]

  handlers:
    - name: Recarregar systemd
      ansible.builtin.systemd_service:
        daemon_reload: true

    - name: Reiniciar tramontana
      ansible.builtin.systemd_service:
        name: tramontana.service
        state: restarted

    - name: Recarregar ssh
      ansible.builtin.systemd_service:
        name: ssh.service
        state: reloaded

Els mecanismes que hi apareixen i mereixen explicació:

notify i manejadors. Un manejador s'executa només si la tasca que el notifica ha informat changed, i una sola vegada al final del play encara que el notifiquin cinc tasques. És exactament el que vols: si app.conf i la unitat canvien, el servei es reinicia una vegada, no dues. I si no canvia res, no es reinicia.

validate. El mòdul escriu el fitxer en un temporal, executa l'ordre de validació amb %s substituït per aquella ruta, i només si té èxit el posa al seu lloc. A la tasca d'SSH això és la xarxa de seguretat de 06-02 automatitzada: un sshd_config amb un error de sintaxi no arriba mai a instal·lar-se, així que no et quedes fora.

backup: true. Deixa una còpia amb data abans de sobreescriure. És la convenció .bak-$(date +%F) del curs, aplicada per l'eina.

L'ordre de les tasques d'ufw. Permetre SSH abans d'habilitar el tallafocs, igual que a 06-03. En un playbook l'ordre és explícit i queda documentat, cosa que és un avantatge sobre un manual d'operació en prosa on aquell detall es pot passar per alt.

block/rescue/always. L'equivalent del trap de 04-06: rescue s'executa si alguna cosa del bloc falla, always sempre. Aquí impedeix que una fallada d'AIDE avorti tota la configuració.

Plantilles Jinja2

És on la gestió de configuració deixa de ser «copiar fitxers» i passa a ser útil:

{# plantilles/app.conf.j2 #}
# FITXER GENERAT PER ANSIBLE — NO EDITAR A MA
# Qualsevol canvi manual es perdra en la propera execucio.
# Font: {{ template_path | default('plantilles/app.conf.j2') }}
# Entorn: {{ entorn }}
# Generat: {{ ansible_date_time.iso8601 }}

db_host=127.0.0.1
db_port=5432
db_name=tramontana_reserves
max_connexions={{ tramontana_max_connexions }}
timeout_consulta={{ tramontana_timeout | default(30) }}
log_nivell={{ tramontana_log_nivell }}
escolta=127.0.0.1
port={{ tramontana_port }}

{% if entorn == 'proves' %}
# Nomes en proves: traces detallades i dades sintetiques
traca_sql=true
dades_sintetiques=true
{% endif %}

{% for casa in cases | default([]) %}
casa_activa={{ casa }}
{% endfor %}
$ ansible-playbook lloc.yml --tags config --diff --limit srv-tramontana-proves

TASK [Generar app.conf des de la plantilla] ***********************************
--- before: /etc/tramontana/app.conf
+++ after: /etc/tramontana/app.conf
@@ -1,10 +1,14 @@
-# FITXER GENERAT PER ANSIBLE — NO EDITAR A MA
-# Entorn: proves
-max_connexions=20
+# FITXER GENERAT PER ANSIBLE — NO EDITAR A MA
+# Entorn: proves
+max_connexions=20
+traca_sql=true
+dades_sintetiques=true
changed: [srv-tramontana-proves]

Aquella capçalera de «no editar a mà» no és decorativa: sense ella, algú editarà el fitxer, funcionarà durant un mes, i la següent execució del playbook ho revertirà sense avisar. Amb ella, almenys sap què passa.

I una plantilla més, la de la unitat de systemd, que mostra el valor de generar configuració a partir de variables:

{# plantilles/tramontana.service.j2 #}
# GENERAT PER ANSIBLE — NO EDITAR
[Unit]
Description=Tramontana Reserves ({{ entorn }})
After=network-online.target postgresql.service
Wants=network-online.target

[Service]
Type=exec
User={{ tramontana_usuari_servei }}
Group={{ tramontana_grup }}
ExecStart=/opt/tramontana/app/tramontana --config /etc/tramontana/app.conf
Restart=on-failure
RestartSec=5s
LoadCredentialEncrypted=db_password:/etc/tramontana/secrets/db_password.cred

# Enfortiment (06-06). Exposicio mesurada: 1.6 OK
NoNewPrivileges=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectSystem=strict
ProtectHome=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
RestrictSUIDSGID=yes
LockPersonality=yes
RestrictNamespaces=yes
RestrictRealtime=yes
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
SystemCallArchitectures=native
SystemCallFilter=@system-service
SystemCallFilter=~@privileged @obsolete
SystemCallFilter=getrandom
CapabilityBoundingSet=
ReadWritePaths=/var/log/tramontana /opt/tramontana/shared/uploads

MemoryMax={{ tramontana_memoria_max | default('512M') }}
TasksMax={{ tramontana_tasques_max | default(64) }}
LimitNOFILE=8192

[Install]
WantedBy=multi-user.target

Fixa't en SystemCallFilter=getrandom: és l'excepció que vas descobrir depurant el SIGSYS a l'exercici de 06-06. Ara és al codi, amb la unitat completa, en lloc de en un drop-in que algú hauria de recordar.

Rols: organitzar per reutilitzar

Un playbook de tres-centes línies és immanejable. Els rols el divideixen en peces amb estructura fixa:

$ ansible-galaxy init roles/tramontana
$ tree roles/ -L 2
roles/
├── comu
│   ├── defaults/main.yml      # variables per defecte (prioritat baixa)
│   ├── files/                 # fitxers a copiar tal qual
│   ├── handlers/main.yml
│   ├── meta/main.yml          # dependencies amb altres rols
│   ├── tasks/main.yml         # les tasques
│   ├── templates/             # plantilles Jinja2
│   └── vars/main.yml          # variables del rol (prioritat alta)
├── seguretat
└── tramontana
# lloc.yml, refactoritzat
- name: Configurar els servidors de Tramontana
  hosts: all
  become: true
  roles:
    - role: comu           # usuaris, paquets, sysctl, zona horaria
    - role: seguretat      # ssh, ufw, fail2ban, auditd, aide, pam, apparmor
    - role: tramontana     # rutes, app.conf, unitat, temporitzadors, logrotate

Tres línies, i cada rol és reutilitzable. L'avantatge es nota quan hi hagi un segon servidor: comu i seguretat s'apliquen tal qual, i només canvia el tercer.

# roles/seguretat/meta/main.yml
dependencies:
  - role: comu

I Ansible Galaxy per no reescriure el que ja està escrit:

$ ansible-galaxy collection install community.general ansible.posix
$ ansible-galaxy role install geerlingguy.postgresql

Amb el mateix advertiment que a 05-03 sobre els dipòsits de tercers: instal·lar un rol de Galaxy és executar codi aliè amb privilegis de root al teu servidor. Es revisa abans, i es fixa la versió.

Ansible Vault i els secrets

Els secrets no poden anar en clar en un repositori. Vault els xifra dins del mateix repositori:

$ ansible-vault create group_vars/all/secrets.yml
New Vault password:
Confirm New Vault password:
# Contingut (desxifrat)
vault_db_password: "Zx9K2pQ7vLm4RtWn"
vault_restic_password: "R3st1c-Tr4m0nt4n4-2026"
vault_aide_clau: "..."
$ head -3 group_vars/all/secrets.yml
$ANSIBLE_VAULT;1.1;AES256
36613264313632373933353436646138613831306638386566613864333331373766373530353137
6134663137363134333661386133343338326238623166390a3833326566...

$ ansible-vault view group_vars/all/secrets.yml
$ ansible-vault edit group_vars/all/secrets.yml
$ ansible-vault rekey group_vars/all/secrets.yml    # canviar la contrasenya

Xifrar un valor solt, que permet que la resta del fitxer continuï sent llegible en un diff:

$ ansible-vault encrypt_string 'Zx9K2pQ7vLm4RtWn' --name 'vault_db_password'
vault_db_password: !vault |
          $ANSIBLE_VAULT;1.1;AES256
          36613264313632373933353436646138613831306638386566613864333331...

I la integració amb el pass de 06-05, que evita tenir dues fonts de veritat dels secrets:

$ cat ~/.ansible-vault-pass.sh
#!/usr/bin/env bash
# Obte la contrasenya de Vault des de pass, per no tenir-la en un fitxer.
set -euo pipefail
pass tramontana/ansible/vault
$ chmod 700 ~/.ansible-vault-pass.sh
# ansible.cfg
[defaults]
vault_password_file = ~/.ansible-vault-pass.sh
$ ansible-playbook lloc.yml    # ja no demana la contrasenya

Així pass continua sent la font de veritat de tots els secrets, amb el seu historial de git i la seva còpia fora del servidor, i Vault és només el mecanisme de transport cap al playbook.

La convenció de noms vault_* per a les variables xifrades, i referenciar-les des de variables normals, permet veure d'un cop d'ull què és secret:

# group_vars/all/main.yml
tramontana_db_password: "{{ vault_db_password }}"

I un advertiment: una variable de Vault feta servir en una plantilla acaba en clar al servidor. Vault protegeix el repositori, no el destí. La credencial de la base de dades continua anant a systemd-creds com a 06-05:

- name: Instal·lar la credencial xifrada de la base de dades
  ansible.builtin.shell:
    cmd: |
      set -o pipefail
      printf '%s' '{{ tramontana_db_password }}' \
        | systemd-creds encrypt --name=db_password - \
              /etc/tramontana/secrets/db_password.cred
    creates: /etc/tramontana/secrets/db_password.cred
  no_log: true          # <- IMPRESCINDIBLE: sense aixo, surt a la sortida
  notify: Reiniciar tramontana

no_log: true és obligatori en qualsevol tasca que manegi un secret. Sense ell, Ansible imprimeix l'ordre completa —amb la contrasenya— a la sortida i a qualsevol registre que es desi. És el mateix principi de 06-05: un secret a la línia d'ordres és un secret exposat.

Execució: --check, --diff, --tags i ansible-lint

# --check: MODE SIMULACIO. Es el --dry-run del curs, aplicat a tot.
$ ansible-playbook lloc.yml --check --diff --limit srv-tramontana-proves

TASK [comu : Instal·lar els paquets base] *************************************
changed: [srv-tramontana-proves]

TASK [tramontana : Generar app.conf des de la plantilla] **********************
--- before
+++ after
@@ -5,7 +5,7 @@
-max_connexions=200
+max_connexions=20
changed: [srv-tramontana-proves]

PLAY RECAP ********************************************************************
srv-tramontana-proves : ok=44  changed=7  unreachable=0  failed=0

--check --diff junts són l'eina més valuosa del dia a dia: diuen exactament què canviaria i quines línies de cada fitxer, sense tocar res. És la convenció del curs —simular abans d'actuar— aplicada a la configuració sencera del servidor.

Amb una limitació honesta: en mode --check, les tasques que depenen del resultat d'una anterior poden fallar o informar malament, perquè l'anterior no es va executar de debò. Un command amb creates sobre un fitxer que es crearia en una tasca prèvia donarà un fals positiu.

# Limitar l abast
$ ansible-playbook lloc.yml --limit srv-tramontana-proves
$ ansible-playbook lloc.yml --tags firewall,ssh
$ ansible-playbook lloc.yml --skip-tags paquets
$ ansible-playbook lloc.yml --start-at-task "Instal·lar la unitat de systemd"

# Detall creixent
$ ansible-playbook lloc.yml -v      # resultats
$ ansible-playbook lloc.yml -vvv    # + connexio SSH i moduls

I l'anàlisi estàtica, que és a Ansible el que shellcheck és a Bash:

$ ansible-lint
WARNING  Listing 3 violation(s) that are fatal
risky-file-permissions: File permissions unset or incorrect
roles/tramontana/tasks/main.yml:24 Task/Handler: Copiar l script de backup

command-instead-of-module: apt-mark used in place of dpkg_selections module
roles/comu/tasks/main.yml:41 Task/Handler: Fixar la versio de postgresql

no-changed-when: Commands should not change things if nothing needs doing
roles/seguretat/tasks/main.yml:88 Task/Handler: Comprovar reinicis pendents

$ ansible-lint --write     # corregeix automaticament el que pot

Els tres avisos són reals i del tipus que importa: permisos sense declarar —que deixarien el fitxer amb l'umask per defecte—, una ordre on hi ha un mòdul idempotent, i una consulta sense changed_when: false que embruta el PLAY RECAP. ansible-lint al ganxo de pre-commit de git és la manera que no se'n colin.

Cas Tramontana: reconstruir el servidor i mesurar l'RTO

L'objectiu del mòdul: que reconstruir srv-tramontana des de zero sigui una operació de codi, no de memòria.

L'experiment

# 1. Una maquina neta, sense res configurat
$ virsh destroy srv-tramontana-proves 2>/dev/null
$ virsh undefine srv-tramontana-proves --remove-all-storage
$ cd /var/lib/libvirt/images
$ sudo qemu-img create -f qcow2 -F qcow2 \
      -b noble-server-cloudimg-amd64.img proves.qcow2 20G
$ sudo virt-install --name srv-tramontana-proves \
      --memory 2048 --vcpus 2 --cpu host-passthrough \
      --disk path=/var/lib/libvirt/images/proves.qcow2,bus=virtio \
      --disk path=/var/lib/libvirt/images/llavor.iso,device=cdrom \
      --network network=default,model=virtio --os-variant ubuntu24.04 \
      --graphics none --import --noautoconsole

# El cloud-init minim: nomes usuari, clau SSH i python3. Res mes.
# Tota la resta ho fa Ansible.
$ sleep 90 && ansible srv-tramontana-proves -m ping
srv-tramontana-proves | SUCCESS => {"changed": false, "ping": "pong"}

La reconstrucció, cronometrada

$ time ansible-playbook lloc.yml --limit srv-tramontana-proves

PLAY [Configurar els servidors de Tramontana] *********************************

TASK [comu : Crear el grup tramontana] ****************************************
changed: [srv-tramontana-proves]
...
TASK [tramontana : Activar el temporitzador de backup] ************************
changed: [srv-tramontana-proves]

RUNNING HANDLER [Recarregar systemd] ******************************************
changed: [srv-tramontana-proves]

RUNNING HANDLER [Reiniciar tramontana] ****************************************
changed: [srv-tramontana-proves]

PLAY RECAP ********************************************************************
srv-tramontana-proves : ok=118  changed=94  unreachable=0  failed=0  skipped=6

real	6m41.882s
user	0m48.204s
sys	0m12.118s

La verificació

Un playbook que acaba sense error no significa que el servidor funcioni. Cal comprovar-ho amb els mateixos criteris de sempre:

$ ansible srv-tramontana-proves -m shell -a '~/scripts/revisio_salut.sh; echo "estat: $?"'
srv-tramontana-proves | CHANGED | rc=0 >>
estat: 0

$ ansible srv-tramontana-proves -b -m shell -a 'systemd-analyze security tramontana.service | tail -1'
→ Overall exposure level for tramontana.service: 1.6 OK 🙂

$ ansible srv-tramontana-proves -b -m shell -a 'ufw status verbose | head -6'
$ ansible srv-tramontana-proves -b -m shell -a 'sshd -T | grep -E "permitrootlogin|passwordauth"'
permitrootlogin no
passwordauthentication no

$ ansible srv-tramontana-proves -b -m shell -a 'sysctl -n kernel.yama.ptrace_scope net.ipv4.tcp_congestion_control'
1
bbr

$ ansible srv-tramontana-proves -b -m shell -a 'aa-status --enabled && echo "apparmor actiu"'
apparmor actiu

Exposició 1.6, SSH enfortit, sysctl aplicats, AppArmor actiu. Idèntic a producció, perquè surt del mateix codi.

I la segona execució

$ ansible-playbook lloc.yml --limit srv-tramontana-proves

PLAY RECAP ********************************************************************
srv-tramontana-proves : ok=118  changed=0   unreachable=0  failed=0  skipped=6

real	1m14.402s

changed=0: el playbook és idempotent de debò. I aquella segona execució de 74 segons és ara també un detector de deriva que es pot llançar setmanalment.

L'RTO

Fase Abans (manual) Amb Ansible
Aprovisionar la màquina 20-30 min 2 min (cloud-init)
Configurar el sistema 6-7 hores 7 min
Restaurar les dades 30-45 min 30-45 min (restic)
Verificar 30 min 5 min (comprovacions automàtiques)
Total ~8 hores ~50 minuts

De 8 hores a menys d'una. I la millora no és només el temps: la reconstrucció manual és propensa a errors i depèn d'una persona concreta; l'automatitzada produeix sempre el mateix resultat i la pot llançar qualsevol de l'equip amb accés al repositori.

Això és el que converteix la regla de 06-04 en un procediment complible: «un servidor compromès es reinstal·la, no es neteja» deixa de ser un consell incòmode quan reinstal·lar costa cinquanta minuts. I convé actualitzar el manual d'operació i parlar amb la Marta: l'RTO acordat pot baixar de 8 hores a 2, amb marge.

El que queda fora, i cal dir-ho

No automatitzat Per què
La clau GPG de pass És la clau mestra: es restaura a mà des de la seva custòdia física
El certificat de Let's Encrypt Es reemet amb certbot; no té sentit copiar-lo
La base de dades d'AIDE S'ha de generar a la màquina reconstruïda, no copiar-se
Les dades (restic) És un procés a part, amb la seva pròpia verificació
La decisió de reconstruir És humana

Errors Comuns i Consells

  • Fer servir command o shell on hi ha un mòdul. Perd la idempotència i la comprovació d'estat. ansible-lint ho detecta.
  • Oblidar changed_when: false a les consultes. El playbook informa changed sempre i perd el seu valor com a detector de deriva.
  • Oblidar no_log: true en una tasca amb secrets. El secret apareix a la sortida i a qualsevol registre que es conservi.
  • Desactivar host_key_checking. És el que recomanen molts tutorials, i elimina la protecció contra intermediaris de 06-02.
  • Editar a mà un fitxer generat per plantilla. Es reverteix en la següent execució. D'aquí la capçalera de «NO EDITAR A MÀ».
  • No fer servir validate en configuracions crítiques. Un sshd_config amb un error de sintaxi et deixa fora. validate: /usr/sbin/sshd -t -f %s ho impedeix.
  • Habilitar ufw abans de permetre SSH. El mateix error de 06-03, ara en codi. L'ordre de les tasques importa.
  • Executar contra producció sense provar en proves. Per a això hi ha --limit, i per a això existeix srv-tramontana-proves.
  • Refiar-se cegament de --check. Les tasques que depenen d'una anterior poden donar falsos positius perquè l'anterior no es va executar.
  • Instal·lar rols de Galaxy sense revisar-los. És executar codi aliè com a root al teu servidor. Revisa'l i fixa la versió.
  • Creure que un playbook sense errors significa un servidor funcionant. Verifica sempre amb comprovacions reals: revisio_salut.sh, sshd -T, systemd-analyze security.
  • Consell de mètode. El playbook és documentació executable: l'única que no pot quedar desfasada, perquè si difereix de la realitat el changed t'ho diu. Tracta'l com a codi: git, revisió de canvis, ansible-lint a pre-commit, i provat en proves abans de producció.

Exercicis

Exercici 1

Escriu el rol seguretat que apliqui l'enfortiment de 06-06: PAM amb pam_pwquality i pam_faillock, els sysctl de seguretat, els muntatges amb noexec, i els mòduls del nucli blocats. Presta especial atenció a no deixar la màquina inaccessible, i explica cada precaució.

Exercici 2

ansible-playbook lloc.yml --check sobre producció informa changed=5, quan hauria de ser 0. Dissenya el procediment per investigar d'on ve aquella deriva i decidir què fer en cada cas.

Exercici 3

La Marta pregunta si amb Ansible es pot reduir l'RTO acordat de 8 hores. Redacta la resposta amb el nou número, el que continua sense estar automatitzat, i què caldria per baixar més.

Solucions

Solució 1

# roles/seguretat/defaults/main.yml
seguretat_pam_minlen: 12
seguretat_pam_minclass: 3
seguretat_faillock_deny: 5
seguretat_faillock_unlock: 600
seguretat_muntatges_noexec: true

seguretat_moduls_blocats:
  - cramfs
  - freevxfs
  - jffs2
  - hfs
  - hfsplus
  - udf
  - dccp
  - sctp
  - rds
  - tipc
# roles/seguretat/tasks/main.yml
---
# =====================================================================
# PRECAUCIO GENERAL: aquest rol pot deixar la maquina inaccessible.
# Es prova SEMPRE contra srv-tramontana-proves abans de produccio.
# =====================================================================

# ---------- sysctl de seguretat ----------
# Risc baix: un valor invalid falla a la tasca, no en arrencar.
- name: Aplicar els parametres de seguretat del nucli
  ansible.posix.sysctl:
    name: "{{ item.key }}"
    value: "{{ item.value }}"
    sysctl_file: /etc/sysctl.d/60-enfortiment.conf
    sysctl_set: true
    state: present
    reload: true
  loop: "{{ sysctl_seguretat | dict2items }}"
  loop_control:
    label: "{{ item.key }}"
  tags: [nucli]

# ---------- Moduls blocats ----------
- name: Blocar moduls del nucli no utilitzats
  ansible.builtin.template:
    src: blacklist-tramontana.conf.j2
    dest: /etc/modprobe.d/blacklist-tramontana.conf
    owner: root
    group: root
    mode: '0644'
  notify: Regenerar initramfs      # <- IMPRESCINDIBLE (llico de 07-01)
  tags: [moduls]

# ---------- PAM: LA PART PERILLOSA ----------
# Un error de sintaxi a PAM impedeix TOT inici de sessio, inclosa la
# consola. Precaucions, en ordre:

- name: "PAM | Precaucio 1: verificar que hi ha acces alternatiu"
  ansible.builtin.assert:
    that:
      - ansible_connection != 'local'
      - inventory_hostname in groups['servidors_proves'] or pam_confirmat | default(false)
    fail_msg: >-
      Configurar PAM en produccio requereix -e pam_confirmat=true i una
      sessio de root oberta en un altre terminal. Prova primer en proves.
  tags: [pam]

- name: "PAM | Precaucio 2: copia de seguretat amb data"
  ansible.builtin.copy:
    src: "{{ item }}"
    dest: "{{ item }}.bak-{{ ansible_date_time.date }}"
    remote_src: true
    mode: preserve
    force: false            # no sobreescriu una copia previa del mateix dia
  loop:
    - /etc/pam.d/common-auth
    - /etc/pam.d/common-password
  tags: [pam]

- name: "PAM | Instal·lar libpam-pwquality"
  ansible.builtin.apt:
    name: libpam-pwquality
    state: present
  tags: [pam]

- name: "PAM | Politica de qualitat de contrasenyes"
  ansible.builtin.template:
    src: pwquality.conf.j2
    dest: /etc/security/pwquality.conf
    owner: root
    group: root
    mode: '0644'
  tags: [pam]

- name: "PAM | Configuracio de faillock"
  ansible.builtin.template:
    src: faillock.conf.j2
    dest: /etc/security/faillock.conf
    owner: root
    group: root
    mode: '0644'
  tags: [pam]

# La pila de PAM es toca amb pam_auth_update, que valida i mante la
# coherencia, en lloc d editar common-auth a ma amb lineinfile.
- name: "PAM | Activar els moduls mitjancant pam-auth-update"
  ansible.builtin.command:
    cmd: pam-auth-update --enable pwquality faillock
  register: pam_update
  changed_when: "'Nothing to do' not in pam_update.stdout"
  tags: [pam]

# ---------- Precaucio 3: VERIFICAR que es pot autenticar ----------
# Si aixo falla, cal revertir IMMEDIATAMENT amb la sessio que continua
# oberta.
- name: "PAM | Verificar que l autenticacio continua funcionant"
  ansible.builtin.command:
    cmd: "runuser -u {{ ansible_user }} -- /usr/bin/true"
  changed_when: false
  register: pam_verificacio
  failed_when: pam_verificacio.rc != 0
  tags: [pam]

- name: "PAM | Verificar que sudo continua funcionant"
  ansible.builtin.command:
    cmd: sudo -n -l
  become: false
  changed_when: false
  tags: [pam]

# ---------- Muntatges segurs: l ALTRA part perillosa ----------
# Un fstab mal escrit impedeix arrencar (07-01). Per aixo: copia, plantilla
# validada, i mount -a ABANS de donar la tasca per bona.

- name: "Muntatges | Copia de seguretat de fstab"
  ansible.builtin.copy:
    src: /etc/fstab
    dest: "/etc/fstab.bak-{{ ansible_date_time.date }}"
    remote_src: true
    mode: preserve
    force: false
  when: seguretat_muntatges_noexec
  tags: [muntatges]

- name: "Muntatges | Comprovar en calent que noexec no trenca res"
  block:
    - name: "Muntatges | Remuntar /tmp amb noexec temporalment"
      ansible.posix.mount:
        path: /tmp
        state: remounted
        opts: defaults,noatime,noexec,nosuid,nodev
        fstype: ext4
        src: "{{ ansible_mounts | selectattr('mount','equalto','/tmp')
                 | map(attribute='device') | first }}"

    - name: "Muntatges | Comprovar que apt continua funcionant"
      ansible.builtin.apt:
        name: tree
        state: present
        force_apt_get: true
      changed_when: false

    - name: "Muntatges | Comprovar que el desplegament continua funcionant"
      ansible.builtin.command:
        cmd: /home/operador/scripts/desplegar.sh --dry-run {{ tramontana_versio }}
      changed_when: false
      become_user: operador
  rescue:
    - name: "Muntatges | Revertir: noexec trenca alguna cosa"
      ansible.posix.mount:
        path: /tmp
        state: remounted
        opts: defaults,noatime
        fstype: ext4
        src: "{{ ansible_mounts | selectattr('mount','equalto','/tmp')
                 | map(attribute='device') | first }}"

    - name: "Muntatges | Avortar l enfortiment de muntatges"
      ansible.builtin.fail:
        msg: "noexec a /tmp trenca apt o el desplegament. Investigar abans de persistir."
  when: seguretat_muntatges_noexec
  tags: [muntatges]

- name: "Muntatges | Persistir a fstab (nomes si la prova en calent ha passat)"
  ansible.posix.mount:
    path: "{{ item.ruta }}"
    src: "{{ item.origen }}"
    fstype: "{{ item.tipus }}"
    opts: "{{ item.opcions }}"
    state: mounted
  loop:
    - { ruta: /var/tmp,  origen: /tmp,  tipus: none,  opcions: 'bind,noexec,nosuid,nodev' }
    - { ruta: /dev/shm,  origen: tmpfs, tipus: tmpfs, opcions: 'defaults,noexec,nosuid,nodev' }
  loop_control:
    label: "{{ item.ruta }}"
  when: seguretat_muntatges_noexec
  tags: [muntatges]

- name: "Muntatges | VERIFICAR fstab abans que algu reinicii"
  ansible.builtin.command:
    cmd: mount -a
  changed_when: false
  tags: [muntatges]

# ---------- Comprovacio final ----------
- name: "Verificar que el servei continua dret despres de l enfortiment"
  ansible.builtin.command:
    cmd: /home/operador/scripts/revisio_salut.sh
  become_user: operador
  changed_when: false
  register: salut
  failed_when: salut.rc != 0
  tags: [verificacio]
# roles/seguretat/handlers/main.yml
- name: Regenerar initramfs
  ansible.builtin.command:
    cmd: update-initramfs -u -k all

Les precaucions, que és el que demana l'exercici:

Precaució Contra què protegeix
assert que exigeix pam_confirmat=true en producció Executar el rol de PAM contra producció per descuit, sense sessió de rescat oberta
force: false a les còpies Sobreescriure una còpia bona amb una de ja trencada si s'executa dues vegades el mateix dia
pam-auth-update en lloc de lineinfile Editar common-auth a mà és la forma clàssica de trencar l'ordre de la pila. L'eina valida i manté la coherència
Verificació amb runuser i sudo -n -l Detectar que l'autenticació s'ha trencat mentre la connexió continua oberta, que és l'única finestra per arreglar-ho
block/rescue als muntatges noexec a /tmp trenca instal·ladors; el rescue reverteix a l'estat anterior en lloc de deixar la màquina a mitges
Prova en calent abans de tocar fstab Un fstab trencat impedeix arrencar (07-01). Es prova amb remounted, que és reversible
mount -a al final La xarxa de seguretat de 05-04 i 07-01, ara automatitzada
notify: Regenerar initramfs Tocar modprobe.d sense regenerar l'initramfs produeix l'indicador (initramfs)
revisio_salut.sh al final Enfortir i trencar el servei no és enfortir

I la precaució que no és al codi i cal escriure al manual d'operació: abans d'executar aquest rol contra producció, virsh snapshot-create-as o l'equivalent. El codi minimitza el risc; no l'elimina.

Solució 2

changed=5 en --check significa que el servidor real difereix del codi. Hi ha tres causes possibles i porten a decisions diferents, així que primer cal identificar quina és cadascuna.

# Pas 1: QUE canviaria exactament. --diff dona les linies concretes.
$ ansible-playbook lloc.yml --check --diff --limit srv-tramontana \
    2>&1 | tee /tmp/deriva-$(date +%F).txt

$ grep -E '^(TASK|changed:)' /tmp/deriva-$(date +%F).txt | grep -B1 changed
TASK [comu : Instal·lar els paquets base]
changed: [srv-tramontana]
TASK [tramontana : Generar app.conf des de la plantilla]
changed: [srv-tramontana]
TASK [seguretat : Aplicar els parametres de seguretat del nucli]
changed: [srv-tramontana]
TASK [seguretat | Configuracio de faillock]
changed: [srv-tramontana]
TASK [tramontana : Instal·lar la unitat de systemd]
changed: [srv-tramontana]
# Pas 2: QUAN va canviar i QUI. Aqui es on la feina del Modul 6 rendeix.
$ ansible srv-tramontana -b -m shell -a 'aide --check 2>&1 | head -30'
$ ansible srv-tramontana -b -m shell -a 'ausearch -k tramontana_conf -ts recent -i | tail -20'
$ ansible srv-tramontana -b -m shell -a 'journalctl --since "7 days ago" | grep -E "sudo:.*COMMAND" | tail -20'
$ ansible srv-tramontana -b -m shell -a 'grep -E "install|upgrade" /var/log/apt/history.log | tail -10'
$ ansible srv-tramontana -b -m shell -a 'ls -la /etc/tramontana/*.bak-* /etc/systemd/system/tramontana.service.d/ 2>/dev/null'

Les tres causes i el seu tractament, que és el fons de l'exercici:

Causa Com es reconeix Què fer
A. Canvi manual legítim no portat al codi auditd mostra un sudo d'algú de l'equip, hi ha un .bak- amb data, i el canvi té sentit Portar el canvi al codi, no revertir-lo
B. Deriva no autoritzada Ningú no ho reconeix, no hi ha traça a auditd, o l'auid és inesperat Investigar com a incident (06-04) abans de tocar res
C. El codi està desactualitzat El servidor està bé i el playbook reflecteix un estat antic Corregir el playbook

Aplicant-ho als cinc canvis d'aquest cas:

# --- Canvi 1: paquets ---
$ grep -A2 'Instal·lar els paquets base' /tmp/deriva-*.txt
# El diff mostra que instal·laria 'sysstat', que ja esta instal·lat...
$ ansible srv-tramontana -b -m shell -a 'dpkg -l sysstat | tail -1'
ii  sysstat  12.6.1-2  amd64  system performance tools

Fals positiu de --check: apt amb update_cache no pot comprovar l'estat real sense actualitzar l'índex, així que informa changed de manera conservadora. No és deriva. Es confirma executant sense --check en proves i veient que dona ok.

# --- Canvi 2: app.conf ---
--- before: /etc/tramontana/app.conf
+++ after: /etc/tramontana/app.conf
@@ -5,7 +5,7 @@
-timeout_consulta=45
+timeout_consulta=30

$ ansible srv-tramontana -b -m shell -a 'ausearch -k tramontana_conf -i | grep -A2 "success=yes" | tail -6'
type=SYSCALL ... auid=luis uid=root euid=root comm="vim" key=tramontana_conf

Causa A. El Luis va pujar el temps d'espera a 45 s per diagnosticar uns errors. Canvi legítim però no documentat. La decisió: no revertir-lo en silenci. Es parla amb el Luis, es decideix si 45 és correcte, i si ho és es porta a group_vars:

# group_vars/servidors_web.yml
tramontana_timeout: 45   # pujat el 2026-08-15 per consultes lentes (veure #142)
# --- Canvi 3: sysctl de seguretat ---
-kernel.yama.ptrace_scope = 0
+kernel.yama.ptrace_scope = 1

Greu. Algú va baixar ptrace_scope, exactament el que es va desaconsellar a 07-02, i això permet llegir la memòria d'altres processos del mateix usuari — inclosa la credencial de la base de dades.

$ ansible srv-tramontana -b -m shell -a 'grep -rn ptrace /etc/sysctl.d/'
$ ansible srv-tramontana -b -m shell -a 'ausearch -k privilegis -ts recent -i | tail'

Si hi ha traça d'algú de l'equip, és causa A amb una decisió que cal revertir i explicar. Si no hi ha traça, és causa B: es tracta com a incident segons el procediment de 06-04, i la db_password es considera compromesa i es rota.

# --- Canvi 4: faillock ---
-deny = 3
+deny = 5

Causa C, probablement: algú va ajustar el valor després de diversos blocatges accidentals i va ser una decisió raonable. Es confirma i es corregeix el codi, que és el que estava desactualitzat.

# --- Canvi 5: unitat de systemd ---
$ ansible srv-tramontana -b -m shell -a 'ls -la /etc/systemd/system/tramontana.service.d/'
-rw-r--r-- 1 root root 214 ago 18 16:12 enfortiment.conf

Hi ha un drop-in que el playbook no coneix. El playbook genera la unitat completa, i el drop-in la modifica: tots dos coexisteixen i el resultat efectiu no és el del codi. Causa A, i cal decidir el model: o el playbook genera també el drop-in, o s'integren les seves directives a la plantilla de la unitat. La segona és més neta, i de fet la plantilla ja les inclou — el drop-in és una resta de la feina manual de 06-06 que cal retirar:

- name: Retirar drop-ins manuals ja integrats a la plantilla
  ansible.builtin.file:
    path: /etc/systemd/system/tramontana.service.d/enfortiment.conf
    state: absent
  notify:
    - Recarregar systemd
    - Reiniciar tramontana

El procediment, generalitzat:

  1. --check --diff, desant la sortida amb data.
  2. Per a cada canvi, correlacionar amb aide --check, ausearch, journalctl i /var/log/apt/history.log.
  3. Classificar en A, B o C.
  4. A: portar-ho al codi després de confirmar-ho amb qui ho va fer. B: procediment d'incidents, no tocar res encara. C: corregir el codi.
  5. Reexecutar fins a changed=0.
  6. Documentar al registre de canvis què era cadascun.

I la millora que evita repetir-ho: automatitzar la detecció amb un temporitzador setmanal que executi --check i avisi només si hi ha canvis, segons el principi de silenci si tot va bé:

$ cat ~/tramontana-infra/comprovar_deriva.sh
#!/usr/bin/env bash
# Detecta deriva de configuracio. Silenci si no n hi ha cap.
set -euo pipefail
sortida="$(mktemp)"; trap 'rm -f "$sortida"' EXIT

ansible-playbook lloc.yml --check --diff --limit srv-tramontana >"$sortida" 2>&1 || true
canvis="$(grep -oP 'changed=\K[0-9]+' "$sortida" | head -1)"

if [[ "${canvis:-0}" -gt 0 ]]; then
    mail -s "[srv-tramontana] Deriva de configuracio: ${canvis} canvis" \
        [email protected] <"$sortida"
    exit 1
fi

Fixa't que això converteix el playbook en un tercer control d'integritat, complementari a AIDE (que vigila fitxers) i auditd (que vigila accessos): aquest vigila l'estat lògic de la configuració, que cap dels altres dos no veu.

Solució 3

Revisió de l'RTO després de l'automatització de la configuració Per a: Marta Vidal · De: Operacions de sistemes · 18 d'agost de 2026

Resposta curta: sí. Proposo baixar l'RTO acordat de 8 hores a 2 hores, amb marge folgat sobre el temps mesurat.


Què ha canviat. Fins ara, la configuració de srv-tramontana existia en dos llocs: al mateix servidor i al meu cap, amb un manual d'operació en prosa com a suport. Reconstruir-lo significava seguir a mà uns quants centenars de passos —usuaris, permisos, paquets, discos, serveis, tallafocs, enfortiment, registres, còpies— en l'ordre correcte i sense oblidar-ne cap.

Des d'aquesta setmana, tota aquella configuració està escrita com a codi versionat, revisable i executable. Reconstruir el servidor consisteix a llançar una ordre.

La mesura. He reconstruït el servidor des de zero a l'entorn de proves, cronometrant-ho:

Fase Abans (manual) Ara (automatitzat)
Crear la màquina 20-30 min 2 min
Configurar el sistema complet 6-7 hores 7 min
Restaurar les dades 30-45 min 30-45 min
Verificar que tot funciona 30 min 5 min
Total ~8 hores ~50 minuts

I he comprovat que el resultat és idèntic a producció amb les mateixes mesures objectives que fem servir habitualment: la puntuació de seguretat del servei, la configuració efectiva d'SSH, els paràmetres del sistema i l'estat del tallafocs.

Per què proposo 2 hores i no 1. El temps mesurat és de 50 minuts en condicions de laboratori. Un incident real hi afegeix coses que no es poden mesurar per endavant:

  • Temps de decisió: adonar-se'n, diagnosticar i decidir reconstruir.
  • Maquinari o proveïdor: esperar que hi hagi una màquina disponible.
  • Imprevistos: una restauració que falla al primer intent, un problema de xarxa.
  • Comunicació i verificació amb l'equip.

Un compromís de 2 hores ens deixa més del doble del temps mesurat com a coixí. Comprometre 1 hora seria just, i un RTO que no es pot complir és pitjor que un de conservador.

El que continua sense estar automatitzat, i consta:

Element Per què Temps
La clau mestra de secrets És la clau de tota la resta; es restaura a mà des de la seva custòdia física, deliberadament 5 min
El certificat del lloc web Es torna a emetre; no té sentit copiar-lo 2 min
La base de dades d'integritat S'ha de generar a la màquina nova, no copiar-se Inclòs
Les dades de reserves És un procés a part, amb la seva pròpia verificació 30-45 min
La decisió de reconstruir És humana, i ho ha de ser Variable

Beneficis addicionals, més enllà del temps. Tres que em semblen tan importants com l'RTO:

  1. Ja no depenem d'una sola persona. Qualsevol de l'equip amb accés al repositori pot reconstruir el servidor. Abans, si jo no era disponible, l'RTO era indefinit.
  2. L'entorn de proves és ara idèntic a producció, perquè surt del mateix codi. Això significa que el que hi provem és representatiu, cosa que abans no podíem garantir.
  3. Detectem canvis no autoritzats. Executant el codi en mode simulació sabem si el servidor difereix del que hauria de ser. Ho he programat setmanalment, i avisa només si hi ha alguna cosa. De fet, la primera execució ja va detectar cinc diferències, quatre de les quals canvis legítims que ningú no havia documentat.

I una cosa que vull destacar. Fa unes setmanes et vaig explicar que un servidor compromès s'ha de reinstal·lar, no netejar. És la recomanació correcta i era, honestament, difícil de complir: ningú no decideix alegrement perdre un dia de feina. Amb 50 minuts de reconstrucció, aquella recomanació passa de ser un consell incòmode a ser el procediment evident. L'automatització no és només eficiència: és el que fa complible una decisió de seguretat.

Què caldria per baixar més. Si en algun moment l'objectiu fos un RTO per sota de 30 minuts, les opcions serien:

Mesura Efecte Cost
Un segon servidor ja configurat i en espera Elimina la fase de reconstrucció Duplicar el servidor
Restauració contínua a un servidor de reserva Redueix també el temps de dades Servidor + feina de manteniment
Dos servidors actius amb repartiment de càrrega La fallada d'un no interromp el servei Duplicar, més un balancejador

Les tres canvien de problema: deixem de parlar de recuperació i passem a parlar de redundància. És l'assumpte que tinc pendent de portar-te amb números, i crec que és la conversa natural després d'aquesta.

Proposta concreta. Actualitzar l'acord de nivell de servei a RTO de 2 hores i RPO de 4 hores (aquest últim sense canvis), i programar un simulacre de recuperació complet cada sis mesos per verificar que el número se sosté. El primer, al setembre.

Conclusió

La configuració de srv-tramontana ja no viu al teu cap. Viu en un repositori de git, escrita com a estat desitjat en lloc de com a seqüència d'instruccions, i aquella diferència és el que fa que el fitxer es llegeixi com una descripció del servidor i no com un procediment. Saps per què la idempotència és la propietat central —permet executar sense por, detecta la deriva i fa possible la convergència periòdica—, i per què command i shell són l'últim recurs: trenquen justament això. Tens inventaris amb variables per grup que fan explícites i mínimes les diferències entre entorns, plantilles Jinja2 que generen app.conf i la unitat de systemd —amb l'excepció de getrandom que vas descobrir depurant un SIGSYS, ara al codi i no a la memòria de ningú—, rols que organitzen el conjunt, i Vault integrat amb el pass de 06-05 per no tenir dues fonts de veritat dels secrets.

I tens les salvaguardes on importen: validate: sshd -t -f %s impedeix instal·lar una configuració d'SSH que et deixaria fora, backup: true deixa la còpia amb data de la convenció del curs, els manejadors reinicien el servei només si alguna cosa ha canviat, l'ordre de les tasques d'ufw permet SSH abans d'habilitar el tallafocs, i --check --diff és el --dry-run del curs portat a la configuració sencera del servidor. Amb l'honestedat de saber que --check té falsos positius i que un playbook sense errors no significa un servidor funcionant: per això cada execució acaba verificant amb revisio_salut.sh, sshd -T i systemd-analyze security.

El número que resumeix el mòdul és de vuit hores a cinquanta minuts. I la seva conseqüència importa més que el número: la regla de 06-04 —«un servidor compromès es reinstal·la, no es neteja»— deixa de ser un consell que ningú no vol seguir i passa a ser el procediment evident. L'automatització no és només eficiència; és el que fa complible una decisió de seguretat que abans era teòrica. Com a efecte secundari, el playbook s'ha convertit en un tercer control d'integritat, al costat d'AIDE i auditd: vigila l'estat lògic de la configuració, que cap dels altres dos no veu.

Però fixa't en el que continua sense resoldre's, i que les últimes tres respostes a la Marta han anat assenyalant cada vegada amb més claredat. Pots reconstruir el servidor en cinquanta minuts. Pots provar qualsevol canvi abans d'aplicar-lo. Pots detectar una intrusió, xifrar els secrets i mesurar el rendiment. I tot i així, si srv-tramontana cau, Tramontana Reserves està caiguda. Un disc, una font d'alimentació, un tall de xarxa, un nucli que no arrenca després d'una actualització: cinquanta minuts d'interrupció en el millor dels casos, i això només si algú està despert per llançar el playbook. Tota la feina de set mòduls descansa sobre una única màquina.

A la lliçó 07-07: Alta Disponibilitat i Balanceig de Càrrega s'ataca això directament. Aprendràs el vocabulari amb precisió —disponibilitat i els «nous» amb els minuts de caiguda a l'any que representen de debò, escalat vertical davant d'horitzontal, MTBF i MTTR i la seva relació amb l'RPO i l'RTO que ja tens acordats—, i veuràs per què l'estat és el problema difícil: replicar processos és fàcil, replicar dades i sessions no. Muntaràs un balancejador amb HAProxy davant de dues instàncies, amb comprovacions de salut que reutilitzen els codis 0/1/2 de revisio_salut.sh i drenatge de connexions per desplegar sense tallar; donaràs alta disponibilitat al mateix balancejador amb keepalived i una IP virtual flotant, evitant l'error de disseny més comú de l'àrea; veuràs la replicació de PostgreSQL, per què la commutació automàtica és perillosa sense quòrum, i què és el cervell dividit. I acabaràs amb l'anàlisi que la Marta fa tres lliçons que demana: quant costa de debò aquesta arquitectura, quant costa una hora de caiguda, i si Tramontana la necessita — perquè la resposta professional no sempre és «sí».

Curs de Linux: De Principiant a Administrador de Sistemes

Mòdul 1: Introducció a Linux

Mòdul 2: Comandes Bàsiques de Linux

Mòdul 3: Habilitats Avançades en la Línia de Comandes

Mòdul 4: Scripting en Shell

Mòdul 5: Administració del Sistema

Mòdul 6: Xarxes i Seguretat

Mòdul 7: Temes Avançats

Mòdul 8: Projectes Pràctics

© Copyright 2026. Tots els drets reservats