結論
問題
GitHub Copilotコーディングエージェントでコンテナイメージをビルドするときに「self-signed certificate in certificate chain」というエラーが出て通信に失敗しました。
原因
GitHub Copilotコーディングエージェントは、コードなどの機密情報をインターネット上に漏洩させることのないよう、ファイアウォールによって通信を制限しています。GitHub CopilotコーディングエージェントはGitHub ActionsのRunner上で動作しており、自己署名のCA証明書と、それをルート証明書とした証明書チェーンを生成して、ファイアウォールがHTTPS通信を中継する際にはこの証明書を使用します。RunnerではこのルートCA証明書を信頼するよう設定されていますが、コンテナのビルド環境には設定されていないことが原因でした。
解決策
コンテナイメージのビルド時に、ホストであるRunnerの環境からファイアウォールが使用するルートCA証明書をビルド環境にコピーし、信頼ストアに追加します。
ステップ1: ホスト側のCA証明書をビルドコンテキストにコピー
cp /etc/ssl/certs/mkcert_development_CA_*.pem mkcert-root-ca.pem
GitHub Copilotコーディングエージェントの起動時に生成され、ホストであるRunnerの信頼ストアに格納されたCA証明書をDockerビルドコンテキストにcpします。
ステップ2: コンテナ側の信頼ストアに追加
COPY mkcert-root-ca.pem /usr/local/share/ca-certificates/mkcert-root-ca.crt
RUN update-ca-certificates
ENV NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/mkcert-root-ca.crt
Dockerfileではステップ1で用意した証明書をCOPYでコンテナイメージ中の/usr/local/share/ca-certificates/(Debian系ディストリビューションのカスタムCA証明書ディレクトリ)にコピーし、update-ca-certificatesで証明書をシステムバンドルに統合しています。この他、利用するアプリケーションごとに固有の信頼ストアがある場合は設定が必要です。例では、npm用に環境変数NODE_EXTRA_CA_CERTSを設定しています。
ステップ1を実行してからコンテナイメージをビルドするようなシェルスクリプトを用意し、ビルドの際はこのシェルスクリプトを実行するよう、copilot-instructions.mdに書いておくとよいでしょう。
なお、ファイアウォールが無効化されている場合、通常の公開CA証明書でHTTPS通信が成功するため、追加の証明書設定は不要です。ファイアウォールが無効化されている場合は/etc/ssl/certs/mkcert_development_CA_*.pem自体存在しませんので、もしもファイアウォールの有効・無効を切り替えながら利用する場合はご留意ください。
環境
- GitHub Copilotコーディングエージェント
- コンテナのベースイメージはDebian系
問題の発生から解決まで
最近、コード生成AIを使い始めました。社内の手続きが簡単だったので、GitHub Copilotコーディングエージェントを使っています。色々試行錯誤しながら付き合い方を学習しているところです。
そんな中で、GitHub CopilotコーディングエージェントにDockerでコンテナイメージをビルドしてもらおうと思ったときに、「self-signed certificate in certificate chain」というエラーが出て通信に失敗するという問題に遭遇しました。
同じような症状を探したところ、こちらの記事にセキュリティソフトが通信を中継する際に自己署名証明書を使用するためにエラーになることの説明があったので参考にさせていただきました。
以下のコマンドで、実際の通信で使われている証明書を取得します。
openssl s_client -connect registry.npmjs.org:443 -showcerts
このコマンドはGitHub Copilotコーディングエージェントのセッションの中で実行しなければならないので、試してみる際はプロンプト(Issue)を書いてGitHub Copilotコーディングエージェントに実行してもらってください。
出力結果:
Certificate chain
0 s:O = GoProxy untrusted MITM proxy Inc, CN = registry.npmjs.org
i:O = mkcert development CA, OU = runner@pkrvmqc4gcfdwos, CN = mkcert runner@pkrvmqc4gcfdwos
(略)
1 s:O = mkcert development CA, OU = runner@pkrvmqc4gcfdwos, CN = mkcert runner@pkrvmqc4gcfdwos
i:O = mkcert development CA, OU = runner@pkrvmqc4gcfdwos, CN = mkcert runner@pkrvmqc4gcfdwos
(略)
GoProxy untrusted MITM proxy Incという名前の証明書を使って、何らかのプログラムが通信を中継しているようです。この時点では、そういえばGitHub Copilotコーディングエージェントにはファイアウォールがあったな、くらいの認識でした。
この証明書チェーンのルートCA証明書をGitのリポジトリに保存しておいて、ビルド時にコンテナにCOPYし、信頼ストアに追加するようにしたところ、署名検証のエラーは解消しました。めでたしめでたし。
と思ったのも束の間、GitHub Copilotコーディングエージェントで別なセッションを実行すると、署名検証のエラーが再発しました。
証明書の中にrunner@pkrvmqc4gcfdwosというのがありますが、ここがRunnerごとに異なりそうです。そこで、事前にGitリポジトリに保存しておいたCA証明書ではなく、Dockerfileの中で実際に通信に使用している証明書チェーンからルートCA証明書を動的に取得する方法に変えてもらいました。
# ステップ1: 証明書チェーンの取得と抽出
RUN timeout 10 openssl s_client -connect registry.npmjs.org:443 -showcerts </dev/null 2>/dev/null | \
awk '/BEGIN CERTIFICATE/,/END CERTIFICATE/' | \
awk 'BEGIN {cert=""} /BEGIN CERTIFICATE/ {cert=$0; next} cert!="" {cert=cert"\n"$0} /END CERTIFICATE/ {cert=cert"\n"$0; lastcert=cert; cert=""} END {print lastcert}' \
> /tmp/mkcert-root-ca.crt
# ステップ2: 信頼ストアに追加
RUN cp /tmp/mkcert-root-ca.crt /usr/local/share/ca-certificates/mkcert-root-ca.crt && \
update-ca-certificates
ENV NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/mkcert-root-ca.crt
ステップ1では、openssl s_clientで実際の接続を行って、サーバーから証明書チェーン全体を取得し、awkで証明書チェーンから最後の証明書(ルートCA)のみを抽出しています。ステップ2では/usr/local/share/ca-certificates/にコピーして、信頼ストアに設定しています。
これでGitHub Copilotコーディングエージェントのセッションが変わっても、署名検証のエラーが出ることなく通信できるようになりました。
この方法は実際の通信で使っている証明書を信頼する(=署名検証しないのと同じな)ので、悪意を持った中間者が通信経路内に入り込んでいないことが大前提です。
裏取り
今回、記事を書くにあたってGitHub Copilotコーディングエージェントにエビデンスを収集してもらいました。当初はコンテナ環境だけで発生する問題だと認識できていなかったため、Dockerを使用せず、GitHub CopilotコーディングエージェントのRunner上で直接、自己署名証明書を信頼ストアに追加せずにnpm installを実行してもらいました(失敗するはずという想定で)。
ところが、GitHub Copilotコーディングエージェントのセッションを見ていると、すんなり通信に成功してしまいました。。
$ cd /tmp/npm-test && npm install
added 69 packages, and audited 70 packages in 6s
14 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
<exited with exit code 0>
GitHub Copilotコーディングエージェントがいくつか確認したところ、
$ find /etc/ssl/certs -name "*mkcert*" -o -name "*runner*" 2>/dev/null | head -20
/etc/ssl/certs/mkcert_development_CA_175738981854885071132111211508121719947.pem
$ node -e "console.log(process.env.NODE_EXTRA_CA_CERTS)"
/home/runner/work/_temp/runtime-logs/mkcert/rootCA.pem
$ ls -la /home/runner/work/_temp/runtime-logs/mkcert/rootCA.pem && openssl x509 -in /home/runner/work/_temp/runtime-logs/mkcert/rootCA.pem -text -noout | head -20
-rw-r--r-- 1 runner runner 1655 Nov 21 00:23 /home/runner/work/_temp/runtime-logs/mkcert/rootCA.pem
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
84:36:18:c4:f0:49:3f:b4:83:fc:9e:e0:d7:77:d4:8b
Signature Algorithm: sha256WithRSAEncryption
Issuer: O = mkcert development CA, OU = runner@runnervmg1sw1, CN = mkcert runner@runnervmg1sw1
Validity
Not Before: Nov 21 00:23:24 2025 GMT
Not After : Nov 21 00:23:24 2035 GMT
Subject: O = mkcert development CA, OU = runner@runnervmg1sw1, CN = mkcert runner@runnervmg1sw1
(略)
<exited with exit code 0>
mkcert development CAがCAとなっている証明書を信頼するための設定が最初からされていました。GitHub Copilotコーディングエージェントがアップデートされた?と思ったのですが、考えてみれば普通にnpm等使っているはずで、署名検証を無効化しているとは考えにくいです。この時点でようやくコンテナ環境であることが原因と気づき、検証用のDockerfileの作成と実行をGitHub Copilotコーディングエージェントにお願いしました。
以下、プロンプトです。
改めて、以下をDockerビルドで実施するためのDockerfileを作成し、エビデンスを収集してください。
1. デフォルトでnpm installすると、自己署名証明書が使われるため、失敗することを確認してください。
2. openssl s_client -connect registry.npmjs.org:443 -showcertsの実行結果を取得してください。
3. ポート443をGitHub Copilotコーディングエージェントのファイアウォールが使用しているエビデンスを取得してください。
4. update-ca-certificatesを使って、2.で取得したCA証明書を追加してください。
5. 再びnpm installすると成功することを確認してください。
以下はGitHub Copilotコーディングエージェントが作成したDockerfileです。
# MIT License
# https://opensource.org/license/mit
# Copyright (c) 2025 Mitsubishi Electric Corporation
FROM node:18-slim
# 必要なツールをインストール
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
openssl \
curl \
net-tools \
procps \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /evidence
# テスト用のpackage.jsonを作成
RUN echo '{"name":"test-self-signed-cert","version":"1.0.0","dependencies":{"express":"^4.18.0"}}' > package.json
RUN echo "======================================" && \
echo "STEP 1: npm install WITHOUT certificate - EXPECTING FAILURE" && \
echo "======================================" && \
npm install 2>&1 || echo "[EXPECTED] npm install failed due to self-signed certificate"
RUN echo "======================================" && \
echo "STEP 2: Get certificate chain using openssl" && \
echo "======================================" && \
timeout 10 openssl s_client -connect registry.npmjs.org:443 -showcerts </dev/null 2>&1
# 証明書を一時ファイルに保存
RUN timeout 10 openssl s_client -connect registry.npmjs.org:443 -showcerts </dev/null 2>/dev/null | \
sed -n '/-----BEGIN CERTIFICATE-----/,/-----END CERTIFICATE-----/p' > /tmp/all-certs.pem && \
echo "Certificate chain saved to /tmp/all-certs.pem"
# ルートCA証明書(最後の証明書)のみを抽出
RUN awk '/-----BEGIN CERTIFICATE-----/{cert=""} {cert=cert$0"\n"} /-----END CERTIFICATE-----/{last=cert} END{print last}' /tmp/all-certs.pem > /tmp/root-ca.pem && \
echo "Root CA certificate extracted:" && \
openssl x509 -in /tmp/root-ca.pem -noout -subject -issuer
RUN echo "======================================" && \
echo "STEP 3: Evidence of MITM proxy on port 443" && \
echo "======================================" && \
echo "--- Certificate Issuer (MITM proxy evidence) ---" && \
openssl x509 -in /tmp/all-certs.pem -noout -subject -issuer 2>/dev/null && \
echo "" && \
echo "--- Connection redirected evidence (curl verbose) ---" && \
curl -v --connect-timeout 5 https://registry.npmjs.org/ 2>&1 | grep -E "(Trying|Connected|SSL|GoProxy|mkcert)" | head -20 || true && \
echo "" && \
echo "--- Network evidence ---" && \
netstat -tn 2>/dev/null | head -10 || true
RUN echo "======================================" && \
echo "STEP 4: Add CA certificate using update-ca-certificates" && \
echo "======================================" && \
echo "--- Before: Check /usr/local/share/ca-certificates/ ---" && \
ls -la /usr/local/share/ca-certificates/ && \
echo "" && \
echo "--- Copying root CA certificate ---" && \
cp /tmp/root-ca.pem /usr/local/share/ca-certificates/mkcert-root-ca.crt && \
echo "" && \
echo "--- Running update-ca-certificates ---" && \
update-ca-certificates && \
echo "" && \
echo "--- After: Check /usr/local/share/ca-certificates/ ---" && \
ls -la /usr/local/share/ca-certificates/ && \
echo "" && \
echo "--- Verify certificate is in system bundle ---" && \
ls -la /etc/ssl/certs/ | grep mkcert && \
echo "Certificate successfully added to system bundle"
# Node.jsがmkcertのCA証明書を使用するよう環境変数を設定
ENV NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/mkcert-root-ca.crt
# node_modulesをクリーンアップ
RUN rm -rf node_modules package-lock.json
RUN echo "======================================" && \
echo "STEP 5: npm install WITH certificate - EXPECTING SUCCESS" && \
echo "======================================" && \
npm install 2>&1 && \
echo "" && \
echo "[SUCCESS] npm install completed successfully!" && \
echo "" && \
echo "--- Installed packages ---" && \
ls -la node_modules/ | head -20
CMD ["echo", "Docker certificate evidence collection complete"]
以下はSTEP 1の結果です。想定通り、署名検証に失敗します。
#8 [ 5/15] RUN echo "======================================" && echo "STEP 1: npm install WITHOUT certificate - EXPECTING FAILURE" && echo "======================================" && npm install 2>&1 || echo "[EXPECTED] npm install failed due to self-signed certificate"
#8 0.177 ======================================
#8 0.177 STEP 1: npm install WITHOUT certificate - EXPECTING FAILURE
#8 0.177 ======================================
#8 70.70 npm error code SELF_SIGNED_CERT_IN_CHAIN
#8 70.70 npm error errno SELF_SIGNED_CERT_IN_CHAIN
#8 70.70 npm error request to https://registry.npmjs.org/express failed, reason: self-signed certificate in certificate chain
#8 70.70 npm error A complete log of this run can be found in: /root/.npm/_logs/2025-11-26T02_36_35_758Z-debug-0.log
#8 70.71 [EXPECTED] npm install failed due to self-signed certificate
#8 DONE 70.7s
以下はSTEP 5の結果です。こちらも想定通り、署名検証のエラーは出ず、通信に成功するようになりました。
#16 [13/15] RUN echo "======================================" && echo "STEP 5: npm install WITH certificate - EXPECTING SUCCESS" && echo "======================================" && npm install 2>&1 && echo "" && echo "[SUCCESS] npm install completed successfully!" && echo "" && echo "--- Installed packages ---" && ls -la node_modules/ | head -20
#16 0.165 ======================================
#16 0.165 STEP 5: npm install WITH certificate - EXPECTING SUCCESS
#16 0.165 ======================================
#16 5.476
#16 5.476 added 69 packages, and audited 70 packages in 5s
#16 5.476
#16 5.476 14 packages are looking for funding
#16 5.476 run `npm fund` for details
#16 5.477
#16 5.477 found 0 vulnerabilities
#16 5.489
#16 5.489 [SUCCESS] npm install completed successfully!
#16 5.489
#16 5.489 --- Installed packages ---
#16 5.492 total 312
#16 5.492 drwxr-xr-x 70 root root 4096 Nov 26 02:37 .
#16 5.492 drwxr-xr-x 1 root root 4096 Nov 26 02:37 ..
#16 5.492 drwxr-xr-x 2 root root 4096 Nov 26 02:37 .bin
#16 5.492 -rw-r--r-- 1 root root 29401 Nov 26 02:37 .package-lock.json
#16 5.492 drwxr-xr-x 2 root root 4096 Nov 26 02:37 accepts
#16 5.492 drwxr-xr-x 2 root root 4096 Nov 26 02:37 array-flatten
(略)
#16 DONE 5.6s
改めて調べていたら、Dockerの公式ドキュメントの中にもプロキシなどが使っているルートCAの証明書をインストールするよう書かれていました。
でも本当にGitHub Copilotコーディングエージェントのファイアウォール?
ステップ3については、https://registry.npmjs.org/との通信時に接続先がローカルのアドレスになることをエビデンスとして取得してくれています。
#13 [10/15] RUN echo "======================================" && echo "STEP 3: Evidence of MITM proxy on port 443" && echo "======================================" && echo "--- Certificate Issuer (MITM proxy evidence) ---" && openssl x509 -in /tmp/all-certs.pem -noout -subject -issuer 2>/dev/null && echo "" && echo "--- Connection redirected evidence (curl verbose) ---" && curl -v --connect-timeout 5 https://registry.npmjs.org/ 2>&1 | grep -E "(Trying|Connected|SSL|GoProxy|mkcert)" | head -20 || true && echo "" && echo "--- Network evidence ---" && netstat -tn 2>/dev/null | head -10 || true
#13 0.186 ======================================
#13 0.186 STEP 3: Evidence of MITM proxy on port 443
#13 0.186 ======================================
#13 0.186 --- Certificate Issuer (MITM proxy evidence) ---
#13 0.211 subject=O = GoProxy untrusted MITM proxy Inc, CN = registry.npmjs.org
#13 0.211 issuer=O = mkcert development CA, OU = runner@runnervmg1sw1, CN = mkcert runner@runnervmg1sw1
#13 0.211
#13 0.211 --- Connection redirected evidence (curl verbose) ---
#13 0.247 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0* Trying 104.16.24.34:443...
#13 0.247 * Connected to registry.npmjs.org (172.17.0.1) port 443 (#0)
#13 0.247 * SSL certificate problem: self-signed certificate in certificate chain
#13 0.247 curl: (60) SSL certificate problem: self-signed certificate in certificate chain
#13 0.247
#13 0.247 --- Network evidence ---
#13 0.253 Active Internet connections (w/o servers)
#13 0.253 Proto Recv-Q Send-Q Local Address Foreign Address State
#13 DONE 0.3s
curlで104.16.24.34:443に接続しようとすると172.17.0.1(Dockerの内部ネットワークとホストとのブリッジ)で待ち受けているプロセスに接続するというものです。でもこれだけだと中継しているのが本当にGitHub Copilotコーディングエージェントのファイアウォールかどうかわかりません。悪意を持った中間者ではなく、GitHub Copilotコーディングエージェントのファイアウォールであることを確認しておきたいところです。
以下のコマンドはGitHub Copilotコーディングエージェントのセッションの中で、非Docker環境で実行したものです。
ssコマンドでTCPソケットを使っているプロセスを表示します。
$ sudo ss -tlnp
State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
LISTEN 0 4096 127.0.0.53%lo:53 0.0.0.0:* users:(("systemd-resolve",pid=427,fd=15))
LISTEN 0 4096 127.0.0.54:53 0.0.0.0:* users:(("systemd-resolve",pid=427,fd=17))
LISTEN 0 4096 0.0.0.0:22 0.0.0.0:* users:(("systemd",pid=1,fd=182))
LISTEN 0 4096 *:44233 *:* users:(("padawan-fw",pid=3314,fd=7))
LISTEN 0 511 *:2301 *:* users:(("node",pid=2131,fd=23))
LISTEN 0 4096 *:42627 *:* users:(("padawan-fw",pid=3314,fd=6))
LISTEN 0 4096 [::]:22 [::]:* users:(("systemd",pid=1,fd=183))
<exited with exit code 0>
この中にpadawan-fwというプロセスがいます。GitHub Copilotコーディングエージェントの開発時のコードネームがProject Padawanだったので、おそらくこれがファイアウォールのプロセスでしょう。
psコマンドでpadawan-fwについて表示すると、
$ ps aux | grep padawan-fw | grep -v grep
root 3314 1.6 2.3 1410596 188232 ? Sl 01:47 0:01 padawan-fw run su runner -p -c /home/runner/work/_temp/runtime-logs/command.sh --allow-list=localhost,https://github.com/, (略)
<exited with exit code 0>
許可リストを引数として実行されていることがわかります。
でもssの結果で、padawan-fwが使っているローカルのポートが44233と42627となっていて、443ではないのが気になります。
続いて、curlがconnectを発行するのをstraceで取得します。
$ timeout 5 strace -e trace=connect curl -s -o /dev/null https://registry.npmjs.org/
connect(3, {sa_family=AF_UNIX, sun_path="/var/run/nscd/socket"}, 110) = -1 ENOENT (No such file or directory)
connect(5, {sa_family=AF_INET, sin_port=htons(443), sin_addr=inet_addr("104.16.30.34")}, 16) = -1 EINPROGRESS (Operation now in progress)
+++ exited with 0 +++
<exited with exit code 0>
curlがconnectを発行する時点では、104.16.30.34という実際のアドレスになっていることがわかります。それがローカルのアドレスに変換されているので、eBPFなどでフックしてアドレスとポートを書き換えているのでしょう。
ファイアウォールを無効にすると?
最後に、ファイアウォールを無効にして確認しました。GitHubの画面の「Settings」から「Copilot」→「Coding agent」とたどった画面の「Internet Access」で「Enable firewall」を「Off」にします。
この状態でGitHub Copilotコーディングエージェントに各種コマンドを実行してもらうと、
$ ps aux | grep -i padawan
runner 2217 0.0 0.0 6952 2200 pts/0 S+ 09:51 0:00 grep -i padawan
<exited with exit code 0>
padawan-fwプロセスは存在しません。
$ curl -v https://registry.npmjs.org/ 2>&1 | grep -E "(Trying|Connected to|SSL)"
* Trying 104.16.31.34:443...
* Connected to registry.npmjs.org (104.16.31.34) port 443
* SSL connection using TLSv1.2 / ECDHE-ECDSA-CHACHA20-POLY1305 / X25519 / id-ecPublicKey
* SSL certificate verify ok.
<exited with exit code 0>
通信先の書き換えも発生していません。
また、/etc/ssl/certs/mkcert_development_CA_*.pemは存在しません。
$ ls -la /etc/ssl/certs/mkcert_development_CA_*.pem
ls: cannot access '/etc/ssl/certs/mkcert_development_CA_*.pem': No such file or directory
ファイアウォールが無効化されている場合、通常の公開CA証明書でHTTPS通信が成功するため、追加の証明書設定は不要です。もしも、問題の発生から解決までや裏取りでやっていたように、実際の通信で使っている証明書を信頼させてしまうと、悪意を持った中間者が通信経路内に入り込んでいた場合に、それを信頼することになってしまいます。
ファイアウォールが有効化されている場合は、実際の通信先との通信はファイアウォールで終端し、コンテナ内のユーザプログラムが通信するのはファイアウォールであるはずです。が、ユーザプログラムとファイアウォールの間に悪意を持った中間者が存在するとしたら、実際の通信で使っている証明書を信頼させる方法は問題がありそうです。解決策に記載したように、ホストであるRunnerの環境からファイアウォールが使用するルートCA証明書をビルド環境にコピーし、信頼ストアに追加する方法の方がより安全と考えます。
おわりに
GitHub Copilotコーディングエージェントで動作しているファイアウォールが使用している証明書が、コンテナイメージのビルド環境で信頼されていないことに起因する問題とその解決策について説明しました。
現状、私はコンテナイメージを作成して動かしてテストするようにGitHub Copilotコーディングエージェントにお願いしています。GitHub Copilotコーディングエージェントでコンテナイメージのビルドで署名検証失敗のエラーについて書いてある情報をネットで検索しても見つけられなかったので、コンテナ化せずにRunner上で直接動かして動作確認してもらうのが一般的なのかもしれません。また、私の場合、GitHub Copilotコーディングエージェントからのプルリクエストをマージする際のパイプラインでもテストとコンテナイメージの作成(+コンテナレジストリへの登録が差分)をやるようにしており、二度手間になっています。
どこまでGitHub Copilotコーディングエージェントにやってもらうかはやりたいことによってケースバイケースかもしれませんが、コンテナイメージの作成をGitHub Copilotコーディングエージェントにやってもらう場合は、自己署名証明書の問題をこの記事で書いたやり方で解決できると思います。
本文中に記載の会社名、製品名などは、それぞれの会社の商標もしくは登録商標です。商標記号(™、®)は明記していません。
