【OCI】Oracle Linux 8 で Let's encrypt の証明書を発行する
作業の流れ
全体の流れは、以下の通り
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
PHP で Oracle Autonomous Database に接続する
Oracle Cloud の手順 Build a PHP Application に沿って進める。
流れとしては以下のような感じ。
- PHP のダウンロード&インストール
- Oracle Instant Client のダウンロード&インストール
- PHP OCI8 のダウンロード&インストール
- 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:
# 接続するデータベースの設定ファイルの場所として 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>
PHP で Oracle DB に接続するサンプルコード:test.php(
<?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) : " ") . "</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)に以下を許可するルールを追加するか、新規セキュリティリストを作ってサブネットに紐付ける。
(参考)ロード・バランシングの開始
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) で不要なものを削除する
なお、本当は 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 では、php や php-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 するだけ。