LAMP uz AlmaLinux 9: Apache, PHP 8.3 un MariaDB
Vecais paradums ierakstīt yum install httpd php mysql un uzreiz dabūt strādājošu tīmekļa serveri beidzās kopā ar CentOS. AlmaLinux 9 pakotnes joprojām ir, bet PHP slēpjas aiz moduļu straumēm, MySQL vietā ir MariaDB, mod_php vairs nav ceļā un SELinux strādā enforcing režīmā jau no pirmās palaišanas. Šajā rakstā ir viss ceļš līdz strādājošam Apache, PHP 8.3 un MariaDB komplektam uz tīras AlmaLinux 9 mašīnas, ieskaitot vietas, kur parasti aiziet puse dienas. Tie paši soļi der Rocky Linux 9 un RHEL 9, jo visas trīs sistēmas ir viens un tas pats pakotņu kopums.
Kāpēc AlmaLinux 9, nevis CentOS
CentOS 8 tika izbeigts 2021. gada beigās, gadus agrāk, nekā visi bija plānojuši, bet CentOS 7 dzīves cikls beidzās 2024. gada 30. jūnijā. Ne viens, ne otrs vairs nesaņem drošības labojumus. CentOS Stream pastāv, taču tas atrodas pirms RHEL, nevis pēc tā: testēšanai noder, serverim, kuram jāstrādā klusi piecus gadus, ne.
AlmaLinux 9 ir binārā līmenī saderīga RHEL 9 pārbūve ar atbalstu līdz 2032. gadam. Pakotņu nosaukumi, ceļi, systemd servisu nosaukumi un SELinux politika sakrīt ar RHEL 9, tāpēc jebkura ražotāja dokumentācija, kas rakstīta RHEL 9, der bez pārtulkošanas. Atbalstīta pāreja no 7 uz 9 uz vietas nepastāv. ELevate spēj pārvest mašīnu no 7 uz 8 un tad no 8 uz 9, un tas tiešām strādā, bet uz virtuālas infrastruktūras parasti ātrāk un daudz drošāk ir uzlikt tīru 9, pārnest datus un pārslēgt DNS. Ja runa ir par nomātu VPS, veco var atstāt darboties, kamēr jaunais tiek pārbaudīts, un atkāpšanās ceļš ir bez maksas.
No kurienes ņemt PHP 8.3
Parasta dnf install php komanda uz AlmaLinux 9 caur moduli nemaz neiet: PHP 8.0 nāk kā parastas, ar moduļiem nesaistītas AppStream pakotnes. AlmaLinux tajās joprojām ieliek drošības labojumus, tātad šī 8.0 nav pamesta, bet versija visu laidiena mūžu paliek 8.0, kamēr pašā PHP projektā 8.0 drošības laidieni beidzās 2023. gada novembrī un bibliotēkas jau sen aizgājušas tālāk. Jaunākas versijas nāk kā AppStream moduļu straumes, kas pievienotas 9.x laidienu gaitā, un neviena no tām nav atzīmēta kā noklusētā, tāpēc pati no sevis neieslēdzas neviena. Nevis pieņemiet, bet paskatieties, kas konkrētajā mašīnā ir pieejams:
dnf module list php
dnf module list mariadb
Atkarībā no laidiena sarakstā būs 8.1, 8.2 un 8.3, un nevienai no tām nebūs [d] atzīmes. Ja 8.3 tur ir un gribat palikt tikai pie distributīva pakotnēm, izlaidiet EPEL un Remi soli un nākamajā blokā ieslēdziet php:8.3 tur, kur rakstīts php:remi-8.3; viss pārējais rakstā nemainās. Šeit izvēlēts Remi, kas jau daudzus gadus ir standarta trešās puses PHP būve Enterprise Linux videi, jo jaunie laidieni tur parādās dažu dienu laikā un 8.4 tajā pašā repozitorijā jau gaida brīdi, kad būs jāiet tālāk. Remi vajag EPEL, bet EPEL uz AlmaLinux 9 vajag ieslēgtu CRB repozitoriju:
dnf -y upgrade
dnf -y install dnf-plugins-core
dnf config-manager --set-enabled crb
dnf -y install epel-release
dnf -y install https://rpms.remirepo.net/enterprise/remi-release-9.rpm
Pirms instalēšanas straume jāatiestata. Ja neviena straume nekad nav bijusi ieslēgta, atiestatīšana neko nedara, bet dnf pats klusi nenomainīs straumi zem jau uzinstalētām pakotnēm, un tieši šeit lielākā daļa iestrēgst:
dnf -y module reset php
dnf -y module enable php:remi-8.3
dnf -y install php php-fpm php-cli php-mysqlnd php-opcache php-gd php-mbstring php-xml php-intl php-zip php-process
php -v
Divas lietas jāpatur prātā. Ja distributīva PHP jau bija uzlikts, pēc Remi straumes ieslēgšanas palaidiet dnf distro-sync, lai viss komplekts pārvietotos kopā, citādi paliek bāzes php-common kopā ar Remi php-mysqlnd un atkarību konflikts. Remi piedāvā arī paralēlās php83-php-* pakotnes, kas uzstādās /opt/remi un sadzīvo ar citām versijām; tās domātas mašīnām, kur vajadzīgas vairākas PHP versijas vienlaikus. Vienas vietnes serverim moduļu straume ir vienkāršāka un liek binārfailu tur, kur visi to gaida, /usr/bin/php.
Divi pakotņu nosaukumi maldina tos, kas nāk no CentOS 7. JSON kopš PHP 8.0 ir iebūvēts php-common, tāpēc atsevišķas php-json pakotnes vairs nav. Un php-mysql sen nepastāv, draiveris saucas php-mysqlnd.
Apache, MariaDB un php-fpm
dnf -y install httpd mariadb-server
systemctl enable --now httpd php-fpm mariadb
Pakotne php-fpm ieliek failu /etc/httpd/conf.d/php.conf, un tieši tas liek Apache nodot .php un .phar pieprasījumus FPM ligzdai /run/php-fpm/www.sock caur mod_proxy_fcgi. Tajā pašā failā ir arī .user.ini aizsardzība, AddType rinda un DirectoryIndex, tātad php.conf ir vienīgā Apache konfigurācija, ko PHP šeit pieliek. Faila /etc/httpd/conf.d/php-fpm.conf uz EL9 nav, lai ko rādītu vecākas CentOS pamācības: php-fpm.conf, kas tiešām eksistē, ir systemd papildfails mapē /usr/lib/systemd/system/httpd.service.d/ (Remi savu kopiju liek /etc/systemd/system/), un tas tikai liek httpd pieprasīt php-fpm servisu. Ar rpm -qf /etc/httpd/conf.d/php.conf redzēsiet, kurai pakotnei fails pieder konkrētajā mašīnā; ar Remi to uzskaita gan php, gan php-fpm. Pati SetHandler rinda atrodas IfModule blokā, kas nostrādā, kamēr nav ielādēts mod_php, un uz EL9 tā ir parastā situācija, jo distributīvs mod_php vispār nepiedāvā. mod_php šeit nav, un tas nav vajadzīgs: ar FPM Apache tikai starpnieko, bet PHP darbojas atsevišķā procesu pūlā ar savu lietotāju, un tieši tas padara vietņu izolāciju iespējamu. Ieskatieties /etc/httpd/conf.modules.d/00-mpm.conf, lai redzētu ieslēgto MPM, un neaiztieciet to bez konkrēta iemesla.
MariaDB drošība
AlmaLinux 9 pēc noklusējuma nāk ar MariaDB 10.5, jaunākos laidienos pieejamas arī svaigākas straumes. Sākot ar 10.5, binārfaili pārsaukti, tāpēc aktuālā komanda ir mariadb-secure-installation, bet mysql_secure_installation ir simboliska saite uz to pašu skriptu:
mariadb-secure-installation
Skripts piedāvās pārslēgt root uz unix_socket autentifikāciju, izmest anonīmos lietotājus, aizliegt attālinātu root pieslēgšanos un dzēst test datubāzi. Uz tīras instalācijas atbildiet ar jā visur. MariaDB 10.4 un jaunākās root parasti jau ir uz unix_socket, tas ir, mariadb -u root strādā operētājsistēmas root lietotājam bez jebkādas paroles un neviens cits tur netiek. Tas mulsina cilvēkus, kuri meklē paroli, ko nekad nav uzstādījuši. Pārbaudiet, kā ir īstenībā:
mariadb -u root -e "SELECT User, Host, plugin FROM mysql.user;"
Tālāk datubāze un lietotājs, kas nav root. Atšķirībā no visiem pārējiem blokiem šīs ir SQL komandas, tāpēc padodiet tās klientam, nevis līmējiet čaulā:
mariadb -u root <<'SQL'
CREATE DATABASE appdb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'appuser'@'localhost' IDENTIFIED BY 'gara-nejausa-parole';
GRANT ALL PRIVILEGES ON appdb.* TO 'appuser'@'localhost';
FLUSH PRIVILEGES;
SQL
Pārbaudiet, uz kā serveris klausās. Bez skaidri norādītas bind-address vērtības MariaDB pieņem savienojumus uz visām saskarnēm, un datubāze, kas sasniedzama no interneta, agri vai vēlu tiks atrasta:
ss -tlnp | grep 3306
Ja lietotne darbojas uz tā paša servera, failā /etc/my.cnf.d/mariadb-server.cnf sadaļā [mysqld] ierakstiet bind-address = 127.0.0.1 un pārstartējiet servisu. Savienojuma problēmu nekad nerisiniet, atverot 3306 portu ugunsmūrī.
firewalld
firewall-cmd --permanent --add-service=http
firewall-cmd --permanent --add-service=https
firewall-cmd --reload
firewall-cmd --list-all
Ja firewall-cmd nav atrodama, attēls uzbūvēts bez firewalld. Vai nu uzstādiet to, vai pārliecinieties, ka filtrēšana notiek tīkla līmenī, bet nepieņemiet, ka minimāls mākoņa attēls pēc noklusējuma ir aizsargāts.
SELinux, tā daļa, kas apēd pusi dienas
Atstājiet SELinux enforcing režīmā. Gandrīz katra neizskaidrojamā LAMP problēma uz EL9 ir marķējuma jautājums ar vienas rindas risinājumu. Vispirms rīki, jo minimālā instalācijā semanage nav:
dnf -y install policycoreutils-python-utils setroubleshoot-server
getsebool httpd_can_network_connect httpd_can_network_connect_db
Abi pēc noklusējuma ir izslēgti. Pirmais bloķē izejošos savienojumus no tīmekļa steka, tāpēc PHP zvans uz maksājumu API, SMTP releju vai attālinātu servisu neizdodas ar kļūdu, kurai ar tīklu nav nekāda sakara. Otrais attiecas uz datubāzi citā serverī. Šie ir divi neatkarīgi slēdži, un blokā zemāk ir abi: palaidiet tikai to rindu, kas jūsu lietotnei tiešām vajadzīga, jo katrs ieslēgtais slēdzis paplašina to, kur uzlauzta vietne var tikt:
setsebool -P httpd_can_network_connect on
setsebool -P httpd_can_network_connect_db on
Svarīgi: uz EL php-fpm darbojas tajā pašā httpd_t domēnā, kur Apache, tātad šie slēdži attiecas uz jūsu PHP kodu, ne tikai uz tīmekļa serveri.
Otra puse ir failu konteksti. Viss zem /var/www automātiski manto httpd_sys_content_t, un tieši tāpēc satura turēšana tur ietaupa darbu. Direktorijām, kurās PHP raksta, vajag httpd_sys_rw_content_t, un tā ir konkrēta mape, nevis visa dokumentu sakne. Noteikums neko nemarķē, kamēr ceļa nav, tāpēc vispirms izveidojiet mapes, īpašnieku tām uzstādīsim nākamajā sadaļā:
install -d /var/www/example.com/public/uploads
semanage fcontext -a -t httpd_sys_content_t "/var/www/example.com(/.*)?"
semanage fcontext -a -t httpd_sys_rw_content_t "/var/www/example.com/public/uploads(/.*)?"
restorecon -Rv /var/www/example.com
ls -Zd /var/www/example.com/public /var/www/example.com/public/uploads
Klasiskās lamatas: mv saglabā veco marķējumu, cp manto galamērķa marķējumu. Pārvietojiet vietni no /root vai /home uz /var/www ar mv, un katrs fails paliek ar tipu, ko Apache nedrīkst lasīt: rezultāts ir 403 un tiesību kļūda žurnālā, lai gan ls -l izskatās nevainojami. Vecs tar arhīvs ar paplašinātajiem atribūtiem uzvedas tāpat. Pēc jebkuras lielas pārvietošanas palaidiet restorecon -Rv.
Ja Apache klausās uz nestandarta porta, arī portam vajag marķējumu:
semanage port -a -t http_port_t -p tcp 8090
Ņemiet portu, kas politikā vēl nav aprakstīts. 8080 šim piemēram neder: tas jau ir definēts kā http_cache_port_t, tāpēc -a apstājas ar kļūdu par jau esošu portu, turklāt šim tipam Apache drīkst piesaistīties arī bez izmaiņām, tātad mainīt nebija ko. Ja portam tips jau ir un to gribat pārcelt, jālieto -m, nevis -a. Ar semanage port -l | grep 8090 redzēsiet, kurš gadījums ir jums.
Kad kaut kas ir liegts, izlasiet liegumu, nevis minat:
ausearch -m AVC,USER_AVC -ts recent
sealert -a /var/log/audit/audit.log
setenforce 0 nav risinājums. Tas paslēpj problēmu līdz pārstartēšanai, un tad problēma atgriežas visnepiemērotākajā brīdī.
Katrai vietnei savs PHP lietotājs
Noklusētais pūls darbojas kā lietotājs apache, tas nozīmē, ka jebkura vietne serverī var lasīt jebkuras citas failus un konfigurāciju. Katrai vietnei uztaisiet sistēmas kontu un atsevišķu pūlu. Vispirms lietotājs un mapes:
useradd -r -M -s /sbin/nologin -d /var/www/example.com example
install -d -o root -g root -m 0755 /var/www/example.com
install -d -o example -g example -m 0755 /var/www/example.com/public
install -d -o example -g example -m 0755 /var/www/example.com/public/uploads
install -d -o example -g example -m 0700 /var/www/example.com/sessions
semanage fcontext -a -t httpd_sys_rw_content_t "/var/www/example.com/sessions(/.*)?"
restorecon -Rv /var/www/example.com
Pēc tam fails /etc/php-fpm.d/example.conf:
[example]
user = example
group = example
listen = /run/php-fpm/example.sock
listen.mode = 0660
listen.acl_users = apache
pm = dynamic
pm.max_children = 20
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 6
php_admin_flag[log_errors] = on
php_admin_value[error_log] = /var/log/php-fpm/example-error.log
php_admin_value[open_basedir] = /var/www/example.com/:/tmp/
php_value[session.save_path] = /var/www/example.com/sessions
systemctl restart php-fpm
ls -l /run/php-fpm/
Sesiju mape ir svarīgāka, nekā izskatās. Pakotnes /var/lib/php/session pieder root:apache ar tiesībām 0770, tāpēc pūls ar citu lietotāju tur pat neiekļūst, un apakšmape neko nelīdz: pati /var/lib/php/session pūla lietotāju tālāk neielaiž. Ceļam jābūt arī open_basedir robežās, citādi session_start() atbild ar open_basedir ierobežojumu un sesija nekur nenonāk. Mape zem vietnes saknes nokārto abas lietas uzreiz: īpašnieks ir pūla lietotājs, open_basedir to jau aptver, un, tā kā tā atrodas blakus public, nevis iekšā, Apache no tās neko neizsniedz. listen.acl_users ir tas, kas ļauj Apache pieslēgties root piederošai ligzdai. FPM galvenais process darbojas kā root, tāpēc ligzda tāpat sanāk root īpašumā, un listen.owner vai listen.group blakus ACL nedod neko citu kā ierakstu par ignorēšanu žurnālā katrā startā. Ja failu sistēma neatbalsta ACL, izmetiet acl rindu un tās vietā lieciet listen.group = apache, atstājot 0660.
pm.max_children aprēķiniet pēc reālas atmiņas: nomēriet noslogota darbinieka RSS un daliet ar to to atmiņas daudzumu, ko esat gatavs atdot PHP. Divdesmit procesi pa 80 MB ir 1.6 GB, pirms datubāze saņēmusi kaut ko. Uz maza VPS turiet skaitli zemu un ļaujiet pieprasījumiem gaidīt rindā, noslogotai vietnei dedicēts serveris dod vietu to palielināt.
Minimāls virtuālais hosts
Enterprise Linux nav sites-available un sites-enabled. Startējot tiek nolasīts viss, kas /etc/httpd/conf.d beidzas ar .conf. Izveidojiet /etc/httpd/conf.d/example.com.conf:
<VirtualHost *:80>
ServerName example.com
ServerAlias www.example.com
DocumentRoot /var/www/example.com/public
<Directory /var/www/example.com/public>
AllowOverride All
Require all granted
</Directory>
<FilesMatch \.(php|phar)$>
SetHandler "proxy:unix:/run/php-fpm/example.sock|fcgi://localhost"
</FilesMatch>
ErrorLog /var/log/httpd/example.com-error.log
CustomLog /var/log/httpd/example.com-access.log combined
</VirtualHost>
FilesMatch bloks virtuālajā hostā pārraksta globālo bloku no pakotnes php.conf, kas norāda uz www.sock. Bez tā vietne strādā uz kopīgā noklusētā pūla ar lietotāju apache un visa iepriekšējā izolācija ir bezjēdzīga. Norādiet tos pašus paplašinājumus, ko pakotnes bloks, proti, .php un .phar: ja atstāsiet tikai .php, pieprasījums uz .phar failu joprojām aizies uz globālo bloku un izpildīsies kopīgajā pūlā ar lietotāju apache, tātad tieši tajā caurumā, kura dēļ pūls tika taisīts. AlmaLinux sveiciena lapa nāk no /etc/httpd/conf.d/welcome.conf: to var iztukšot, bet nedzēsiet pašu failu, pakotnes atjauninājums to atliks atpakaļ.
Kā pārbaudīt visu steku
httpd -t
php-fpm -t
systemctl restart httpd php-fpm
systemctl is-enabled httpd php-fpm mariadb
curl -sI -H "Host: example.com" http://127.0.0.1/
httpd -t brīdinājums par to, ka nevar noteikt pilnu domēna vārdu, ir nekaitīgs; ja traucē, uzstādiet ServerName globāli. Tagad jāpierāda, ka PHP tiešām izpildās un tieši pareizajā pūlā:
printf '%s\n' '<?php echo PHP_VERSION, " as ", posix_getpwuid(posix_geteuid())["name"], "\n";' > /var/www/example.com/public/v.php
curl -s -H "Host: example.com" http://127.0.0.1/v.php
rm -f /var/www/example.com/public/v.php
Atbildē jābūt versijai un pūla lietotājam, nevis apache. Funkcijas posix nāk no pakotnes php-process, tāpēc tā ir instalēšanas rindā augstāk: bez tās lapa apstājas ar nezināmas funkcijas kļūdu un atbildes nav. Failu uzreiz izdzēsiet. phpinfo lapu uz publiska servera atstāt nedrīkst, tā uzbrucējam uzdāvina ceļus, paplašinājumus un konfigurāciju.
Datubāzi pārbaudiet caur tīmekļa serveri, nevis no komandrindas. php -r vienrindnieks, palaists kā root, darbojas ārpus ierobežotā domēna un pieslēgsies bez problēmām, kamēr tas pats kods zem httpd_t neizdodas, un cilvēks meklē PHP kļūdu, kuras nav. Dariet to tikai pēc tam, kad iepriekšējā pārbaude atgrieza pūla lietotāju: ja dokumentu sakne PHP vēl neizpilda, fails tiks izsniegts kā teksts kopā ar paroli:
printf '%s\n' '<?php $d = new PDO("mysql:host=localhost;dbname=appdb;charset=utf8mb4", "appuser", "gara-nejausa-parole"); echo $d->query("SELECT VERSION()")->fetchColumn();' > /var/www/example.com/public/db.php
curl -s -H "Host: example.com" http://127.0.0.1/db.php
rm -f /var/www/example.com/public/db.php
Žurnāli un tipiskās kļūmes
tail -f /var/log/httpd/example.com-error.log
tail -f /var/log/php-fpm/example-error.log
tail -f /var/log/mariadb/mariadb.log
journalctl -xeu php-fpm
ausearch -m AVC -ts recent
503 no Apache kopā ar connection refused vai permission denied nozīmē, ka FPM ligzdas nav vai tā nav nolasāma. Pārbaudiet, vai php-fpm ir startējis, vai /run/php-fpm/example.sock eksistē un vai listen.acl_users satur apache. Atcerieties, ka /run ir tmpfs, ligzda tiek izveidota katrā startā un pašrocīgi tur kaut ko taisīt nav jēgas.
Ja pārlūks .php failu lejupielādē, nevis izpilda, apstrādātājs nav nostrādājis: vai nu mod_proxy_fcgi nav ielādēts, vai FilesMatch atrodas ārpus virtuālā hosta. 403 uz failiem, kurus kā root var izlasīt, gandrīz vienmēr ir SELinux marķējums. Pilnīgi tukša lapa ar statusu 200 ir PHP fatālā kļūda ar izslēgtu display_errors, un tā gaida pūla žurnālā. Sesijas, kas pazūd katrā pieprasījumā, atkal ir sesiju mape: vai nu pūla lietotājs tur nevar rakstīt, vai ceļš paliek ārpus open_basedir.
Kad steks atbild uz 80. porta, pirms reāla satura pievienojiet TLS: uzstādiet mod_ssl, atveriet https servisu firewalld un izsniedziet sertifikātu. Ja gribat, lai pakotnes atjaunina un marķējumus kārto kāds cits, mūsu inženieri ir pieejami visu diennakti un serveri stāv mūsu pašu datu centrā Rīgā, skatiet IT atbalsts.
Turpini lasīt
Gatavs sākt?
Palaid dažās minūtēs vai aprunājies ar inženieri par piemērotāko risinājumu.