Help us understand the problem. What is going on with this article?

SPIREでSPIFFEのFederationを試してみる

今年のAdvent Calendarの内容はSPIFFEが定義するFederation APIについて、SPIREを使ってどのようなものか試してみたいと思います。

SPIFFEについてやFederation APIについては昨年の記事を参考にしてください。

構成

今回の構成としては異なるトラストドメインを持つSPIRE Serverを別々のKubernetes Namespaceにデプロイします。また、それぞれのNamespaceのWorkloadは異なるNodeにデプロイされるようにし、最終的にフェデレーションの設定が上手くいけばお互いのWorkload間でSVIDが検証できるというものです。

※ 異なるNamespaceにSPIREをデプロイするような構成は一般的ではないと思います。今回はあくまでフェデレーションの機能を試してみることを目的として構成しています。

スクリーンショット 2019-12-17 14.52.43.png

インストールと事前準備

まずは kind などを使って適当にk8s クラスタを作成します。

❯ cat cluster.yaml
kind: Cluster
apiVersion: kind.sigs.k8s.io/v1alpha3
kubeadmConfigPatches:
- |
  apiVersion: kubeadm.k8s.io/v1beta2
  kind: ClusterConfiguration
  metadata:
    name: config
  apiServer:
    extraArgs:
      "service-account-key-file": "/etc/kubernetes/pki/sa.pub"
      "service-account-signing-key-file": "/etc/kubernetes/pki/sa.key"
      "service-account-issuer": "api"
      "service-account-api-audiences": "api,spire-server"
nodes:
- role: control-plane
- role: worker
- role: worker

❯ kind create cluster --image kindest/node:v1.17.0 --name spire-federation --config cluster.yaml
Creating cluster "spire-federation" ...
 ✓ Ensuring node image (kindest/node:v1.17.0) 🖼
 ✓ Preparing nodes 📦📦📦📦
 ✓ Creating kubeadm config 📜
 ✓ Starting control-plane 🕹️
 ✓ Installing CNI 🔌
 ✓ Installing StorageClass 💾
 ✓ Joining worker nodes 🚜
Cluster creation complete. You can now use the cluster with:

export KUBECONFIG="$(kind get kubeconfig-path --name="spire-federation")"
kubectl cluster-info

❯ kubectl get nodes
NAME                             STATUS   ROLES    AGE   VERSION
spire-federation-control-plane   Ready    master   88s   v1.17.0
spire-federation-worker          Ready    <none>   57s   v1.17.0
spire-federation-worker2         Ready    <none>   57s   v1.17.0

今回の検証用に一方のWorkerにはNamespace=foo用のPodをデプロイするためのTaintの設定を、もう一方にはNamespace=bar用の設定をします。

❯ kubectl taint nodes spire-federation-worker app=foo:NoSchedule
node/spire-federation-worker tainted

❯ kubectl taint nodes spire-federation-worker2 app=bar:NoSchedule
node/spire-federation-worker2 tainted

また、今回はSPIREのUpstreamCAとして静的なルートCA証明書を使う(disk pluginを利用する)ため、以下を参考にそれぞれのトラストドメイン用のCA証明書を発行します。
今回はNamecpace=fooにデプロイするSPIREにはfoo.example.comを、Namespace=barにはbar.example.comをトラストドメインとして設定したいのでそれぞれ異なるルートCA証明書を発行します。

❯ cat <<EOF > foo-ca.json
{
  "CN": "foo root",
  "key": {
    "algo": "ecdsa",
    "size": 256
  },
  "names": [
    {
      "C": "JP",
      "L": "Tokyo",
      "O": "SPIRE",
      "OU": "CA"
    }
  ]
}

❯ cfssl gencert -initca foo-ca.csr | cfssljson -bare foo-ca

SPIRE Serverのデプロイ

次にSPIRE Serverを以下のような設定ファイルでk8sクラスタにデプロイしていきます。設定ファイルについてはserver.experimentalの部分がフェデレーションのキモになります。自身のbundleエンドポイントの設定と、フェデレーション先のbundleエンドポイントの情報を設定しています。

k8sにデプロイするためのマニフェストファイルやその他設定については spire-examples を参考にしてください。また、その際には先程設定したTaintsに対するTolerationsをPodのspecに設定します。

Namespace=fooにデプロイするSPIREのPodに設定するTolerationsの例

    spec:
        tolerations:
        - key: "app"
          operator: "Equal"
          value: "foo"
          effect: "NoSchedule"

