teleo-infrastructure/ops/run_gcp_leoclean_nosend_iap.py

1279 lines
52 KiB
Python

#!/usr/bin/env python3
"""Build and optionally dispatch one fixed-target leoclean no-send IAP request."""
from __future__ import annotations
import argparse
import gzip
import hashlib
import io
import json
import os
import re
import shlex
import stat
import subprocess
import sys
import tarfile
import tempfile
import textwrap
import zipfile
from collections.abc import Callable, Mapping
from pathlib import Path
from typing import Any
CONTRACT_ROOT = Path(__file__).resolve().parents[1]
if str(CONTRACT_ROOT) not in sys.path:
sys.path.insert(0, str(CONTRACT_ROOT))
from ops import gcp_leoclean_nosend_package as package # noqa: E402
from ops import install_gcp_leoclean_nosend_release as installer # noqa: E402
SCHEMA = "livingip.leocleanNoSendIapRequest.v1"
PLAN_SCHEMA = "livingip.leocleanNoSendIapPlan.v1"
REMOTE_RESULT_SCHEMA = "livingip.leocleanNoSendIapRemoteResult.v1"
RESULT_RECEIPT_SCHEMA = "livingip.leocleanNoSendIapResultReceipt.v1"
EXECUTION_SCHEMA = "livingip.leocleanNoSendIapExecution.v1"
PROJECT = package.PROJECT
ZONE = package.ZONE
INSTANCE = package.INSTANCE
SERVICE = package.SERVICE
SERVICE_ACCOUNT = package.SERVICE_ACCOUNT
GCLOUD = "/opt/homebrew/bin/gcloud"
ORIGIN_URL = "git@github.com:living-ip/teleo-infrastructure.git"
REMOTE_UPLOAD_ROOT = "/tmp"
REMOTE_HELPER_SOURCE = "ops/gcp_leoclean_nosend_iap_remote.py"
KEY_LIFETIME = "5m"
MAX_ARCHIVE_BYTES = 8 * 1024 * 1024
MAX_MEMBER_BYTES = 2 * 1024 * 1024
MAX_COMMAND_OUTPUT_BYTES = 256 * 1024
REQUEST_ID = re.compile(r"^iap-[a-z0-9]{12,32}$")
HEX_40 = re.compile(r"^[0-9a-f]{40}$")
HEX_64 = re.compile(r"^[0-9a-f]{64}$")
OPERATIONS = ("plan", "install", "verify", "rollback")
SOURCE_MEMBERS = (
"ops/gcp_leoclean_nosend_package.py",
"ops/install_gcp_leoclean_nosend_release.py",
"ops/leoclean_nosend_service_control.py",
REMOTE_HELPER_SOURCE,
"ops/run_gcp_leoclean_nosend_iap.py",
"scripts/leo_behavior_manifest.py",
"scripts/leo_identity_manifest.py",
)
RELEASE_ARTIFACT_MEMBERS = (
"evidence/oci-smoke.json",
"evidence/publish-outcome.json",
"evidence/runtime-receipt.json",
f"final/release-v3/{SERVICE}",
"final/release-v3/release.json",
"receipts/build-push-receipt.json",
)
FORBIDDEN_OUTPUT_MARKERS = (
"authorization:",
"access_token",
"password",
"private_key",
"telegram",
"77.42.65.182",
"teleo-prod-1",
)
BOOTSTRAP_CODE = textwrap.dedent(
r"""
import fcntl, hashlib, io, json, os, pwd, re, stat, sys, tarfile
mode, operation, request_id, runner_revision, release_revision, archive_hash, helper_hash = sys.argv[1:]
fail = lambda: (_ for _ in ()).throw(RuntimeError("bootstrap_failed"))
if mode not in {"start", "cleanup"} or operation not in {"install", "verify", "rollback"}: fail()
if not re.fullmatch(r"iap-[a-z0-9]{12,32}", request_id): fail()
if not re.fullmatch(r"[0-9a-f]{40}", runner_revision) or not re.fullmatch(r"[0-9a-f]{40}", release_revision): fail()
if not re.fullmatch(r"[0-9a-f]{64}", archive_hash) or not re.fullmatch(r"[0-9a-f]{64}", helper_hash): fail()
caller = os.environ.get("SUDO_USER", "")
if not caller or caller == "root": fail()
caller_uid = pwd.getpwnam(caller).pw_uid
upload = f"/tmp/livingip-leoclean-nosend-{request_id}.tar.gz"
stage_root = "/run/livingip-leoclean-nosend-iap-bootstrap"
stage = f"{stage_root}/{request_id}"
lock_path = "/run/leoclean-gcp-nosend-iap-bootstrap.lock"
def stable(path, uid, expected, limit):
fd = os.open(path, os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_CLOEXEC", 0))
try:
before = os.fstat(fd); data = b""
if not stat.S_ISREG(before.st_mode) or before.st_uid != uid or stat.S_IMODE(before.st_mode) != 0o600 or before.st_nlink != 1 or before.st_size > limit: fail()
while len(data) <= limit:
chunk = os.read(fd, min(65536, limit + 1 - len(data)))
if not chunk: break
data += chunk
after = os.fstat(fd); named = os.stat(path, follow_symlinks=False)
if len(data) != after.st_size or (before.st_dev, before.st_ino, before.st_mode, before.st_nlink, before.st_size, before.st_mtime_ns) != (after.st_dev, after.st_ino, after.st_mode, after.st_nlink, after.st_size, after.st_mtime_ns): fail()
if (after.st_dev, after.st_ino, after.st_mode, after.st_nlink) != (named.st_dev, named.st_ino, named.st_mode, named.st_nlink): fail()
if hashlib.sha256(data).hexdigest() != expected: fail()
return data, (after.st_dev, after.st_ino)
finally: os.close(fd)
def unlink_exact(path, identity):
observed = os.stat(path, follow_symlinks=False)
if not stat.S_ISREG(observed.st_mode) or (observed.st_dev, observed.st_ino) != identity: fail()
os.unlink(path)
def safe_identity(path, uid, limit):
fd = os.open(path, os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_CLOEXEC", 0))
try:
observed = os.fstat(fd); named = os.stat(path, follow_symlinks=False)
if not stat.S_ISREG(observed.st_mode) or observed.st_uid != uid or stat.S_IMODE(observed.st_mode) != 0o600 or observed.st_nlink != 1 or observed.st_size > limit: fail()
if (observed.st_dev, observed.st_ino, observed.st_mode, observed.st_nlink) != (named.st_dev, named.st_ino, named.st_mode, named.st_nlink): fail()
return (observed.st_dev, observed.st_ino)
finally: os.close(fd)
def write_exact(path, data):
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_NOFOLLOW", 0), 0o600)
with os.fdopen(fd, "wb") as handle: handle.write(data); handle.flush(); os.fsync(handle.fileno())
lock_fd = os.open(lock_path, os.O_RDWR | os.O_CREAT | getattr(os, "O_NOFOLLOW", 0), 0o600)
try:
lock_state = os.fstat(lock_fd)
if lock_state.st_uid != 0 or stat.S_IMODE(lock_state.st_mode) != 0o600 or not stat.S_ISREG(lock_state.st_mode): fail()
fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
if mode == "cleanup":
try:
identity = safe_identity(upload, caller_uid, 8 * 1024 * 1024)
unlink_exact(upload, identity)
except FileNotFoundError: pass
if os.path.isdir(stage) and not os.path.islink(stage):
state = os.stat(stage, follow_symlinks=False)
if state.st_uid != 0 or stat.S_IMODE(state.st_mode) != 0o700: fail()
for name in os.listdir(stage):
path = f"{stage}/{name}"
expected = archive_hash if name == "request.tar.gz" else helper_hash if name == "remote-helper.py" else None
if expected is None: fail()
_, identity = stable(path, 0, expected, 8 * 1024 * 1024)
unlink_exact(path, identity)
os.rmdir(stage)
print(json.dumps({"schema":"livingip.leocleanNoSendIapCleanup.v1","status":"pass","request_id":request_id}, sort_keys=True))
raise SystemExit(0)
archive, upload_identity = stable(upload, caller_uid, archive_hash, 8 * 1024 * 1024)
if not os.path.isdir(stage_root):
os.mkdir(stage_root, 0o700); os.chown(stage_root, 0, 0)
root_state = os.stat(stage_root, follow_symlinks=False)
if root_state.st_uid != 0 or stat.S_IMODE(root_state.st_mode) != 0o700 or not stat.S_ISDIR(root_state.st_mode): fail()
os.mkdir(stage, 0o700); os.chown(stage, 0, 0)
write_exact(f"{stage}/request.tar.gz", archive)
with tarfile.open(fileobj=io.BytesIO(archive), mode="r:gz") as request:
matches = [member for member in request.getmembers() if member.name == "ops/gcp_leoclean_nosend_iap_remote.py"]
if len(matches) != 1 or not matches[0].isfile() or matches[0].size > 2 * 1024 * 1024: fail()
stream = request.extractfile(matches[0]); helper = stream.read() if stream else b""
if hashlib.sha256(helper).hexdigest() != helper_hash: fail()
write_exact(f"{stage}/remote-helper.py", helper)
unlink_exact(upload, upload_identity)
os.execve("/usr/bin/python3", ["/usr/bin/python3", "-I", f"{stage}/remote-helper.py", operation, request_id, runner_revision, release_revision, archive_hash, helper_hash], {"PATH":"/usr/bin:/bin:/usr/sbin:/sbin", "SUDO_USER":caller})
finally: os.close(lock_fd)
"""
).strip()
Runner = Callable[..., subprocess.CompletedProcess[bytes]]
class IapRequestError(RuntimeError):
"""The request could not be proven safe enough to dispatch."""
def _require(condition: bool, message: str) -> None:
if not condition:
raise IapRequestError(message)
def _sha256_bytes(value: bytes) -> str:
return hashlib.sha256(value).hexdigest()
def _canonical_json(value: Mapping[str, Any]) -> bytes:
try:
return (json.dumps(value, allow_nan=False, indent=2, sort_keys=True) + "\n").encode("utf-8")
except (TypeError, ValueError) as exc:
raise IapRequestError("request manifest could not be encoded") from exc
def _strict_json(raw: bytes, label: str) -> dict[str, Any]:
def reject_duplicates(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
value: dict[str, Any] = {}
for key, item in pairs:
if key in value:
raise ValueError("duplicate key")
value[key] = item
return value
try:
value = json.loads(
raw.decode("utf-8", "strict"),
object_pairs_hook=reject_duplicates,
parse_constant=lambda _value: (_ for _ in ()).throw(ValueError("constant")),
)
except (UnicodeError, ValueError, json.JSONDecodeError) as exc:
raise IapRequestError(f"{label} was invalid JSON") from exc
_require(isinstance(value, dict), f"{label} was not a JSON object")
return value
def _run_bounded(
argv: list[str],
*,
runner: Runner,
label: str,
input_bytes: bytes | None = None,
environment: Mapping[str, str] | None = None,
) -> subprocess.CompletedProcess[bytes]:
try:
completed = runner(
argv,
check=False,
stdin=subprocess.DEVNULL if input_bytes is None else None,
input=input_bytes,
capture_output=True,
timeout=300,
env=dict(environment or {"PATH": "/usr/bin:/bin:/usr/sbin:/sbin:/opt/homebrew/bin"}),
)
except (OSError, subprocess.SubprocessError) as exc:
raise IapRequestError(f"{label} failed") from exc
_require(
len(completed.stdout) <= MAX_COMMAND_OUTPUT_BYTES and len(completed.stderr) <= MAX_COMMAND_OUTPUT_BYTES,
f"{label} output exceeded the safety limit",
)
_require(completed.returncode == 0, f"{label} failed")
return completed
def _validate_owned_directory(path: Path, label: str) -> Path:
_require(path.is_absolute() and not path.is_symlink(), f"{label} was unsafe")
resolved = path.resolve()
observed = resolved.stat(follow_symlinks=False)
_require(
stat.S_ISDIR(observed.st_mode) and observed.st_uid == os.geteuid() and observed.st_mode & 0o022 == 0,
f"{label} was unsafe",
)
return resolved
def gcloud_environment(*, probe_runner: Runner = subprocess.run) -> dict[str, str]:
home_value = os.environ.get("HOME", "")
_require(home_value, "HOME was not configured")
home = _validate_owned_directory(Path(home_value), "HOME")
config_value = os.environ.get("CLOUDSDK_CONFIG") or str(home / ".config/gcloud")
config = _validate_owned_directory(Path(config_value), "Cloud SDK configuration")
candidates = [os.environ.get("CLOUDSDK_PYTHON"), str(CONTRACT_ROOT / ".venv/bin/python3")]
candidates.append(sys.executable)
selected: Path | None = None
for value in candidates:
if not value or not Path(value).is_absolute():
continue
try:
candidate = Path(value).resolve(strict=True)
observed = candidate.stat(follow_symlinks=False)
except OSError:
continue
if not stat.S_ISREG(observed.st_mode) or not os.access(candidate, os.X_OK) or observed.st_mode & 0o022:
continue
try:
probe = probe_runner(
[str(candidate), "--version"],
check=False,
stdin=subprocess.DEVNULL,
capture_output=True,
timeout=10,
env={"PATH": "/usr/bin:/bin"},
)
except (OSError, subprocess.SubprocessError):
continue
output = (probe.stdout + probe.stderr).decode("ascii", "ignore")
match = re.search(r"Python ([0-9]+)\.([0-9]+)(?:\.[0-9]+)?", output)
if probe.returncode == 0 and match and (int(match.group(1)), int(match.group(2))) >= (3, 10):
selected = candidate
break
_require(selected is not None, "no compatible absolute Cloud SDK Python was available")
return {
"PATH": "/usr/bin:/bin:/usr/sbin:/sbin:/opt/homebrew/bin",
"HOME": str(home),
"CLOUDSDK_CONFIG": str(config),
"CLOUDSDK_PYTHON": str(selected),
}
def _git_output(arguments: list[str], *, runner: Runner) -> bytes:
completed = _run_bounded(["/usr/bin/git", "-C", str(CONTRACT_ROOT), *arguments], runner=runner, label="Git check")
return completed.stdout
def validate_clean_revision(runner_revision: str, *, runner: Runner = subprocess.run) -> None:
_require(HEX_40.fullmatch(runner_revision) is not None, "runner revision must be one full lowercase Git commit")
top = _git_output(["rev-parse", "--show-toplevel"], runner=runner).decode("utf-8", "strict").strip()
head = _git_output(["rev-parse", "--verify", "HEAD^{commit}"], runner=runner).decode("ascii", "strict").strip()
dirty = _git_output(["status", "--porcelain=v1", "-z", "--untracked-files=all"], runner=runner)
origin_main = (
_git_output(
["rev-parse", "--verify", "refs/remotes/origin/main^{commit}"],
runner=runner,
)
.decode("ascii", "strict")
.strip()
)
_require(Path(top).resolve() == CONTRACT_ROOT, "source checkout was not the contract repository")
_require(head == runner_revision, "source checkout differed from the requested runner revision")
_require(origin_main == runner_revision, "runner revision was not exact local origin/main")
_require(not dirty, "source checkout was dirty")
def validate_remote_main(runner_revision: str, *, runner: Runner = subprocess.run) -> None:
_require(HEX_40.fullmatch(runner_revision) is not None, "runner revision was invalid")
origin = _git_output(["remote", "get-url", "origin"], runner=runner).decode("utf-8", "strict").strip()
_require(origin == ORIGIN_URL, "origin remote was not the fixed LivingIP SSH repository")
readback = _git_output(["ls-remote", "--exit-code", "origin", "refs/heads/main"], runner=runner)
try:
line = readback.decode("ascii", "strict").strip()
except UnicodeError as exc:
raise IapRequestError("remote main readback was invalid") from exc
_require(line == f"{runner_revision}\trefs/heads/main", "runner revision was not exact live origin/main")
def _read_stable_file(path: Path, *, root: Path, max_bytes: int = MAX_MEMBER_BYTES) -> tuple[bytes, int]:
_require(path.is_absolute(), "request member path was not absolute")
try:
relative = path.relative_to(root)
except ValueError as exc:
raise IapRequestError("request member escaped its fixed root") from exc
_require(not any(part in {"", ".", ".."} for part in relative.parts), "request member path was unsafe")
flags = os.O_RDONLY
if hasattr(os, "O_CLOEXEC"):
flags |= os.O_CLOEXEC
if hasattr(os, "O_NOFOLLOW"):
flags |= os.O_NOFOLLOW
try:
descriptor = os.open(path, flags)
except OSError as exc:
raise IapRequestError("request member was missing or unsafe") from exc
try:
before = os.fstat(descriptor)
_require(
stat.S_ISREG(before.st_mode) and before.st_nlink == 1 and before.st_size <= max_bytes,
"request member was not a bounded regular file",
)
content = b""
while len(content) <= max_bytes:
chunk = os.read(descriptor, min(65536, max_bytes + 1 - len(content)))
if not chunk:
break
content += chunk
after = os.fstat(descriptor)
named = path.stat(follow_symlinks=False)
identity = (before.st_dev, before.st_ino, before.st_mode, before.st_nlink, before.st_size, before.st_mtime_ns)
_require(
len(content) == after.st_size
and identity
== (after.st_dev, after.st_ino, after.st_mode, after.st_nlink, after.st_size, after.st_mtime_ns)
and (after.st_dev, after.st_ino, after.st_mode, after.st_nlink)
== (named.st_dev, named.st_ino, named.st_mode, named.st_nlink),
"request member changed while it was read",
)
return content, stat.S_IMODE(after.st_mode)
except OSError as exc:
raise IapRequestError("request member could not be read") from exc
finally:
os.close(descriptor)
def _validate_expected_bindings(
*,
expected_artifact_sha256: str,
expected_release_sha256: str,
expected_image_digest: str,
expected_image_config_digest: str,
) -> dict[str, str]:
_require(HEX_64.fullmatch(expected_artifact_sha256) is not None, "expected artifact hash was invalid")
_require(HEX_64.fullmatch(expected_release_sha256) is not None, "expected release hash was invalid")
_require(
re.fullmatch(r"sha256:[0-9a-f]{64}", expected_image_digest) is not None,
"expected image manifest digest was invalid",
)
_require(
re.fullmatch(r"sha256:[0-9a-f]{64}", expected_image_config_digest) is not None,
"expected image config digest was invalid",
)
return {
"release_artifact_sha256": expected_artifact_sha256,
"release_sha256": expected_release_sha256,
"image_digest": expected_image_digest,
"image_config_digest": expected_image_config_digest,
}
def validate_release(
bundle: Path,
release_revision: str,
*,
expected_release_sha256: str,
expected_image_digest: str,
expected_image_config_digest: str,
) -> tuple[dict[str, Any], bytes]:
try:
release, unit = installer.validate_release_bundle(bundle)
except (installer.InstallError, package.PackageError) as exc:
raise IapRequestError("finalized release bundle did not validate") from exc
image = release.get("image")
runtime = release.get("image_input", {}).get("runtime")
_require(
isinstance(image, dict) and package.IMAGE_REFERENCE.fullmatch(str(image.get("reference"))),
"image was not immutable",
)
_require(
isinstance(runtime, dict) and runtime.get("teleo_git_head") == release_revision,
"release differed from release revision",
)
_require(
release.get("release_sha256") == expected_release_sha256
and image.get("digest") == expected_image_digest
and image.get("config_digest") == expected_image_config_digest
and image.get("reference") == f"{package.IMAGE_REPOSITORY}@{expected_image_digest}",
"release differed from the independently supplied expected binding",
)
return release, unit
def validate_release_artifact(
artifact: Path,
*,
bundle: Path,
release: Mapping[str, Any],
expected_artifact_sha256: str,
) -> tuple[dict[str, Any], bytes]:
raw, mode = _read_stable_file(artifact.absolute(), root=artifact.absolute().parent, max_bytes=MAX_ARCHIVE_BYTES)
_require(mode & 0o022 == 0, "release artifact archive was writable by group or other")
_require(
_sha256_bytes(raw) == expected_artifact_sha256,
"release artifact differed from the independently supplied expected hash",
)
try:
archive = zipfile.ZipFile(io.BytesIO(raw), mode="r")
except (OSError, zipfile.BadZipFile) as exc:
raise IapRequestError("release artifact archive was invalid") from exc
with archive:
infos = archive.infolist()
names = [info.filename for info in infos]
_require(
names == list(RELEASE_ARTIFACT_MEMBERS) or sorted(names) == sorted(RELEASE_ARTIFACT_MEMBERS),
"release artifact member allowlist was not exact",
)
_require(len(names) == len(set(names)), "release artifact contained duplicate members")
members: list[dict[str, Any]] = []
payloads: dict[str, bytes] = {}
for info in infos:
path = Path(info.filename)
unix_mode = (info.external_attr >> 16) & 0o177777
_require(
info.filename
and not info.filename.startswith("/")
and all(part not in {"", ".", ".."} for part in path.parts)
and not info.is_dir()
and info.file_size <= MAX_MEMBER_BYTES
and (unix_mode == 0 or stat.S_ISREG(unix_mode)),
"release artifact contained an unsafe member",
)
content = archive.read(info)
_require(len(content) == info.file_size, "release artifact member size differed")
payloads[info.filename] = content
members.append(
{
"path": info.filename,
"sha256": _sha256_bytes(content),
"size": len(content),
}
)
bundle_root = bundle.resolve()
release_bytes, _release_mode = _read_stable_file(bundle_root / "release.json", root=bundle_root)
unit_bytes, _unit_mode = _read_stable_file(bundle_root / SERVICE, root=bundle_root)
_require(
payloads["final/release-v3/release.json"] == release_bytes
and payloads[f"final/release-v3/{SERVICE}"] == unit_bytes,
"release bundle differed from its original artifact archive",
)
build_receipt = _strict_json(payloads["receipts/build-push-receipt.json"], "artifact build/push receipt")
publish_outcome = _strict_json(payloads["evidence/publish-outcome.json"], "artifact publish outcome")
_require(build_receipt == release["build_push_receipt"], "artifact build/push receipt differed from release")
try:
package.validate_publish_outcome(publish_outcome, image_input=dict(release["image_input"]))
except package.PackageError as exc:
raise IapRequestError("artifact publish outcome did not validate") from exc
_require(publish_outcome.get("status") == "pass", "artifact publish outcome did not pass")
return (
{
"archive_sha256": _sha256_bytes(raw),
"archive_size": len(raw),
"members": sorted(members, key=lambda item: str(item["path"])),
},
raw,
)
def _member_payloads(bundle: Path, unit: bytes, release_artifact: bytes) -> dict[str, tuple[bytes, int]]:
payloads: dict[str, tuple[bytes, int]] = {}
for relative in SOURCE_MEMBERS:
payloads[relative] = _read_stable_file(CONTRACT_ROOT / relative, root=CONTRACT_ROOT)
release_bytes, release_mode = _read_stable_file(bundle.resolve() / "release.json", root=bundle.resolve())
_unit_bytes, unit_mode = _read_stable_file(bundle.resolve() / SERVICE, root=bundle.resolve())
_require(_unit_bytes == unit, "release unit changed after validation")
payloads["release/release.json"] = (release_bytes, release_mode)
payloads[f"release/{SERVICE}"] = (unit, unit_mode)
payloads["release-artifact.zip"] = (release_artifact, 0o600)
return payloads
def build_manifest(
*,
operation: str,
request_id: str,
runner_revision: str,
release_revision: str,
release: Mapping[str, Any],
payloads: Mapping[str, tuple[bytes, int]],
source_binding: Mapping[str, Any],
release_artifact: Mapping[str, Any],
) -> dict[str, Any]:
_require(operation in OPERATIONS, "operation was not allowlisted")
_require(REQUEST_ID.fullmatch(request_id) is not None, "request ID was invalid")
files = [
{
"path": relative,
"sha256": _sha256_bytes(content),
"size": len(content),
"mode": mode,
}
for relative, (content, mode) in sorted(payloads.items())
]
stable = {
"schema": SCHEMA,
"operation": operation,
"request_id": request_id,
"runner_revision": runner_revision,
"release_revision": release_revision,
"target": {
"project": PROJECT,
"zone": ZONE,
"instance": INSTANCE,
"service": SERVICE,
"service_account": SERVICE_ACCOUNT,
},
"release": {
"release_sha256": release["release_sha256"],
"image_reference": release["image"]["reference"],
"image_digest": release["image"]["digest"],
"image_config_digest": release["image"]["config_digest"],
},
"installer_source": dict(source_binding),
"release_artifact": dict(release_artifact),
"files": files,
}
return {**stable, "manifest_sha256": package.canonical_sha256(stable)}
def _tar_info(name: str, *, size: int, mode: int) -> tarfile.TarInfo:
_require(name and not name.startswith("/") and ".." not in Path(name).parts, "archive member path was unsafe")
info = tarfile.TarInfo(name)
info.size = size
info.mode = mode
info.uid = 0
info.gid = 0
info.uname = "root"
info.gname = "root"
info.mtime = 0
info.type = tarfile.REGTYPE
return info
def build_archive(
output: Path,
*,
manifest: Mapping[str, Any],
payloads: Mapping[str, tuple[bytes, int]],
) -> str:
_require(output.is_absolute(), "archive output path was not absolute")
parent = output.parent
parent_state = parent.stat(follow_symlinks=False)
_require(
stat.S_ISDIR(parent_state.st_mode)
and parent_state.st_uid == os.geteuid()
and parent_state.st_mode & 0o077 == 0,
"archive output directory was not private",
)
_require(not output.exists() and not output.is_symlink(), "archive output already existed")
manifest_bytes = _canonical_json(manifest)
members = {"request-manifest.json": (manifest_bytes, 0o600), **payloads}
temporary: Path | None = None
try:
descriptor, temporary_name = tempfile.mkstemp(prefix=".iap-request-", dir=parent)
temporary = Path(temporary_name)
os.fchmod(descriptor, 0o600)
with os.fdopen(descriptor, "wb", closefd=True) as raw:
with gzip.GzipFile(filename="", mode="wb", fileobj=raw, mtime=0) as compressed:
with tarfile.open(fileobj=compressed, mode="w", format=tarfile.PAX_FORMAT) as archive:
for relative, (content, mode) in sorted(members.items()):
archive.addfile(_tar_info(relative, size=len(content), mode=mode), io.BytesIO(content))
raw.flush()
os.fsync(raw.fileno())
_require(temporary.stat().st_size <= MAX_ARCHIVE_BYTES, "request archive exceeded the safety limit")
os.link(temporary, output, follow_symlinks=False)
temporary.unlink()
temporary = None
directory = os.open(parent, os.O_RDONLY)
try:
os.fsync(directory)
finally:
os.close(directory)
return package.sha256_file(output)
except OSError as exc:
raise IapRequestError("request archive could not be published") from exc
finally:
if temporary is not None:
try:
temporary.unlink()
except FileNotFoundError:
pass
def remote_names(request_id: str) -> tuple[str, str]:
_require(REQUEST_ID.fullmatch(request_id) is not None, "request ID was invalid")
return (
f"livingip-leoclean-nosend-{request_id}.tar.gz",
f"livingip-leoclean-nosend-{request_id}-ssh-key",
)
def build_gcloud_argv(
*,
operation: str,
request_id: str,
runner_revision: str,
release_revision: str,
archive: Path,
archive_sha256: str,
ssh_key: Path,
helper_sha256: str,
) -> tuple[list[str], list[str], list[str]]:
_require(operation in {"install", "verify", "rollback"}, "plan is local-only")
_require(REQUEST_ID.fullmatch(request_id) is not None, "request ID was invalid")
_require(HEX_40.fullmatch(runner_revision) is not None, "runner revision was invalid")
_require(HEX_40.fullmatch(release_revision) is not None, "release revision was invalid")
_require(HEX_64.fullmatch(archive_sha256) is not None, "archive hash was invalid")
_require(HEX_64.fullmatch(helper_sha256) is not None, "helper hash was invalid")
archive_name, key_name = remote_names(request_id)
_require(archive.name == archive_name and ssh_key.name == key_name, "request filenames were not exact")
key_flag = f"--ssh-key-file={ssh_key}"
scp = [
GCLOUD,
"compute",
"scp",
f"--project={PROJECT}",
f"--zone={ZONE}",
"--tunnel-through-iap",
f"--ssh-key-expire-after={KEY_LIFETIME}",
key_flag,
"--quiet",
str(archive),
f"{INSTANCE}:{REMOTE_UPLOAD_ROOT}/",
]
remote = [
"sudo",
"--",
"/usr/bin/python3",
"-I",
"-c",
BOOTSTRAP_CODE,
"start",
operation,
request_id,
runner_revision,
release_revision,
archive_sha256,
helper_sha256,
]
cleanup_remote = [*remote[:6], "cleanup", *remote[7:]]
ssh = [
GCLOUD,
"compute",
"ssh",
INSTANCE,
f"--project={PROJECT}",
f"--zone={ZONE}",
"--tunnel-through-iap",
f"--ssh-key-expire-after={KEY_LIFETIME}",
key_flag,
"--quiet",
f"--command={shlex.join(remote)}",
]
cleanup = [*ssh[:-1], f"--command={shlex.join(cleanup_remote)}"]
return scp, ssh, cleanup
def _action_receipts(commands: tuple[list[str], list[str], list[str]]) -> list[dict[str, str]]:
labels = ("upload_request", "execute_remote_request", "cleanup_failed_transfer")
return [
{
"label": label,
"argv_sha256": package.canonical_sha256({"argv": argv}),
}
for label, argv in zip(labels, commands, strict=True)
]
def prepare_request(
operation: str,
request_id: str,
runner_revision: str,
release_revision: str,
bundle: Path,
release_artifact_path: Path,
output_directory: Path,
expected_artifact_sha256: str,
expected_release_sha256: str,
expected_image_digest: str,
expected_image_config_digest: str,
*,
git_runner: Runner = subprocess.run,
) -> dict[str, Any]:
_require(operation in OPERATIONS, "operation was not allowlisted")
_require(REQUEST_ID.fullmatch(request_id) is not None, "request ID was invalid")
output_directory = output_directory.absolute()
output_state = output_directory.stat(follow_symlinks=False)
_require(
stat.S_ISDIR(output_state.st_mode)
and output_state.st_uid == os.geteuid()
and output_state.st_mode & 0o077 == 0,
"request output directory was not private",
)
expected = _validate_expected_bindings(
expected_artifact_sha256=expected_artifact_sha256,
expected_release_sha256=expected_release_sha256,
expected_image_digest=expected_image_digest,
expected_image_config_digest=expected_image_config_digest,
)
validate_clean_revision(runner_revision, runner=git_runner)
release, unit = validate_release(
bundle.absolute(),
release_revision,
expected_release_sha256=expected_release_sha256,
expected_image_digest=expected_image_digest,
expected_image_config_digest=expected_image_config_digest,
)
release_artifact, release_artifact_bytes = validate_release_artifact(
release_artifact_path.absolute(),
bundle=bundle.absolute(),
release=release,
expected_artifact_sha256=expected_artifact_sha256,
)
source_binding = installer.reviewed_source_binding()
_require(
source_binding.get("revision") == runner_revision, "installer source binding differed from runner revision"
)
payloads = _member_payloads(bundle.absolute(), unit, release_artifact_bytes)
manifest = build_manifest(
operation=operation,
request_id=request_id,
runner_revision=runner_revision,
release_revision=release_revision,
release=release,
payloads=payloads,
source_binding=source_binding,
release_artifact=release_artifact,
)
archive_name, key_name = remote_names(request_id)
archive = output_directory / archive_name
ssh_key = output_directory / key_name
_require(
not ssh_key.exists()
and not ssh_key.is_symlink()
and not Path(f"{ssh_key}.pub").exists()
and not Path(f"{ssh_key}.pub").is_symlink(),
"request-local SSH key path already existed",
)
helper_sha256 = _sha256_bytes(payloads[REMOTE_HELPER_SOURCE][0])
try:
archive_sha256 = build_archive(archive, manifest=manifest, payloads=payloads)
except Exception:
for created in (archive,):
try:
observed = created.stat(follow_symlinks=False)
if stat.S_ISREG(observed.st_mode) and observed.st_uid == os.geteuid() and observed.st_nlink == 1:
created.unlink()
except FileNotFoundError:
pass
raise
if operation == "plan":
actions: list[dict[str, str]] = []
else:
commands = build_gcloud_argv(
operation=operation,
request_id=request_id,
runner_revision=runner_revision,
release_revision=release_revision,
archive=archive,
archive_sha256=archive_sha256,
ssh_key=ssh_key,
helper_sha256=helper_sha256,
)
actions = _action_receipts(commands)
return {
"schema": PLAN_SCHEMA,
"status": "pass",
"mode": "dry_run",
"operation": operation,
"request_id": request_id,
"runner_revision": runner_revision,
"release_revision": release_revision,
"target": manifest["target"],
"release": manifest["release"],
"release_artifact": manifest["release_artifact"],
"expected": expected,
"request": {
"archive": str(archive),
"archive_sha256": archive_sha256,
"helper_sha256": helper_sha256,
"manifest_sha256": manifest["manifest_sha256"],
"ssh_key": str(ssh_key),
},
"actions": actions,
"authority": {
"gcp_mutation": operation in {"install", "rollback"},
"service_restart": operation in {"install", "rollback"},
"database_or_secret_provisioning": False,
"production_or_transport": False,
"vps": False,
},
}
def _cleanup_local_ssh_key(path: Path) -> None:
for candidate in (path, Path(f"{path}.pub")):
try:
observed = candidate.stat(follow_symlinks=False)
except FileNotFoundError:
continue
_require(
stat.S_ISREG(observed.st_mode) and observed.st_uid == os.geteuid() and observed.st_nlink == 1,
"request-local SSH key cleanup refused an unsafe path",
)
candidate.unlink()
def _validate_verification(value: Any, label: str) -> None:
_require(
isinstance(value, dict)
and set(value)
== {
"active_state",
"sub_state",
"main_pid",
"n_restarts",
"invocation_id",
"supervising_process",
"container_id",
"container_pid",
"health",
"validated_process_environment_fields",
"dropins",
"environment_files",
}
and value.get("active_state") == "active"
and value.get("sub_state") == "running"
and isinstance(value.get("main_pid"), int)
and value["main_pid"] > 1
and isinstance(value.get("n_restarts"), int)
and value["n_restarts"] >= 0
and re.fullmatch(r"[0-9a-f]{32}", str(value.get("invocation_id"))) is not None
and HEX_64.fullmatch(str(value.get("container_id"))) is not None
and isinstance(value.get("container_pid"), int)
and value["container_pid"] > 1
and value.get("health") == "healthy"
and value.get("dropins") == "absent"
and value.get("environment_files") == "absent",
f"{label} was invalid",
)
def _validate_database_summary(value: Any, request_id: str, expected_name: str) -> None:
expected_path = f"/var/lib/livingip/leoclean-nosend-iap/requests/{request_id}/{expected_name}"
_require(
isinstance(value, dict)
and set(value) == {"schema", "status", "receipt_sha256", "receipt_path", "proof"}
and value.get("schema") == "livingip.leocleanNoSendContainerDatabaseVerification.v1"
and value.get("status") == "pass"
and HEX_64.fullmatch(str(value.get("receipt_sha256"))) is not None
and value.get("receipt_path") == expected_path,
"database verification summary was invalid",
)
_require(
value.get("proof")
== {
"proposal_staging": "function_only",
"proposal_transaction": "rolled_back",
"durable_proposal_created": False,
"canonical_direct_writes": "denied",
"stage_direct_writes": "denied",
"role_escalation": "denied",
},
"database verification proof summary was invalid",
)
def validate_remote_result(plan: Mapping[str, Any], result: Mapping[str, Any]) -> None:
_require(
set(result)
== {
"schema",
"status",
"operation",
"request_id",
"runner_revision",
"release_revision",
"release_sha256",
"image_digest",
"image_config_digest",
"release_artifact_sha256",
"request_binding",
"service",
"database",
"authority",
},
"remote result fields were not exact",
)
operation = plan["operation"]
request_id = plan["request_id"]
request = plan["request"]
release = plan["release"]
_require(
result.get("schema") == REMOTE_RESULT_SCHEMA
and result.get("status") == "pass"
and result.get("operation") == operation
and result.get("request_id") == request_id
and result.get("runner_revision") == plan["runner_revision"]
and result.get("release_revision") == plan["release_revision"]
and result.get("release_sha256") == release["release_sha256"]
and result.get("image_digest") == release["image_digest"]
and result.get("image_config_digest") == release["image_config_digest"]
and result.get("release_artifact_sha256") == plan["release_artifact"]["archive_sha256"]
and result.get("request_binding")
== {
"archive_sha256": request["archive_sha256"],
"helper_sha256": request["helper_sha256"],
"manifest_sha256": request["manifest_sha256"],
}
and result.get("authority")
== {
"sending_enabled": False,
"production_traffic_changed": False,
"vps_touched": False,
"canonical_proposal_approved": False,
},
"remote result did not match the request",
)
service = result["service"]
database = result["database"]
if operation == "install":
_require(
isinstance(service, dict) and set(service) == {"name", "before_restart", "after_restart"},
"install service result was invalid",
)
_require(service["name"] == SERVICE, "install service target differed")
_validate_verification(service["before_restart"], "pre-restart verification")
_validate_verification(service["after_restart"], "post-restart verification")
before = service["before_restart"]
after = service["after_restart"]
_require(
before["main_pid"] != after["main_pid"]
and before["invocation_id"] != after["invocation_id"]
and before["container_id"] != after["container_id"],
"restart identities did not change",
)
_require(
isinstance(database, dict)
and set(database) == {"status", "before_restart", "after_restart"}
and database.get("status") == "pass",
"install database result was invalid",
)
_validate_database_summary(database["before_restart"], request_id, "database-before-restart.json")
_validate_database_summary(database["after_restart"], request_id, "database-after-restart.json")
elif operation == "verify":
_require(
isinstance(service, dict) and set(service) == {"name", "verification"} and service.get("name") == SERVICE,
"verify service result was invalid",
)
_validate_verification(service["verification"], "verify running release")
_validate_database_summary(database, request_id, "database-verify.json")
else:
_require(
isinstance(service, dict)
and set(service) == {"name", "restored_posture", "restored_release_sha256", "verification"}
and service.get("name") == SERVICE
and service.get("restored_posture") in {"active", "inactive", "absent"}
and database == {"status": "not_run_after_explicit_rollback"},
"rollback result was invalid",
)
if service["restored_posture"] == "active":
_require(
HEX_64.fullmatch(str(service["restored_release_sha256"])) is not None, "restored release was invalid"
)
_validate_verification(service["verification"], "restored active verification")
elif service["restored_posture"] == "inactive":
_require(
HEX_64.fullmatch(str(service["restored_release_sha256"])) is not None
and service["verification"] is None,
"restored inactive result was invalid",
)
else:
_require(
service["restored_release_sha256"] is None and service["verification"] is None,
"restored absent result was invalid",
)
def _write_result_receipt(plan: Mapping[str, Any], result: Mapping[str, Any]) -> tuple[Path, dict[str, Any]]:
archive = Path(str(plan["request"]["archive"]))
output = archive.parent / f"livingip-leoclean-nosend-result-{plan['request_id']}.json"
_require(not output.exists() and not output.is_symlink(), "local result receipt already existed")
stable = {
"schema": RESULT_RECEIPT_SCHEMA,
"status": "pass",
"operation": plan["operation"],
"request_id": plan["request_id"],
"runner_revision": plan["runner_revision"],
"release_revision": plan["release_revision"],
"request": {
"archive_sha256": plan["request"]["archive_sha256"],
"helper_sha256": plan["request"]["helper_sha256"],
"manifest_sha256": plan["request"]["manifest_sha256"],
},
"release": dict(plan["release"]),
"release_artifact": dict(plan["release_artifact"]),
"expected": dict(plan["expected"]),
"remote_result": dict(result),
}
receipt = {**stable, "receipt_sha256": package.canonical_sha256(stable)}
content = _canonical_json(receipt)
descriptor, temporary_name = tempfile.mkstemp(prefix=".iap-result-", dir=output.parent)
temporary: Path | None = Path(temporary_name)
try:
os.fchmod(descriptor, 0o600)
with os.fdopen(descriptor, "wb") as handle:
handle.write(content)
handle.flush()
os.fsync(handle.fileno())
os.link(temporary, output, follow_symlinks=False)
temporary.unlink()
temporary = None
directory = os.open(output.parent, os.O_RDONLY)
try:
os.fsync(directory)
finally:
os.close(directory)
except OSError as exc:
raise IapRequestError("local result receipt could not be published") from exc
finally:
try:
if temporary is not None:
temporary.unlink()
except FileNotFoundError:
pass
return output, receipt
def execute_plan(
plan: Mapping[str, Any],
*,
runner: Runner = subprocess.run,
git_runner: Runner = subprocess.run,
gcloud_env: Mapping[str, str] | None = None,
) -> dict[str, Any]:
_require(
set(plan)
== {
"schema",
"status",
"mode",
"operation",
"request_id",
"runner_revision",
"release_revision",
"target",
"release",
"release_artifact",
"expected",
"request",
"actions",
"authority",
}
and plan.get("schema") == PLAN_SCHEMA
and plan.get("status") == "pass"
and plan.get("mode") == "dry_run",
"request plan fields were not exact",
)
operation = plan.get("operation")
_require(operation in {"install", "verify", "rollback"}, "operation was not executable")
request_id = plan.get("request_id")
runner_revision = plan.get("runner_revision")
release_revision = plan.get("release_revision")
_require(isinstance(request_id, str) and REQUEST_ID.fullmatch(request_id) is not None, "request ID was invalid")
_require(
isinstance(runner_revision, str) and HEX_40.fullmatch(runner_revision) is not None,
"runner revision was invalid",
)
_require(
isinstance(release_revision, str) and HEX_40.fullmatch(release_revision) is not None,
"release revision was invalid",
)
_require(
plan.get("target")
== {
"project": PROJECT,
"zone": ZONE,
"instance": INSTANCE,
"service": SERVICE,
"service_account": SERVICE_ACCOUNT,
},
"request target was not exact",
)
expected = plan.get("expected")
_require(
isinstance(expected, dict)
and expected
== _validate_expected_bindings(
expected_artifact_sha256=str(expected.get("release_artifact_sha256")),
expected_release_sha256=str(expected.get("release_sha256")),
expected_image_digest=str(expected.get("image_digest")),
expected_image_config_digest=str(expected.get("image_config_digest")),
)
and plan.get("release")
== {
"release_sha256": expected["release_sha256"],
"image_reference": f"{package.IMAGE_REPOSITORY}@{expected['image_digest']}",
"image_digest": expected["image_digest"],
"image_config_digest": expected["image_config_digest"],
}
and isinstance(plan.get("release_artifact"), dict)
and plan["release_artifact"].get("archive_sha256") == expected["release_artifact_sha256"],
"request expected bindings were not exact",
)
request = plan.get("request")
_require(
isinstance(request, dict)
and set(request) == {"archive", "archive_sha256", "helper_sha256", "manifest_sha256", "ssh_key"},
"request files were invalid",
)
archive = Path(str(request["archive"]))
ssh_key = Path(str(request["ssh_key"]))
_require(archive.is_absolute() and ssh_key.is_absolute(), "request file path was not absolute")
archive_content, archive_mode = _read_stable_file(archive, root=archive.parent, max_bytes=MAX_ARCHIVE_BYTES)
_require(
archive_mode == 0o600
and _sha256_bytes(archive_content) == request.get("archive_sha256")
and HEX_64.fullmatch(str(request.get("helper_sha256"))) is not None
and HEX_64.fullmatch(str(request.get("manifest_sha256"))) is not None,
"request files differed from the dry-run plan",
)
_require(
not ssh_key.exists()
and not ssh_key.is_symlink()
and not Path(f"{ssh_key}.pub").exists()
and not Path(f"{ssh_key}.pub").is_symlink(),
"request-local SSH key path was already occupied",
)
expected_commands = build_gcloud_argv(
operation=str(operation),
request_id=request_id,
runner_revision=runner_revision,
release_revision=release_revision,
archive=archive,
archive_sha256=str(request["archive_sha256"]),
ssh_key=ssh_key,
helper_sha256=str(request["helper_sha256"]),
)
_require(
plan.get("actions") == _action_receipts(expected_commands), "request actions differed from the fixed IAP plan"
)
scp, ssh, cleanup = expected_commands
validate_remote_main(runner_revision, runner=git_runner)
environment = dict(gcloud_env or gcloud_environment())
try:
try:
_run_bounded(list(scp), runner=runner, label="IAP upload", environment=environment)
completed = _run_bounded(list(ssh), runner=runner, label="IAP remote operation", environment=environment)
except IapRequestError as exc:
try:
_run_bounded(
list(cleanup),
runner=runner,
label="IAP failed-transfer cleanup",
environment=environment,
)
except IapRequestError as cleanup_exc:
raise IapRequestError("IAP operation failed and remote upload cleanup was unproven") from cleanup_exc
raise exc
finally:
_cleanup_local_ssh_key(ssh_key)
result = _strict_json(completed.stdout, "remote result")
validate_remote_result(plan, result)
encoded = json.dumps(result, allow_nan=False, sort_keys=True).casefold()
_require(not any(marker in encoded for marker in FORBIDDEN_OUTPUT_MARKERS), "remote result was not sanitized")
receipt_path, receipt = _write_result_receipt(plan, result)
return {
"schema": EXECUTION_SCHEMA,
"status": "pass",
"operation": operation,
"request_id": request_id,
"runner_revision": runner_revision,
"release_revision": release_revision,
**expected,
"receipt_path": str(receipt_path),
"receipt_sha256": receipt["receipt_sha256"],
}
class _Parser(argparse.ArgumentParser):
def error(self, _message: str) -> None:
raise IapRequestError("invalid arguments")
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
parser = _Parser(description=__doc__, add_help=False)
parser.add_argument("--operation", required=True, choices=OPERATIONS)
parser.add_argument("--request-id", required=True)
parser.add_argument("--runner-revision", required=True)
parser.add_argument("--release-revision", required=True)
parser.add_argument("--bundle", required=True, type=Path)
parser.add_argument("--release-artifact", required=True, type=Path)
parser.add_argument("--output-directory", required=True, type=Path)
parser.add_argument("--expected-artifact-sha256", required=True)
parser.add_argument("--expected-release-sha256", required=True)
parser.add_argument("--expected-image-digest", required=True)
parser.add_argument("--expected-image-config-digest", required=True)
parser.add_argument("--execute", action="store_true")
return parser.parse_args(argv)
def main(argv: list[str] | None = None) -> int:
try:
args = parse_args(argv)
plan = prepare_request(
args.operation,
args.request_id,
args.runner_revision,
args.release_revision,
args.bundle,
args.release_artifact,
args.output_directory,
args.expected_artifact_sha256,
args.expected_release_sha256,
args.expected_image_digest,
args.expected_image_config_digest,
)
result = execute_plan(plan) if args.execute else plan
sys.stdout.write(json.dumps(result, allow_nan=False, separators=(",", ":"), sort_keys=True) + "\n")
return 0
except (IapRequestError, installer.InstallError, package.PackageError):
sys.stderr.write('{"error":"iap_request_failed","status":"fail"}\n')
return 65
except Exception:
sys.stderr.write('{"error":"iap_request_failed","status":"fail"}\n')
return 65
if __name__ == "__main__":
raise SystemExit(main())