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 を搭載した新しい Ubuntu 24.04 サーバーで実行します(確認には openssl version を使用します)。ここで説明する処理にインターネットアクセスは必要ありません。すべてエアギャップ環境で動作します。
古いワンライナーで 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 はバージョン 58 である 2017年4月以降、Common Name を参照しなくなりました。RFC 2818 も 2000年の時点で CN による照合を非推奨にしていました。Firefox、Safari、curl、Python も同じ動作をします。証明書は SAN 拡張によってサーバーを識別します。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 では、/etc/ssl/private はすでに 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 リポジトリにコピーを置くこと、/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 job に貼り付けられた後、deploy script、さらに本番コードへとコピーされ、どの接続が一時的なものだったのか誰も把握できなくなります。デバッグセッションの終了後も 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 database を読み取ります。
sudo apt install libnss3-toolsを実行し、ユーザーごとにcertutil -d sql:$HOME/.pki/nssdb -A -t "C,," -n "git.internal" -i git.internal.crtを実行します。 - Firefox には独自の store があります。Settings → Privacy & Security → Certificates → Import の順に選択するか、
about:configでsecurity.enterprise_roots.enabledをtrueに変更してシステムの store を読み取らせます。 - 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:
NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/git.internal.crtを export します。
Windows クライアントでは、.crt をダブルクリックし、Trusted Root Certification Authorities にインストールします。macOS では、Keychain Access で System keychain に追加し、Always Trust に設定します。
多数のサービスで1つのrootを使う: 小規模なプライベートCA
証明書ごとの信頼設定は、すぐに拡張の限界に達します。6個のサービスと4台のクライアントマシンでは、信頼設定が24件必要です。新しいサービスを追加するたびに、さらに増えます。解決策はプライベートCAです。クライアントは1つのrootだけを信頼し、各サービスの証明書にそのCAで署名します。
使いやすい選択肢はmkcertです。Ubuntu 24.04のリポジトリに含まれており、update-ca-certificatesが対応していないNSSストア(Chrome、Firefox)も処理できます。
sudo apt install -y mkcert libnss3-tools
mkcert -install
mkcert git.internal.lan "*.internal.lan" 10.8.0.1mkcert -installはrootを作成し、そのマシン上のすべての信頼ストアに登録します。3番目のコマンドはgit.internal.lan+2.pemとgit.internal.lan+2-key.pemを生成します。これらは、前述のnginxまたはApacheの設定にそのまま配置できます。この設計は開発マシンを前提としています。root keyは-installを実行したマシンに保存されます。そのため、開発用ノートPCには最適ですが、サーバーフリートには適していません。
サーバーでは、OpenSSLだけでCA全体を5つのコマンドで構築できます。
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を厳重に保護してください。モードは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の手順を参照してください)。または、コピーしたファイルの末尾が.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が証明書を信頼していないことを示します。self-signed certificate in certificate chainという変種も、プライベートCAで署名された証明書では同じ意味です。一時的な修正はcurl --cacert lab-ca.crt https://...です。恒久的な修正は信頼ストアを設定することです。-kではありません。
unable to load certificate ... Expecting: TRUSTED CERTIFICATE(またはExpecting: CERTIFICATE REQUEST、no start line)は、PEMの種類が一致していないことを示します。証明書を想定しているOpenSSLに、keyや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) は、証明書とkeyが対応していないことを示します。通常は、生成コマンドを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データベースを使用する点にも注意してください。エラーが 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を使用してください。無料で自動化でき、すべてのクライアントですでに信頼されています。自己署名証明書またはプライベート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、Javaはそれぞれ独自の信頼ストアを保持するため、証明書を個別に追加する必要があります。