セルフホステッドランナーでvault-actionを試す

Github ActionsでCI/CDする際に必要なシークレットを
全部リポジトリごとにGithub上で管理するのはなかなか大変ですし、
外部に機密情報を保存するのはなるべく避けたいものです。

というわけでオンプレに構築したvaultから、
同じくオンプレで起動するセルフホステッドランナーで
vault-action を使いシークレット情報を取得したいと思います。

環境

色々いじってたら壊れたのでRancherをアップグレードして、
ダウンストリートクラスターを再構築しています。
■ Rancher Cluster

root@rancher:~# kubectl get node -o wide
NAME      STATUS   ROLES                       AGE    VERSION        INTERNAL-IP     EXTERNAL-IP   OS-IMAGE           KERNEL-VERSION     CONTAINER-RUNTIME
rancher   Ready    control-plane,etcd,master   338d   v1.32.5+k3s1   192.168.0.208   <none>        Ubuntu 24.04 LTS   6.8.0-51-generic   containerd://2.0.5-k3s1.32

■ Downstream Cluster
このクラスタはRaspberry Piで構築しているのでarm64環境です。

$ k get node -o wide
NAME   STATUS   ROLES                       AGE   VERSION          INTERNAL-IP    EXTERNAL-IP   OS-IMAGE             KERNEL-VERSION     CONTAINER-RUNTIME
k8s1   Ready    control-plane,etcd,master   33h   v1.32.5+rke2r1   192.168.0.51   <none>        Ubuntu 24.04.2 LTS   6.8.0-1028-raspi   containerd://2.0.5-k3s1
k8s2   Ready    worker                      33h   v1.32.5+rke2r1   192.168.0.52   <none>        Ubuntu 24.04.2 LTS   6.8.0-1018-raspi   containerd://2.0.5-k3s1
k8s3   Ready    worker                      33h   v1.32.5+rke2r1   192.168.0.53   <none>        Ubuntu 24.04.2 LTS   6.8.0-1018-raspi   containerd://2.0.5-k3s1

Downstream Clusterにvault serverとActions Runner Controllerをインストールしています。
それぞれの構築手順は以下をみてください。
ラズパイk8sにVault serverを構築
ラズパイk8sにself-hosted runnersをActions Runner Controllerでインストール

ubuntu@k8s1:~$ k get pod,ingress -n vault
NAME          READY   STATUS    RESTARTS   AGE
pod/vault-0   1/1     Running   0          33h
pod/vault-1   1/1     Running   0          33h

NAME                              CLASS    HOSTS                       ADDRESS                     PORTS   AGE
ingress.networking.k8s.io/vault   <none>   vault.tsuchinokometal.com   192.168.0.52,192.168.0.53   80      33h

ubuntu@k8s1:~$ k get pod -n arc-systems
NAME                                     READY   STATUS    RESTARTS   AGE
arc-gha-rs-controller-78b5f4b45b-62rhb   1/1     Running   0          31h
arc-runner-set-754b578d-listener         1/1     Running   0          31h

ちなみにバージョンは以下の通り。

ubuntu@k8s1:~$ vault status | grep Version
Version                 1.19.0
ubuntu@k8s1:~$ helm list -n arc-systems
NAME	NAMESPACE  	REVISION	UPDATED                                	STATUS  	CHART                                 	APP VERSION
arc 	arc-systems	1       	2025-06-15 13:53:17.434598985 +0900 JST	deployed	gha-runner-scale-set-controller-0.12.0	0.12.0     
ubuntu@k8s1:~$ helm list -n arc-runners
NAME          	NAMESPACE  	REVISION	UPDATED                                	STATUS  	CHART                      	APP VERSION
arc-runner-set	arc-runners	1       	2025-06-15 13:55:31.506930496 +0900 JST	deployed	gha-runner-scale-set-0.12.0	0.12.0 

tokenを使ってシークレットを取得する

簡単な方法としては vault token ですかね

試しに以下のワークフローを作成しました。
動きとしてはkubeconfigファイルの内容をvaultに登録して、
vault-actionで取得しそれを使って別クラスタである
rancher clusterに対してkubectlを実行するというものです。

なぜわざわざこんなことをしているかというと、
取得したシークレットは全部自動でマスクされるので単純にechoで確認できないからです!

name: kubectl demo
on:
  workflow_dispatch:

defaults:
  run:
    shell: bash