Namespace=fooにデプロイするSPIREの設定ファイルの例

server {
  bind_address = "0.0.0.0"
  bind_port = "8081"
  trust_domain = "foo.example.com"
  data_dir = "/run/spire/data"
  log_level = "DEBUG"
  svid_ttl = "1h"
  upstream_bundle = true

  experimental {
    bundle_endpoint_enabled = true
    bundle_endpoint_port = 8443

    federates_with "bar.example.com" {
      bundle_endpoint_address = "spire-server.bar.svc.cluster.local"
      bundle_endpoint_port = 8443
      bundle_endpoint_spiffe_id = "spiffe://bar.example.com/spire/server"
    }
  }
}

plugins {
 ...省略...
  UpstreamCA "disk" {
    plugin_data {
      ttl = "12h"
      key_file_path = "/run/spire/secrets/bootstrap.key" #先程発行したfoo用の秘密鍵
      cert_file_path = "/run/spire/config/bootstrap.crt" #先程発行したfoo用のCA証明書
    }
  }
}

フェデレーションの設定

フェデレーションするにあたって、現状は初回のみ相手のbundleエンドポイントにhttpsで接続するためのCA証明書を設定する必要があります。CA証明書はSPIRE ServerのUpstreamCAの設定によりますが、先程記載したSPIRE Serverの設定例のように事前に発行したCA証明書を設定している場合、設定するCA証明書はそれと同じ証明書となります。このとき、セットするのは相手のCA証明書(Namespace=fooのSPIREに設定するのはNamespace=barのSPIREに設定したCA証明書)であることに注意です。一度接続が成功すると以降は自動でローテーションしてくれるようです。

❯ kubectl exec -it -n foo spire-server-0 sh

# export BUNDLE_FILE=$(mktemp)

# cat <<EOF > $BUNDLE_FILE
-----BEGIN CERTIFICATE-----
...省略...
-----END CERTIFICATE-----
EOF

# /opt/spire/bin/spire-server bundle set --id spiffe://bar.example.com --path $BUNDLE_FILE

無事にフェデレーションが成功すると以下のコマンドでフェデレーション先の証明書バンドルが取得できます。

# /opt/spire/bin/spire-server experimental bundle list
****************************************
* spiffe://bar.example.com
****************************************
{
    "keys": [
        {
            "use": "x509-svid",
            "kty": "EC",
            "crv": "P-256",
            "x": "f1uyyj6...省略...TyBlL6pxwg0k",
            "y": "uJXLJ58...省略...LcyiMOgjP4S4",
            "x5c": [
                "MIIB3j...省略...ojQAtjC8EMUtPg="
            ]
        },
        {
            "use": "jwt-svid",
            "kty": "EC",
            "kid": "quGwcMt...省略...gJFZBaCkU9vL",
            "crv": "P-256",
            "x": "bY1k4N...省略...lsH13JTGzslJe00",
            "y": "TIMoI8...省略...UAqftiisZ6IISAQ"
        }
    ],
    "spiffe_refresh_hint": 15768000
}

SPIRE Serverでは以下のようなログが出力されるようです。

time="2019-12-17T00:29:32Z" level=info msg="Bundle refreshed" subsystem_name=bundle_client trust_domain=bar.example.com
time="2019-12-17T00:29:32Z" level=debug msg="Scheduling next bundle refresh" at="2020-01-31T15:29:32Z" subsystem_name=bundle_client trust_domain=bar.example.com

Namespace=fooとbarの両方でフェデレーションの設定が正しくできたことを確認できたらServerの設定は一旦終わりです。

ここからはフェデレーションによって実際に異なるトラストドメイン間でSVIDの検証が行えることを確認していきます。

Agentのデプロイ

SPIRE AgentのデプロイについてもServerと同様に spire-example を参考にして foo, bar それぞれのNamespaceにデプロイします。Tolerationsも忘れずに設定します。

❯ kubectl get pods -n foo -o wide
NAME                READY   STATUS    RESTARTS   AGE    IP           NODE                      NOMINATED NODE   READINESS GATES
spire-agent-64s95   1/1     Running   0          3m9s   172.17.0.3   spire-federation-worker   <none>           <none>
spire-server-0      1/1     Running   0          67m    10.244.1.3   spire-federation-worker   <none>           <none>

