アットウィキロゴ

[AWS]ACMのエクスポート証明書によるSSLサーバ証明書自動更新

AWS ACMのエクスポートパブリック証明書をAWS以外の外部WEBサーバで自動更新します。

1. SSLサーバ証明書の構成

(1) 仕様
対象ドメイン名 *.example.jp / example.jp(1枚のSAN証明書として発行)
認証種別 DV認証
認証方式 DNS認証
(2) 証明書ファイル構成
ファイル名 用途
/etc/httpd/certs/example.jp/acm/example.jp.privkey_nopwd.pem 秘密鍵(パスフレーズなし)
/etc/httpd/certs/example.jp/acm/example.jp.inca.pem 中間CA証明書
/etc/httpd/certs/example.jp/acm/example.jp.cert.pem サーバ証明書
/etc/httpd/certs/example.jp/acm/example.jp.full_cert.pem サーバ証明書 + 中間CA証明書

2. AWSコンソールでの作業

(1) AWS Certificate Manager (ACM)でのSSLサーバ証明書のリクエスト
  • 「Certificate Manager」に移動します。
  • 「証明書をリクエスト」をクリックします。
[a] 証明書をリクエスト
  • 「パブリック証明書をリクエスト」を選択して「次へ」をクリックします。
[b] パブリック証明書をリクエスト
必ずエクスポートの許可を有効化します。
ドメイン名 *.example.jp
example.jp
エクスポートを許可 エクスポートを有効にする
検証方法 DNS 検証
キーアルゴリズム RSA 2048
  • 「リクエスト」をクリックします。
[c] ドメインの検証
ドメインの検証を行います。
■ Route 53の場合
  • 「Route 53 でレコードを作成」をクリックします。
  • 「Amazon Route 53 で DNS レコードを作成」画面で「レコードを作成」をクリックします。
■ その他のDNSの場合
  • 「CSV にエクスポート」をクリックします。
  • ダウンロードした「DNS_設定.csv」の内容を該当のDNSに設定します。

ステータスが「発行済み」になればOKです。
  • 証明書の「ARN」をメモしておきます。
(2) IAMポリシーの作成
対象証明書のDescribeとExportのみ許可する最小権限のポリシーを作成します。
  • 「IAM」に移動します。
  • ローカルメニューから「ポリシー」をクリックします。
  • 「ポリシーの作成」をクリックします。
  • 「ポリシーエディタ」を「JSON」にします。
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ACMExportSpecificCertificate",
      "Effect": "Allow",
      "Action": [
        "acm:DescribeCertificate",
        "acm:ExportCertificate"
      ],
      "Resource": "arn:aws:acm:ap-northeast-1:123456789012:certificate/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
    }
  ]
} 
「Resource」にACM証明書の「ARN」を差し替えて下さい。
  • 「次へ」をクリックします。
ポリシー名 ACMExport_example.jp_Policy
説明 – オプション ACM Exportable Certificate Policy
「ポリシー名」のドメイン名は実際のドメインに差し替えて下さい。
  • 「ポリシーの作成」をクリックします。
(3) IAMユーザーの作成
  • ローカルメニューから「IAM ユーザー」をクリックします。
  • 「ユーザーの作成」をクリックします。
ユーザー名 acm-export-user_example.jp
「ユーザー名」のドメイン名は実際のドメインに差し替えて下さい。
  • 「次へ」をクリックします。
  • 「ポリシーを直接アタッチする」を選択します。
  • 「ACMExport_example.jp_Policy」にチェックを付けます。
  • 「次へ」をクリックします。
  • 「ユーザーの作成」をクリックします。
  • 作成したユーザー名をクリックします。
  • 「セキュリティ認証情報」-「アクセスキー」で「アクセスキーを作成」をクリックします。
コマンドラインインターフェイス (CLI)
上記のレコメンデーションを理解し、アクセスキーを作成します。
  • 「次へ」をクリックします。
  • 「アクセスキーを作成」をクリックします。
  • 「.csv ファイルをダウンロード」をクリックして、アクセスキー情報をダウンロードします。

2. AWS CLI v2の導入

