はじめに

こんにちは。あやかです。

今回は、Smallstepが開発しているオープンソースの認証局(CA)ソフトウェア「step-ca」をDockerで起動して、テスト用の証明書を発行するまでの流れを記事にしたいと思います。

背景としては、自宅ネットワークのVPN環境をIKEv2に移行する計画があって、その認証に証明書を使いたいと考えています。 拠点間VPNとモバイル端末からの接続の両方を証明書ベースで認証する構成にしたいので、自前でCAを持てるstep-caを導入することにしました。

まずは「ちゃんと動くか試してみる」がゴールです。 VPN用の証明書発行は次回の記事で扱う予定なので、今回はstep-caの起動からテスト証明書の発行までをカバーします。

step-caとは

step-caは、Smallstep社が開発しているオープンソースのプライベート認証局です。 ひとことで言うと、自分専用のLet’s Encryptみたいなものです。

以前、KMIPの記事でopensslコマンドを使って自己署名証明書を作成しましたが、あれはすべて手作業でした。 Root CAの鍵生成、CSRの作成、署名、SAN設定……ひとつずつコマンドを打つ必要があって、証明書の枚数が増えてくると管理が大変になります。

step-caを使うと、この一連の作業がぐっとシンプルになります。

opensslで手動運用する場合:

  • Root CA秘密鍵の生成 → CSR作成 → 自己署名 → SAN設定ファイル作成 → 署名……と手順が多い
  • 証明書の有効期限管理も手動
  • 失効処理(CRL)を自分で管理する必要がある

step-caを使う場合:

  • step ca certificateコマンド1つで証明書が発行される
  • 有効期限のデフォルトが24時間で、自動更新の仕組みも用意されている
  • APIベースで動作するので、自動化との相性がいい

今回はまず「動かしてみる」段階なので、step-caの全機能を使いこなすところまでは踏み込みません。 でも、将来的にIKEv2 VPN用の証明書を発行することを見据えて、環境を整えておきます。

環境情報

今回使用した環境はこんな感じです。

  • NAS: Synology DS923+
  • DSM: 7.3.2-86009
  • step-ca: smallstep/step-ca(Docker Hub公式イメージ)
  • step CLI: macOS(Homebrew経由でインストール)

step CLIとbootstrapの関係を理解する

実際の構築に入る前に、step-caの仕組みで重要な概念を整理しておきます。

step-caの環境には、「サーバー側」と「クライアント側」の2つの役割があります。

サーバー側(step-ca):

  • 認証局本体。証明書の発行・管理を行う
  • 今回はSynology NAS上のDockerコンテナとして動作

クライアント側(step CLI):

  • step-caに対して証明書の発行リクエストを送るツール
  • 普段の作業端末にインストールする

この2つをつなぐのが「bootstrap」という操作です。 bootstrapは、クライアント(step CLI)に対して「このCAを信頼してね」と設定する作業です。具体的には、CAのURLとRoot CA証明書のフィンガープリントをクライアントに登録します。

一度bootstrapが完了すれば、あとはstep ca certificateコマンドで簡単に証明書を発行できるようになります。

今回は、macOSの作業端末にstep CLIをインストールして、NAS上のstep-caにbootstrapする構成で進めます。

接続構成の検討

step-caをどのようにアクセスさせるか、構成を検討しました。

リバースプロキシは使わない

Synology NASで他のDockerサービス(ZabbixやFreshRSSなど)を動かすときは、DSMのリバースプロキシを経由してポート443でアクセスする構成にしています。 step-caも同じようにリバースプロキシを通せないか最初は考えたのですが、やめました。

理由は、step-caがmTLS(相互TLS認証)を使う場面があるためです。 たとえば、証明書の更新(step ca renew)ではクライアント証明書を使ってCAに認証します。 リバースプロキシでTLSを終端してしまうと、このクライアント証明書の情報が失われて、認証が通らなくなる可能性があります。

L4(TCP)レベルでTLSを終端せずに転送する方法もありますが、SynologyのリバースプロキシはL7(HTTP)のみ対応なので使えません。 NginxやHAProxyを別途立てれば実現できますが、そこまでする必要もないかなと。

結論として、step-caにはポート9000で直接アクセスする構成にしました。 CAにアクセスするのはstep CLIだけなので、ブラウザで開くサービスと違ってポート番号がついていても不便はありません。

