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-provesno és igual que producció, per molt acurat que fos eluser-datade 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
- Estat desitjat davant d'instruccions
- Idempotència, i per què ho canvia tot
- Per què Ansible: sense agent, sobre SSH, en YAML
- Instal·lació, inventari i variables
- Mòduls i tasques
- Playbooks: manejadors, condicions, bucles i blocs
- Plantilles Jinja2
- Rols: organitzar per reutilitzar
- Ansible Vault i els secrets
- Execució: --check, --diff, --tags i ansible-lint
- 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 operadorExecuta 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 operadorFunciona, 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: presentLa 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=3Aquell changed=0 de la segona execució és l'objectiu, i té tres conseqüències que convé enunciar:
- 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.
- 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. - 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_configsense 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.cfgEl 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=300shost_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-genericAquí 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.dbLes 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 > 2changed_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: reloadedEls 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.targetFixa'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, logrotateTres 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.
I Ansible Galaxy per no reescriure el que ja està escrit:
$ ansible-galaxy collection install community.general ansible.posix
$ ansible-galaxy role install geerlingguy.postgresqlAmb 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:
# 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 contrasenyaXifrar 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.shAixí 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:
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 tramontanano_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 modulsI 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 potEls 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.118sLa 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 actiuExposició 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.402schanged=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
commandoshellon hi ha un mòdul. Perd la idempotència i la comprovació d'estat.ansible-lintho detecta. - Oblidar
changed_when: falsea les consultes. El playbook informachangedsempre i perd el seu valor com a detector de deriva. - Oblidar
no_log: trueen 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
validateen configuracions crítiques. Unsshd_configamb un error de sintaxi et deixa fora.validate: /usr/sbin/sshd -t -f %sho impedeix. - Habilitar
ufwabans 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ò existeixsrv-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
changedt'ho diu. Tracta'l com a codi: git, revisió de canvis,ansible-linta 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 allLes 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 toolsFals 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_confCausa 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)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.
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.confHi 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 tramontanaEl procediment, generalitzat:
--check --diff, desant la sortida amb data.- Per a cada canvi, correlacionar amb
aide --check,ausearch,journalctli/var/log/apt/history.log. - Classificar en A, B o C.
- 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.
- Reexecutar fins a
changed=0. - 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
fiFixa'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-tramontanaexistia 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:
- 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.
- 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.
- 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
- Què és Linux?
- Història de Linux
- Distribucions de Linux
- Instal·lant Linux
- Primer Contacte amb el Sistema
- Estructura del Sistema de Fitxers de Linux
Mòdul 2: Comandes Bàsiques de Linux
- Introducció a la Línia de Comandes
- Obtenir Ajuda i Documentació del Sistema
- Navegant pel Sistema de Fitxers
- Operacions amb Fitxers i Directoris
- Visualització i Edició de Fitxers
- Enllaços Durs i Simbòlics
- Permisos i Propietat dels Fitxers
Mòdul 3: Habilitats Avançades en la Línia de Comandes
- L'Entorn del Shell: Variables, Àlies i Historial
- Ús de Comodins i Expressions Regulars
- Cerca de Fitxers i Contingut: find, locate i grep
- Canonades i Redirecció
- Processament de Text: cut, sort, uniq, sed i awk
- Gestió de Processos
- Programació de Tasques amb Cron
- Comandes de Xarxa
Mòdul 4: Scripting en Shell
- Introducció al Scripting en Shell
- Variables i Tipus de Dades
- Entrada, Sortida i Arguments d'un Script
- Estructures de Control
- Funcions i Biblioteques
- Depuració i Gestió d'Errors
- Scripts de Producció: Bones Pràctiques
Mòdul 5: Administració del Sistema
- Gestió d'Usuaris i Grups
- sudo i Permisos Especials
- Gestió de Paquets
- Gestió de Discs
- systemd i la Gestió de Serveis
- Registres del Sistema: journald i syslog
- Monitoratge del Sistema i Optimització del Rendiment
- Còpies de Seguretat i Restauració
Mòdul 6: Xarxes i Seguretat
- Configuració de Xarxes
- SSH i Accés Remot
- Tallafocs i Seguretat Perimetral
- Sistemes de Detecció d'Intrusions
- Gestió de Secrets i Certificats TLS
- Assegurant Sistemes Linux
Mòdul 7: Temes Avançats
- El Procés d'Arrencada i la Recuperació del Sistema
- Diagnòstic Avançat: strace, perf i eBPF
- Optimització del Nucli de Linux
- Virtualització amb Linux
- Contenidors de Linux i Docker
- Automatització amb Ansible
- Alta Disponibilitat i Balanceig de Càrrega