❯ kubectl get pods -n bar -o wide
NAME                READY   STATUS    RESTARTS   AGE     IP           NODE                       NOMINATED NODE   READINESS GATES
spire-agent-xvlbj   1/1     Running   0          12s     172.17.0.2   spire-federation-worker2   <none>           <none>
spire-server-0      1/1     Running   0          5m47s   10.244.2.4   spire-federation-worker2   <none>           <none>

ワークロードのデプロイ

テスト用のワークロードとしてNamespace=fooには簡単なHTTP Serverをデプロイし、Namespace=barのPodからアクセスしてみたいと思います。Namespace=fooにはHTTP ServerのSidecar Proxyとしてghostunnelを利用します。ghostunnelは起動時にSPIREのWorkload APIと通信してPodに割り当てられたSVIDを取得します。Namespace=barのワークローも同様にclientモードのghostunnelをSidecar Proxyにした適当なPodを作成します。

フェデレーションを利用する場合、ワークロード用のRegistration Entryを作成する際のポイントとしては federatesWith でワークロードがフェデレーションを許可するトラストドメインを指定することが必要になります。

Namespace=fooのRegistration Entry登録例

(foo spire)# /opt/spire/bin/spire-server entry create \
        --spiffeID spiffe://foo.example.com/spire-agent \
        --selector k8s_psat:agent_ns:foo \
        --node

(foo spire)# /opt/spire/bin/spire-server entry create \
        --parentID spiffe://foo.example.com/spire-agent \
        --spiffeID spiffe://foo.example.com/http-server \
        --selector k8s:pod-label:app:http-server \
        --dns http-server \
        --dns http-server.foo.svc.cluster.local \
        --federatesWith spiffe://bar.example.com

Namespace=barのRegistration Entry登録例

(bar spire)# /opt/spire/bin/spire-server entry create \
        --spiffeID spiffe://bar.example.com/spire-agent \
        --selector k8s_psat:agent_ns:bar \
        --node

(bar spire)#  /opt/spire/bin/spire-server entry create \
        --parentID spiffe://bar.example.com/spire-agent \
        --spiffeID spiffe://bar.example.com/client \
        --selector k8s:pod-label:app:client \
        --federatesWith spiffe://foo.example.com

Namespace=fooにデプロイするマニフェスト例

apiVersion: apps/v1
kind: Deployment
metadata:
  name: http-server
  namespace: foo
  labels:
    app: http-servrer
spec:
  replicas: 1
  selector:
    matchLabels:
      app: http-server
  template:
    metadata:
      labels:
        app: http-server
    spec:
      tolerations:
      - key: "app"
        operator: "Equal"
        value: "foo"
        effect: "NoSchedule"
      containers:
      - name: ghostunnel
        image: squareup/ghostunnel:v1.5.2
        args:
        - "server"
        - "--listen=0.0.0.0:8443"
        - "--target=127.0.0.1:8080"
        - "--use-workload-api-addr=unix:///run/spire/sockets/agent.sock"
        - "--allow-uri=spiffe://bar.example.com/client"
        ports:
        - containerPort: 8443
        volumeMounts:
        - name: spire-agent-socket
          mountPath: /run/spire/sockets
          readOnly: false
      - name: http-server
        image: python:3.8.0-alpine
        args: ["python", "-m", "http.server", "8080"]
        ports:
        - containerPort: 8080
      volumes:
        - name: spire-agent-socket
          hostPath:
            path: /run/spire/sockets
            type: DirectoryOrCreate
---
apiVersion: v1
kind: Service
metadata:
  name: http-server
  namespace: foo
spec:
  type: NodePort
  ports:
    - name: https
      port: 18443
      targetPort: 8443
      protocol: TCP
  selector:
    app: http-server

Namespace=barにデプロイするマニフェスト例

apiVersion: apps/v1
kind: Deployment
metadata:
  name: client
  namespace: bar
  labels:
    app: client
spec:
  replicas: 1
  selector:
    matchLabels:
      app: client
  template:
    metadata:
      labels:
        app: client
    spec:
      tolerations:
      - key: "app"
        operator: "Equal"
        value: "bar"
        effect: "NoSchedule"
      containers:
      - name: ghostunnel
        image: squareup/ghostunnel:v1.5.2
        args:
        - "client"
        - "--listen=0.0.0.0:8080"
        - "--target=http-server.foo.svc.cluster.local:18443"
        - "--use-workload-api-addr=unix:///run/spire/sockets/agent.sock"
        - "--verify-uri=spiffe://foo.example.com/http-server"
        - "--unsafe-listen"
        volumeMounts:
        - name: spire-agent-socket
          mountPath: /run/spire/sockets
          readOnly: false
      - name: sleep
        image: alpine
        command: ['sh', '-c', 'echo Hello Kubernetes! && sleep 3600']
      volumes:
        - name: spire-agent-socket
          hostPath:
            path: /run/spire/sockets
            type: DirectoryOrCreate

