🐧

cfssl を使って自己認証局による HTTPS 対応:Linux 使い(略)Advent Calendar 2024

2024/12/17に公開

はじめに

これは「Linux 使いになりたい人のための Advent Calendar 2024」の記事です。

筆者は、Web エンジニアを志望する人には、セルフホスト Git サービスを稼働させて利用することをオススメしています。もし Git を使ったことがないなら、Git を学ぶときに、セルフホスト Git サービスを稼働させることも視野に含めながら学習するのが効率的だと考えています。

cfssl を使って自己認証局による HTTPS 対応

前回はセルフホスト Git サービスを稼働させるにあたり、GitBucket の CI 用プラグインで CI/CD を実現する方法について説明しました。GitBucket では、あれだけ簡単に実現できた CI/CD も、GitLab + GitLab Runner といった環境で用意しようとなると、なかなか大変なことになります。

HTTP を使うのなら、あまり困らないのです。HTTPS を使うようになると、サーバー証明書の妥当性検証に対する設定が必要になってきます。技術的に正面から対応しようと考えると、自己認証局による HTTPS 対応をするのが妥当なので、ここではそうしてみます。

自己認証局を用意するには、openssl コマンドを使うのが基本ですが、少し複雑な設定を理解する必要があり使うのが大変です。

ここで、HTTPS 対応は開発時にも必要なことがあるためか、次のように簡易的に用意するためのツールがいくつか世の中にあります。

今回は、機能が豊富な cloudflare/cfssl を使ってみましょう。このツールは Docker イメージも用意されていますから、試用もしやすいです。

なお、ここではサーバー証明書、自己認証局といったものについての説明は省略させていただきます。HTTPS 対応するにあたって、ここでは「安全な通信を保証するために、そういうものが必要なのだ」程度に思っておいてかまいません。

きちんと理解するには、公開鍵方式での暗号化通信についての基本を理解してから、HTTPS による通信手順について理解するというのが良いでしょう。いずれにせよ、このあたりはきちんと専門的に解説しているものを参照していただいた方が確実です。

cfssl コマンド

Web サーバーの設定をしたことがある人は知っているのですが、HTTPS を利用できるように Web サーバーを設定するには、サーバー用の秘密鍵ファイルとユーザーへ提供するサーバー証明書ファイルが必要です。サーバー証明書ファイルについては、認証局というものによって署名されたものが必要です。

商用の Web サーバーでは、サーバー用の秘密鍵ファイルは、Web サーバー提供者が用意します。また、その人が、認証局へ署名されたサーバー証明書の発行を依頼します。

認証局は発行依頼に応じてサーバー証明書ファイルを Web サーバー提供者へ発行します。

Web サーバー提供者は、認証局からサーバー証明書ファイルを受理したら、それを Web サーバーへ設置して HTTPS 対応した Web サイトの公開をします。

Web サーバー提供者からの視点で一連の作業の流れを整理すると次のようになります。

  1. サーバー用の秘密鍵ファイル作成
  2. 認証局へサーバー証明書ファイルの発行依頼
  3. 認証局からサーバー証明書ファイルを受理
  4. Web サーバーにサーバー用の秘密鍵ファイルとサーバー証明書を設定
  5. Web サーバー公開

この作業の中で、cfssl ツールを使って、次の処理ができます。

  • サーバー用の秘密鍵ファイル作成
  • サーバー証明書ファイルの発行依頼時に必要なファイルの作成
  • 認証局により署名されたサーバー証明書ファイルの作成

ポイントは、ここでは自分が Web サーバー提供者の役割と、認証局の役割の両方を受け持つという点です。どの立場で作業しているかを意識しながら cfssl ツールを使う必要があります。

cfssl 用 compose.yaml ファイル

さて、cfssl コマンドを使うには、OS へ直接インストールすることもできますが、そんなにしょっちゅう使うツールというわけでもありません。環境構築のときだけ使えれば良いので、こういったものは Docker で用意するのが手軽です。

また、慣れてくると docker コマンドで直接作業ができるのですが、慣れないうちは compose.yaml ファイルを用意して作業するのが確実です。ということで、ここでは 次のディレクトリー構成でファイルを用意します。なお、これ以降、${CFSSL_DIR} は cfssl ディレクトリーを表すとします。

cfssl/
├── ca-config.json ... CA(自己認証局)用設定ファイル
├── ca-csr.json ... CA 用キーペア作成時に使うファイル
├── compose.yaml ... cfssl コンテナー用
├── server.json ... サーバー証明書用ファイル
├── ca/ ... CA(自己認証局)用ファイル
│   ├── ca-key.pem
│   ├── ca.csr
│   └── ca.pem
└── server/ ... サーバー証明書関連ファイル
     ├── server-key.pem
     ├── server.csr
     └── server.pem

また、ここでは次の cfssl コマンド用の compose.yaml ファイルを用意します。

name: cfssl

