OpenSSLでTLS証明書を作る

etcdクラスタをTLS有効にして運用するため,TLS証明書を作成する必要があった.
ちゃんとした手順をあまり理解できていなかったため,備忘録として残しておく.

TLS証明書発行までの流れ

TLS証明書は次にような流れで発行する.

  1. 秘密鍵を作成
  2. CSRを作成
  3. TLS証明書を作成

これ以降,上記の具体的な手順について説明する.

今回使用したOpenSSLのバージョンは次の通り.

$ openssl version
OpenSSL 1.1.1  11 Sep 2018

秘密鍵を作成

秘密鍵の作成は, genpkey サブコマンドを使用する.
genrsa サブコマンドでもできるようだが,マニュアルに genrsa 含めいくつかのコマンドは genpkey に置き換えられたという記述があるので,今回は genpkey を使用する.

今回は次のような鍵を作成する.
* 公開鍵アルゴリズム: RSA
* 鍵長: 2048bit
* 秘密鍵を暗号化するためのアルゴリズム: AES 128bit

この条件で秘密鍵を作成するには次のようなコマンドを使用する.

$ openssl genpkey -algorithm rsa -pkeyopt rsa_keygen_bits:2048 -aes128 -out ca.key

コマンドを実行するとパスフレーズを求められるので,適当なものを入力する.
実行すると次のようになる.

$ openssl genpkey -algorithm rsa -pkeyopt rsa_keygen_bits:2048 -aes128 -out ca.key
..........................................................+++++
................................................+++++
Enter PEM pass phrase:
Verifying - Enter PEM pass phrase:

オプションの意味は次の通り.
* -algorithm rsa : 公開鍵アルゴリズムとしてRSAを使用する
* -pkeyopt rsa_keygen_bits:2048 : RSAの鍵長を2048bitにする
* -aes128 : 秘密鍵をAES 128bitで暗号化する
* -out ca.key : ca.key という名前で秘密鍵を生成する

CSRを作成

TLS証明書を作成するには,まずCSR (Certificate Signing Request) を作成する必要がある.
このCSRを元に,CA (Certificate Authority) がTLS証明書を作成するという流れになる.

この手順では,前手順で作成した秘密鍵を使ってCSRを作成する.
これは req サブコマンドで行うことができる.

$ openssl req -new -key ca.key -out ca.csr

実行すると次のようになる.
まずパスフレーズを聞かれるので,秘密鍵を作成したときに入力したのと同じものを入力する.

その後,いくつか情報を聞かれるので必要に応じて入力する.
このとき,何も入力せずにEnterを押すとデフォルト値が使用されるが,フィールドを空にしておきたい場合は . (ピリオド)を入力してからEnterを押すようにする.
. をつけることにより,このフィールドは空だと明示的に指定できる.

$ openssl req -new -key ca.key -out ca.csr
Enter pass phrase for ca.key:
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]:JP
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) []:mas9612.net
Email Address []:.

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:.
An optional company name []:.

オプションの意味は次の通り.
* -new : 新しいCSRを作成するときに指定する
* -key ca.key : 秘密鍵を指定する.この鍵とペアになる公開鍵が署名される
* -out ca.csr : ca.csr という名前でCSRを作成する

TLS証明書を作成

CSRが作成できたら,最後にTLS証明書を作成する.

今回は,次の2種類の方法を試す.
* 自己署名: 自分の秘密鍵を使って署名する
* 別に用意したCAによる署名: CAの秘密鍵を使って署名する

自己署名

自己署名を行うには,次のようなコマンドを実行する.

$ openssl x509 -req -in ca.csr -out ca.crt -signkey ca.key -days 365

コマンドを実行すると次のようになる.
ここでもパスフレーズを聞かれるので,秘密鍵作成時のものを入力する.

$ openssl x509 -req -in ca.csr -out ca.crt -signkey ca.key -days 365
Signature ok
subject=C = JP, CN = mas9612.net
Getting Private key
Enter pass phrase for ca.key:

オプションの意味は次の通り.
* -req : このオプションを指定すると,CSRを読み込んでTLS証明書を作成する
* -in ca.csr : 読み込むCSRファイルを指定
* -out ca.crt : ca.crt という名前でTLS証明書を出力する
* -signkey ca.key : ca.key を使って署名を行う
* -days 365 : TLS証明書の期限を365日にする

