
PHP는 2026년 10월 21일 원격 코드 실행 취약점 CVE-2026-109618과 CVE-2026-109619를 수정한 PHP 8.3.36을 비롯해 8.2.35, 8.4.27, 8.5.12를 공개했다. Ubuntu 20.04에서는 PHP 8 패키지를 제공하던 Ondřej Surý의 PPA가 더 이상 빌드되지 않아 apt upgrade로 이 버전을 설치할 수 없다. Vincent Schmalbach는 운영 중인 서버의 nginx는 그대로 두고 Docker 컨테이너에서 PHP-FPM만 교체하는 방법을 소개했다.
Ubuntu 20.04의 공식 저장소는 PHP 7.4만 제공한다. PHP 8을 설치한 서버는 대개 ppa:ondrej/php를 이용했지만, 이 PPA에서 Ubuntu 20.04용 PHP 8.3의 마지막 빌드는 2025년 5월에 나온 8.3.21이며 현재 삭제된 상태다. Ubuntu 20.04의 표준 보안 유지보수도 2025년 5월에 끝났다. Ubuntu Pro는 Ubuntu 자체 패키지를 2030년 5월까지 패치하지만 별도 저장소인 PPA의 PHP 8에는 적용되지 않는다. Ubuntu 18.04에도 수정된 빌드가 없다.
Schmalbach가 관리하는 두 Laravel 서버는 PHP 8.3.12와 8.3.10을 사용하고 있었다. 그는 보안 사고가 진행 중인 상황에서 운영 서버의 Ubuntu를 즉시 업그레이드하는 대신, Docker 공식 PHP 이미지로 PHP-FPM을 실행하고 nginx가 그쪽으로 요청을 보내도록 했다. 두 취약점에 관한 앞선 글도 별도로 소개했다.
nginx는 PHP를 직접 실행하지 않고 FastCGI 프로토콜로 PHP-FPM에 요청을 전달한다. 따라서 사이트 설정의 fastcgi_pass가 가리키는 주소를 바꾸면 PHP-FPM만 교체할 수 있다. Docker의 공식 php 이미지에는 10월 21일 저녁(UTC) 기준으로 수정 버전 태그가 공개돼 있었다. 컨테이너는 자체 Debian 라이브러리를 사용하고, 호스트는 nginx와 TLS 인증서, 데이터베이스, Redis, 사이트 파일을 계속 맡는다. 사이트 파일은 바인드 마운트로 컨테이너 안에서 읽는다. Ubuntu 20.04에서는 apt install docker.io로 Ubuntu 저장소의 Docker를 설치할 수 있다.
사이트가 사용하는 PHP 확장 모듈은 호스트에서 php8.3 -m으로 확인한다. 공식 이미지에 빠진 확장은 mlocati의 install-php-extensions 2.12.3로 설치할 수 있다. 컨테이너 안의 사용자는 호스트에서 사이트 파일을 소유한 사용자와 같은 숫자 UID와 GID를 사용해야 한다. Schmalbach의 Laravel Forge 서버에서는 forge의 ID가 1000이었고, www-data 그룹도 추가해야 했다. Laravel의 storage 디렉터리가 www-data 그룹 소유에 권한 775로 설정돼 있었기 때문이다. 이 그룹을 빼먹으면 페이지가 500 오류를 내고 로그에 "Permission denied"가 기록됐다.
이미지의 기본 PHP-FPM 설정 대신 pool.conf를 직접 지정할 때는 daemonize = no를 포함해야 한다. 기본 zz-docker.conf를 제거하면 이 설정도 함께 없어지기 때문이다. 예시에서 사용한 포트 9836은 임의의 포트이며, 기존 호스트 풀 설정의 pm 값을 복사해 작업자 수를 맞추도록 안내한다. 컨테이너 실행 시 호스트 네트워크를 사용하면 PHP-FPM은 호스트의 127.0.0.1에서 수신하고, nginx는 연결할 수 있지만 인터넷에서는 접근할 수 없다. 사이트 디렉터리는 호스트와 같은 경로에 마운트하고, 호스트의 php.ini도 읽기 전용으로 연결해 메모리 한도와 업로드 크기 같은 설정을 유지한다.
PHP의 파일 세션을 사용하는 앱은 세션 디렉터리도 마운트해야 한다. Ubuntu는 세션을 /var/lib/php/sessions에 저장하지만 공식 이미지는 컨테이너 안의 /tmp를 사용하므로, 마운트한 경로로 session.save_path를 설정하지 않으면 전환 뒤 사용자가 로그아웃될 수 있다. nginx 설정을 바꾸기 전에는 컨테이너의 PHP 버전과 모듈을 확인하고 실제 요청을 보내 점검한다. cgi-fcgi는 libfcgi-bin 패키지에 들어 있다.
사이트의 nginx 설정에서 PHP 위치 블록의 fastcgi_pass를 컨테이너 주소로 바꾼 뒤 nginx -t && systemctl reload nginx를 실행한다. 되돌릴 때는 기존 소켓 경로로 다시 바꾸면 된다. Laravel Forge로 관리하는 서버라면 파일을 직접 고치지 말고 Forge의 nginx 편집기를 사용해야 한다. Forge에서 사이트의 PHP 버전을 바꾸면 fastcgi_pass가 이전 소켓으로 돌아갈 수도 있다.
Schmalbach는 컨테이너를 거치면서 페이지당 약 5~10ms가 추가됐다고 보고했다. 직접 FastCGI 요청으로 측정한 세 페이지의 응답 시간은 호스트 PHP-FPM 소켓에서 109, 102, 98ms였고 컨테이너에서는 113, 111, 111ms였다.
이 방식은 웹 요청에 사용하는 PHP만 바꾼다. 호스트의 명령줄 PHP는 그대로이므로 큐 작업자, cron 스케줄러, php artisan으로 실행하는 작업은 이전 버전을 계속 사용한다. 두 취약점은 네트워크 요청을 통해 공격하는 유형이므로 Schmalbach는 큐 작업자와 cron 작업의 노출이 훨씬 낮다고 봤지만, 이 작업들도 늦어도 2025년 5월부터 보안 수정을 받지 못한 PHP를 실행한다. 함께 컨테이너에서 실행하려면 docker exec -u forge php-fixed php artisan ...을 사용할 수 있다.
컨테이너는 Ubuntu 20.04 자체의 보안 지원을 복구하지 않는다. Schmalbach는 두 서버를 다음 날 업그레이드했다. 각 서버의 스냅샷을 만든 뒤 do-release-upgrade로 20.04에서 22.04, 이어서 24.04로 올리고, 호스트에 PPA의 PHP 8.3.36을 설치한 다음 nginx를 기존 소켓으로 되돌리고 컨테이너를 중지했다. 사이트는 재부팅 두 차례 동안 각각 약 1분간 접속할 수 없었다.
업그레이드 과정에서는 커널 업데이트 뒤 재부팅하지 않은 서버가 중단될 수 있고, Hetzner Cloud 이미지의 미러 설정이 업그레이드 도구에서 인식되지 않아 패키지를 찾지 못할 수 있다. Schmalbach는 시작 전에 미러를 archive.ubuntu.com과 security.ubuntu.com으로 바꾸라고 안내했다. 업그레이드를 막는 보류 패키지를 먼저 해결해야 하며, Oracle 저장소의 MySQL을 Ubuntu 패키지로 교체한 사례도 있었다. Redis나 Node.js 같은 서드파티 저장소는 업그레이드 중 비활성화되므로 새 릴리스용으로 다시 추가해야 한다. Ondřej의 nginx PPA는 Launchpad에서 사라졌고, 그곳에서 설치한 nginx는 OpenSSL 1.1에 연결된 채 남아 Ubuntu 패키지로 교체했다. Laravel Horizon 같은 장시간 실행 PHP 프로세스도 재시작 전까지 이전 PHP 바이너리를 메모리에 유지한다.
댓글 (0)
로그인하면 이 기사에 내 생각을 남길 수 있어요