services:
  cfssl:
    image: 'cfssl/cfssl:v1.6.5'
    container_name: 'cfssl'
    hostname: 'cfssl.local.internal'
    tty: true
    entrypoint: /bin/bash

ここで使っている Docker イメージはデフォルトだと cfssl コマンドが実行されるものなのですが、entrypoint: /bin/bash を指定してコンテナーへアタッチして cfssl コマンドを実行する方法で使うことにします。

起動は次のようにします。

docker compose up -d

CA 用ファイルの用意

それから、cfssl コンテナーへアタッチします。

docker compose -p cfssl exec cfssl bash

CA 用設定ファイルなどの雛形作成

コンテナー内では、次の手順で設定ファイルを作成します。これを雛形にして、自分が利用する設定ファイルを作成します。なお、cfssl.local.internal# はコンテナーのプロンプトとします。

cfssl.local.internal# mkdir /cfssl && cd /cfssl
cfssl.local.internal# cfssl print-defaults config > ca-config.json
cfssl.local.internal# cfssl print-defaults csr > ca-csr.json

ファイル編集は Docker ホストでした方が楽なのと、コンテナー終了後も残しておく必要があるので、Docker ホストで次のコマンドを実行してコピーしておきます。

cd ${CFSSL_DIR}
docker compose -p cfssl cp cfssl:/cfssl/ca-config.json ./
docker compose -p cfssl cp cfssl:/cfssl/ca-csr.json ./

ca-config.json の用意

初期値だと有効期間が 8760h (365 日)なので、43800h (5年) へ変更します。ca-config.json は次のようになります。

ca-config.json
{
    "signing": {
        "default": {
            "expiry": "43800h"
        },
        "profiles": {
            "server": {
                "expiry": "43800h",
                "usages": [
                    "signing",
                    "key encipherment",
                    "server auth"
                ]
            },
            "client": {
                "expiry": "43800h",
                "usages": [
                    "signing",
                    "key encipherment",
                    "client auth"
                ]
            },
            "peer": {
                "expiry": "43800h",
                "usages": [
                    "signing",
                    "key encipherment",
                    "server auth",
                    "client auth"
                ]
            }
        }
    }
}

ca-csr.json の用意

CA 用のキーペアを作成するにあたって必要なファイルである ca-csr.json は次のようにします。ここでは CN (Common Name) が ca.local.internal の CA を作成することにしています。

ca-csr.json
{
    "CN": "ca.local.internal",
    "key": {
        "algo": "rsa",
        "size": 2048
    },
    "names": [
        {
            "C": "JP",
            "L": "CA",
            "O": "local.internal",
            "ST": "Tokyo",
            "OU": "local"
        }
    ]
}

CA 用ファイルの生成

ファイルの用意ができたら、cfssl コンテナーへコピーしてコンテナー内のファイルを更新します。

docker compose -p cfssl cp ca-config.json cfssl:/cfssl/
docker compose -p cfssl cp ca-csr.json cfssl:/cfssl/

次に cfssl gencert コマンドを使って CA 用の秘密鍵と公開鍵を作成します。CA 用の初期ファイルを作成する場合は -initca オプションをつけます。

cfssl.local.internal# cd /cfssl
cfssl.local.internal# cfssl gencert -initca ca-csr.json | cfssljson -bare ca -

これで次のファイルが作成されます。

  • ca-key.pem ... サーバー証明書署名時に使用
  • ca.csr ... この後使いません
  • ca.pem ... 他のマシンで認証局用にインポートするときに使用

ca-key.pem は CA 用の秘密鍵となります。厳重に保管しましょう。また、所有者のみ読み取りができるようにしておきます。コピー時にはファイルモードが変わらないよう注意しましょう。

どれも Docker ホストへコピーして大切に保管しておきます。ここでは Docker ホストで次のコマンドを実行します。まず、${CFSSL_DIR}/ca ディレクトリーを作成して、そこへ生成したファイルをコピーしています。

cd ${CFSSL_DIR}
if [ ! -e ./ca/ ]; then mkdir ./ca/; fi
for f in ca-key.pem ca.csr ca.pem; do
    docker compose -p cfssl cp cfssl:/cfssl/${f} ./ca/
done

サーバー証明書の用意

次にサーバー証明書の用意をします。

sever.json の雛形作成

cfssl コンテナーで次のコマンドを実行して、サーバー証明書用のファイルを生成するときに使う sever.json の雛形を作成します。

cfssl.local.internal# cd /cfssl
cfssl.local.internal# cfssl print-defaults csr > server.json

これについても Docker ホストで編集するので、Docker ホストで次のコマンドを実行してコピーします。

cd ${CFSSL_DIR}
docker compose -p cfssl cp cfssl:/cfssl/server.json ./

server.json の編集

ここでは、sever.json を編集して、次の内容としました。

