【OCI】Oracle Linux 8 で Let's encrypt の証明書を発行する

作業の流れ

全体の流れは、以下の通り

  1. Let's encrypt で SSL 証明書を発行する
  2. SSL 証明書の自動更新設定

Let's encrypt で SSL 証明書を発行する

適当な Compute インスタンスcertbot をインストールし証明書を発行する。

今回は OCI Compute (OS: Oracle Linux 8, Arm) を使う場合で説明する。

公式では snapd による certbot install が推奨となっているが、Oracle Linux 8 on Arm64 on OCI の環境では certbot が Segmentation fault になり機能しなかった。 (同様の例: Segmentation fault · Issue #9138 · certbot/certbot · GitHub

そのため、certbot は dnf で install する。

(参考) Certbot Instructions | Certbot

# EPEL repository の有効化
$ sudo dnf install https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm

# certbot と nginx 設定用のパッケージのインストール
$ sudo dnf install certbot python3-certbot-nginx -y

# --nginx を指定すると、証明書発行だけでなく、 nginx の conf への追記、nginx の reload まで実施する
$ sudo certbot --nginx 
... # 指示に従ってメールアドレスや、ドメインを入力する

SSL 証明書の自動更新設定

Let's encrypt で発行した証明書の有効期限は3ヶ月のため定期的に更新が必要。 証明書更新後は nginx の reload が必要なので --post-hook オプションで証明書更新後に実行するコマンドを設定する。

毎週月曜午前4時に証明書の更新を試みるコマンドを crontab に登録する例

$ sudo crontab -e
00 04 * * mon certbot renew --post-hook "systemctl reload nginx"

なお、通常 certbot renew は証明書の有効期限が残り30日を切っていないのであれば署名書更新は行わずスキップされる。 つまり、実際の証明書更新は2ヶ月に1度しか行われない。 あえて毎週更新を試みているのは、2ヶ月に一度のコマンド実行だと、運悪くそのタイニングでインスタンスがダウンしていたり、ネットワークに問題が起きていると証明書の有効期限が切れてしまうため。

Oracle Cloud Free Tier で Autonomous Database に接続する PHP アプリケーションを構築する

Oracle Cloud Free Tier が無料で色々使えるとのことだったので、これまで使っていた PHPレンタルサーバを解約して、Oracle Cloud に移行する際のメモ。

Oracle Cloud Free Tier でインスタンスと DB を作成

今回利用する Oracle Cloud Free Tier の無料枠

  • Compute: Arm ベースプロセッサ Ampere 4 OCPU(= 8 vCPU) と 24GB メモリのリソース(4インスタンスまで分割利用可)
  • Database: 1 OCPU と 20 GBのストレージのデーターベース2つ
  • Flexible Load Balancer:1インスタンス、10 Mbps。

(詳細)Oracle Cloud Free Tier | Oracle 日本

システム構成

  • 開発環境:Compute 1台 + Autonomous Database(Transaction Processing)
  • 本番環境:Compute 2台 + Autonomous Database(Transaction Processing) + Flexible Load Balancer

Compute

開発環境・本番環境もインスタンスのスペックは同一

Autonomus Database

Oracle 19c の Transaction Processing Type のデータベースを作成する。

ADMIN アカウントのパスワードを入力

アクセスを 許可するインスタンスの Public IP を指定する。

サービスゲートウェイ経由でのアクセスにする場合、vcn や Private IP の指定も可能だがここでは割愛する。

(参考)Provision Autonomous Database

PHPOracle Autonomous Database に接続する

Oracle Cloud の手順 Build a PHP Application に沿って進める。

流れとしては以下のような感じ。

  1. PHP のダウンロード&インストール
  2. Oracle Instant Client のダウンロード&インストール
  3. PHP OCI8 のダウンロード&インストール
  4. Database のクライアント証明書のダウンロード

PHP のダウンロード&インストール

RHEL8 系から導入されたモジュール方式のパッケージ管理を使って PHP-7.4 をインストールする。

モジュールには複数の Stream(バージョン)があり、希望するStreamを指定することで好きなバージョンのパッケージをインストールすることができる。

# php の利用可能 Stream(バージョン)を確認
$ dnf module list php
Oracle Linux 8 Application Stream (aarch64)
Name                            Stream                                Profiles                                             Summary
php                             7.2 [d][e]                            common [d], devel, minimal                           PHP scripting language
php                             7.3                                   common [d], devel, minimal                           PHP scripting language
php                             7.4                                   common [d], devel, minimal                           PHP scripting language

Hint: [d]efault, [e]nabled, [x]disabled, [i]nstalled

# php:7.4 を enable にする
$ sudo dnf module enable php:7.4 -y

# php:7.4 が enable になっているか確認する
$ dnf module list php
Oracle Linux 8 Application Stream (aarch64)
Name                             Stream                             Profiles                                              Summary
php                              7.2 [d]                            common [d], devel, minimal                            PHP scripting language
php                              7.3                                common [d], devel, minimal                            PHP scripting language
php                              7.4 [e]                            common [d], devel, minimal                            PHP scripting language

Hint: [d]efault, [e]nabled, [x]disabled, [i]nstalled

# php をインストールする
$ sudo dnf install php -y

PHP extention を install する際に必要な pecl コマンドが使えるよう「php-pear」と、PHP extention を build するときに必要な phpize コマンドが使えるよう「php-devel」もインストールしておく。

$ sudo dnf install php-pear php-devel -y

(参考)

Oracle Instant Client のダウンロード&インストール

Oracle Instant Client は Oracle Database に接続するためのライブラリ(クライアント)。

# Oracle Instant Client を配布する yum repository を使うためのパッケージ
$ sudo dnf install oracle-release-el8 -y
# Oracle Instant Client
$ sudo dnf install oracle-instantclient19.10-basic oracle-instantclient19.10-devel -y

以上で Oracle Instant Client のセットアップは完了。

PHP OCI8 のダウンロード&インストール

PHP OCI8 は PHP から Oracle Instant Client を使ってデータベースに接続するための PHP のライブラリ。

途中で instantclient のライブラリのパスを求められるが autodetect に任せるのでそのまま Enter で OK。

# 最新の oci8 は php 8.1.0 以上を求められるのでバージョンを指定(oci8-2.2.0)する
# pecl/oci8 requires PHP (version >= 8.1.0)
$ sudo PHP_DTRACE=yes pecl install oci8-2.2.0
Please provide the path to the ORACLE_HOME directory. Use 'instantclient,/path/to/instant/client/lib' if you're compiling with Oracle Instant Client [autodetect] : # autodetect に任せるのでそのまま Enter
...

# oci8 拡張の有効化と設定
$ sudo sh -c 'cat <<EOF > /etc/php.d/20-oci8.ini
extension=oci8.so
oci8.events=On
EOF'

Database のクライアント証明書のダウンロード

作成した Autonomus Database の詳細ページの「DB Connection」タブを開いて、Wallet (クライアント証明書) を作成し、zip 形式でダウンロードする。

ダウンロードした Wallet は Oracle インスタンスに scp する。

$ scp ~/Downloads/Wallet_testdb.zip dev:

以降 Oracle インスタンス側の作業。

# 接続するデータベースの設定ファイルの場所として TNS_ADMIN 環境変数を設定する
# インストールする instantclient のバージョンによって ORACLE_HOME のパスが異なるので注意
$ export TNS_ADMIN=/usr/lib/oracle/19.10/client64/lib/network/admin

$ sudo mv Wallet_testdb.zip $TNS_ADMIN
$ cd $TNS_ADMIN
$ sudo unzip Wallet_testdb.zip 
Archive:  Wallet_devariuedb.zip
replace README? [y]es, [n]o, [A]ll, [N]one, [r]ename: y
...
$ sudo rm Wallet_testdb.zip


# 設定ファイルの修正(zip を展開したディレクトリのパスを記載する。今回は opc ユーザーを使っている。)
$ sudo cp sqlnet.ora{,.bak}
$ sudo vim sqlnet.ora # 以下の diff になるように修正する

# 環境変数を参照する設定にすることで、アプリケーションごとに異なる Oracle Database へ接続することができる
$ diff sqlnet.ora{.bak,}
1,2c1,2
< WALLET_LOCATION = (SOURCE = (METHOD = file) (METHOD_DATA = (DIRECTORY="?/network/admin")))
< SSL_SERVER_DN_MATCH=yes
\ No newline at end of file
---
> WALLET_LOCATION = (SOURCE = (METHOD = file) (METHOD_DATA = (DIRECTORY="$TNS_ADMIN")))
> SSL_SERVER_DN_MATCH=yes
$

(参考)104: クレデンシャル・ウォレットを利用して接続してみよう | Oracle Cloud Infrastructure チュートリアル

PHP から Oracle DB への接続確認テスト

テスト用のテーブルを作成

コマンドラインから Oracle DB に接続するツールの sqlplus を使ってテスト用のテーブルを作成する(接続確認テストを行わない場合はインストール不要)

# sqlplus のインストール
$ sudo dnf install oracle-instantclient19.10-sqlplus -y

# sqlplus を使った接続コマンド
$ sqlplus <user_name>@<database_name($TNS_ADMIN/tnsnames.ora で定義されている名前)>
Enter password: <password>

SQL> CREATE TABLE Persons (
    PersonID int,
    LastName varchar(255),
    FirstName varchar(255),
    Address varchar(255),
    City varchar(255)
);
SQL> INSERT INTO Persons VALUES (1, 'Tanaka', 'Taro', 'Tokyo', 'Minato-ku');
SQL> SELECT * from Persons;
<結果が帰ってきたらOK>

PHPOracle DB に接続するサンプルコード:test.php, , <database名> は適宜変えてください)

