Kubernetes ServiceAccount トークンの発行と利用方法を解説

## はじめに

Kubernetes では **ServiceAccount** を用いてアプリケーションやユーザーを認証する方法があります。本記事では、ServiceAccount のトークン発行方法やトークンの仕組み、実際に利用するときのコード例などをまとめて紹介します。具体的には:

- `kubectl create token` コマンドを使った発行
- REST API（curl）を使った発行
- Go言語の `client-go` を使った実装例
- トークンの形式や使用例
- 応用的な kubeconfig 生成方法

など、幅広い観点で詳しく解説しています。本記事の内容は主に **kind** 環境を用いて動作検証しましたので、「手元で気軽に試してみたい」という方にも参考になるでしょう。

## この記事で利用した環境

本記事で紹介するコマンドやAPI呼び出しは、以下のバージョンで動作確認を行いました。

- Kubernetes: 1.32.0
- kind: v0.26.0 go1.23.4 darwin/arm64

**備考**: すべて、kindで作成したクラスタ上で動作を確認しています。

## ServiceAccountトークンAPIのOpenAPI仕様

まず、Kubernetesの公式APIドキュメントが定義している **ServiceAccountの `/token` サブリソース** について整理しましょう。これにより、`TokenRequest` オブジェクトを使ってAPI経由でトークンを発行できます。  
以下は、OpenAPI仕様を `kubectl` で取得して `/api/v1/namespaces/{namespace}/serviceaccounts/{name}/token` に関するエンドポイントを確認する例です。

```bash
kubectl get --raw '/openapi/v3/api/v1' | jq '.paths["/api/v1/namespaces/{namespace}/serviceaccounts/{name}/token"]'
```

すると、以下のような JSON が返ってきます。

```json
{
  "post": {
    "tags": [
      "core_v1"
    ],
    "description": "create token of a ServiceAccount",
    "operationId": "createCoreV1NamespacedServiceAccountToken",
    "requestBody": {
      "content": {
        "*/*": {
          "schema": {
            "$ref": "#/components/schemas/io.k8s.api.authentication.v1.TokenRequest"
          }
        }
      },
      "required": true
    },
    "responses": {
      "200": {
        "description": "OK",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/io.k8s.api.authentication.v1.TokenRequest"
            }
          },
          "application/vnd.kubernetes.protobuf": {
            "schema": {
              "$ref": "#/components/schemas/io.k8s.api.authentication.v1.TokenRequest"
            }
          },
          "application/yaml": {
            "schema": {
              "$ref": "#/components/schemas/io.k8s.api.authentication.v1.TokenRequest"
            }
          }
        }
      },
      "201": {
        "description": "Created",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/io.k8s.api.authentication.v1.TokenRequest"
            }
          },
          "application/vnd.kubernetes.protobuf": {
            "schema": {
              "$ref": "#/components/schemas/io.k8s.api.authentication.v1.TokenRequest"
            }
          },
          "application/yaml": {
            "schema": {
              "$ref": "#/components/schemas/io.k8s.api.authentication.v1.TokenRequest"
            }
          }
        }
      },
      "202": {
        "description": "Accepted",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/io.k8s.api.authentication.v1.TokenRequest"
            }
          },
          "application/vnd.kubernetes.protobuf": {
            "schema": {
              "$ref": "#/components/schemas/io.k8s.api.authentication.v1.TokenRequest"
            }
          },
          "application/yaml": {
            "schema": {
              "$ref": "#/components/schemas/io.k8s.api.authentication.v1.TokenRequest"
            }
          }
        }
      },
      "401": {
        "description": "Unauthorized"
      }
    },
    "x-kubernetes-action": "post",
    "x-kubernetes-group-version-kind": {
      "group": "authentication.k8s.io",
      "version": "v1",
      "kind": "TokenRequest"
    }
  },
  "parameters": [
    {
      "name": "dryRun",
      "in": "query",
      "description": "When present, indicates that modifications should not be persisted...",
      "schema": {
        "type": "string",
        "uniqueItems": true
      }
    },
    {
      "name": "fieldManager",
      "in": "query",
      "description": "fieldManager is a name associated with the actor or entity...",
      "schema": {
        "type": "string",
        "uniqueItems": true
      }
    },
    {
      "name": "fieldValidation",
      "in": "query",
      "description": "fieldValidation instructs the server on how to handle objects...",
      "schema": {
        "type": "string",
        "uniqueItems": true
      }
    },
    {
      "name": "name",
      "in": "path",
      "description": "name of the TokenRequest",
      "required": true,
      "schema": {
        "type": "string",
        "uniqueItems": true
      }
    },
    {
      "name": "namespace",
      "in": "path",
      "description": "object name and auth scope...",
      "required": true,
      "schema": {
        "type": "string",
        "uniqueItems": true
      }
    },
    {
      "name": "pretty",
      "in": "query",
      "description": "If 'true', then the output is pretty printed...",
      "schema": {
        "type": "string",
        "uniqueItems": true
      }
    }
  ]
}
```

