为PHP应用程序构建准备生产的Dockerfile的最佳实践
#php #docker #kubernetes #containersecurity

Docker是一个容器化平台,用于将您的代码,依赖关系和运行时环境捆绑到在不同环境中相同运行的独立单元中。

对PHP应用程序进行扩展,通过将PHP运行时,Web服务器以及您的源代码和作曲家依赖项打包到容器中来简化部署。开始使用Docker很容易。但是,在生产中安全地使用它之前,您需要避免一些陷阱。选择适当的基本图像,为工作量选择正确的PHP运行时,并为安全性而硬化图像对于防止问题至关重要。在本教程中,您将学习如何为PHP应用程序编写可靠的Dockerfile。

为PHP建造准备生产的Dockerfile

如果您正在阅读本文,则可能已经编写了PHP应用程序,现在您想将其扩展为部署。以下是十种最大化性能,安全性,可靠性和易于开发的最佳实践:

1.使用特定的基本图像

PHP发布了所有受支持的语言版本的an official Docker image。提供了几种图像变体,涵盖了基本操作系统,语言版本和Web服务器集成的矩阵(例如Apache或FastCGI Process Manager (FPM))。

选择最适合您的要求的特定图像很重要。避免使用通用标签(例如php:latest),因为这些标签将使您接触到仅仅用于成熟操作系统所需的潜在不必要的,破坏代码的更改和不必要的库。例如,php:apache图像当前包括PHP 8.2,但该版本的版本将立即切换到PHP 9.0。选择php:8.2-apache可确保您的图像始终运行php 8.2,即使它不再是当前频道:

FROM php:8.2-apache
WORKDIR /var/www/html

COPY index.php .

相同的原理适用于您可能依赖的任何其他图像,例如composer用于依赖性安装。选择与您在计算机上使用的主要作曲家版本相匹配的图像标签,而不是composer:latest

2.使用环境变量进行配置

Docker容器设计为无状态。最好使用容器启动时设置的环境变量来配置应用程序。这比依赖配置文件更有利,这需要您始终设置持久的容器存储using a Docker volume

使用docker run启动容器时,请使用-e标志设置环境变量:

$ docker run -d -e DATABASE_HOST=172.26.0.2 php-app:latest

您的php代码可以通过调用getenv() function
来检索变量的值

// 172.26.0.2
echo getenv("DATABASE_HOST");

这些变量是在单个容器上设置的运行时变量。有时,您可能需要在Dockerfile中设置一个变量,以便自动提供给容器。要实现这一目标,请使用ENV指令:

FROM php:8.2-apache
WORKDIR /var/www/html

ENV DATABASE_DRIVER=sqlite

COPY index.php .

环境变量也可以在构建时间填充,并使用ARG关键字在dockerfile的其他说明中引用:

FROM php:8.2-apache
WORKDIR /var/www/html

ARG DEFAULT_DATABASE_DRIVER

RUN echo '{"db":"$DEFAULT_DATABASE_DRIVER"}' > config.json
COPY index.php .

要设置参数的值,请在运行docker build时使用--build-arg标志:

$ docker build --build-arg DEFAULT_DATABASE_DRIVER=sqlite -t php-app:latest .

3.考虑php fpm vs. nginx/apache

FPM是将PHP与Web服务器集成在一起的替代PHP FastCGI实现,包括Apache和Nginx。与Apache的mod_php这样的服务器模块使用的标准FastCGI相比,FPM提供了高级的非阻滞过程管理,创建不同的工作过程池的能力以及更优雅的错误处理。使用FPM通常为大型应用提供a significant performance boost

php的Docker base image在fpm和mod_php变体中均可使用:

  • **php:8.2-fpm** 此图像和类似标签公开了FPM连接。
  • **php:8.2-apache** 这些图像捆绑了apache安装并使用mod_php处理PHP请求。

如果您使用的是Apache并对mod_php感到满意,请选择:apache基本图像之一。您可以将PHP代码添加到/var/www/html目录并立即开始运行。

要使用FPM,您需要一个单独的容器,该容器运行Nginx之类的服务器。应将其配置为PHP容器中FPM进程暴露的端口的reverse proxy, which forwards PHP请求。端口号默认为9000

当您想要最高性能时,请使用FPM,并且不必担心管理两个容器:FPM容器和Web服务器。基于Apache的图像是快速启动和运行的绝佳起点,而无需配置其他容器或微调FPM Worker Pool Configs。

4.禁用生产中不必要的调试日志/错误报告

生产应用程序不应公开披露攻击者可能会发现有用的任何信息。禁用PHP的display_errorsdisplay_startup_errors设置,以防止未经治疗的例外显示在您的网站上。