(1) インストール
AWS CLI v2のインストールを行います。
# cd /usr/local/src
# curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
# unzip awscliv2.zip
# ./aws/install
# /usr/local/bin/aws --version
(2) プロファイルの作成
ACMのSSLサーバ証明書をエクスポートするプロファイルを作成します。
プロファイル名 acm-export_example.jp
ドメイン名は実際のドメイン名を差し替えて下さい。
# aws configure --profile  acm-export_example.jp
「プロファイル名」のドメイン名は実際のドメインに差し替えて下さい。
ASW Access Key ID: (アクセスキー)
ASW Secret Access Key(シークレットアクセスキー)
Default region name:ap-northeast-1
Default output format:json

3. 証明書設置の準備作業

ここで記載している「exmaple.jp」は実際のドメイン名に差し替えて下さい。
(1) 証明書ディレクトリの作成
証明書ディレクトリ /etc/httpd/certs/exampel.jp/acm/
# mkdir -p /etc/httpd/certs/example.jp/acm
(2) ACMエクスポート用パスフレーズファイルの作成
パスフレーズファイル名 /root/.acm/example.jp/.acm_export_passphrase
# mkdir -p /root/.acm/example.jp
# openssl rand -base64 48 > /root/.acm/example.jp/.acm_export_passphrase
# chmod 600 /root/.acm/example.jp/.acm_export_passphrase
(3) jqコマンドのインストール
# dnf install jq

4. 初回エクスポートスクリプト

初回のACM証明書をエクスポートしてサーバに設置を行うスクリプトです。
# vi /root/.acm/example.jp/acm-export.sh
パス名のドメインは実際のドメイン名に書き換えて下さい。
#!/usr/bin/env bash

set -Eeuo pipefail
umask 077
 
############################################################
# 基本設定
############################################################
DOMAIN="${DOMAIN:-example.jp}"
AWS_REGION="${AWS_REGION:-ap-northeast-1}"
CERT_ARN="${CERT_ARN:-arn:aws:acm:ap-northeast-1:123456789012:certificate/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}"
 
# 証明書に含まれていることを確認するホスト名。
# 必要に応じて "www.example.jp" などを追加してください。
EXPECTED_HOSTS=(
  "example.jp"
)
 
############################################################
# 証明書・作業ファイル
############################################################
CERT_DIR="${CERT_DIR:-/etc/httpd/certs/${DOMAIN}/acm}"
PASSPHRASE_FILE="${PASSPHRASE_FILE:-/root/.acm/${DOMAIN}/.acm_export_passphrase}"
WORK_BASE="${WORK_BASE:-/root/acm-export}"
 
PRIVKEY_FILE="${CERT_DIR}/${DOMAIN}.privkey.pem"
PRIVKEY_NOPWD_FILE="${CERT_DIR}/${DOMAIN}.privkey_nopwd.pem"
INCA_FILE="${CERT_DIR}/${DOMAIN}.inca.pem"
CERT_FILE="${CERT_DIR}/${DOMAIN}.cert.pem"
FULL_CERT_FILE="${CERT_DIR}/${DOMAIN}.full_cert.pem"
 
LOG_DIR="${LOG_DIR:-/var/log/acm}"
LOG_FILE="${LOG_FILE:-${LOG_DIR}/acm_export.log}"
 
SAFE_DOMAIN="${DOMAIN//[^[:alnum:]._-]/_}"
LOCK_FILE="${LOCK_FILE:-/run/lock/acm_export_${SAFE_DOMAIN}.lock}"
 
############################################################
# コマンド
############################################################
AWS_BIN="${AWS_BIN:-/usr/local/bin/aws}"
AWS_PROFILE="${AWS_PROFILE:-acm-export_${DOMAIN}}"  # 未使用の場合は空文字にする
JQ="${JQ:-/usr/bin/jq}"
OPENSSL="${OPENSSL:-/usr/bin/openssl}"
APACHECTL="${APACHECTL:-/usr/sbin/apachectl}"
SYSTEMCTL="${SYSTEMCTL:-/usr/bin/systemctl}"
SHA256SUM="${SHA256SUM:-/usr/bin/sha256sum}"
FLOCK="${FLOCK:-/usr/bin/flock}"
STAT="${STAT:-/usr/bin/stat}"
CMP="${CMP:-/usr/bin/cmp}"
CA_BUNDLE="${CA_BUNDLE:-/etc/pki/tls/certs/ca-bundle.crt}"
RESTORECON="${RESTORECON:-/usr/sbin/restorecon}"
 
