朋友的 WordPress 網站是五年前幫他架設在 AWS EC2 上,這五年來沒有特別的維護與升級,終於(?)在今年的一月被駭客入侵,網站出現一大堆亂七八糟的帳號和色情文章。於是開始拯救與升級 legacy 架構的挑戰,此篇分享整個遷移和升級過程,大家一起來看看吧!
目錄
原始狀況描述
- 2018 年架設後就沒有再做特別的維護
- EC2 是老舊的 Ubuntu 16.04
- WordPress v4.9.3 和 php 7.0 fpm 直接安裝在 host 上,透過 nginx 作反向代理
- AWS RDS 作為資料庫實例 ,運行老舊的 MySQL 5.7,有開啟自動備份
- 圖片等上傳檔除了本機 EBS 有一份,S3 上也會 sync 一份
- 今年一月時網站被駭客透過 WordPress 漏洞入侵,建立很多假帳號,並注入一堆有問題的程式碼和檔案
- 因網站被注入,效能大幅下降導致 SEO 變差,從 google search console 中看到點擊率在一月被入侵後開始受到嚴重影響
緩解入侵
首先必須先緩解掉持續的暴力破解,先幫朋友申請 Cloudflare 並擋在 EC2 前面,再依照之前的文章 防範 WordPress 被攻擊的 8 個方法 設定好防火牆,開啟 on attack 模式,大量阻擋可疑 request。
修改成現代化架構
以目前 Web 架設概念應該要盡可能的讓資料與應用伺服器分離,也就是說應用伺服器應該是無狀態、隨時可替換的。並使用 docker,讓 application 跑在 container 裡面。運行環境跟著 container,某方面來說使 host 與 application 解依賴,因此 host 隨時可更換,不用擔心系統升級等問題。
但 WordPress 先天限制,User 上傳的圖檔放在 WordPress source code 目錄之中,若要將上傳的圖片檔案改放置 S3 上可能需要魔改 WordPress 或加裝套件。
比較簡單的作法是把使用者上傳相關的資料夾從 container 掛載出來
實現分離,並將掛載出來的資料 放在另一顆 EBS 上面
。未來若有需要,直接拔掉 EBS 接到另一台 EC2 上即可。
但你懂得,有時候會需要進去手改一下 wp-config.php
或其他主題相關的 source code,因此我會把 WordPress container 中整個 /var/www/html
掛出來,這樣要調整比較方便,不怕 container 移除後重啟導致資料不見。
另外擋案上傳和 php 運行的設定需要調整,因此把 upload.ini
、 php.ini-production
也掛載出來。
wp:
depends_on:
- db
image: wordpress:5.0.3-php7.3-fpm
container_name: bgs_wp
volumes:
- ./wp_html:/var/www/html
- ./php_cfg/uploads.ini:/usr/local/etc/php/conf.d/uploads.ini
- ./php_cfg/php.ini:/usr/local/etc/php/php.ini-production
networks:
wp:
restart: always
logging:
driver: "json-file"
options:
max-size: "1m"
max-file: "3"
environment:
WORDPRESS_DB_HOST: db:3306
WORDPRESS_DB_USER: <user>
WORDPRESS_DB_PASSWORD: <user>
WORDPRESS_DB_NAME: <db>
資料庫改使用 docker 的方式啟動,將 /var/lib/mysql
資料夾掛載出來(一樣掛到另一顆 EBS 上),讓資料庫檔案可以儲存在外部,避免 container 重建後消失。
同時建立定期備份 script,透過 host cronjob 觸發,將 db dump 到 /tmp/db_dump
再上傳至 S3。 後面會描述備份的實作方式。
這裡把 RDS 拿掉的原因是朋友的網站流量沒有大到需要開獨立的資料庫,透過 docker 全部起在同一台 EC2 上,雖然會喪失 RDS 自動備份功能,但卻能省下原本一半以上的 cost。同時自己建立簡單的備份機制並不難,因此這部份最後決定這樣作。
db:
image: mysql/mysql-server:8.0.22
container_name: bgs_db
volumes:
- ./mysql:/var/lib/mysql
- ./db_dump:/tmp/db_dump
restart: always
networks:
wp:
command: mysqld --default-authentication-plugin=mysql_native_password
logging:
driver: "json-file"
options:
max-size: "1m"
max-file: "3"
environment:
MYSQL_ROOT_PASSWORD: <pwd>
MYSQL_DATABASE: <db>
MYSQL_USER: <user>
MYSQL_PASSWORD: <pwd>
nginx 的部分
/etc/nginx/conf.d
掛出來,提供 nginx 的設定檔/etc/nginx/ssl
掛出來,提供 SSL key 和 cert/var/www/html
和 WordPress container 掛到同一個目錄上,讓 nginx 可以 pass fastcgi 外,也能直接 hostwp-content
或wp-includes
的靜態檔
nginx:
depends_on:
- wp
image: nginx:1.23.3
container_name: bgs_nginx
restart: always
ports:
- "80:80"
- "443:443"
networks:
wp:
volumes:
- ./wp_html:/var/www/html
- ./nginx:/etc/nginx/conf.d
- ./ssl:/etc/nginx/ssl
environment:
TZ: Asia/Taipe
三者結合起來的 docker-compose.yml
如下
version: '3'
services:
db:
image: mysql/mysql-server:8.0.22
container_name: bgs_db
volumes:
- ./mysql:/var/lib/mysql
- ./db_dump:/tmp/db_dump
restart: always
networks:
wp:
command: mysqld --default-authentication-plugin=mysql_native_password
logging:
driver: "json-file"
options:
max-size: "1m"
max-file: "3"
environment:
MYSQL_ROOT_PASSWORD: <pwd>
MYSQL_DATABASE: <db>
MYSQL_USER: <user>
MYSQL_PASSWORD: <pwd>
wp:
depends_on:
- db
image: wordpress:5.0.3-php7.3-fpm
container_name: bgs_wp
volumes:
- ./wp_html:/var/www/html
- ./php_cfg/uploads.ini:/usr/local/etc/php/conf.d/uploads.ini
- ./php_cfg/php.ini:/usr/local/etc/php/php.ini-production
networks:
wp:
restart: always
logging:
driver: "json-file"
options:
max-size: "1m"
max-file: "3"
environment:
WORDPRESS_DB_HOST: db:3306
WORDPRESS_DB_USER: <user>
WORDPRESS_DB_PASSWORD: <user>
WORDPRESS_DB_NAME: <db>
nginx:
depends_on:
- wp
image: nginx:1.23.3
container_name: bgs_nginx
restart: always
ports:
- "80:80"
- "443:443"
networks:
wp:
volumes:
- ./wp_html:/var/www/html
- ./nginx:/etc/nginx/conf.d
- ./ssl:/etc/nginx/ssl
environment:
TZ: Asia/Taipei
networks:
wp:
name: wp
driver: bridge
方向有了,開始下手吧!
有了遷移的目標,接下來開始把 legacy 架構的 WordPress 搬過去,同時需要達到以下目標:
- 駭客 inject 的 source code 不能帶過去
- WordPress 要從 4.9.3 升級到 5.0.3 版 (目前套用主題最高支援版本)
- 文章、圖片不能丟失
那就開始動手囉!
Step 1:準備好 new EC2,安裝 docker
這部份不特別描述,大致就是去 aws web console 開一台新的 EC2,更新系統、設定好防火牆、SSH key 和安裝 docker
Step 2:dump 資料庫與複製 uploads 資料
在 aws web console 新建一顆 EBS 並 attach 到 old EC2 上,再到 old EC2 中下指令 format 後掛載
# 確認新的 EBS 裝置名稱
$ lsblk
# 新增的 EBS 完全空白,需要分割並建立磁區
$ sudo mkfs -t xfs /dev/nvme1n1
# 建立 /data folder
$ sudo mkdir /data
# 將 EBS 掛到 /data
$ sudo mount /dev/nvme1n1 /data
# 建立 bgs 資料夾,等一下要搬移資料使用
$ sudo mkdir /data/bgs
# 改變資料夾權限
$ sudo chown ubuntu:ubuntu /data/bgs
透過 old EC2 連接 RDS,將 db dump 到 EBS 上。
$ docker run -it --rm -v /data/bgs/db_dump:/tmp/db_dump mysql:8.0.32 bash -c "mysqldump -h xxxx.xxxx.us-west-2.rds.amazonaws.com -u <user> -pxxxxxxx db > /tmp/db_dump/db_dump.sql --set-gtid-purged=OFF --column-statistics=0
因為我使用 MySQL 8 的 image 對 5.7 作 dump,若直接跑會看到以下錯誤
mysqldump throws: Unknown table 'COLUMN_STATISTICS' in information_schema (1109)
依照這篇討論,加上 --column-statistics=0
參數即可解決
接下來將 old EC2 中的 /var/www/html/wp-content/uploads
整個複製到 EBS 上
$ sudo cp -R /var/www/html/wp-content/uploads /data/bgs/
最後將 EBS 從 old EC2 上拔掉。先在 old EC2 上下 umount
後,再去 aws web console detach EBS。
$ sudo umount /data
Step3:restore 資料庫
在 aws web console 將 EBS attach 到 new EC2,並在 new EC2 中掛載。
# 建立 /data folder
$ sudo mkdir /data
# 將 EBS 掛到 /data
$ sudo mount /dev/nvme1n1 /data
因這顆 EBS 要一直給 new EC2 使用,需設定自動 mount after reboot。
首先查詢 EBS 的 UUID
$ sudo blkid
...
/dev/nvme1n1: UUID="680badd5-9f95-485a-b267-4b2aaddfd1bb" BLOCK_SIZE="512" TYPE="xfs"
...
修改 /etc/fstab
令 Ubuntu 開機時自動掛載 EBS
$ sudo nano /etc/fstab
在檔案最後一行增加以下這一行,儲存離開即可(請依照上一步查尋的 UUID 作替換)
UUID=680badd5-9f95-485a-b267-4b2aaddfd1b /data xfs defaults,nofail 0 2
緊接著 docker 起 MySQL,restore 剛剛 dump 的資料
$ cd /data/bgs
$ docker compose up -d db
$ docker exec -it bgs_db bash -c "mysql -h localhost -u root -pxxxxxx -e \"CREATE DATABASE db;GRANT ALL PRIVILEGES ON db.* TO '<username>'@'%';\""
$ docker exec -it bgs_db bash -c "mysql -h localhost -u root -pxxxxxx db < /tmp/db_dump/db_dump.sql"
完成後,docker 再起 WordPress v4.9.3 + nginx,開啟瀏覽器進入網站。此步是先確定文章等資料有正常匯入,避免直接用 v5 的 WordPress 起可能遇到 migration 問題而不好確認 root cause 並排除。
$ docker compose up -d
此時網頁圖片消失是正常,因為我們還沒有把 uploads
資料夾放入正確路徑。
Step4:升級 WordPress
接下來要升級 WordPress 到 v5.0.3。必須先 down wp container, 並把 4.9.3 初始化的 wp_html 資料夾整個刪掉
,再將 docker-compose.yml
中的 wp container 修改成 v5.0.3
,up 啟動讓他重新初始化目錄。
# 移除 wordpress v4 container
$ docker compose down wp
# 刪掉整個 wp source code
$ sudo rm -Rf wp_html
# 修改 docker compose 中 wordpress 的版本
$ nano docker-compose.yml
# 重新建立 wordpress v5 container
$ docker compose up -d
會這麼搞剛的原因是,Wordpress web 升級頁面預設只能 升級到最新版
,以目前來說會是 v6.1.1,若要升級到指定版本,只能自己上傳原始碼並作替換。
思考了下,與其我手動上傳,不如清掉目錄檔案,並替換成對應版本的 image 讓他重新初始化來的方便。
替換完成後,瀏覽器再進入 WordPress 一次,此時出現需要 migrate db 提示,大膽給他按下去,不到五秒就會完成了。
升級一切順利,可以進入下一步。
Step 5:還原 uploads 資料
將剛剛從 old EC2 搬過來的 uploads
資料夾,移入 WordPress container 掛出來目錄中對應位置即可。記得修改權限成 www-data
,後續才能透過網頁上傳新的圖片
# 搬移 uploads 資料夾到正確位置
$ sudo mv uploads wp_html/wp-content/
# 修改權限,直接對整個 wp folder 下比較方便
$ sudo chown -R www-data:www-data wp-html
此時再重新進入網站圖片就能正確出現了!但外掛和主題沒有搬過來,此時頁面排版會全部亂掉!沒關係,只需要人工從 wordpress 後台重新安裝即可,如此也能避免將被駭客污染的程式碼帶過來。
清除駭客建立的假帳號
接下來要對假帳號作清理,但假帳號 (Subscriber) 多達一萬五千個!人工手動從 WordPress 後台刪除非常耗時
查了一下,Wordpress 有提供 cli 工具,能透過 user type 過濾出 subscriber 直接作 batch delete。
但 WordPress image 預設沒有安裝 cli 工具,難道還要自己進去安裝?其實不用, WordPress 官方 還有另外推出 tag 為 cli
開頭的 docker image,可以直接起他呼叫指令。
為了後續方便維運操作,簡單寫一個 wp_cli.yml
專門起 wordpress cli container,並將 command 用 sleep infinity
override,讓 container 啟動後可以活著但不做事,方便進入裡面下指令操作。
version: '3'
services:
wp-cli:
image: wordpress:cli-2.5.0-php7.3
container_name: bgs_wp_cli
volumes:
- ./wp_html:/var/www/html
networks:
wp:
restart: always
command: sleep infinity
logging:
driver: "json-file"
options:
max-size: "1m"
max-file: "3"
environment:
WORDPRESS_DB_HOST: db:3306
WORDPRESS_DB_USER: <user>
WORDPRESS_DB_PASSWORD: <pwd>
WORDPRESS_DB_NAME: <db>
networks:
wp:
name: wp
driver: bridge
external: true
進入 wordpress cli container 開始大量刪除假帳號吧!加參數 --number=10
先刪 10 個確認下,沒問題後拔掉參數一口氣全部刪除!
# at host
$ docker exec -it wp-cli bash
# in container
$ wp user delete $(wp user list --role=subscriber --field=ID --number=10) --reassign=1
刪除可能需要一小段時間,因為他是 一筆筆刪除 user
,沒有使用 bulk delete,刪除量越大越耗時
建立自動化備份
最後要建立自動化備份,首先是 db backup,一樣用 mysqldump
將 db dump 出來, tar
壓縮後,透過 aws cli container sync 到 s3 上,最後再清掉本機上的備份檔。
#!/bin/bash
docker exec -it bgs_db bash -c "mysqldump -h localhost -u root -pxxxxx db > /tmp/db_dump/db.sql"
cd /data/bgs/db_dump/
sudo tar zcvPf /data/bgs/db_dump/db_dump$(date '+%Y%m%d').tar.gz db_dump.sql
docker run --rm -it -v /data/aws:/root/.aws -v /data/bgs/db_dump:/db_dump amazon/aws-cli s3 cp /db_dump/db_dump$(date '+%Y%m%d').tar.gz s3://<bucket>/backup/db/
sudo rm /data/bgs/db_dump/db_dump.sql
sudo rm /data/bgs/db_dump/db_dump$(date '+%Y%m%d').tar.gz
再來是備份 uploads
裡的檔案,一樣透過 aws cli container sync 到 s3
#!/bin/bash
docker run --rm -it -v /data/aws:/root/.aws -v /data/bgs/wp_html/wp-content/uploads:/uploads amazon/aws-cli s3 sync /uploads s3://<bucket>/backup/uploads
將以上兩份 script 透過 crontab 定期執行來達成自動化備份
看看成效吧
自此搬移與升級就算完成了,清理掉駭客 inject 的程式碼並升級到 v5 後,本來首頁的 TTFB 高達 3.33 s,現在只需要 0.6s,下降了 80%
從 google search console 中能看到修復後點擊率開始回升
但 WordPress 5.0.3 還是很舊,因此後續需要朋友去 survey 合適的主題,替換後再作一次 WordPress 版本升級。
至此分享整個搬遷和升級過程,希望能幫助到有類似需求的人!
延伸閱讀
防範 WordPress 被攻擊的 8 個方法
WordPress 部落格被攻擊全記錄