FQDNでのアクセス

step-caにca.example.co.jpというFQDNでアクセスしたいのですが、ここでひとつ問題があります。

権威DNSサーバー(パブリックDNS)にNASのプライベートIPアドレスをAレコードとして登録すれば名前解決はできます。 ただ、この方法だとdigコマンド等で誰でもプライベートIPアドレスを確認できてしまいます。 直接的な攻撃にはつながりにくいとはいえ、ネットワーク構成の一部を外部に公開することになるので、あまり気持ちのいいものではありません。

そこで今回は、ルーターの簡易DNS機能を使って、LAN内でのみca.example.co.jpをNASのプライベートIPアドレスに解決する構成にしました。 権威DNSにはプライベートIPを一切登録しないので、外部からネットワーク構成を推測される心配がありません。

私の環境ではNEC IXシリーズのルーターを使っているので、dns hostコマンドで静的エントリを登録しています。

dns host ca.example.co.jp ip 192.168.123.123

LAN内の端末がルーターのプロキシDNSを経由していれば、この設定だけでca.example.co.jpがNASのIPアドレスに解決されるようになります。 他のルーターを使っている場合でも、静的DNSエントリやhostsファイルの機能があれば同様の構成が可能です。

デュアルスタック環境での注意点

この構成を試したところ、最初はうまくいきませんでした。 macOSからca.example.co.jpを名前解決しても、IXルーターに問い合わせが飛んでいなかったんです。

原因は、IPv6のDNSが優先されていたことでした。

私の環境では、MAP-Eを終端している無線LANルーターがLAN側にグローバルIPv6アドレスとDNSv6アドレス(無線LANルーター自身のアドレス)をRAで配布していました。 macOSはIPv6のDNSサーバーを優先するため、名前解決の要求がIXルーターではなく無線LANルーターに送られていたわけです。 無線LANルーターにはdns hostのような静的エントリの機能がないので、当然ca.example.co.jpは解決できません。

対処として、無線LANルーターの設定画面からDNSv6アドレスの配布をオフにしました。 これでmacOSに通知されるDNSサーバーがIXルーターのみになり、dns hostで登録した静的エントリが正しく参照されるようになりました。

名前解決の経路としてはmacOS → IXルーター → 無線LANルーター → プロバイダDNSと1ホップ増えますが、DNSクエリはミリ秒単位の処理なので体感への影響はほぼありません。 むしろIXのDNSキャッシュが効くぶん、2回目以降の解決は速くなる場合もあります。

デュアルスタック環境でルーターの簡易DNS機能を使う場合は、どのDNSサーバーが優先されているかを確認しておくことをおすすめします。

compose.yamlの準備

まずは、step-caを起動するためのcompose.yamlを作成します。

