SSD Nodes Learn 🎉 VPS $5.50/月〜
ガイド Matt Connor著者 Matt Connor

Ubuntuに独自CAを追加してHTTPSを信頼させる方法

opensslでプライベートCAとリーフ証明書を作成し、ルート証明書を/usr/local/share/ca-certificatesへ配置してupdate-ca-certificatesを実行します。Ubuntu 24.04での手順と検証結果を確認できます。

Ubuntu の信頼ストアに独自の CA を追加する

独自の CA を Ubuntu の信頼ストアに追加するには、ルート証明書を /usr/local/share/ca-certificates/.crt で終わる名前でコピーし、その後 sudo update-ca-certificates を実行します。CA(認証局)は、他の証明書への署名を許可された証明書を持つ鍵ペアです。マシンがルートを信頼すると、そのルートが署名したすべての証明書が受け入れられるため、自分のサービス間の HTTPS で検証エラーが発生しなくなります。

このガイドでは、openssl を使って証明書チェーン全体をオフラインで構築します。ルート鍵とルート証明書を作成し、サーバー用のリーフ証明書を 1 つ発行してから、ルートをインストールします。その後、同じ検証コマンドの結果がどのように変わるかを確認します。この順序が重要です。インストール前後で検証することで、結果を変えたのがインストールであることを確認できます。

Ubuntu 24.04 のデフォルトイメージには OpenSSL 3 と ca-certificates パッケージが含まれているため、最初にインストールするものはありません(2026 年 8 月に確認済みです)。

独自の CA を運用すべきタイミング

Let's Encrypt などのパブリック CA では、パブリック DNS に登録された名前と、CA から到達できるサーバーが必要です。内部名は対象になりません。プライベートネットワーク上のデータベースや、トンネルにバインドされた管理パネルは、パブリック証明書を取得できません。証明書を取得するためだけにインターネットへ公開すべきでもありません。

Ubuntu 上の自己署名証明書で解決できるのは、1 台のホストだけです。すべてのクライアントでその証明書を信頼する必要があり、次のホストでは同じ作業を繰り返します。プライベート CA では、信頼の設定を 1 段階上に移せます。クライアントでルート証明書を 1 回信頼すれば、その後にルートが署名するすべての証明書が信頼されます。まだ存在しないホスト向けの証明書も対象です。

コストも無視できません。ルートキーは、制約で許可されたあらゆる証明書に署名できます。そのため、ca.key を読める人物は、マシンが受け入れる証明書を発行できます。SSH キー管理で秘密鍵を保護する場合と同じ方法で、ルートキーを保護してください。サービスにパブリック DNS 名がある場合は、これらをすべて省略してパブリック CA を使用します。nginx と Let's Encrypt で Certbot を使用するほうが作業が少なく、クライアント側に何もインストールする必要がありません。

CA 鍵とルート証明書を作成する

自分のユーザーだけが開けるディレクトリで作業します。ルート鍵をこのディレクトリの外へ出してはいけません。

install -d -m 700 ~/ca
cd ~/ca
openssl genrsa -aes256 -out ca.key 4096
chmod 600 ca.key

-aes256 を指定すると、選択したパスフレーズで鍵が暗号化されます。この鍵で署名する後続のコマンドでは、毎回パスフレーズの入力を求められます。-aes256 を省略すると、鍵が暗号化されずにディスクへ保存されます。その場合、バックアップや別の管理者アカウントだけで、あなたのマシンが信頼する証明書を発行できる状態になります。

次に、CA 鍵自身が署名するルート証明書を作成します。

openssl req -x509 -new -key ca.key -sha256 -days 3650 \
  -subj "/O=Example Internal/CN=Example Internal Root CA" \
  -addext "basicConstraints=critical,CA:TRUE,pathlen:0" \
  -addext "keyUsage=critical,keyCertSign,cRLSign" \
  -addext "subjectKeyIdentifier=hash" \
  -addext "nameConstraints=critical,permitted;DNS:internal.example" \
  -out ca.crt

internal.example を実際に使用する名前のサフィックスへ置き換えます。最後の拡張を残す前に、次のセクションを読んでください。

各拡張には 1 つの役割があります。

  • basicConstraintsCA:TRUE により、これが CA 証明書になります。これがないと、署名自体が正しくても、この鍵が署名した証明書をクライアントは拒否します。
  • pathlen:0 により、CA はリーフ証明書には署名できますが、その下位の CA には署名できません。
  • keyUsage により、鍵の用途が証明書と失効リストへの署名に制限されます。これにより、同じ鍵を誤って TLS サーバー鍵として使用できなくなります。
  • subjectKeyIdentifier により、ルートに識別子が付与されます。リーフ証明書はこの識別子を参照するため、数百個の証明書を保持するストア内から正しい発行者をクライアントが見つけられます。
  • nameConstraints により、この CA が保証できる名前が制限されます。

