Kubernetes Authentication with Client Certificate

Kubernetes Authentication

When we use Kubernetes API, the request is checked by following order after TLS is established[1].

  1. Authentication: Checked whether user is granted to access API
  2. Authorization: Checked whether user is granted to do requested action to specified object
  3. Admission Control: Modify or reject request

To test and study Kubernetes authentication flow, I was tested it with minikube.

Environment

  • Kubernetes cluster bootstrapped with minikube
$ kubectl version
Client Version: version.Info{Major:"1", Minor:"13", GitVersion:"v1.13.1", GitCommit:"eec55b9ba98609a46fee712359c7b5b365bdd920", GitTreeState:"clean", BuildDate:"2018-12-13T19:44:19Z", GoVersion:"go1.11.2", Compiler:"gc", Platform:"darwin/amd64"}
Server Version: version.Info{Major:"1", Minor:"12", GitVersion:"v1.12.4", GitCommit:"f49fa022dbe63faafd0da106ef7e05a29721d3f1", GitTreeState:"clean", BuildDate:"2018-12-14T06:59:37Z", GoVersion:"go1.10.4", Compiler:"gc", Platform:"linux/amd64"}

Authentication

In this test, we used X509 Client Certs (Client certificate authentication) as authentication strategy.
For more information about it or other authentication strategy, see [2].

Procedure:

  1. Create client key and CSR (Certificate Signing Request)
  2. Sign CSR created in previous step with CA key
  3. Register client key and certificate to kubectl config

In minikube, client certificate authentication is enabled by default.
So we don’t need to enable it manually.
Let’s create client key and certificate.

Create client key and CSR

First, we must create client key and CSR (Certificate Signing Request).
We can create these with openssl command.

# generate RSA private key (Algorithm: RSA, Key bits: 2048, Key name: client.key)
$ openssl genpkey -algorithm rsa -pkeyopt rsa_keygen_bits:2048 -out client.key

# generate CSR (Username: mas9612, Group: users)
$ openssl req -new -key client.key -out client.csr -subj "/CN=mas9612/O=users"

When issuing CSR, we must pass username and group at -subj option.
CN means username, O means group. User can be associated with multiple groups.
To add user more than one groups, simply add O section to -subj option.

# user mas9612 is now a member of "users" and "member" groups
$ openssl req -new -key client.key -out client.csr -subj "/CN=mas9612/O=users/O=member"

Sign CSR with CA key

After create CSR, we must sign it with CA (Certificate Authority) key.

First, we must fetch CA key and certificate from minikube VM.
We can do that with following commands.

$ minikube ssh
$ sudo cp /var/lib/minikube/certs/ca.crt /var/lib/minikube/certs/ca.key ~
$ sudo chown $(id -u):$(id -g) ~/ca.crt ~/ca.key
$ exit

$ scp -i $(minikube ssh-key) docker@$(minikube ip):~/ca.crt .
$ scp -i $(minikube ssh-key) docker@$(minikube ip):~/ca.key .

Finally, sign CSR with fetch key and certificate.

$ openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
-out client.crt -days 3650

To check created certificate, run following command.

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

Register client key and certificate to kubectl config

After create client key and certificate, we should register it to kubectl config to use it easily.

# create "testuser" with given key and certificate
$ kubectl config set-credentials testuser --client-key=./client.key --client-certificate=./client.crt

# bind "testuser" and minikube cluster as "authtest" context
$ kubectl config set-context authtest --cluster=minikube --user=testuser

# change to use "authtest" context instead of minikube default
$ kubectl config use-context authtest

After register credentials, we can use kubectl command as created user.
But now, we aren’t granted to use any API so any request will be rejected.

$ kubectl get pods
Error from server (Forbidden): pods is forbidden: User "mas9612" cannot list resource "pods" in API group "" in the namespace "default"

To allow API request, we must assign appropriate Role to User.

Authorization

In the previous section, we tried to create new user and to query running pod information.
We confirmed that user is properly created but any operation is not allowed.

In this section, we will examine Kubernetes RBAC API.

Role/ClusterRole, RoleBinding/ClusterRoleBinding[5]

Kubernetes RBAC API has 4 types: Role, ClusterRole, RoleBinding, ClusterRoleBinding.

Types prefixed by Cluster- have cluster-wide effect.
In contrast, types non-prefixed by Cluster- have only specific namespace (e.g. default namespace)

Role/ClusterRole contains rules that represent a set of permissions.
Default permission is all deny so you must add some rule to allow operation (e.g. list pods, create new deployment, etc.).

RoleBinding/ClusterRoleBinding grants the permissions to a user (or a set of users).

So if we want to add some permission to user, first create appropriate Role and then bind it to user by RoleBinding.

Create Role

Let’s create Role to list running pods in default namespace.
Create client-role.yml with the following content.

kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
namespace: default
name: test-role
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "watch", "list"]

After create manifest, apply it with kubectl:

# make sure that we're using minikube context
$ kubectl config use-context minikube

$ kubectl apply -f client-role.yml
role.rbac.authorization.k8s.io/test-role created

