SPIREのセレクタ周りがわからん！

# はじめに

最近SPIFFEとその実装であるSPIREを触っているのですが、SPIFFEIDをWorkloadに紐づけたり、Nodeのグルーピングを行うためのEntryや、それに含まれている「セレクタ」の仕組みがよくわからず、非常に混乱したので、整理しました。

SPIREは「基本的な道具」となるソフトウェアで、様々な構成で利用できるように作られています。これ自体は非常に良いことなのですが、それ故にドキュメントを読んでも、具体例が思い浮かばず、混乱してしまうことがあると感じています。

この記事では、Nodeのグルーピングを行うためのEntryや、それに含まれている「セレクタ」の仕組みについて、簡単な例を示して、解説することを心がけています。

この記事で示した以外の利用方法もたくさん想定されているので、この記事の例は「あくまで一例」として参考にすることをお勧めします。

# 足早にSPIREについての解説

この記事では、SPIREの「セレクタ」やSPIFFEIDがどのようにWorkloadに紐づけられるか、といった内容を解説していきますが、その前に・・少しだけSPIREについて紹介します。

## なぜSPIFFE/SPIREが必要なのか

従来のIPベースのアクセス管理は既に限界がきています。例えば・・

- ネットワーク内からの攻撃が一般的になってきている
- クラウドネイティブ時代はIPは「はかないもの」となりACLとして利用できない

そこでIPではない、何かを使ってサービス間のアクセスを認証する必要が出てきています。

## mTLS

ここで使えそうなのはmTLSです。
これはサーバ認証とクライアント認証を相互に行う通信方式です。

手元の小さなサービス間でmTLSを行う限りにおいては独自の方式を用いてこの認証をおこなうという解決策もあり得ますが、チーム間や会社間で相互に認証を行うとなると話は別で、この方式を何らかの形で「標準化」する必要性が生じてきます。

認証の方法はX509で標準化されているものを利用するとして、証明書の中にどのようにIDを格納するか？ワークロードがどのような方法で証明書を取得するか？といった方式を決める必要があります。

ここで出てくるのがSPIFFE/SPIREです。

## SPIFFE と SPIRE

SPIFFEはこのIDやワークロードが証明書を取得するためのAPIの仕様などの「規格」であり、SPIREはそのSPIFFEという「規格」を実装した「ソフトウェア」です。

## SPIFFE/SPIREが実現する世界

SPIFFE/SPIREを利用することで以下のような構成が実現でき、これによりサービス間のアクセス認証を標準的な方法で実現することができます。

- Workloadは自身が動作するNodeに同居するAgentに問い合わせることで、mTLSに必要な証明書を得ることができる。
- 互いにSPIFFEIDと呼ばれる識別子を付与され、アプリケーションの実装によっては、このIDにより認可を行うこともできる。
- SPIRE Serverは中央集権。SPIRE Agentは各Nodeに存在する。

この記事ではSPIFFEIDと呼ばれるSPIFFEの世界で互いを識別するIDがどのように各AgentやWorkloadに紐づけられるかという視点で紹介します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100375/45cfd675-84ea-8d5c-43b0-d72374467854.png)

## SPIFFE/SPIREについてもっと知りたい人は・・

- [SPIFFEとその実装であるSPIREについて](https://qiita.com/hiyosi/items/8eec00f396af2050db61)
- [SPIFFEで何が解決できるのか](https://qiita.com/hiyosi/items/6e8d56b257c55a709b4d)
- [注目のSPIFFE、その概要とKubernetesへの導入方法
](https://thinkit.co.jp/article/17919)

# WorkloadがSVIDを得るための流れ

ここからはWorkloadがSPIFFEIDをどのように取得するのかという流れを順を、追って説明していきます。

## SPIRE Agentの起動時の処理

SPIRE Agentは起動時にSPIRE Serverに対して自身の証明書（および自身に紐づくWorkloadの証明書）をリクエストします。SPIRE Serverは誰にでも証明書を配布するわけにはいかないので、リクエストが来たSPIRE Agentが本当に信頼できる相手なのかを確認するAttestという動作を行います。（NodeAttestationという）

この処理はAgent, Server共にプラグインとなっていますが、最も単純なJoinToken方式の場合は、事前に手動でSPIRE Serverから発行したTokenをSPIER Serverに送信することで、自身を証明します。

SPIRE ServerでそのTokenの正しさが確認できればSPIRE AgentにSVIDを返却します。
このSVIDの中にはこのAgentを一意に特定するためのSPIFFEIDも含まれています。

以降SPIRE AgentはこのSVIDを使ってmTLSでServerと通信します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100375/d5bbd821-2f64-241f-d863-a6ce679e2144.png)

## Workloadの登録

SPIFFEの世界では互いに認証して通信を行う主体をWorkloadと読んでいます。ここではWorkloadは単なるプロセスだと考えてください。

Workloadはあらかじめ決められた条件を満たしたものがそのSPIFFEIDを名乗れるという仕組みになっています。

そのため、まずはその条件を登録します。

これはRegistration Entryと呼ばれる構造で

- そのWorkloadを表すSPIFFEID
- Workloadの条件（Selector）
- 親のSPIFFEID
  - ここではSPIRE AgentのSPIFFEIDを指定します（他の例もあります）

で構成されていいます。図にすると下のような感じ。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100375/624fb023-32cf-70e4-ebe3-04cbe7dc167a.png)

例えばこのようなレコードで登録したとします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100375/794b6969-bb8f-97cd-bcd4-5e8c830cde21.png)

