最近替主機換新的硬體,順便重新檢視虛擬主機的設計,做些改良後決定寫下來
使用者帳號與網頁檔案
因為規模小,使用者採用unix系統帳戶來管理,網站使用以下的格式放置在每個人的家目錄裡面
/home/{user_name}/sites/{domain_name}/
每個網站的目錄底下各會有「backup」「log」「temp」「webroot」四個資料夾
- 「backup」是放定期打包好的網站目錄,畢竟直接用FTP拉一堆資料夾效率很差
- 「log」有apache_access、apache_error、php_error的檔案,並會定期做logrotate
- 「temp」是開給php放暫存的檔案,不論是上傳檔案、session、opcache通通在裡面
- 「webroot」顧名思義就是放網站的地方
整體而言會需要存取家目錄的程式有「httpd」「php-fpm」「vsftpd」「logrotate」這四個
這邊採取的安全措施有三種,略分為「file permission」「software access control」「SELinux context」
「file permission」就是磁碟的權限,家目錄下一律是使用者擁有自己的檔案
- 「httpd」主動加入使用者的群組,並將家目錄設為群組可讀
- 「php-fpm」直接以使用者的身分啟動 process
- 「vsftpd」使用 chroot_local_user 切換成使用者的身分
- 「logrotate」是 root 身分不受權限影響
「software access control」是軟體本身定義存取的範圍
- 「httpd」設定 DocumentRoot 在 webroot
- 「php-fpm」利用 open_basedir 把存取範圍限制在 temp 和 webroot
- 「vsftpd」使用 chroot 限制在家目錄裡面
- 「logrotate」從 config 定義 log 的位置
「SELinux context」是系統對軟體的存取控制,可輔助磁碟權限的不足,我對家目錄用了以下規則
semanage fcontext -a -t httpd_log_t '/home/[^/]+/sites/[^/]+/log(/.*)?'
semanage fcontext -a -t httpd_sys_rw_content_t '/home/[^/]+/sites/[^/]+/temp(/.*)?'
semanage fcontext -a -t httpd_sys_rw_content_t '/home/[^/]+/sites/[^/]+/webroot(/.*)?'
- 「httpd」和「php-fpm」在 SELinux 底下都屬於 httpd ,除了 httpd context 以外的家目錄內容都無法存取
- 「vsftpd」預設會看不到非 user_home_t 的內容,需要打開 SELinux boolean 的 ftpd_full_access
- log 資料夾如果沒有改 context 的話,「logrotate」會無法操作檔案,改成 var_log_t 可以操作但 httpd 又會無法寫入
httpd
用戶在 httpd 的設定我是直接用 template 做 sed 取代字串後丟進去
template 就像下面這個樣子,一個 virtual host block 同時具備兩種連線能力
<VirtualHost *:80 *:443> DocumentRoot "/home/%user%/sites/%domain%/webroot" ServerName %domain% Protocols h2 http/1.1 SSLEngine on SSLCertificateFile "/etc/letsencrypt/live/%domain%/fullchain.pem" SSLCertificateKeyFile "/etc/letsencrypt/live/%domain%/privkey.pem" ErrorLog "/home/%user%/sites/%domain%/log/apache_error_log" CustomLog "/home/%user%/sites/%domain%/log/apache_access_log" common <Directory "/home/%user%/sites/%domain%/webroot"> AllowOverride Limit AuthConfig FileInfo Require method GET POST HEAD </Directory> <IfModule proxy_module> ProxyTimeout 30 <FilesMatch \.php$> SetHandler "proxy:unix:/var/php-fpm/%domain%.sock|fcgi://localhost/" </FilesMatch> </IfModule> </VirtualHost>
值得注意的是,這種寫法的設定必須在 default virtual host 後面載入,不然會出現這個錯誤訊息 speaking plain HTTP to an SSL-enabled server
再來是要與 php-fpm 連線的話,記得要開啟這兩個 SELinux boolean
- httpd_can_network_connect
- httpd_enable_cgi
letsencrypt 的憑證則是用 certbot 來取得,我個人不喜歡在 web server 裡面插一堆外掛,所以都是用 certonly webroot 的方式來取得憑證
等虛擬主機的 config 載入後,利用下面這段,把 /var/www/challenges/
掛在所有網站下面,這樣只需要固定的資料夾來呼叫 certbot,不會因為網站而有差別
Alias "/.well-known/acme-challenge/" "/var/www/challenges/.well-known/acme-challenge/" <Directory "/var/www/challenges/.well-known/acme-challenge/"> Require all granted </Directory>
php-fpm
php-fpm 預設會帶有一個叫做 www 的 pool,通常你會想要直接把他刪掉,但這個設定檔是跟著套件來的,如果直接把檔案刪掉,下次更新時他會以為檔案不存在又弄一個出來
比較好的辦法是直接修改原檔,你可能又想說直接清空好了,可惜這樣做也是不行
上層 php-fpm.conf 在 include 設定檔的時候,要是 include 到空的檔案會直接爆炸
因此我推薦寫成下面這樣,ondemand 模式在沒有流量的時候不會開 process 來用
[www] user = nobody group = nobody listen = /var/php-fpm/www.sock listen.owner = root listen.group = root listen.mode = 0660 pm = ondemand pm.max_children = 1
使用者的設定檔則是和 httpd 那邊一樣,利用 template 做 sed 的方式來生成,並在裡面利用 include 來引入共同的設定
[%domain%] listen = /var/php-fpm/%domain%.sock user = %user% group = %user% ; Include common setting include=/etc/php-fpm.common.setting php_admin_value[error_log] = /home/%user%/sites/%domain%/log/php-error.log php_admin_value[session.save_path] = /home/%user%/sites/%domain%/temp/session php_admin_value[sys_temp_dir] = /home/%user%/sites/%domain%/temp/other php_admin_value[upload_tmp_dir] = /home/%user%/sites/%domain%/temp/upload php_admin_value[opcache.file_cache] = /home/%user%/sites/%domain%/temp/opcache php_admin_value[opcache.lockfile_path] = /home/%user%/sites/%domain%/temp/opcache php_admin_value[open_basedir] = /home/%user%/sites/%domain%/temp/:/home/%user%/sites/%domain%/webroot/
一般來說 socket file 會放在 /var/run 下面,但我的作業系統把 /var/run 掛成 tmpfs,每次重開機就清空了,因此我在 /var 下面開了一個 php-fpm 資料夾來裝
如果你跟我一樣是自己開資料夾來裝,記得要把資料的 SELinux context 設成 var_run_t
,不這樣做的話啟動時會直接爆炸
mysql
這部分就沒想到好的設計,因為資料庫名不接受小數點,導致用來管理的 unix 系統帳號和網域名稱沒辦法直接對應到 mysql 內的資料庫,再者是 mysql 帳號密碼和認證,仍必須手動處理並填入網站的檔案裡
所以這小段要講的是關於改用 mysql 8.0 要注意的地方
照本來習慣的操作建使用者、開資料庫、匯入,完成後卻會發現 php 那邊因為帳號密碼錯誤連不上資料庫,但帳號密碼直接用 console 連卻可以
php 可能會看到類似這樣的錯誤訊息:The server requested authentication method unknown to the client
這種情形是因為 mysql 的 password hashing 預設值被改為 caching_sha2_password,如果 client 尚未支援新版的密碼算法就會無法驗證,目前的解法是改回去用舊版的密碼算法
建使用者的時候指定使用 mysql_native_password 即可
CREATE USER 'vhost_user'@'localhost' IDENTIFIED WITH mysql_native_password BY 'very_secure_password';
不想每次都加的話可以在 mysqld 設定裡面指定 default_authentication_plugin
[mysqld] default_authentication_plugin = mysql_native_password
等到可以連上後,又會發現怎麼還是有錯誤
PHP 會有類似這樣的錯誤訊息:Server sent charset unknown to the client.
這是因為 mysql 8.0 開始 default collation 改為 utf8mb4_0900_ai_ci ,server 回報的 default collation 如果 client 不認得就會發生此錯誤
解法是修改 mysql 的 collation-server 成別的語言
[mysqld] collation-server = utf8mb4_unicode_ci character-set-server = utf8mb4
留言