異なるトラストドメイン間での認証

Namespace=barにデプロイしたPodからNamespace=fooのHTTP Serverにアクセスしてみます。今回の構成ではクライアントはghostunnelを経由してアクセスします。ghostunnelはお互いにmTLSで通信し、その際にSPIREから発行されたSVIDを使って認証しています。

Namespace=fooとbarは異なるトラストドメインのため、通常はお互いの証明書を検証できずに通信が失敗します。今回はフェデレーションを有効にしているため、お互いのトラストドメインのCA証明書バンドルを交換済みであり、ghostunnelはWorkload API経由でSVIDを取得する際にフェデレーションが許可されたトラストドメインの証明書バンドルを手にいれることができます。

$ kubectl exec -it -n bar ${POD_NAME} sh

# curl --silent http://localhost:8080/ | head
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Directory listing for /</title>
</head>
<body>
<h1>Directory listing for /</h1>
<hr>
<ul>

フェデレーションしていない場合

念のためフェデレーションしていない場合に通信が失敗することを確認してみます。
先程作成したRegistration Entryを一度削除し、以下のようにフェデレーションの設定をせずに再登録します。
登録された結果に FederatesWith の値が含まれていない点を確認してください。

# /opt/spire/bin/spire-server entry show
Found 2 entries
Entry ID      : cdb445de-e14d-44d0-af7b-b9aecb677331
SPIFFE ID     : spiffe://foo.example.com/http-server
Parent ID     : spiffe://foo.example.com/spire-agent
TTL           : 3600
Selector      : k8s:pod-label:app:http-server
DNS name      : http-server
DNS name      : http-server.foo.svc.cluster.local

Entry ID      : 06f327bd-cdc5-464c-b315-fef71dc5726b
SPIFFE ID     : spiffe://foo.example.com/spire-agent
Parent ID     : spiffe://foo.example.com/spire/server
TTL           : 3600
Selector      : k8s_psat:agent_ns:foo


#  /opt/spire/bin/spire-server entry show
Found 2 entries
Entry ID      : 0b260337-da76-49b3-98bb-1c169a4e4711
SPIFFE ID     : spiffe://bar.example.com/client
Parent ID     : spiffe://bar.example.com/spire-agent
TTL           : 3600
Selector      : k8s:pod-label:app:client

Entry ID      : ec192d5d-b6b5-4d7a-b368-934e4b7773f7
SPIFFE ID     : spiffe://bar.example.com/spire-agent
Parent ID     : spiffe://bar.example.com/spire/server
TTL           : 3600
Selector      : k8s_psat:agent_ns:bar

そのうえで先程と同じワークロードのマニフェストをそれぞれのNamespaceにデプロイし、クライアントから接続してみます。

❯ kubectl exec -it -n bar ${POD_NAME} sh
# curl  http://localhost:8080/
curl: (56) Recv failure: Connection reset by peer

このようにフェデレーションをしていない場合は接続が失敗するようになりました。
Namespace=barのghostunnelでは以下のようにfoo.example.comのCA証明書が見つからない旨のエラーがでています。

❯ kubectl logs -n bar ${POD_NAME} ghostunnel
...省略...
[1] 2019/12/17 05:26:35.997469 error on dial: no roots for peer trust domain "spiffe://foo.example.com"

Namesapce=fooのghostunnelでは証明書エラーのログが出ています。

❯ kubectl logs -n foo ${POD_NAME} ghostunnel
...省略...
[1] 2019/12/17 05:26:41.381518 error on TLS handshake from 10.244.2.15:33568: remote error: tls: bad certificate

まとめ

今回の検証手順はちょっと長くなってしまいしたが、SPIREでフェデレーションする際にはServerの設定でフェデレーションを有効にすることと、Registraion Entryで受け入れるトラストドメインを指定することで、異なるトラストドメインであってもお互いのSVIDを検証することができるようになります。

今年は念願のSPIFFE Meetup Tokyoも開催できたり、SPIFFE/SPIREを少しは知ってもらえる機会を作れたのかなと思っています。Meetup開催に協力して頂いた方、ご参加頂いた方々には感謝しています。来年も引き続き盛り上げていきたいと思いますのでよろしくおねがいします。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした