From b95289d5b0ec32e85a715d8751b79657266e7190 Mon Sep 17 00:00:00 2001 From: panxiao81 Date: Mon, 21 Sep 2026 03:28:20 +0000 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=20SPIFFE=20OpenBao=20?= =?UTF-8?q?=E4=B8=8E=20Nexus=20Actions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LICENSE | 21 ++++++++++++++++ README.md | 33 ++++++++++++++++++++++++- setup-nexus/action.yml | 40 +++++++++++++++++++++++++++++++ spiffe-openbao-login/action.yml | 16 +++++++++++++ spiffe-openbao-login/dist/main.js | 37 ++++++++++++++++++++++++++++ spiffe-openbao-login/dist/post.js | 12 ++++++++++ 6 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 LICENSE create mode 100644 setup-nexus/action.yml create mode 100644 spiffe-openbao-login/action.yml create mode 100644 spiffe-openbao-login/dist/main.js create mode 100644 spiffe-openbao-login/dist/post.js diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6475504 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 panxiao81 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 9f21e18..bd48b8d 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,34 @@ # ci-actions -Gitea Actions:SPIFFE/OpenBao 登录与 Nexus CI 配置 \ No newline at end of file +homelab Gitea Actions 的可复用安全构件。目前提供: + +- `spiffe-openbao-login`:用 runner 的 JWT-SVID 换取短期 OpenBao token,并在 job 的 + post 阶段主动吊销。 +- `setup-nexus`:为匿名读取配置 Ansible Galaxy、Go Modules 与 OCI repository 地址。 + +## Runner 前提 + +`spiffe-openbao-login` 不下载工具;runner image 必须包含 Node.js 20、`spire-agent`,并挂载 +可访问的 SPIFFE Workload API socket。身份注册、OpenBao role 与 policy 仍由基础设施 IaC +管理,Action 不创建或扩大授权。 + +```yaml +- uses: panxiao81/ci-actions/spiffe-openbao-login@v1 + with: + role: ci-example + +- uses: panxiao81/ci-actions/setup-nexus@v1 + with: + ansible: 'true' + go: 'true' + oci: 'true' +``` + +登录 Action 将 `BAO_ADDR`、`BAO_TOKEN` 写入 runner 的 `GITHUB_ENV`,并通过 +`GITHUB_STATE` 将 token 交给 post action。它们属于 runner 管理的 job 临时文件;因此只允许 +在一次性 Pod/VM runner 使用,不得在共享常驻 runner 或会持久化工作目录的 runner 上使用。 +token 不作为 Action output,也不上传 artifact/cache。 + +`setup-nexus` 只配置匿名读取,不读取 OpenBao secret。正式发布账号建立后,应新增独立的 +Nexus login Action,并绑定精确 SPIFFE ID 与最小 OpenBao policy;不得让本 Action 读取 +管理员凭据。 diff --git a/setup-nexus/action.yml b/setup-nexus/action.yml new file mode 100644 index 0000000..397cd72 --- /dev/null +++ b/setup-nexus/action.yml @@ -0,0 +1,40 @@ +name: Setup Nexus +description: Configure anonymous Nexus endpoints for CI dependency downloads +inputs: + nexus-url: + default: https://nexus.ad.ddupan.top + ansible: + default: 'false' + go: + default: 'false' + oci: + default: 'false' +outputs: + oci-public: + value: ${{ steps.configure.outputs.oci-public }} + oci-hosted: + value: ${{ steps.configure.outputs.oci-hosted }} +runs: + using: composite + steps: + - id: configure + shell: bash + env: + NEXUS_URL: ${{ inputs.nexus-url }} + ENABLE_ANSIBLE: ${{ inputs.ansible }} + ENABLE_GO: ${{ inputs.go }} + ENABLE_OCI: ${{ inputs.oci }} + run: | + set -euo pipefail + nexus_url="${NEXUS_URL%/}" + if [[ "$ENABLE_ANSIBLE" == true ]]; then + printf '%s\n' 'ANSIBLE_GALAXY_SERVER_LIST=nexus' \ + "ANSIBLE_GALAXY_SERVER_NEXUS_URL=$nexus_url/repository/ansible-public/" >> "$GITHUB_ENV" + fi + if [[ "$ENABLE_GO" == true ]]; then + printf 'GOPROXY=%s/repository/go-public/\n' "$nexus_url" >> "$GITHUB_ENV" + fi + if [[ "$ENABLE_OCI" == true ]]; then + printf 'oci-public=%s/oci-public\noci-hosted=%s/oci-hosted\n' \ + "$nexus_url" "$nexus_url" >> "$GITHUB_OUTPUT" + fi diff --git a/spiffe-openbao-login/action.yml b/spiffe-openbao-login/action.yml new file mode 100644 index 0000000..adf65bd --- /dev/null +++ b/spiffe-openbao-login/action.yml @@ -0,0 +1,16 @@ +name: SPIFFE OpenBao Login +description: Exchange a JWT-SVID for a short-lived OpenBao token and revoke it in post +inputs: + bao-address: + default: https://bao.ad.ddupan.top:8200 + role: + description: Exact OpenBao jwt-spire role + required: true + audience: + default: openbao + socket-path: + default: /run/spire/agent-sockets/spire-agent.sock +runs: + using: node20 + main: dist/main.js + post: dist/post.js diff --git a/spiffe-openbao-login/dist/main.js b/spiffe-openbao-login/dist/main.js new file mode 100644 index 0000000..7f2b085 --- /dev/null +++ b/spiffe-openbao-login/dist/main.js @@ -0,0 +1,37 @@ +'use strict'; +const fs = require('node:fs'); +const crypto = require('node:crypto'); +const { spawnSync } = require('node:child_process'); + +const input = (name, fallback = '') => process.env[`INPUT_${name.toUpperCase()}`] || fallback; +function appendCommand(file, name, value) { + if (!file) throw new Error(`${name} command file is unavailable`); + const delimiter = `ci_actions_${crypto.randomUUID()}`; + fs.appendFileSync(file, `${name}<<${delimiter}\n${value}\n${delimiter}\n`, { mode: 0o600 }); +} + +async function main() { + const address = input('BAO-ADDRESS', 'https://bao.ad.ddupan.top:8200').replace(/\/$/, ''); + const role = input('ROLE'); + const audience = input('AUDIENCE', 'openbao'); + const socket = input('SOCKET-PATH', '/run/spire/agent-sockets/spire-agent.sock').replace(/^unix:\/\//, ''); + if (!role) throw new Error('input role is required'); + const result = spawnSync('spire-agent', ['api', 'fetch', 'jwt', '-audience', audience, + '-socketPath', socket, '-output', 'json'], { encoding: 'utf8', maxBuffer: 16777216 }); + if (result.status !== 0) throw new Error(`spire-agent JWT fetch failed: ${result.stderr.trim()}`); + const jwt = JSON.parse(result.stdout)?.[0]?.svids?.[0]?.svid; + if (!jwt) throw new Error('spire-agent returned no JWT-SVID'); + const response = await fetch(`${address}/v1/auth/jwt-spire/login`, { + method: 'POST', headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ role, jwt }), signal: AbortSignal.timeout(15000), + }); + if (!response.ok) throw new Error(`OpenBao login failed (${response.status}): ${(await response.text()).slice(0, 1000)}`); + const token = (await response.json())?.auth?.client_token; + if (!token) throw new Error('OpenBao response contained no client token'); + appendCommand(process.env.GITHUB_ENV, 'BAO_ADDR', address); + appendCommand(process.env.GITHUB_ENV, 'BAO_TOKEN', token); + appendCommand(process.env.GITHUB_STATE, 'bao_address', address); + appendCommand(process.env.GITHUB_STATE, 'bao_token', token); + process.stdout.write('SPIFFE JWT-SVID exchanged for a short-lived OpenBao token\n'); +} +main().catch((error) => { process.stderr.write(`spiffe-openbao-login: ${error.message}\n`); process.exitCode = 1; }); diff --git a/spiffe-openbao-login/dist/post.js b/spiffe-openbao-login/dist/post.js new file mode 100644 index 0000000..1f484f1 --- /dev/null +++ b/spiffe-openbao-login/dist/post.js @@ -0,0 +1,12 @@ +'use strict'; +async function post() { + const address = (process.env.STATE_bao_address || '').replace(/\/$/, ''); + const token = process.env.STATE_bao_token || ''; + if (!address || !token) return process.stdout.write('No OpenBao token state found; nothing to revoke\n'); + const response = await fetch(`${address}/v1/auth/token/revoke-self`, { + method: 'POST', headers: { 'x-vault-token': token }, signal: AbortSignal.timeout(15000), + }); + if (!response.ok) throw new Error(`OpenBao revoke-self failed (${response.status})`); + process.stdout.write('OpenBao token revoked\n'); +} +post().catch((error) => { process.stderr.write(`spiffe-openbao-login post: ${error.message}\n`); process.exitCode = 1; });