Ubuntu 18.04에서 PHP2Shell 대응하기 — Nginx + PHP-FPM 전환 기록

by

이전글에서 이어지는 두번째 기록입니다.

이전글은 도커 환경에서의 php 교체기록이면 이번엔 순수 우분투 환경에서 php를 교체하는 작업입니다.


개인적으로 도커보다 이 작업이 더 어려웠습니다...



앞선 두 서버의 Docker PHP 교체를 마친 뒤, 마지막 서버의 PHP2Shell 대응을 진행했다. 이번 서버는 Ubuntu에 Nginx와 PHP-FPM을 직접 설치해 운영하는 환경이었다.


컨테이너 이미지를 교체하는 대신 새 PHP를 별도 경로에 빌드하고, 독립된 FPM 서비스를 띄운 다음 Nginx의 Unix 소켓 연결을 바꿨다. 기존 PHP는 전환 확인이 끝날 때까지 유지했다.


작업은 2026년 10월 23일에 진행했다. 이 글에서는 임시 완화부터 소스 패치, 빌드 중 메모리 오류, 서비스 전환, 실제 웹 응답 확인까지 정리한다. 서버 주소와 도메인, 사용자 작업 디렉터리, 설치 prefix는 공개용 예시로 바꿨다. 명령과 설정은 당시 작업의 핵심 부분을 발췌했으며, 전체 자동 설치 스크립트는 아니다.



1. 기존 환경과 이번 작업의 범위

기존 요청 처리 구조는 단순했다.


웹 요청 → Nginx → Unix 소켓 → PHP 7.2 FPM → 기존 애플리케이션Copy


항목작업 전 확인한 상태
운영체제Ubuntu 18.04.6 LTS, x86-64
웹 서버호스트에 직접 설치한 Nginx
PHP7.2.24-0ubuntu0.18.04.17
FPM 서비스php7.2-fpm.service
PHP 설정/etc/php/7.2/fpm/php.ini
Nginx 연결unix:/var/run/php/php7.2-fpm.sock
메모리약 1.9GiB, 조회 당시 available 약 1.5GiB
Swap없음
루트 디스크약 39GB, 처음 조회 시 여유 1.8GB 



이번 목표는 웹 요청을 처리하는 PHP를 두 취약점의 공식 최소 보안 백포트를 적용한 PHP 7.4.33으로 전환하는 것이었다. OS 업그레이드나 애플리케이션 전면 개편까지 한 번에 진행하지는 않았다.


PHP 7.4는 정규 지원이 종료된 버전이다. 이번 선택은 기존 서비스의 호환성과 당시 적용 경로를 고려한 것이며, Ubuntu 18.04에서 사용할 수 있는 PHP의 기술적 최대 버전이라는 뜻은 아니다. 장기적인 OS·PHP 지원 버전 이전은 별도 과제로 구분했다. [PHP 지원 종료 목록]



2. PHP 7.4라는 숫자만으로 보안 대응을 판단하지 않기

2026년 10월 21일 PHP 프로젝트가 공개한 권고는 PHP-FPM을 포함한 웹 요청 처리 환경에 영향을 주는 두 취약점을 다뤘다. [PHP 공식 보안 권고]


취약점핵심문제
CVE-2026-109618메모리 부족 처리 도중 불완전하게 초기화된 요청 데이터가 사용될 수 있음
CVE-2026-109619해제된 요청 메모리를 가리키는 내부 상태가 남아 후속 요청에서 사용될 수 있음


이 서버에서는 Ubuntu Pro를 사용하지 않는 조건이었다. 일반 저장소의 패키지 목록을 갱신해도 설치된 PHP 7.2 패키지와 Candidate가 같았다.


Installed: 7.2.24-0ubuntu0.18.04.17
Candidate: 7.2.24-0ubuntu0.18.04.17Copy



이때 확인한 저장소만으로 목표 수정본을 확보할 수 없었으므로 별도 소스 빌드 경로를 택했다. PHP 프로젝트는 이번 이슈에 한해 PHP 7.4용 최소 소스 백포트도 제공했다. [공식 백포트 안내]


사용한 커밋은 다음과 같다.


712e60f9edfd03f0960fc4fc3c2c375f42b90aa2Copy



커밋 메시지는 `Avoid state corruption via bailout`이며 두 CVE를 명시한다. 변경 대상은 Zend 엔진, 세션과 요청 처리 관련 코드 등 17개 파일이다. [PHP 공식 수정 커밋]


