blog

DeNAのエンジニアが考えていることや、担当しているサービスについて情報発信しています

2023.07.25 技術記事

ローカル SSL 証明書の有効期限管理

by Yuya Nakamura

#infrastructure #sre #ad-cs #ssl #powershell #情シス

はじめに

こんにちは。IT本部IT戦略部システム基盤グループの中村です。

DeNA では社内向けの WEB サービスを多数運用しています。 そうした WEB サービスには社内のプライベート証明機関(以下 CA と表記)が発行したローカル SSL 証明書を使っています。 ご存知の通り SSL 証明書は1年程度の有効期限があるので、それぞれのサービスで定期的に SSL 証明書の更新が必要になります。

しかし、発行した証明書の期限管理については、各サービスを運用する部署(利用者)側で行う運用になっていたため、度々証明書の期限切れが発生していました。

今回は、ローカル SSL 証明書の期限管理を一括で行うために構築したシステムをご紹介します。

ローカル SSL 証明書管理者が証明書期限の一括管理をするときに、参考になれば嬉しいです。

構築したシステムの概要

このシステムを構築するにあたり、必要な情報はすべて git 管理することにしました。

git 管理にした理由は以下の通りです。

  • 変更履歴の管理が楽
    • 誰がどんな変更を行ったか等
  • 構築したシステムの外から編集できる
    • 慣れている git 操作で情報を編集できる

git 管理している情報は以下の通りです。

  • CN(Common Name, Web サーバの FQDN)
  • 連絡先メールアドレス
  • 有効期限
  • 更新要否

これらの情報を csv ファイルで管理し、日次で以下の処理を自動で行っています。

  • 期限が近いものは利用者にメールで通知
  • 期限まで一週間のものは証明書管理者にもメールで通知
  • CA の情報を基に csv を更新
    • csv に存在しない証明書があれば csv に追加
      • 連絡先は空欄
    • 有効期限に変化がある場合(証明書更新があった場合)は csv 側の有効期限を更新して更新要否フラグを要更新に設定する
    • 期限切れとなった証明書情報は csv から削除
      • 要更新のフラグがたっている場合は期限が切れたことを利用者と証明書管理者にメールで通知
  • 連絡先が空欄のものがある場合は証明書管理者にメールで通知
    • 通知メールを受け取ったら、証明書管理者が連絡先を確認して、 csv を編集して連絡先を追記する

overview

概要図

証明書更新フローは下図の通りです。

flow

利用者視点の更新フロー

システム詳細

ここからは各処理の詳細をご紹介します。

証明書発行

利用者から証明書発行申請が上がってきたら、証明書管理者は PowerShell スクリプトを用いて証明書を発行します。

このスクリプトは以下のように CN と連絡先メールアドレスが必須引数となっていて、発行後に自動的に csv を更新するようになっています。

PS> New-Certificate.ps1 -CommonName <CN> -Group <MailAddress>

このスクリプトを使わずに証明書を発行した場合は、翌日の日次処理で自動的に csv に追加されますが、連絡先メールアドレスを特定できないため、 csv への追加と同時に証明書管理者に通知メールが送信されます。

日次処理

メール通知処理

メール通知には Send-MailMessage を利用しています。

件名や本文に日本語を用いる場合、文字列をそのまま渡すと文字化けが発生するため、今回は UTF8 の文字列を渡し、 -Encoding オプションで UTF8 であることを明示しました。

具体的には以下のような function を用いてメールを送信しています。

$SmtpServer = 'SMTP SERVER ADDRESS'
function Send-Mail {
    Param(
        [parameter(Mandatory=$True)][string]$To,
        [string]$Cc,
        [parameter(Mandatory=$True)][string]$Subject,
        [parameter(Mandatory=$True)][string]$Body,
        [string]$From
    )
    if ([string]::IsNullOrEmpty($Cc)) {
        Send-MailMessage -From $From -To $To -Subject ($Subject | Get-UTF8String) -Body ($Body | Get-UTF8String) -SmtpServer $SmtpServer -Encoding ([System.Text.Encoding]::UTF8)
    } else {
        Send-MailMessage -From $From -To $To -Cc $Cc -Subject ($Subject | Get-UTF8String) -Body ($Body | Get-UTF8String) -SmtpServer $SmtpServer -Encoding ([System.Text.Encoding]::UTF8)
    }
}

Send-MailMessage は非推奨のアナウンスが出ているため、これからメール送信処理を実装する場合は利用しないことをおすすめします。

証明書の期限を取得

発行した証明書の情報は発行元の CA に残っていますので、 CA から抽出するのが簡単です。

証明書の情報は、以下のようなスクリプトを用いて取得しています。(諸般の事情で CA サーバ上で実行する必要があります)

Param(
    [string]$CertificateAuthority,
    [datetime]$ExpireAfter,
    [datetime]$ExpireBefore,
    [string]$TemplateName,
    [string]$CommonName
)

BEGIN {}