CAによる署名

次に,CAによる署名を試してみる.
といっても,自分で何かCAを運用しているわけではないので,今回は先程作った秘密鍵とTLS証明書をCAのものと仮定し,それを使って署名をするということを試す.

まず,先ほどとは別の秘密鍵とCSRを作成しておく.

$ openssl genpkey -algorithm rsa -pkeyopt rsa_keygen_bits:2048 -aes128 -out etcd0.key
$ openssl req -new -key ca.key -out etcd0.csr

CSRまで作成できたら,それをCAの証明書で署名する手順に移る.
これも自己署名と同様に, x509 サブコマンドを使うと簡単にできる.
次のようなコマンドを実行する.

$ openssl x509 -req -in etcd0.csr -out etcd0-ca.crt -days 365 -CA ca.crt -CAkey ca.key -CAcreateserial

実行すると次のようになる.

$ openssl x509 -req -in etcd0.csr -out etcd0-ca.crt -days 365 -CA ca.crt -CAkey ca.key -CAcreateserial
Signature ok
subject=C = JP, CN = mas9612.net
Getting CA Private Key
Enter pass phrase for ca.key:

CA関連のオプションは次の通り.
* -CA ca.crt : 署名に使用するCAのTLS証明書を指定する
* -CAkey ca.key : 署名に使用するCA秘密鍵を指定する
* -CAcreateserial : CAのシリアルナンバーファイルが存在しない場合,自動で作成する

秘密鍵やCSR,TLS証明書の内容を確認する

上記の手順でTLS証明書までの作成ができた.
作成した各種ファイルは,opensslコマンドを使用することでその内容を確認することができる.

ここではそれについて説明する.

秘密鍵の内容確認

秘密鍵の内容を確認するには, rsa サブコマンドを使用する.

普通に秘密鍵を読み込むには, -in オプションに秘密鍵のファイル名を指定するだけでできる.
なお,このコマンドを実行してもただ単にファイルの内容がそのまま表示されるだけである.

$ openssl rsa -in ca.key
Enter pass phrase for ca.key:
writing RSA key
-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEAoTo44Vgr5vUZvhlfhDGrUK3DBVKexWoG5Hq29oMhEc5HCSTk
XBL28/gGVoW6NtW7HMiM2zkPE0ETC/Hi8ef9CVjE414F5OpIgppBjYxjjmDEDita
...(省略)
bBxlNDpyMteIfxg1cix3U2V+D1mWhBAKqF95xJNASQZtfeabZHZzCH7YbO0eGFIv
m9ZFXwYPhq+ORWBJE9+hL1PsgvkiruEECIKTE2Pfeb8TkiO1Gls=
-----END RSA PRIVATE KEY-----

これに -text オプションを指定すると,秘密鍵の内容を調べてNやE,Dを値を表示してくれる.

$ openssl rsa -in ca.key -text
Enter pass phrase for ca.key:
RSA Private-Key: (2048 bit, 2 primes)
modulus:
    00:a1:3a:38:e1:58:2b:e6:f5:19:be:19:5f:84:31:
    ...(省略)
    43:c8:f7:b1:7f:e0:9f:5f:9c:25:83:55:1d:d4:b7:
    de:9f
publicExponent: 65537 (0x10001)
privateExponent:
    58:d7:6d:5a:77:2c:91:f2:c3:81:a6:17:a5:0f:7d:
    ...(省略)
    b4:d7:70:bb:59:56:df:92:9f:99:40:a4:42:97:4d:
    c9
prime1:
    00:d1:a0:9d:d8:96:8d:8d:48:d0:76:c8:76:8e:b9:
    ...(諸略)
    a0:30:e1:b3:b5:d2:e8:d4:00:f3:65:93:ab:d5:b3:
    2f:0e:aa:bd:94:75:2d:a2:05
prime2:
    00:c4:e4:ac:2a:c5:59:aa:a1:d2:3c:2a:8c:dd:bf:
    ...(省略)
    bf:32:c3:4b:98:dc:57:ab:53
exponent1:
    4c:d1:5f:06:8f:a5:2f:b1:0f:33:78:22:7a:0a:ef:
    ...(省略)
    ac:16:45:82:b1:ae:17:41
exponent2:
    41:b8:e3:0f:53:d8:de:70:2d:b1:0f:b2:fd:c2:17:
    ...(省略)
    ee:9d:e9:fa:18:72:db:29
