Merge current main into PR 221

This commit is contained in:
fwazb 2026-07-21 17:04:38 -07:00
commit a8a1e7958b
6 changed files with 637 additions and 52 deletions

View file

@ -1,9 +1,6 @@
name: gcp-artifact name: gcp-artifact
on: on:
push:
branches:
- main
workflow_dispatch: workflow_dispatch:
permissions: permissions:
@ -28,6 +25,13 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 20 timeout-minutes: 20
steps: steps:
- name: Require manual main dispatch
shell: bash
run: |
set -euo pipefail
test "${GITHUB_EVENT_NAME}" = "workflow_dispatch"
test "${GITHUB_REF}" = "refs/heads/main"
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- id: auth - id: auth

View file

@ -24,6 +24,7 @@ the current readiness run passes.
- `roles/storage.objectViewer` - `roles/storage.objectViewer`
- GitHub Actions can publish Artifact Registry images through Workload Identity Federation: - GitHub Actions can publish Artifact Registry images through Workload Identity Federation:
- workflow: `.github/workflows/gcp-artifact.yml` - workflow: `.github/workflows/gcp-artifact.yml`
- trigger: explicit `workflow_dispatch` only; ordinary branch pushes never publish
- provider: `projects/785938879453/locations/global/workloadIdentityPools/github-actions/providers/living-ip-github` - provider: `projects/785938879453/locations/global/workloadIdentityPools/github-actions/providers/living-ip-github`
- service account: `sa-artifact-builder@teleo-501523.iam.gserviceaccount.com` - service account: `sa-artifact-builder@teleo-501523.iam.gserviceaccount.com`
- repository scope: `living-ip/teleo-infrastructure` - repository scope: `living-ip/teleo-infrastructure`
@ -53,13 +54,31 @@ the current readiness run passes.
## How To Build ## How To Build
Automatic Artifact Registry publishing runs on pushes to `main` through GitHub Actions. To run the same lane manually from GitHub: The legacy `teleo-pipeline-gcp-staging` image is never published by an ordinary
push to `main`. Publishing it is a live Artifact Registry mutation and requires
an explicit manual dispatch from the reviewed `main` revision:
```bash ```bash
gh workflow run gcp-artifact.yml --repo living-ip/teleo-infrastructure --ref main gh workflow run gcp-artifact.yml --repo living-ip/teleo-infrastructure --ref main
``` ```
The workflow authenticates to GCP with Workload Identity Federation, builds `Dockerfile.gcp-staging`, runs the image smoke test, pushes the image, and uploads `gcp-artifact-image.txt` as a run artifact. The workflow authenticates to GCP with Workload Identity Federation, builds
`Dockerfile.gcp-staging`, runs the image smoke test, pushes the image, and
uploads `gcp-artifact-image.txt` as a run artifact. This is the legacy Teleo
pipeline image path; it does not publish the separately reviewed
`leoclean-nosend-staging` artifact.
The first job step fails before checkout or cloud authentication unless the
event is `workflow_dispatch` and the selected ref is `refs/heads/main`. This is
a repository-owned execution guard. The current Workload Identity Federation
condition remains repository-scoped rather than IAM ref-scoped; binding the
principal to `main` in IAM is a separate reviewed live-GCP hardening decision.
The manual-only governance change does not attest or repair that legacy image.
The last run that reached a runner (`29810180369`) failed before publication
because `Dockerfile.gcp-staging` did not copy the packaged
`observatory_read_adapter` directory. Do not dispatch the legacy publisher
until a separate reviewed repair or retirement decision closes that defect.
For a read-only GCP posture probe through the same Workload Identity path: For a read-only GCP posture probe through the same Workload Identity path:

View file

@ -231,8 +231,16 @@ explicit inputs; inherited `DOCKER_*`, `BUILDKIT_*`, and `BUILDX_*` overrides
are removed and Docker stdin is closed. The pull does not are removed and Docker stdin is closed. The pull does not
start a container, install a unit, restart a service, or contact Cloud SQL. start a container, install a unit, restart a service, or contact Cloud SQL.
Registry authentication and this pull require a separate live-read approval. Registry authentication and this pull require a separate live-read approval.
While holding an advisory lock on the explicit Docker config directory, the The package opens every component of the explicit Docker config path without
command pulls and records the dynamically observed config ID. It intentionally following links and retains that full descriptor chain while Docker runs. Every
component must be root- or current-user-owned and non-writable by group/other;
a root-owned sticky temporary directory is accepted as a boundary only when the
operator directory below it is private. Docker starts in the held final
directory and receives `--config .`, so replacing and restoring the named path
mid-command cannot redirect credential reads. The package also revalidates the
named chain before and after each call while holding an advisory lock on the
final directory. The command pulls and records the dynamically observed config
ID. It intentionally
retains the exact repository-digest reference: Docker's image cache belongs to retains the exact repository-digest reference: Docker's image cache belongs to
the daemon, so a config-directory lock cannot prove that another client did not the daemon, so a config-directory lock cannot prove that another client did not
start relying on the same immutable reference after the precheck. Repeated runs start relying on the same immutable reference after the precheck. Repeated runs

View file