要更改这些值,请修改容器中的php.ini文件。

首先创建您的修改文件:

display_errors = Off
display_startup_errors = Off

然后调整您的Dockerfile,将php.ini复制到容器文件系统中的正确位置:

FROM php:8.2-apache
WORKDIR /var/www/html

COPY php.ini /usr/local/etc/php/php.ini
COPY index.php .

PHP的error_reporting level可能需要更改生产。低级错误,例如通知和贬值警告,可以迅速填充日志中的难以恢复的信息。您可以根据环境变量在脚本开始时动态设置此值:

if (getenv("IS_PRODUCTION")) {
    // Disable error reports which are not an immediate issue
    error_reporting(E_ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATED);
}

5.作为非root用户运行您的PHP容器

Docker默认为运行容器为root。容器内部运行的过程具有与主机上的root相同的特权。妥协容器并脱离其隔离允许恶意过程在主机上运行任意命令。

作为专用的非根系用户运行容器有助于降低这些风险。使用dockerfile中的USER指令从此开始切换到其他用户:

FROM php:8.2-apache
WORKDIR /var/www/html
USER appuser

COPY index.php .

6.设置PHP容器健康检查

Docker具有内置的健康检查机制,可以检测容器何时失败。您可以将Docker配置为每三十秒自动运行健康检查命令。如果命令使用Code 0退出,则将容器视为健康。但是,使用Code 1退出的健康检查表示失败的应用程序。

您可以使用Dockerfile中的HEALTHCHECK instruction为应用程序设置健康检查。对于PHP服务,您可以使用curl向应用程序的一个端点之一提出网络请求。如果curl使用0退出,则意味着它在2xx(成功)范围内收到了带有HTTP状态代码的响应:

FROM php:8.2-apache
WORKDIR /var/www/html
USER appuser

HEALTHCHECK CMD curl --fail http://localhost/index.php || exit 1
COPY index.php .

您的容器的健康状况显示在docker ps命令的输出中:

$ docker ps
CONTAINER ID    IMAGE       COMMAND                     CREATED     STATUS
335889ed4698    php-demo-app        "docker-php-entrypoi…"    2 mins ago  Up 2 mins (healthy)

健康检查失败后,容器的状态将显示为unhealthy。诸如Kubernetes之类的集装箱编排者,使用此信号在失败后自动重新启动或更换容器。

7.限制您使用的端口数量

您应该避免在容器上暴露不必要的端口。仅应公开外部流程来达到的端口。这些是其他容器和公共用户将连接到的端口。

对于PHP应用程序,使用捆绑的Web服务器(例如php:8.2-apache)时,通常是port 80。基于FPM的图像应将端口9000揭露,准备连接到单独的Web服务器容器。 FPM端口不应公开开放。

同样,要注意通过环境变量所示的信息。删除未使用的变量以减少披露风险。诸如docker inspect和容器内的任何恶意软件之类的工具始终可以访问您设置的变量的完整列表。

8.定期更新依赖关系

应定期更新堆栈中的所有组件,以应用新的错误修复和安全更新。其中包括以下内容:

  • 在您的Docker Image中更新到基本操作系统。
  • 更新您正在使用的PHP版本和扩展。
  • 您已在项目中安装为依赖项的作曲家软件包。

这是至关重要的,因此您可以保护新漏洞。您应该使用docker build --pull重建图像,以每次构建时都能获取更新。这将吸引最新版本的基本图像,包括任何更新的操作系统软件包和PHP版本。

您还应该定期在项目中运行composer update,以添加依赖项的最新补丁。您可以使用诸如Snyk之类的工具来查找过时或包含漏洞的依赖关系,并建议如何更新固定版本。在使用作曲家应用程序包更新后,重建​​了Docker映像,以便生产部署使用新版本。

9.将容器功能限制为所需的最低功能

linux kernel capabilities限制了过程可以执行的潜在敏感动作。 Docker已经运行了容器with an unprivileged set功能,但是对于典型的PHP工作负载,这些功能仍然过多。例如,默认值允许容器进程更改文件权限,杀死进程并操纵用户ID。

您可以使用docker run--cap-drop标志添加和删除容器功能。使用--cap-drop=all删除所有功能,然后添加您应用所需的特定功能。

是最安全的。

以下示例仅给容器CHOWN功能。容器可以将所有权更改应用于文件和文件夹,但将阻止执行任何其他特权操作:

$ docker run -d --cap-drop=all --cap-add=CHOWN php-app:latest

10.使用多阶段构建进行构建/运行时分离

Multistage builds是一种机制,它使编写和维护更复杂的构建管道变得更加容易。它们让您在构建过程中涉及多个基本图像,同时保持输出较小而精确。