コマンドの意図どおりに作成されたと仮定せず、作成結果を確認します。

openssl x509 -noout -subject -issuer -serial -dates -in ca.crt
openssl x509 -noout -text -in ca.crt

ルート証明書は自分自身で署名するため、Subject と issuer には同じ文字列が表示されます。serial と 2 つの日付は、直前に作成したファイルの内容に基づきます。そのため、ガイドに記載された値ではなく、この出力から確認してください。

CA が署名できる範囲を制限する

システムストアに登録された root は、別途指定しない限り、インターネット上のすべての名前を信頼します。1 台のサーバーにある 1 つのファイルに、これほど大きな権限を持たせることになります。nameConstraints でその範囲を制限できます。root に permitted;DNS:internal.example を設定すると、internal.example の範囲外の名前に対するこの CA からのチェーンは、署名が正しくても拒否されます。

信頼ストアに登録するのではなく、その動作をテストします。

openssl req -new -newkey rsa:2048 -nodes -keyout /tmp/outside.key -out /tmp/outside.csr \
  -subj "/CN=www.example.com"
openssl x509 -req -in /tmp/outside.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
  -days 30 -sha256 \
  -extfile <(printf 'subjectAltName=DNS:www.example.com\n') -out /tmp/outside.crt
openssl verify -CAfile ca.crt /tmp/outside.crt
echo $?

CA は要求された内容に対して署名するため、証明書自体は発行されます。失敗するのは検証時です。終了ステータスは 0 以外になり、OpenSSL は該当した制約を示します。これが拡張機能の効果です。CA の鍵が盗まれても、サブツリーの範囲外の名前に対して有効な証明書を作成することはできません。完了したら rm /tmp/outside.* で残ったファイルを削除します。

制約を適用する前に、4 つの点を理解しておく必要があります。この拡張機能には critical が付いているため、拡張機能を理解しないクライアントは無視せず、チェーンを拒否する必要があります。これは安全な動作ですが、古い TLS ライブラリでは問題になることがあります。DNS 名に対する許可サブツリーは、IP アドレスの SAN を制限しません。サブツリーが指定されていない名前の種類は制限されないため、証明書に IP アドレスを含める場合は、同じ拡張機能に permitted;IP:10.0.0.0/255.255.0.0 も追加します。サブツリーには、今後発行するすべての名前を含める必要があります。短いホスト名も対象です。そのため、単独の名前 app に対する証明書は、上記の例では検証に失敗します。さらに、この制約は root に組み込まれます。方針を変更する場合は、新しい root 証明書を作成し、すべてのクライアントに再インストールする必要があります。

CA で署名したリーフ証明書を発行する

リーフ証明書は、サーバーがクライアントに提示する証明書です。まず、リーフ証明書専用の鍵と CSR(証明書署名要求)を作成します。CSR には公開鍵と要求する名前が含まれます。CSR はリーフ鍵で署名され、要求者が秘密鍵を保持していることを証明します。

openssl req -new -newkey rsa:2048 -nodes \
  -keyout app.key -out app.csr \
  -subj "/CN=app.internal.example"
chmod 600 app.key

重要な名前は、CSR ではなく拡張ファイルに記述します。クライアントはホスト名を subjectAltName(SAN)と照合し、common name は完全に無視します。そのため、CN があり SAN がない証明書は、CN の内容に関係なく、現在のすべてのクライアントでホスト名検証に失敗します。

basicConstraints = CA:FALSE
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = DNS:app.internal.example, DNS:api.internal.example
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always

これを app.ext として保存し、その後、CA で要求に署名します。

openssl x509 -req -in app.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
  -days 397 -sha256 -extfile app.ext -out app.crt

-CAcreateserial は、次のシリアル番号を保持する ca.srl を CA の隣に作成します。2 つの証明書で同じシリアル番号を使用しないよう、このファイルは CA ディレクトリに残してください。-days 397 は選択値であり、ツールの上限ではありません。プライベート CA には失効インフラストラクチャがないため、公開 CA の場合よりも短い有効期間が重要です。構築しない限り CRL も OCSP responder も存在しないため、漏えいしたリーフ鍵は証明書の有効期限が切れるまで使用可能な状態になります。

trust store に進む前に、結果を確認してください。

openssl x509 -noout -subject -issuer -serial -dates -in app.crt
openssl x509 -noout -ext subjectAltName -in app.crt

