Compare commits

...
4 Commits
Author SHA1 Message Date
panxiao81 4d6ae62abe Merge pull request:添加嵌套 CI 的 SPIRE JWT broker
test / python (push) Successful in 8s
test / shell (push) Successful in 15s
2026-09-16 13:38:48 +00:00
panxiao81 7c3cd6a921 添加嵌套 CI 的 SPIRE JWT broker
test / python (pull_request) Successful in 8s
test / shell (pull_request) Successful in 17s
2026-09-16 13:37:40 +00:00
panxiao81 0785d9a403 Merge pull request:添加 SPIRE JWT-SVID 冒烟测试
test / shell (push) Successful in 18s
test / python (push) Successful in 7s
2026-09-16 13:31:27 +00:00
panxiao81 dfdcfd3b50 添加 SPIRE JWT-SVID 冒烟测试
test / shell (pull_request) Successful in 1m11s
test / python (pull_request) Successful in 1m27s
2026-09-16 13:29:04 +00:00
6 changed files with 147 additions and 1 deletions
+33
View File
@@ -0,0 +1,33 @@
name: SPIRE identity smoke
on:
workflow_dispatch:
jobs:
jwt-svid:
runs-on: self-hosted
container:
volumes:
- /run/spire/agent-sockets:/run/spire/agent-sockets:ro
steps:
- name: Fetch pinned SPIRE CLI
shell: bash
run: |
set -euo pipefail
archive=/tmp/spire.tar.gz
curl --fail --location --silent --show-error \
--output "$archive" \
https://github.com/spiffe/spire/releases/download/v1.15.3/spire-1.15.3-linux-amd64-musl.tar.gz
printf '%s %s\n' \
ca1a4d1155317bdd2afc7f36663828a10410c7c840e54725b90b4064b0a301c7 \
"$archive" | sha256sum --check --status
tar -xzf "$archive" -C /tmp spire-1.15.3/bin/spire-agent
- name: Fetch short-lived zot JWT-SVID
shell: bash
run: |
set -euo pipefail
/tmp/spire-1.15.3/bin/spire-agent api fetch jwt \
-audience zot \
-socketPath /run/spire/agent-sockets/spire-agent.sock \
>/dev/null
+3
View File
@@ -12,6 +12,9 @@
Cloud Hypervisor,退出后完整清理。
- `guest-runner`:在 guest 中领取一次性 runner registration token,注册 ephemeral
runner,执行一个 job 后关机。
- `jwt-broker`:运行在 Kubernetes runner 外层 Pod 中,以可被 SPIRE attestation
的 PID 获取固定 `aud=zot` JWT-SVIDDinD job 通过受限 HTTP endpoint 获取短期
token。broker 不记录响应、不缓存 token,也不接受调用方指定 audience。
消息流使用一个 `WorkQueuePolicy` stream。相同 runner label 的所有 worker 共享同一
durable consumer;扩容只需要增加 worker 或提高单机 capacity。
+3
View File
@@ -1,3 +1,5 @@
FROM ghcr.io/spiffe/spire-agent:1.15.3@sha256:41b0dcd8b258a69db9e2768292a060766fb76fd866e4bc925849981ea1b825ff AS spire
FROM python:3.12.11-alpine3.22 AS build
WORKDIR /src
COPY pyproject.toml README.md ./
@@ -7,6 +9,7 @@ RUN python -m venv /venv && /venv/bin/pip install --no-cache-dir .
FROM python:3.12.11-alpine3.22
RUN addgroup -S -g 65532 runner && adduser -S -D -H -u 65532 -G runner runner
COPY --from=build /venv /venv
COPY --from=spire /opt/spire/bin/spire-agent /opt/spire/bin/spire-agent
USER 65532:65532
EXPOSE 8787
ENTRYPOINT ["/venv/bin/gitea-microvm-controller"]
+2 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "gitea-microvm-runner"
version = "0.1.0"
version = "0.2.0"
description = "On-demand Cloud Hypervisor runners for Gitea Actions"
requires-python = ">=3.11"
dependencies = ["aiohttp==3.12.15", "nats-py==2.11.0"]
@@ -15,6 +15,7 @@ test = ["pytest==8.4.2", "pytest-asyncio==1.2.0"]
[project.scripts]
gitea-microvm-controller = "gitea_microvm_runner.controller:main"
gitea-microvm-worker = "gitea_microvm_runner.worker:cli"
gitea-spire-jwt-broker = "gitea_microvm_runner.jwt_broker:main"
[tool.pytest.ini_options]
asyncio_mode = "auto"
+90
View File
@@ -0,0 +1,90 @@
#!/usr/bin/env python3
"""Fixed-audience JWT-SVID broker for nested Gitea job containers."""
import asyncio
import json
import os
from aiohttp import web
SPIRE_AGENT = os.environ.get("SPIRE_AGENT", "/opt/spire/bin/spire-agent")
SPIRE_SOCKET = os.environ.get(
"SPIRE_SOCKET", "/run/spire/agent-sockets/spire-agent.sock"
)
AUDIENCE = os.environ.get("JWT_AUDIENCE", "zot")
MAX_CONCURRENCY = int(os.environ.get("MAX_CONCURRENCY", "4"))
semaphore = asyncio.Semaphore(MAX_CONCURRENCY)
def extract_svid(document: object) -> str:
if not isinstance(document, list):
raise ValueError("unexpected SPIRE response")
for item in document:
if not isinstance(item, dict):
continue
svids = item.get("svids")
if not isinstance(svids, list):
continue
for svid in svids:
if isinstance(svid, dict) and isinstance(svid.get("svid"), str):
return svid["svid"]
raise ValueError("SPIRE response contains no JWT-SVID")
async def fetch_svid() -> str:
async with semaphore:
process = await asyncio.create_subprocess_exec(
SPIRE_AGENT,
"api",
"fetch",
"jwt",
"-output",
"json",
"-audience",
AUDIENCE,
"-socketPath",
SPIRE_SOCKET,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=10)
if process.returncode != 0:
message = stderr.decode(errors="replace").strip()
raise RuntimeError(f"SPIRE agent failed: {message}")
return extract_svid(json.loads(stdout))
async def token(_: web.Request) -> web.Response:
try:
value = await fetch_svid()
except (RuntimeError, ValueError, json.JSONDecodeError, asyncio.TimeoutError):
raise web.HTTPServiceUnavailable(text="identity unavailable\n")
return web.Response(
text=f"{value}\n",
content_type="text/plain",
headers={"Cache-Control": "no-store"},
)
async def health(_: web.Request) -> web.Response:
return web.Response(text="ok\n")
def create_app() -> web.Application:
app = web.Application(client_max_size=1024)
app.router.add_post("/token", token)
app.router.add_get("/healthz", health)
return app
def main() -> None:
web.run_app(
create_app(),
host=os.environ.get("LISTEN", "0.0.0.0"),
port=int(os.environ.get("PORT", "8788")),
access_log=None,
)
if __name__ == "__main__":
main()
+16
View File
@@ -0,0 +1,16 @@
import pytest
from gitea_microvm_runner.jwt_broker import extract_svid
def test_extract_svid_from_spire_json():
assert extract_svid([
{"svids": [{"hint": "", "spiffe_id": "spiffe://example/ci", "svid": "jwt"}]},
{"bundles": {}},
]) == "jwt"
@pytest.mark.parametrize("document", [{}, [], [{"bundles": {}}]])
def test_extract_svid_rejects_unexpected_response(document):
with pytest.raises(ValueError):
extract_svid(document)