Pinning APT: come evitare che un aggiornamento sovrascriva il tuo PHP corretto

Come APT sceglie la versione da installare, la sintassi dei file di preferenze, tre ricette per proteggere un PHP corretto o limitare deb.sury.org a un solo ramo, e come verificare le regole prima che lo faccia un aggiornamento.

Copertina con il logo php∞, la categoria Distribuzioni Linux, il titolo Pinning APT per PHP e il numero 23

Hai installato su un server Debian o Ubuntu un PHP ricostruito con le patch di sicurezza, l’hai provato con l’applicazione e tutto funziona. Qualche settimana dopo un aggiornamento di routine, magari automatico, installa al suo posto un pacchetto con lo stesso nome preso da un altro repository. Le patch spariscono, a volte cambiano anche le librerie di sistema, e nessuno se ne accorge finché non esce la vulnerabilità successiva o il sito non smette di funzionare.

Il pinning di APT serve esattamente a evitare questo. In questo articolo vediamo come APT sceglie la versione da installare quando più repository offrono lo stesso pacchetto, come si scrive un file di preferenze, tre ricette pronte per i casi più comuni con PHP e come verificare il risultato prima che lo faccia un aggiornamento.

Come APT sceglie la versione da installare

Per ogni pacchetto APT raccoglie tutte le versioni disponibili nei repository configurati, più quella già installata, e assegna a ciascuna una priorità. La versione con la priorità più alta diventa la candidata, quella che verrà installata. A parità di priorità vince il numero di versione più alto, confrontato con le regole di dpkg.

Senza file di preferenze le priorità di default sono semplici: 500 per le versioni disponibili nei repository e 100 per la versione installata. In pratica vince quasi sempre il numero di versione più alto, da qualunque repository arrivi. Fanno eccezione i repository marcati come NotAutomatic, come experimental, che hanno priorità 1, e i backports, che hanno priorità 100. Se imposti una release di destinazione con apt-get -t o con APT::Default-Release, le sue versioni salgono a 990.

Il problema nasce proprio dal confronto dei numeri di versione. Un pacchetto ricostruito con un suffisso locale, per esempio 7.4.33-1+deb11u12+pl1, è più recente della versione di Debian da cui deriva. Ma basta che Debian pubblichi un deb11u13, o che un altro repository usi un epoch come 1: davanti al numero, perché la tua versione perda il confronto.

# Confronta due versioni con le stesse regole di APT (stampa il risultato)
dpkg --compare-versions 7.4.33-1+deb11u12+pl1 gt 7.4.33-1+deb11u12 && echo "vince la prima"
dpkg --compare-versions 7.4.33-1+deb11u12+pl1 gt 7.4.33-1+deb11u13 || echo "vince la seconda"
dpkg --compare-versions 7.4.33-1+deb11u12+pl1 gt 1:7.4.33-31 || echo "vince la versione con epoch"

Le priorità e cosa significano

Con un file di preferenze puoi assegnare priorità diverse da quelle di default. Il valore numerico conta per fasce, ciascuna con un effetto preciso descritto nella pagina di manuale apt_preferences(5).

Tabella delle fasce di priorità di APT con il loro effetto e gli esempi tipici. Priorità maggiore o uguale a 1000: installa anche se è un downgrade, per esempio 1001 per il repository con i pacchetti corretti. Da 990 a 999: installa anche fuori dalla release di destinazione, per esempio 990 per la release scelta con apt -t o APT::Default-Release. Da 500 a 989: installa salvo versioni della release di destinazione, per esempio 500 per ogni repository senza preferenze. Da 100 a 499: installa salvo versioni di altre distribuzioni, per esempio 100 per la versione già installata e per i backports. Da 1 a 99: installa solo se il pacchetto non è installato, per esempio 1 per i repository NotAutomatic come experimental. Minore di 0: non installare mai questa versione, per esempio -1 per escludere un repository per PHP. La priorità 0 ha un comportamento indefinito e non va usata
Le fasce di priorità definite in apt_preferences(5).

Per proteggere un PHP corretto contano soprattutto tre valori. Sopra 1000 la versione viene installata anche se è più vecchia di quella presente, quindi APT è in grado di tornare al tuo pacchetto dopo un aggiornamento sbagliato. Un valore negativo esclude del tutto le versioni di un repository. E 500 è la priorità normale, da cui partire per capire quale regola sposta l’equilibrio.

