Ubuntu 24.04でChromeが受け入れる自己署名証明書の作り方
Ubuntu 24.04でChromeが拒否しない自己署名TLS証明書を作成します。SAN付きのopensslコマンド、nginxとApacheの設定、curl -kを使わず信頼させる方法を解説します。
構築するもの
現代のブラウザーやクライアントが実際に受け入れる自己署名 TLS 証明書、正しい subjectAltName、適切な鍵の権限、nginx または Apache への組み込みを構築します。さらに、多くのガイドが省略する重要な部分として、警告を無視したり、スクリプトに curl -k を永続的にハードコードしたりせず、クライアントに証明書を正しく信頼させます。最後に、内部サービスが 1 つから 6 つに増えた場合に使える、5 コマンドで構築するプライベート CA も扱います。
まず、自己署名証明書を使うか判断します。自己署名証明書は、実際に使われているほど適切な選択ではありません。サービスが実際の DNS 名でパブリックインターネットから到達可能なら、ここで読むのを止めて、nginx で certbot を使って無料の Let's Encrypt 証明書を取得するか、Apache で同等の設定を行うべきです。費用はかからず、自動更新され、主要なブラウザーとオペレーティングシステムがすでに信頼しています。公開サイトで自己署名証明書を使うと、ユーザーはセキュリティ警告を無視するようになります。これは、平文 HTTP を使うより悪い習慣です。
パブリックインターネットを使わない場合は、自己署名証明書が適しています。たとえば、VPS 上の WireGuard トンネルアドレスにバインドした管理パネル、プライベートネットワーク上のステージングサーバー、バックエンド間のサービス間通信、ホームラボのアプライアンス、または Webmin が port 10000 で自動生成するプレースホルダー証明書の置き換えです。Let's Encrypt は 10.8.0.1 や git.internal.lan に対して発行できません。公開 CA は、プライベート IP や作成した TLD を証明書に含めないためです。このような名前では、自分で CA を運用します。
以下の手順は、OpenSSL 3.0.x (openssl version を確認) が搭載された新しい Ubuntu 24.04 サーバーで実行します。インターネット接続は必要ありません。完全にエアギャップされた環境でも動作します。
古いワンライナーで Chrome が拒否する証明書が生成される理由
2017 年以前のチュートリアルで示されているコマンドは、次のようなものです。
# Do not run this: shown so you recognise it in old guides
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout selfsigned.key -out selfsigned.crtこのコマンドは一連の対話形式の質問を表示し、ホスト名を Common Name フィールドに設定します。ただし、subjectAltName 拡張機能を含まない証明書を生成します。この証明書は最初から使用できません。Chrome は 2017 年 4 月の version 58 で Common Name の読み取りを停止しました。RFC 2818 も、2000 年の時点ですでに CN による照合を非推奨としていました。Firefox と Safari も同じ動作をします。curl と Python の ssl モジュールは、証明書に SAN がまったくない場合、Common Name へのフォールバックを引き続き行います。そのため、SAN のない証明書でも簡単な curl テストには合格し、すべてのブラウザーでは失敗することがあります。ブラウザーにとって、証明書がサーバーを識別できるのは SAN 拡張機能によってのみです。そうでない場合、ブラウザーは次のメッセージをそのまま表示します。
NET::ERR_CERT_COMMON_NAME_INVALID
This server could not prove that it is git.internal.lan; its security
certificate does not specify Subject Alternative Names.このエラーは、信頼ストアを調整しても解消できません。証明書に実際に名前が記載されていないためです。現在 NET::ERR_CERT_COMMON_NAME_INVALID が表示されている場合、証明書に SAN がないか、正しくない SAN が設定されています。新しい証明書を発行する必要があります。幸い、修正には 1 つのコマンドだけで済みます。
ブラウザーが受け入れる証明書を1コマンドで作成する
OpenSSL 1.1.1では-addextフラグが追加されたため、以前のガイドでSANを挿入するために使っていた設定ファイルの複雑な手順は不要です。Ubuntu 24.04では次のように実行します。
sudo openssl req -x509 -newkey rsa:4096 -sha256 -days 730 -noenc \
-keyout /etc/ssl/private/git.internal.key \
-out /etc/ssl/certs/git.internal.crt \
-subj "/CN=git.internal.lan" \
-addext "subjectAltName=DNS:git.internal.lan,IP:10.8.0.1"各フラグの役割は次のとおりです。
-x509は、署名要求ではなく、自己署名証明書を直接生成します。-newkey rsa:4096は、同じ手順で新しい鍵を生成します。RSA 4096は従来のクライアントでも問題ありません。接続元がすべて最新の場合は、-newkey ec -pkeyopt ec_paramgen_curve:P-256のほうが小さく高速です。-noencは、以前の-nodesに相当するOpenSSL 3.xでの表記です。鍵にパスフレーズを設定しません。どちらの表記も使用できます。パスフレーズ付きの鍵を使用すると、nginxは起動のたびに入力待ちで停止します。そのため、サーバー鍵ではこの指定を使用します。-days 730は有効期間を2年に設定します。この数値については、有効期限のセクションで詳しく説明します。-subjは対話形式の質問にコマンド内で回答します。現在、CNは装飾的な情報ですが、プライマリ名を設定してください。一部のツールではCNが表示されます。-addext "subjectAltName=..."が重要なフラグです。クライアントが入力するすべての名前とすべてのIPアドレスを列挙します。ホスト名にはDNS:のエントリを使用します(DNS:*.internal.lanのようなワイルドカードも使用できます)。アドレスにはIP:のエントリを使用します。https://10.8.0.1へアクセスする場合は、IP:10.8.0.1のエントリを必ず追加してください。DNS名だけのSANを指定すると、クライアントには再びNET::ERR_CERT_COMMON_NAME_INVALIDが発生します。
設定を行う前に、SANが実際に含まれていることを確認します。
openssl x509 -in /etc/ssl/certs/git.internal.crt -noout -ext subjectAltName正しい出力は次のとおりです。
X509v3 Subject Alternative Name:
DNS:git.internal.lan, IP Address:10.8.0.1代わりにNo extensions in certificateと表示された場合、証明書にSANがありません。ブラウザーはその証明書を拒否するため、そのまま進めずに再生成してください。
鍵を厳格に保護する
サーバー上の全ユーザーが読み取れる秘密鍵は、秘密鍵とはいえません。標準構成の Ubuntu 24.04 では、/etc/ssl/private はすでに 700 root:root になっているため、root 以外は一覧表示すらできません。ssl-cert パッケージが依存関係でインストールされた場合(apache2 が推奨しています)に限り、710 root:ssl-cert になります。いずれの場合も、ファイル自体を明示的に設定します。
sudo chown root:root /etc/ssl/private/git.internal.key
sudo chmod 600 /etc/ssl/private/git.internal.keynginx と Apache は、権限を下げる前に root として証明書を読み込むため、root:root の mode 600 で動作します。サービス固有のユーザーで実行され、そのユーザー自身が鍵を読み込むサービスの場合(Node アプリ、Gitea、Python デーモンなど)は、chown をそのサービスユーザーに変更し、mode 600 のままにします。絶対にしてはいけないことは、mode 644 にすること、git repository にコピーを置くこと、/tmp にコピーを置くことです。
nginx に組み込む
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name git.internal.lan;
ssl_certificate /etc/ssl/certs/git.internal.crt;
ssl_certificate_key /etc/ssl/private/git.internal.key;
location / {
proxy_pass http://127.0.0.1:3000;
}
}sudo nginx -t && sudo systemctl reload nginxnginx -t は、reload が処理を開始する前に syntax is ok と test is successful を出力する必要があります。代わりに SSL_CTX_use_PrivateKey_file() failed ... key values mismatch が出力される場合、証明書と鍵は異なる生成処理で作成されています。障害モードのセクションを参照してください。
Apache に組み込む
sudo a2enmod ssl proxy proxy_httpここでは ssl だけでは不十分です。以下の vhost では ProxyPass を使用します。mod_proxy と mod_proxy_http がないと、設定テストは Invalid command 'ProxyPass', perhaps misspelled or defined by a module not included in the server configuration で失敗します。vhost を /etc/apache2/sites-available/git-internal.conf として保存します。
<VirtualHost *:443>
ServerName git.internal.lan
SSLEngine on
SSLCertificateFile /etc/ssl/certs/git.internal.crt
SSLCertificateKeyFile /etc/ssl/private/git.internal.key
ProxyPass / http://127.0.0.1:3000/
ProxyPassReverse / http://127.0.0.1:3000/
</VirtualHost>sudo a2ensite git-internal
sudo apache2ctl configtest && sudo systemctl reload apache2configtest は Syntax OK に応答するはずです。次に、クライアントマシンからテストします。
curl -v https://git.internal.lan/すると、エラーが発生します。
curl: (60) SSL certificate problem: self-signed certificateこれはバグではありません。TLS が正しく動作しているためです。curl は証明書を認識したことがなく、認証できないサーバーとの通信を拒否します。次のセクションで本当の解決方法を説明します。これは、今この瞬間にインターネット上の半分の環境で行われている方法ではありません。
クライアントに証明書を信頼させる方法と、避けるべきアンチパターン
まず、誤った修正方法を正しく理解します。スクリプトに curl -k(または --insecure)を記述する方法、Python requests で verify=False を指定する方法、Node で NODE_TLS_REJECT_UNAUTHORIZED=0 を指定する方法は、いずれも証明書を信頼させるものではありません。これらは証明書の検証を無効化します。そのため、クライアントは、攻撃者が通信経路に配置した証明書を含め、どのサーバーが提示したどの証明書でも受け入れて通信します。TLS のオーバーヘッドだけが残り、TLS の目的である認証を失います。さらに、これらのフラグは広がりやすい点も問題です。1 つの cron ジョブに貼り付けた設定が、デプロイスクリプト、さらに本番コードへとコピーされ、どの接続を一時的に許可する予定だったのか誰も覚えていない状態になります。デバッグのために追加した verify=False がデバッグ終了後も残っているなら、設計が誤っています。
正しい修正方法は、各クライアント OS にこの証明書を信頼できる root として登録することです。Ubuntu および Debian クライアントでは、次を実行します。
sudo cp git.internal.crt /usr/local/share/ca-certificates/git.internal.crt
sudo update-ca-certificates出力で確認すべき行は次の行です(その後に Running hooks in /etc/ca-certificates/update.d... ブロックが続きます)。
Updating certificates in /etc/ssl/certs...
1 added, 0 removed; done.この手順には 2 つの注意点があります。ファイルの末尾は必ず .crt にしてください。.pem 拡張子は無視されるため、エラーメッセージなしで 0 added になります。また、内容は PEM 形式でなければなりません。ファイルは -----BEGIN CERTIFICATE----- で始まります。DER バイナリの場合は、まず openssl x509 -inform der -in file.der -out file.crt で変換します。自己署名証明書自体を root として追加できるのは、自己署名証明書がそれ自体の root だからです。
この設定後は、curl、wget、git、apt、その他の OpenSSL を使ってシステムの証明書バンドルを参照するクライアントが、フラグなしでサーバーを信頼します。一部のクライアントは独自の trust store を持つため、個別の設定が必要です。
- Linux 上の Chrome/Chromium はシステムの store ではなく NSS データベースを読み取ります。
sudo apt install libnss3-toolsを実行し、ユーザーごとにcertutil -d sql:$HOME/.local/share/pki/nssdb -A -t "C,," -n "git.internal" -i git.internal.crtを実行します。このパスは version 146 以降の Chrome のデフォルトです。ただし、古いインストール時の~/.pki/nssdbが残っている場合、Chrome は代わりにそのファイルを読み続けます。そのため、そのマシンでは-dをsql:$HOME/.pki/nssdbに指定します。 - Firefox は独自の store を使用します。Settings → Privacy & Security → Certificates → Import を選択します。Windows と macOS では、
about:configでsecurity.enterprise_roots.enabledをtrueに変更し、OS の store を読み取らせることもできます。Mozilla がこの設定を文書化しているのは Windows と macOS であり、Linux ではありません。そのため、Ubuntu では import を使用します。 - Python requests は独自の CA bundle(certifi)を同梱し、システムの store を無視します。
verify="/usr/local/share/ca-certificates/git.internal.crt"を渡すか、REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crtを export します。 - Node.js はデフォルトで同梱された独自の root を使用します。
NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/git.internal.crtを export するか、--use-system-caフラグ(Linux 上の Node 23.9 以降)またはNODE_USE_SYSTEM_CA=1(Node 24.6、および 22.19 以降)を使用して、更新したシステムの store を読み取らせます。
Windows クライアントでは、.crt をダブルクリックし、Trusted Root Certification Authorities にインストールします。macOS では、Keychain Access で System keychain に追加し、Always Trust に設定します。
多数のサービスを 1 つの root で運用する: 小規模なプライベート CA
証明書ごとに信頼設定を行う方法は、すぐに拡張できなくなります。6 個のサービスと 4 台のクライアントマシンでは、信頼設定が 24 件必要です。新しいサービスを追加するたびに、さらに増えます。解決策はプライベート CA です。クライアントは 1 つの root だけを信頼し、各サービスの証明書をその root で署名します。
手軽な方法は mkcert です。Ubuntu 24.04 のリポジトリに含まれており、update-ca-certificates が扱えない NSS ストア(Firefox、および以下の注意点を除く Chrome)にも対応します。
sudo apt install -y mkcert libnss3-tools
mkcert -install
mkcert git.internal.lan "*.internal.lan" 10.8.0.1mkcert -install は root を作成し、システムストア、検出できるブラウザーの NSS ストア、および JAVA_HOME が設定されている場合は Java のストアにも登録します。Node は指示しない限りシステムストアを読み込まないため、前述のとおり --use-system-ca(または NODE_USE_SYSTEM_CA=1)を指定して Node アプリを起動するか、NODE_EXTRA_CA_CERTS="$(mkcert -CAROOT)/rootCA.pem" を export します。3 番目のコマンドは git.internal.lan+2.pem と git.internal.lan+2-key.pem を生成します。これらは前述の nginx または Apache の設定にそのまま追加できます。このツールは開発マシンを前提として設計されています。root key は -install を実行したマシンに保存されます。そのため、開発用ラップトップには最適ですが、サーバーフリートには適していません。
2026 年 10 月時点の注意点として、mkcert は Chrome の NSS データベースを、従来の ~/.pki/nssdb パスでのみ検索します。Chrome は version 146 以降、デフォルトで ~/.local/share/pki/nssdb を使用します。従来のデータベースがないマシンでは、前のセクションにある certutil コマンドを使って、Chrome に root を自分で追加します。-i "$(mkcert -CAROOT)/rootCA.pem" を指定してください。
サーバーでは、OpenSSL だけで 5 個のコマンドにより CA 全体を構築できます。
openssl genrsa -out lab-ca.key 4096
openssl req -x509 -new -key lab-ca.key -sha256 -days 3650 \
-out lab-ca.crt -subj "/CN=Lab Internal CA" \
-addext "basicConstraints=critical,CA:TRUE,pathlen:0" \
-addext "keyUsage=critical,keyCertSign,cRLSign"
openssl genrsa -out git.key 2048
openssl req -new -key git.key -out git.csr -subj "/CN=git.internal.lan" \
-addext "subjectAltName=DNS:git.internal.lan,IP:10.8.0.1"
openssl x509 -req -in git.csr -CA lab-ca.crt -CAkey lab-ca.key \
-CAcreateserial -days 730 -sha256 -copy_extensions copy -out git.crt最後のコマンドには注意が必要です。openssl x509 -req はデフォルトで CSR からすべての拡張を削除します。慎重に追加した SAN も対象です。-copy_extensions copy は OpenSSL 3.x のオプションで、24.04 で使用できます。このオプションにより拡張が引き継がれます。省略すると、署名済み証明書に SAN がなくなり、Chrome は再び NET::ERR_CERT_COMMON_NAME_INVALID と表示します。前と同じ openssl x509 -noout -ext subjectAltName のチェックで確認してください。
lab-ca.crt は、上記の信頼ストア設定を使ってクライアントに配布します。各マシンで 1 回設定すれば、以後は不要です。現在は重要な鍵となっている lab-ca.key を厳重に保護してください。mode 600 にし、可能であれば、署名対象のサーバーではないマシンに保管します。この鍵を持つ者は、クライアントが信頼する任意の名前の証明書を発行できるためです。
有効期限とローテーション
公開CA証明書の有効期間は短縮されています。CA/Browser Forumは、2026年3月に新規発行する公開信頼証明書の上限を200日に設定しました。これは従来の398日からの短縮です。2027年には100日、2029年3月までに47日へ段階的に短縮されます。ただし、これらの規則が適用されるのは公開信頼CAです。プライベートCAは対象外で、ブラウザも手動でインストールしたルート証明書に対してこれらの規則を強制しません。実際に適用される制約が1つあります。Appleのプラットフォームは、発行者に関係なく、有効期間が825日を超えるTLSサーバー証明書を拒否します。そのため、iPhoneやMacから接続する場合は、リーフ証明書の有効期間を2年以下にしてください。-days 730なら、すべての環境でこの条件を満たします。ルート証明書を10年、リーフ証明書を2年とする構成は、内部用途として無理がありません。
有効期間の長い証明書は、1つの方法でのみ問題になります。誰も設定した覚えのない日に、通知なしで一斉に無効になります。現在の設定を確認します。
openssl x509 -in /etc/ssl/certs/git.internal.crt -noout -enddate更新を実際のカレンダーに登録するか、cronで30日前に通知させてください。openssl x509 -checkend 2592000 -in cert.crtは、有効期限までの残り時間が指定した秒数以内になると、ゼロ以外の終了コードで終了します。すでにステータス監視にUptime Kumaを使用している場合は、HTTPSモニターが証明書の有効期限の接近を無料で通知します。
プライベートCAでのローテーションは単純です。CSRの作成と署名のコマンドを再実行し、ファイルを置き換えて、Webサーバーをreloadします。ルート証明書は変わらないため、クライアント側では何も起こりません。
エラーの種類と表示されるメッセージ
NET::ERR_CERT_AUTHORITY_INVALID は、信頼設定を行う前に想定される状態です。証明書の不具合ではありません。root を追加した後も続く場合は、次の原因が考えられます。Linux では Chrome はシステムの証明書ストアではなく NSS を読み取ります(certutil の手順を参照)。Chrome 146 以降、このデータベースは ~/.local/share/pki/nssdb にあります。ただし、古い ~/.pki/nssdb が存在する場合を除きます。そのため、別のパスに追加した root は認識されません。または、コピーしたファイルの末尾が .crt ではなく、update-ca-certificates の結果が 0 added だった可能性があります。サーバーが、信頼した証明書とは別の証明書を提示している可能性もあります。openssl s_client -connect git.internal.lan:443 </dev/null 2>/dev/null | openssl x509 -noout -fingerprint -sha256 でフィンガープリントを比較してください。
NET::ERR_CERT_COMMON_NAME_INVALID は、証明書に SAN がないか、SAN がアドレスバーの名前を対象にしていないことを示します。典型的な例は、SAN に DNS:git.internal.lan が記載されているのに、ユーザーが https://10.8.0.1 にアクセスした場合です。信頼ストアを変更しても解決できません。不足しているエントリを追加して証明書を再発行してください。
curl: (60) SSL certificate problem: self-signed certificate は、curl が証明書を信頼していないことを示します。プライベート CA が署名した証明書では、self-signed certificate in certificate chain も同じ意味です。一時的な対処は curl --cacert lab-ca.crt https://... です。恒久的な対処は信頼ストアを設定することです。-k ではありません。
unable to load certificate ... Expecting: TRUSTED CERTIFICATE(または Expecting: CERTIFICATE REQUEST、no start line)は、PEM の形式が正しくないことを示します。証明書を想定している OpenSSL に、鍵や CSR、または PEM を想定している場所に DER バイナリを渡しています。head -1 filename で実際のファイル形式を確認できます。証明書は -----BEGIN CERTIFICATE----- で始まります。DER の場合は openssl x509 -inform der -in file.der -out file.crt で変換してください。
nginx: [emerg] SSL_CTX_use_PrivateKey_file(...) failed (SSL: error ... key values mismatch) は、証明書と鍵が対応していないことを示します。通常は、生成コマンドを 2 回実行してファイルが混在したことが原因です。openssl x509 -in git.internal.crt -noout -pubkey | sha256sum と openssl pkey -in git.internal.key -pubout | sha256sum を比較して確認してください。ハッシュが一致すれば、証明書と鍵は対応しています。一致しない場合は、両方を同時に再生成してください。
FAQ
Chrome で自己署名証明書を作成した後も「安全ではありません」と表示されるのはなぜですか?
エラーが NET::ERR_CERT_AUTHORITY_INVALID の場合、証明書に問題はありません。Chrome がまだその証明書を信頼する理由を持っていないだけです。クライアントのトラストストアに証明書(またはプライベート CA のルート証明書)をインストールしてください。Linux では、Chrome はシステムストアではなく、certutil を介して NSS データベースを使用します。Chrome 146 以降は ~/.local/share/pki/nssdb にあり、存在する場合は従来の ~/.pki/nssdb が使用されます。エラーが NET::ERR_CERT_COMMON_NAME_INVALID の場合、証明書に URL と一致する Subject Alternative Name がありません。-addext "subjectAltName=..." を付けて再発行する必要があります。
-k を使わずに curl で自己署名証明書を信頼させるにはどうすればよいですか?
証明書(PEM 形式、拡張子は .crt)を /usr/local/share/ca-certificates/ にコピーし、sudo update-ca-certificates を実行します。出力には 1 added と表示される必要があります。以後、curl は公開証明書と同じようにその証明書を検証します。システムに変更を加えず、1 回だけ実行する場合は、curl --cacert /path/to/cert.crt でそのファイルだけを使って検証できます。-k は検証を完全に無効にするため、スクリプトでは使用しないでください。
自己署名証明書の有効期間はどのくらいにできますか?
技術的には任意の期間にできます。CA/Browser Forum の制限(現在は 200 日、2029 年には 47 日)は、公開信頼された CA に適用されるものであり、プライベートな信頼には適用されません。実際の運用では、サーバー証明書の有効期間を 825 日以内にしてください。発行者に関係なく、それを超える証明書を Apple デバイスが拒否するためです。10 年間有効なプライベートルート証明書と、2 年間有効な(-days 730)リーフ証明書の組み合わせが妥当な既定値です。ただし、更新日を予定表に登録してください。内部証明書の期限が切れると、誰も覚えていない日にすべてのサービスが気付かないまま停止する可能性があります。
自己署名証明書と Let's Encrypt のどちらを使うべきですか?
サービスに公開 DNS 名があり、インターネットから到達可能なら、常に Let's Encrypt を使用してください。無料で自動化でき、多くのブラウザーとオペレーティングシステムがすでに信頼しています。非常に古いデバイスから接続する必要がある場合は、Let's Encrypt の互換性一覧を確認してください。自己署名証明書(またはプライベート CA)は、Let's Encrypt が発行できない用途に使います。たとえば、プライベート IP、.lan のような内部専用ホスト名、エアギャップネットワーク、VPN の背後に意図的に隠したサービスです。判断基準は到達性と名前であり、セキュリティ強度ではありません。暗号方式は同一です。
/usr/local/share/ca-certificates に追加した後も証明書が拒否されるのはなぜですか?
3 点を確認してください。ファイルの末尾は .crt でなければなりません。.pem 拡張子のファイルは警告なしにスキップされ、update-ca-certificates は 0 added を報告します。内容は -----BEGIN CERTIFICATE----- で始まる PEM テキストでなければならず、DER バイナリでは使用できません。また、アプリケーションが実際にシステムストアを使用している必要があります。Linux 上の Chrome、Firefox、Python requests、Node.js(--use-system-ca で起動した場合を除く)、Java はそれぞれ独自のトラストストアを保持しているため、証明書を個別に追加する必要があります。