<?php

$conn = oci_pconnect('<user>', '<password>', '<database名(ex. hogehoge_low)>');
if (!$conn) {
    $e = oci_error();
    trigger_error(htmlentities($e['message'], ENT_QUOTES), E_USER_ERROR);
}

$stid = oci_parse($conn, 'SELECT * FROM Persons');
oci_execute($stid);

echo "<table border='1'>\n";
while ($row = oci_fetch_array($stid, OCI_ASSOC+OCI_RETURN_NULLS)) {
    echo "<tr>\n";
    foreach ($row as $item) {
        echo "    <td>" . ($item !== null ? htmlentities($item, ENT_QUOTES) : "&nbsp;") . "</td>\n";
    }
    echo "</tr>\n";
}
echo "</table>\n";

?>

実行して正しく出力されれば PHP からの DB 接続は OK。

$ php test.php
<table border='1'>
<tr>
    <td>1</td>
    <td>Tanaka</td>
    <td>Taro</td>
    <td>Tokyo</td>
    <td>Minato-ku</td>
</tr>
</table>

動作確認後、不要なテーブルは削除する

$ sqlplus <user_name>@<database_name>
SQL> DROP TABLE Persons;

nginx, php-fpm を使って PHP アプリケーションサーバーをたてる

単発実行の PHP プログラムで Autonomous Database への接続ができることを確認したら、実際に外部にWebページを公開できるようにする。