## SPIRE Agent によるSVIDの同期

SPIRE AgentはSPIRE Serverと定期的に通信しており、自身が発行する可能性のあるWorklaodのSVIDとその条件（セレクタ)をあらかじめ取得しています。

ここまでの例だと 親のSPIFFEIDが自身のSPIFFEIDとなっているEntryが対象となります。（他にもありますが後述します。）

そのようなWorkloadが今動いているか・いないかにかかわらず、あらかじめSVIDを取得しておきます。

また、SVIDに含まれる証明書の有効期限が切れないように更新し続けます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100375/fa6cfec7-a562-6773-b94f-aae0bd011c62.png)

## WorkloadがSVIDを取得する

Workloadは例えばLinuxのプロセスです。このプロセスがSPIREが提供するライブラリを使うことで、同じマシンで動作しているSPIRE AgentへUnix Domain Socket経由で通信を行い、SVIDを要求します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100375/a58ddf63-7bcc-29d8-6ec9-3e8fe957d17b.png)


SPIRE Agentは該当プロセスの属性を調べ、セレクタを生成します。前工程で取得しておいたWorkload用のSVIDとセレクタの組の中から、そのWorkloadに合致するSVIDを選び、Workloadに返却します。条件に合致するSVIDが複数存在する場合は、その全てを返却します。Workloadのセレクタを調べ、該当するSVIDを配布するこの部分をWorkload Attestaionと呼びます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100375/8b5f3211-823c-0520-a8dd-e47acea285cd.png)

このような流れで、WorkloadがSVIDを取得しています。
このようにしてSVIDを取得したWorkload同士は、SVIDを利用し安全な通信を行うことができます。

## Advanced: Nodeのグルーピング

ここまでの話だと、Workloadが複数のサーバ上で動作するためには、サーバ台数分のEntryが必要となります。
Workloadが複数のサーバ上で動作するとは・・、よくある水平スケールした状態です。

「Node1,Node2,Node3があり、それぞれでサービスAのプロセスが動いている。」といった状況です。

もちろんサーバ台数分のEntryを用意しても良いのですが、煩雑です。

そのような時に利用できる「サーバをグルーピングする仕組み」があります。

ここでも先ほど利用したEntryという構造を利用します。
サーバをグルーピングする場合は、

- そのNodeのグループを表すSPIFFEID
- Nodeの条件（Selector）
- 親のSPIFFEIDにSPIRE ServerのSPIFFE ID（特別なパスである /spire/serverを持つ）を指定する

（個人的にはWorkloadとSPIFFEIDを紐づけるEntryが、Nodeのグルーピングという別の用途に利用されているというのが分からず、混乱しました。）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100375/f6a5e600-a1f7-40cf-0e1a-107540151fa1.png)

ということで例えばこのようなEntryを登録したとします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100375/879c93a1-7fa9-ed6f-f58b-895b710fa1a9.png)

実はNodeAttestationが完了したタイミングで、SPIRE ServerはSPIRE AgentのSPIFFE IDと一緒にAgentが稼働するNodeの属性情報をセレクタの形で保存しています。
Nodeのグルーピングの際には、そのセレクタを使ってグループ化を行います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100375/997ba1f9-b465-f9ec-08d7-da543a3d33ad.png)


グルーピングしたNodeのどこでも動作するワークロードを登録するためには、Parent SPIFFEIDに先ほど作った グルーピングのためのEntryのSPIFFEIDを指定したEntryを作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100375/a675be2c-8886-2d80-25d7-955248eedf30.png)

この2つのエントリにより、グルーピングしたNode群の上で条件に合致したWorkloadからSVIDを取得できるようになりました。

登場人物をまとめると下記のようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100375/2dc80ac6-8db6-ac12-6029-5a2dcfa6fe32.png)

