Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
.git
.github
.venv
.mypy_cache
.pytest_cache
.ruff_cache
__pycache__
*.py[cod]
*.egg-info
benchmarks
docs
examples
experiments
governance
overrides
scripts
tests
dist
site
35 changes: 30 additions & 5 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,37 @@ on:
push:
tags:
- "v*"
pull_request:
paths:
- "Dockerfile"
- ".dockerignore"
- "pyproject.toml"
- "src/**"
- ".github/workflows/docker.yml"

env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"

jobs:
build-pr:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
permissions:
contents: read

steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0

- name: Build image without publishing
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
with:
context: .
push: false
tags: ghcr.io/agentrust-io/ca2a-runtime:${{ github.sha }}

build-and-push:
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
permissions:
contents: read
Expand All @@ -19,10 +44,10 @@ jobs:

steps:
- name: Checkout
uses: actions/checkout@v7
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0

- name: Log in to GitHub Container Registry
uses: docker/login-action@v4
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4
with:
registry: ghcr.io
username: ${{ github.actor }}
Expand All @@ -34,7 +59,7 @@ jobs:

- name: Build and push
id: build
uses: docker/build-push-action@v7
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
with:
context: .
push: true
Expand All @@ -43,15 +68,15 @@ jobs:
ghcr.io/agentrust-io/ca2a-runtime:latest

- name: Install cosign
uses: sigstore/cosign-installer@v3
uses: sigstore/cosign-installer@f713795cb21599bc4e5c4b58cbad1da852d7eeb9 # v3

- name: Sign the image (keyless, by digest)
env:
DIGEST: ${{ steps.build.outputs.digest }}
run: cosign sign --yes ghcr.io/agentrust-io/ca2a-runtime@${DIGEST}

- name: Attest build provenance (SLSA)
uses: actions/attest-build-provenance@v4
uses: actions/attest-build-provenance@8beda2b7ed98355c0e97c0a63bec38ae472e66c4 # v4
with:
subject-name: ghcr.io/agentrust-io/ca2a-runtime
subject-digest: ${{ steps.build.outputs.digest }}
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
and publishes only after those gates pass. Runtime `__version__` now comes from
installed package metadata instead of a stale independent constant.

- Added the missing release container as a multi-stage, rootless image. Runtime
installation is offline from the builder wheelhouse and the build context
excludes VCS, tests, docs, and local environments. Pull requests now build the
image without registry/signing privileges, while tagged releases retain
version and `latest` tags, keyless signing, and provenance attestation. All
third-party container actions are pinned to immutable commits.

- **A delegation chain was a bearer credential: any party holding a copy was granted the leaf's authority.** The inbound path verified signatures, continuity, attenuation, depth and replay, then granted, without ever requiring the caller to demonstrate a relationship to the chain it presented. `PeerRequest` had no field that could carry such a proof, and `subject` — an Ed25519 public key — was only ever compared as a string for continuity, never used as a key.

Chains are published deliberately: handed to auditors for offline verification, embedded in provenance DAGs, and shipped in `examples/`. So the credential intended for publication was the credential that granted authority. A chain lifted from any of those and replayed verbatim was accepted, and the provenance record emitted afterwards named the legitimate subject, so the audit trail attributed the call to the wrong party. Nothing was forged, so nothing failed a check and nothing anomalous reached a log; verbatim replay leaves no tamper evidence to find. `CREDENTIAL_REPLAY` does not cover it, catching only a duplicate `credential_id` inside one chain rather than replay of a whole valid chain by a different party.
Expand Down
30 changes: 30 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
FROM python:3.11.15-slim-bookworm AS builder

ENV PIP_DISABLE_PIP_VERSION_CHECK=1 \
PIP_NO_CACHE_DIR=1

WORKDIR /build
COPY pyproject.toml README.md LICENSE NOTICE ./
COPY src ./src
RUN python -m pip wheel --wheel-dir /wheels .

FROM python:3.11.15-slim-bookworm AS runtime

ENV PIP_DISABLE_PIP_VERSION_CHECK=1 \
PIP_NO_CACHE_DIR=1 \
PYTHONUNBUFFERED=1