80 ポートの外部公開

VCN(Virtual Cloud Network)のサブネットのデフォルトのセキュリティリストには SSH くらいしか許可しないルールが設定されているので、 80 ポートへのアクセスを許可するために VCN のサブネットのデフォルトセキュリティリストを更新する。

該当インスタンスを含むサブネットのセキュリティリストの Ingress Rule (= in-traffic rule)に以下を許可するルールを追加するか、新規セキュリティリストを作ってサブネットに紐付ける。

  • ソース・タイプ: CIDR
  • ソースCIDR: 0.0.0.0/0(全てに公開)
  • IPプロトコル: TCP
  • 宛先ポート: 80 , 443(https で通信する場合)

(参考)ロード・バランシングの開始

Oracle Linux 8 で外部から HTTP 通信するためには firewalld の設定追加も必要。

$ sudo firewall-cmd --permanent --zone=public --add-service=http 

# https を有効化する場合
$ sudo firewall-cmd --permanent --zone=public --add-service=https

$ sudo firewall-cmd --reload

SELinux の無効化

SELinux が有効のままだと後ほどインストールする php-fpm から OCI8 の動的ライブラリの読み込みに失敗したので、無効にしておく。

# 現在の設定値の確認
$ sudo getenforce
Enforcing
$ sudo vim /etc/selinux/config
SELINUX=disabled
$ sudo reboot

