Cloudbase Sensor: Windows版の署名処理について

はじめに

こんにちは、Cloudbase で Workload Security チームのエンジニアをしている ikuto です。

Cloudbase では、お客様のオンプレミス環境やクラウド環境のインフラを監視するエージェントとして「Cloudbase Sensor」を提供しています。
Sensor は対象マシン上で動作し、SBOM(Software Bill of Materials)の収集やシステム情報の取得を行います。このたび Windows 版の Sensor を提供するにあたり、実行ファイルにコード署名を施す仕組みが必要になりました。
Windows では署名のない実行ファイルに対して SmartScreen による警告が表示されるため、セキュリティ製品として配布する Cloudbase Sensor にとって、コード署名は避けて通れない課題でした。
また、エンタープライズ環境では「署名済みバイナリのみ実行を許可する」というポリシーが設定されていることも珍しくありません。

今回、私がこの Windows 版 Sensor のコード署名機能の設計と実装を担当しました。
この記事では、AWS CloudHSM を用いて CI/CD パイプライン上でコード署名を自動化した仕組みについて紹介します。

背景と課題

Windows のコード署名がなぜ重要か

Windows 環境でコード署名が重要な理由はいくつかあります。

まず、SmartScreen による未署名 EXE のブロックです。 Windows ではインターネットからダウンロードした実行ファイルに対して SmartScreen フィルターが働き、署名のないファイルには「Windows によって PC が保護されました」という警告が表示されます。 お客様がインストーラーを起動するたびにこの警告が出るのは、セキュリティ製品としてあるべき姿ではありません。

次に、セキュリティ製品としての信頼性です。Cloudbase Sensor はお客様のインフラ上で動作するソフトウェアです。 配布するバイナリそのものの信頼性を証明することは、セキュリティ製品を提供する立場として極めて重要です。

さらに、エンタープライズ環境のポリシー対応もあります。 大規模な組織ではグループポリシーや Intune 等でアプリケーション制御が実施されており、署名済みバイナリでなければインストールが許可されないケースがあります。

EV コード署名証明書と HSM の要件

コード署名証明書にはいくつかの種類がありますが、SmartScreen での即時信頼を得るためには EVコード署名証明書が必要です。
EV コード署名証明書には重要な制約があります。認証局の要件として、秘密鍵をハードウェアセキュリティモジュール(HSM)上に保管しなければなりません
ソフトウェア上のファイルとして秘密鍵を持つことが許されていないのです。

CI/CD パイプラインでの課題

Cloudbase Sensor のビルドとリリースは GitHub Actions を通じた CI/CD パイプラインで自動化されています。
物理的な USB トークン型の HSM を使う方法では、CI/CD パイプラインに組み込むことができません。
クラウド上で動作し、かつ自動化パイプラインから利用可能な HSM ソリューションが必要でした。

CloudHSM とは

AWS CloudHSM は、AWS が提供するクラウドベースの HSM サービスです。
専用のハードウェアセキュリティモジュールが AWS のデータセンター内で稼働し、暗号鍵の生成・保管・署名処理をハードウェア内部で完結させます。
CloudHSM の主な特徴は以下のとおりです。

  • FIPS 140-2 Level 3 準拠: 物理的な耐タンパー性を備えた HSM を使用
  • PKCS#11 インターフェース: 標準的な暗号 API を通じて鍵にアクセス可能
  • 鍵がハードウェア外に出ない: 秘密鍵の生成も署名処理も HSM 内部で実行される

ソフトウェア上に秘密鍵を持つ方式(例えば、ファイルベースの PFX 証明書)では、CI/CD 環境のメモリ上やディスク上に秘密鍵が一時的にでも存在します。
一方 CloudHSM では、秘密鍵は常に HSM 内部にあり、署名処理のリクエストを送る形になるため、鍵の漏洩リスクが根本的に低減されます。
CloudHSM を採用した理由をまとめると、以下のとおりです。

  • EV コード署名証明書の要件として HSM が必須
  • Cloudbase のインフラが AWS 上にあり、CI/CD パイプラインとの親和性が高い
  • PKCS#11 インターフェースにより、osslsigncode 等の既存署名ツールとの連携が可能

実装した内容

ここからは、実際に実装した署名の仕組みを紹介します。

全体アーキテクチャ

署名処理を含む Windows 版 Sensor のリリースフローは以下のようになっています。

全体アーキテクチャ