coefficient:
    2f:ba:6e:47:c5:bb:60:2e:4f:35:4f:c2:d1:12:61:
    ...(省略)
    79:bf:13:92:23:b5:1a:5b
writing RSA key
-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEAoTo44Vgr5vUZvhlfhDGrUK3DBVKexWoG5Hq29oMhEc5HCSTk
XBL28/gGVoW6NtW7HMiM2zkPE0ETC/Hi8ef9CVjE414F5OpIgppBjYxjjmDEDita
...(省略)
m9ZFXwYPhq+ORWBJE9+hL1PsgvkiruEECIKTE2Pfeb8TkiO1Gls=
-----END RSA PRIVATE KEY-----

いろいろ出力されるが,NやEに対応するのは次の部分.
* N: modulus
* E: publicExponent
* D: privateExponent
* prime1: p
* prime2: q

また, -noout オプションを指定すると,秘密鍵をエンコーディングした内容は出力されなくなる( BEGIN RSA PRIVATE KEY から END RSA PRIVATE KEY の部分).

CSRの内容確認

CSRの内容確認には, req サブコマンドを使用する.
秘密鍵の内容確認と同じように, -in-text-noout が使える.

$ openssl req -in ca.csr -text -noout

TLS証明書の内容確認

TLS証明書の内容確認には, x509 サブコマンドを使用する.
これも同じように, -in-text-noout が使える.

$ openssl x509 -in ca.crt -text -noout

etcd入門 (1)

Kubernetesにも採用されている分散型KVSについて,何回かに分けて勉強していく.
今回のモチベーションとして,Kubernetesのアーキテクチャを詳しく勉強したい,Terraformのstate保存をローカルではなくetcdにしたいという2つがある.

まず,インストールとクラスタの作成について見ていく.

インストール

クラスタ作成のためには,まずetcd本体のバイナリが必要になる.
GitHubのreleaseページから,自分のOSにあったバイナリをダウンロードしてくる.

$ curl -LO https://github.com/etcd-io/etcd/releases/download/v3.3.10/etcd-v3.3.10-linux-amd64.tar.gz

ダウンロードしたファイルは圧縮されており,それを解凍するといくつかのファイルの中に2つのバイナリが確認できる.

$ tar xzf etcd-v3.3.10-linux-amd64.tar.gz
$ cd etcd-v3.3.10-linux-amd64.tar.gz
$ ll etcd*
-rwxr-xr-x. 1 yamazaki yamazaki 19237536 Oct 11  2018 etcd
-rwxr-xr-x. 1 yamazaki yamazaki 15817472 Oct 11  2018 etcdctl

etcd はetcd本体, etcdctl はetcdのクライアントとなるプログラム.
これら2つをPATHが通っている場所に置いてあげる.

$ sudo mv etcd* /usr/local/bin/
$ ll /usr/local/bin/etcd*
-rwxr-xr-x. 1 yamazaki yamazaki 19237536 Oct 11  2018 /usr/local/bin/etcd
-rwxr-xr-x. 1 yamazaki yamazaki 15817472 Oct 11  2018 /usr/local/bin/etcdctl

これでインストールは完了.
きちんとインストールされているか一応確認する.

$ etcd --version
etcd Version: 3.3.10
Git SHA: 27fc7e2
Go Version: go1.10.4
Go OS/Arch: linux/amd64

クラスタの作成

まず,1台のみでetcdクラスタを作成して簡単に使い方を把握し,その後複数メンバでのetcdクラスタを作成していく.

1台のetcdクラスタ作成

基本的にGitHubに書いてある手順通りに試していく.

1台のみでetcdクラスタを作成するときは,特に何も考えずに etcd コマンドを実行する.

$ etcd

etcd コマンドを実行すると,多くのログが出力される.
ここで,作成したetcdクラスタを使って,データの登録と取得を試してみる.

etcdクラスタとのやり取りには, etcdctl コマンドを使用する.
etcd コマンドを実行しているシェルとは別にもう1つシェルを起動し,そこで etcdctl コマンドを使用していく.
なお,etcd APIにはバージョンがいくつかあるが,今回はバージョン3を使用する.
etcdctl を普通に使うとv2 APIが使われてしまうので,v3 APIを使うために ETCDCTL_API という環境変数の値を 3 に設定する必要があることに注意する.
公式のREADMEのように etcdctl コマンドを実行するごとに毎回 ETCDCTL_API を指定してもよいが,毎回記述するのも面倒なのであらかじめexportしておく.