# reboot 後確認
$ sudo getenforce
Disabled

nginx, php-fpm のインストール

$ sudo dnf install nginx php-fpm -y

# configure php-fpm
* 変更点1: user, group を apache → nginx に変える(php-fpm と nginx は別のユーザーで実行することがセキュリティ的には望ましいがここでは簡易化して進める)
* 変更点2: clear_env=no のコメントアウトを外して php-fpm worker プロセスが環境変数を読み込めるようにする
* 変更点3: OCI8 を使うための各種環境変数を設定
$ sudo cp /etc/php-fpm.d/www.conf{,.bak}
$ sudo vim /etc/php-fpm.d/www.conf # 以下の diff になるように修正する
$ diff /etc/php-fpm.d/www.conf{.bak,}
24c24
< user = apache
---
> user = nginx
26c26
< group = apache
---
> group = nginx
379c379
< ;clear_env = no
---
> clear_env = no
396a397,400
> env[TNS_ADMIN] = /usr/lib/oracle/19.10/client64/lib/network/admin
> env[NLS_LANG]=Japanese_Japan.AL32UTF8
> env[LD_LIBRARY_PATH]=/usr/lib/oracle/19.10/client64/lib:$LD_LIBRARY_PATH
> env[ORA_SDTZ]=Japan

nginx, php-fpm の起動と動作確認

$ sudo systemctl start nginx php-fpm
$ sudo systemctl enable nginx php-fpm
$ curl '<public_ip>'
# nginx のデフォルトページが返ってきたら OK 
# うまくいかない場合、セキュリティリストや Firewalld の設定を見直す

必要に応じて、DB 接続テスト用のプログラムを使って、php-fpm から Oracle DB に接続できることを確認する。

$ sudo cp test.php /usr/share/nginx/html/test.php
$ curl 'http://<public_ip>/test.php' #
<table border='1'>
<tr>
    <td>1</td>
    <td>Tanaka</td>
    <td>Taro</td>
    <td>Tokyo</td>
    <td>Minato-ku</td>
</tr>
</table>

うまくいかない場合、phpinfo で OCI8 が enable になっているか、php-fpm の journal にエラーや警告が出ていないかを nginx の error.log 等で確認する。

PDO を使った DB アクセスをする場合

PDO を使って DB 接続する場合は、pdo_oci のインストールが必要。

pdo_oci に必要な依存パッケージを事前にインストールする。

$ sudo dnf install sqlite-devel unixODBC-devel php-pdo -y
$ sudo pecl install sqlsrv pdo_sqlsrv

pdo_oci のインストールは、インスタンスが利用している php バージョンのソースコードから pdo_oci のみをコンパイルしてインストールする。

$ wget https://museum.php.net/php7/php-7.4.19.tar.gz
$ tar xzvf php-7.4.19.tar.gz
$ cd php-7.4.19/ext/pdo_oci/
$ phpize
$ ./configure --with-pdo-oci=instantclient,/usr/lib/oracle/19.10/client64/lib/,19.10
$ make
$ sudo make install
$ sudo sh -c "echo extension=pdo_oci.so > /etc/php.d/20-pdo_oci.ini"
$ sudo systemctl restart php-fpm

(参考) PHP: Oracle (PDO) - Manual

PDO を使ったサンプルプログラム:pdo_test.php

<?php
$db['dbname']='oci:dbname=<databasename>;charset=AL32UTF8';
$db['user']=<user>;
$db['pass']=<pass>;

try {
    $pdo = new PDO ( $db['dbname'], $db['user'], $db['pass'], array (   
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
    ) );
    $statement = $pdo->query ( 'SELECT * FROM Persons' );
    while ( $row = $statement->fetch ( PDO::FETCH_ASSOC ) ) {
        var_dump($row);
    }
    $statement->closeCursor();
} catch ( PDOException $e ) {
    exit ( 'Failed to connect database: ' . $e->getMessage () );
}

