18 April 2020

PHP-FPM chroot: fopen(URL) SSL and non-SSL problems

While setting up a PHP application in a PHP-FPM chrooted environment, you may come across some difficulties with the fopen() function called for URLs. These difficulties are fairly simple to solve if one understands what is happening in and out of the chroot(), but might become burdensome otherwise.

Besides the php.ini setting:

allow_url_fopen = On

which is the first place to look at, no matter whether in chroot or not, there is a little bit more.

Test script

Prepare a simple script that will fopen() a site and print out its content.

<?php
$f = fopen('http://some.site.domain', 'r')
    or die("fopen() fail");
while($line = fread($f, 1000))
    print($line);
fclose($f);
?>

Should everything be all right, you will see the content of some.site.domain.

Resolver

If you get this error:

PHP Warning:  fopen(http://some.site.domain): failed to open stream: php_network_getaddresses: getaddrinfo failed: Name does not resolve in /testscript.php on line 2'

this gives us a clue, that the system resolver could not find the host by name. The way the system resolver works vary from one system to another, but there are some general rules that apply to most of the popular unix-like systems.

Look at the /etc/nsswitch.conf:

(...)
hosts: files dns
(...)

In the given case the system will look first at /etc/hosts, then fallback to the DNS.

Since the gethostbyname() call is executed from within the chroot(), the resolver has no way to access the hosts and resolv.conf files in the base system, thus does not know what the DNS servers are. Just copy the /etc/resolv.conf to your chroot location. If you use /etc/hosts, copy this one too.

# cp /etc/hosts /your/chroot/path/etc/hosts
# cp /etc/resolv.conf /your/chroot/path/etc/resolv.conf

Now the unencrypted HTTP request should succeed.

OpenSSL

Next try to access an SSL site. Modify the test script replacing fopen('http://some.site.domain', 'r') with fopen('https://some.site.domain', 'r'). If the request fails, check if you have openssl extension enabled in the php.ini:

extension=openssl

Missing openssl extension will rise an error:

PHP Warning:  fopen(https://some.site.domain): failed to open stream: Unable to find the socket transport &quot;ssl&quot; - did you forget to enable it when you configured PHP? in /testscript.php on line 2'

FreeBSD has the extension in a separate package (assuming it is not built from ports), thus

# pkg install php74-openssl

This step however is just a hint. Other systems will require a different approach.

If the openssl extension has been enabled, yet you see:

fopen(): SSL operation failed with code 1. OpenSSL Error messages:\nerror:1416F086:SSL routines:tls_process_server_certificate:certificate verify failed in /testscript.php on line 2
PHP message: PHP Warning:  fopen(): Failed to enable crypto in /testscript.php on line 2
PHP message: PHP Warning:  fopen(https://some.site.domain): failed to open stream: operation failed in /testscript.php on line 2'

you have to provide the OpenSSL library with its mandatory files, eg. CA root certificates. In FreeBSD 12.1 all certificates are stored in a bundle /etc/ssl/cert.pem:

# cp /etc/ssl/cert.pem /your/chroot/path/etc/ssl/cert.pem

In many Linux distributions those certificates usually come as single files located in a subdirectory under /etc/ssl.

Now the fopen() function should work fine for both http:// and https:// URLs.

Without success?

If you can't get it working, try to deduce from the PHP errors which of the base system files the PHP internal calls may require. Then copy those files to the directory that PHP-FPM chroots to (preserving paths). Understanding of the system, libraries and applications may be essential here.