APNs 証明書の有効性を New Relic で監視する

この記事は、[New Relic Advent Calendar 2021](https://qiita.com/advent-calendar/2021/newrelic) 4日目の記事です。

APNs 証明書の有効性チェック結果を New Relic に送信して監視するシェルスクリプトを書きました。

## 要件

- 環境変数は `.env` ファイルにまとめる
- pem, pkcs#12 (p12) 両形式に対応する
- Revoke にも対応する
    - 有効期限を過ぎていなくても無効になる場合があることを考慮する
- 以下の形式で Event API を送信する

```
[
  {
    "eventType":"APNsCertificateCheck",
    "hostname":"ホスト名",
    "certfile":"チェックする証明書ファイルのパス",
    "valid":有効なら true,
    "daysRemaining":残り日数(数値)、期限を過ぎるとマイナスになる,
    "exitCode":curlコマンドのexit code,
    "errorMessage":"curlコマンドエラー時のメッセージ"
  }
]
```

## スクリプト

というわけで書いたスクリプトはこちら。

```shell
# .env ファイル
# アカウントIDの部分を埋めること
NEWRELIC_EVENT_API=https://insights-collector.newrelic.com/v1/accounts/(アカウントID)/events
# ライセンスキー
NEWRELIC_LICENSE_KEY=
# 証明書ファイルパス (.pem または .p12)
CERT_FILE=/tmp/sample.pem
# .p12 の展開に必要なパスワード .pem やパス無しの場合は空のままで OK
PKCS12_PASSWORD=
# 以下は Ubuntu 20.04 の場合 CentOS 7 の場合は /etc/ssl/certs/ca-bundle.crt
CA_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
# HTTP2 対応 curl のパス
CURL=/usr/bin/curl-7.79.1
```

```check_apns_cert.sh
#!/bin/sh

ENV_DIR=`dirname $0`
. $ENV_DIR/.env


validate_certificate()
{
    if [ $EXT = "pem" ]; then
        CURL_OPT="--cert $CERT_FILE --cacert $CA_CERT_FILE"
    elif [ $EXT = "p12" ]; then
        CURL_OPT="--cert-type P12 --cert $CERT_FILE:$PKCS12_PASSWORD --cacert $CA_CERT_FILE"
    else
        echo "Error: Unknown extension."
        exit 1
    fi

    CURL_RESULT=`$CURL -s -S https://api.development.push.apple.com $CURL_OPT 2>&1`
    CURL_EXIT_CODE=$?
    CURL_ERROR_MESSAGE=`echo $CURL_RESULT | sed -ne '/err/p'`

    if [ $CURL_EXIT_CODE -eq 0 ]; then
        echo "This certificate is valid."
        VALID=true
    else
        echo "This certificate is invalid."
        VALID=false
    fi
}

calc_days_remaining()
{
    if [ $EXT = "pem" ]; then
        END_DATE=`openssl x509 -enddate -noout -in $CERT_FILE | cut -d= -f 2`
    elif [ $EXT = "p12" ]; then
        END_DATE=`openssl pkcs12 -password pass:$PKCS12_PASSWORD -in $CERT_FILE -nokeys | openssl x509 -noout -enddate | cut -d= -f 2`
    else
        echo "Error: Unknown extension."
        exit 1
    fi
    DAYS_REMAINING=`expr \( \`date --date "$END_DATE" +%s\` - \`date +%s\` \) / 86400`
}

send_newrelic_event()
{
    cat << EOS | gzip -c | $CURL -X POST --cacert $CA_CERT_FILE -H "Content-Type: application/json" -H "Api-Key: $NEWRELIC_LICENSE_KEY" -H "Content-Encoding: gzip" $NEWRELIC_EVENT_API --data-binary @-
[
  {
    "eventType":"APNsCertificateCheck",
    "hostname":"`hostname`",
    "certfile":"$CERT_FILE",
    "valid":$VALID,
    "daysRemaining":$DAYS_REMAINING,
    "exitCode":$CURL_EXIT_CODE,
    "errorMessage":"$CURL_ERROR_MESSAGE"
  }
]
EOS
}