RUN groupadd --gid 10001 ca2a \
&& useradd --uid 10001 --gid 10001 --no-create-home --home-dir /var/lib/ca2a ca2a \
&& install -d -o ca2a -g ca2a /var/lib/ca2a /etc/ca2a

COPY --from=builder /wheels /wheels
RUN python -m pip install --no-index --find-links=/wheels ca2a-runtime \
&& rm -rf /wheels

USER 10001:10001
WORKDIR /var/lib/ca2a
EXPOSE 8443

ENTRYPOINT ["ca2a"]
CMD ["--help"]
14 changes: 14 additions & 0 deletions docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,20 @@ This walkthrough builds a delegation chain and verifies it offline. It needs no
pip install --pre ca2a-runtime
```

Or run the published rootless container with a read-only configuration mount:

```bash
docker run --rm -p 8443:8443 \
--read-only --tmpfs /tmp:rw,noexec,nosuid,size=16m \
-v "$PWD/ca2a-config.yaml:/etc/ca2a/config.yaml:ro" \
ghcr.io/agentrust-io/ca2a-runtime:v0.1.0a1 \
start --config /etc/ca2a/config.yaml
```

The image runs as UID/GID 10001. Hardware-backed providers additionally need
the relevant device passed through with permissions for that identity; do not
run the whole container as root to obtain device access.

cA2A is in alpha, so `--pre` is required to opt into the pre-release. Contributors working from a checkout can instead install from source: `pip install -e ".[dev]"`.

## Build an example chain
Expand Down
63 changes: 63 additions & 0 deletions tests/unit/test_container_release.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""Static guarantees for the releasable, least-privilege runtime container."""

from __future__ import annotations

from pathlib import Path

import yaml


def test_runtime_image_is_multistage_non_root_and_offline_installed() -> None:
dockerfile = Path("Dockerfile").read_text(encoding="utf-8")
assert dockerfile.count("FROM python:3.11.15-slim-bookworm") == 2
assert "AS builder" in dockerfile
assert "pip wheel --wheel-dir /wheels ." in dockerfile
assert "pip install --no-index --find-links=/wheels ca2a-runtime" in dockerfile
assert "USER 10001:10001" in dockerfile
assert 'ENTRYPOINT ["ca2a"]' in dockerfile


def test_container_context_excludes_development_and_vcs_state() -> None:
ignored = set(Path(".dockerignore").read_text(encoding="utf-8").splitlines())
assert {".git", ".github", ".venv", "tests", "docs", "examples", "dist"} <= ignored


def test_pull_requests_build_without_registry_or_signing_privileges() -> None:
workflow = yaml.safe_load(Path(".github/workflows/docker.yml").read_text(encoding="utf-8"))
triggers = workflow[True]
job = workflow["jobs"]["build-pr"]
steps = job["steps"]
build = next(step for step in steps if step.get("name") == "Build image without publishing")

assert "pull_request" in triggers
assert job["permissions"] == {"contents": "read"}
assert build["with"]["push"] is False
assert "github.sha" in build["with"]["tags"]
assert all("login-action" not in step.get("uses", "") for step in steps)
assert all("cosign" not in step.get("uses", "") for step in steps)


def test_third_party_container_actions_are_pinned() -> None:
workflow = yaml.safe_load(Path(".github/workflows/docker.yml").read_text(encoding="utf-8"))
for job in workflow["jobs"].values():
for step in job["steps"]:
uses = step.get("uses")
if uses:
ref = uses.rsplit("@", 1)[1].split()[0]
assert len(ref) == 40
assert all(char in "0123456789abcdef" for char in ref)


def test_release_job_retains_version_latest_signing_and_attestation() -> None:
workflow = yaml.safe_load(Path(".github/workflows/docker.yml").read_text(encoding="utf-8"))
job = workflow["jobs"]["build-and-push"]
steps = job["steps"]
build = next(step for step in steps if step.get("name") == "Build and push")
names = {step.get("name") for step in steps}

assert job["permissions"]["packages"] == "write"
assert job["permissions"]["id-token"] == "write"
assert build["with"]["push"] is True
assert "steps.tag.outputs.tag" in build["with"]["tags"]
assert "ca2a-runtime:latest" in build["with"]["tags"]
assert {"Sign the image (keyless, by digest)", "Attest build provenance (SLSA)"} <= names