多阶段模式非常适合PHP Dockerfiles,因为通常您需要在构建过程中安装作曲家依赖项。 PHP的官方Docker图像与作曲家无关,而是该项目发布其图像。使用多阶段构建,您可以方便地引用Dockerfile中的作曲家二进制文件:

FROM php:8.2-apache
WORKDIR /var/www/html

COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
COPY composer.json .
COPY composer.lock .
RUN composer install --no-dev
RUN rm /usr/bin/composer

COPY index.php .

构建完成后,composer:2图像将被丢弃。这种方法在构建和运行时保持良好的分离,而不会增加最终图像的大小。

用Snyk识别和解决漏洞

使用Docker可以通过在PHP应用程序及其主机环境之间提供一定程度的隔离来提高安全性。但是,容器图像通常包含由于过时的操作系统软件包而引起的安全漏洞,并且composer dependencies


在将图像部署到生产之前,扫描图像是否是关键的一步。您可以使用Snyk扫描PHP Docker映像并识别和解决漏洞。 Snyk Vulnerability Database包括所有流行的操作系统和依赖项的记录,包括发布给PackagistPHP packages

要开始使用Snyk识别漏洞,您需要创建一个简单的PHP文件并将其保存到index.php

php

echo "Hello World";

?

接下来,为您的应用程序写一个Dockerfile:

FROM php:8.2-apache
WORKDIR /var/www/html

COPY index.php

使用Docker构建图像:

$ docker build -t php-app:latest .

然后前往Snyk网站,然后单击启动免费以创建您的帐户。按照步骤设置,然后安装SNYK CLI。您还需要安装Node.js and npm

$ npm install -g snyk

接下来,运行snyk auth命令。这将打开一个新的浏览器选项卡,您可以在其中登录SNYK并验证CLI。之后,返回您的终端。

要扫描您的图像,请运行以下命令:

$ snyk container test php-app:latest

扫描可能需要一些时间才能完成。结果将显示在您的终端中。您将看到所有检测到的漏洞的列表,包括在场的包装,脆弱版本以及获取更多信息的链接:


最后的摘要显示了发现的问题的总数,并提出了较少漏洞的替代基本图像。

修复检测到的漏洞

如果发现漏洞,则应评估每个漏洞,以确定您是否需要采取行动。对于您的用例,并非每个漏洞都一定是一个问题。首先要专注于解决关键,高和中等问题。

最有效的起点通常是使用更新的基础重建图像。诸如php:8.2-fpm之类的流行社区图像定期重新发布,其中包括包括漏洞修复的新版本。重建图像后,重复扫描以检查其如何影响您的分数。

该报告可能会在低级操作系统组件中列出大量漏洞。考虑使用较小,更小的基本图像来减少整体攻击表面。 Alpine Linux变体(例如php:fpm-alpine)包含较少的包装,这通常意味着更少的漏洞。

在写作时,php:8.2-fpm图像包含98个检测到的漏洞,例如:


php:8.2-fpm-alpine变体仅具有十个,因为它包含的操作系统软件包要少得多:


某些漏洞可能无法在您的项目中解析。例如,操作系统软件包中的问题可能不会影响您的工作负载,否则可能没有补丁程序。您可以通过将其ID传递给snyk ignore命令中的SNYK报告中的漏洞。您可以通过查看漏洞URL的最后一部分来找到ID,例如https://security.snyk.io/vuln/SNYK-ALPINE317-CURL-3320725中的SNYK-ALPINE317-CURL-3320725

$ snyk ignore --id=SNYK-ALPINE317-CURL-3320725

最后,请记住,容器安全是多个组件的总和。您需要一个安全的基本图像,PHP运行时的更新和支持的版本以及依赖关系,工具链组件的修补版本,and application code。定期扫描您的图像以了解漏洞,将使您了解您的安全位置。

概括

阅读本文后,您应该拥有一个可以生产的Docker映像来运行PHP应用程序。本指南涵盖了一些用于编写Dockerfile和使用您构建的图像的关键最佳实践,包括选择特定的基本图像,使用多阶段构建来安装依赖性的步骤,以及扫描安全漏洞以确保您的工作负载适合部署到部署生产。

如果您需要有关Docker Images和PHP的更多建议,请查看Docker's documentationother articles available on the Snyk blog

Snyk是一个适用于确保代码的人的开发人员友好的安全平台。它可以扫描您的Docker图像以在依赖项,操作系统软件包,and PHP code中找到漏洞。 Snyk还提供了IDE plugin,该IDE plugin执行静态分析以检测出现的脆弱性。 Sign up now自动查找并修复PHP容器图像中的漏洞。