# 新しい証明書が24時間以内に期限切れになる場合は配置しない。
MIN_VALID_SECONDS="${MIN_VALID_SECONDS:-86400}"
 
############################################################
# 実行時変数
############################################################
TMP_DIR=""
BACKUP_DIR=""
ROLLBACK_REQUIRED=0
STAGE_SUFFIX=".new.$$"
 
TARGET_FILES=(
  "${PRIVKEY_FILE}"
  "${PRIVKEY_NOPWD_FILE}"
  "${INCA_FILE}"
  "${CERT_FILE}"
  "${FULL_CERT_FILE}"
)
 
############################################################
# 共通関数
############################################################
log() {
  local msg="$1"
  local now
  now="$(date '+%Y-%m-%d %H:%M:%S')"
 
  if [[ -d "${LOG_DIR}" ]]; then
    printf '[%s] %s\n' "${now}" "${msg}" | tee -a "${LOG_FILE}"
  else
    printf '[%s] %s\n' "${now}" "${msg}" >&2
  fi
}
 
die() {
  log "ERROR: $*"
  exit 1
}
 
run_and_log() {
  "$@" >>"${LOG_FILE}" 2>&1
}
 
aws_cmd() {
  [[ -x "${AWS_BIN}" ]] || die "AWS CLIを実行できません: ${AWS_BIN}"
 
  if [[ -n "${AWS_PROFILE}" ]]; then
    "${AWS_BIN}" --profile "${AWS_PROFILE}" "$@"
  else
    "${AWS_BIN}" "$@"
  fi
}
 
restore_selinux_contexts() {
  if [[ -x "${RESTORECON}" ]]; then
    "${RESTORECON}" -RF "${CERT_DIR}" >>"${LOG_FILE}" 2>&1 || \
      log "WARNING: restoreconに失敗しました。SELinuxコンテキストを確認してください。"
  fi
}
 
rollback_files() {
  local target base
 
  [[ -n "${BACKUP_DIR}" && -d "${BACKUP_DIR}" ]] || return 0
 
  log "旧証明書ファイルへロールバックします..."
 
  for target in "${TARGET_FILES[@]}"; do
    base="$(basename -- "${target}")"
 
    if [[ -e "${BACKUP_DIR}/${base}" ]]; then
      cp -a -- "${BACKUP_DIR}/${base}" "${target}"
    else
      rm -f -- "${target}"
    fi
  done
 
  restore_selinux_contexts
}
 
cleanup() {
  local rc=$?
  local target
 
  set +e
 
  if (( ROLLBACK_REQUIRED == 1 )); then
    rollback_files
  fi
 
  for target in "${TARGET_FILES[@]}"; do
    rm -f -- "${target}${STAGE_SUFFIX}"
  done
 
  if [[ -n "${TMP_DIR}" && -d "${TMP_DIR}" ]]; then
    rm -rf -- "${TMP_DIR}"
  fi
 
  if (( rc != 0 )); then
    log "ACM証明書エクスポート処理が異常終了しました(終了コード: ${rc})。"
  fi
 
  trap - EXIT
  exit "${rc}"
}
 
trap cleanup EXIT
 
############################################################
# 事前チェック
############################################################
check_executable() {
  local command_path="$1"
  local command_name="$2"
  [[ -x "${command_path}" ]] || die "${command_name}を実行できません: ${command_path}"
}
 
