Featured image of post scratchイメージ上のGoで任意の証明書を信頼する

scratchイメージ上のGoで任意の証明書を信頼する

"x509: certificate signed by unknown authority"のエラーを回避する

まとめ

マルチステージビルドで実行用のバイナリをscratchに載せる場合、
ビルドと同じタイミングで信頼したい証明書を用意して、
CA証明書のリストを更新したものをアプリと一緒にコピーすればよさそう。

環境

  • Docker version 20.10.17, build 100c701
  • go version go1.18.1 darwin/arm64
  • nginx:1.23.1

やったこと

オレオレ証明書を作成してnginxをHTTPSで建てる

まずは検証用にオレオレ証明書を作成する。
opensslが使えるならどんなやり方でもいいが、自分はDocker上で作ってホストに持ってくるのがすき。

# ホストOSのボリュームをマウントする
$ docker container run --rm -it -v="$PWD:/workdir" -w="/workdir" --entrypoint=/bin/bash nginx:1.23.1

root@e08bb1ebe3da:/workdir# openssl version
OpenSSL 1.1.1n  15 Mar 2022

# 秘密鍵の作成
root@e08bb1ebe3da:/workdir# openssl genpkey -algorithm RSA -out server-private-key.pem
...................+++++
......................................................................................................+++++

# オレオレ証明書の作成
root@e08bb1ebe3da:/workdir# openssl req -x509 -key server-private-key.pem -out server-cert.pem -addext 'subjectAltName = DNS:hogehoge.uzimihsr.com'
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:
State or Province Name (full name) [Some-State]:
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]:
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:hogehoge.uzimihsr.com
Email Address []:

root@e08bb1ebe3da:/workdir#
exit

# ホスト上に鍵と証明書が置かれている
$ ls
server-cert.pem		server-private-key.pem

作成した秘密鍵とオレオレ証明書を使ってnginxをHTTPSで立ててみる。

# ディレクトリの状態
$ tree .
.
├── docker-compose.yaml
├── https.conf
├── server-cert.pem
└── server-private-key.pem

0 directories, 4 files

# nginxを起動
$ docker compose up -d

$ docker compose ps
NAME                    COMMAND                  SERVICE             STATUS              PORTS
hogehoge.uzimihsr.com   "/docker-entrypoint.…"   nginx               running             80/tcp, 0.0.0.0:443->443/tcp

# CA証明書を指定して叩くとHTTPSで通信できている
$ curl -I https://hogehoge.uzimihsr.com --cacert ./server-cert.pem --resolve "hogehoge.uzimihsr.com:443:127.0.0.1"
HTTP/1.1 200 OK
...

HTTPリクエストを送るGoアプリの作成

先ほど立てたnginx(HTTPS)に対してHTTPリクエストを送る簡単なアプリを実装する。

内容としては引数で指定されたURLに対してHTTP GETしてそのステータスコードを表示する(200系以外は異常終了する)だけのもの。

$ go run main.go https://example.com
2022/09/28 23:45:32 target: https://example.com, insecure: false
2022/09/28 23:45:33 status: 200 OK

これをマルチステージビルドでscratchに乗せて動かしてみる。

起動したコンテナを確認すると、オレオレ証明書を信頼できずにエラーとなっている。

$ tree .
.
├── Dockerfile
├── docker-compose.yaml
├── go.mod
├── https.conf
├── main.go
├── server-cert.pem
└── server-private-key.pem

0 directories, 7 files

$ cat go.mod
module http-status-checker

go 1.18

$ docker compose up -d

$ docker compose ps
NAME                    COMMAND                  SERVICE               STATUS              PORTS
hogehoge.uzimihsr.com   "/docker-entrypoint.…"   nginx                 running             80/tcp, 0.0.0.0:443->443/tcp
http-status-checker     "./app https://hogeh…"   http-status-checker   exited (1)

$ docker compose logs http-status-checker
http-status-checker  | 2022/09/28 12:17:41 target: https://hogehoge.uzimihsr.com/, insecure: false
http-status-checker  | 2022/09/28 12:17:41 [ERROR] HTTP(S) request failed: Get "https://hogehoge.uzimihsr.com/": x509: certificate signed by unknown authority

オレオレ証明書を信頼する

こんなとき、実装にもあるようにtls.Config.InsecureSkipVerifyを指定することで証明書エラーを回避することは可能だが、
どうしてもオレオレ証明書を信頼する設定で動かしたくなった。

どうしたものかと悩んだが、単純にupdate-ca-certificatesで信頼した証明書をコピーする方法に落ち着いた。

Dockerfileの設定を次のように書き換える。

変更点は次の部分。
信頼したい証明書を/usr/local/share/ca-certificates/配下に.crtという名前でコピーして、update-ca-certificatesを実行している。
これにより、ビルド用コンテナ内の/etc/ssl/certs/ca-certificates.crtが更新されて対象の証明書を信頼できるようになる。

COPY server-cert.pem /usr/local/share/ca-certificates/server-cert.crt
RUN update-ca-certificates

再度実行してみると、今度はオレオレ証明書が信頼できていてエラーが発生しなくなった。

$ docker compose up -d --remove-orphans --build

$ docker compose ps
NAME                    COMMAND                  SERVICE               STATUS              PORTS
hogehoge.uzimihsr.com   "/docker-entrypoint.…"   nginx                 running             80/tcp, 0.0.0.0:443->443/tcp
http-status-checker     "./app https://hogeh…"   http-status-checker   exited (0)

$ docker compose logs http-status-checker
http-status-checker  | 2022/09/28 13:44:43 target: https://hogehoge.uzimihsr.com/, insecure: false
http-status-checker  | 2022/09/28 13:44:43 status: 200 OK

今回はgolangのimageがdebian系だったのでこの手順で試したが、
Red Hat系なら同様に.crtファイルを/usr/share/pki/ca-trust-source/anchors/配下にコピーして、
update-ca-trustすると/etc/pki/tls/certs/ca-bundle.crtが更新されたはず。(試してない)

おわり

久しぶりにGo+scratchでimageを作ったときに詰まったのでおさらいした。
こんなことしてる暇があったらdistrolessに移行しろと言われればそれはそう…

おまけ

自分のしっぽを踏むねこ