패치를 적용해도 PHP 버전 표시는 `7.4.33`으로 남는다. 따라서 이후 검증은 버전 문자열에 더해 원본·패치 해시, 패치 적용 결과, 빌드와 설치, 실제 웹 요청이 연결되는 경로까지 확인하는 방식으로 진행했다.



3. 빌드에 앞서 기존 PHP에 임시 완화 적용

패치된 런타임을 준비하는 동안 기존 FPM의 설정을 먼저 조정했다. 원본 php.ini를 백업하고 다음 값을 적용했다.


memory_limit = 128M
post_max_size = 32M
upload_max_filesize = 30M
session.upload_progress.enabled = 0Copy



기존 POST 한도는 2048M, 업로드 한도는 1024M이었다. 이를 32M과 30M으로 낮추고 세션 기반 업로드 진행률 기능을 껐다. 주석 처리된 설정은 기본값이 적용될 수 있으므로, `session.upload_progress.enabled`는 주석을 해제해 명시적으로 0을 넣었다.


설정 검사 뒤 FPM을 재로딩하고 기존 워커가 교체됐는지 확인했다.


sudo /usr/sbin/php-fpm7.2 -t
sudo systemctl reload php7.2-fpm
sudo systemctl status php7.2-fpm --no-pager -lCopy



별도의 FPM 정보 조회에서도 `32M / 30M / Off`가 나오는 것을 확인했다. 이 조회 프로세스의 설정값과 실제 서비스 워커 교체는 각각 확인했다.


공식 권고의 OOM 완화에는 `memory_limit=-1`도 포함돼 있었지만, 약 2GB 메모리의 운영 서버에서는 128M을 유지했다. 따라서 이 단계는 일부 경로의 노출을 줄이는 부분 완화였다. 설정 조정만으로 두 취약점 대응이 끝났다고 판단하지 않았다. [공식 임시 완화 안내]



4. 생각보다 먼저 막힌 것은 디스크 공간이었다

처음 확인한 디스크 여유는 1.8GB였다. 소스 압축 파일이 약 10MB라고 해서 빌드까지 그 정도 공간만 필요한 것은 아니다. 압축 해제한 소스, 개발 패키지, 오브젝트 파일, 설치본이 함께 남는다.


먼저 같은 파일시스템 안에서 어디에 공간을 사용하는지 확인했다.


df -h / /var/www /opt
sudo du -xhd1 / 2>/dev/null | sort -h
sudo du -xhd1 /var 2>/dev/null | sort -hCopy



대부분은 웹 서비스 디렉터리에 있었고, 더 내려가 보니 게시판 첨부 PDF와 에디터 업로드 자료였다. 오래되거나 큰 파일이라는 이유만으로 정리할 수 있는 자료는 아니었다.


대신 설치된 오래된 커널 헤더 패키지를 확인했다. 실행 중인 커널과 보존할 패키지를 제외한 명시적인 목록을 만들고, `apt-get -s remove`로 제거 계획을 먼저 확인했다. 최종적으로 지정한 오래된 헤더 패키지 28개를 정리했다. 전체 `autoremove`를 실행하거나 `/usr/src`를 직접 지우지는 않았다.


시점루트 디스크 여유사용률
정리 전 조회1.8GB96%
헤더 정리 후 조회4.8GB88%



이 표는 두 시점의 `df` 결과다. 여유 차이 전체를 특정 패키지의 정확한 제거 용량으로 환산하지는 않았다. 웹 자료 삭제와 디스크 증설 없이 빌드 준비를 이어갈 수 있었다.



5. 기존 확장과 라이브러리를 먼저 대조

새 PHP를 만들기 전에 기존 FPM의 모듈 목록과 설정을 확인했다.


sudo /usr/sbin/php-fpm7.2 -m
sudo /usr/sbin/php-fpm7.2 -iCopy



주로 확인한 항목은 다음과 같다.


구분확인한 내용
DBmysqli, mysqlnd, PDO, pdo_mysql
문자열·문서mbstring, iconv, XML 계열, XSL
파일 처리fileinfo, zip, zlib
이미지GD, FreeType, JPEG, PNG, WebP, XPM
기타curl, OpenSSL, sodium, BCMath, readline, OPcache
세션저장 경로, serializer, GC 설정
기존 문법short_open_tag=On



GD는 외부 GD 라이브러리의 기능을 유지하도록 `--enable-gd --with-external-gd`를 사용했다. readline은 기존 정보에 `EditLine wrapper`가 표시돼 `libedit-dev`와 `--with-libedit=shared`를 선택했다.