このOpenAPI仕様を読み解くと、Kubernetes は `/api/v1/namespaces/{namespace}/serviceaccounts/{name}/token` というエンドポイントで `TokenRequest` を受け取り、新しいトークンを発行していることがわかります。

## kubectlを使ったToken取得例

もっとも簡単な取得方法として、`kubectl create token` コマンドを使う例を紹介します。以下のコマンドを実行するだけで、指定した ServiceAccount の新しいトークンが発行されます。

```bash
kubectl create token default --duration=1h
```

ここでは、`namespace=default`、`ServiceAccount=default` に対して、1時間（`--duration=1h`）有効なトークンを要求しています。  
大変シンプルで使いやすいアプローチです。

## curlを使ったToken取得例

`kubectl` 以外にも、**curl** でAPIサーバーに直接リクエストを送り、ServiceAccountトークンを発行することが可能です。  
ただし、APIサーバーへ直接アクセスするためには、すでに何らかの **認証トークン** を所持している必要があります。以下では、

1. ServiceAccount `test-sa` にクラスター管理権限を与える  
2. 取得したトークンを使って別のトークンを発行する

という手順を例として解説します。

### 1. 事前準備

```bash
# ServiceAccountの作成
kubectl create serviceaccount test-sa

# clusterrolebindingの作成 (例: cluster-adminを付与)
kubectl create clusterrolebinding test-sa-binding \
  --clusterrole=cluster-admin \
  --serviceaccount=default:test-sa
```

### 2. curl を使ってトークン取得APIを呼び出す

```bash
# 1時間(3600秒)のトークンを新規発行する例
curl -X POST \
     -H "Content-Type: application/json" \
     -H "Authorization: Bearer $(kubectl create token test-sa)" \
     -d '{"spec":{"expirationSeconds":3600}}' \
     https://127.0.0.1:xxxx/api/v1/namespaces/default/serviceaccounts/default/token --insecure
```

上記コマンドを実行すると、以下の形式の JSON レスポンスが返ってきます。`"status.token"` フィールドに JWT 形式のトークンが格納されていれば発行成功です。

```json
{
  "kind": "TokenRequest",
  "apiVersion": "authentication.k8s.io/v1",
  "metadata": {
    "name": "default",
    "namespace": "default",
    "creationTimestamp": "2025-01-17T05:42:50Z",
    "managedFields": [
      {
        "manager": "curl",
        "operation": "Update",
        "apiVersion": "authentication.k8s.io/v1",
        "time": "2025-01-17T05:42:50Z",
        "fieldsType": "FieldsV1",
        "fieldsV1": {
          "f:spec": {
            "f:expirationSeconds": {}
          }
        },
        "subresource": "token"
      }
    ]
  },
  "spec": {
    "audiences": [
      "https://kubernetes.default.svc.cluster.local"
    ],
    "expirationSeconds": 3600,
    "boundObjectRef": null
  },
  "status": {
    "token": "xxx.yyy.zzz",
    "expirationTimestamp": "2025-01-17T06:42:50Z"
  }
}
```

## Credential plugins

Kubernetesの公式クライアント（`k8s.io/client-go`）と、それを使用する `kubectl` や `kubelet` などのツールは、**外部コマンドを実行してユーザーの認証情報を取得**する仕組みをサポートしています。  
これを **Credential plugins** と呼びます。LDAP や OAuth2、SAML など、`client-go` がネイティブにサポートしていない認証方式でも外部コマンドを組み合わせることで連携が可能です。