check_passphrase_file() {
  local owner_uid mode_text mode_value size line_count
 
  [[ -f "${PASSPHRASE_FILE}" ]] || \
    die "パスフレーズファイルがありません: ${PASSPHRASE_FILE}"
 
  owner_uid="$(${STAT} -c '%u' "${PASSPHRASE_FILE}")"
  [[ "${owner_uid}" == "0" ]] || \
    die "パスフレーズファイルの所有者をrootにしてください: ${PASSPHRASE_FILE}"
 
  mode_text="$(${STAT} -c '%a' "${PASSPHRASE_FILE}")"
  mode_value=$((8#${mode_text}))
  (( (mode_value & 077) == 0 )) || \
    die "パスフレーズファイルをroot以外から読めない権限にしてください(推奨: chmod 600): 現在 ${mode_text}"
 
  size="$(${STAT} -c '%s' "${PASSPHRASE_FILE}")"
  (( size >= 4 && size <= 128 )) || \
    die "パスフレーズは4~128バイトにしてください: 現在 ${size} バイト"
 
  # AWS公式仕様により、パスフレーズファイルは改行で終端してはいけない。
  line_count="$(wc -l < "${PASSPHRASE_FILE}")"
  (( line_count == 0 )) || \
    die "パスフレーズファイルに改行が含まれています。printfで改行なしで作成してください。"
 
  if LC_ALL=C grep -q $'\r' "${PASSPHRASE_FILE}"; then
    die "パスフレーズファイルにCR文字が含まれています。"
  fi
 
  if LC_ALL=C grep -q '[^ -~]' "${PASSPHRASE_FILE}"; then
    die "パスフレーズには印字可能なASCII文字だけを使用してください。"
  fi
 
  if grep -q '[#$%]' "${PASSPHRASE_FILE}"; then
    die "パスフレーズにはAWS ACMで使用できない文字(#、$、%)が含まれています。"
  fi
}
 
check_requirements() {
  [[ "${EUID}" -eq 0 ]] || die "rootユーザーで実行してください。"
 
  mkdir -p "${WORK_BASE}"
  mkdir -p "${CERT_DIR}"
  mkdir -p "${LOG_DIR}"
  chmod 700 "${WORK_BASE}" "${CERT_DIR}"
  chmod 750 "${LOG_DIR}"
  touch "${LOG_FILE}"
  chmod 600 "${LOG_FILE}"
 
  check_executable "${AWS_BIN}" "AWS CLI"
  check_executable "${JQ}" "jq"
  check_executable "${OPENSSL}" "OpenSSL"
  check_executable "${APACHECTL}" "apachectl"
  check_executable "${SYSTEMCTL}" "systemctl"
  check_executable "${SHA256SUM}" "sha256sum"
  check_executable "${FLOCK}" "flock"
  check_executable "${STAT}" "stat"
  check_executable "${CMP}" "cmp"
 
  [[ -r "${CA_BUNDLE}" ]] || die "CAバンドルを読み込めません: ${CA_BUNDLE}"
 
  check_passphrase_file
 
  # 同時実行を防止する。
  exec 9>"${LOCK_FILE}"
  "${FLOCK}" -n 9 || die "同じ証明書のエクスポート処理がすでに実行中です。"
 
  "${SYSTEMCTL}" is-active --quiet httpd || \
    die "httpdが起動していません。証明書配置前にhttpdの状態を確認してください。"
 
  TMP_DIR="$(mktemp -d "${WORK_BASE}/tmp_${SAFE_DOMAIN}.XXXXXXXX")"
}
 
############################################################
# ACM証明書の確認とエクスポート
############################################################
check_acm_certificate() {
  local metadata status cert_type export_option key_origin acm_domain
  local metadata_file="${TMP_DIR}/describe.json"
  local error_file="${TMP_DIR}/describe.err"
 
  log "ACM証明書の状態を確認します..."
 
  if ! aws_cmd acm describe-certificate \
      --region "${AWS_REGION}" \
      --certificate-arn "${CERT_ARN}" \
      >"${metadata_file}" 2>"${error_file}"; then
    while IFS= read -r line; do log "AWS CLI: ${line}"; done <"${error_file}"
    die "ACM証明書情報の取得に失敗しました。"
  fi
 
  status="$(${JQ} -er '.Certificate.Status' "${metadata_file}")"
  cert_type="$(${JQ} -er '.Certificate.Type' "${metadata_file}")"
  export_option="$(${JQ} -r '.Certificate.Options.Export // ""' "${metadata_file}")"
  key_origin="$(${JQ} -r '.Certificate.CertificateKeyPairOrigin // ""' "${metadata_file}")"
  acm_domain="$(${JQ} -er '.Certificate.DomainName' "${metadata_file}")"
 
  [[ "${status}" == "ISSUED" ]] || \
    die "ACM証明書の状態がISSUEDではありません: ${status}"
 
  [[ "${cert_type}" != "IMPORTED" ]] || \
    die "インポート証明書の秘密鍵はACMから再エクスポートできません。"
 
  [[ "${key_origin}" != "ACME" ]] || \
    die "ACM ACMEで発行した証明書はExportCertificate APIの対象外です。"
 
  if [[ "${cert_type}" == "AMAZON_ISSUED" && "${export_option}" != "ENABLED" ]]; then
    die "ACMパブリック証明書でエクスポートが有効になっていません: Options.Export=${export_option:-未設定}"
  fi
 
  log "ACM証明書: DomainName=${acm_domain}, Type=${cert_type}, Status=${status}, Export=${export_option:-N/A}"
}
 
export_certificate() {
  local export_file="${TMP_DIR}/export.json"
  local error_file="${TMP_DIR}/export.err"
 
  log "ACMから証明書をエクスポートします..."
 
  if ! aws_cmd acm export-certificate \
      --region "${AWS_REGION}" \
      --certificate-arn "${CERT_ARN}" \
      --passphrase "fileb://${PASSPHRASE_FILE}" \
      >"${export_file}" 2>"${error_file}"; then
    while IFS= read -r line; do log "AWS CLI: ${line}"; done <"${error_file}"
    die "ACM証明書のエクスポートに失敗しました。"
  fi
 
  "${JQ}" -er '.Certificate | select(type == "string" and length > 0)' \
    "${export_file}" >"${TMP_DIR}/${DOMAIN}.cert.pem" || \
    die "エクスポート結果にCertificateがありません。"
 
  "${JQ}" -er '.CertificateChain | select(type == "string" and length > 0)' \
    "${export_file}" >"${TMP_DIR}/${DOMAIN}.inca.pem" || \
    die "エクスポート結果にCertificateChainがありません。"
 
  "${JQ}" -er '.PrivateKey | select(type == "string" and length > 0)' \
    "${export_file}" >"${TMP_DIR}/${DOMAIN}.privkey.pem" || \
    die "エクスポート結果にPrivateKeyがありません。"
 
  cat "${TMP_DIR}/${DOMAIN}.cert.pem" \
      "${TMP_DIR}/${DOMAIN}.inca.pem" \
      >"${TMP_DIR}/${DOMAIN}.full_cert.pem"
 
  chmod 600 "${TMP_DIR}"/*
}
 
############################################################
# PEMファイルの検証
############################################################
public_key_sha256_from_cert() {
  local cert_path="$1"
  local hash_line
 
  if ! hash_line="$(
    "${OPENSSL}" x509 -in "${cert_path}" -pubkey -noout \
      | "${OPENSSL}" pkey -pubin -outform DER 2>/dev/null \
      | "${SHA256SUM}"
  )"; then
    return 1
  fi
 
  [[ -n "${hash_line}" ]] || return 1
  printf '%s\n' "${hash_line%% *}"
}
 
public_key_sha256_from_key() {
  local key_path="$1"
  local hash_line
 
  if ! hash_line="$(
    "${OPENSSL}" pkey -in "${key_path}" -pubout -outform DER 2>/dev/null \
      | "${SHA256SUM}"
  )"; then
    return 1
  fi
 
  [[ -n "${hash_line}" ]] || return 1
  printf '%s\n' "${hash_line%% *}"
}
 
validate_exported_files() {
  local cert_tmp="${TMP_DIR}/${DOMAIN}.cert.pem"
  local inca_tmp="${TMP_DIR}/${DOMAIN}.inca.pem"
  local encrypted_key_tmp="${TMP_DIR}/${DOMAIN}.privkey.pem"
  local nopwd_key_tmp="${TMP_DIR}/${DOMAIN}.privkey_nopwd.pem"
  local cert_pub_sha key_pub_sha host
 
  log "エクスポートした証明書と秘密鍵を検証します..."
 
  "${OPENSSL}" x509 -in "${cert_tmp}" -noout >/dev/null 2>&1 || \
    die "サーバ証明書をPEM形式として読み込めません。"
 
  # 暗号化秘密鍵の復号には鍵種別に依存しないopenssl pkeyを使用する。
  "${OPENSSL}" pkey \
    -in "${encrypted_key_tmp}" \
    -passin "file:${PASSPHRASE_FILE}" \
    -out "${nopwd_key_tmp}" \
    >/dev/null 2>>"${LOG_FILE}" || \
    die "秘密鍵の復号に失敗しました。パスフレーズを確認してください。"
 
  chmod 600 "${nopwd_key_tmp}"
 
  "${OPENSSL}" pkey -in "${nopwd_key_tmp}" -check -noout \
    >>"${LOG_FILE}" 2>&1 || die "秘密鍵の整合性チェックに失敗しました。"
 
  if ! "${OPENSSL}" x509 -in "${cert_tmp}" -checkend "${MIN_VALID_SECONDS}" -noout \
      >>"${LOG_FILE}" 2>&1; then
    die "新しい証明書が${MIN_VALID_SECONDS}秒以内に期限切れになるため配置しません。"
  fi
 
  for host in "${EXPECTED_HOSTS[@]}"; do
    [[ -n "${host}" ]] || continue
    if ! "${OPENSSL}" x509 -in "${cert_tmp}" -checkhost "${host}" -noout \
        >>"${LOG_FILE}" 2>&1; then
      die "証明書のSAN/CNに想定ホスト名が含まれていません: ${host}"
    fi
  done
 
  cert_pub_sha="$(public_key_sha256_from_cert "${cert_tmp}")"
  key_pub_sha="$(public_key_sha256_from_key "${nopwd_key_tmp}")"
 
  [[ -n "${cert_pub_sha}" && "${cert_pub_sha}" == "${key_pub_sha}" ]] || \
    die "証明書と秘密鍵の公開鍵が一致しません。"
 
  if ! "${OPENSSL}" verify \
      -CAfile "${CA_BUNDLE}" \
      -untrusted "${inca_tmp}" \
      "${cert_tmp}" >>"${LOG_FILE}" 2>&1; then
    die "証明書チェーンの検証に失敗しました。"
  fi
 
  while IFS= read -r line; do
    log "証明書情報: ${line}"
  done < <("${OPENSSL}" x509 -in "${cert_tmp}" -noout \
            -subject -issuer -serial -dates -fingerprint -sha256)
}
 
certificate_is_unchanged() {
  local cert_tmp="${TMP_DIR}/${DOMAIN}.cert.pem"
  local inca_tmp="${TMP_DIR}/${DOMAIN}.inca.pem"
  local full_tmp="${TMP_DIR}/${DOMAIN}.full_cert.pem"
  local new_pub_sha current_key_sha
  local target
 
  for target in "${TARGET_FILES[@]}"; do
    [[ -s "${target}" ]] || return 1
  done
 
  "${CMP}" -s "${cert_tmp}" "${CERT_FILE}" || return 1
  "${CMP}" -s "${inca_tmp}" "${INCA_FILE}" || return 1
  "${CMP}" -s "${full_tmp}" "${FULL_CERT_FILE}" || return 1
 
  new_pub_sha="$(public_key_sha256_from_cert "${cert_tmp}")" || return 1
  current_key_sha="$(public_key_sha256_from_key "${PRIVKEY_NOPWD_FILE}")" || return 1
 
  [[ "${new_pub_sha}" == "${current_key_sha}" ]]
}
 
############################################################
# バックアップ・配置・Apache反映
############################################################
backup_current_files() {
  local target
 
  BACKUP_DIR="${WORK_BASE}/backup_${SAFE_DOMAIN}_$(date +%Y%m%d_%H%M%S)"
  mkdir -m 700 "${BACKUP_DIR}"
 
  log "既存の証明書をバックアップします: ${BACKUP_DIR}"
 
  for target in "${TARGET_FILES[@]}"; do
    if [[ -e "${target}" ]]; then
      cp -a -- "${target}" "${BACKUP_DIR}/"
    fi
  done
}
 
stage_and_install_files() {
  local src target mode
 
  log "証明書ファイルを一時名で配置します..."
 
  while IFS='|' read -r src target mode; do
    install -o root -g root -m "${mode}" "${src}" "${target}${STAGE_SUFFIX}"
  done <<EOF_STAGE
${TMP_DIR}/${DOMAIN}.privkey.pem|${PRIVKEY_FILE}|600
${TMP_DIR}/${DOMAIN}.privkey_nopwd.pem|${PRIVKEY_NOPWD_FILE}|600
${TMP_DIR}/${DOMAIN}.inca.pem|${INCA_FILE}|644
${TMP_DIR}/${DOMAIN}.cert.pem|${CERT_FILE}|644
${TMP_DIR}/${DOMAIN}.full_cert.pem|${FULL_CERT_FILE}|644
EOF_STAGE
 
  # ここから先で失敗した場合、EXITトラップで旧ファイルへ戻す。
  ROLLBACK_REQUIRED=1
 
  for target in "${TARGET_FILES[@]}"; do
    mv -f -- "${target}${STAGE_SUFFIX}" "${target}"
  done
 
  restore_selinux_contexts
}
 
reload_apache() {
  log "Apache設定を確認します..."
 
  if ! run_and_log "${APACHECTL}" configtest; then
    die "apachectl configtestに失敗しました。旧証明書へ戻します。"
  fi
 
  log "Apacheをreloadします..."
 
  if ! run_and_log "${SYSTEMCTL}" reload httpd; then
    log "ERROR: httpdのreloadに失敗しました。旧証明書へ戻します。"
    rollback_files
    ROLLBACK_REQUIRED=0
 
    # reloadが一部実行されていた場合に備えて、旧証明書で再度reloadを試す。
    if run_and_log "${APACHECTL}" configtest && run_and_log "${SYSTEMCTL}" reload httpd; then
      log "旧証明書への復旧とApacheのreloadが完了しました。"
    else
      log "CRITICAL: 旧証明書へ戻しましたがApacheのreloadに失敗しました。手動確認が必要です。"
    fi
 
    die "Apacheへの新証明書反映に失敗しました。"
  fi
 
  ROLLBACK_REQUIRED=0
}
 
############################################################
# メイン処理
############################################################
main() {
  log "========== ACM証明書エクスポート開始 =========="
 
  check_requirements
  check_acm_certificate
  export_certificate
  validate_exported_files
 
  if certificate_is_unchanged; then
    chown root:root "${TARGET_FILES[@]}"
    chmod 600 "${PRIVKEY_FILE}" "${PRIVKEY_NOPWD_FILE}"
    chmod 644 "${INCA_FILE}" "${CERT_FILE}" "${FULL_CERT_FILE}"
    restore_selinux_contexts
 
    log "ACM上の証明書は現在の配置済み証明書と同一です。Apacheのreloadは行いません。"
    log "========== ACM証明書エクスポート正常終了 =========="
    return 0
  fi
 
  backup_current_files
  stage_and_install_files
  reload_apache
 
  log "ACM証明書の配置とApacheへの反映が完了しました。"
  log "バックアップ先: ${BACKUP_DIR}"
  log "========== ACM証明書エクスポート正常終了 =========="
}
 
main "$@"

■基本設定
基本設定の箇所は実際の環境に合わせて書き換えて下さい。
DOMAIN="${DOMAIN:-example.jp}"
AWS_REGION="${AWS_REGION:-ap-northeast-1}"
CERT_ARN="${CERT_ARN:-arn:aws:acm:ap-northeast-1:123456789012:certificate/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}"
 
# 証明書に含まれていることを確認するホスト名。
# 必要に応じて "www.example.jp" などを追加してください。
EXPECTED_HOSTS=(
  "example.jp"
) 
最終更新:2026年07月30日 13:23