jobs:
  kubectl-demo:
    runs-on: arc-runner-set
    env:
      KUBE_VERSION: "1.32.5"
    steps:
      - name: Import Secrets
        id: import-secrets
        uses: hashicorp/vault-action@v3.4.0
        with:
          token: ${{ secrets.VAULT_TOKEN }}
          url: http://vault.tsuchinokometal.com
          exportEnv: false
          secrets: arc-runners/data/kubeconfig/rancher-cluster config | KUBECONFIG_B64

      - name: Setup kubectl
        run: |
          curl -LO https://dl.k8s.io/release/v${{ env.KUBE_VERSION }}/bin/linux/arm64/kubectl
          chmod +x ./kubectl
          mkdir -p ~/.local/bin
          mv ./kubectl ~/.local/bin/kubectl
          echo "PATH=$PATH:~/.local/bin" >> $GITHUB_ENV

      - name: Setup kubeconfig
        run: |
          echo "${{ steps.import-secrets.outputs.KUBECONFIG_B64 }}" | base64 -d > /tmp/kubeconfig
          echo "KUBECONFIG=/tmp/kubeconfig" >> $GITHUB_ENV
          
      - name: Get node
        run: |
          kubectl get node -o wide

解説します。

事前にRepository secretsでVAULT_TOKEN作成し、
token: ${{ secrets.VAULT_TOKEN }} で読み込んでいます。
ひとまずテストとしてvault operator init実行時に生成されるルートトークンを使いました。
なんでもできちゃうやつですね。

vaultへのシークレット登録は以下のような感じです。
kubeconfig-rancher-cluster.yamlがkubeconfigファイルです。
base64でエンコードしないとうまくいかなかったです。

ubuntu@k8s1:~$ vault secrets enable -path=arc-runners -description="github app for arc" kv-v2
Success! Enabled the kv-v2 secrets engine at: arc-runners/
ubuntu@k8s1:~$ cat kubeconfig-rancher-cluster.yaml | base64 | vault kv put -mount=arc-runners kubeconfig/rancher-cluster config=-
=============== Secret Path ===============
arc-runners/data/kubeconfig/rancher-cluster

======= Metadata =======
Key                Value
---                -----
created_time       2025-06-16T12:28:30.566613147Z
custom_metadata    <nil>
deletion_time      n/a
destroyed          false
version            1

このdataを含んだSecret Pathで指定します。

Setup kubeconfigのstepで取得したシークレットをファイルに出力しています。
KUBECONFIGという環境変数で指定すればkubectlで読み込んでくれるのでそうしている感じです。
outputsで読み込んでいますが、デフォルトだと環境変数でも設定してくれます。
ただそうするとマスクされたenvが複数行ログに残るのがちょっと嫌なので
今回はexportEnv: falseにしています。

Setup kubectlのstepでkubectlをインストールしています。
Azure/setup-kubectl がarm64環境だとうまく動かなかったので、
まんま こちら の手順をやっているだけです。

実行結果が以下です。
無事kubectlが実行できていますね。

vault-action_01.png

kubrernetes認証を使ってシークレットを取得する

外部からアクセスできないとはいえ、ルートトークンを外部に登録するのは嫌ですね。
今回構築したrunnerはkubernetesクラスタで起動するので kubernetes認証 を試してみます。

今回はvault側の設定は以下のようにしました。
こちら とかも参考になると思います。

ubuntu@k8s1:~$ vault auth enable --path=my-cluster/arc-runners kubernetes
Success! Enabled kubernetes auth method at: my-cluster/arc-runners/

ubuntu@k8s1:~$ vault write auth/my-cluster/arc-runners/config kubernetes_host="https://kubernetes.default.svc.cluster.local:443"
Success! Data written to: auth/my-cluster/arc-runners/config

ubuntu@k8s1:~$ vault policy write arc-runners-policy - <<EOF
path "arc-runners/data/kubeconfig/*" {
  capabilities = ["read"]
}
EOF

ubuntu@k8s1:~$ vault write auth/my-cluster/arc-runners/role/actions bound_service_account_names=arc-runner-set-gha-rs-no-permission bound_service_account_namespaces=arc-runners policies=arc-runners-policy ttl=20m
Success! Data written to: auth/my-cluster/arc-runners/role/actions

kubernetes_hostはrunnerとvaultが同じクラスタにいるのでclusterIPを指定しています。
roleはactionsという名前にしてサービスアカウントは
runner scale setをhelmでインストールするとデフォルトで
このサービスアカウントで起動していたのでそれを指定しています。

Actionsワークフローは以下のようになります。

name: kubectl demo
on:
  workflow_dispatch:

