Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c3cd6a921
|
||
|
|
0785d9a403 | ||
|
|
dfdcfd3b50
|
@@ -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
|
||||
@@ -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-SVID;DinD job 通过受限 HTTP endpoint 获取短期
|
||||
token。broker 不记录响应、不缓存 token,也不接受调用方指定 audience。
|
||||
|
||||
消息流使用一个 `WorkQueuePolicy` stream。相同 runner label 的所有 worker 共享同一
|
||||
durable consumer;扩容只需要增加 worker 或提高单机 capacity。
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user