services:
  step-ca:
    image: smallstep/step-ca
    container_name: step-ca
    ports:
      - "9000:9000"
    environment:
      - DOCKER_STEPCA_INIT_NAME=SumikkoWorks Root CA
      - DOCKER_STEPCA_INIT_DNS_NAMES=localhost,step-ca,ca.example.co.jp,192.168.123.123
    volumes:
      - ./step-data:/home/step
    healthcheck:
      test: ["CMD", "curl", "-k", "-f", "https://localhost:9000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
    restart: unless-stopped

ポイント:

  • DOCKER_STEPCA_INIT_NAMEはCAの名前です。証明書のIssuer欄に表示されるので、わかりやすい名前をつけておきます
  • DOCKER_STEPCA_INIT_DNS_NAMESには、CAにアクセスする際に使うホスト名やIPアドレスをすべて指定します。ここに含まれていないホスト名やIPでアクセスしようとすると、TLSエラーになります
  • ca.example.co.jp192.168.123.123に名前解決できるよう、DNSの設定を事前に済ませておいてください
  • IPアドレスの192.168.123.123はNASのIPアドレスに置き換えてください
  • ボリュームは./step-dataにバインドマウントしています。Synology NASのFile Stationから直接設定ファイルを確認できるようにするためです
  • healthcheckについては後述しますが、これがないとContainer Managerで警告状態になります

環境変数による初期化は、コンテナの初回起動時にのみ実行されます。 2回目以降の起動では、step-dataディレクトリに保存された設定が使われるので、環境変数を変更しても反映されません。 設定をやり直したい場合は、step-dataディレクトリを削除してからコンテナを再作成してください。

Container Managerでの起動

起動手順はいつも通りシンプルです。

  1. File Stationでstep-caディレクトリを作成
  2. 作成したディレクトリにcompose.yamlをアップロード
  3. Container Managerを開いて「プロジェクト」タブを選択
  4. 「新規作成」をクリック
  5. プロジェクト名を入力(私は「step-ca」にしました)
  6. 「ソースを設定」で「既存のDocker Composeファイルを使用」を選択
  7. パスに先ほどのディレクトリを指定
  8. 「次へ」→「完了」でコンテナを起動

起動後、コンテナのログを確認します。 Container Managerで「step-ca」コンテナを選択して、「詳細」→「ログ」タブを開いてください。

初回起動時は、以下のようなログが出力されます。

Generating root certificate... done!
Generating intermediate certificate... done!

✔ Root certificate: /home/step/certs/root_ca.crt
✔ Root private key: /home/step/secrets/root_ca_key
✔ Root fingerprint: abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789
✔ Intermediate certificate: /home/step/certs/intermediate_ca.crt
✔ Intermediate private key: /home/step/secrets/intermediate_ca_key
✔ Database folder: /home/step/db
✔ Default configuration: /home/step/config/defaults.json
✔ Certificate Authority configuration: /home/step/config/ca.json

Your PKI is ready to go.

ここで表示されるRoot fingerprintは、後のbootstrapで使うので必ずメモしておいてください。

また、プロビジョナーのパスワードも自動生成されます。 パスワードはstep-data/secrets/passwordファイルに保存されているので、こちらも確認しておきましょう。

👉 Your CA administrative password is: xxxxxxxxxxxxxxxxxxxx

File Stationでstep-ca/step-dataディレクトリを見ると、以下のような構成になっているはずです。

step-data/
├── certs/
│   ├── intermediate_ca.crt
│   └── root_ca.crt
├── config/
│   ├── ca.json
│   └── defaults.json
├── db/
├── secrets/
│   ├── intermediate_ca_key
│   ├── password
│   └── root_ca_key
└── templates/

step CLIのインストール(macOS)

次に、作業端末のmacOSにstep CLIをインストールします。

Homebrewを使えば簡単にインストールできます。

brew install step

インストール後、バージョンを確認して動作をチェックします。

step version

以下のように表示されればOKです。

Smallstep CLI/0.28.x (darwin/arm64)
Release Date: 20xx-xx-xxTxx:xx:xxZ

bootstrapの実行

step CLIがインストールできたら、step-caとの信頼関係を構築します。 これがbootstrapです。

step ca bootstrap \
  --ca-url https://ca.example.co.jp:9000 \
  --fingerprint abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789
  • --ca-urlには、step-caのFQDNとポート9000を指定します。IPアドレス(https://192.168.123.123:9000)でも接続できますが、DNS名で統一しておく方が管理しやすいです
  • --fingerprintには、先ほどコンテナのログでメモしたRoot fingerprintを指定します

成功すると、以下のように表示されます。

The root certificate has been saved in /Users/あなたのユーザー名/.step/certs/root_ca.crt.
Your configuration has been saved in /Users/あなたのユーザー名/.step/config/defaults.json.

これで、macOSのstep CLIがNAS上のstep-caを認証局として認識するようになりました。

もし--installオプションを追加すると、Root CA証明書がOSのトラストストアにも追加されます。 今回はテスト段階なので、トラストストアへの追加はしていません。 必要に応じて後からstep certificate install ~/.step/certs/root_ca.crtで追加できます。

CAの動作確認

bootstrapが完了したら、CAが正常に動作しているか確認しましょう。

step ca health

以下のように表示されれば、CAは正常に稼働しています。

ok

テスト用サーバー証明書の発行

いよいよ証明書を発行してみます。 まずは動作確認のために、テスト用のサーバー証明書を1枚発行します。

step ca certificate test.home.local test.crt test.key

コマンドを実行すると、プロビジョナーの選択とパスワードの入力を求められます。

✔ Provisioner: admin (JWK) [kid: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx]
✔ Please enter the password to decrypt the provisioner key:

パスワードは、先ほど確認したstep-data/secrets/passwordファイルの内容を入力してください。

成功すると、カレントディレクトリにtest.crttest.keyが生成されます。

✔ CA: https://ca.example.co.jp:9000
✔ Certificate: test.crt
✔ Private Key: test.key

発行した証明書の確認

発行された証明書の内容を確認してみましょう。

step certificate inspect test.crt --short

以下のような出力が表示されます。

X.509v3 TLS Certificate (ECDSA P-256) [Serial: xxxx...xxxx]
  Subject:     test.home.local
  Issuer:      SumikkoWorks Root CA Intermediate CA
  Provisioner: admin [ID: xxxx...xxxx]
  Valid from:  2026-03-11T09:00:00Z
          to:  2026-03-12T09:00:00Z

注目してほしいのは、有効期限が24時間になっている点です。 step-caのデフォルトでは、証明書の有効期限は24時間に設定されています。

これはセキュリティ的には良い設計なのですが、VPN用の証明書として使う場合は短すぎます。 有効期限の調整は、IKEv2 VPN用の証明書を発行する際にca.jsonのclaims設定で変更する予定です。

--shortオプションを外すと、SANや鍵の詳細など、すべての情報が確認できます。

step certificate inspect test.crt

テスト証明書のクリーンアップ

動作確認が終わったら、テスト用の証明書は削除しておきましょう。

rm test.crt test.key

トラブルシューティング

構築中に遭遇する可能性のある問題と対処法です。

Container Managerで警告状態になる

コンテナは起動していてログにもエラーがないのに、Container Managerのステータスが警告(黄色)になることがあります。 私も最初これに遭遇して「え、何か壊れた?」と焦りました。

原因は、step-caのDockerイメージにデフォルトで定義されているヘルスチェックです。 コンテナ内部からhttps://localhost:9000/healthにcurlでアクセスする仕組みになっているのですが、step-caはHTTPSで動作しているため、自己署名証明書の検証でcurlが失敗してしまいます。 その結果、ヘルスチェックが「unhealthy」と判定されて、Container Managerが警告を表示するというわけです。

対処法は、compose.yamlでヘルスチェックを上書きすることです。 先ほどのcompose.yamlでは、curlに-kオプション(証明書検証をスキップ)をつけたヘルスチェックを定義しています。

    healthcheck:
      test: ["CMD", "curl", "-k", "-f", "https://localhost:9000/health"]
      interval: 30s
      timeout: 10s
      retries: 3

compose.yamlを差し替えてプロジェクトを再構築すれば、ステータスが正常(緑色)に変わるはずです。 step-dataディレクトリは削除する必要はありません。

bootstrapで接続できない

step ca bootstrap実行時にタイムアウトする場合は、以下を確認してください。

  • NASのファイアウォールでポート9000が開いているか
  • DOCKER_STEPCA_INIT_DNS_NAMESにNASのIPアドレスが含まれているか
  • コンテナが正常に起動しているか(Container Managerでログを確認)

DOCKER_STEPCA_INIT_DNS_NAMESにIPアドレスが含まれていない場合、TLS証明書の検証でエラーになります。 この場合は、step-dataディレクトリを削除して、compose.yamlの環境変数を修正してから再起動してください。

パスワードがわからない

プロビジョナーのパスワードはstep-data/secrets/passwordに保存されています。 File Stationでテキストエディタを使って確認できます。

コンテナが起動しない

step-dataディレクトリのパーミッションに問題がある可能性があります。 step-caコンテナはstepユーザー(UID: 1000)で動作するため、ボリュームのオーナーが合っていないと起動に失敗することがあります。

NASのSSHから以下のコマンドでパーミッションを確認してみてください。

ls -la step-data/

おわりに

step-caをDockerで起動して、テスト用のサーバー証明書を発行するところまでを確認しました。

やってみた感想としては、opensslで手動運用していたときと比べると、証明書の発行がコマンド1つで済むのは本当に楽です。 CAの初期化も環境変数で自動化されるので、compose.yamlを書いて起動するだけで使える状態になります。

一方で、step CLIのbootstrapという概念は、最初はちょっとわかりにくかったです。 「CAに接続するためのクライアント設定」と理解してしまえば、難しくはないのですが。

次回は、IKEv2 VPN用の証明書を発行する手順を書く予定です。 拠点間VPNとモバイル端末向けの証明書では要件が異なるので、ca.jsonのclaims設定で有効期限を調整するところも含めて記事にしたいと思います。