defaults:
  run:
    shell: bash

jobs:
  kubectl-demo:
    runs-on: arc-runner-set
    env:
      KUBE_VERSION: "1.32.5"
    steps:
      - name: Import Secrets
        id: import-secrets
        uses: hashicorp/vault-action@v3.4.0
        with:
          method: kubernetes
          url: http://vault.tsuchinokometal.com
          path: my-cluster/arc-runners
          role: actions
          exportEnv: false
          secrets: arc-runners/data/kubeconfig/rancher-cluster config | KUBECONFIG_B64

      - name: Setup kubectl
        run: |
          curl -LO https://dl.k8s.io/release/v${{ env.KUBE_VERSION }}/bin/linux/arm64/kubectl
          chmod +x ./kubectl
          mkdir -p ~/.local/bin
          mv ./kubectl ~/.local/bin/kubectl
          echo "PATH=$PATH:~/.local/bin" >> $GITHUB_ENV

      - name: Setup kubeconfig
        run: |
          echo "${{ steps.import-secrets.outputs.KUBECONFIG_B64 }}" | base64 -d > /tmp/kubeconfig
          echo "KUBECONFIG=/tmp/kubeconfig" >> $GITHUB_ENV
          
      - name: Get node
        run: |
          kubectl get node -o wide

methodをkubernetesにして、
pathとroleをvault側の設定に合わせています。

うまくいけばtokenの時と同じ結果になるはずです。

JWT with OIDC Providerを使ってシークレットを取得する

OIDC も試してみました。
こちらのドキュメント を参考に進めます。
またこちら も参考にさせていただきました。

ubuntu@k8s1:~$ vault auth enable jwt
Success! Enabled jwt auth method at: jwt/

ubuntu@k8s1:~$ vault write auth/jwt/config \
  bound_issuer="https://token.actions.githubusercontent.com" \
  oidc_discovery_url="https://token.actions.githubusercontent.com"
Success! Data written to: auth/jwt/config

vault write auth/jwt/role/actions -<<EOF
{
  "role_type": "jwt",
  "user_claim": "actor",
  "bound_audiences": "https://github.com/<username>",
  "bound_claims": {
    "repository": "<username>/<repo_name>"
  },
  "policies": ["arc-runners-policy"],
  "ttl": "10m"
}
EOF

<username>と<repo_name>は自身の環境に書き換えてください。
policyはkubernetes認証で作成したものを利用しています。

以下のエラーが出て悩んだのですが、bound_audiencesを加えることで解消しました。
個人アカウントで試しているのでこうしていますが、
組織ですと"https://github.com/<org_name>“になるんじゃないかと思います。

failed to retrieve vault token. code: ERR_NON_2XX_3XX_RESPONSE, message: Response code 400 (Bad Request), vaultResponse: {"errors":["audience claim found in JWT but no audiences bound to the role"]}

Actionsワークフローは以下のようになります。

name: kubectl demo
on:
  workflow_dispatch:

defaults:
  run:
    shell: bash

jobs:
  kubectl-demo:
    permissions:
      id-token: write
      contents: read
    runs-on: arc-runner-set
    env:
      KUBE_VERSION: "1.32.5"
    steps:
      - name: Import Secrets
        id: import-secrets
        uses: hashicorp/vault-action@v3.4.0
        with:
          method: jwt
          url: http://vault.tsuchinokometal.com
          role: actions
          exportEnv: false
          secrets: arc-runners/data/kubeconfig/rancher-cluster config | KUBECONFIG_B64

      - name: Setup kubectl
        run: |
          curl -LO https://dl.k8s.io/release/v${{ env.KUBE_VERSION }}/bin/linux/arm64/kubectl
          chmod +x ./kubectl
          mkdir -p ~/.local/bin
          mv ./kubectl ~/.local/bin/kubectl
          echo "PATH=$PATH:~/.local/bin" >> $GITHUB_ENV

      - name: Setup kubeconfig
        run: |
          echo "${{ steps.import-secrets.outputs.KUBECONFIG_B64 }}" | base64 -d > /tmp/kubeconfig
          echo "KUBECONFIG=/tmp/kubeconfig" >> $GITHUB_ENV
          
      - name: Get node
        run: |
          kubectl get node -o wide

ドキュメントにも書いてありますが、permissionsを忘れないようにしてください。
うまくいけばtokenの時と同じ結果になるはずです。

いやーセルフホステッドランナーだと
料金を気にせずトライアンドエラーできるので良いですねー