(参考) PHP: PDO_OCI DSN - Manual

正常に実行できれば PDO を使って Oracle DB に接続ができている

$ php pdo_test.php
array(5) {
  ["PERSONID"]=>
  string(1) "1"
  ["LASTNAME"]=>
  string(6) "Tanaka"
  ["FIRSTNAME"]=>
  string(4) "Taro"
  ["ADDRESS"]=>
  string(5) "Tokyo"
  ["CITY"]=>
  string(9) "Minato-ku"
}
$

実アプリケーションの配置

/usr/share/nginx/html/ に PHP アプリケーションを配置して動作することを確認する。

本番用 Load Balancer の利用

冗長化、負荷分散のために本番環境は LB を使ってバックエンドサーバ2台にアクセスが振られるようにする。

バックエンドサーバのインスタンスセットアップ後、LB を作成する。

(適当に設定)

LB 作成後、バックエンドリスナーにバックエンドサーバを追加する。

作成後、LB 経由でバックエンドサーバーにアクセスできることを確認する。

$ curl 'http://<LB の public ip>'

不要なアクセスを遮断する

セットアップ後は SSH や ICMP など不要なアクセスを遮断するため、不要な Ingress Rule を削除する。

注:以降の設定を実施すると SSH 接続もできなくなり、LB 経由での 80 アクセスしかできなくなるのでアプリケーションのセットアップが完全に終わってから実施すること。

サブネットのデフォルトセキュリティリストの Ingress(in-traffic) で不要なものを削除する

ingress_rule

なお、本当は Egress Rule やルート表(参考:ロード・バランシングの開始)で Autonomous Database 以外の public network へのアクセスを遮断したかったが、Autonomous Database の ip は変わる可能性があるので制限が難しかった。

(Free Tier では使えないが、「Private Endpoint Access to Autonomous Database」を使うと VCN にでプライベートなエンドポイントを生やせるよう)

通常、80 ポートのみ公開していれば Web サーバー(nginx)に致命的な脆弱性が見つからない限りは大丈夫だろうが、その辺は要件次第で検討。

備考

今回はあまり触れられなかったが、実際には以下の点を考慮に入れる。

php-fpm alpine の docker image を動かす際の覚書

利用するベース Docker image

php:8.1-fpm-alpine Docker Hub

作成する Dockerfile とビルド

Dockerfile の設定は最小限。

Dockerfile

FROM php:8.0-fpm-alpine

RUN echo "<?php echo 'Hello World!'; " > /var/www/html/index.php 

COPY index.php /var/www/html/index.php

ビルドコマンド

# build image
$ docker build --pull --rm -t fpmtest:latest -f Dockerfile .  

# run container and forward port
$ docker run -p 9000:9000 -d fpmtest:latest

php-fpm 単体での動作確認

php-fpm への接続は unix ドメインソケットか TCP(デフォルト)で行う必要がある http 接続には対応していないため、php-fpm 単体では curl による動作確認は不可能。 curl の代わりに cgi-fcgi というコマンドで動作確認ができる。

# install for Mac OS
$ brew install fcgi

# request to php-fpm
$ SCRIPT_NAME=index.php\
  SCRIPT_FILENAME=/var/www/html/index.php\
  REQUEST_METHOD=GET\
  cgi-fcgi -bind -connect 127.0.0.1:9000
X-Powered-By: PHP/8.0.18
Content-type: text/html; charset=UTF-8

Hello World!%

(参考)fcgi コマンドでターミナルから php-fpm(FastCGI)の動作を確認する - Qiita

設定ファイル

php:8.1-fpm-alpine image では、phpphp-fpm の設定ファイルは以下の場所にある。

/var/www/html # ls /usr/local/etc/
pear.conf             php                   php-fpm.conf          php-fpm.conf.default  php-fpm.d

/usr/local/etc/php-fpm.d/www.conf に、9000 port で listen する設定などがある

(抜粋)

listen = 127.0.0.1:9000

2022年時点での今どきのPHP実行環境を調べた