[認証 | Kubernetes](https://kubernetes.io/ja/docs/reference/access-authn-authz/authentication/#%E8%A8%AD%E5%AE%9A)

### kubeconfigの例

Credential plugins を活用すると、`kubeconfig` の `users` セクションに「どのコマンドを呼ぶか」「どの引数を渡すか」などを記述できます。以下はEKS(Elastic Kubernetes Service)でよく使われる形式の例です。

```yaml
apiVersion: v1
clusters:
- cluster:
    certificate-authority-data: xxxx
    server: https://xxxx.gr7.ap-northeast-1.eks.amazonaws.com
  name: arn:aws:eks:ap-northeast-1:xxxx:cluster/some-eks-cluster-name
contexts:
- context:
    cluster: arn:aws:eks:ap-northeast-1:xxxx:cluster/some-eks-cluster-name
    user: arn:aws:eks:ap-northeast-1:xxxx:cluster/some-eks-cluster-name
  name: arn:aws:eks:ap-northeast-1:xxxx:cluster/some-eks-cluster-name
current-context: arn:aws:eks:ap-northeast-1:xxxx:cluster/some-eks-cluster-name
kind: Config
preferences: {}
users:
- name: arn:aws:eks:ap-northeast-1:xxxx:cluster/some-eks-cluster-name
  user:
    exec:
      apiVersion: client.authentication.k8s.io/v1beta1
      args:
      - --region
      - ap-northeast-1
      - eks
      - get-token
      - --cluster-name
      - some-eks-cluster-name
      - --output
      - json
      command: aws
      env:
      - name: AWS_PROFILE
        value: xxxx
      interactiveMode: IfAvailable
      provideClusterInfo: false
```

EKSを例にすると、以下コマンドの出力が `exec` によって呼び出され、認証用トークンとして利用されます。

```bash
$ AWS_PROFILE=xxxx aws eks get-token --cluster-name some-eks-cluster-name --region ap-northeast-1 --output json
{
    "kind": "ExecCredential",
    "apiVersion": "client.authentication.k8s.io/v1beta1",
    "spec": {},
    "status": {
        "expirationTimestamp": "2025-01-24T06:01:18Z",
        "token": "k8s-aws-v1.xxxx"
    }
}
```

`token` フィールドに `k8s-aws-v1.xxxx` という値が入り、この文字列が認証に使われます。実際には、この `k8s-aws-v1.xxxx` を base64 デコードすると以下のような **署名付きURL** が埋め込まれています。

```
https://sts.amazonaws.com/?Action=GetCallerIdentity&Version=2011-06-15&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=XXXXJPFRILKNSRC2W5QA%2F20200219%2Fus-xxxx-1%2Fsts%2Faws4_request&X-Amz-Date=20200219T155427Z&X-Amz-Expires=60&X-Amz-SignedHeaders=host%3Bx-k8s-aws-id&X-Amz-Signature=XXXf8f3285e320ddb5e683a5c9a405301ad76546f24f28111fdad09cf648a393
```

詳細は以下のドキュメントを参照してください。  
[Identity and Access Management - Amazon EKS](https://docs.aws.amazon.com/ja_jp/eks/latest/best-practices/identity-and-access-management.html#_controlling_access_to_eks_clusters)

## GoコードによるTokenRequest作成例

次に、Go 言語の Kubernetes クライアントライブラリである `client-go` を使ってトークンを発行する方法を見てみましょう。内部的には先ほどのエンドポイント `/api/v1/namespaces/{namespace}/serviceaccounts/{name}/token` に `POST` する形になっています。

```go
// CreateToken takes the representation of a tokenRequest and creates it.  
// Returns the server's representation of the tokenRequest, and an error, if there is any.
func (c *serviceAccounts) CreateToken(ctx context.Context, serviceAccountName string, tokenRequest *authenticationv1.TokenRequest, opts metav1.CreateOptions) (result *authenticationv1.TokenRequest, err error) {
	result = &authenticationv1.TokenRequest{}
	err = c.GetClient().Post().
		UseProtobufAsDefault().
		Namespace(c.GetNamespace()).
		Resource("serviceaccounts").
		Name(serviceAccountName).
		SubResource("token").
		VersionedParams(&opts, scheme.ParameterCodec).
		Body(tokenRequest).
		Do(ctx).
		Into(result)
	return
}
```

このメソッドを呼び出すことで簡単に新しいトークンを取得できます。

## `kubernetes.io/service-account-token` のSecret

Kubernetes には、`kubernetes.io/service-account-token` タイプの Secret リソースを通じて ServiceAccount トークンを管理する仕組みも存在します。マニフェストの例を示すと、以下のようになります。

```yaml
apiVersion: v1
kind: Secret
metadata:
  annotations:
    kubernetes.io/service-account.name: test-sa
  name: test-sa-token
type: kubernetes.io/service-account-token
```

この Secret が作成されると、`data` フィールドに ServiceAccount トークンやクラスタの CA 証明書等が格納されます。

```yaml
apiVersion: v1
data:
  ca.crt: xxx
  namespace: xxx
  token: xxx
kind: Secret
metadata:
  annotations:
    kubernetes.io/service-account.name: test-sa
  name: test-sa-token
  namespace: default
type: kubernetes.io/service-account-token
```

この方式で発行されるトークンは **期限がなく**、Secretを削除すれば即時に無効化できます。

## Tokenの形式

ここでは、同じ`ServiceAccount トークン`でも、どのように発行するかによって JWT の中身が異なる点に着目します。  
内部クレーム（Claims）を確認すると、**TokenRequest API** を使った場合と `kubernetes.io/service-account-token` タイプの Secret で発行されたトークンでは、構造が異なることがわかります。

### `/api/v1/namespaces/{namespace}/serviceaccounts/{name}/token` から取得したtoken

以下のように、`kubernetes.io` 以下に構造化された情報を持ち、`exp`(有効期限) が設定されています。

```
Token header
------------
{
  "alg": "RS256",
  "kid": "2UZIz_6ZkOi7DiwG7ILvSNXYX8RUzWyCcXKgc-x8Fd0"
}

Token claims
------------
{
  "aud": [
    "https://kubernetes.default.svc.cluster.local"
  ],
  "exp": 1737683684,
  "iat": 1737680084,
  "iss": "https://kubernetes.default.svc.cluster.local",
  "jti": "1f035edf-429e-41fc-bb74-af89b3991485",
  "kubernetes.io": {
    "namespace": "projectsveltos",
    "serviceaccount": {
      "name": "projectsveltos",
      "uid": "e049717e-92f3-4c1f-8147-ec8ae120bf66"
    }
  },
  "nbf": 1737680084,
  "sub": "system:serviceaccount:projectsveltos:projectsveltos"
}
```

### `kubernetes.io/service-account-token` type の Secret の `.data.token` にあるtoken

一方、こちらは `kubernetes.io/serviceaccount/namespace` のようにフラットなキーで格納されており、`exp` が存在しません。

```
Token header
------------
{
  "alg": "RS256",
  "kid": "2UZIz_6ZkOi7DiwG7ILvSNXYX8RUzWyCcXKgc-x8Fd0"
}

Token claims
------------
{
  "iss": "kubernetes/serviceaccount",
  "kubernetes.io/serviceaccount/namespace": "projectsveltos",
  "kubernetes.io/serviceaccount/secret.name": "projectsveltos",
  "kubernetes.io/serviceaccount/service-account.name": "projectsveltos",
  "kubernetes.io/serviceaccount/service-account.uid": "e049717e-92f3-4c1f-8147-ec8ae120bf66",
  "sub": "system:serviceaccount:projectsveltos:projectsveltos"
}
```

`exp` が無い = 有効期限が無期限なので、運用上の管理に注意が必要です。不要になったらSecretを削除すれば使えなくなる点が特徴です。

### 補足: ServiceAccount 再作成とトークンの破棄

Kubernetesでは、同じNamespaceかつ同じ名前のServiceAccountを削除して再作成すると、`service-account.uid` が変わります。  
したがって、**発行済みトークンを無効化したい場合** は、ServiceAccountを削除して作り直すだけで該当トークンが利用不能になります。

### 補足: Webhook認証

上記でも例として出しましたが、Kubernetesでは[Webhookトークン認証](https://kubernetes.io/ja/docs/reference/access-authn-authz/authentication/#webhook-token-authentication)をサポートしている為、jwt形式ではないtokenが渡されたりする可能性があります.

```
k8s-aws-v1.xxxx
```

## Tokenの有効性の確認方法

ServiceAccount トークンを検証する場合、よく使われるのが **Token Review API** です。  
認証自体をアプリケーションで実装するのではなく、Kubernetesにトークンの検証を任せられるため、アプリケーションの開発・保守がシンプルになります。

- 参考: [KubernetesのToken Review API #kubernetes - Qiita](https://qiita.com/hiyosi/items/b6a1d07b30ff7dff1961)

もしくは、Token Review APIを利用せずに、そのtokenを利用して実際にk8sのapiを叩く様な場合もあります。

## tokenの使い道

ServiceAccount トークンは **Kubernetesクラスタへのアクセス** に限らず、外部アプリケーションの認証や認可の仕組みにも活用できます。  
ここでは具体的なユースケースとして、いくつかのアプリケーション例を挙げます。

### k8sを認証基盤としてアプリに組み込む

#### 1. Token Review API を利用したWebアプリ認証

ユーザーが UI で ServiceAccountトークンを入力し、Webアプリ側で [Token Review API](https://qiita.com/hiyosi/items/b6a1d07b30ff7dff1961) を呼び出してトークンの有効性やユーザー情報を確認する、といったパターンです。  
Kubernetesクラスターが`IDプロバイダ`のように振る舞うことができます。

#### 2. Kubernetes公式Dashboard

- GitHub: [kubernetes/dashboard](https://github.com/kubernetes/dashboard)

![kubernetes dashboard UI](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/141711/66d7b1c3-7064-90a4-bd89-81a200ab5a67.png)

Dashboardのログイン画面でServiceAccountトークンを直接貼り付ける運用は、最も分かりやすい例の一つです。

#### 3. Argo Workflows

- 公式サイト: [Argo Workflows](https://argoproj.github.io/workflows/)

![Argo Workflows UI](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/141711/a4bd9dda-9454-7de2-0a5f-9d5111b6f82c.png)

Argo WorkflowsのUIでもServiceAccountトークンを認証に利用できます。

#### 4. Sveltos Dashboard

- 公式サイト: [Sveltos Dashboard](https://projectsveltos.github.io/sveltos/getting_started/install/dashboard/)

![Sveltos UI](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/141711/e3e86fa4-9224-4cdb-aaad-c56e076890fe.png)

Sveltos DashboardもServiceAccountトークンを利用した認証をサポートしています。

## Podの中で起動しているプログラムから環境の情報を元にtokenを使ってkubeconfigを組み立てる方法

通常は、Kubernetes上で動作するツールやライブラリ（例: `k8s.io/client-go`）が自動的に設定を読み込んでくれるため、自分で kubeconfig を組み立てる必要はあまりありません。しかし、  
**「Pod 内で動的に kubeconfig を生成し、複数のクラスタへアクセスしたい」**  
というニーズがある場合は、以下のような手順で kubeconfig を生成できます。

### 最小限の構成例 (TypeScriptっぽい記述例)

```ts
const template = `apiVersion: v1
kind: Config
clusters:
- name: this
  cluster:
    server: ${process.env.KUBERNETES_PORT_443_TCP_ADDR}
    certificate-authority-data: "${readFileSync('/var/run/secrets/kubernetes.io/serviceaccount/ca.crt', 'utf8')}"
users:
- name: this
  user:
    token: ${readFileSync('/var/run/secrets/kubernetes.io/serviceaccount/token', 'utf8')}
contexts:
- name: this
  context:
    cluster: this
    user: this
current-context: this`;
```

- `server` : Pod 内からみた Kubernetes APIサーバーアドレス (環境変数 `KUBERNETES_PORT_443_TCP_ADDR` など)
- `certificate-authority-data`: `/var/run/secrets/kubernetes.io/serviceaccount/ca.crt` を base64エンコード or 文字列そのまま埋め込み
- `token`: `/var/run/secrets/kubernetes.io/serviceaccount/token` の中身

Goでの実装例は、以下のリポジトリなどで確認できます。  
[projectsveltos/sveltoscluster-manager - sveltoscluster_controller.go#L398-L424](https://github.com/projectsveltos/sveltoscluster-manager/blob/0338afc5d15b2ee20a6ea1b45ac73bfb31096d7b/controllers/sveltoscluster_controller.go#L398-L424)

### Pod内の環境変数

Pod 内では、デフォルトで次のような環境変数が設定されます。

```bash
$ env | grep KUBE
KUBERNETES_SERVICE_PORT=443
KUBERNETES_PORT=tcp://10.115.0.1:443
KUBERNETES_PORT_443_TCP_ADDR=10.115.0.1
KUBERNETES_PORT_443_TCP_PORT=443
KUBERNETES_PORT_443_TCP_PROTO=tcp
KUBERNETES_PORT_443_TCP=tcp://10.115.0.1:443
KUBERNETES_SERVICE_PORT_HTTPS=443
KUBERNETES_SERVICE_HOST=10.115.0.1
```

`KUBERNETES_PORT_443_TCP_ADDR` や `KUBERNETES_SERVICE_HOST` を参照すれば、APIサーバーのURLを取得できます。

## kubeconfigを用いてcontroller-runtimeを用意する方法

最後に、マルチクラスタ対応のコントローラを開発したい場合に役立つ例として、**`controller-runtime` のクライアントを動的に構築する方法** を紹介します。  
kubeconfigを複数用意することで、複数のKubernetesクラスタを切り替えて操作できます。例えば以下のリポジトリでは、kubeconfig を読み込んで `controller-runtime` のマネージドクライアントを作成し、クラスタをまたいだリソース管理を行っています。

[projectsveltos/sveltoscluster-manager - sveltoscluster_controller.go#L156-L168](https://github.com/projectsveltos/sveltoscluster-manager/blob/0338afc5d15b2ee20a6ea1b45ac73bfb31096d7b/controllers/sveltoscluster_controller.go#L156-L168)