Su un server Red Hat Enterprise Linux, CentOS, Rocky Linux o AlmaLinux, “abbiamo PHP 7.4” può voler dire cose molto diverse. Il pacchetto può arrivare dalla distribuzione, dalle Software Collections di Red Hat, dal repository di Remi Collet con i pacchetti che sostituiscono quelli di sistema o dalle sue collezioni installate in parallelo. Finché tutto viene aggiornato la differenza si nota poco. Quando il ramo è fuori supporto ed esce una vulnerabilità, la differenza diventa decisiva.
In questo articolo vediamo come riconoscere la famiglia di pacchetti installata, perché una correzione va applicata proprio a quel pacchetto e come si ricostruisce un RPM di PHP con le patch di sicurezza: sorgente, modifica dello spec, compilazione pulita, test, repository e rilascio. Ci soffermiamo sulle particolarità delle Software Collections e dei moduli di dnf, che sono le fonti più comuni di sorprese.
Perché l’origine del pacchetto conta
Un pacchetto RPM di PHP è molto più del codice sorgente ufficiale compilato. Contiene le opzioni di compilazione scelte da chi lo mantiene, la suddivisione delle estensioni in sottopacchetti, i percorsi dei file di configurazione, le patch già applicate e i file di servizio per PHP-FPM. Due pacchetti della stessa versione, uno di Red Hat e uno di Remi, possono differire in tutti questi punti.
Per questo una correzione di sicurezza non si installa compilando PHP a mano dal sito ufficiale. Una compilazione manuale finisce in percorsi diversi, perde le estensioni separate, sfugge al database dei pacchetti e verrebbe sovrascritta dal primo aggiornamento. Il modo corretto è partire dal pacchetto sorgente da cui è nato quello installato, aggiungere la correzione e ricostruirlo: il risultato è identico all’originale, con in più la patch.
Le quattro famiglie di pacchetti
I pacchetti della distribuzione
Su RHEL 7 e CentOS 7 il PHP di sistema è il 5.4. Su RHEL 8 e 9 le versioni alternative sono flussi del modulo php nel repository AppStream, mentre su RHEL 10 sono normali pacchetti RPM. Rocky Linux e AlmaLinux ricostruiscono gli stessi pacchetti. Versioni e scadenze sono nell’articolo su PHP su RHEL 8, 9 e 10.
Le Software Collections di Red Hat
Su RHEL 6 e 7 Red Hat distribuiva versioni più recenti di PHP come Software Collections: pacchetti con un prefisso nel nome, come rh-php73, installati sotto /opt/rh accanto al PHP di sistema. Il progetto CentOS le ridistribuiva per CentOS 7. Secondo il ciclo di vita delle Red Hat Software Collections, l’ultima versione, PHP 7.3, ha raggiunto la fine del supporto a giugno 2024. Da RHEL 8 in poi le collezioni per PHP non esistono più, sostituite dagli Application Streams.
Il repository di Remi
Remi Collet, che contribuisce a PHP e a Fedora, mantiene da anni il repository più usato per PHP sui sistemi della famiglia Red Hat. Offre due tipi di pacchetti. Quelli base hanno lo stesso nome dei pacchetti di sistema e li sostituiscono: su EL 7 si sceglieva la versione abilitando un repository come remi-php74, su EL 8, 9 e 10 si abilita un flusso del modulo, come php:remi-8.2. Le collezioni, come php74-php-fpm, si installano invece sotto /opt/remi e permettono di avere più versioni sullo stesso server, una per sito.
Le FAQ del repository spiegano che per alcuni rami fuori supporto Remi include nei build più recenti le correzioni di sicurezza che riesce a riportare. Se il tuo server è su EL 8, 9 o 10 e usa Remi, la prima verifica è quindi di essere all’ultimo pacchetto disponibile. Su EL 6 e 7 invece i repository di Remi sono chiusi e nessun aggiornamento arriverà più.
Riconoscere cosa è installato
Bastano pochi comandi per capire a quale famiglia appartiene il PHP del server. Il campo Release è il più eloquente: un suffisso come .el8.remi indica Remi, il prefisso rh-php nel nome le collezioni di Red Hat, php74-php quelle di Remi.
# Nome, versione, release e fornitore di ogni pacchetto PHP
rpm -qa --qf '%{NAME}-%{VERSION}-%{RELEASE} %{VENDOR}\n' | grep php | sort
# Repository da cui è stato installato ogni pacchetto (EL 8, 9 e 10)
dnf list installed 'php*'
# Flusso del modulo php attivo (EL 8 e 9)
dnf module list --enabled php
# Collezioni installate in parallelo e relativi servizi FPM
ls -d /opt/rh/* /opt/remi/* 2>/dev/null
systemctl list-units --type=service | grep -i fpm
Il procedimento, passo per passo
Il lavoro di ricostruzione segue sempre la stessa sequenza, qualunque sia la famiglia. Cambiano i dettagli dello spec e la configurazione dell’ambiente di compilazione.
Ecco i comandi principali, con un esempio su un sistema EL 9 e un PHP 7.4 di Remi. I nomi dei file vanno adattati alla versione installata:
# 1. Scarica il pacchetto sorgente della versione installata
# (richiede dnf-plugins-core e i repository dei sorgenti abilitati)
dnf download --source php-cli
# 2. Installa il sorgente nell'albero di rpmbuild ed estrai il codice
rpm -ivh php-7.4.*.src.rpm
rpmbuild -bp --nodeps ~/rpmbuild/SPECS/php.spec
# 3. Aggiungi la patch allo spec (riga PatchNNN, applicazione
# nella sezione %prep, Release più alta) e crea il nuovo sorgente
rpmbuild -bs ~/rpmbuild/SPECS/php.spec
# 4. Compila in un ambiente pulito; per i pacchetti di Remi la
# configurazione di mock deve includere EPEL e i suoi repository
mock -r rocky+epel-9-x86_64 --rebuild ~/rpmbuild/SRPMS/php-7.4.*.src.rpm
# 5. Firma i pacchetti (chiave indicata in %_gpg_name) e crea il repository
rpm --addsign /var/lib/mock/rocky+epel-9-x86_64/result/*.rpm
cp /var/lib/mock/rocky+epel-9-x86_64/result/*.rpm /srv/repo/php/el9/
createrepo_c /srv/repo/php/el9/
La parte che richiede esperienza è il passaggio 3. Le correzioni pubblicate dal progetto PHP sono scritte per i rami supportati, e il codice dei rami vecchi può essere molto diverso: ogni patch va letta, adattata e verificata, a volte riscritta. È qui che si concentra il lavoro di backport. Il changelog dello spec deve elencare ogni CVE corretta, così chiunque può verificarla con rpm -q --changelog.
Il numero di release
Il numero di release del pacchetto ricostruito va scelto con attenzione. Deve essere più alto di quello installato, altrimenti dnf non lo considera un aggiornamento. Allo stesso tempo deve restare più basso di un eventuale build successivo pubblicato da chi mantiene il pacchetto originale: se Remi o la distribuzione rilasciano una nuova versione, il server deve poterla ricevere senza conflitti. Per questo aggiungiamo un suffisso riconoscibile alla release esistente, invece di incrementare il primo numero. Il suffisso rende anche evidente, a chi legge l’elenco dei pacchetti installati, quale build contiene le correzioni aggiuntive. Quando il repository originale è chiuso, come per EL 7, il problema non si pone e il suffisso resta utile per gli audit.
Le particolarità delle collezioni
Gli spec delle Software Collections usano le macro %scl per aggiungere il prefisso ai nomi e spostare i file sotto /opt. Per compilarli l’ambiente di build deve contenere i pacchetti che definiscono quelle macro per la collezione specifica, e il pacchetto ricostruito deve mantenere esattamente lo stesso prefisso. Il servizio da riavviare dopo l’aggiornamento ha il nome della collezione, per esempio rh-php73-php-fpm o php74-php-fpm, e la configurazione resta in /etc/opt.
Le particolarità dei moduli
Su EL 8 e 9, quando un flusso del modulo php è attivo, dnf nasconde i pacchetti con lo stesso nome che arrivano da repository non modulari. È il cosiddetto filtro modulare, ed è la ragione per cui un pacchetto ricostruito e pubblicato in un repository normale sembra non esistere. Le soluzioni sono due: rigenerare i metadati del modulo nel proprio repository, oppure dichiarare il repository come contenitore di correzioni con l’opzione module_hotfixes prevista da dnf.
# /etc/yum.repos.d/php-backport.repo
[php-backport]
name=PHP con le patch di sicurezza
baseurl=https://repo.esempio.lan/php/el9/
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-php-backport
# Disattiva il filtro modulare per questo repository
module_hotfixes=1
# Priorità più alta (numero più basso) rispetto agli altri repository
priority=10
I test prima del rilascio
Compilare il pacchetto è solo metà del lavoro. Prima di arrivare in produzione passa da un ambiente di prova con l’applicazione reale, dove verifichiamo almeno questi punti:
- la suite di test di PHP, che lo spec può eseguire durante la build, senza nuovi fallimenti rispetto al pacchetto originale;
- lo stesso elenco di estensioni caricate e la stessa configurazione prima e dopo l’aggiornamento;
- la sintassi della configurazione di PHP-FPM, prima di riavviare il servizio;
- i percorsi critici dell’applicazione: accesso, ricerca, carrello, pagamento, invio di email, attività pianificate.
# Estensioni caricate prima dell'aggiornamento
php -m | sort > /root/php-moduli-prima.txt
# Dopo l'aggiornamento: nessuna differenza attesa
php -m | sort | diff /root/php-moduli-prima.txt -
# Verifica della configurazione di PHP-FPM prima del riavvio
php-fpm -t
Rilascio e ritorno indietro
Il rilascio lo facciamo con Ansible, un server alla volta, partendo da quello meno critico. Il pacchetto precedente resta nel repository, così il ritorno indietro è un comando: dnf history undo sulla transazione appena eseguita, oppure dnf downgrade sui pacchetti PHP. Su EL 7 i comandi equivalenti sono quelli di yum. Per un intervento d’urgenza su una singola CVE il nostro obiettivo è di 24 ore dall’analisi al rilascio, e la reperibilità H24 copre anche questa fase.
Sui sistemi fuori supporto, come CentOS 7, la ricostruzione si fa in un ambiente di compilazione della stessa versione, con i pacchetti archiviati della distribuzione: i dettagli sono nell’articolo su CentOS 7 dopo la fine del supporto. Vale anche qui il limite di ogni backport: ricostruire PHP corregge PHP, mentre il resto del sistema resta com’è. Se vuoi capire come applichiamo questo procedimento ai tuoi server, trovi i dettagli nella sezione sugli interventi.
Riassumendo
Sui sistemi della famiglia Red Hat PHP arriva da quattro famiglie di pacchetti: la distribuzione, con AppStream e moduli; le Software Collections di Red Hat, chiuse con PHP 7.3 a giugno 2024; i pacchetti base di Remi, che sostituiscono quelli di sistema; le collezioni di Remi, installate in parallelo sotto /opt/remi. Una correzione di sicurezza va applicata al pacchetto della famiglia già installata, ricostruendolo dal suo sorgente con la patch adattata e una release più alta, compilandolo con mock in un ambiente pulito, provandolo con l’applicazione reale e pubblicandolo in un repository firmato. Sui sistemi con moduli serve l’opzione module_hotfixes o i metadati del modulo; sulle collezioni bisogna rispettare prefisso e percorsi. Il risultato è un aggiornamento normale per il server, con un ritorno indietro sempre pronto.