新增 SPIFFE OpenBao 与 Nexus Actions

This commit is contained in:
2026-09-21 03:28:20 +00:00
parent 6192a52a6a
commit b95289d5b0
6 changed files with 158 additions and 1 deletions
+21
View File
@@ -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.
+32 -1
View File
@@ -1,3 +1,34 @@
# ci-actions
Gitea ActionsSPIFFE/OpenBao 登录与 Nexus CI 配置
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 读取
管理员凭据。
+40
View File
@@ -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
+16
View File
@@ -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
+37
View File
@@ -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; });
+12
View File
@@ -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; });