echo "SSL Certificate Check Start"

EXT=${CERT_FILE##*.}

validate_certificate

calc_days_remaining

send_newrelic_event

echo "SSL Certificate Check End"
```


## 使い方

APNs 証明書のあるサーバに `.env` と `check_apns_cert.sh` を同じディレクトリに置いて `cron` などで定期実行します。`.env` の設定は適宜変更して下さい。
実行ユーザは証明書が読めるユーザなら何でも良いです。

```shell
# /etc/crontab
# 毎時 01 分に実行
01 * * * * root /opt/check_apns_cert/check_apns_cert.sh
```


## 説明

スクリプトを書く上でちょっと考えたポイントを説明します。

### validate_certificate

Apple のサーバに接続してみて証明書の有効性を確認する関数です。

実際に接続してみることで有効期限切れだけでなく Revoke された場合のエラーも確認できます。`curl` の出力は `err` を含む文字のみ抽出しています。もし有効期限が切れていた場合は以下のようなエラーとなります。

```
curl: (56) OpenSSL SSL_read: error:14094415:SSL routines:ssl3_read_bytes:sslv3 alert certificate expired, errno 0
```

証明書のチェックには `curl` を使います。APNs は HTTP2 対応が必須ですから HTTP2 に対応した curl を使います。（バージョン 7.43 以降）

例えば CentOS 7 の場合、標準の `curl` では対応していません。別の `curl` を使いましょう。

```shell
# HTTP2 に対応した curl のダウンロード
curl -o curl-7.79.1 -L https://github.com/moparisthebest/static-curl/releases/download/v7.79.1/curl-amd64
chmod +x curl-7.79.1
```

実際に接続してのチェックは `openssl s_client ...` をコマンドを使うこともできるのですが、以下の理由から `curl` を使っています。

- openssl 1.0.2 と 1.1.1 で出力の挙動が異なる
    - 1.0.2 では接続後必ず入力を待つが、1.1.1 ではエラーがあると即切断される
- 証明書が PEM または DER 形式でないと NG
    - 変換の工夫が必要になってしまう
    - `curl` の場合は PKCS#12 も指定できる


### calc_days_remaining

有効期限の残り日数を計算する関数です。

有効期限の終わりの日は `openssl x509 -enddate` で取得できます。これは以下のような形式です。`notAfter=` 以降の文字列はそのまま `date` に食わせることができます。

```
notAfter=Nov 28 23:59:59 2022 GMT
```

pkcs#12 ではこのような出力を得るコマンドがないため pem (x509) 形式に変換するコマンドをパイプしています。


### send_newrelic_event

New Relic Event API で結果をイベントとして送信する関数です。

Event API のドキュメントはこちらです。
[Introduction to the Event API | New Relic Documentation](https://docs.newrelic.com/docs/data-apis/ingest-apis/introduction-event-api/)

このスクリプトで送信した内容は以下の NRQL で確認できます。

```
FROM APNsCertificateCheck SELECT *
```


## New Relic Alert で監視

Alert policy では以下 2 つの NRQL で監視するようにしました。`hostname` での絞り込みは必要に応じて変えてください。

- `valid` が false の場合（＝証明書の有効性が切れた場合）は critical アラートを出す

```
FROM APNsCertificateCheck SELECT count(*) WHERE valid IS FALSE AND hostname = '特定のホスト名'
```

- `daysRemaining` が閾値を下回った場合（＝証明書の有効期限が近づいてきた場合）は critical / warning アラートを出す

```
FROM APNsCertificateCheck SELECT latest(daysRemaining) WHERE hostname = '特定のホスト名'
```

---

## 補足：普通の SSL 証明書監視の場合

HTTPS で使う普通の SSL 証明書を監視したい場合は New Relic Synthetics の SSL 証明書監視を使いましょう。ポチポチするだけで簡単に SSL 証明書を監視できます。

![SSL 証明書監視](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2318198/ed7aedba-f675-91f2-6d53-75027a4521db.png)