server.json
{
    "CN": "gitlab-ce-https.local.internal",
    "hosts": [
        "127.0.0.1",
        "gitlab-ce-https.local.internal",
        "gitlab-ce.local.internal",
        "mailpit.local.internal",
        "localhost",
        "localhost:8443",
        "localhost:8025",
        "gitlab-ce-https.local.internal:8443",
        "gitlab-ce.local.internal:8443",
        "mailpit.local.internal:8025"
    ],
    "key": {
        "algo": "ecdsa",
        "size": 256
    },
    "names": [
        {
            "C": "JP",
            "ST": "Tokyo",
            "L": "CA"
        }
    ]
}

後で、gitlab-ce-https.local.internal 用に使うことを想定しています。また、複数のサーバー証明書を用意するのは面倒なので、https://gitlab-ce-https.local.internal:8443https://mailpit.local.internal:8025 などでも使えるようにしてあります。これらは Docker で動かすことを想定しているので localhost についても対応できるようにしてあります。

サーバー証明書関連ファイルの生成

ファイルを用意したら cfssl コンテナーへコピーします。

cd ${CFSSL_DIR}
docker compose -p cfssl cp ./server.json cfssl:/cfssl/

それから cfssl コンテナーで次のコマンドを実行します。

cfssl.local.internal# cd /cfssl
cfssl.local.internal# cfssl gencert \
    -ca=ca.pem \
    -ca-key=ca-key.pem \
    -config=ca-config.json \
    -profile=server \
    server.json \
    | cfssljson -bare server

これで次のファイルが生成されます。

  • server-key.pem ... サーバー用秘密鍵
  • server.csr ... この後使いません。
  • server.pem ... サーバー証明書

これらも Docker ホストへ保存するために、次のコマンドを Docker ホストで実行します。

cd ${CFSSL_DIR}
if [ ! -e ./server/ ]; then mkdir ./server/; fi
for f in server-key.pem server.csr server.pem; do
    docker compose -p cfssl cp cfssl:/cfssl/${f} ./server/
done

生成されたファイルの確認

なお、生成されたファイルについて内容を確認するには次のように openssl コマンドを実行します。

cfssl.local.internal# cd /cfssl
cfssl.local.internal# openssl x509 -in ca.pem -text -noout
cfssl.local.internal# openssl x509 -in server.pem -text -noout

作業が終了したら cfssl コンテナーは削除します。Docker ホストで docker compose down コマンドを使います。

docker compose -p cfssl down

サーバー証明書の更新

サーバー証明書は有効期限を過ぎると更新が必要です。その場合は次のように cfssl コンテナーを起動して、必要なファイルを Docker ホストからコピーします。それから、サーバー証明書の作成時と同じように cfssl コマンドを実行します。

ここでは、Docker ホストで作業する前提でコマンドを表記しました。

docker compose -p cfssl exec cfssl sh -c "if [ ! -e /cfssl ]; then mkdir /cfssl; fi"
for f in ca/ca.pem ca/ca-key.pem ca-config.json server.json; do
    docker compose -p cfssl cp ./${f} cfssl:/cfssl/ 
done

docker compose -p cfssl exec --workdir=/cfssl cfssl sh -c '\
cfssl gencert \
    -ca=ca.pem \
    -ca-key=ca-key.pem \
    -config=ca-config.json \
    -profile=server \
    server.json \
    | cfssljson -bare server \
'

for f in server-key.pem server.csr server.pem; do
    docker compose -p cfssl cp cfssl:/cfssl/${f} ./server/
done

これをスクリプトにしておくと、ワンラインで更新されたサーバー証明書関連のファイルが手に入ります。

おわりに

今回は、cfssl コマンドを使って簡易的な自己認証局を用意する方法と、それを使ったサーバー証明書関連ファイルの作成方法について説明しました。

最近は Docker や cfssl プロジェクトのおかげで、以前より、かなり簡単に HTTPS 対応がしやすくなりました。

ただし、HTTPS 対応した Web サーバーへアクセスするクライアント側で自己認証局の証明書を登録するなどの作業が必要となり、Docker を使う場合はそこが少し課題となることがあります。とはいえ、対応が必要なパターンはそれほど多くないので、複雑なことをしなければ難しくありません。

HTTPS 対応については、インターネット環境があって Let's encrypt などの無償のサーバー証明書を取得可能な環境があれば、それを使って対応すれば済むのですが、LAN 環境用の Web サーバーを用意する場合は簡単な対応方法がありません。

LAN 環境で Web ブラウザが警告を出さないように HTTPS 対応の Web サーバーを用意する場合は、自己認証局を用意して環境整備するのが結局の所は確実だと個人的には考えています。

Web サービスの開発をしていたら、ローカルの環境で HTTPS 対応が必要になることがたまにはあります。そういうときに慌てないためにも、セルフホスト Git サービスを稼働させるついでに、関連技術を理解しておくのが良いのではないでしょうか。

参考資料

Discussion