La sintassi di un file di preferenze

Le preferenze stanno in /etc/apt/preferences o, meglio, in file separati dentro /etc/apt/preferences.d/. Ogni file contiene uno o più record separati da una riga vuota, ciascuno con tre campi obbligatori e un commento facoltativo:

  • Explanation: una o più righe di commento, all’inizio del record;
  • Package: i pacchetti a cui si applica la regola, con il nome esatto, un’espressione glob come php7.4* o un’espressione regolare tra barre come /^php7\.4/, anche più di uno separati da spazi;
  • Pin: quali versioni selezionare, per numero (version 7.4.33*), per proprietà della release (release o=Debian,n=bullseye) o per nome host del repository (origin packages.sury.org);
  • Pin-Priority: la priorità da assegnare alle versioni selezionate.

Le proprietà usate da Pin: release sono quelle del file Release del repository: o= per Origin, l= per Label, a= per la suite, n= per il nome in codice, c= per il componente. Per esempio il repository di sicurezza di Debian ha Origin Debian e Label Debian-Security, mentre deb.sury.org ha Origin deb.sury.org. Con apt-cache policy le vedi per ogni repository configurato.

Due dettagli fanno perdere molto tempo. I file in preferences.d devono avere nomi senza estensione o con estensione .pref, e solo lettere, numeri, trattini, trattini bassi e punti: un file php.conf viene ignorato senza avvisi. E quando più regole riguardano la stessa versione, la prima regola specifica che la seleziona decide la priorità; per questo le regole generiche con Package: * vanno scritte per prime.

Tre ricette per PHP

Proteggere i pacchetti corretti da un repository proprio

È il caso tipico dopo un backport delle patch: i pacchetti PHP ricostruiti stanno in un repository interno, e devono vincere su Debian e su deb.sury.org anche se questi pubblicano numeri di versione più alti. Nell’esempio il repository interno è repo.example.com: sostituiscilo con il nome host del tuo.

# /etc/apt/preferences.d/php74-corretto
Explanation: PHP 7.4 solo dal repository interno, anche a costo di un downgrade
Package: /^php7\.4/ libapache2-mod-php7.4
Pin: origin "repo.example.com"
Pin-Priority: 1001

Explanation: deb.sury.org non deve mai fornire PHP 7.4 su questo server
Package: /^php7\.4/ libapache2-mod-php7.4
Pin: origin "packages.sury.org"
Pin-Priority: -1

La priorità 1001 ha un effetto collaterale da tenere presente. Finché la regola esiste, APT ignorerà le versioni di Debian anche se più recenti, quindi le correzioni devono arrivare dal repository interno. Se un giorno smetti di mantenerlo, togli la regola.

Usare deb.sury.org solo per un ramo

Se aggiungi deb.sury.org per installare PHP 5.6 accanto al PHP della distribuzione, senza regole il repository aggiorna anche il PHP di Debian e diverse librerie di sistema, perché ha numeri di versione più alti. Con questa regola i pacchetti del repository restano disponibili, ma vengono scelti solo se il pacchetto non è già installato da Debian, e quelli di PHP 5.6 hanno la priorità normale.

# /etc/apt/preferences.d/sury-solo-php56
Explanation: di norma deb.sury.org non sostituisce i pacchetti di Debian
Package: *
Pin: origin "packages.sury.org"
Pin-Priority: 100

Explanation: da deb.sury.org arriva PHP 5.6
Package: /^php5\.6/ libapache2-mod-php5.6
Pin: origin "packages.sury.org"
Pin-Priority: 500

Alcuni pacchetti PHP di deb.sury.org richiedono versioni delle librerie più recenti di quelle di Debian. In quel caso APT segnala dipendenze non soddisfatte: va valutato pacchetto per pacchetto se consentire anche quelle librerie, aggiungendole alla seconda regola.

Fermare un pacchetto su una versione precisa

Per bloccare una versione specifica, per esempio durante una finestra di test, puoi usare Pin: version con un asterisco finale. Oppure apt-mark hold, che è più semplice e impedisce qualsiasi aggiornamento del pacchetto, comprese le correzioni di sicurezza.

# /etc/apt/preferences.d/php82-fermo
Explanation: PHP 8.2 fermo alla versione provata in collaudo
Package: /^php8\.2/
Pin: version 8.2.32-1~deb12u1*
Pin-Priority: 1001