# check Role is created properly
$ kubectl get role
NAME        AGE
test-role   102s

Create RoleBinding

Next, we create RoleBinding to bind User and Role.
Create client-rolebinding.yml with the following content.

kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: client-read-access
namespace: default
subjects:
- kind: User
name: mas9612
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: test-role
apiGroup: rbac.authorization.k8s.io

Please change username to that you created.

After create manifest, apply it with kubectl:

$ kubectl apply -f client-rolebinding.yml
rolebinding.rbac.authorization.k8s.io/client-read-access created

$ kubectl get rolebindings
NAME                 AGE
client-read-access   48s

Finally, change context to auth-test and try to query pods again!

$ kubectl config use-context auth-test
Switched to context "auth-test".

$ kubectl get pods
No resources found.

References

GoでのファイルI/O

GoでのファイルI/Oについて,改めてまとめた.
いろいろな方法があるので,それぞれどういったものかを確認しながらまとめる.

ファイルオープン

読み書きを行う前に,まずファイルオープンしないとどうにもならないのでそこから.
osパッケージを見ると,2つのファイルオープンメソッドがあることがわかる.

  • os.Open
  • os.OpenFile

os.Open

func Open(name string) (*File, error)

引数に与えられた名前のファイルを 読み取り専用 でオープンする.
そのため,もしファイルが存在しなければエラーとなる( *PathError が返却される)

// os.Open attempts to open given file as read only mode.
// Therefore, if it doesn't exist, then *os.PathError will occur.
_, err := os.Open("thisdoesntexist.txt")
if err != nil {
    if os.IsNotExist(err) {
        log.Println("file not found", err)
    } else {
        log.Println(err)
    }
}

上の例では便利メソッドとして os.IsNotExist を使っている.
このメソッドに os.Open から返却されたエラーを渡すと,ファイルが存在しないために発生したエラーかどうかを教えてくれる.
os.IsNotExist の返り値が true なら,ファイルが存在しないという意味になる.

os.OpenFile

func OpenFile(name string, flag int, perm FileMode) (*File, error)

引数に与えられた名前のファイルを,指定したモード,パーミッションでオープンする.
flag の指定方法次第で,追記モードや,存在しない場合に作成する,等が可能になる.

// os.OpenFile attempts to open given file as given mode and permission.
// In this example, open "newfile.txt" as write-only mode and it permission is 0600 (r/w only allowed to file owner)
file, err := os.OpenFile("newfile.txt", os.O_WRONLY|os.O_CREATE, 0600)
if err != nil {
    log.Fatalln(err)
}
defer file.Close()

(おそらく)最も基本となる方法

ファイルをオープンし,バイト型のスライスを使ってデータの読み書きを行う方法.

Read

前提として,ファイルからの読み取りができるモードでオープンされている必要がある.

file, err := os.Open("newfile.txt")
if err != nil {
    log.Fatalln(err)
}
defer file.Close()

// *File.Read reads slice of bytes up to len(slice) from file.
buffer := make([]byte, 1024)
n, err := file.Read(buffer)
if err != nil {
    log.Fatalln(err)
}
log.Printf("%d bytes read by *File.Read()\n", n)
log.Printf("file content: %s\n", string(buffer))

Write

前提として,ファイルに書き込みができるモードでオープンされている必要がある.

// os.OpenFile attempts to open given file as given mode and permission.
// In this example, open "newfile.txt" as write-only mode and it permission is 0600 (r/w only allowed to file owner)
file, err := os.OpenFile("newfile.txt", os.O_WRONLY|os.O_CREATE, 0600)
if err != nil {
    log.Fatalln(err)
}
defer file.Close()

// *File.Write writes slice of bytes to file.
byteData := []byte("Hello world\n")
n, err := file.Write(byteData)
if err != nil {
    log.Fatalln(err)
}
log.Printf("%d bytes written by os.Write()\n", n)

また,バイト型のスライスの代わりにstringを書き込むこともできる.
stringの書き込みには WriteString メソッドを使用する.

// *File.WriteString writes strings to file instead of slice of bytes.
stringData := "We can write not only []byte but also string :)"
n, err = file.WriteString(stringData)
if err != nil {
    log.Fatalln(err)
}
log.Printf("%d bytes written by os.WriteString()\n", n)

ファイルの内容すべてを読み込む

io/ioutilパッケージの ReadAll メソッドを使用すると,ファイルの内容すべてを読み込むことができる.

type Reader interface {
    Read(p []byte) (n int, err error)
}

func ReadAll(r io.Reader) ([]byte, error)

ReadAllの引数に与える io.Reader は, Read メソッドを持つインタフェースと定義されている.
そのため,通常通りオープンしたファイルをそのまま渡すことができる.

file, err := os.Open("newfile.txt")
if err != nil {
    log.Fatalln(err)
}
defer file.Close()

bytes, err := ioutil.ReadAll(file)
if err != nil {
    log.Fatalln(err)
}
log.Printf("Read all contents by ioutil.ReadAll(): %s\n", string(bytes))

バッファありのファイルI/O