前の項目の「Agentがあらかじめ自身に関係するSVIDをServerから取得しておく流れ」の際に説明していませんでしたが、Serverは「対象AgentのSVIDがParentとなっているEntry」の他に『「AgentのグルーピングのためのEntry」の中で対象Agentが含まれれるもの』をParentとするEntryも返却しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100375/0fbb3fe0-1407-643d-4066-9856a8dce5a9.png)

この仕組みにより、SPIRE Agentにはそのグループ化されたNodeで動くことが許されたWorkloadのためのSVIDと、そのWorkloadを識別するためのセレクタを取得できます。

あとはリクエストが来たWorkloadの属性情報（セレクタ）を取得して、該当するSVIDを返却するという流れは前説明した通りです。

# 実例

ここからは実例を交えて どのようにWorkloadにSPIFFEIDが割り当てられるかを見ていきます。

## 実例1

さて、ここまでの知識を使って、SPIFFEのチュートリアルを見てみます。

まずはLinuxマシンを利用した例です。

https://spiffe.io/docs/latest/try/getting-started-linux-macos-x/

※チュートリアルの中身は説明しないので、上記ページを確認してください。

ここでは同じサーバにSPIRE server, SPIRE agentをインストールしています。
そしてSPIRE agentはJoinTokenの仕組みで Node Attestaionしています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100375/032c110c-b36f-5b80-9704-6bce55d49be1.png)

AgentのSPIFFEIDは `spiffe://example.org/myagent` です。

Entryは下記のコマンドラインで登録しています。

```
$ bin/spire-server entry create -parentID spiffe://example.org/myagent \
    -spiffeID spiffe://example.org/myservice -selector unix:uid:$(id -u)
Entry ID      : ac5e2354-596a-4059-85f7-5b76e3bb53b3
SPIFFE ID     : spiffe://example.org/myservice
Parent ID     : spiffe://example.org/myagent
TTL           : 3600
Selector      : unix:uid:501
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100375/03ae065c-30ca-eb7e-4c33-445cecfe84a9.png)

これは「Parent IDに AgentのSPIFFEID」をもつEntryです。
このエントリにより、条件（ここではプロセスのUID）を満たしたWorkloadがこのAgentからSVIDを取得できるようになります。

## 実例2

Kubrenetesの例も見てみます。

https://spiffe.io/docs/latest/try/getting-started-k8s/

この例では2つのEntryを登録しています。

1つ目がこちら。

```
$ kubectl exec -n spire spire-server-0 -- \
    /opt/spire/bin/spire-server entry create \
    -spiffeID spiffe://example.org/ns/spire/sa/spire-agent \
    -selector k8s_sat:cluster:demo-cluster \
    -selector k8s_sat:agent_ns:spire \
    -selector k8s_sat:agent_sa:spire-agent \
    -node
```

こちらは`-node`という引数で登録しており、これが「Nodeをグループ化」するためのEntryです。（ParentIDのパスが`/spire/server`となっているものです）

よく読むと

```
    -selector k8s_sat:cluster:demo-cluster \
    -selector k8s_sat:agent_ns:spire \
    -selector k8s_sat:agent_sa:spire-agent \
```

のセレクタを満たすようなNodeを`spiffe://example.org/ns/spire/sa/spire-agent`というSPIFFEIDでグループ化しています。

もう一つのEntryがこちら。

```
$ kubectl exec -n spire spire-server-0 -- \
    /opt/spire/bin/spire-server entry create \
    -spiffeID spiffe://example.org/ns/default/sa/default \
    -parentID spiffe://example.org/ns/spire/sa/spire-agent \
    -selector k8s:ns:default \
    -selector k8s:sa:default
```

ParentIDに先ほどのグループ化したSPIFFEIDを指定しています。

これにより、前述のセレクタを満たすすべてのNode上で、セレクタ[`k8s:ns:default`, `k8s:sa:default`]を満たすWorkloadがSVIDを取得できるようになります。

ここで出てきているノードのセレクタである`k8s_sat`が前置されたものや、Workloadのセレクタである`k8s`が前置されたものはプラグインにより付与されたものです。

- https://github.com/spiffe/spire/blob/v0.12/doc/plugin_agent_nodeattestor_k8s_psat.md
- https://github.com/spiffe/spire/blob/v0.12/doc/plugin_agent_workloadattestor_k8s.md

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100375/e48dc12a-5ced-8c1a-e01d-8782ba9af9ad.png)

# おわりに

SPIREの「セレクタ」とEntryについて少しはわかったでしょうか？

自分もまだまだ学んでいる最中なので、この記事にも間違いや、ミスリードを誘うような表現が含まれているかもしれません。

お気づきの点がありましたら、お気軽にコメント・編集リクエストを出していただけるありがたいです。