# In alternativa, blocco di singoli pacchetti
sudo apt-mark hold php8.2-fpm php8.2-cli php8.2-common
apt-mark showhold
sudo apt-mark unhold php8.2-fpm php8.2-cli php8.2-common

La differenza è di scopo. apt-mark hold ferma tutto, e va tolto appena possibile. Il pinning per origine, come nelle prime due ricette, decide da dove devono arrivare gli aggiornamenti e può restare in modo permanente.

Verificare prima che lo faccia un aggiornamento

Una regola scritta male non produce errori: viene ignorata, o seleziona pacchetti diversi da quelli previsti. Per questo ogni modifica alle preferenze va verificata subito con apt-cache policy, che mostra per ogni versione la priorità effettiva, e con una simulazione dell’aggiornamento.

# Priorità di ogni versione e candidata scelta da APT
apt-cache policy php7.4-fpm

# Priorità di ogni repository configurato
apt-cache policy

# Cosa farebbe l’aggiornamento, senza installare nulla
sudo apt-get update
apt-get -s upgrade | grep -i php

Nell’output di apt-cache policy php7.4-fpm la riga Candidate deve indicare la versione del tuo repository, e accanto a ogni versione compare la priorità assegnata. Se la candidata è quella giusta e la simulazione non propone di sostituire i pacchetti PHP, la regola funziona.

Scenario illustrativo su Debian 11 con tre repository che offrono php7.4-fpm: il repository proprio con la versione installata 7.4.33-1+deb11u12+pl1, Debian LTS con 7.4.33-1+deb11u12 e deb.sury.org con 1:7.4.33-31 e un suffisso di build. Senza preferenze tutti hanno priorità 500 e vince deb.sury.org, perché l’epoch 1 rende la sua versione più alta. Con il pinning il repository proprio ha priorità 1001, Debian 500 e deb.sury.org -1, quindi vince il pacchetto proprio e deb.sury.org è escluso per PHP 7.4
Senza regole decide il numero di versione, con il pinning decidi tu.

Gli errori più comuni

  • File con l’estensione sbagliata: un file .conf o .list in preferences.d viene ignorato;
  • confusione tra le due origini: Pin: origin confronta il nome host del repository, mentre Pin: release o= confronta il campo Origin del file Release, e i due valori spesso sono diversi;
  • espressioni troppo larghe: php* seleziona anche php-common, phpmyadmin e qualunque altro pacchetto che inizia per «php»;
  • priorità 1001 su un intero repository: con Package: * provoca il downgrade di tutto ciò che il repository contiene, librerie comprese;
  • pacchetti dimenticati: se la regola copre php7.4-fpm ma non php7.4-common, APT può proporre combinazioni incoerenti, che le dipendenze bloccano con errori difficili da leggere;
  • regole orfane: quando togli un repository, togli anche le regole che lo riguardano.

Su un parco di server conviene distribuire i file di preferenze con lo stesso strumento che installa i pacchetti, così regole e versioni restano allineate. Ne parliamo nell’articolo sul patching PHP con Ansible su parchi misti.

In sintesi

APT installa la versione con la priorità più alta e, a parità, quella con il numero di versione più alto: senza regole, un pacchetto PHP corretto può essere sostituito da qualunque repository pubblichi un numero maggiore, come accade su Debian 11 con l’epoch dei pacchetti di deb.sury.org. Un file in /etc/apt/preferences.d/, senza estensione o con .pref, risolve il problema: priorità 1001 per il repository con i pacchetti corretti, -1 per escludere le altre fonti di quel ramo, 100 per usare un repository esterno solo dove serve. apt-mark hold resta utile per blocchi temporanei. Ogni regola va verificata con apt-cache policy e con una simulazione, prima che sia un aggiornamento a scoprire se funziona.

IL PROSSIMO PASSO

Mettiamo
in sicurezza.

Indicaci versione di PHP, sistema operativo e cosa ti preoccupa. Ti risponderemo con una valutazione dell’esposizione e una proposta di intervento.

Richiedi supporto

Ti porta al modulo di richiesta, in quattro passi.

PHP LEGACY · MANAGED SERVER S.R.L.info@phplegacy.it +39 02 5656 9681

Lunedì – venerdì · 09:30 – 19:30

Urgenza in corso? Reperibilità H24
+39 340 985 4556 ↗