PROCESS {
    $Properties = @(
        'Issued Common Name',
        'Certificate Expiration Date',
        'Certificate Effective Date',
        'Certificate Template',
        'Issued Email Address',
        'Issued Request ID',
        'Certificate Hash',
        'Request Disposition',
        'Request Disposition Message',
        'Requester Name'
    )

    $CVR_SEEK_EQ = 1
    $CVR_SEEK_LT = 2
    $CVR_SEEK_GT = 16

    $CAView = New-Object -ComObject CertificateAuthority.View
    $CAView.OpenConnection($CertificateAuthority)
    $CAView.SetResultColumnCount($Properties.Count)

    $Properties | ForEach-Object {
        $Index = $CAView.GetColumnIndex($False, $_)
        $CAView.SetResultColumn($Index)
    }

    if (![string]::IsNullOrEmpty($TemplateName)) {
        $MatchedTemplates = @(Get-CATemplate | Where-Object { $_.Name -eq $TemplateName })
        if ($MatchedTemplates.Length -gt 0) {
            $TemplateOid = $MatchedTemplates[0].Oid
            $CAView.SetRestriction($CaView.GetColumnIndex($False, 'Certificate Template'), $CVR_SEEK_EQ, 0, $TemplateOid)
        } else {
            Write-Error "Failed to get template oid: $TemplateName"
            exit 1
        }
    }

    if (![string]::IsNullOrEmpty($CommonName)) {
        $CAView.SetRestriction($CaView.GetColumnIndex($False, 'Issued Common Name'), $CVR_SEEK_EQ, 0, $CommonName)
    }

    if ($Null -ne $ExpireAfter) {
        $CAView.SetRestriction($CaView.GetColumnIndex($False, 'Certificate Expiration Date'), $CVR_SEEK_GT, 0, $ExpireAfter)
    }
    if ($Null -ne $ExpireBefore) {
        $CAView.SetRestriction($CaView.GetColumnIndex($False, 'Certificate Expiration Date'), $CVR_SEEK_LT, 0, $ExpireBefore)
    }

    # Issued certificate
    $CAView.SetRestriction($CAView.GetColumnIndex($False, 'Request Disposition'), $CVR_SEEK_EQ, 0, 20)

    $CV_OUT_BASE64HEADER = 0
    $CV_OUT_BASE64 = 1

    $Rows = $CAView.OpenView()

    while ($Rows.Next() -ne -1) {
        $Certificate = New-Object -TypeName psobject
        $Column = $Rows.EnumCertViewColumn()
        $Null = $Column.Next()
        do {
            $DisplayName = $Column.GetDisplayName()
            if ($DisplayName -eq 'Binary Certificate') {
                $Certificate | Add-Member -MemberType NoteProperty -Name $DisplayName -Value $($Column.GetValue($CV_OUT_BASE64HEADER)) -Force
            } else {
                $Certificate | Add-Member -MemberType NoteProperty -Name $DisplayName -Value $($Column.GetValue($CV_OUT_BASE64)) -Force
            }
        } until ($Column.Next() -eq -1)
        Clear-Variable -Name Column

        $Certificate
    }
}

END {
    $Rows.Reset()
    $CAView = $Null
    [GC]::Collect()
}

上記スクリプトで証明書一覧を取得した場合、同じ CN の証明書が複数含まれる場合があるので、利用する側のスクリプト等でフィルタリングする必要があります。 今回は、各 CN の証明書のうち期限までの日数が最も長いものを抽出して利用しています。

フィルタリングの手法は複数ありますが、グループ化すると簡単です。 以下のコマンドは CN 毎にグループ化する一例です。

PS> Get-IssuedCertificate.ps1 | Group-Object -Property 'Issued Common Name'

期限が近い場合のメール通知

期限までの日数を取得して、以下の通りメール通知を送信します。

期限までの日数 通知先
30日 利用者
14日 利用者
7日 利用者, 証明書管理者

期限までの日数は以下の function を用いて計算しています。

function Get-DaysToExpiration {
    Param([parameter(Mandatory=$True)][datetime]$ExpirationDate)
    return ($ExpirationDate.Date - (Get-Date).Date).Days
}

expiring

期限直前の通知メール

失効した場合のメール通知

証明書が失効したかどうかの判定は、以下のフローで行っています。

  1. 最新の csv のデータを git pull で取得
  2. 先述のスクリプトを用いて CA から有効な証明書の情報一覧を取得
  3. csv に存在していて、有効な証明書の情報一覧に存在しない証明書があれば、その証明書が失効したと判断

上記フローで失効したと判断された証明書があれば、連絡先メールアドレスに通知メールを送信します。

expired

失効時の通知メール

おわりに

今回、この証明書の期限を一括管理するシステムを構築したことで、利用者側での監視忘れによる証明書の失効を防止することができるようになりました。

しかし、まだ課題は残っており、現時点では以下の項目が課題となっています。

  • CA サーバ上でしか実行できない
  • 証明書の受け渡しが煩雑
  • Send-MailMessage が非推奨
  • 期限までの期間が最も長いものを抽出しているため、過去に発行された期限の長い証明書が抽出されることがある

今後は、上記の課題の解決に向けて取り組んでいきたいと考えています。

最後まで読んでいただき、ありがとうございます!
この記事をシェアしていただける方はこちらからお願いします。

recruit

DeNAでは、失敗を恐れず常に挑戦し続けるエンジニアを募集しています。