issuer 行には、リーフ証明書自体ではなく CA の名前が表示されます。SAN 行には、この証明書が有効な名前が一覧表示されます。クライアントはその一覧だけを照合します。

明示的な -CAfile を指定して、インストール前に確認する

openssl verify -CAfile ca.crt app.crt
echo $?

これは、app.crtca.crt の証明書まで連鎖するかという、1 つの点だけを確認します。コマンドラインで root を OpenSSL に渡しているため、このマシンが何を信頼しているかは確認できません。ここで失敗する場合は、証明書自体に問題があります。先に修正してから続行してください。

次に、マシンの信頼ストアを確認します。

openssl verify app.crt
echo $?

-CAfile を指定しない場合、OpenSSL は組み込みの証明書ディレクトリを使用します。openssl version -d はビルドで使用されるベースディレクトリを表示します。Ubuntu では、その下の certs ディレクトリは /etc/ssl/certs に解決されます。まだ root はそこにないため、検証は失敗します。証明書チェーンは、ストアにない発行者まで到達し、追加で確認できる場所も残っていないためです。終了ステータスを確認してください。2 ステップ後に変わるのは、この値です。

openssl verify よりも、実際のクライアントを使うほうが適切です。証明書チェーンだけでなく、ホスト名も確認できるためです。証明書を提供して取得します。

openssl s_server -accept 8443 -cert app.crt -key app.key -www &
curl --resolve app.internal.example:8443:127.0.0.1 https://app.internal.example:8443/

--resolve は接続先を 127.0.0.1 にしながら、app.internal.example を要求します。そのため SAN が一致し、残る問題は信頼だけになります。curl は失敗し、証明書チェーンを検証できなかった理由を表示します。詳細を表示するには -v を追加してください。テストサーバーは起動したままにします。

/usr/local/share/ca-certificates に root をインストールする

sudo cp ca.crt /usr/local/share/ca-certificates/example-internal-root.crt
sudo chmod 644 /usr/local/share/ca-certificates/example-internal-root.crt
sudo update-ca-certificates

これが機能するかどうかを決める詳細は次のとおりです。

  • ファイル名は .crt で終わる必要があります。update-ca-certificates のマニュアルページには、/usr/local/share/ca-certificates 以下にある .crt 拡張子の証明書が取り込まれ、暗黙的に信頼されると記載されています。root.pemroot.cer という名前のファイルは、何も通知されずにスキップされます。
  • 内容は PEM である必要があります。これは、BEGIN CERTIFICATE 行と END CERTIFICATE 行で囲まれた base64 ブロックです。DER ファイルの名前を .crt に変更してもバイナリ形式のままであり、読み込まれません。openssl x509 -inform DER -in ca.der -out ca.crt で変換してください。
  • ここに置くのは root だけです。CA private key と leaf certificate を trust store に置いてはいけません。

update-ca-certificates は、追加および削除した証明書の数を表示します。1 つも追加されない場合は、拡張子またはファイル形式が原因です。

そのメッセージではなく、システム側から変更を確認してください。

ls -l /etc/ssl/certs/$(openssl x509 -noout -subject_hash -in /usr/local/share/ca-certificates/example-internal-root.crt).0
grep -c 'BEGIN CERTIFICATE' /etc/ssl/certs/ca-certificates.crt

最初のコマンドは、証明書自身の subject hash からファイル名を組み立て、そのファイルを一覧表示します。update-ca-certificates がこのシンボリックリンクを作成し、インストールしたファイルを指しています。2 番目のコマンドは、単一ファイルの bundle に含まれる証明書の数を数えます。インストール前にも実行すると、数が 1 増えることを確認できます。

この root を他のマシンにコピーする場合は、インストール前にコピーが完全に到着したことを確認してください。root certificate は、システム上で内容を誤ると最も危険なファイルです。使用前に checksum で検証する他のダウンロードと同じように扱ってください。

システムストアに対して再度検証する

openssl verify app.crt
echo $?
curl --resolve app.internal.example:8443:127.0.0.1 https://app.internal.example:8443/

同じコマンド、同じ証明書ファイルでも、結果は異なります。app.crtには何も変更を加えておらず、サーバーも先ほど起動したものです。唯一の違いは、クライアントが参照するストアに root が配置されたため、チェーンが完成したことです。覚えておくべき仕組みは、検証とはクライアントがすでに信頼している発行者を検索する処理であり、CA をインストールすると、その発行者が検索対象の場所に登録されるということです。

kill %1でテストサーバーを停止します。

/etc/ssl/certs にファイルを置く場所ではない理由