bufioパッケージのメソッドを使用すると,読み書きの際に内部でバッファを使ってくれる.
そのため,そのままデータを読み書きするよりも効率的に処理を行うことができる.

ファイルI/Oに使えそうなものは次の3種類.

  • bufio.Reader
  • bufio.Scanner
  • bufio.Writer

bufio.Reader

基本的な使い方は通常のファイルと似ているが,いくつか便利なメソッドが定義されている.

reader := bufio.NewReader(file)
buffer := make([]byte, 5)
// basic Read method
if _, err := reader.Read(buffer); err != nil {
    log.Fatalln(err)
}
log.Printf("content: %s\n", string(buffer))

// ReadBytes reads until delimiter found.
// Read contents is slice of bytes.
// In this example, read until first '\n' character found.
bytes, err := reader.ReadBytes('\n')
if err != nil {
    log.Fatalln(err)
}
log.Printf("content: %s\n", string(bytes))

// ReadString reads until delimiter found.
// Read contents is string.
// In this example, read until first '\n' character found.
str, err := reader.ReadString('\n')
if err != nil {
    log.Fatalln(err)
}
log.Printf("content: %s\n", str)

bufio.Scanner

bufio.Readerと似ているが,こちらは改行区切りのテキストを扱う時に便利なものになっている.

file, err := os.Open("newfile.txt")
if err != nil {
    log.Fatalln(err)
}
scanner := bufio.NewScanner(file)
for scanner.Scan() {
    log.Println(scanner.Text())
}

Text メソッドを呼ぶと,改行文字まで(=1行分の文字)を返してくれる.

Scantrue の間は,まだ読んでいない行があるということを示している.
なので, Scanfalse になるまでループを回してあげれば結果的にファイルの内容すべてを読むことができる.

bufio.Writer

Readerと同様,io.Writerと似ている.
注意しなければならない点として,最後に Writer.Flush を呼び出す必要がある点がある.
これを呼び出さないと正常に書き込みされないので注意する.

file, err := os.OpenFile("newfile.txt", os.O_WRONLY|os.O_CREATE, 0600)
if err != nil {
    log.Fatalln(err)
}
defer file.Close()

writer := bufio.NewWriter(file)

byteData := []byte("Hello world\n")
n, err := writer.Write(byteData)
if err != nil {
    log.Fatalln(err)
}
log.Printf("%d bytes written\n", n)

stringData := "Write string :)"
n, err = writer.WriteString(stringData)
if err != nil {
    log.Fatalln(err)
}
log.Printf("%d bytes written\n", n)

writer.Flush()

References

pipeによるプロセス間通信

fork() で作成した子プロセスと親プロセスの間で情報のやり取りをするために,IPC(Inter Process Communication)の一つであるパイプを利用した.
一度理解してしまえば特に難しいものではなかったので,文章としてまとめておく.

パイプの概要

シェルを使用していると,「何かのコマンドの出力をgrepしたい」というとき等,あるコマンドの出力を別のコマンドの入力として扱いたいということが多々ある.このような場合,「パイプ」という機能を使って次のようにコマンドを実行することで実現できる.

$ cat something.txt | grep Hello

上記のコマンドを実行すると,catコマンドの出力から, Hello を含んでいる行のみを画面に出力させることができる.
(上記の例ではパイプを使わずともgrepコマンド単体で同じことが可能であるが)

このように,パイプの入口・出口となるファイルディスクリプタを接続することができるという機能を持つ.

パイプの利用

次のような簡単なサンプルプログラムを作成した.

特に難しいことはやっておらず,ただ単に fork() した後,親プロセスから子プロセスに文字列を送るだけのプログラム.

パイプを使うため, fork() を呼び出す前に pipe() を呼び出しておく.
pipe() システムコールを呼び出すと,引数に与えた配列の0番目に「読み取り用」,1番目に「書き込み用」のファイルディスクリプタを格納してくれる.
これらに対して書き込み・読み取りをすると,それぞれ対応するファイルディスクリプタから読み取り・書き込みを行うことができる.

pipe() を呼び出した跡は通常通り fork() を呼び出す.
これにより,子プロセスが作成され, pipe() によって作成されたファイルディスクリプタのペアも複製される.
その後,親プロセスと子プロセスで,次の必要ないファイルディスクリプタをそれぞれクローズしておく.

  • 親プロセス→読み取り用のファイルディスクリプタ( fds[0]
  • 親プロセスからは書き込みのみを行うため
  • 子プロセス→書き込み用のファイルディスクリプタ( fds[1]
  • 子プロセスからは読み取りのみを行うため

あとは,通常通り read()write() を呼び出すだけ.

今回は, fork() した後でも親・子ともに同じプログラムを実行していたが, execve() 等を使って子プロセスでは別のプログラムを動作させることももちろん可能.
この場合,子プロセスで標準入力からデータを読み込みたい場合は, dup2() を使って fds[0]0 (標準入力)に複製してあげると良い.

参考文献

  • Michael Kerrisk,Linuxプログラミングインタフェース,2012年12月 発行,ISBN978-4-87311-585-6

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を使ったクラスタ作成,無停止でのメンバアップグレード等について勉強していく予定.