$ export ETCDCTL_API=3

etcdクラスタにデータを登録するには,putコマンドを使用する.

# Usage
$ etcdctl put <key> <value>

$ etcdctl put mykey "this is awesome"
OK

putコマンドを実行して, OK と表示されれば成功している.

登録した値を取得するにはgetコマンドを使用する.

# Usage
$ etcdctl get <key>

$ etcdctl get mykey
mykey
this is awesome

指定したキーの名前とデータが続けて出力されれば成功.

次に,複数台のetcdクラスタを作成してみる.
その前に,今使っていたetcdクラスタを停止させておく.
etcd コマンドを実行していたシェルに戻り,Ctrl-Cで終了する.

複数メンバでのetcdクラスタ作成

今回は1つのノードの中にetcdを複数立ち上げることで,複数メンバで構成されるetcdクラスタを作成する.
もちろん,複数のノードを使ってノード1台につきetcdを1つ動作させるという形で作成することもできる.

公式には,etcdはTCP 2379番・2380番のポートを使用する.

  • 2379/tcp: クライアントとの通信
  • 2380/tcp: etcdメンバ間の通信

今回は同じノード内に複数etcdを立ち上げるため,それぞれのetcdでポートが競合しないようにしておく.
今回は3つのetcdメンバを動作させ,それぞれの名前,ポートは次の表のようにした.

メンバ クライアント通信用 メンバ間通信用
etcd1 2379 2380
etcd2 12379 12380
etcd3 22379 22380

実際に3つのetcdメンバを動かしていく.
ターミナルのウィンドウを3つ開いて,それぞれのターミナルにつき1つのetcdメンバを起動する.
それぞれのターミナルで,次のようなコマンドを実行する.

1つ目のターミナル

$ etcd --name etcd1 \
    --initial-advertise-peer-urls http://localhost:2380 \
    --listen-peer-urls http://localhost:2380 \
    --advertise-client-urls http://localhost:2379 \
    --listen-client-urls http://localhost:2379 \
    --initial-cluster etcd1=http://localhost:2380,etcd2=http://localhost:12380,etcd3=http://localhost:22380 \
    --initial-cluster-state new \
    --initial-cluster-token etcd-cluster-1

2つ目のターミナル

$ etcd --name etcd2 \
    --initial-advertise-peer-urls http://localhost:12380 \
    --listen-peer-urls http://localhost:12380 \
    --advertise-client-urls http://localhost:12379 \
    --listen-client-urls http://localhost:12379 \
    --initial-cluster etcd1=http://localhost:2380,etcd2=http://localhost:12380,etcd3=http://localhost:22380 \
    --initial-cluster-state new \
    --initial-cluster-token etcd-cluster-1

3つ目のターミナル

$ etcd --name etcd3 \
    --initial-advertise-peer-urls http://localhost:22380 \
    --listen-peer-urls http://localhost:22380 \
    --advertise-client-urls http://localhost:22379 \
    --listen-client-urls http://localhost:22379 \
    --initial-cluster etcd=http://localhost:2380,etcd2=http://localhost:12380,etcd3=http://localhost:22380 \
    --initial-cluster-state new \
    --initial-cluster-token etcd-cluster-1

正しくコマンドを入力できていれば,正常にetcdクラスタが起動しているはず.
出力されるログでエラー等が出ていなければひとまずOK.