/etc/ssl/certs は生成される出力です。update-ca-certificates は実際の証明書ファイルを指すシンボリックリンクを作成して配置し、連結バンドル /etc/ssl/certs/ca-certificates.crt をその隣に書き込みます。

このディレクトリに証明書を手動でコピーしても、何も検出できません。OpenSSL のディレクトリ検索は、証明書の subject hash に基づく名前のファイルだけを開くため、myca.crt という名前のファイルは認識されません。Ubuntu の curl はバンドルファイルを読み込みます。このバンドルは登録済みのソースから再構築されるため、手動でコピーしたファイルはそのパスにも含まれません。update-ca-certificates --fresh を実行すると、ディレクトリ内のシンボリックリンクが削除されて再構築されるため、手動で作成したリンクも削除されます。

この分割のもう一方が /usr/share/ca-certificates です。これは ca-certificates パッケージに属し、/etc/ca-certificates.conf に記載されています。パッケージを更新すると、このファイルは書き換えられます。/usr/local/share/ca-certificates はローカル管理者用に予約されたディレクトリです。そのため、残りを管理するパッケージをアップグレードしても、独自の CA は保持されます。

どのプログラムがシステムの信頼ストアを無視するか

OpenSSL を使用する、または /etc/ssl/certs を読み込むすべてのプログラムでは、ルート証明書をインストールすれば問題が解決します。curl、wget、git、Python の標準 ssl モジュール、Linux でシステムファイルを読み込む Go プログラムが該当します。独自の証明書一覧を組み込んでいるランタイムは影響を受けません。インストールが正常に完了した後も混乱が生じる主な原因はこれです。

  • Node.js はコンパイル時に組み込まれた一覧を使用します。プロセスの起動前に環境変数へ NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/example-internal-root.crt を設定し、ルート証明書を指定してください。Node は起動時にこの変数を 1 回だけ読み取るためです。現在の Node リリースには、システムストアを読み込むオプションもあります。node --help | grep -i system-ca を実行して、使用中のバージョンが対応しているか確認してください。
  • Python の requests ライブラリは certifi バンドルを使用します。そのプロセスに対して REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt を設定するか、呼び出し時に verify="/etc/ssl/certs/ca-certificates.crt" を渡してください。pip も同じ理由で --cert を受け取ります。
  • Java はキーストアを読み込みます。Ubuntu では、ca-certificates-java パッケージが /etc/ca-certificates/update.d/ 配下にフックをインストールします。そのため、このパッケージが存在すれば update-ca-certificates によって Java のキーストアも更新されます。存在しない場合は、keytool -importcert でルート証明書をインポートしてください。
  • Firefox は独自のストアを保持し、/etc/ssl/certs を参照しません。証明書設定からインポートしてください。Linux の Chromium はユーザーごとの NSS データベースを読み込みます。libnss3-tools パッケージの certutil を使用して編集します。
  • コンテナには独自のファイルシステムがあるため、ホストのストアはコンテナ内では意味を持ちません。ルート証明書をイメージへコピーし、ビルド中に update-ca-certificates を実行してください。サービスを VPS 上の Docker Compose で実行する場合は、この点をあらかじめ計画に含めてください。

クリーンインストール後もプログラムが証明書を拒否する場合は、ほかの変更を行う前に、どのファイルを開いているか確認してください。strace -f -e trace=openat <command> 2>&1 | grep -i cert は詳細を限定せずに調査でき、1 回の実行でこの疑問に答えられます。

長期的に CA を使用できる状態に保つ

リーフ証明書を再発行するには、同じ app.ext ファイルを使って、CSR の作成手順と署名手順をもう一度実行します。信頼する root が変わっていないため、クライアント側での作業は不要です。次回の発行を記憶から再構築せず、正常に動作したコマンドを再実行できるよう、CA ディレクトリ内の ca.srl とすべての .ext ファイルを保持してください。

ca.keyca.crt は、暗号化したままマシンの外部にもバックアップしてください。鍵を失うと、新しい証明書を発行できません。2 つ目の CA を構築し、その root を、最初の CA の root を配置したすべての場所にインストールする必要があります。root を受け取ったすべてのマシンとアプリケーションストアを記録した一覧を作成してください。この一覧がなければ、ローテーションや削除を実施できません。

root 自体の有効期限が近づいたら、早めに後継の root を生成し、両方の root を並行してインストールします。ストアに 2 つの root があっても問題なく、クライアントはどちらも受け入れられます。新しい root でリーフ証明書を再発行し、古い root に依存するものがなくなったら削除します。

信頼ストアから CA を削除する

