보안 · 10 min read · Jan 22, 2026
공유 호스팅 환경에서의 PHP-FPM/Nginx 보안 (Debian/Ubuntu)
공유 호스팅 환경에서의 PHP-FPM/Nginx 보안 (Debian/Ubuntu)
버전 1.0
저자: Falko Timme
Twitter에서 나를 팔로우하세요
nginx와 PHP-FPM을 공유 호스팅 환경에서 사용하고자 한다면 보안에 대해 고민해야 합니다. Apache/PHP 환경에서는 suExec 및/또는 suPHP를 사용하여 PHP가 www-data와 같은 시스템 사용자 대신 개별 사용자 계정으로 실행되도록 할 수 있습니다. PHP-FPM에는 그런 것이 없지만, 다행히도 PHP-FPM은 각 웹사이트에 대해 PHP 스크립트를 해당 풀에 정의된 사용자/그룹으로 실행할 수 있는 “풀“을 설정할 수 있게 해줍니다. 이는 suPHP의 모든 이점을 제공하며, 추가로 PHP 스크립트가 풀에 정의된 사용자/그룹으로 실행되기 위해 특정 사용자/그룹의 소유일 필요가 없기 때문에 FTP 또는 SCP 전송 문제도 없습니다.
이것이 당신에게 작동할 것이라는 보장은 하지 않습니다!
1. 사전 참고
여기서는 www.example.com / example.com이라는 vhost를 사용하며, 문서 루트는 /var/www/www.example.com/web입니다.
작동하는 LEMP 설치가 있어야 하며, 이는 다음 튜토리얼에서 보여줍니다:
- Debian Squeeze에서 PHP5 및 MySQL 지원과 함께 Nginx 설치하기
- Ubuntu 11.04에서 PHP5 (및 PHP-FPM) 및 MySQL 지원과 함께 Nginx 설치하기
우분투 사용자에 대한 주의:
이 튜토리얼의 모든 단계를 루트 권한으로 실행해야 하므로, 이 튜토리얼의 모든 명령 앞에 sudo 문자열을 추가하거나, 지금 바로 다음을 입력하여 루트가 될 수 있습니다:
sudo su2. 지금까지의 내용
Debian/Ubuntu에서 PHP-FPM의 풀 디렉토리는 /etc/php5/fpm/pool.d/입니다. 여기서 새로운 풀들이 생성됩니다. PHP-FPM에서 사용되는 php.ini는 /etc/php5/fpm/php.ini입니다. 이미 하나의 풀, www.conf가 있습니다. 이를 살펴보겠습니다:
vi /etc/php5/fpm/pool.d/www.conf| ; 'www'라는 이름의 새 풀을 시작합니다. ; 변수 $pool은 모든 지시문에서 사용될 수 있으며, ; 풀 이름('www' 여기)을 대체합니다. [www] ; 풀당 접두사 ; 다음 지시문에만 적용됩니다: ; - 'slowlog' ; - 'listen' (unixsocket) ; - 'chroot' ; - 'chdir' ; - 'php_values' ; - 'php_admin_values' ; 설정되지 않으면 전역 접두사(또는 /usr)가 대신 적용됩니다. ; 주의: 이 지시문은 전역 접두사에 상대적일 수도 있습니다. ; 기본값: 없음 ;prefix = /path/to/pools/$pool ; FastCGI 요청을 수신할 주소입니다. ; 유효한 구문은 다음과 같습니다: ; 'ip.add.re.ss:port' - 특정 주소의 TCP 소켓에서 특정 포트로 수신; ; 'port' - 모든 주소의 TCP 소켓에서 특정 포트로 수신; ; '/path/to/unix/socket' - 유닉스 소켓에서 수신합니다. ; 주의: 이 값은 필수입니다. listen = 127.0.0.1:9000 ; listen(2) 백로그를 설정합니다. '-1' 값은 무제한을 의미합니다. ; 기본값: 128 (-1은 FreeBSD 및 OpenBSD에서) ;listen.backlog = -1 ; 연결을 허용하는 FastCGI 클라이언트의 ipv4 주소 목록입니다. ; 원래 PHP FCGI (5.2.2+)의 FCGI_WEB_SERVER_ADDRS 환경 변수와 동등합니다. ; TCP 수신 소켓에서만 의미가 있습니다. 각 주소는 ; 쉼표로 구분되어야 합니다. 이 값을 비워두면, ; 모든 IP 주소에서 연결이 허용됩니다. ; 기본값: any ;listen.allowed_clients = 127.0.0.1 ; 유닉스 소켓을 사용하는 경우 권한을 설정합니다. 리눅스에서는 읽기/쓰기 ; 권한이 설정되어야 웹 서버에서의 연결을 허용합니다. 많은 ; BSD 파생 시스템은 권한에 관계없이 연결을 허용합니다. ; 기본값: 사용자 및 그룹은 실행 중인 사용자로 설정됩니다. ; 모드는 0666으로 설정됩니다. ;listen.owner = www-data ;listen.group = www-data ;listen.mode = 0666 ; 프로세스의 유닉스 사용자/그룹 ; 주의: 사용자는 필수입니다. 그룹이 설정되지 않으면 기본 사용자의 그룹이 ; 사용됩니다. user = www-data group = www-data ; 프로세스 관리자가 자식 프로세스의 수를 제어하는 방법을 선택합니다. ; 가능한 값: ; static - 고정된 수 (pm.max_children)의 자식 프로세스; ; dynamic - 자식 프로세스의 수는 다음 지시문에 따라 동적으로 설정됩니다: ; pm.max_children - 동시에 살아 있을 수 있는 최대 자식 수. ; pm.start_servers - 시작 시 생성되는 자식 수. ; pm.min_spare_servers - '유휴' 상태의 최소 자식 수 ; (처리 대기 중). '유휴' 프로세스의 수가 ; 이 수보다 적으면 일부 자식이 생성됩니다. ; pm.max_spare_servers - '유휴' 상태의 최대 자식 수 ; (처리 대기 중). '유휴' 프로세스의 수가 ; 이 수보다 많으면 일부 자식이 종료됩니다. ; 주의: 이 값은 필수입니다. pm = dynamic ; pm이 'static'으로 설정될 때 생성될 자식 프로세스의 수. ; pm이 'dynamic'으로 설정될 때 생성될 최대 자식 프로세스의 수. ; 이 값은 동시에 제공될 요청 수의 한계를 설정합니다. ; ApacheMaxClients 지시문과 동등합니다. ; 원래 PHP CGI의 PHP_FCGI_CHILDREN 환경 변수와 동등합니다. ; 주의: pm이 'static' 또는 'dynamic'으로 설정될 때 사용됩니다. ; 주의: 이 값은 필수입니다. pm.max_children = 50 ; 시작 시 생성되는 자식 프로세스의 수. ; 주의: pm이 'dynamic'으로 설정될 때만 사용됩니다. ; 기본값: min_spare_servers + (max_spare_servers - min_spare_servers) / 2 ;pm.start_servers = 20 ; 원하는 최소 유휴 서버 프로세스 수. ; 주의: pm이 'dynamic'으로 설정될 때만 사용됩니다. ; 주의: pm이 'dynamic'으로 설정될 때 필수입니다. pm.min_spare_servers = 5 ; 원하는 최대 유휴 서버 프로세스 수. ; 주의: pm이 'dynamic'으로 설정될 때만 사용됩니다. ; 주의: pm이 'dynamic'으로 설정될 때 필수입니다. pm.max_spare_servers = 35 ; 각 자식 프로세스가 재생성되기 전에 실행해야 하는 요청 수. ; 이는 3자 라이브러리의 메모리 누수를 해결하는 데 유용할 수 있습니다. ; 무한 요청 처리를 위해 '0'을 지정합니다. PHP_FCGI_MAX_REQUESTS와 동등합니다. ; 기본값: 0 ;pm.max_requests = 500 ; FPM 상태 페이지를 보기 위한 URI입니다. 이 값이 설정되지 않으면, ; 상태 페이지로 인식되는 URI가 없습니다. 기본적으로 상태 페이지는 다음과 같은 정보를 표시합니다: ; accepted conn - 풀에 의해 수락된 요청 수; ; pool - 풀의 이름; ; process manager - static 또는 dynamic; ; idle processes - 유휴 프로세스의 수; ; active processes - 활성 프로세스의 수; ; total processes - 유휴 + 활성 프로세스의 수. ; max children reached - 프로세스 제한에 도달한 횟수, ; pm이 더 많은 자식을 시작하려고 할 때 (pm 'dynamic'에 대해서만 작동) ; '유휴 프로세스', '활성 프로세스' 및 '총 프로세스'의 값은 ; 매초 업데이트됩니다. '수락된 연결'의 값은 실시간으로 업데이트됩니다. ; 예시 출력: ; accepted conn: 12073 ; pool: www ; process manager: static ; idle processes: 35 ; active processes: 65 ; total processes: 100 ; max children reached: 1 ; 기본적으로 상태 페이지 출력은 text/plain 형식으로 포맷됩니다. 쿼리 문자열로 'html' 또는 'json'을 전달하면 해당 출력 ; 구문이 반환됩니다. 예시: ; http://www.foo.bar/status ; http://www.foo.bar/status?json ; http://www.foo.bar/status?html ; 주의: 값은 선행 슬래시(/)로 시작해야 합니다. 값은 ; 무엇이든 될 수 있지만, .php 확장을 사용하는 것은 좋지 않을 수 있으며, ; 실제 PHP 파일과 충돌할 수 있습니다. ; 기본값: 설정되지 않음 ;pm.status_path = /status ; FPM의 모니터링 페이지를 호출하기 위한 핑 URI입니다. 이 값이 설정되지 않으면, ; 핑 페이지로 인식되는 URI가 없습니다. 이는 외부에서 ; FPM이 살아 있고 응답하는지 테스트하는 데 사용될 수 있습니다. ; - FPM 가용성 그래프 생성 (rrd 등); ; - 응답하지 않는 서버를 그룹에서 제거 (로드 밸런싱); ; - 운영 팀에 대한 경고 트리거 (24/7). ; 주의: 값은 선행 슬래시(/)로 시작해야 합니다. 값은 ; 무엇이든 될 수 있지만, .php 확장을 사용하는 것은 좋지 않을 수 있으며, ; 실제 PHP 파일과 충돌할 수 있습니다. ; 기본값: 설정되지 않음 ;ping.path = /ping ; 이 지시문은 핑 요청의 응답을 사용자 정의하는 데 사용될 수 있습니다. ; 응답은 text/plain 형식으로 200 응답 코드와 함께 포맷됩니다. ; 기본값: pong ;ping.response = pong ; 단일 요청을 제공하는 타임아웃으로, 이 후에 작업 프로세스가 ; 종료됩니다. 이 옵션은 'max_execution_time' ini 옵션이 ; 어떤 이유로 스크립트 실행을 중지하지 않을 때 사용해야 합니다. '0' 값은 'off'를 의미합니다. ; 사용 가능한 단위: s(초)(기본값), m(분), h(시간), 또는 d(일) ; 기본값: 0 ;request_terminate_timeout = 0 ; 단일 요청을 제공하는 타임아웃으로, 이 후에 PHP 백트레이스가 ; 'slowlog' 파일에 덤프됩니다. '0s' 값은 'off'를 의미합니다. ; 사용 가능한 단위: s(초)(기본값), m(분), h(시간), 또는 d(일) ; 기본값: 0 ;request_slowlog_timeout = 0 ; 느린 요청에 대한 로그 파일 ; 기본값: 설정되지 않음 ; 주의: request_slowlog_timeout이 설정된 경우 slowlog는 필수입니다. ;slowlog = log/$pool.log.slow ; 열린 파일 설명자 rlimit을 설정합니다. ; 기본값: 시스템 정의 값 ;rlimit_files = 1024 ; 최대 코어 크기 rlimit을 설정합니다. ; 가능한 값: 'unlimited' 또는 0보다 크거나 같은 정수 ; 기본값: 시스템 정의 값 ;rlimit_core = 0 ; 시작 시 이 디렉토리로 chroot합니다. 이 값은 ; 절대 경로로 정의되어야 합니다. 이 값이 설정되지 않으면, ; chroot는 사용되지 않습니다. ; 주의: '$prefix'로 접두사를 붙여 풀 접두사 또는 그 하위 디렉토리로 chroot할 수 있습니다. ; 풀 접두사가 설정되지 않으면 전역 접두사가 대신 사용됩니다. ; 주의: chroot는 훌륭한 보안 기능이며 가능한 한 사용해야 합니다. ; 그러나 모든 PHP 경로는 chroot에 상대적입니다 ; (error_log, sessions.save_path, ...). ; 기본값: 설정되지 않음 ;chroot = ; 시작 시 이 디렉토리로 chdir합니다. ; 주의: 상대 경로를 사용할 수 있습니다. ; 기본값: 현재 디렉토리 또는 chroot 시 / chdir = / ; 작업자의 stdout 및 stderr를 기본 오류 로그로 리디렉션합니다. 설정되지 않으면, ; stdout 및 stderr는 FastCGI 사양에 따라 /dev/null로 리디렉션됩니다. ; 주의: 고부하 환경에서는 페이지 처리 시간에 지연이 발생할 수 있습니다 ; (수 밀리초). ; 기본값: 아니오 ;catch_workers_output = yes ; LD_LIBRARY_PATH와 같은 환경 변수를 전달합니다. 모든 $VARIABLE은 ; 현재 환경에서 가져옵니다. ; 기본값: 깨끗한 환경 ;env[HOSTNAME] = $HOSTNAME ;env[PATH] = /usr/local/bin:/usr/bin:/bin ;env[TMP] = /tmp ;env[TMPDIR] = /tmp ;env[TEMP] = /tmp ; 이 풀의 작업자에 특정한 추가 php.ini 정의입니다. 이러한 설정은 ; 이전에 php.ini에서 정의된 값을 덮어씁니다. 지시문은 PHP SAPI와 동일합니다: ; php_value/php_flag - 클래식 ini 정의를 설정할 수 있으며, ; PHP 호출 'ini_set'에서 덮어쓸 수 있습니다. ; php_admin_value/php_admin_flag - 이러한 지시문은 PHP 호출 'ini_set'에 의해 ; 덮어쓰이지 않습니다. ; php_*flag의 경우, 유효한 값은 on, off, 1, 0, true, false, yes 또는 no입니다. ; 'extension'을 정의하면 해당 공유 확장이 ; extension_dir에서 로드됩니다. 'disable_functions' 또는 'disable_classes'를 정의하면 ; 이전에 정의된 php.ini 값을 덮어쓰지 않고, 대신 새로운 값을 추가합니다. ; 주의: 경로 INI 옵션은 상대적일 수 있으며 접두사 ; (풀, 전역 또는 /usr)로 확장됩니다. ; 기본값: 기본적으로 php.ini 및 ; -d 인수로 시작 시 지정된 값 외에는 정의된 것이 없습니다. ;php_admin_value[sendmail_path] = /usr/sbin/sendmail -t -i -f [email protected] ;php_flag[display_errors] = off ;php_admin_value[error_log] = /var/log/fpm-php.www.log ;php_admin_flag[log_errors] = on ;php_admin_value[memory_limit] = 32M |
이 풀은 localhost(127.0.0.1)에서 포트 9000을 수신하고 있으며, www-data 사용자 및 그룹으로 실행되고 있습니다.
vhost의 PHP 구성을 살펴보겠습니다:
vi /etc/nginx/sites-available/example.com.vhost| server { [...] location ~ \.php$ { try_files $uri =404; fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param PATH_INFO $fastcgi_script_name; include /etc/nginx/fastcgi_params; } [...] } |
중요한 부분은 fastcgi_pass 127.0.0.1:9000;입니다. 이는 nginx가 PHP 요청을 localhost(127.0.0.1)의 포트 9000에서 수신 대기 중인 PHP-FPM 프로세스에 전달하도록 합니다. 기억하시겠지만, 이는 /etc/php5/fpm/pool.d/www.conf에 정의된 풀로, PHP 스크립트가 www-data 사용자 및 그룹으로 실행됨을 의미합니다.
3. 각 웹사이트에 대한 개별 풀 정의하기
내 example.com 웹사이트는 사용자 web1과 그룹 client0의 소유이므로, PHP 스크립트가 해당 사용자 및 그룹으로 실행되기를 원합니다. 따라서 /etc/php5/fpm/pool.d/example.com.conf에 새 풀을 정의합니다:
vi /etc/php5/fpm/pool.d/example.com.conf| [example.com] listen = 127.0.0.1:9001 listen.allowed_clients = 127.0.0.1 user = web1 group = client0 pm = dynamic pm.max_children = 50 pm.start_servers = 20 pm.min_spare_servers = 5 pm.max_spare_servers = 35 chdir = / |
보시다시피, 이 풀은 9000 대신 9001에서 수신 대기하도록 설정하고, 사용자를 web1, 그룹을 client0으로 정의합니다. 원하는 만큼 많은 풀을 정의할 수 있지만, 각 풀에 대해 사용되지 않는 포트를 사용해야 합니다(9002, 9003 등).
PHP-FPM을 다시 로드합니다:
/etc/init.d/php5-fpm reload이제 새 풀을 사용하도록 vhost 구성을 변경합니다. fastcgi_pass 줄의 포트만 변경하면 됩니다:
vi /etc/nginx/sites-available/example.com.vhost| server { [...] location ~ \.php$ { try_files $uri =404; fastcgi_pass 127.0.0.1:9001; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param PATH_INFO $fastcgi_script_name; include /etc/nginx/fastcgi_params; } [...] } |
그 후 nginx를 다시 로드합니다:
/etc/init.d/nginx reload그게 전부입니다! 이제 PHP 스크립트가 사용자 web1 및 그룹 client0으로 실행되고 있습니다.
각 vhost에 대해 PHP 설정을 개별적으로 변경하여 PHP를 더욱 안전하게 만들 수 있습니다. /etc/php5/fpm/pool.d/www.conf의 하단을 살펴보면, 이를 달성하는 방법에 대한 몇 가지 예가 있습니다.
예를 들어, /etc/php5/fpm/pool.d/example.com.conf 풀에서 open_basedir 또는 disable_functions을 설정할 수 있습니다.
vi /etc/php5/fpm/pool.d/example.com.conf| [example.com] listen = 127.0.0.1:9001 listen.allowed_clients = 127.0.0.1 user = web1 group = client0 pm = dynamic pm.max_children = 50 pm.start_servers = 20 pm.min_spare_servers = 5 pm.max_spare_servers = 35 chdir = / php_admin_value[open_basedir] = /var/www/www.example.com:/usr/share/php5:/tmp:/usr/share/phpmyadmin:/etc/phpmyadmin:/var/lib/phpmyadmin php_admin_value[disable_functions] = dl,exec,passthru,shell_exec,system,proc_open,popen,curl_exec,curl_multi_exec,parse_ini_file,show_source |
PHP-FPM을 다시 로드합니다:
/etc/init.d/php5-fpm reload3.1 TCP 연결 대신 소켓 사용하기
지금까지 우리는 PHP-FPM 풀에 대해 TCP 연결(127.0.0.1:9000, 127.0.0.1:9001 등)을 사용했습니다. 이는 약간의 오버헤드를 발생시킵니다. 다행히도 우리는 TCP 연결 대신 유닉스 소켓을 사용할 수 있으며 이 오버헤드를 제거할 수 있습니다. 따라서 유닉스 소켓은 TCP 연결보다 성능이 더 좋습니다.
소켓이 /var/run/php5-fpm 디렉토리에 생성되도록 하려면, 먼저 해당 디렉토리를 생성해야 합니다:
mkdir /var/run/php5-fpm유닉스 소켓을 사용하려면, 풀 정의의 listen 줄을 변경하고, listen.allowed_clients 줄을 주석 처리하거나 제거하며(이는 TCP 연결에만 의미가 있음), listen.owner(소켓의 소유자를 정의), listen.group(소켓의 그룹을 정의), listen.mode(소켓의 권한을 정의) 줄을 추가합니다:
vi /etc/php5/fpm/pool.d/example.com.conf| [example.com] listen = /var/run/php5-fpm/example.com.sock ;listen.allowed_clients = 127.0.0.1 listen.owner = web1 listen.group = client0 listen.mode = 0660 user = web1 group = client0 pm = dynamic pm.max_children = 50 pm.start_servers = 20 pm.min_spare_servers = 5 pm.max_spare_servers = 35 chdir = / |
그 후 PHP-FPM을 다시 로드합니다:
/etc/init.d/php5-fpm reload/var/run/php5-fpm 디렉토리를 살펴보세요:
ls -l /var/run/php5-fpm여기서 소켓 example.com.sock이 권한 0660으로, 사용자 web1 및 그룹 client0에 의해 소유되고 있는 것을 찾아야 합니다:
root@server1:~# ls -l /var/run/php5-fpm
total 0
srw-rw---- 1 web1 client0 0 2011-09-21 11:08 example.com.sock
root@server1:~#마지막으로 nginx vhost의 fastcgi_pass 줄을 fastcgi_pass unix:/var/run/php5-fpm/example.com.sock;로 변경해야 합니다:
vi /etc/nginx/sites-available/example.com.vhost| server { [...] location ~ \.php$ { try_files $uri =404; fastcgi_pass unix:/var/run/php5-fpm/example.com.sock; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param PATH_INFO $fastcgi_script_name; include /etc/nginx/fastcgi_params; } [...] } |
그 후 nginx를 다시 로드합니다:
/etc/init.d/nginx reload그게 전부입니다!
4. 링크
- PHP-FPM: http://php-fpm.org/
- nginx: http://nginx.org/
- nginx 위키: http://wiki.nginx.org/
- Debian: http://www.debian.org/
- Ubuntu: http://www.ubuntu.com/
저자에 대하여
Falko Timme는 Timme Hosting(초고속 nginx 웹 호스팅)의 소유자입니다. 그는 HowtoForge의 주요 유지 관리자(2005년부터)이며 ISPConfig의 핵심 개발자 중 한 명(2000년부터)입니다. 그는 또한 O’Reilly의 “Linux System Administration” 책에 기여했습니다.
새 게시물을 받은 편지함에서 받기
스팸은 없습니다. 언제든지 구독 해지 가능합니다.