虛擬主機設計與規劃

最近替主機換新的硬體,順便重新檢視虛擬主機的設計,做些改良後決定寫下來

使用者帳號與網頁檔案

因為規模小,使用者採用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

留言

粗體斜體刪除線連結引用圖片程式碼

注意:您的電子信箱將不會被公開,且網站連結不會被搜尋引擎採計