sudo rm /usr/local/share/ca-certificates/example-internal-root.crt
sudo update-ca-certificates --fresh

--fresh/etc/ssl/certs内のシンボリックリンクを削除し、残っているソースから再構築します。そのため、削除したルート証明書はディレクトリとバンドルの両方から除去されます。インストール時と同じ方法で、削除を確認します。

openssl verify app.crt
echo $?
grep -c 'BEGIN CERTIFICATE' /etc/ssl/certs/ca-certificates.crt
ls -l /etc/ssl/certs/$(openssl x509 -noout -subject_hash -in ~/ca/ca.crt).0

検証は再び失敗し、証明書数は開始時の状態に戻り、ハッシュ用のシンボリックリンクもなくなります。

このコマンドが変更するのはシステムストアだけです。それ以外の場所では、手動でインストールを取り消します。NODE_EXTRA_CA_CERTSを空にし、Java keystore に登録した alias を削除し、各ブラウザープロファイルからルート証明書を削除し、組み込み済みのコンテナーイメージを再構築します。ルート証明書を削除しても、その証明書が署名した証明書は無効になりません。ルート証明書を信頼し続けるすべてのマシンでは有効なままです。そのため、プライベート CA では、ルート証明書を配置した場所を記録した一覧が実務上必要になります。完全に撤回できない CA は恒久的な脆弱性になります。したがって、設定した当日に、一覧がまだ短いうちに、1 台のマシンで削除をテストしてください。

FAQ

Ubuntu では CA 証明書をどこに配置しますか?

/usr/local/share/ca-certificates/ に配置します。ファイル名は .crt で終わり、内容は PEM 形式にします。その後、sudo update-ca-certificates を実行します。このディレクトリはローカル管理者用に予約されているため、パッケージのアップグレードで変更されません。/usr/share/ca-certificatesca-certificates パッケージに属し、/etc/ssl/certs は両方から生成されます。そのため、これらのいずれかに配置したファイルは上書きされるか、無視されます。

update-ca-certificates の実行後も curl が証明書を拒否するのはなぜですか?

原因を順番に確認します。ファイル名が .crt で終わっていないか、DER 形式である可能性があります。その場合、update-ca-certificates はファイルをスキップし、何も追加しません。証明書にホスト名と一致する subjectAltName がない可能性もあります。これは信頼の問題ではなく、ホスト名の問題です。openssl x509 -noout -ext subjectAltName -in app.crt で確認します。サーバーがリーフ証明書だけを送信し、中間証明書も必要な可能性があります。curl が CURL_CA_BUNDLE または --cacert によって別のバンドルを参照している可能性もあります。また、長時間稼働するサービスでは再起動が必要です。多くのプログラムは起動時に一度だけトラストストアを読み込むためです。

システムのトラストストアで Firefox、Chrome、Node、Java も対応できますか?

いいえ。curl、wget、git、Python の標準 ssl モジュール、Go プログラムはシステムファイルを読み込むため、update-ca-certificates の実行後すぐに動作します。Firefox は独自のストアを保持します。Linux 上の Chromium はユーザーごとの NSS データベースを使用し、libnss3-tools パッケージの certutil で編集します。Node.js では、ルート証明書ファイルを指定する NODE_EXTRA_CA_CERTS が必要です。Java はキーストアを読み込みます。update-ca-certificatesca-certificates-java パッケージがインストールされている場合にのみ、そのキーストアを更新します。Python の requestscertifi を使用するため、REQUESTS_CA_BUNDLE が必要です。

Ubuntu のトラストストアから CA を削除するにはどうしますか?

/usr/local/share/ca-certificates/ からファイルを削除し、sudo update-ca-certificates --fresh を実行します。--fresh オプションは /etc/ssl/certs 内のシンボリックリンクを消去して再構築します。そのため、証明書はハッシュ用シンボリックリンクと ca-certificates.crt バンドルから同時に削除されます。CA が署名した証明書に対して openssl verify を実行し、終了ステータスを確認します。その後、追加した他のすべてのストアでも削除を繰り返します。このコマンドは他のストアには作用しないためです。

公開サイトで Let's Encrypt の代わりにプライベート CA を使用できますか?

いいえ。訪問者のブラウザーはそのルート証明書を認識していないため、ページ全体の警告を表示します。また、管理していないマシンにルート証明書をインストールすることはできません。プライベート CA は、自分のマシンだけが名前解決できる名前や、自分で管理するクライアント向けです。不特定の利用者が訪問するサイトでは、公開 CA から証明書を取得してください。

#tls#certificates#openssl#ubuntu#security#pki