개발 의존성으로 선택한 목록은 다음과 같다.


build-essential pkg-config
libxml2-dev libxslt1-dev libssl-dev libcurl4-openssl-dev
libonig-dev libsodium-dev libzip-dev zlib1g-dev
libgd-dev libedit-devCopy



설치 계획을 조회한 뒤 APT 시뮬레이션으로 신규·업그레이드·삭제 여부를 대조했다. 당시 계획은 신규 60개, 기존 패키지 업그레이드 0개, 삭제 0개였고, 예상 다운로드는 20.4MiB, 설치 후 순증가는 91.1MiB였다.


처음 만든 계획 조회 코드에서는 신규 패키지까지 upgrades 항목으로 분류되는 문제가 있었다. 실제 `apt-get -s` 결과로 교정했다. 계획 출력의 이름만 보고 기존 패키지 60개가 업그레이드된다고 해석하지 않은 이유다.


설치 뒤에는 `pkg-config`로 필요한 라이브러리의 탐지를 확인했다. 여기서는 libxml2 2.9.4, OpenSSL 1.1.1, libcurl 7.58.0, GD 2.2.5, libzip 1.1.2 등이 인식됐다.


기존 모듈 중 WDDX는 PHP 7.4 기본 배포에서 빠졌다. 관련 코드 검색에서는 호출이 나오지 않았고 세션 serializer도 `php`였다. 검색 범위 내 결과를 확인한 것이므로 모든 동적 호출까지 없음을 증명한 것으로 확대하지 않았다. [PHP 7.4에서 제외된 확장]



6. 공식 소스와 패치의 입력부터 고정

소스는 PHP 7.4.33 공식 배포 파일, 패치는 위의 고정 커밋을 사용했다. 이동할 수 있는 브랜치의 최신 상태 대신 특정 커밋을 기록했다.


(
set -euo pipefail
umask 077
mkdir -m 700 "$HOME/php74-security-build"
cd "$HOME/php74-security-build"

curl --proto '=https' --tlsv1.2 -fL --retry 2 \
  https://www.php.net/distributions/php-7.4.33.tar.xz \
  -o php-7.4.33.tar.xz

curl --proto '=https' --tlsv1.2 -fL --retry 2 \
  https://github.com/php/php-src/commit/712e60f9edfd03f0960fc4fc3c2c375f42b90aa2.patch \
  -o php2shell.patch

sha256sum -c <<'SHA256'
924846abf93bc613815c55dd3f5809377813ac62a9ec4eb3778675b82a27b927  php-7.4.33.tar.xz
5fe57ca6ee96d59b36c6e0ca842843c50884f177bef1e85234cf669b577f5e48  php2shell.patch
SHA256
)Copy



서버에서 두 파일 모두 `OK`를 확인한 뒤 압축을 풀고 패치를 적용했다.


(
set -euo pipefail
umask 077
cd "$HOME/php74-security-build"
mkdir php-7.4.33
tar -xJf php-7.4.33.tar.xz --strip-components=1 -C php-7.4.33
cd php-7.4.33

patch --batch --forward --fuzz=0 --dry-run -p1 < ../php2shell.patch
patch --batch --forward --fuzz=0 -p1 < ../php2shell.patch
)Copy



17개 파일에 패치가 적용됐고 offset이나 fuzz 없이 완료됐다. 별도의 로컬 소스 검토에서는 변경된 17개 파일의 적용 전후 Git blob 해시가 공식 패치의 index와 일치하는지도 대조했다.


여기까지는 입력 소스와 패치 적용에 대한 확인이다. 컴파일 성공이나 운영 웹 적용 여부는 다음 단계에서 확인했다.



7. 기존 PHP와 경로를 분리해 빌드


설치 prefix는 `/opt/php74-security-r1`로 분리했다. 아래 경로는 글 전체에서 사용하는 공개용 예시다.


/opt/php74-security-r1/
├── bin/php
├── sbin/php-fpm
├── etc/php.ini
├── etc/conf.d/99-native-security.ini
├── etc/php-fpm.conf
├── etc/php-fpm.d/www.conf
└── var/
    ├── run/php-fpm.sock
    └── log/php-fpm.logCopy



기존 `/usr/sbin/php-fpm7.2`와 `/etc/php/7.2`는 유지하고, 새 바이너리와 설정을 다른 경로에 설치했다. configure의 핵심 옵션은 다음과 같다.