ポイントは、EXE と MSI の 2 段階署名です。
まず EXE ファイルに署名し、その署名済み EXE を含めて MSI インストーラーをビルドし、最後に MSI 自体にも署名します。
こうすることで、インストーラーを開いた際も、インストール後の実行ファイルも、どちらも署名が検証される状態になります。

署名コンテナの構築

署名処理は Docker コンテナ内で実行します。Ubuntu 24.04 をベースとしたイメージを構築しました。

FROM ubuntu:24.04

# 基本的なビルドツールと依存ライブラリのインストール
RUN apt-get update && apt-get install -y --no-install-recommends \
    curl ca-certificates unzip jq git cmake build-essential \
    libssl-dev libcurl4-openssl-dev zlib1g-dev \
    libengine-pkcs11-openssl libp11-3t64 opensc p11-kit \
    && rm -rf /var/lib/apt/lists/*

# OpenSSL 3.4.3 をソースからビルド
RUN curl -sSL https://www.openssl.org/source/openssl-3.4.3.tar.gz -o openssl.tar.gz \
    && tar -xzf openssl.tar.gz && cd openssl-3.4.3 \
    && ./config && make -j$(nproc) && make install

# osslsigncode 2.10 をソースからビルド
RUN git clone https://github.com/mtrojnar/osslsigncode.git \
    && cd osslsigncode && git checkout 2.10 \
    && mkdir build && cd build \
    && cmake -S .. && cmake --build . && cmake --install .

# CloudHSM PKCS#11 ライブラリのインストール(Ubuntu 24.04 Noble 用)
RUN curl -sSL <cloudhsm-pkcs11-noble-package-url> -o cloudhsm-pkcs11.deb \
    && apt install -y ./cloudhsm-pkcs11.deb

# 非 root ユーザーで実行
RUN groupadd -r appgroup && useradd -r -g appgroup appuser
USER appuser

OpenSSL と osslsigncode をソースからビルドしている理由は後述しますが、ここがもっとも苦労したポイントの一つです。

CloudHSM 接続設定

署名コンテナが起動すると、まず CloudHSM への接続設定を行います。

# CloudHSM クラスターの IP を AWS API で動的に取得
CLOUDHSM_IPS=$(aws cloudhsmv2 describe-clusters \
    --filters clusterIds="${CLOUDHSM_CLUSTER_ID}" \
    --query 'Clusters[0].Hsms[*].EniIp' --output text)

# PKCS#11 ライブラリに HSM の IP を設定
/opt/cloudhsm/bin/configure-pkcs11 -a "${CLOUDHSM_IPS}"

# Customer CA 証明書を Secrets Manager から取得して配置
aws secretsmanager get-secret-value \
    --secret-id "$CUSTOMER_CA_SECRET_NAME" \
    --query 'SecretString' --output text \
    > /opt/cloudhsm/etc/customerCA.crt

# EV CA チェーン(Root → Intermediate 2 → Intermediate 1)を取得
aws secretsmanager get-secret-value \
    --secret-id "$EV_CA_SECRET_NAME" \
    --query SecretString --output text |
    jq -r '.["ev-root"], .["ev-intermediate-2"], .["ev-intermediate-1"]' |
    sed 's/\\n/\n/g' > /opt/cloudhsm/etc/ev_chain.pem

# Customer CA 証明書を PKCS#11 に登録
/opt/cloudhsm/bin/configure-pkcs11 \
    --hsm-ca-cert /opt/cloudhsm/etc/customerCA.crt

# 単一 HSM 構成のため Key Availability Check を無効化
/opt/cloudhsm/bin/configure-pkcs11 --disable-key-availability-check

CloudHSM クラスターの IP はインスタンスの再作成で変わる可能性があるため、describe-clusters API で毎回動的に取得する方式にしました。
また、CA 証明書チェーンや CU(Crypto User)の認証情報は AWS Secrets Manager で管理しています。

署名処理

実際の署名は osslsigncode を使って PKCS#11 経由で CloudHSM 上の秘密鍵にアクセスします。

osslsigncode sign \
    -pkcs11engine /usr/lib/x86_64-linux-gnu/engines-3/pkcs11.so \
    -pkcs11module /opt/cloudhsm/lib/libcloudhsm_pkcs11.so \
    -certs "$EV_CA_CHAIN_PATH" \
    -key "pkcs11:token=hsm1;object=$KEY_LABEL;type=private" \
    -pass "$HSM_PIN" \
    -t "http://timestamp.globalsign.com/?signature=sha2" \
    -h sha256 \
    -in "$input_file" \
    -out "$output_file" \
    -login

ここでの処理の流れを見てみます。

  1. osslsigncode が OpenSSL の pkcs11 enginepkcs11.so)を読み込む
  2. pkcs11 engine が CloudHSM の PKCS#11 モジュールlibcloudhsm_pkcs11.so)を読み込む
  3. PKCS#11 モジュールが CloudHSM に接続し、HSM 内の秘密鍵で署名処理を実行
  4. SHA-256 ハッシュと GlobalSign の SHA-2 タイムスタンプを付与
  5. 署名後、osslsigncode verify で署名の検証を実施

この 2 層構造(pkcs11 engine → CloudHSM PKCS#11 モジュール)により、osslsigncode のような既存のツールから CloudHSM の秘密鍵を透過的に利用できます。

CI/CD パイプライン

GitHub Actions のワークフローは 3 つのジョブで構成されています。

Job 1: build-and-sign-binaries(Linux ランナー)

  • Go のクロスコンパイルで Windows 向けの sensor.exeupdater.exe をビルド
  • 未署名バイナリを S3 にアップロード
  • ecspresso を使って ECS Fargate の署名タスクを起動し、EXE ファイルに署名
  • 署名済み EXE で S3 上のファイルを上書き

Job 2: build-msi-installer(Windows ランナー)

  • S3 から署名済み EXE をダウンロード
  • WiX Toolset を使って MSI インストーラーをビルド
  • 未署名の MSI を S3 にアップロード

Job 3: sign-and-deploy(Linux ランナー)

  • 再び ECS Fargate の署名タスクを起動し、今度は MSI に署名
  • CloudFront のキャッシュを無効化して最新バイナリを配信
  • Aurora DB にバージョン情報を upsert
  • 本番リリース時は GitHub Release を作成しアセットを添付

ECS Fargate タスクの起動には ecspresso を利用しています。
タスク定義では Terraform の state から CloudHSM のクラスター ID や Secrets Manager の ARN を動的に解決する仕組みにしました。

{
  "environment": [
    {
      "name": "CLOUDHSM_CLUSTER_ID",
      "value": "{{ tfstate `module.shinobi_signing.aws_cloudhsm_v2_cluster.main.cluster_id` }}"
    }
  ]
}

skip_sign フラグにより、開発環境(dev)では署名をスキップし、本番環境(prd)では必ず署名を実施するという切り替えも実装しました。

Terraform インフラ

CloudHSM のインフラは Terraform で管理しています。shinobi-signing モジュールとして、以下のリソースを定義しました。

  • CloudHSM クラスター: hsm2m.medium インスタンスタイプ、FIPS モードで稼働
  • Secrets Manager: CU(Crypto User)の認証情報、Customer CA 証明書、EV CA 証明書チェーンを管理
  • Security Group: ECS Fargate タスクから CloudHSM への接続(ポート 2223-2225)を許可
  • EC2 クライアントインスタンス: 初期セットアップ時の鍵生成や証明書登録のために使用。普段は停止しておき、必要なときだけ起動する運用です

苦労した点・ハマりどころ

ここからは、実装の過程で実際に苦労した点を共有します。同じような構成を検討されている方の参考になれば幸いです。

OpenSSL バージョンの問題

もっとも厄介だった問題の一つが、OpenSSL のバージョンに起因する Segmentation fault です。
osslsigncode 2.10 は OpenSSL 3.x 系を必要としますが、Ubuntu 24.04 のパッケージ版 OpenSSL をそのまま使うと、署名実行時に Segmentation fault が発生しました。これは osslsigncode の Issue(#388)でも報告されている問題です。
解決策として、OpenSSL 3.4.3 をソースからビルドして利用する構成にしました。
Dockerfile 内でのビルドが追加されるためイメージのビルド時間は長くなりましたが、安定した署名処理が実現できています。

osslsigncode のバージョン問題

OpenSSL と同様に、osslsigncode 自体のバージョンでも問題が発生しました。
Ubuntu のパッケージマネージャー(dpkg)からインストールできるバージョンは古く、署名時にエラーが発生します。Git リポジトリから直接 2.10 タグをチェックアウトしてビルドすることで解決しました。
OpenSSL と osslsigncode、両方ともソースビルドが必要というのは、当初想定していなかった作業でした。

Secrets Manager ARN の参照問題

ecspresso のタスク定義から Terraform state の Secret ARN を正しく参照する設定には、何度も修正を繰り返しました。
コミット履歴を振り返ると 4 回以上の修正コミットが残っています。
特に、secrets フィールド(ECS の Secrets Manager 参照)と environment フィールド(通常の環境変数)でそれぞれ異なる参照形式が必要であり、ecspresso の tfstate テンプレート関数と Secrets Manager の ARN フォーマットを正しく組み合わせる必要がありました。

CloudHSM IP の動的取得

当初は CloudHSM インスタンスの IP アドレスを静的に設定していましたが、HSM インスタンスの再作成(メンテナンスやスケーリング)で IP が変わるという問題に直面しました。
describe-clusters API から Cluster ID をキーにして IP を動的に取得する方式に変更することで、インスタンスの入れ替えがあっても署名処理が自動的に新しい IP に追従するようにしました。

CloudHSM SDK バージョン移行

CloudHSM の SDK 5 への移行に伴い、従来の CloudHSM Client コマンド群が CLI コマンドに変更されました。
セットアップスクリプトの書き換えが必要になり、特に configure-pkcs11 コマンドのオプション体系が変わった部分の対応に手間がかかりました。

ECS 実行環境の最適化

署名タスクの実行環境は、当初 EC2 互換で検討していましたが、最終的に Fargate に変更しました。
Fargate のほうが EC2 インスタンスの管理が不要で、タスク単位での起動・終了が容易です。
Fargate 向けにネットワーク設定(awsvpc モード)やリソース配分(1 vCPU, 2GB メモリ)を調整しました。

Key Availability Check の無効化

CloudHSM は 2 つ以上のインスタンスでの運用が推奨されていますが、コスト最適化の観点から単一インスタンスで運用しています。
この場合、デフォルトの Key Availability Check(複数 HSM 間での鍵の同期確認)を無効化する必要があります。
configure-pkcs11 --disable-key-availability-check で無効化しましたが、この設定が必要であることに気づくまでに調査時間を要しました。

3 ジョブ CI/CD の構築

Linux クロスコンパイル → ECS 署名(EXE)→ Windows MSI ビルド → ECS 署名(MSI)→ デプロイという複数ジョブ間の依存関係の管理は複雑でした。
特に、EXE の署名が完了してから MSI をビルドしなければならず、MSI の署名が完了してからデプロイを実行しなければならないという順序制約を GitHub Actions のジョブ依存関係(needs)で正しく表現する必要がありました。
また、Linux ランナーと Windows ランナーを跨ぐため、成果物の受け渡しに S3 を中間ストレージとして利用しています。

価値と学び

今回の実装によって、以下の価値を実現できました。

Windows 版 Sensor の信頼性向上: SmartScreen の警告が表示されなくなり、お客様が安心して Sensor を導入できるようになりました。
セキュリティ製品として配布する実行ファイルの信頼性を証明できたことは大きな前進です。

CI/CD パイプラインへの完全自動化: リリース時に手動で署名する必要がなくなりました。
GitHub Actions からワンクリックでビルド・署名・デプロイまで完結します。運用面では、リリース前に CloudHSM インスタンスが起動していることを確認するだけで済みます。

HSM による鍵管理のセキュリティ強化: 秘密鍵が HSM の外に出ない構成を実現しました。
CI/CD のログやコンテナのファイルシステム上に秘密鍵が残る心配がありません。

個人的な学びとしては、CloudHSM、PKCS#11、コード署名という領域を深く知る機会になりました。
HSM の初期セットアップ(クラスター作成、CU 作成、鍵生成、証明書登録)から、PKCS#11 を介した署名ツールとの連携まで、一通り経験できたことは大きな財産です。

まとめ

本記事では、AWS CloudHSM を用いた Windows 版 Cloudbase Sensor のコード署名機能について紹介しました。

  • CloudHSM と PKCS#11 を活用し、EV コード署名証明書の要件を満たしつつ CI/CD パイプライン上での自動署名を実現しました
  • ECS Fargate 上の署名コンテナ により、署名処理をオンデマンドで実行する効率的な構成を構築しました
  • EXE と MSI の 2 段階署名 により、インストーラーから実行ファイルまで一貫した信頼性を確保しました

今後の改善余地としては、CloudHSM インスタンスのライフサイクル自動化(リリース時のみ起動し、完了後に停止する仕組み)や、HSM インスタンスの冗長化によるさらなる可用性の向上が考えられます。


Cloudbase では、セキュリティプロダクトの開発を一緒に推進してくれるエンジニアを募集しています。
もしご興味がありましたら、ぜひお気軽にお問い合わせください。

https://cloudbase.co.jp/careers