Docker のコンテナ内から https の URL に対して、 curl
できたのに PHP で file_get_contents
できないパターンがありました。
「?」と思ったので検証してみます。
Windows 10 Pro を使用しています。
まとめ
SSL 化されたコンテンツに対する
file_get_contents
の可否と、 443 番ポートに関するopenssl
の疎通の可否が一致した。
このことから、file_get_contents
とopenssl
には少なからず関連があり、file_get_contents
がうまくいかない場合にopenssl
での疎通確認を行うことは効果的である。
準備
まずはプロジェクトフォルダを好きな場所に作成します。
mkdir -p /path/to/dir
cd /path/to/dir
サーバーで使う証明書を用意する
今回は mkcert
で証明書を作成します。
mkcert
をインストールします。
私は Windows 使いなので chocolatey でインストールします。
PowerShell を管理者権限で開いて実行します。
cinst -y mkcert
初回のみ、次のコマンドを実行します。
mkcert -install
セキュリティに関する警告が出てきますが、「はい」をクリックします。
$ mkcert -install
Created a new local CA 💥
The local CA is now installed in the system trust store! ⚡️
絵文字が出てくるのがかわいいですね
次に localhost
に対する証明書を発行します。
mkdir ssl
cd ssl
mkcert localhost php-apache.com
$ mkcert localhost php-apache.com
Created a new certificate valid for the following names 📜
- "localhost"
- "php-apache.com"
The certificate is at "./localhost+1.pem" and the key at "./localhost+1-key.pem" ✅
It will expire on 28 February 2023 🗓
ルート証明書も後で使うので、コピーしておきます。
ルート証明書の場所は次のコマンドで確認できます。
mkcert -CAROOT
確認したら、そこから rootCA.pem
をコピーします。
使用するファイルを用意する
今回用意するファイルは以下の通りです。
/path/to/dir
│ docker-compose.yml
│ Dockerfile
│
├─html
│ hello.php
│ index.php
│
└─ssl(用意済み)
localhost+1-key.pem
localhost+1.pem
rootCA.pem
./docker-compose.yml
今回はコンテナ内でコマンドを打ちますので、ポートを開ける必要はありません。
version: '3'
services:
php-apache:
container_name: php-apache.com
build: .
volumes:
- ./html:/var/www/html
./Dockerfile
この後の検証で何度か編集しますので、今は空のファイルを用意しておきます。
./html/index.php
<?php
$url = 'https://php-apache.com/hello.php';
echo file_get_contents($url);
./html/hello.php
<?php
header('Content-type: text/plain; charset=UTF-8');
echo 'hello';
exit;
これで準備は終わりです
概要
ここからは実際の検証に入る前に、検証の概要について説明します。
今回はコンテナの状態を 4 つ作り、それぞれに対して 3 つの方法を試します。
コンテナの状態
- 最初の状態: mkcert で作った証明書を配置して ssl を有効にしただけの状態
- 状態 2: 最初の状態で、新たに CA 証明書を配置し、環境変数
CURL_CA_BUNDLE
に設定した状態 - 状態 3: 最初の状態で、 CA 証明書を配置し、 OpenSSL のディレクトリに CA 証明書へのシンボリックリンクを設定した状態
- 状態 4: 最初の状態で、 CA 証明書を配置し、環境変数
CURL_CA_BUNDLE
に設定し、 OpenSSL のディレクトリに CA 証明書へのシンボリックリンクを設定した状態
方法
- PHP の file_get_contents 関数で
https://php-apache.com/
を読み込む
winpty docker exec -it php-apache.com php -r "echo file_get_contents('https://php-apache.com/'), PHP_EOL;"
成功とする基準: PHP の Error や Warning が出ない
- コンテナの中から curl コマンドで
https://php-apache.com/
にリクエストする
winpty docker exec -it php-apache.com curl https://php-apache.com/
成功とする基準: curl に関する エラーメッセージが出ない
- コンテナの中から openssl コマンドで
php-apache.com:443
への疎通確認を行う
winpty docker exec -it php-apache.com openssl s_client -quiet -connect php-apache.com:443
成功とする基準:
verify error
が出ない
以上の方法を行う前には、 docker-compose up -d
でコンテナを立ち上げ、行った後には docker-compose down -v
でコンテナを終了します。
検証
最初の状態: mkcert で作った証明書を配置して ssl を有効にしただけの状態
Dockerfile
を以下のように編集します。
FROM php:apache-buster
RUN cp /usr/local/etc/php/php.ini-development /usr/local/etc/php/php.ini
# mkcert で作った証明書を配置する
COPY ./ssl/"localhost+1.pem" /etc/ssl/certs/ssl-cert-snakeoil.pem
COPY ./ssl/"localhost+1-key.pem" /etc/ssl/private/ssl-cert-snakeoil.key
# Enable SSL
RUN a2enmod ssl && a2ensite default-ssl
結果
file_get_contents, curl, openssl ともに失敗しました。
No. | 方法 | 結果 |
---|---|---|
1 | file_get_contents | 失敗 |
2 | curl | 失敗 |
3 | openssl | 失敗 |
詳細
- file_get_contents
SSL に関するエラーが出ていることに注目します。
$ winpty docker exec -it php-apache.com php -r "echo file_get_contents('https://php-apache.com/'), PHP_EOL;"
PHP Warning: file_get_contents(): SSL operation failed with code 1. OpenSSL Error messages:
error:1416F086:SSL routines:tls_process_server_certificate:certificate verify failed in Command line code on line 1
Warning: file_get_contents(): SSL operation failed with code 1. OpenSSL Error messages:
error:1416F086:SSL routines:tls_process_server_certificate:certificate verify failed in Command line code on line 1
PHP Warning: file_get_contents(): Failed to enable crypto in Command line code on line 1
Warning: file_get_contents(): Failed to enable crypto in Command line code on line 1
PHP Warning: file_get_contents(https://php-apache.com/): failed to open stream: operation failed in Command line code on line 1
Warning: file_get_contents(https://php-apache.com/): failed to open stream: operation failed in Command line code on line 1
- curl
$ winpty docker exec -it php-apache.com curl https://php-apache.com/
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.haxx.se/docs/sslcerts.html
curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.
- openssl
$ winpty docker exec -it php-apache.com openssl s_client -quiet -connect php-apache.com:443
depth=0 O = mkcert development certificate, OU = MYCOMPUTER\\norit@MyComputer (Noritaka IZUMI)
verify error:num=20:unable to get local issuer certificate
verify return:1
depth=0 O = mkcert development certificate, OU = MYCOMPUTER\\norit@MyComputer (Noritaka IZUMI)
verify error:num=21:unable to verify the first certificate
verify return:1
...
状態 2: 最初の状態で、新たに CA 証明書を配置し、環境変数 CURL_CA_BUNDLE
に設定した状態
参考: [curl] HTTPS通信できない (unable to get local issuer certificate) - noknow
Dockerfile
は以下の通りです。
FROM php:apache-buster
RUN cp /usr/local/etc/php/php.ini-development /usr/local/etc/php/php.ini
# mkcert で作った証明書を配置する
COPY ./ssl/"localhost+1.pem" /etc/ssl/certs/ssl-cert-snakeoil.pem
COPY ./ssl/"localhost+1-key.pem" /etc/ssl/private/ssl-cert-snakeoil.key
# CA 証明書を配置する
COPY ./ssl/rootCA.pem /etc/ssl/certs/rootCA.pem
ENV CURL_CA_BUNDLE=/etc/ssl/certs/rootCA.pem
# Enable SSL
RUN a2enmod ssl && a2ensite default-ssl
結果
curl が成功したのに対し、
file_get_contents と openssl は失敗しました。
いわゆる curl
できたのに file_get_contents
できないパターン ですね。
No. | 方法 | 結果 |
---|---|---|
1 | file_get_contents | 失敗 |
2 | curl | 成功 |
3 | openssl | 失敗 |
詳細
- file_get_contents
$ winpty docker exec -it php-apache.com php -r "echo file_get_contents('https://php-apache.com/'), PHP_EOL;"
PHP Warning: file_get_contents(): SSL operation failed with code 1. OpenSSL Error messages:
error:1416F086:SSL routines:tls_process_server_certificate:certificate verify failed in Command line code on line 1
Warning: file_get_contents(): SSL operation failed with code 1. OpenSSL Error messages:
error:1416F086:SSL routines:tls_process_server_certificate:certificate verify failed in Command line code on line 1
PHP Warning: file_get_contents(): Failed to enable crypto in Command line code on line 1
Warning: file_get_contents(): Failed to enable crypto in Command line code on line 1
PHP Warning: file_get_contents(https://php-apache.com/): failed to open stream: operation failed in Command line code on line 1
Warning: file_get_contents(https://php-apache.com/): failed to open stream: operation failed in Command line code on line 1
- curl
PHP でエラーが起こっていますが、 curl に関してエラーが出ていない ので成功とします。
$ winpty docker exec -it php-apache.com curl https://php-apache.com/
<br />
<b>Warning</b>: file_get_contents(): SSL operation failed with code 1. OpenSSL Error messages:
error:1416F086:SSL routines:tls_process_server_certificate:certificate verify failed in <b>/var/www/html/index.php</b> on line <b>3</b><br />
<br />
<b>Warning</b>: file_get_contents(): Failed to enable crypto in <b>/var/www/html/index.php</b> on line <b>3</b><br />
<br />
<b>Warning</b>: file_get_contents(https://php-apache.com/hello.php): failed to open stream: operation failed in <b>/var/www/html/index.php</b> on line <b>3</b><br />
- openssl
$ winpty docker exec -it php-apache.com openssl s_client -quiet -connect php-apache.com:443
depth=0 O = mkcert development certificate, OU = MYCOMPUTER\\norit@MyComputer (Noritaka IZUMI)
verify error:num=20:unable to get local issuer certificate
verify return:1
depth=0 O = mkcert development certificate, OU = MYCOMPUTER\\norit@MyComputer (Noritaka IZUMI)
verify error:num=21:unable to verify the first certificate
verify return:1
...
状態 3: 最初の状態で、 CA 証明書を配置し、 OpenSSL のディレクトリに CA 証明書へのシンボリックリンクを設定した状態
参考: [OpenSSL] [エラー] Verification error: unable to get local issuer certificate - noknow
Dockerfile
は以下の通りです。
FROM php:apache-buster
RUN cp /usr/local/etc/php/php.ini-development /usr/local/etc/php/php.ini
# mkcert で作った証明書を配置する
COPY ./ssl/"localhost+1.pem" /etc/ssl/certs/ssl-cert-snakeoil.pem
COPY ./ssl/"localhost+1-key.pem" /etc/ssl/private/ssl-cert-snakeoil.key
# OpenSSL のディレクトリに CA 証明書へのシンボリックリンクを設定する
COPY ./ssl/rootCA.pem /etc/ssl/certs/rootCA.pem
RUN ln -s /etc/ssl/certs/rootCA.pem $(openssl version -d | cut -d' ' -f2 | sed 's/"//g')/cert.pem
# Enable SSL
RUN a2enmod ssl && a2ensite default-ssl
結果
file_get_contents と openssl が成功したのに対し、
curl は失敗しました。
No. | 方法 | 結果 |
---|---|---|
1 | file_get_contents | 成功 |
2 | curl | 失敗 |
3 | openssl | 成功 |
詳細
- file_get_contents
$ winpty docker exec -it php-apache.com php -r "echo file_get_contents('https://php-apache.com/'), PHP_EOL;"
hello
- curl
$ winpty docker exec -it php-apache.com curl https://php-apache.com/
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.haxx.se/docs/sslcerts.html
curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.
- openssl
$ winpty docker exec -it php-apache.com openssl s_client -quiet -connect php-apache.com:443
depth=1 O = mkcert development CA, OU = MYCOMPUTER\\norit@MyComputer (Noritaka IZUMI), CN = mkcert MYCOMPUTER\\norit@MyComputer (Noritaka IZUMI)
verify return:1
depth=0 O = mkcert development certificate, OU = MYCOMPUTER\\norit@MyComputer (Noritaka IZUMI)
verify return:1
...
状態 4: 最初の状態で、 CA 証明書を配置し、環境変数 CURL_CA_BUNDLE
に設定し、 OpenSSL のディレクトリに CA 証明書へのシンボリックリンクを設定した状態
Dockerfile
は以下の通りです。
FROM php:apache-buster
RUN cp /usr/local/etc/php/php.ini-development /usr/local/etc/php/php.ini
# mkcert で作った証明書を配置する
COPY ./ssl/"localhost+1.pem" /etc/ssl/certs/ssl-cert-snakeoil.pem
COPY ./ssl/"localhost+1-key.pem" /etc/ssl/private/ssl-cert-snakeoil.key
# CA 証明書を配置する
COPY ./ssl/rootCA.pem /etc/ssl/certs/rootCA.pem
ENV CURL_CA_BUNDLE=/etc/ssl/certs/rootCA.pem
# OpenSSL のディレクトリに CA 証明書へのシンボリックリンクを設定する
RUN ln -s /etc/ssl/certs/rootCA.pem $(openssl version -d | cut -d' ' -f2 | sed 's/"//g')/cert.pem
# Enable SSL
RUN a2enmod ssl && a2ensite default-ssl
結果
file_get_contents, curl, openssl ともに成功しました。
No. | 方法 | 結果 |
---|---|---|
1 | file_get_contents | 成功 |
2 | curl | 成功 |
3 | openssl | 成功 |
詳細
- file_get_contents
$ winpty docker exec -it php-apache.com php -r "echo file_get_contents('https://php-apache.com/'), PHP_EOL;"
hello
- curl
$ winpty docker exec -it php-apache.com curl https://php-apache.com/
hello
- openssl
$ winpty docker exec -it php-apache.com openssl s_client -quiet -connect php-apache.com:443
depth=1 O = mkcert development CA, OU = MYCOMPUTER\\norit@MyComputer (Noritaka IZUMI), CN = mkcert MYCOMPUTER\\norit@MyComputer (Noritaka IZUMI)
verify return:1
depth=0 O = mkcert development certificate, OU = MYCOMPUTER\\norit@MyComputer (Noritaka IZUMI)
verify return:1
...
最終結果と考察
あらためてそれぞれの状態での結果を一つの表にまとめてみると、file_get_contents と openssl で結果が一致していることがわかります
No. | 方法 | 最初の状態 | 状態 2 | 状態 3 | 状態 4 |
---|---|---|---|---|---|
1 | file_get_contents | 失敗 | 失敗 | 成功 | 成功 |
2 | curl | 失敗 | 成功 | 失敗 | 成功 |
3 | openssl | 失敗 | 失敗 | 成功 | 成功 |
OS や PHP, Apache の状態によっては結果が変わる可能性もあるため、一概に file_get_contents と openssl の疎通可否を必要十分条件ということはできませんが、少なくともある程度有益な情報が得られたとは思います
もし、同じエラーに詰まった方がいらっしゃいましたら、参考にしていただけると幸いです