cd "$HOME/php74-security-build/php-7.4.33"
unset EXTENSION_DIR PHPRC PHP_INI_SCAN_DIR

CFLAGS='-O2' CXXFLAGS='-O2' LC_ALL=C ./configure \
  --prefix=/opt/php74-security-r1 \
  --sysconfdir=/opt/php74-security-r1/etc \
  --with-config-file-path=/opt/php74-security-r1/etc \
  --with-config-file-scan-dir=/opt/php74-security-r1/etc/conf.d \
  --enable-cli --enable-fpm \
  --with-fpm-user=www-data --with-fpm-group=www-data \
  --disable-cgi --disable-phpdbg --disable-debug --without-pear \
  --without-sqlite3 --without-pdo-sqlite \
  --enable-opcache --enable-bcmath --enable-calendar \
  --with-curl --enable-exif --enable-ftp \
  --enable-gd --with-external-gd --with-gettext --enable-mbstring \
  --with-mysqli=mysqlnd --with-openssl --with-pdo-mysql=mysqlnd \
  --with-libedit=shared --enable-shmop --enable-sockets --with-sodium \
  --enable-sysvmsg --enable-sysvsem --enable-sysvshm \
  --with-xsl --with-zip --with-zlibCopy



이 옵션은 이번 서버의 기존 모듈 구성에 맞춰 선택한 값이다. 다른 서버에서는 실제 사용하는 확장 목록과 기능을 먼저 대조해야 한다.



8. fileinfo 컴파일 도중 메모리 부족으로 중단


운영 중인 작은 서버였으므로 컴파일은 한 작업씩 실행했다. 실제 빌드 명령은 시작 전에 MemAvailable이 1,310,720KiB(1.25GiB) 이상인지 확인하고 부족하면 중단하도록 했다. 다음 코드는 이 사전 확인을 통과한 뒤 적용한 빌드 조건을 발췌한 것이다.


(
  set -euo pipefail
  cd "$HOME/php74-security-build/php-7.4.33"
  unset PHPRC PHP_INI_SCAN_DIR EXTENSION_DIR MAKEFLAGS MFLAGS
  export LC_ALL=C
  ulimit -v 1048576
  nice -n 10 make -j1 2>&1 | tee ../build.log
)Copy



`ulimit -v 1048576`은 이 빌드 서브셸과 하위 프로세스의 프로세스별 가상 주소 공간 한도다. 전체 빌드 프로세스를 합친 RAM 사용량을 1GiB로 보장하는 장치는 아니다. PHP-FPM 운영 서비스의 제한이나 취약점 완화 설정으로 추가한 것도 아니다.


그런데 병렬 빌드를 하지 않아도 fileinfo의 다음 파일에서 컴파일이 멈췄다.


virtual memory exhausted: Cannot allocate memory
make: *** [ext/fileinfo/libmagic/apprentice.lo] Error 1Copy



이 메시지만으로 호스트의 물리 RAM이 모두 소진됐다거나 커널 OOM killer가 동작했다고 단정하지 않았다. 당시 다시 조회한 available 메모리는 약 1.5GiB였고, 컴파일 프로세스에는 위의 가상 주소 공간 상한이 걸려 있었다.


fileinfo를 빼거나 전체 소스를 다시 시작하는 대신, 실패한 오브젝트 하나에 GCC 가비지 컬렉션 관련 옵션을 추가해 재시도했다.


(
  set -euo pipefail
  cd "$HOME/php74-security-build/php-7.4.33"
  unset PHPRC PHP_INI_SCAN_DIR EXTENSION_DIR MAKEFLAGS MFLAGS
  export LC_ALL=C
  ulimit -v 1048576

  nice -n 10 make -j1 -f Makefile -f - \
    ext/fileinfo/libmagic/apprentice.lo <<'MAKE' 2>&1 | tee ../build-fileinfo-retry.log
ext/fileinfo/libmagic/apprentice.lo: override EXTRA_CFLAGS += --param ggc-min-expand=10 --param ggc-min-heapsize=32768
MAKE
)Copy



GCC의 내부 수거 동작을 조정하는 옵션이다. 기존 `-O2`와 가상 메모리 상한은 유지했고, 영구 Makefile도 수정하지 않았다. [GCC 최적화·파라미터 문서]


이 오브젝트가 완료된 뒤 같은 조건으로 전체 `make -j1`을 재개했고 최종 빌드가 성공했다. 메모리 한도를 풀거나 스왑을 새로 만들지 않고 이 단계의 실패를 해결했다.