@ -1916,6 +1916,100 @@ def _docker_environment() -> dict[str, str]:
} }
def _close_descriptors(descriptors: tuple[int, ...] | list[int]) -> None:
for descriptor in reversed(descriptors):
os.close(descriptor)
def _open_directory_chain_descriptors(
directory: Path,
label: str,
) -> tuple[tuple[int, ...], tuple[tuple[int, int], ...]]:
"""Retain every directory component without following links."""
_require(directory.is_absolute() and ".." not in directory.parts, f"{label} must be an absolute direct path")
flags = os.O_RDONLY
if hasattr(os, "O_DIRECTORY"):
flags |= os.O_DIRECTORY
if hasattr(os, "O_NOFOLLOW"):
flags |= os.O_NOFOLLOW
descriptors: list[int] = []
identities: list[tuple[int, int]] = []
try:
descriptors.append(os.open("/", flags))
observed = os.fstat(descriptors[-1])
_require(stat.S_ISDIR(observed.st_mode), f"{label} root is unsafe")
identities.append((observed.st_dev, observed.st_ino))
for component in directory.parts[1:]:
_require(component not in {"", ".", ".."}, f"{label} path is unsafe")
descriptors.append(os.open(component, flags, dir_fd=descriptors[-1]))
observed = os.fstat(descriptors[-1])
_require(stat.S_ISDIR(observed.st_mode), f"{label} path is unsafe")
identities.append((observed.st_dev, observed.st_ino))
named = os.stat(directory, follow_symlinks=False)
_require(
stat.S_ISDIR(named.st_mode) and identities[-1] == (named.st_dev, named.st_ino),
f"{label} path identity drifted",
)
except (OSError, PackageError) as exc:
_close_descriptors(descriptors)
if isinstance(exc, PackageError):
raise
raise PackageError(f"{label} is missing or contains a symbolic link") from exc
return tuple(descriptors), tuple(identities)
def _open_directory_chain_nofollow(
directory: Path,
label: str,
) -> tuple[int, tuple[tuple[int, int], ...]]:
"""Open an absolute directory one component at a time without following links."""
descriptors, identities = _open_directory_chain_descriptors(directory, label)
try:
descriptor = os.dup(descriptors[-1])
except OSError as exc:
raise PackageError(f"{label} descriptor cannot be retained") from exc
finally:
_close_descriptors(descriptors)
return descriptor, identities
def _validate_docker_config_chain_posture(descriptors: tuple[int, ...]) -> None:
_require(bool(descriptors), "Docker config directory chain is empty")
for index, descriptor in enumerate(descriptors):
observed = os.fstat(descriptor)
mode = stat.S_IMODE(observed.st_mode)
final = index == len(descriptors) - 1
if final:
trusted = observed.st_uid == os.geteuid() and mode & 0o022 == 0
else:
trusted_owner = observed.st_uid in {0, os.geteuid()}
nonwritable = mode & 0o022 == 0
root_sticky_boundary = observed.st_uid == 0 and bool(mode & stat.S_ISVTX)
trusted = trusted_owner and (nonwritable or root_sticky_boundary)
_require(
stat.S_ISDIR(observed.st_mode) and trusted,
"Docker config directory must have only root/current-user trusted ancestors; "
"a writable ancestor is allowed only when it is a root-owned sticky boundary",
)
def _open_trusted_docker_config_chain(
docker_config: Path,
) -> tuple[tuple[int, ...], tuple[tuple[int, int], ...]]:
descriptors, identities = _open_directory_chain_descriptors(
docker_config,
"Docker config directory",
)
try:
_validate_docker_config_chain_posture(descriptors)
except (OSError, PackageError):
_close_descriptors(descriptors)
raise
return descriptors, identities
def _validate_docker_authority(docker_binary: Path, docker_host: str, docker_config: Path) -> None: def _validate_docker_authority(docker_binary: Path, docker_host: str, docker_config: Path) -> None:
_require(docker_binary.is_absolute(), "Docker binary must be absolute") _require(docker_binary.is_absolute(), "Docker binary must be absolute")
_require("\x00" not in str(docker_binary) and "\n" not in str(docker_binary), "Docker binary is invalid") _require("\x00" not in str(docker_binary) and "\n" not in str(docker_binary), "Docker binary is invalid")
@ -1928,31 +2022,57 @@ def _validate_docker_authority(docker_binary: Path, docker_host: str, docker_con
and "\n" not in docker_host, and "\n" not in docker_host,
"Docker host must be a local Unix endpoint", "Docker host must be a local Unix endpoint",
) )
_require( descriptors, _identities = _open_trusted_docker_config_chain(docker_config)
docker_config.is_absolute() and docker_config.is_dir() and not docker_config.is_symlink(), _close_descriptors(descriptors)
"Docker config directory is missing or unsafe",
)
class _DockerCacheLock: class _DockerCacheLock:
def __init__(self, docker_config: Path) -> None: def __init__(self, docker_config: Path) -> None:
self._docker_config = docker_config self._docker_config = docker_config
self._descriptor: int | None = None self._descriptor: int | None = None
self._descriptors: tuple[int, ...] = ()
self._identities: tuple[tuple[int, int], ...] = ()
def __enter__(self) -> None: def __enter__(self) -> _DockerCacheLock:
flags = os.O_RDONLY _require(self._descriptor is None, "Docker cache authority lock is already active")
if hasattr(os, "O_DIRECTORY"): descriptors: tuple[int, ...] = ()
flags |= os.O_DIRECTORY
if hasattr(os, "O_NOFOLLOW"):
flags |= os.O_NOFOLLOW
try: try:
descriptor = os.open(self._docker_config, flags) descriptors, identities = _open_trusted_docker_config_chain(self._docker_config)
descriptor = descriptors[-1]
fcntl.flock(descriptor, fcntl.LOCK_EX | fcntl.LOCK_NB) fcntl.flock(descriptor, fcntl.LOCK_EX | fcntl.LOCK_NB)
except OSError as exc: except (OSError, PackageError) as exc:
if "descriptor" in locals(): _close_descriptors(descriptors)
os.close(descriptor)
raise PackageError("Docker cache authority is already in use or unsafe") from exc raise PackageError("Docker cache authority is already in use or unsafe") from exc
self._descriptors = descriptors
self._identities = identities
self._descriptor = descriptor self._descriptor = descriptor
return self
def revalidate(self, docker_config: Path) -> None:
_require(
self._descriptor is not None
and bool(self._descriptors)
and bool(self._identities)
and docker_config == self._docker_config,
"Docker cache authority is not active for this config",
)
_validate_docker_config_chain_posture(self._descriptors)
named_descriptors, named_identities = _open_trusted_docker_config_chain(docker_config)
try:
_require(
named_identities == self._identities,
"Docker config directory ancestor identity or posture drifted",
)
finally:
_close_descriptors(named_descriptors)
def enter_subprocess_config_directory(self) -> None:
_require(self._descriptor is not None, "Docker cache authority is not active")
os.fchdir(self._descriptor)
def subprocess_config_authority(self) -> tuple[str, tuple[int, ...]]:
_require(self._descriptor is not None, "Docker cache authority is not active")
return ".", (self._descriptor,)
def __exit__(self, _kind: object, _value: object, _traceback: object) -> None: def __exit__(self, _kind: object, _value: object, _traceback: object) -> None:
if self._descriptor is None: if self._descriptor is None:
@ -1960,8 +2080,10 @@ class _DockerCacheLock:
try: try:
fcntl.flock(self._descriptor, fcntl.LOCK_UN) fcntl.flock(self._descriptor, fcntl.LOCK_UN)
finally: finally:
os.close(self._descriptor) _close_descriptors(self._descriptors)
self._descriptor = None self._descriptor = None
self._descriptors = ()
self._identities = ()
def _run_docker( def _run_docker(
@ -1972,16 +2094,36 @@ def _run_docker(
*, *,
check: bool, check: bool,
timeout: int, timeout: int,
authority: _DockerCacheLock | None = None,
) -> subprocess.CompletedProcess[bytes]: ) -> subprocess.CompletedProcess[bytes]:
_validate_docker_authority(docker_binary, docker_host, docker_config) _validate_docker_authority(docker_binary, docker_host, docker_config)
return subprocess.run( _require("--config" not in arguments, "Docker arguments cannot override the retained config authority")
[str(docker_binary), "--host", docker_host, "--config", str(docker_config), *arguments], if authority is None:
check=check, with _DockerCacheLock(docker_config) as owned_authority:
capture_output=True, return _run_docker(
stdin=subprocess.DEVNULL, docker_binary,
timeout=timeout, docker_host,
env=_docker_environment(), docker_config,
) arguments,
check=check,
timeout=timeout,
authority=owned_authority,
)
authority.revalidate(docker_config)
subprocess_config, pass_fds = authority.subprocess_config_authority()
try:
return subprocess.run(
[str(docker_binary), "--host", docker_host, "--config", subprocess_config, *arguments],
check=check,
capture_output=True,
stdin=subprocess.DEVNULL,
timeout=timeout,
env=_docker_environment(),
pass_fds=pass_fds,
preexec_fn=authority.enter_subprocess_config_directory,
)
finally:
authority.revalidate(docker_config)
def _bounded_docker_output(result: subprocess.CompletedProcess[bytes], label: str) -> None: def _bounded_docker_output(result: subprocess.CompletedProcess[bytes], label: str) -> None:
@ -1996,6 +2138,8 @@ def _inspect_local_image(
docker_host: str, docker_host: str,
docker_config: Path, docker_config: Path,
image_reference: str, image_reference: str,
*,
authority: _DockerCacheLock | None = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
result = _run_docker( result = _run_docker(
docker_binary, docker_binary,
@ -2004,6 +2148,7 @@ def _inspect_local_image(
["image", "inspect", image_reference], ["image", "inspect", image_reference],
check=True, check=True,
timeout=60, timeout=60,
authority=authority,
) )
_bounded_docker_output(result, "local image inspection") _bounded_docker_output(result, "local image inspection")
inspected = _load_docker_inspection(result.stdout) inspected = _load_docker_inspection(result.stdout)
@ -2019,6 +2164,7 @@ def _local_tag_config_id(
*, *,
repository: str, repository: str,
tag: str, tag: str,
authority: _DockerCacheLock | None = None,
) -> str | None: ) -> str | None:
_require( _require(
repository in {LOCAL_BUILD_REPOSITORY, IMAGE_REPOSITORY} repository in {LOCAL_BUILD_REPOSITORY, IMAGE_REPOSITORY}
@ -2034,6 +2180,7 @@ def _local_tag_config_id(
["image", "ls", "--no-trunc", "--format", "{{json .}}", repository], ["image", "ls", "--no-trunc", "--format", "{{json .}}", repository],
check=True, check=True,
timeout=60, timeout=60,
authority=authority,
) )
_bounded_docker_output(inventory, "local image inventory") _bounded_docker_output(inventory, "local image inventory")
matching_ids: set[str] = set() matching_ids: set[str] = set()
@ -2059,7 +2206,13 @@ def _local_tag_config_id(
if not matching_ids: if not matching_ids:
return None return None
inventory_config_id = next(iter(matching_ids)) inventory_config_id = next(iter(matching_ids))
inspected = _inspect_local_image(docker_binary, docker_host, docker_config, reference) inspected = _inspect_local_image(
docker_binary,
docker_host,
docker_config,
reference,
authority=authority,
)
config_id = inspected.get("Id") config_id = inspected.get("Id")
_require(config_id == inventory_config_id, "local image inventory and inspection disagree") _require(config_id == inventory_config_id, "local image inventory and inspection disagree")
return inventory_config_id return inventory_config_id
@ -2084,6 +2237,44 @@ def _target_repository_digests(inspected: dict[str, Any]) -> list[str]:
return matching return matching
def _validate_receipt_context_separation(
context: Path,
output_receipt: Path,
*,
output_parent_descriptor: int | None = None,
) -> None:
_require(
context.is_absolute() and output_receipt.is_absolute() and output_receipt.name not in {"", ".", ".."},
"build/push context and receipt paths must be absolute",
)
context_descriptor, _context_chain = _open_directory_chain_nofollow(
context,
"prepared image context",
)
output_descriptor: int | None = None
try:
output_descriptor, output_chain = _open_directory_chain_nofollow(
output_receipt.parent,
"build/push receipt parent",
)
context_stat = os.fstat(context_descriptor)
context_identity = (context_stat.st_dev, context_stat.st_ino)
if output_parent_descriptor is not None:
held_parent = os.fstat(output_parent_descriptor)
_require(
stat.S_ISDIR(held_parent.st_mode) and (held_parent.st_dev, held_parent.st_ino) == output_chain[-1],
"build/push receipt parent identity drifted",
)
_require(
context_identity not in output_chain,
"build/push receipt must be outside the image context",
)
finally:
if output_descriptor is not None:
os.close(output_descriptor)
os.close(context_descriptor)
def build_and_push_staging_image( def build_and_push_staging_image(
repo_root: Path, repo_root: Path,
context: Path, context: Path,
@ -2101,15 +2292,12 @@ def build_and_push_staging_image(
validate_reviewed_context_source(repo_root, context, image_input) validate_reviewed_context_source(repo_root, context, image_input)
_validate_docker_authority(docker_binary, docker_host, docker_config) _validate_docker_authority(docker_binary, docker_host, docker_config)
_validate_receipt_output_path(output_receipt) _validate_receipt_output_path(output_receipt)
_require( _validate_receipt_context_separation(context, output_receipt)
context != output_receipt and context not in output_receipt.parents,
"build/push receipt must be outside the image context",
)
build_tag = f"input-{image_input['input_sha256']}" build_tag = f"input-{image_input['input_sha256']}"
build_reference = f"{LOCAL_BUILD_REPOSITORY}:{build_tag}" build_reference = f"{LOCAL_BUILD_REPOSITORY}:{build_tag}"
try: try:
with _DockerCacheLock(docker_config): with _DockerCacheLock(docker_config) as authority:
_require( _require(
_local_tag_config_id( _local_tag_config_id(
docker_binary, docker_binary,
@ -2117,6 +2305,7 @@ def build_and_push_staging_image(
docker_config, docker_config,
repository=LOCAL_BUILD_REPOSITORY, repository=LOCAL_BUILD_REPOSITORY,
tag=build_tag, tag=build_tag,
authority=authority,
) )
is None, is None,
"local build tag must be absent before build", "local build tag must be absent before build",
@ -2142,9 +2331,16 @@ def build_and_push_staging_image(
command, command,
check=True, check=True,
timeout=1800, timeout=1800,
authority=authority,
) )
_bounded_docker_output(built, "staging image build") _bounded_docker_output(built, "staging image build")
built_image = _inspect_local_image(docker_binary, docker_host, docker_config, build_reference) built_image = _inspect_local_image(
docker_binary,
docker_host,
docker_config,
build_reference,
authority=authority,
)
observed = validate_image_configuration(image_input, built_image) observed = validate_image_configuration(image_input, built_image)
build_config_id = observed["config_digest"] build_config_id = observed["config_digest"]
_require( _require(
@ -2154,6 +2350,7 @@ def build_and_push_staging_image(
docker_config, docker_config,
repository=LOCAL_BUILD_REPOSITORY, repository=LOCAL_BUILD_REPOSITORY,
tag=build_tag, tag=build_tag,
authority=authority,
) )
== build_config_id, == build_config_id,
"local build tag differs from the inspected image", "local build tag differs from the inspected image",
@ -2178,6 +2375,7 @@ def build_and_push_staging_image(
docker_config, docker_config,
repository=IMAGE_REPOSITORY, repository=IMAGE_REPOSITORY,
tag=candidate_tag, tag=candidate_tag,
authority=authority,
) )
is None, is None,
"content-bound staging candidate tag already exists locally", "content-bound staging candidate tag already exists locally",
@ -2189,6 +2387,7 @@ def build_and_push_staging_image(
["image", "tag", build_reference, candidate_reference], ["image", "tag", build_reference, candidate_reference],
check=True, check=True,
timeout=60, timeout=60,
authority=authority,
) )
_bounded_docker_output(tagged, "staging candidate tag") _bounded_docker_output(tagged, "staging candidate tag")
_require( _require(
@ -2198,6 +2397,7 @@ def build_and_push_staging_image(
docker_config, docker_config,
repository=IMAGE_REPOSITORY, repository=IMAGE_REPOSITORY,
tag=candidate_tag, tag=candidate_tag,
authority=authority,
) )
== build_config_id, == build_config_id,
"staging candidate tag differs from the built image", "staging candidate tag differs from the built image",
@ -2207,6 +2407,7 @@ def build_and_push_staging_image(
docker_host, docker_host,
docker_config, docker_config,
candidate_reference, candidate_reference,
authority=authority,
) )
_require( _require(
validate_image_configuration(image_input, pre_push_image) == observed, validate_image_configuration(image_input, pre_push_image) == observed,
@ -2223,6 +2424,7 @@ def build_and_push_staging_image(
["image", "push", "--quiet", candidate_reference], ["image", "push", "--quiet", candidate_reference],
check=True, check=True,
timeout=1800, timeout=1800,
authority=authority,
) )
_bounded_docker_output(pushed, "staging image push") _bounded_docker_output(pushed, "staging image push")
post_push_image = _inspect_local_image( post_push_image = _inspect_local_image(
@ -2230,6 +2432,7 @@ def build_and_push_staging_image(
docker_host, docker_host,
docker_config, docker_config,
candidate_reference, candidate_reference,
authority=authority,
) )
_require( _require(
validate_image_configuration(image_input, post_push_image) == observed, validate_image_configuration(image_input, post_push_image) == observed,
@ -2243,7 +2446,12 @@ def build_and_push_staging_image(
receipt = build_build_push_receipt(image_input, post_push_image=post_push_image) receipt = build_build_push_receipt(image_input, post_push_image=post_push_image)
except (OSError, subprocess.SubprocessError, PackageError) as exc: except (OSError, subprocess.SubprocessError, PackageError) as exc:
raise PackageError("staging image build/push failed") from exc raise PackageError("staging image build/push failed") from exc
publish_build_push_receipt(output_receipt, receipt, image_input=image_input) publish_build_push_receipt(
output_receipt,
receipt,
image_input=image_input,
outside_context=context,
)
return receipt return receipt
@ -2252,6 +2460,8 @@ def _local_image_reference_config_id(
docker_host: str, docker_host: str,
docker_config: Path, docker_config: Path,
image_reference: str, image_reference: str,
*,
authority: _DockerCacheLock | None = None,
) -> str | None: ) -> str | None:
inventory = _run_docker( inventory = _run_docker(
docker_binary, docker_binary,
@ -2260,6 +2470,7 @@ def _local_image_reference_config_id(
["image", "ls", "--digests", "--no-trunc", "--format", "{{json .}}", IMAGE_REPOSITORY], ["image", "ls", "--digests", "--no-trunc", "--format", "{{json .}}", IMAGE_REPOSITORY],
check=True, check=True,
timeout=60, timeout=60,
authority=authority,
) )
_require(len(inventory.stdout) <= MAX_DOCKER_INSPECTION_BYTES, "local image inventory output is too large") _require(len(inventory.stdout) <= MAX_DOCKER_INSPECTION_BYTES, "local image inventory output is too large")
match = IMAGE_REFERENCE.fullmatch(image_reference) match = IMAGE_REFERENCE.fullmatch(image_reference)
@ -2295,6 +2506,7 @@ def _local_image_reference_config_id(
["image", "inspect", image_reference], ["image", "inspect", image_reference],
check=True, check=True,
timeout=60, timeout=60,
authority=authority,
) )
inspected = _load_docker_inspection(result.stdout) inspected = _load_docker_inspection(result.stdout)
_require(isinstance(inspected, list) and len(inspected) == 1, "local image reference inspection is not exact") _require(isinstance(inspected, list) and len(inspected) == 1, "local image reference inspection is not exact")
@ -2329,7 +2541,7 @@ def inspect_registry_image(
_validate_docker_authority(docker_binary, docker_host, docker_config) _validate_docker_authority(docker_binary, docker_host, docker_config)
inspection: dict[str, Any] | None = None inspection: dict[str, Any] | None = None
try: try:
with _DockerCacheLock(docker_config): with _DockerCacheLock(docker_config) as authority:
_run_docker( _run_docker(
docker_binary, docker_binary,
docker_host, docker_host,
@ -2337,12 +2549,14 @@ def inspect_registry_image(
["pull", "--quiet", "--platform", PLATFORM, image_reference], ["pull", "--quiet", "--platform", PLATFORM, image_reference],
check=True, check=True,
timeout=600, timeout=600,
authority=authority,
) )
pulled_config_id = _local_image_reference_config_id( pulled_config_id = _local_image_reference_config_id(
docker_binary, docker_binary,
docker_host, docker_host,
docker_config, docker_config,
image_reference, image_reference,
authority=authority,
) )
_require(pulled_config_id is not None, "pulled registry image reference is missing") _require(pulled_config_id is not None, "pulled registry image reference is missing")
result = _run_docker( result = _run_docker(
@ -2352,6 +2566,7 @@ def inspect_registry_image(
["image", "inspect", image_reference], ["image", "inspect", image_reference],
check=True, check=True,
timeout=60, timeout=60,
authority=authority,
) )
inspected = _load_docker_inspection(result.stdout) inspected = _load_docker_inspection(result.stdout)
_require(isinstance(inspected, list) and len(inspected) == 1, "registry image inspection is not exact") _require(isinstance(inspected, list) and len(inspected) == 1, "registry image inspection is not exact")
@ -2625,16 +2840,7 @@ def _rename_noreplace(source: Path, destination: Path) -> None:
def _open_private_operator_directory(directory: Path, label: str) -> int: def _open_private_operator_directory(directory: Path, label: str) -> int:
_require(directory.is_absolute(), f"{label} must be absolute") descriptor, _identities = _open_directory_chain_nofollow(directory, label)
flags = os.O_RDONLY
if hasattr(os, "O_DIRECTORY"):
flags |= os.O_DIRECTORY
if hasattr(os, "O_NOFOLLOW"):
flags |= os.O_NOFOLLOW
try:
descriptor = os.open(directory, flags)
except OSError as exc:
raise PackageError(f"{label} is missing or unsafe") from exc
try: try:
observed = os.fstat(descriptor) observed = os.fstat(descriptor)
named = os.stat(directory, follow_symlinks=False) named = os.stat(directory, follow_symlinks=False)
@ -2767,11 +2973,14 @@ def publish_build_push_receipt(
receipt: dict[str, Any], receipt: dict[str, Any],
*, *,
image_input: dict[str, Any], image_input: dict[str, Any],
outside_context: Path | None = None,
) -> None: ) -> None:
"""Atomically publish one validated, self-hashed build/push receipt.""" """Atomically publish one validated, self-hashed build/push receipt."""
validate_build_push_receipt(receipt, image_input=image_input) validate_build_push_receipt(receipt, image_input=image_input)
_validate_receipt_output_path(output_receipt) _validate_receipt_output_path(output_receipt)
if outside_context is not None:
_validate_receipt_context_separation(outside_context, output_receipt)
try: try:
payload = (json.dumps(receipt, allow_nan=False, indent=2, sort_keys=True) + "\n").encode("utf-8") payload = (json.dumps(receipt, allow_nan=False, indent=2, sort_keys=True) + "\n").encode("utf-8")
except (TypeError, ValueError) as exc: except (TypeError, ValueError) as exc:
@ -2784,6 +2993,12 @@ def publish_build_push_receipt(
descriptor: int | None = None descriptor: int | None = None
identity: tuple[int, int] | None = None identity: tuple[int, int] | None = None
try: try:
if outside_context is not None:
_validate_receipt_context_separation(
outside_context,
output_receipt,
output_parent_descriptor=parent_descriptor,
)
_require( _require(
_directory_entry_stat(parent_descriptor, output_receipt.name) is None, _directory_entry_stat(parent_descriptor, output_receipt.name) is None,
"build/push receipt output already exists", "build/push receipt output already exists",
@ -2834,6 +3049,12 @@ def publish_build_push_receipt(
label="retained build/push receipt link", label="retained build/push receipt link",
) )
_fsync_directory_descriptor(parent_descriptor) _fsync_directory_descriptor(parent_descriptor)
if outside_context is not None:
_validate_receipt_context_separation(
outside_context,
output_receipt,
output_parent_descriptor=parent_descriptor,
)
except (OSError, PackageError) as exc: except (OSError, PackageError) as exc:
if isinstance(exc, PackageError): if isinstance(exc, PackageError):
raise raise

View file

@ -1,8 +1,31 @@
from pathlib import Path from pathlib import Path
import yaml
WORKFLOW = Path(".github/workflows/gcp-artifact.yml")
def test_gcp_artifact_workflow_is_manual_only() -> None:
workflow = yaml.load(WORKFLOW.read_text(), Loader=yaml.BaseLoader)
assert set(workflow["on"]) == {"workflow_dispatch"}
assert "push" not in workflow["on"]
def test_gcp_artifact_workflow_requires_main_before_authentication() -> None:
workflow = WORKFLOW.read_text()
guard = workflow.index("Require manual main dispatch")
checkout = workflow.index("actions/checkout@v4")
auth = workflow.index("google-github-actions/auth@v2")
assert 'test "${GITHUB_EVENT_NAME}" = "workflow_dispatch"' in workflow
assert 'test "${GITHUB_REF}" = "refs/heads/main"' in workflow
assert guard < checkout < auth
def test_gcp_artifact_workflow_retains_immutable_image_digest() -> None: def test_gcp_artifact_workflow_retains_immutable_image_digest() -> None:
workflow = Path(".github/workflows/gcp-artifact.yml").read_text() workflow = WORKFLOW.read_text()
assert "gcloud artifacts docker images describe" in workflow assert "gcloud artifacts docker images describe" in workflow
assert "Image tag already exists; reusing immutable Artifact Registry image." in workflow assert "Image tag already exists; reusing immutable Artifact Registry image." in workflow

View file

@ -9,6 +9,7 @@ import os
import shutil import shutil
import subprocess import subprocess
import sys import sys
import tempfile
from pathlib import Path from pathlib import Path
import pytest import pytest
@ -1138,6 +1139,277 @@ def test_build_push_requires_explicit_execution_flag_before_validation(tmp_path:
) )
def test_build_push_rejects_writable_docker_config_before_docker(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
image_input = image_input_fixture(tmp_path)
build_args = {
"ARTIFACT_SHA256": image_input["runtime"]["artifact_sha256"],
"IDENTITY_SHA256": image_input["identity"]["bundle_sha256"],
"INPUT_SHA256": image_input["input_sha256"],
"TELEO_REVISION": image_input["runtime"]["teleo_git_head"],
}
context = tmp_path / "context"
context.mkdir()
repo_root = tmp_path / "repo"
repo_root.mkdir()
output = tmp_path / "writable-config-build-push-receipt.json"
docker_binary, docker_host, docker_config = docker_authority(tmp_path)
docker_config.chmod(0o777)
docker_calls: list[list[str]] = []
monkeypatch.setattr(package, "validate_prepared_image_context", lambda _context: (image_input, build_args))
monkeypatch.setattr(package, "validate_reviewed_context_source", lambda *_args: None)
monkeypatch.setattr(
package.subprocess,
"run",
lambda arguments, **_kwargs: docker_calls.append(arguments),
)
with pytest.raises(package.PackageError, match="Docker config directory"):
package.build_and_push_staging_image(
repo_root,
context,
docker_binary=docker_binary,
docker_host=docker_host,
docker_config=docker_config,
output_receipt=output,
execute_staging_push=True,
)
assert docker_calls == []
assert not output.exists()
def test_docker_authority_rejects_nonsticky_writable_ancestor(tmp_path: Path) -> None:
docker_binary, docker_host, _docker_config = docker_authority(tmp_path)
shared = tmp_path / "shared"
shared.mkdir(mode=0o777)
shared.chmod(0o777)
operator = shared / "operator"
operator.mkdir(mode=0o700)
docker_config = operator / "docker-config"
docker_config.mkdir(mode=0o700)
with pytest.raises(package.PackageError, match="trusted ancestors"):
package._validate_docker_authority(docker_binary, docker_host, docker_config)
def test_docker_authority_accepts_sticky_root_with_private_operator_directory() -> None:
sticky_root = Path("/private/tmp") if sys.platform == "darwin" else Path("/tmp")
operator = Path(tempfile.mkdtemp(prefix="teleo-docker-authority-", dir=sticky_root))
operator.chmod(0o700)
try:
docker_binary = operator / "docker"
docker_binary.write_text("", encoding="utf-8")
docker_binary.chmod(0o755)
docker_config = operator / "docker-config"
docker_config.mkdir(mode=0o700)
package._validate_docker_authority(
docker_binary,
"unix:///tmp/livingip-docker.sock",
docker_config,
)
with package._DockerCacheLock(docker_config) as authority:
authority.revalidate(docker_config)
subprocess_config, pass_fds = authority.subprocess_config_authority()
assert subprocess_config == "."
assert len(pass_fds) == 1
finally:
shutil.rmtree(operator)
def test_run_docker_uses_retained_config_during_mid_subprocess_swap_restore(
tmp_path: Path,
) -> None:
docker_binary = tmp_path / "bin" / "docker"
docker_binary.parent.mkdir()
docker_binary.write_text(
"""#!/usr/bin/env python3
import sys
from pathlib import Path
config = Path(sys.argv[sys.argv.index("--config") + 1])
original = Path(sys.argv[-2])
retained = Path(sys.argv[-1])
original.rename(retained)
original.mkdir(mode=0o700)
(original / "marker").write_text("attacker", encoding="utf-8")
try:
print((config / "marker").read_text(encoding="utf-8"))
finally:
(original / "marker").unlink()
original.rmdir()
retained.rename(original)
""",
encoding="utf-8",
)
docker_binary.chmod(0o755)
docker_config = tmp_path / "docker-config"
docker_config.mkdir(mode=0o700)
(docker_config / "marker").write_text("trusted", encoding="utf-8")
retained_config = tmp_path / "retained-docker-config"
result = package._run_docker(
docker_binary,
"unix:///tmp/livingip-docker.sock",
docker_config,
["probe", str(docker_config), str(retained_config)],
check=True,
timeout=30,
)
assert result.stdout == b"trusted\n"
assert (docker_config / "marker").read_text(encoding="utf-8") == "trusted"
assert not retained_config.exists()
def test_build_push_rejects_replaced_docker_config_before_mutation(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
image_input = image_input_fixture(tmp_path)
build_args = {
"ARTIFACT_SHA256": image_input["runtime"]["artifact_sha256"],
"IDENTITY_SHA256": image_input["identity"]["bundle_sha256"],
"INPUT_SHA256": image_input["input_sha256"],
"TELEO_REVISION": image_input["runtime"]["teleo_git_head"],
}
context = tmp_path / "context"
context.mkdir()
repo_root = tmp_path / "repo"
repo_root.mkdir()
output = tmp_path / "replaced-config-build-push-receipt.json"
docker_binary, docker_host, docker_config = docker_authority(tmp_path)
retained_config = tmp_path / "retained-docker-config"
docker_calls: list[list[str]] = []
monkeypatch.setattr(package, "validate_prepared_image_context", lambda _context: (image_input, build_args))
monkeypatch.setattr(package, "validate_reviewed_context_source", lambda *_args: None)
def replace_config_before_build(*_args: object, **_kwargs: object) -> None:
docker_config.rename(retained_config)
docker_config.mkdir(mode=0o700)
return None
monkeypatch.setattr(package, "_local_tag_config_id", replace_config_before_build)
monkeypatch.setattr(
package.subprocess,
"run",
lambda arguments, **_kwargs: docker_calls.append(arguments),
)
with pytest.raises(package.PackageError, match="staging image build/push failed") as raised:
package.build_and_push_staging_image(
repo_root,
context,
docker_binary=docker_binary,
docker_host=docker_host,
docker_config=docker_config,
output_receipt=output,
execute_staging_push=True,
)
assert "identity or posture drifted" in str(raised.value.__cause__)
assert docker_calls == []
assert not output.exists()
def test_build_push_rejects_receipt_parent_symlink_alias_before_docker(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
image_input = image_input_fixture(tmp_path)
build_args = {
"ARTIFACT_SHA256": image_input["runtime"]["artifact_sha256"],
"IDENTITY_SHA256": image_input["identity"]["bundle_sha256"],
"INPUT_SHA256": image_input["input_sha256"],
"TELEO_REVISION": image_input["runtime"]["teleo_git_head"],
}
context = tmp_path / "context"
context.mkdir()
(context / "receipts").mkdir()
alias = tmp_path / "context-alias"
alias.symlink_to(context, target_is_directory=True)
output = alias / "receipts" / "receipt.json"
repo_root = tmp_path / "repo"
repo_root.mkdir()
docker_binary, docker_host, docker_config = docker_authority(tmp_path)
docker_calls: list[list[str]] = []
monkeypatch.setattr(package, "validate_prepared_image_context", lambda _context: (image_input, build_args))
monkeypatch.setattr(package, "validate_reviewed_context_source", lambda *_args: None)
monkeypatch.setattr(
package.subprocess,
"run",
lambda arguments, **_kwargs: docker_calls.append(arguments),
)
with pytest.raises(package.PackageError, match="symbolic link"):
package.build_and_push_staging_image(
repo_root,
context,
docker_binary=docker_binary,
docker_host=docker_host,
docker_config=docker_config,
output_receipt=output,
execute_staging_push=True,
)
assert docker_calls == []
assert not output.exists()
def test_build_push_rechecks_receipt_context_separation_at_publication(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
image_input = image_input_fixture(tmp_path)
build_args = {
"ARTIFACT_SHA256": image_input["runtime"]["artifact_sha256"],
"IDENTITY_SHA256": image_input["identity"]["bundle_sha256"],
"INPUT_SHA256": image_input["input_sha256"],
"TELEO_REVISION": image_input["runtime"]["teleo_git_head"],
}
context = tmp_path / "context"
context.mkdir()
(context / "receipts").mkdir()
repo_root = tmp_path / "repo"
repo_root.mkdir()
output_parent = tmp_path / "receipts"
output_parent.mkdir()
retained_parent = tmp_path / "retained-receipts"
output = output_parent / "receipt.json"
docker_binary, docker_host, docker_config = docker_authority(tmp_path)
fake_run, state, calls = mocked_build_push_docker(image_input)
original_publish = package.publish_build_push_receipt
monkeypatch.setattr(package, "validate_prepared_image_context", lambda _context: (image_input, build_args))
monkeypatch.setattr(package, "validate_reviewed_context_source", lambda *_args: None)
monkeypatch.setattr(package.subprocess, "run", fake_run)
def alias_parent_before_publish(*args: object, **kwargs: object) -> None:
output_parent.rename(retained_parent)
output_parent.symlink_to(context / "receipts", target_is_directory=True)
original_publish(*args, **kwargs)
monkeypatch.setattr(package, "publish_build_push_receipt", alias_parent_before_publish)
with pytest.raises(package.PackageError, match="symbolic link"):
package.build_and_push_staging_image(
repo_root,
context,
docker_binary=docker_binary,
docker_host=docker_host,
docker_config=docker_config,
output_receipt=output,
execute_staging_push=True,
)
assert state == {"build": True, "candidate": True, "digests": True, "pushes": 1}
assert calls
assert not output.exists()
assert not (context / "receipts" / "receipt.json").exists()
@pytest.mark.parametrize( @pytest.mark.parametrize(
"references", "references",
[ [
@ -1437,6 +1709,41 @@ def test_build_push_receipt_publication_rolls_back_failed_parent_fsync(
assert retained[0].stat().st_ino == output.stat().st_ino assert retained[0].stat().st_ino == output.stat().st_ino
def test_build_push_receipt_detects_parent_replacement_after_publication(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
image_input = image_input_fixture(tmp_path)
receipt = build_push_receipt_fixture(image_input)
context = tmp_path / "prepared-context"
context.mkdir()
output_parent = tmp_path / "operator-receipts"
output_parent.mkdir()
retained_parent = tmp_path / "retained-operator-receipts"
output = output_parent / "receipt.json"
original_fsync = package._fsync_directory_descriptor
def replace_parent_after_fsync(descriptor: int) -> None:
original_fsync(descriptor)
output_parent.rename(retained_parent)
output_parent.mkdir(mode=0o700)
monkeypatch.setattr(package, "_fsync_directory_descriptor", replace_parent_after_fsync)
with pytest.raises(package.PackageError, match="parent identity drifted"):
package.publish_build_push_receipt(
output,
receipt,
image_input=image_input,
outside_context=context,
)
assert not output.exists()
retained_output = retained_parent / output.name
assert retained_output.exists()
assert retained_output.stat().st_nlink == 2
def test_release_cross_binds_receipt_registry_and_independent_inspection(tmp_path: Path) -> None: def test_release_cross_binds_receipt_registry_and_independent_inspection(tmp_path: Path) -> None:
image_input = image_input_fixture(tmp_path) image_input = image_input_fixture(tmp_path)
reference = f"{package.IMAGE_REPOSITORY}@sha256:{IMAGE_DIGEST}" reference = f"{package.IMAGE_REPOSITORY}@sha256:{IMAGE_DIGEST}"
@ -1818,7 +2125,7 @@ def test_registry_inspection_absent_pull_dependent_retains_new_reference(
image_input = image_input_fixture(tmp_path) image_input = image_input_fixture(tmp_path)
reference = f"{package.IMAGE_REPOSITORY}@sha256:{IMAGE_DIGEST}" reference = f"{package.IMAGE_REPOSITORY}@sha256:{IMAGE_DIGEST}"
docker_binary, docker_host, docker_config = docker_authority(tmp_path) docker_binary, docker_host, docker_config = docker_authority(tmp_path)
prefix = [str(docker_binary), "--host", docker_host, "--config", str(docker_config)] prefix = [str(docker_binary), "--host", docker_host, "--config"]
calls: list[list[str]] = [] calls: list[list[str]] = []
environments: list[dict[str, str]] = [] environments: list[dict[str, str]] = []
state = {"present": False, "dependent": False, "inspection_calls": 0} state = {"present": False, "dependent": False, "inspection_calls": 0}
@ -1829,7 +2136,10 @@ def test_registry_inspection_absent_pull_dependent_retains_new_reference(
def fake_run(arguments: list[str], **kwargs): def fake_run(arguments: list[str], **kwargs):
calls.append(arguments) calls.append(arguments)
environments.append(kwargs["env"]) environments.append(kwargs["env"])
assert arguments[:5] == prefix assert arguments[:4] == prefix
assert arguments[4] == "."
assert len(kwargs["pass_fds"]) == 1
assert kwargs["preexec_fn"] is not None
operation = arguments[5:] operation = arguments[5:]
if operation[:2] == ["image", "ls"]: if operation[:2] == ["image", "ls"]:
return subprocess.CompletedProcess( return subprocess.CompletedProcess(