Merge pull request '新增 SPIFFE OpenBao 与 Nexus Actions' (#1) from feat/initial-actions into main
This commit is contained in:
@@ -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.
|
||||
@@ -1,3 +1,34 @@
|
||||
# ci-actions
|
||||
|
||||
Gitea Actions:SPIFFE/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 读取
|
||||
管理员凭据。
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
Vendored
+37
@@ -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; });
|
||||
Vendored
+12
@@ -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; });
|
||||
Reference in New Issue
Block a user