빌드 도중 나온 `PEAR package PHP_Archive not installed`는 phar 생성 과정의 안내였다. 최종 빌드 성공과 실제 Phar 모듈 로딩 결과를 함께 보고 판단했다.



9. 설치 전 실행 확인과 독립 INI 구성

설치 전에 빌드 디렉터리의 CLI와 FPM 바이너리를 각각 실행했다. 기존 INI를 섞지 않도록 `-n`을 사용하고, 이번에 만든 OPcache와 readline만 절대 경로로 로드했다.


cd "$HOME/php74-security-build/php-7.4.33"
./sapi/fpm/php-fpm -n \
  -d "zend_extension=$PWD/modules/opcache.so" \
  -d "extension=$PWD/modules/readline.so" \
  -mCopy



버전과 모듈을 확인한 뒤 Makefile의 prefix, 바이너리·설정·확장 설치 경로가 모두 새 경로를 가리키는지 확인했다. 이후 `umask 022`와 make 관련 환경 변수를 정리한 조건에서 `make install`을 실행했다.


설치에는 `sudo /usr/bin/make -j1 install INSTALL_ROOT=`를 사용했다. `/usr/bin/php`의 링크나 시스템 기본 CLI 경로를 바꾸는 작업은 하지 않았다.


새 INI는 기존 main php.ini를 검토한 뒤 복사하고, `conf.d`는 새로 만들었다. 기존 main 파일에는 활성 extension 로딩 지시어가 없음을 먼저 확인했다. 다른 서버에서 이 방식을 적용한다면 main 파일의 로딩 지시어도 함께 점검해야 한다.


기존 PHP 7.2의 확장 경로는 `20170718`, 새 PHP 7.4의 확장 경로는 `20190902`였다. 옛 `conf.d`와 `.so`를 그대로 옮기지 않고 새 빌드에 맞는 모듈을 로드했다.


`etc/conf.d/99-native-security.ini`의 주요 내용은 다음과 같다.


extension_dir = "/opt/php74-security-r1/lib/php/extensions/no-debug-non-zts-20190902"
zend_extension = "/opt/php74-security-r1/lib/php/extensions/no-debug-non-zts-20190902/opcache.so"
extension = "/opt/php74-security-r1/lib/php/extensions/no-debug-non-zts-20190902/readline.so"

memory_limit = 128M
post_max_size = 32M
upload_max_filesize = 30M
short_open_tag = On
date.timezone = "Asia/Seoul"
opcache.enable = 1
session.upload_progress.enabled = 0

session.save_handler = files
session.save_path = "/var/lib/php/sessions"
session.serialize_handler = php
session.gc_probability = 1
session.gc_divisor = 1000
session.gc_maxlifetime = 2400Copy



세션 저장 경로뿐 아니라 serializer와 GC 설정도 기존값을 유지했다. 기존 세션 파일을 직접 수정하거나 정리하는 작업은 하지 않았다.



10. 새 FPM 서비스를 기존 FPM과 나란히 기동

새 PHP를 설치한 것만으로 Nginx가 자동으로 새 FPM을 사용하지는 않는다. 새 pid, 로그, Unix 소켓을 가진 FPM 설정과 systemd unit을 따로 준비했다.


먼저 `etc/php-fpm.conf`는 다음과 같이 구성했다.


