ServiceAccountのトークンで使われているJWTの処理はどこに書かれているのか

KubernetesのServiceAccountと、そのトークンで使われているJWTのSign/Verifyのコードについて追った。ServiceAccountとJWTについての説明はしません。

ServiceAccountのトークンはServiceAccountと紐づくSecretに含まれている。このSecretはkube-controller-manager内のtoken-controllerが作成している。

https://github.com/kubernetes/kubernetes/blob/v1.16.2/pkg/controller/serviceaccount/tokens_controller.go#L383-L400

トークンの署名にはkube-controller-managerの引数である --service-account-private-key-file で渡した秘密鍵を使う。

トークンはKubernetesのAPIを利用する際の認証・認可に使われる。トークンのベリファイにはkube-apiserverの引数である --service-account-key-file で渡した秘密鍵もしくは公開鍵を使う。指定していない場合は --tls-private-key-file で渡した鍵が使われる。--service-account-key-file を渡している場合、jwtTokenAuthenticatorがauthenticatorsとして使えるようになる。

https://github.com/kubernetes/kubernetes/blob/v1.16.2/pkg/kubeapiserver/authenticator/config.go#L139-L145

jwtTokenAuthenticatorはトークンのベリファイをAuthenticateTokenメソッドで行う。

最後に、Sign/ParseSignの練習コードを乗せておく。RS256で、秘密鍵と証明書を使っている。https://github.com/takaishi/hello2019/blob/master/jwt/rs256/main.go にもある。

package main

import (
	"crypto/rsa"
	"crypto/x509"
	"encoding/pem"
	"errors"
	"fmt"
	"github.com/k0kubun/pp"
	"gopkg.in/square/go-jose.v2"
	"gopkg.in/square/go-jose.v2/jwt"
	"io/ioutil"
	"time"
)

func readPrivateKey(path string) (*rsa.PrivateKey, error) {
	privateKeyData, err := ioutil.ReadFile(path)
	if err != nil {
		panic(err)
	}
	privateKeyBlock, _ := pem.Decode(privateKeyData)
	if privateKeyBlock == nil {
		panic(errors.New("invalid private key data"))
	}
	if privateKeyBlock.Type != "RSA PRIVATE KEY" {
		panic(errors.New(fmt.Sprintf("invalid private key type : %s", privateKeyBlock.Type)))
	}

	return x509.ParsePKCS1PrivateKey(privateKeyBlock.Bytes)
}

func readCertificate(path string) (*x509.Certificate, error) {
	certificateData, err := ioutil.ReadFile(path)
	if err != nil {
		panic(err)
	}
	certificateBlock, _ := pem.Decode(certificateData)
	if certificateBlock == nil {
		panic(errors.New("invalid private key data"))
	}
	return x509.ParseCertificate(certificateBlock.Bytes)
}

func main() {
	privateKey, err := readPrivateKey("./service-account-key.pem")
	if err != nil {
		panic(err)
	}

	privateJWK := &jose.JSONWebKey{
		Algorithm: string(jose.RS256),
		Key:       privateKey,
		Use:       "sig",
	}

	signer, err := jose.NewSigner(
		jose.SigningKey{
			Key:       privateJWK,
			Algorithm: jose.RS256,
		},
		nil,
	)

	cl := jwt.Claims{
		Subject:   "subject",
		Issuer:    "issuer",
		NotBefore: jwt.NewNumericDate(time.Date(2016, 1, 1, 0, 0, 0, 0, time.UTC)),
		Audience:  jwt.Audience{"leela", "fry"},
		ID:        "ryo",
	}
	raw, err := jwt.Signed(signer).Claims(cl).CompactSerialize()
	if err != nil {
		panic(err)
	}

	fmt.Println(raw)

	tok, err := jwt.ParseSigned(raw)
	if err != nil {
		panic(err)
	}

	certificate, err := readCertificate("./service-account.pem")
	if err != nil {
		panic(err)
	}

	out := jwt.Claims{}
	err = tok.Claims(certificate.PublicKey, &out)
	if err != nil {
		panic(err)
	}
	pp.Print(out)

}