2022/04/18 時点で PHP の実行環境を調べたので整理してメモ。 結論としては「nginx + php-fpm」の構成を使う。

Webサーバー

アクセス頻度が高く、負荷が気になるなら nginx。 逆にアクセス頻度が低く、安定性を重視するなら、使い果たされた枯れた技術という意味で apache も候補に出るだろう。

PHP 実行方式

PHP の実行方法はモジュール方式(mod_php)と CGI 方式の2つがある。

CGI 方式は Webサーバーとは別個のプロセスでプログラムを動かす方式。 毎回プログラムをメモリにロードする分、モジュール版よりも実行速度が劣っている。 一方、良いこともあり、CGI(方式の PHP)を動かすユーザーと Webサーバー本体を動かすユーザーを別にできるためセキュリティ面や CGI を動かすユーザー同士の干渉の問題が起こりにくい。

モジュール方式は、Webサーバーに組み込まれたモジュールを使って、Web サーバー本体のプロセスの中で PHP を実行する方式。

一昔前はセキュリティは現在よりも軽視されがちで、マシンリソースも十分ではなかったことから PHP を動かすといえば、 Apache + mod_php(モジュール方式)の構成がほとんどだった。

現在は CGI を改良した FastCGI の仕様が策定されており、PHP における FastCGI 実装として PHP: FastCGI Process Manager (FPM) - Manual がよく利用されている。

参考

grafana の https 化

前提:SSL 証明書を生成済み (参考)AWS で let's encrypt SSL を導入 - kmikmy's blog

grafana サーバは grafana ユーザで実行されるため、証明書のあるディレクトリの中身を辿れるようにしておく(+rは付与する必要はない)。

$ sudo chmod +x /etc/letsencrypt/live
$ sudo chmod +x /etc/letsencrypt/archive

/etc/grafana/grafana.ini で修正が必要なのは下記の項目

[server]
# Protocol (http, https, socket)
protocol = https
...
# The http port  to use
http_port = 443
...
# https certs & key file
cert_file = /etc/letsencrypt/live/ドメイン名/fullchain.pem
cert_key = /etc/letsencrypt/live/ドメイン名/privkey.pem

grafana-server の再起動を行って設定を反映させれば https 化完了。

$ sudo systemctl restart grafana-server

AWS で let's encrypt SSL を導入

準備として、Webサーバとして使うインスタンスのセキュリティグループで HTTP(80)、HTTPS(443) を開けておく。

(基本的にこちらを参照 Let's Encrypt の使い方 - Let's Encrypt 総合ポータル

certbot というプログラムを使って証明書を発行する。 certbot は epel リポジトリからインストールできるため、まずは epel リポジトリを有効にしてから certbot をインストールする。

$ sudo yum install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
$ sudo yum install certbot

証明書の取得。

$ sudo certbot certonly --standalone -d <domain>
インタラプト画面になる
<email address を登録>
<規約に同意: Y>
<ML に登録するか否か: Y/N>

以下のファイルが生成されれば成功

サーバ証明書(公開鍵)
/etc/letsencrypt/live/ドメイン名/cert.pem
中間証明書
/etc/letsencrypt/live/ドメイン名/chain.pem
サーバ証明書と中間証明書が結合されたファイル
/etc/letsencrypt/live/ドメイン名/fullchain.pem
秘密鍵
/etc/letsencrypt/live/ドメイン名/privkey.pem

その他

AWS のデフォルトドメインで証明書取ろうとしたらエラーになった。ちゃんとしたドメインでないと取れないらしい。 hacknote.jp

grafana を 80 番ポートで起動させる

root ユーザのプロセス以外で1024未満のポートをバインドしたい場合は、バインドしたいプログラムに特権を与える必要があるとのことだった。

(参考)[Linux] 一般ユーザのプロセスをポート1024番未満でBindする方法 – Roguer

grafana の場合は次のコマンドを実行する。

$ sudo setcap 'cap_net_bind_service=+ep' /usr/sbin/grafana-server

Configuration | Grafana Documentation より

あとは通常通り /etc/grafana/grafana.ini に指定する http ポート番号を 80 (default: 3000) に変更して、grafana-server を restart するだけ。