[global]
pid = /opt/php74-security-r1/var/run/php-fpm.pid
error_log = /opt/php74-security-r1/var/log/php-fpm.log
include = /opt/php74-security-r1/etc/php-fpm.d/*.confCopy



pool 설정인 `etc/php-fpm.d/www.conf`에는 기존 사용자와 프로세스 수 설정을 유지했다.


[www]
user = www-data
group = www-data

listen = /opt/php74-security-r1/var/run/php-fpm.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660

pm = dynamic
pm.max_children = 10
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3Copy



설정·로그·런타임 디렉터리는 root 소유로 준비하고, Nginx 워커가 접근할 Unix 소켓에는 위의 소유자·그룹과 0660 모드를 적용했다.


`/etc/systemd/system/php74-security-fpm.service`는 다음과 같이 구성했다.


[Unit]
Description=PHP 7.4 FPM with PHP2Shell security backport
After=network.target

[Service]
Type=simple
Environment="PHPRC=/opt/php74-security-r1/etc"
Environment="PHP_INI_SCAN_DIR=/opt/php74-security-r1/etc/conf.d"
ExecStart=/opt/php74-security-r1/sbin/php-fpm --nodaemonize --fpm-config /opt/php74-security-r1/etc/php-fpm.conf
ExecReload=/bin/kill -USR2 $MAINPID
KillSignal=SIGQUIT
KillMode=mixed
Restart=on-failure
RestartSec=5s

[Install]
WantedBy=multi-user.targetCopy



서비스 타입은 `simple`로 하고 FPM을 foreground로 실행했다. 어떤 INI를 읽는지 달라지지 않도록 두 환경변수도 명시했다. unit 파일 안의 `$MAINPID`는 그대로 저장했다.


설정 검사도 같은 INI 환경에서 수행한 뒤 새 FPM을 시작했다.


sudo env \
  PHPRC=/opt/php74-security-r1/etc \
  PHP_INI_SCAN_DIR=/opt/php74-security-r1/etc/conf.d \
  /opt/php74-security-r1/sbin/php-fpm \
  -t -y /opt/php74-security-r1/etc/php-fpm.conf

sudo systemctl daemon-reload
sudo systemctl start php74-security-fpm.serviceCopy



서비스가 active인 것과 실제 소켓이 생긴 것을 모두 확인했다. 소켓의 소유자·그룹은 `www-data:www-data`, 모드는 660이었다. 이어 `www-data` 권한으로 Unix 소켓에 연결되는지도 확인했다.


이 연결 시험은 소켓 접근 권한 확인이다. 이때 애플리케이션 FastCGI 요청까지 실행한 것은 아니며, 실제 웹 요청 확인은 Nginx 전환 뒤 진행했다.



11. Nginx의 연결 세 곳만 새 소켓으로 전환

Nginx 설정을 확인해 보니 서로 다른 사이트의 PHP handler 세 곳이 같은 기존 FPM 소켓을 사용하고 있었다.


fastcgi_pass unix:/var/run/php/php7.2-fpm.sock;Copy



`sites-enabled/default`는 `sites-available/default`를 가리키는 심볼릭링크였다. 실제 수정 대상은 링크가 가리키는 원본 파일로 정했다.


바로 원본을 바꾸는 대신 먼저 백업과 후보 파일을 만들었다. 두 보관 파일은 root 소유 0600으로 두고, diff에서 다음 세 줄의 변경만 있는지 확인했다.


- fastcgi_pass unix:/var/run/php/php7.2-fpm.sock;
+ fastcgi_pass unix:/opt/php74-security-r1/var/run/php-fpm.sock;Copy



전환 순서는 다음과 같았다.


1) 현재 Nginx 원본이 준비 당시 백업과 같은지, 임시 교체 경로가 비어 있는지 확인

2) 새 FPM의 active 상태와 새 소켓 존재 확인

3) 새 FPM의 자동 시작 활성화

4) 같은 디렉터리의 임시 파일을 거쳐 후보 설정으로 원본 교체

5) Nginx 설정 검사

6) 검사 성공 시 reload

7) 실제 새 워커와 사이트 응답 확인



설정 파일 교체는 같은 디렉터리에서 rename하는 방식으로 처리하고, 기존 enabled 심볼릭링크를 유지했다. 원자적으로 교체한 대상은 설정 파일이다. 전체 서비스 전환의 무중단을 보장했다는 뜻으로 확대하지는 않았다.


후보 적용 후 검사나 reload가 실패하면 백업을 복구하도록 준비했다. 실제 이번 전환은 검사와 reload가 정상 진행돼 실패 복구 분기는 실행되지 않았다.


sudo nginx -t
sudo systemctl reload nginx
sudo systemctl status nginx php74-security-fpm --no-pager -l
ps -C nginx -o pid,ppid,lstart,argsCopy



Nginx master PID는 그대로였고 새 worker가 reload 시각에 생성됐다. 조회한 프로세스 목록에는 종료를 기다리는 옛 worker가 남아 있지 않았다. 사이트 접속도 정상으로 확인했다.



12. 전환 확인 뒤 기존 PHP 7.2 FPM 종료

새 Nginx worker와 새 FPM 상태를 확인하고 사이트 접속 정상 보고를 받은 다음, 기존 PHP 7.2 FPM의 실행과 자동 시작을 해제했다.


sudo systemctl disable --now php7.2-fpm.service

sudo systemctl show php7.2-fpm.service php74-security-fpm.service \
  -p Id -p ActiveState -p SubState -p UnitFileState -p MainPIDCopy


서비스최종 상태
기존 php7.2-fpm.serviceinactive / dead / disabled / MainPID=0
새 php74-security-fpm.serviceactive / running / enabled



기존 소켓이 사라지고 새 소켓이 유지되는 것도 확인했다. 기존 PHP 패키지와 설정은 원복을 위해 보관했다.


시스템 기본 `php` 명령의 경로는 바꾸지 않았고 그 명령의 현재 버전을 별도 조회하지도 않았다. 이번에 확인한 전환 범위는 Nginx가 연결하는 웹 FPM이다. 서비스의 enabled 상태를 확인했으며 재부팅 시험은 수행하지 않았다.



13. 후속 점검으로 실제 웹 phpinfo까지 대조

설치본 CLI와 FPM 정보 조회에서 새 설정을 확인한 데 이어, 기존 PHP 7.2 FPM의 종료까지 마친 뒤 후속 점검으로 실제 웹 phpinfo를 대조했다.


최종 웹 요청의 설정값은 다음과 같았다. 두 값이 있는 항목은 Local Value와 Master Value가 일치했다.


항목웹 응답 확인값
PHP Version7.4.33
Server APIFPM/FastCGI
Loaded Configuration File새 prefix의 etc/php.ini
Additional .ini files parsed새 prefix의 99-native-security.ini
extension_dir새 prefix의 20190902 경로
memory_limit128M
post_max_size32M
upload_max_filesize30M
session.upload_progress.enabledOff
short_open_tagOn
date.timezoneAsia/Seoul
OPcacheUp and Running, Startup OK
display_errors / display_startup_errorsOff / Off
expose_phpOff
log_errorsOn



FPM 모듈은 47개로 설치 전 확인한 새 빌드의 목록과 일치했다. 기존 WDDX를 제외한 구성이며, GD의 FreeType·JPEG·PNG·XPM·WebP와 DB·문자열·파일 관련 주요 확장도 표시됐다.


phpinfo에서는 `Scan this dir for additional .ini files`가 `no value`로 보이는 항목도 있었다. 이를 이유로 INI가 전혀 적용되지 않았다고 판단하지 않았다. 실제 parsed 파일 목록에는 새 99 INI가 있었고, 해당 설정과 shared 모듈도 로드돼 있었다. 로컬 소스 검토에서는 환경변수 경로 포인터와 FPM 프로세스 제목 변경에 따른 메모리 재사용으로 이런 표시가 가능함을 확인했다. 이번 프로세스의 메모리를 추적해 원인을 확정한 결과는 아니다.


웹 phpinfo는 새 런타임과 설정으로 요청이 처리되는지 확인하는 자료다. 앞서 적용한 보안 패치 내용을 버전 표시 하나로 증명하는 자료로 사용하지는 않았다.


진단용 phpinfo 페이지는 서버 내부 경로와 설정을 공개하므로 확인 후 제거해야 한다. 이 작업 기록에는 페이지 삭제 완료 결과까지 포함돼 있지는 않다.



14. 후속으로 Nginx 버전 노출 숨기기

PHP 전환을 마친 뒤 Nginx 버전 번호 노출도 줄였다. `/etc/nginx/nginx.conf`를 백업하도록 한 다음 기존 `http` 블록 안에 다음 한 줄을 적용했다.


server_tokens off;Copy



이 옵션은 Server 응답 헤더와 기본 오류 페이지에서 Nginx 버전 번호 노출을 제어한다. 제품명 `nginx`는 남는다. [Nginx server_tokens 문서]


저장 후에는 검사 성공 시에만 reload하도록 실행했다.


sudo nginx -t && sudo systemctl reload nginxCopy



실제 외부 HTTP 요청의 결과도 확인했다.


확인 대상상태Server 헤더
사이트 A의 기본 주소200nginx
사이트 B의 기본 주소200nginx
사이트 C의 기본 주소301nginx
사이트 A의 존재하지 않는 PHP 경로404nginx



확인한 404 본문에도 버전 번호가 없었다. 세 기본 응답에 X-Powered-By 헤더도 없었다. 사이트 C의 301 이후 리다이렉트를 따라간 검사는 아니므로 HTTPS 응답 전체까지 확인했다고 쓰지는 않았다.


버전 숨김은 정보 노출을 줄이는 후속 설정으로 다뤘다. PHP 코어의 보안 패치 적용 여부는 앞선 소스와 런타임 전환 근거로 확인했다.



15. 원복은 기존 FPM을 먼저 살리는 순서로 준비

실제로 원복을 실행하지는 않았지만, 장애가 생길 때 사용할 순서도 남겼다. 네이티브 환경에서는 설정 백업을 되돌리는 것과 FPM 프로세스를 준비하는 일이 함께 필요했다.


1) 현재 Nginx 설정이 전환 당시 저장한 후보와 같은지 확인한다. 이후 수정이 있으면 덮어쓰지 않는다.

2) 기존 PHP 7.2 FPM을 enable/start한다.

3) 기존 Unix 소켓이 준비되고 서비스가 active인지 확인한다.

4) Nginx의 기존 fastcgi_pass가 포함된 백업을 복원한다.

5) 설정 검사 후 reload한다.

6) 설정 검사가 실패하면 새 후보 설정을 복구하고 재검사한 뒤 중단한다. 이 분기에서는 reload하지 않는다. reload 명령이 실패한 경우에는 새 후보 복구·재검사 후 reload한다.



새 FPM은 이 원복 절차에서 자동으로 중지하지 않도록 했다. 원인 분석이나 재전환 때 사용할 수 있기 때문이다. 원복하면 패치 이전 PHP 7.2가 웹 요청을 처리하므로 장애 대응용 임시 복구로 취급해야 한다.


보관한 항목은 기존 PHP 패키지·INI, Nginx 전환 전 백업과 새 후보, 공식 원본·패치, 빌드 로그였다. 웹 코드나 DB를 원복 대상으로 덮어쓰는 절차는 넣지 않았다.



16. Docker 환경과 달랐던 운영 지점

항목앞선 Docker 서버이번 Nginx + PHP-FPM 서버
배포 단위보안 PHP 이미지와 컨테이너별도 prefix의 바이너리·INI·FPM unit
PHP 연결컨테이너 네트워크의 FastCGI 주소호스트의 Unix 소켓
서비스 관리Compose와 컨테이너 기동systemd와 FPM pool
설정 격리이미지와 마운트 경로설치 prefix, 확장 경로, INI 환경변수
전환 방법공용 PHP 컨테이너 교체새 FPM 기동 후 Nginx 소켓 연결 변경
원복 방법이전 이미지·설정으로 재생성기존 FPM 준비 후 Nginx 백업 복구


이번에는 기존 FPM과 새 FPM을 잠시 함께 띄울 수 있었기 때문에 새 소켓을 준비한 뒤 연결을 전환하는 흐름을 사용했다. 반대로 소스 빌드 의존성, 설치 권한, 서비스 자동 시작과 로그 경로는 호스트에서 직접 관리해야 했다.



17. 확인한 결과와 남겨 둔 범위

최종적으로 공식 백포트를 적용한 PHP 7.4.33의 빌드·설치가 완료됐고, Nginx의 PHP 연결 세 곳을 새 FPM으로 전환했다. 웹 요청에서 목표 INI와 모듈, OPcache 상태를 확인했으며 기존 FPM은 중지하고 자동 시작을 해제했다. 후속 Nginx 버전 번호 숨김도 실제 HTTP 응답으로 확인했다.


각 확인 자료가 의미하는 범위는 구분했다.


자료확인한 범위
로컬 원본·패치 대조입력 해시와 소스 변경 내용
서버 터미널 실행 결과패치 적용, 빌드, 설치, FPM·Nginx 전환과 서비스 상태
실제 웹 phpinfo해당 요청의 PHP·INI·모듈 실효값
사이트 접속 확인전환 후 기본 사이트 접근 정상
외부 HTTP 조회조회한 응답의 Server 헤더와 404 본문



전체 `make test`, 별도 공격 코드 재현, 모든 로그인·업로드·업무 기능의 회귀 시험, 재부팅과 실제 원복 시험은 이 기록의 완료 항목에 포함하지 않았다.


이번 작업에서 가장 오래 걸린 부분은 버전 문자열을 바꾸는 일이 아니라, 기존 서비스가 사용하던 설정과 확장을 확인하고 새 FPM으로 연결되는 경로를 단계마다 확인하는 과정이었다. 작은 서버의 디스크와 컴파일 메모리 제약도 실제 변수였다. 입력 소스부터 실행 프로세스, 마지막 HTTP 응답까지 근거를 이어 두니 전환 결과와 문제가 생겼을 때 돌아갈 지점을 함께 남길 수 있었다.


#php #문제해결