コマンドラインオプションをたくさん使っているので複雑に見えるが,一度理解してしまうとそこまで難しくはないと思う.
今回指定しているコマンドラインオプションは,大きく分けて次の2つに分かれている.

  • 新しいクラスタを作成するときに使用するもの(Clustering flags
  • 他のメンバやクライアントとの通信や,etcdの設定に関するもの(Member flags

Clustering flagsでは,新しいクラスタを一から作成するときに必要となる情報を指定する.
今回使用したものは次の通り.

flags description
--initial-advertise-peer-urls クラスタ内の他のetcdメンバからの通信を受け付けるURLを指定する
--advertise-client-urls クライアントからの通信を受け付けるURLを指定する
--initial-cluster クラスタを構成するetcdメンバの情報をカンマ区切りで指定する
--initial-cluster-state 新しいクラスタを作成する場合は new を指定する
--initial-cluster-token クラスタ作成中に用いられるトークン.複数クラスタを管理している際,意図せず別のクラスタに影響を与えるのを防ぐ

Member flagsでは,作成するメンバに関する情報等の設定ができる.
今回使用したものは次の通り.

flags description
--name メンバ名
--listen-peer-urls クラスタ内の他のetcdメンバからの通信を受け付けるURLを指定する
--listen-client-urls クライアントからの通信を受け付けるURLを指定する

注意点として, --initial-cluster で指定しているメンバ情報は, --name--listen-peer-urls で指定した名前とURLに一致させなければならない.
もし一致していないと,クラスタ起動時にエラーとなる.

次のコマンドで,きちんと3つのメンバが表示されたら成功.

$ etcdctl member list
29727fd1bdf9fb62, started, etcd1, http://localhost:2380, http://localhost:2379
44dd3cd8faa339d0, started, etcd3, http://localhost:22380, http://localhost:22379
b59b01c27098773e, started, etcd2, http://localhost:12380, http://localhost:12379

次は,作成したクラスタを使ってもう少しetcdctlの使い方を勉強し,その後TLSの設定やDiscoveryを使ったクラスタ作成,無停止でのメンバアップグレード等について勉強していく予定.

GoのType AssertionとType Switches

Goでは,型を interface{} として宣言してあげることで,とりあえずどんな値でも格納することが出来る.
SlackのEvent API等,メッセージの形式がEventごとに異なるといった場合に使うと便利.

Example

func eventHandler(w http.ResponseWriter, r *http.Request) {
    var event map[string]interface{}
    decoder := json.NewDecoder(r.Body)
    if err := decoder.Decode(&event); err != nil {
        log.Fatalln(err)
    }
}

この例だと,変数 event に受け取ったイベントが格納される.
eventmap[string]interface{} として宣言されているので, event["token"] のように値を取得しても返ってくるのは interface{} 型である.
そのため,値を使いたいときに適切な型へ変換してあげる必要がある.

このような場合に,Goの言語仕様として用意されているType AssertionやType Switchesというものを使ってあげるとうまく型変換が出来る.

Type assertions

Type assertionsを使うと, interface{} から指定した型に変換することが出来る.
Type assertionsでは,次のような順番で処理が行われる.

  1. 値がnilであるかどうかを検査
  2. 指定した型に変換して値を返却

書き方は次の2つある.

1. 基本形

var x interface{} = 7   // interface{}としてxを宣言
i := x.(int)            // Type assertionsを使ってint型に変換

Type assertionsが成功すると,指定した型に変換された値が返り値として返される(上の例だと int に変換された 7 が返ってくる).
もし次の例のように正しく変換できなかった場合は,panicが発生する.

var x interface{} = "hello" // interface{}としてxを宣言
i := x.(int)                // xの値 "hello" はint型に変換できない => panic発生

2. 別の書き方

別の書き方として,次のように返り値を2つ受け取るバージョンがある.

var x interface{} = 7   // interface{}としてxを宣言
i, ok := x.(int)        // okには正しく変換できたかどうかがboolで格納される.成功した場合はtrue

この書き方を使うと,失敗したときでもpanicは発生せず, okfalse が設定されるだけとなる.
この時,1つめの返り値は指定した型のゼロ値となる.

var x interface{} = "hello" // interface{}としてxを宣言
i, ok := x.(int)            // xの値 "hello" はint型に変換できない => okはfalse
fmt.Println(i)              // 0(intのゼロ値)が出力される

Type Switches

ドキュメントでは,Type SwitchesはType assertionの特別形であると説明されている.
Type assertionでは変換したい型名を指定していたが,Type Switchesでは代わりに type を指定する.

var x interface{} = "hello"
switch i := x.(type) {
    case int:
        fmt.Println("x is int")
    case string:
        fmt.Println("x is string")
}

これを使うと,複数の型候補がある場合にswitch文を使って分岐させることが出来る.

参考文献

Goでバイナリを作る

ネットワークパケットを作るために,構造体や変数からバイト型のスライスへの変換方法を調べた.

テストで書いてみたプログラムは次の通り.
とりあえずint型の変数をいくつか対象としてバイト変換を試してみた.

何らかの型(構造体やint等のプリミティブ型)からバイト型のスライスに変換するには, encoding/binary パッケージの Write メソッドを使用すると良い.

binary.Write() メソッドは次のような定義になっている.

func Write(w io.Writer, order ByteOrder, data interface{}) error

第3引数に指定したデータを,第2引数で指定したバイトオーダーで第1引数の w に書き込む.

このメソッドを使うために,まず io.Writer インタフェースを実装したものが必要になる.
バイト型のデータを書き込みたいので, bytes パッケージの Buffer を使用することにする.

buf := new(bytes.Buffer)

注意点として, new() を使う点が挙げられる.
bytes.BufferWrite() メソッドはポインタ型( *bytes.Buffer )がレシーバとなっているため, new() を使ってポインタを取得して上げる必要がある.

これで io.Writer を用意できたので, binary.Write() を使ってデータを書き込む.
適当にint型の変数を用意して binary.Write() による書き込みを行う.
なお,今回はネットワークパケットを作りたいので,第2引数のバイトオーダはビッグエンディアンを指定する.

var val32bit int32
val32bit = 123

err = binary.Write(buf, binary.BigEndian, val32bit)
if err != nil {
    log.Fatalln(err)
}
fmt.Printf("uint32: % x\n", buf.Bytes())

これで無事書き込みができた.

サンプルプログラムの実行結果

uint8: 7b
uint16: 00 7b
uint32: 00 00 00 7b

今回はint型のみを扱ったが,構造体を書き込みたいときも同じようにして扱うことができるようだ.

HTTPリダイレクトについて調査した

HTTPのリダイレクトには,大きく分けて次の二つ存在する.

  • 一時的なリダイレクト
  • 恒久的なリダイレクト

一般的に用いられるリダイレクトのステータスコードは 301302 である(個人的な考えですが).
しかし,他にもいくつかリダイレクトのステータスコードは存在する.

また,似たような意味を持つステータスコードもあるので,整理するために少し調査した.

今回対象とするリダイレクトのステータスコード

今回は,次の4つを対象とする.

  • 301 Moved Permanently
  • 302 Found
  • 307 Temporary Redirect
  • 308 Permanent Redirect

それぞれのステータスコードについて,RFCを参考に概要を以下に示す.
308以外はRFC 7231,308はRFC 7238で定義されている.

301 Moved Permanently

  • 恒久的なリダイレクトを表す
  • 歴史的な経緯から,301を受け取ったクライアントはリクエストメソッドをPOSTからGETに変更する可能性がある
    • これを望まない場合,代わりに307を用いることができる
    • しかし,307とは意味が異なる(恒久的↔一時的)
      • 308が新しく定義された

302 Found

  • 一時的なリダイレクトを表す
  • 歴史的な経緯から,301を受け取ったクライアントはリクエストメソッドをPOSTからGETに変更する可能性がある
    • これを望まない場合,代わりに307を用いることができる

307 Temporary Redirect

  • 一時的なリダイレクトを表す
  • これを受け取ったクライアントはリクエストメソッドを変更してはいけない

308 Permanent Redirect

  • 恒久的なリダイレクトを表す
  • これを受け取ったクライアントはリクエストメソッドを変更してはいけない

挙動の確認

それぞれのステータスコードに対する挙動を確認するため,簡単なサーバプログラムをGoで作成し,そのサーバに4種類のクライアントで接続した.
作成したサーバプログラムは mas9612/http-redirect-test に置いている.

用いたクライアント
* curl 7.43.0
* Safari 11.1.2
* Google Chrome 67.0.3396.99
* Firefox 59.0.2

上記それぞれのクライアントから,GETとPOST2つのメソッドで作成したサーバに接続した.
結果を次の表に示す.

Method GET

client 301 302 307 308
curl GET GET GET GET
Safari GET GET GET GET
Chrome GET GET GET GET
Firefox GET GET GET GET

Method POST

client 301 302 307 308
curl POST POST POST POST
Safari GET GET POST POST
Chrome GET GET POST POST
Firefox GET GET POST POST

実験結果より,curl以外のクライアントは,POSTリクエストの応答結果が301と302の時,リダイレクト先へのリクエストメソッドをGETに変更していることがわかった.
また,RFCの定義どおり,307と308はきちんとリクエストメソッドが維持されていることが確認できた.

References