Merge pull request #231 from living-ip/feat/leoclean-container-db-proof
Some checks are pending
CI / lint-and-test (push) Waiting to run
Some checks are pending
CI / lint-and-test (push) Waiting to run
Owner security review completed for exact head 6d7ea14bfb. I reviewed and accept the scoped credential, Secret Manager, container identity, fail-closed database verification, and non-production boundaries. Authorized to merge with a merge commit.
This commit is contained in:
commit
3d35a7fafb
10 changed files with 1093 additions and 54 deletions
|
|
@ -42,6 +42,7 @@ COPY identity-sources /opt/livingip/leoclean-nosend/identity-sources
|
||||||
COPY scripts /opt/livingip/leoclean-nosend/scripts
|
COPY scripts /opt/livingip/leoclean-nosend/scripts
|
||||||
COPY entrypoint.py /opt/livingip/leoclean-nosend/entrypoint.py
|
COPY entrypoint.py /opt/livingip/leoclean-nosend/entrypoint.py
|
||||||
COPY package_contract.py /opt/livingip/leoclean-nosend/package_contract.py
|
COPY package_contract.py /opt/livingip/leoclean-nosend/package_contract.py
|
||||||
|
COPY runtime_permissions_verifier.py /opt/livingip/leoclean-nosend/runtime_permissions_verifier.py
|
||||||
COPY artifact-receipt.json /opt/livingip/leoclean-nosend/metadata/artifact-receipt.json
|
COPY artifact-receipt.json /opt/livingip/leoclean-nosend/metadata/artifact-receipt.json
|
||||||
COPY image-input.json /opt/livingip/leoclean-nosend/metadata/image-input.json
|
COPY image-input.json /opt/livingip/leoclean-nosend/metadata/image-input.json
|
||||||
COPY Dockerfile /opt/livingip/leoclean-nosend/metadata/Dockerfile
|
COPY Dockerfile /opt/livingip/leoclean-nosend/metadata/Dockerfile
|
||||||
|
|
@ -59,6 +60,7 @@ RUN ln -s /usr/local/bin/python3.11 /usr/bin/python3 \
|
||||||
&& chmod 0444 /usr/local/libexec/livingip/leoclean-kb/cloudsql-server-ca.pem \
|
&& chmod 0444 /usr/local/libexec/livingip/leoclean-kb/cloudsql-server-ca.pem \
|
||||||
&& chmod 0555 /opt/livingip/leoclean-nosend/entrypoint.py \
|
&& chmod 0555 /opt/livingip/leoclean-nosend/entrypoint.py \
|
||||||
&& chmod 0444 /opt/livingip/leoclean-nosend/package_contract.py \
|
&& chmod 0444 /opt/livingip/leoclean-nosend/package_contract.py \
|
||||||
|
&& chmod 0444 /opt/livingip/leoclean-nosend/runtime_permissions_verifier.py \
|
||||||
&& find /opt/livingip/leoclean-nosend/identity -type f -exec chmod 0444 {} + \
|
&& find /opt/livingip/leoclean-nosend/identity -type f -exec chmod 0444 {} + \
|
||||||
&& case "$TELEO_REVISION" in (*[!0-9a-f]*|'') exit 65;; esac \
|
&& case "$TELEO_REVISION" in (*[!0-9a-f]*|'') exit 65;; esac \
|
||||||
&& test "${#TELEO_REVISION}" -eq 40 \
|
&& test "${#TELEO_REVISION}" -eq 40 \
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,23 @@ def _load_adjacent_contract() -> Any:
|
||||||
return module
|
return module
|
||||||
|
|
||||||
|
|
||||||
|
def _load_adjacent_runtime_permissions_verifier() -> Any:
|
||||||
|
path = Path(__file__).resolve().with_name("runtime_permissions_verifier.py")
|
||||||
|
spec = importlib.util.spec_from_file_location("livingip_leoclean_runtime_permissions_verifier", path)
|
||||||
|
if spec is None or spec.loader is None:
|
||||||
|
raise ImportError("the adjacent runtime permissions verifier cannot be loaded")
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
sys.modules[spec.name] = module
|
||||||
|
try:
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
except BaseException:
|
||||||
|
sys.modules.pop(spec.name, None)
|
||||||
|
raise
|
||||||
|
return module
|
||||||
|
|
||||||
|
|
||||||
contract = _load_adjacent_contract()
|
contract = _load_adjacent_contract()
|
||||||
|
runtime_permissions_verifier = _load_adjacent_runtime_permissions_verifier()
|
||||||
|
|
||||||
ROOT = Path(contract.RUNTIME_ROOT)
|
ROOT = Path(contract.RUNTIME_ROOT)
|
||||||
ARTIFACT = ROOT / "artifact"
|
ARTIFACT = ROOT / "artifact"
|
||||||
|
|
@ -38,6 +54,7 @@ IMAGE_INPUT = ROOT / "metadata" / "image-input.json"
|
||||||
DOCKERFILE = ROOT / "metadata" / "Dockerfile"
|
DOCKERFILE = ROOT / "metadata" / "Dockerfile"
|
||||||
ENTRYPOINT = ROOT / "entrypoint.py"
|
ENTRYPOINT = ROOT / "entrypoint.py"
|
||||||
PACKAGE_CONTRACT = ROOT / "package_contract.py"
|
PACKAGE_CONTRACT = ROOT / "package_contract.py"
|
||||||
|
RUNTIME_PERMISSIONS_VERIFIER = ROOT / "runtime_permissions_verifier.py"
|
||||||
CA = Path(contract.CA_DESTINATION)
|
CA = Path(contract.CA_DESTINATION)
|
||||||
READINESS = PROFILE / "state" / "readiness.json"
|
READINESS = PROFILE / "state" / "readiness.json"
|
||||||
PSQL = Path("/usr/bin/psql")
|
PSQL = Path("/usr/bin/psql")
|
||||||
|
|
@ -85,6 +102,10 @@ def _validate_installed() -> dict[str, Any]:
|
||||||
contract.sha256_file(PACKAGE_CONTRACT) == bindings["package_contract.py"],
|
contract.sha256_file(PACKAGE_CONTRACT) == bindings["package_contract.py"],
|
||||||
"installed package contract binding drifted",
|
"installed package contract binding drifted",
|
||||||
)
|
)
|
||||||
|
_require(
|
||||||
|
contract.sha256_file(RUNTIME_PERMISSIONS_VERIFIER) == bindings["runtime_permissions_verifier.py"],
|
||||||
|
"installed runtime permissions verifier binding drifted",
|
||||||
|
)
|
||||||
for relative in contract.IDENTITY_CONTRACT_SOURCES:
|
for relative in contract.IDENTITY_CONTRACT_SOURCES:
|
||||||
installed = ROOT / relative
|
installed = ROOT / relative
|
||||||
_require(
|
_require(
|
||||||
|
|
@ -262,6 +283,40 @@ def _verification_result(image_input: dict[str, Any]) -> dict[str, Any]:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _database_verification(image_input: dict[str, Any], run_id: str) -> dict[str, Any]:
|
||||||
|
_drop_privileges()
|
||||||
|
try:
|
||||||
|
permissions_receipt = runtime_permissions_verifier.verify_runtime_permissions(
|
||||||
|
run_id,
|
||||||
|
credential_backend="metadata",
|
||||||
|
execution_mode="container",
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
if isinstance(exc, (KeyboardInterrupt, SystemExit)):
|
||||||
|
raise
|
||||||
|
raise EntrypointError("container database verification failed") from None
|
||||||
|
_require(
|
||||||
|
isinstance(permissions_receipt, dict) and permissions_receipt.get("status") == "pass",
|
||||||
|
"container database verification did not pass",
|
||||||
|
)
|
||||||
|
stable = {
|
||||||
|
"schema": "livingip.leocleanNoSendContainerDatabaseVerification.v1",
|
||||||
|
"status": "pass",
|
||||||
|
"run_id": run_id,
|
||||||
|
"container": {
|
||||||
|
"teleo_git_head": image_input["runtime"]["teleo_git_head"],
|
||||||
|
"artifact_sha256": image_input["runtime"]["artifact_sha256"],
|
||||||
|
"identity_sha256": image_input["identity"]["bundle_sha256"],
|
||||||
|
"input_sha256": image_input["input_sha256"],
|
||||||
|
"runtime_permissions_verifier_sha256": image_input["source_bindings"][
|
||||||
|
"runtime_permissions_verifier.py"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"permissions": permissions_receipt,
|
||||||
|
}
|
||||||
|
return {**stable, "receipt_sha256": contract.canonical_sha256(stable)}
|
||||||
|
|
||||||
|
|
||||||
def main(argv: list[str] | None = None) -> int:
|
def main(argv: list[str] | None = None) -> int:
|
||||||
arguments = argv if argv is not None else sys.argv[1:]
|
arguments = argv if argv is not None else sys.argv[1:]
|
||||||
arguments = arguments or ["service"]
|
arguments = arguments or ["service"]
|
||||||
|
|
@ -287,6 +342,9 @@ def main(argv: list[str] | None = None) -> int:
|
||||||
)
|
)
|
||||||
_require(tuple(arguments[1:]) == expected, "image build arguments drifted from image input")
|
_require(tuple(arguments[1:]) == expected, "image build arguments drifted from image input")
|
||||||
result = _verification_result(image_input)
|
result = _verification_result(image_input)
|
||||||
|
elif command == "verify-database":
|
||||||
|
_require(len(arguments) == 2, "database verification arguments are not exact")
|
||||||
|
result = _database_verification(image_input, arguments[1])
|
||||||
else:
|
else:
|
||||||
raise EntrypointError("unsupported container command")
|
raise EntrypointError("unsupported container command")
|
||||||
print(json.dumps(result, separators=(",", ":"), sort_keys=True))
|
print(json.dumps(result, separators=(",", ":"), sort_keys=True))
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,8 @@ the closure. The final image also binds:
|
||||||
- the exact no-send model, skills, tool registry, proposal-only database policy,
|
- the exact no-send model, skills, tool registry, proposal-only database policy,
|
||||||
and runtime-probe receipt that produced the identity runtime contract;
|
and runtime-probe receipt that produced the identity runtime contract;
|
||||||
- the Cloud SQL server CA hash;
|
- the Cloud SQL server CA hash;
|
||||||
- the Dockerfile, entrypoint, and package-contract hashes.
|
- the Dockerfile, entrypoint, package-contract, and runtime-permission-verifier
|
||||||
|
hashes.
|
||||||
|
|
||||||
The runtime image starts with a capability-limited bootstrap that creates a
|
The runtime image starts with a capability-limited bootstrap that creates a
|
||||||
fresh tmpfs profile, then drops all groups, UID/GID to `65532`, and all Linux
|
fresh tmpfs profile, then drops all groups, UID/GID to `65532`, and all Linux
|
||||||
|
|
@ -64,6 +65,36 @@ The PostgreSQL base image's inherited data-volume path is explicitly shadowed
|
||||||
by a tiny read-only, non-executable, mode-`0000` tmpfs. This prevents Docker
|
by a tiny read-only, non-executable, mode-`0000` tmpfs. This prevents Docker
|
||||||
from exposing an unused writable database directory to the runtime.
|
from exposing an unused writable database directory to the runtime.
|
||||||
|
|
||||||
|
## Container-bound database proof
|
||||||
|
|
||||||
|
The image exposes one additional fixed operator command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec --user 65532:65532 livingip-leoclean-nosend \
|
||||||
|
/opt/livingip/leoclean-nosend/venv/bin/python \
|
||||||
|
/opt/livingip/leoclean-nosend/entrypoint.py \
|
||||||
|
verify-database <run-id>
|
||||||
|
```
|
||||||
|
|
||||||
|
It is not a database administration or application command. It reuses the
|
||||||
|
reviewed least-privilege verifier from inside the exact running image. The
|
||||||
|
container obtains a short-lived token from the GCE metadata service, requires
|
||||||
|
the exact staging service-account email, reads only the scoped runtime-password
|
||||||
|
secret through the Secret Manager REST API, requires the administrator secret
|
||||||
|
to be IAM-denied, and connects with TLS to canonical Cloud SQL as
|
||||||
|
`leoclean_kb_runtime`. It then proves the allowlisted reads, a transaction-rolled-
|
||||||
|
back call to `kb_stage.stage_leoclean_proposal(...)`, and the existing negative
|
||||||
|
write/escalation contract.
|
||||||
|
|
||||||
|
The command runs as UID/GID `65532`, writes no credential file, retains no token
|
||||||
|
or password, and emits only a sanitized, self-hashed receipt bound to the image
|
||||||
|
revision, artifact, identity, input, and verifier hashes. Metadata, Secret
|
||||||
|
Manager, PostgreSQL, malformed-response, or unexpected-identity failures return
|
||||||
|
only the fixed redacted container error. The command does not grant IAM, create
|
||||||
|
or rotate a secret, provision a PostgreSQL role, install a service, or approve a
|
||||||
|
proposal. Only a live staging run can prove Docker-bridge reachability and the
|
||||||
|
effective attached identity.
|
||||||
|
|
||||||
## Prepare a build context
|
## Prepare a build context
|
||||||
|
|
||||||
First compile and release-verify the no-send artifact from the exact clean
|
First compile and release-verify the no-send artifact from the exact clean
|
||||||
|
|
|
||||||
|
|
@ -24,25 +24,35 @@ until the GCP service has passed soak and restore testing.
|
||||||
|
|
||||||
## Current state
|
## Current state
|
||||||
|
|
||||||
- Current main/base: `f04956ce7c008009c284e1a779a359d20cd02f11`.
|
- Current main/base: `3f09bb2b6c8ac3113fc1743529a03c0e9b6ea587`.
|
||||||
- PRs #220, #221, #226, #227, #228, and #229 are merged. Main therefore contains the
|
- PRs #220, #221, #226, #227, #228, #229, and #230 are merged. Main therefore contains the
|
||||||
source-first V3 rebuild foundation, the PostgreSQL large-object boundary,
|
source-first V3 rebuild foundation, the PostgreSQL large-object boundary,
|
||||||
bounded Observatory prerequisite preservation, Cloud SQL provider-database
|
bounded Observatory prerequisite preservation, Cloud SQL provider-database
|
||||||
residual handling, and the manual exact-revision no-send publication
|
residual handling, and the manual exact-revision no-send publication
|
||||||
workflow plus the composable prerequisite/runtime ACL boundary.
|
workflow plus the composable prerequisite/runtime ACL boundary and normalized
|
||||||
- The exact-main GCP preflight passed for `f04956c`: the staging VM and private
|
copied-artifact directory contract.
|
||||||
|
- The exact-main GCP preflight passed for `3f09bb2`: the staging VM and private
|
||||||
PostgreSQL 16 Cloud SQL endpoint were reachable, the Artifact Registry
|
PostgreSQL 16 Cloud SQL endpoint were reachable, the Artifact Registry
|
||||||
repository was immutable, and no public database attachment was present.
|
repository was immutable, and no public database attachment was present.
|
||||||
- Manual publication run #2 was bound to `f04956c` and failed closed before a
|
- Manual publication run #3 passed for that exact revision. Artifact Registry
|
||||||
Docker build, tag, or push. The pinned Hermes archive contains one empty
|
independently reports immutable manifest digest
|
||||||
`tinker-atropos/` directory; preparation copied it although the file-only
|
`sha256:c326154acd0b9ae468449abe1c5fd398b6757f375a1a86fdb40e982c515a19a4`
|
||||||
artifact manifest cannot bind it, so the exact directory allowlist rejected
|
and config digest
|
||||||
the context. The current narrow correction removes only unmanifested empty
|
`sha256:5c4d5e1bcfc8cd112e5c999217fce064b5be718409708a154e9d58f007a521ad`,
|
||||||
directories from the copied artifact, validates the complete context before
|
with the expected revision and package labels. This is a valid intermediate
|
||||||
requesting GCP identity, and retains the later pre-push revalidation.
|
candidate, not an approved deployment candidate.
|
||||||
|
- The original run artifact still needs to be retained and checked for its
|
||||||
|
non-reconstructible publication-outcome journal. Final registry state alone
|
||||||
|
cannot prove whether the run freshly pushed, reused, or reconciled a candidate.
|
||||||
|
- The published image cannot yet perform a container-bound database proof. A
|
||||||
|
narrow follow-up is adding a hash-bound `verify-database` command that uses the
|
||||||
|
attached staging service-account metadata identity, the scoped secret, and the
|
||||||
|
existing least-privilege SQL verifier from inside the running container. The
|
||||||
|
next deployable image must be published from the merge revision containing
|
||||||
|
that verifier.
|
||||||
- The GCP VM, private Cloud SQL instance, and scoped Secret Manager secret are
|
- The GCP VM, private Cloud SQL instance, and scoped Secret Manager secret are
|
||||||
known to exist, but no new current-main no-send image has yet been published
|
known to exist, but the scoped VM secret binding and runtime role have not been
|
||||||
by the merged #228 workflow.
|
activated for this service.
|
||||||
- Live Cloud SQL provisioning of `leoclean_kb_runtime`, VM access to the scoped
|
- Live Cloud SQL provisioning of `leoclean_kb_runtime`, VM access to the scoped
|
||||||
secret, installation of the no-send service, effective running-process
|
secret, installation of the no-send service, effective running-process
|
||||||
identity, restart behavior, and rollback remain unproven.
|
identity, restart behavior, and rollback remain unproven.
|
||||||
|
|
@ -53,10 +63,12 @@ until the GCP service has passed soak and restore testing.
|
||||||
|
|
||||||
## Execution order
|
## Execution order
|
||||||
|
|
||||||
1. Review and merge the narrow copied-artifact empty-directory normalization.
|
1. Review and merge the container-bound database verifier. Run its disposable
|
||||||
2. Run the merged GCP preflight against the resulting exact main revision, then
|
PostgreSQL and OCI fail-closed tests, then publish one exact-main replacement
|
||||||
manually dispatch the publication workflow for that same revision. Verify
|
candidate and retain the complete publication artifact.
|
||||||
the resulting immutable image/config digests and publication receipt.
|
2. Add the narrow IAP transfer/install/restart/rollback runner that consumes the
|
||||||
|
finalized release bundle, pulls only its immutable digest, invokes the existing
|
||||||
|
installer, and binds the live container proof to systemd/container identity.
|
||||||
3. Provision `leoclean_kb_runtime`, grant only the staging VM access to the
|
3. Provision `leoclean_kb_runtime`, grant only the staging VM access to the
|
||||||
scoped secret, and prove the Cloud SQL role's allowed reads and
|
scoped secret, and prove the Cloud SQL role's allowed reads and
|
||||||
function-only proposal staging plus denied writes and escalation.
|
function-only proposal staging plus denied writes and escalation.
|
||||||
|
|
@ -89,9 +101,9 @@ until the GCP service has passed soak and restore testing.
|
||||||
|
|
||||||
## Human gates
|
## Human gates
|
||||||
|
|
||||||
The publication correction requires exact-revision package/release-CI review
|
The container verifier and deployment runner each require exact-revision human
|
||||||
before merge and a separate authorization before the manual workflow rerun.
|
review before merge. Publication, GCP role/IAM mutation, live no-send service
|
||||||
GCP role/IAM mutation, V3 canonical promotion, and Telegram cutover remain
|
installation, V3 canonical promotion, and Telegram cutover remain
|
||||||
distinct critical gates. Evidence from tests or agent review is advisory; the
|
distinct critical gates. Evidence from tests or agent review is advisory; the
|
||||||
exact revision and live receipts must be presented to the human development
|
exact revision and live receipts must be presented to the human development
|
||||||
lead at each gate.
|
lead at each gate.
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ if str(CONTRACT_ROOT) not in sys.path:
|
||||||
|
|
||||||
from scripts import leo_identity_manifest as strict_identity_manifest # noqa: E402
|
from scripts import leo_identity_manifest as strict_identity_manifest # noqa: E402
|
||||||
|
|
||||||
IMAGE_INPUT_SCHEMA = "livingip.leocleanNoSendImageInput.v2"
|
IMAGE_INPUT_SCHEMA = "livingip.leocleanNoSendImageInput.v3"
|
||||||
IMAGE_INSPECTION_SCHEMA = "livingip.leocleanNoSendImageInspection.v1"
|
IMAGE_INSPECTION_SCHEMA = "livingip.leocleanNoSendImageInspection.v1"
|
||||||
BUILD_PUSH_RECEIPT_SCHEMA = "livingip.leocleanNoSendBuildPushReceipt.v1"
|
BUILD_PUSH_RECEIPT_SCHEMA = "livingip.leocleanNoSendBuildPushReceipt.v1"
|
||||||
PUBLISH_OUTCOME_SCHEMA = "livingip.leocleanNoSendPublishOutcome.v1"
|
PUBLISH_OUTCOME_SCHEMA = "livingip.leocleanNoSendPublishOutcome.v1"
|
||||||
|
|
@ -168,6 +168,7 @@ PACKAGE_SOURCE_PATHS = (
|
||||||
"docker/leoclean-nosend/entrypoint.py",
|
"docker/leoclean-nosend/entrypoint.py",
|
||||||
"ops/gcp-teleo-pgvector-standby-server-ca.pem",
|
"ops/gcp-teleo-pgvector-standby-server-ca.pem",
|
||||||
"ops/gcp_leoclean_nosend_package.py",
|
"ops/gcp_leoclean_nosend_package.py",
|
||||||
|
"ops/verify_gcp_leoclean_runtime_permissions.py",
|
||||||
*IDENTITY_CONTRACT_SOURCES,
|
*IDENTITY_CONTRACT_SOURCES,
|
||||||
)
|
)
|
||||||
NO_SEND_TOOL_SURFACE = strict_identity_manifest.LEOCLEAN_NO_SEND_TOOL_SURFACE
|
NO_SEND_TOOL_SURFACE = strict_identity_manifest.LEOCLEAN_NO_SEND_TOOL_SURFACE
|
||||||
|
|
@ -1264,6 +1265,7 @@ def validate_image_input(value: dict[str, Any]) -> None:
|
||||||
"Dockerfile",
|
"Dockerfile",
|
||||||
"entrypoint.py",
|
"entrypoint.py",
|
||||||
"package_contract.py",
|
"package_contract.py",
|
||||||
|
"runtime_permissions_verifier.py",
|
||||||
"scripts/leo_behavior_manifest.py",
|
"scripts/leo_behavior_manifest.py",
|
||||||
"scripts/leo_identity_manifest.py",
|
"scripts/leo_identity_manifest.py",
|
||||||
}
|
}
|
||||||
|
|
@ -1334,6 +1336,9 @@ def build_image_input(
|
||||||
"docker/leoclean-nosend/entrypoint.py": sha256_file(entrypoint),
|
"docker/leoclean-nosend/entrypoint.py": sha256_file(entrypoint),
|
||||||
"ops/gcp-teleo-pgvector-standby-server-ca.pem": sha256_file(ca),
|
"ops/gcp-teleo-pgvector-standby-server-ca.pem": sha256_file(ca),
|
||||||
"ops/gcp_leoclean_nosend_package.py": sha256_file(package_contract),
|
"ops/gcp_leoclean_nosend_package.py": sha256_file(package_contract),
|
||||||
|
"ops/verify_gcp_leoclean_runtime_permissions.py": sha256_file(
|
||||||
|
contract_source_root / "ops" / "verify_gcp_leoclean_runtime_permissions.py"
|
||||||
|
),
|
||||||
**identity_contract_sources,
|
**identity_contract_sources,
|
||||||
}
|
}
|
||||||
reviewed_source_hashes = {
|
reviewed_source_hashes = {
|
||||||
|
|
@ -1351,6 +1356,9 @@ def build_image_input(
|
||||||
"Dockerfile": fixed_source_hashes["docker/leoclean-nosend/Dockerfile"],
|
"Dockerfile": fixed_source_hashes["docker/leoclean-nosend/Dockerfile"],
|
||||||
"entrypoint.py": fixed_source_hashes["docker/leoclean-nosend/entrypoint.py"],
|
"entrypoint.py": fixed_source_hashes["docker/leoclean-nosend/entrypoint.py"],
|
||||||
"package_contract.py": fixed_source_hashes["ops/gcp_leoclean_nosend_package.py"],
|
"package_contract.py": fixed_source_hashes["ops/gcp_leoclean_nosend_package.py"],
|
||||||
|
"runtime_permissions_verifier.py": fixed_source_hashes[
|
||||||
|
"ops/verify_gcp_leoclean_runtime_permissions.py"
|
||||||
|
],
|
||||||
**identity_contract_sources,
|
**identity_contract_sources,
|
||||||
},
|
},
|
||||||
"claim_ceiling": runtime["claim_ceiling"],
|
"claim_ceiling": runtime["claim_ceiling"],
|
||||||
|
|
@ -1434,11 +1442,13 @@ def prepare_context(
|
||||||
dockerfile = repo_root / "docker" / "leoclean-nosend" / "Dockerfile"
|
dockerfile = repo_root / "docker" / "leoclean-nosend" / "Dockerfile"
|
||||||
entrypoint = repo_root / "docker" / "leoclean-nosend" / "entrypoint.py"
|
entrypoint = repo_root / "docker" / "leoclean-nosend" / "entrypoint.py"
|
||||||
package_contract = repo_root / "ops" / "gcp_leoclean_nosend_package.py"
|
package_contract = repo_root / "ops" / "gcp_leoclean_nosend_package.py"
|
||||||
|
runtime_permissions_verifier = repo_root / "ops" / "verify_gcp_leoclean_runtime_permissions.py"
|
||||||
ca = repo_root / "ops" / "gcp-teleo-pgvector-standby-server-ca.pem"
|
ca = repo_root / "ops" / "gcp-teleo-pgvector-standby-server-ca.pem"
|
||||||
for path, label in (
|
for path, label in (
|
||||||
(dockerfile, "Dockerfile"),
|
(dockerfile, "Dockerfile"),
|
||||||
(entrypoint, "entrypoint"),
|
(entrypoint, "entrypoint"),
|
||||||
(package_contract, "package contract"),
|
(package_contract, "package contract"),
|
||||||
|
(runtime_permissions_verifier, "runtime permissions verifier"),
|
||||||
(ca, "Cloud SQL CA"),
|
(ca, "Cloud SQL CA"),
|
||||||
):
|
):
|
||||||
_require(path.is_file() and not path.is_symlink(), f"{label} source is missing or unsafe")
|
_require(path.is_file() and not path.is_symlink(), f"{label} source is missing or unsafe")
|
||||||
|
|
@ -1469,6 +1479,7 @@ def prepare_context(
|
||||||
shutil.copyfile(dockerfile, output / "Dockerfile")
|
shutil.copyfile(dockerfile, output / "Dockerfile")
|
||||||
shutil.copyfile(entrypoint, output / "entrypoint.py")
|
shutil.copyfile(entrypoint, output / "entrypoint.py")
|
||||||
shutil.copyfile(package_contract, output / "package_contract.py")
|
shutil.copyfile(package_contract, output / "package_contract.py")
|
||||||
|
shutil.copyfile(runtime_permissions_verifier, output / "runtime_permissions_verifier.py")
|
||||||
scripts = output / "scripts"
|
scripts = output / "scripts"
|
||||||
scripts.mkdir(mode=0o755)
|
scripts.mkdir(mode=0o755)
|
||||||
for relative in IDENTITY_CONTRACT_SOURCES:
|
for relative in IDENTITY_CONTRACT_SOURCES:
|
||||||
|
|
@ -1481,6 +1492,7 @@ def prepare_context(
|
||||||
"Dockerfile",
|
"Dockerfile",
|
||||||
"entrypoint.py",
|
"entrypoint.py",
|
||||||
"package_contract.py",
|
"package_contract.py",
|
||||||
|
"runtime_permissions_verifier.py",
|
||||||
):
|
):
|
||||||
(output / name).chmod(0o644)
|
(output / name).chmod(0o644)
|
||||||
(output / "artifact" / "artifact-manifest.json").chmod(0o644)
|
(output / "artifact" / "artifact-manifest.json").chmod(0o644)
|
||||||
|
|
@ -1530,6 +1542,7 @@ def validate_prepared_image_context(context: Path) -> tuple[dict[str, Any], dict
|
||||||
"identity-sources",
|
"identity-sources",
|
||||||
"image-input.json",
|
"image-input.json",
|
||||||
"package_contract.py",
|
"package_contract.py",
|
||||||
|
"runtime_permissions_verifier.py",
|
||||||
"scripts",
|
"scripts",
|
||||||
},
|
},
|
||||||
"prepared image context entry set is not exact",
|
"prepared image context entry set is not exact",
|
||||||
|
|
@ -1551,6 +1564,7 @@ def validate_prepared_image_context(context: Path) -> tuple[dict[str, Any], dict
|
||||||
("Dockerfile", bindings["Dockerfile"]),
|
("Dockerfile", bindings["Dockerfile"]),
|
||||||
("entrypoint.py", bindings["entrypoint.py"]),
|
("entrypoint.py", bindings["entrypoint.py"]),
|
||||||
("package_contract.py", bindings["package_contract.py"]),
|
("package_contract.py", bindings["package_contract.py"]),
|
||||||
|
("runtime_permissions_verifier.py", bindings["runtime_permissions_verifier.py"]),
|
||||||
("scripts/leo_behavior_manifest.py", bindings["scripts/leo_behavior_manifest.py"]),
|
("scripts/leo_behavior_manifest.py", bindings["scripts/leo_behavior_manifest.py"]),
|
||||||
("scripts/leo_identity_manifest.py", bindings["scripts/leo_identity_manifest.py"]),
|
("scripts/leo_identity_manifest.py", bindings["scripts/leo_identity_manifest.py"]),
|
||||||
("cloudsql-server-ca.pem", image_input["runtime"]["cloudsql_ca_sha256"]),
|
("cloudsql-server-ca.pem", image_input["runtime"]["cloudsql_ca_sha256"]),
|
||||||
|
|
@ -1610,6 +1624,7 @@ def validate_prepared_image_context(context: Path) -> tuple[dict[str, Any], dict
|
||||||
"entrypoint.py": 0o644,
|
"entrypoint.py": 0o644,
|
||||||
"image-input.json": 0o644,
|
"image-input.json": 0o644,
|
||||||
"package_contract.py": 0o644,
|
"package_contract.py": 0o644,
|
||||||
|
"runtime_permissions_verifier.py": 0o644,
|
||||||
}
|
}
|
||||||
for entry in artifact_files:
|
for entry in artifact_files:
|
||||||
raw_mode = entry["mode"]
|
raw_mode = entry["mode"]
|
||||||
|
|
@ -1660,6 +1675,7 @@ def validate_reviewed_context_source(
|
||||||
"docker/leoclean-nosend/entrypoint.py": bindings["entrypoint.py"],
|
"docker/leoclean-nosend/entrypoint.py": bindings["entrypoint.py"],
|
||||||
"ops/gcp-teleo-pgvector-standby-server-ca.pem": image_input["runtime"]["cloudsql_ca_sha256"],
|
"ops/gcp-teleo-pgvector-standby-server-ca.pem": image_input["runtime"]["cloudsql_ca_sha256"],
|
||||||
"ops/gcp_leoclean_nosend_package.py": bindings["package_contract.py"],
|
"ops/gcp_leoclean_nosend_package.py": bindings["package_contract.py"],
|
||||||
|
"ops/verify_gcp_leoclean_runtime_permissions.py": bindings["runtime_permissions_verifier.py"],
|
||||||
"scripts/leo_behavior_manifest.py": bindings["scripts/leo_behavior_manifest.py"],
|
"scripts/leo_behavior_manifest.py": bindings["scripts/leo_behavior_manifest.py"],
|
||||||
"scripts/leo_identity_manifest.py": bindings["scripts/leo_identity_manifest.py"],
|
"scripts/leo_identity_manifest.py": bindings["scripts/leo_identity_manifest.py"],
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,32 @@
|
||||||
#!/usr/bin/python3 -I
|
#!/usr/bin/python3 -I
|
||||||
"""Retain a sanitized least-privilege receipt for the GCP Leo runtime role.
|
"""Retain a sanitized least-privilege receipt for the GCP Leo runtime role.
|
||||||
|
|
||||||
This verifier is intentionally service-independent. It must run on the private
|
The default host mode is intentionally service-independent and must run on the
|
||||||
GCP VM as ``teleo`` and connects directly to the private Cloud SQL address. The
|
private GCP VM as ``teleo``. The separately selected container mode requires
|
||||||
only database credential is read from the scoped Secret Manager secret into
|
UID/GID ``65532``, the exact attached staging service-account identity from GCE
|
||||||
process memory, passed to each ``psql`` child through ``PGPASSWORD``, and never
|
metadata, and Secret Manager REST access. Both modes connect directly to the
|
||||||
included in an argument, message, receipt, or output file.
|
private Cloud SQL address. The only database credential is read from the scoped
|
||||||
|
Secret Manager secret into process memory, passed to each ``psql`` child through
|
||||||
|
``PGPASSWORD``, and never included in an argument, message, receipt, or output
|
||||||
|
file.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import base64
|
||||||
|
import binascii
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import pwd
|
import pwd
|
||||||
import re
|
import re
|
||||||
|
import ssl
|
||||||
import stat
|
import stat
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import urllib.error
|
||||||
|
import urllib.request
|
||||||
from collections.abc import Callable, Mapping
|
from collections.abc import Callable, Mapping
|
||||||
from contextlib import nullcontext
|
from contextlib import nullcontext
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
@ -37,6 +45,9 @@ TEMPLATE_DATABASE = "template1"
|
||||||
RUNTIME_DATABASE_ROLE = "leoclean_kb_runtime"
|
RUNTIME_DATABASE_ROLE = "leoclean_kb_runtime"
|
||||||
STAGE_OWNER_DATABASE_ROLE = "leoclean_kb_stage_owner"
|
STAGE_OWNER_DATABASE_ROLE = "leoclean_kb_stage_owner"
|
||||||
RUNTIME_UNIX_USER = "teleo"
|
RUNTIME_UNIX_USER = "teleo"
|
||||||
|
CONTAINER_RUNTIME_UID = 65532
|
||||||
|
CONTAINER_RUNTIME_GID = 65532
|
||||||
|
STAGING_RUNTIME_SERVICE_ACCOUNT = "sa-teleo-staging-vm@teleo-501523.iam.gserviceaccount.com"
|
||||||
SCOPED_PASSWORD_SECRET = "gcp-teleo-pgvector-standby-leoclean-kb-runtime-password"
|
SCOPED_PASSWORD_SECRET = "gcp-teleo-pgvector-standby-leoclean-kb-runtime-password"
|
||||||
ADMINISTRATOR_PASSWORD_SECRET = "gcp-teleo-pgvector-standby-postgres-password"
|
ADMINISTRATOR_PASSWORD_SECRET = "gcp-teleo-pgvector-standby-postgres-password"
|
||||||
GCLOUD_BIN = "/usr/bin/gcloud"
|
GCLOUD_BIN = "/usr/bin/gcloud"
|
||||||
|
|
@ -45,6 +56,18 @@ SERVER_CA_PATH = Path("/usr/local/libexec/livingip/leoclean-kb/cloudsql-server-c
|
||||||
RUNTIME_CLOUDSDK_CONFIG = Path("/usr/local/libexec/livingip/leoclean-kb/gcloud-config")
|
RUNTIME_CLOUDSDK_CONFIG = Path("/usr/local/libexec/livingip/leoclean-kb/gcloud-config")
|
||||||
SERVER_CA_SHA256 = "80701e768f0e1f6b9d621aa0b53f6e851daaa276c6d9a8e51a300fbc015539cb"
|
SERVER_CA_SHA256 = "80701e768f0e1f6b9d621aa0b53f6e851daaa276c6d9a8e51a300fbc015539cb"
|
||||||
COMMAND_TIMEOUT_SECONDS = 30
|
COMMAND_TIMEOUT_SECONDS = 30
|
||||||
|
HTTP_TIMEOUT_SECONDS = 5.0
|
||||||
|
MAX_METADATA_IDENTITY_BYTES = 256
|
||||||
|
MAX_METADATA_TOKEN_BYTES = 8_192
|
||||||
|
MAX_SECRET_RESPONSE_BYTES = 16_384
|
||||||
|
MAX_SECRET_VALUE_BYTES = 4_096
|
||||||
|
METADATA_ROOT = "http://169.254.169.254/computeMetadata/v1"
|
||||||
|
METADATA_HEADERS = {"Metadata-Flavor": "Google"}
|
||||||
|
SYSTEM_TLS_CA_PATH = Path("/etc/ssl/certs/ca-certificates.crt")
|
||||||
|
HOST_CREDENTIAL_BACKEND = "gcloud"
|
||||||
|
CONTAINER_CREDENTIAL_BACKEND = "metadata"
|
||||||
|
HOST_EXECUTION_MODE = "host"
|
||||||
|
CONTAINER_EXECUTION_MODE = "container"
|
||||||
CHILD_PATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
CHILD_PATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||||
RUN_ID_RE = re.compile(r"[a-z0-9][a-z0-9-]{6,62}[a-z0-9]\Z")
|
RUN_ID_RE = re.compile(r"[a-z0-9][a-z0-9-]{6,62}[a-z0-9]\Z")
|
||||||
SQLSTATE_RE = re.compile(r"(?m)^(?:ERROR|FATAL):\s+([0-9A-Z]{5}):")
|
SQLSTATE_RE = re.compile(r"(?m)^(?:ERROR|FATAL):\s+([0-9A-Z]{5}):")
|
||||||
|
|
@ -349,6 +372,33 @@ Runner = Callable[[list[str], Mapping[str, str], int, bool], CommandResult]
|
||||||
DependencyValidator = Callable[[], None]
|
DependencyValidator = Callable[[], None]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class HttpResponse:
|
||||||
|
status: int
|
||||||
|
headers: Mapping[str, str]
|
||||||
|
body: bytes
|
||||||
|
|
||||||
|
|
||||||
|
HttpTransport = Callable[[str, Mapping[str, str], float, int], HttpResponse]
|
||||||
|
|
||||||
|
|
||||||
|
class _RejectRedirectHandler(urllib.request.HTTPRedirectHandler):
|
||||||
|
def redirect_request(
|
||||||
|
self,
|
||||||
|
req: urllib.request.Request,
|
||||||
|
fp: Any,
|
||||||
|
code: int,
|
||||||
|
msg: str,
|
||||||
|
headers: Any,
|
||||||
|
newurl: str,
|
||||||
|
) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class _HttpTransportFailure(RuntimeError):
|
||||||
|
"""An HTTP failure whose provider-controlled details must never escape."""
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class NegativeCheck:
|
class NegativeCheck:
|
||||||
name: str
|
name: str
|
||||||
|
|
@ -407,6 +457,43 @@ def subprocess_runner(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def urllib_http_transport(
|
||||||
|
url: str,
|
||||||
|
headers: Mapping[str, str],
|
||||||
|
timeout: float,
|
||||||
|
max_bytes: int,
|
||||||
|
) -> HttpResponse:
|
||||||
|
request = urllib.request.Request(url, headers=dict(headers), method="GET")
|
||||||
|
try:
|
||||||
|
tls_context = ssl.create_default_context(cafile=str(SYSTEM_TLS_CA_PATH))
|
||||||
|
opener = urllib.request.build_opener(
|
||||||
|
urllib.request.ProxyHandler({}),
|
||||||
|
urllib.request.HTTPSHandler(context=tls_context),
|
||||||
|
_RejectRedirectHandler(),
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
response = opener.open(request, timeout=timeout)
|
||||||
|
except urllib.error.HTTPError as exc:
|
||||||
|
response = exc
|
||||||
|
try:
|
||||||
|
body = response.read(max_bytes + 1)
|
||||||
|
status = response.getcode()
|
||||||
|
response_headers = {str(key).casefold(): str(value) for key, value in response.headers.items()}
|
||||||
|
finally:
|
||||||
|
response.close()
|
||||||
|
except Exception:
|
||||||
|
raise _HttpTransportFailure from None
|
||||||
|
if (
|
||||||
|
isinstance(status, bool)
|
||||||
|
or not isinstance(status, int)
|
||||||
|
or not 100 <= status <= 599
|
||||||
|
or not isinstance(body, bytes)
|
||||||
|
or len(body) > max_bytes
|
||||||
|
):
|
||||||
|
raise _HttpTransportFailure
|
||||||
|
return HttpResponse(status=status, headers=response_headers, body=body)
|
||||||
|
|
||||||
|
|
||||||
def validate_run_id(value: str) -> str:
|
def validate_run_id(value: str) -> str:
|
||||||
if not RUN_ID_RE.fullmatch(value):
|
if not RUN_ID_RE.fullmatch(value):
|
||||||
raise ValueError("run id must be 8-64 lowercase letters, digits, or hyphens")
|
raise ValueError("run id must be 8-64 lowercase letters, digits, or hyphens")
|
||||||
|
|
@ -472,8 +559,20 @@ def _assert_trusted_executable(raw_path: str) -> None:
|
||||||
|
|
||||||
def validate_runtime_dependencies() -> None:
|
def validate_runtime_dependencies() -> None:
|
||||||
_assert_trusted_executable(GCLOUD_BIN)
|
_assert_trusted_executable(GCLOUD_BIN)
|
||||||
_assert_trusted_executable(PSQL_BIN)
|
_validate_psql_and_cloudsql_ca()
|
||||||
runtime_cloudsdk_config_path()
|
runtime_cloudsdk_config_path()
|
||||||
|
|
||||||
|
|
||||||
|
def validate_container_runtime_dependencies() -> None:
|
||||||
|
_validate_psql_and_cloudsql_ca()
|
||||||
|
for candidate in _path_and_parents(SYSTEM_TLS_CA_PATH):
|
||||||
|
_assert_root_owned_nonwritable(candidate)
|
||||||
|
if not SYSTEM_TLS_CA_PATH.is_file():
|
||||||
|
raise VerificationError("runtime_dependency_untrusted", "runtime_dependencies")
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_psql_and_cloudsql_ca() -> None:
|
||||||
|
_assert_trusted_executable(PSQL_BIN)
|
||||||
server_ca = runtime_server_ca_path()
|
server_ca = runtime_server_ca_path()
|
||||||
for candidate in _path_and_parents(server_ca):
|
for candidate in _path_and_parents(server_ca):
|
||||||
_assert_root_owned_nonwritable(candidate)
|
_assert_root_owned_nonwritable(candidate)
|
||||||
|
|
@ -620,6 +719,229 @@ def _assert_administrator_secret_denied(*, env: Mapping[str, str], runner: Runne
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _http_call(
|
||||||
|
url: str,
|
||||||
|
*,
|
||||||
|
headers: Mapping[str, str],
|
||||||
|
max_bytes: int,
|
||||||
|
transport: HttpTransport,
|
||||||
|
code: str,
|
||||||
|
check: str,
|
||||||
|
) -> HttpResponse:
|
||||||
|
try:
|
||||||
|
response = transport(url, headers, HTTP_TIMEOUT_SECONDS, max_bytes)
|
||||||
|
except Exception:
|
||||||
|
raise VerificationError(code, check) from None
|
||||||
|
if (
|
||||||
|
not isinstance(response, HttpResponse)
|
||||||
|
or isinstance(response.status, bool)
|
||||||
|
or not isinstance(response.status, int)
|
||||||
|
or not 100 <= response.status <= 599
|
||||||
|
or not isinstance(response.body, bytes)
|
||||||
|
or len(response.body) > max_bytes
|
||||||
|
or not isinstance(response.headers, Mapping)
|
||||||
|
or any(not isinstance(key, str) or not isinstance(value, str) for key, value in response.headers.items())
|
||||||
|
):
|
||||||
|
raise VerificationError(code, check)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
def _strict_json_object(body: bytes, *, code: str, check: str) -> dict[str, Any]:
|
||||||
|
def reject_duplicate_keys(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
|
||||||
|
value: dict[str, Any] = {}
|
||||||
|
for key, item in pairs:
|
||||||
|
if key in value:
|
||||||
|
raise ValueError("duplicate JSON key")
|
||||||
|
value[key] = item
|
||||||
|
return value
|
||||||
|
|
||||||
|
try:
|
||||||
|
decoded = body.decode("utf-8", errors="strict")
|
||||||
|
value = json.loads(
|
||||||
|
decoded,
|
||||||
|
object_pairs_hook=reject_duplicate_keys,
|
||||||
|
parse_constant=lambda _constant: (_ for _ in ()).throw(ValueError("non-finite JSON value")),
|
||||||
|
)
|
||||||
|
except (UnicodeDecodeError, ValueError, TypeError):
|
||||||
|
raise VerificationError(code, check) from None
|
||||||
|
if not isinstance(value, dict):
|
||||||
|
raise VerificationError(code, check)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _metadata_url(path: str) -> str:
|
||||||
|
return f"{METADATA_ROOT}/{path}"
|
||||||
|
|
||||||
|
|
||||||
|
def _secret_access_url(secret_name: str) -> str:
|
||||||
|
return (
|
||||||
|
f"https://secretmanager.googleapis.com/v1/projects/{PROJECT_ID}/secrets/"
|
||||||
|
f"{secret_name}/versions/latest:access"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _metadata_get(
|
||||||
|
path: str,
|
||||||
|
*,
|
||||||
|
max_bytes: int,
|
||||||
|
transport: HttpTransport,
|
||||||
|
code: str,
|
||||||
|
check: str,
|
||||||
|
) -> bytes:
|
||||||
|
response = _http_call(
|
||||||
|
_metadata_url(path),
|
||||||
|
headers=METADATA_HEADERS,
|
||||||
|
max_bytes=max_bytes,
|
||||||
|
transport=transport,
|
||||||
|
code=code,
|
||||||
|
check=check,
|
||||||
|
)
|
||||||
|
normalized_headers = {key.casefold(): value for key, value in response.headers.items()}
|
||||||
|
if response.status != 200 or normalized_headers.get("metadata-flavor") != "Google":
|
||||||
|
raise VerificationError(code, check)
|
||||||
|
return response.body
|
||||||
|
|
||||||
|
|
||||||
|
def _read_metadata_identity(*, transport: HttpTransport) -> str:
|
||||||
|
body = _metadata_get(
|
||||||
|
"instance/service-accounts/default/email",
|
||||||
|
max_bytes=MAX_METADATA_IDENTITY_BYTES,
|
||||||
|
transport=transport,
|
||||||
|
code="metadata_service_account_identity_failed",
|
||||||
|
check="metadata_service_account_identity",
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
service_account = body.decode("ascii", errors="strict")
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
raise VerificationError(
|
||||||
|
"metadata_service_account_identity_failed",
|
||||||
|
"metadata_service_account_identity",
|
||||||
|
) from None
|
||||||
|
if service_account != STAGING_RUNTIME_SERVICE_ACCOUNT:
|
||||||
|
raise VerificationError("metadata_service_account_mismatch", "metadata_service_account_identity")
|
||||||
|
return service_account
|
||||||
|
|
||||||
|
|
||||||
|
def _read_metadata_access_token(*, transport: HttpTransport) -> str:
|
||||||
|
token_payload = _strict_json_object(
|
||||||
|
_metadata_get(
|
||||||
|
"instance/service-accounts/default/token",
|
||||||
|
max_bytes=MAX_METADATA_TOKEN_BYTES,
|
||||||
|
transport=transport,
|
||||||
|
code="metadata_access_token_failed",
|
||||||
|
check="metadata_access_token",
|
||||||
|
),
|
||||||
|
code="metadata_access_token_invalid",
|
||||||
|
check="metadata_access_token",
|
||||||
|
)
|
||||||
|
access_token = token_payload.get("access_token")
|
||||||
|
token_type = token_payload.get("token_type")
|
||||||
|
expires_in = token_payload.get("expires_in")
|
||||||
|
if (
|
||||||
|
token_type != "Bearer"
|
||||||
|
or not isinstance(access_token, str)
|
||||||
|
or re.fullmatch(r"[!-~]{1,4096}", access_token) is None
|
||||||
|
or isinstance(expires_in, bool)
|
||||||
|
or not isinstance(expires_in, int)
|
||||||
|
or expires_in <= 0
|
||||||
|
):
|
||||||
|
raise VerificationError("metadata_access_token_invalid", "metadata_access_token")
|
||||||
|
return access_token
|
||||||
|
|
||||||
|
|
||||||
|
def _secret_rest_request(
|
||||||
|
secret_name: str,
|
||||||
|
access_token: str,
|
||||||
|
*,
|
||||||
|
transport: HttpTransport,
|
||||||
|
code: str,
|
||||||
|
check: str,
|
||||||
|
) -> HttpResponse:
|
||||||
|
return _http_call(
|
||||||
|
_secret_access_url(secret_name),
|
||||||
|
headers={"Accept": "application/json", "Authorization": f"Bearer {access_token}"},
|
||||||
|
max_bytes=MAX_SECRET_RESPONSE_BYTES,
|
||||||
|
transport=transport,
|
||||||
|
code=code,
|
||||||
|
check=check,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _read_scoped_password_from_metadata(access_token: str, *, transport: HttpTransport) -> str:
|
||||||
|
response = _secret_rest_request(
|
||||||
|
SCOPED_PASSWORD_SECRET,
|
||||||
|
access_token,
|
||||||
|
transport=transport,
|
||||||
|
code="scoped_secret_access_failed",
|
||||||
|
check="scoped_secret_access",
|
||||||
|
)
|
||||||
|
if response.status != 200:
|
||||||
|
raise VerificationError("scoped_secret_access_failed", "scoped_secret_access")
|
||||||
|
secret_response = _strict_json_object(
|
||||||
|
response.body,
|
||||||
|
code="scoped_secret_response_invalid",
|
||||||
|
check="scoped_secret_access",
|
||||||
|
)
|
||||||
|
payload = secret_response.get("payload")
|
||||||
|
encoded_secret = payload.get("data") if isinstance(payload, dict) else None
|
||||||
|
if not isinstance(encoded_secret, str):
|
||||||
|
raise VerificationError("scoped_secret_response_invalid", "scoped_secret_access")
|
||||||
|
try:
|
||||||
|
credential_bytes = base64.b64decode(encoded_secret, validate=True)
|
||||||
|
except (binascii.Error, ValueError):
|
||||||
|
raise VerificationError("scoped_secret_response_invalid", "scoped_secret_access") from None
|
||||||
|
if (
|
||||||
|
not credential_bytes
|
||||||
|
or len(credential_bytes) > MAX_SECRET_VALUE_BYTES
|
||||||
|
or any(byte in credential_bytes for byte in (0, 10, 13))
|
||||||
|
):
|
||||||
|
raise VerificationError("scoped_secret_value_invalid", "scoped_secret_access")
|
||||||
|
try:
|
||||||
|
return credential_bytes.decode("utf-8", errors="strict")
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
raise VerificationError("scoped_secret_value_invalid", "scoped_secret_access") from None
|
||||||
|
|
||||||
|
|
||||||
|
def _assert_administrator_secret_denied_from_metadata(
|
||||||
|
access_token: str,
|
||||||
|
*,
|
||||||
|
transport: HttpTransport,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
response = _secret_rest_request(
|
||||||
|
ADMINISTRATOR_PASSWORD_SECRET,
|
||||||
|
access_token,
|
||||||
|
transport=transport,
|
||||||
|
code="administrator_secret_denial_unavailable",
|
||||||
|
check="administrator_secret_access",
|
||||||
|
)
|
||||||
|
if response.status == 200:
|
||||||
|
raise VerificationError("administrator_secret_unexpectedly_readable", "administrator_secret_access")
|
||||||
|
if response.status != 403:
|
||||||
|
raise VerificationError("administrator_secret_denial_not_iam_permission", "administrator_secret_access")
|
||||||
|
denial = _strict_json_object(
|
||||||
|
response.body,
|
||||||
|
code="administrator_secret_denial_not_iam_permission",
|
||||||
|
check="administrator_secret_access",
|
||||||
|
)
|
||||||
|
error = denial.get("error")
|
||||||
|
message = error.get("message") if isinstance(error, dict) else None
|
||||||
|
if (
|
||||||
|
not isinstance(error, dict)
|
||||||
|
or error.get("code") != 403
|
||||||
|
or error.get("status") != "PERMISSION_DENIED"
|
||||||
|
or not isinstance(message, str)
|
||||||
|
or IAM_PERMISSION not in message.casefold()
|
||||||
|
):
|
||||||
|
raise VerificationError("administrator_secret_denial_not_iam_permission", "administrator_secret_access")
|
||||||
|
return {
|
||||||
|
"classification": "iam_permission_denied",
|
||||||
|
"permission": IAM_PERMISSION,
|
||||||
|
"response_body_retained": False,
|
||||||
|
"result": "denied",
|
||||||
|
"stdout_discarded": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _conninfo(database: str) -> str:
|
def _conninfo(database: str) -> str:
|
||||||
return (
|
return (
|
||||||
f"host={PRIVATE_CLOUDSQL_HOST} port={PRIVATE_CLOUDSQL_PORT} dbname={database} "
|
f"host={PRIVATE_CLOUDSQL_HOST} port={PRIVATE_CLOUDSQL_PORT} dbname={database} "
|
||||||
|
|
@ -2260,29 +2582,69 @@ def verify_runtime_permissions(
|
||||||
runner: Runner = subprocess_runner,
|
runner: Runner = subprocess_runner,
|
||||||
base_env: Mapping[str, str] | None = None,
|
base_env: Mapping[str, str] | None = None,
|
||||||
effective_user: str | None = None,
|
effective_user: str | None = None,
|
||||||
dependency_validator: DependencyValidator = validate_runtime_dependencies,
|
dependency_validator: DependencyValidator | None = None,
|
||||||
|
credential_backend: str = HOST_CREDENTIAL_BACKEND,
|
||||||
|
execution_mode: str = HOST_EXECUTION_MODE,
|
||||||
|
effective_uid: int | None = None,
|
||||||
|
effective_gid: int | None = None,
|
||||||
|
http_transport: HttpTransport = urllib_http_transport,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
try:
|
try:
|
||||||
run_id = validate_run_id(run_id)
|
run_id = validate_run_id(run_id)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise VerificationError("run_id_invalid", "arguments") from None
|
raise VerificationError("run_id_invalid", "arguments") from None
|
||||||
|
|
||||||
if effective_user is None:
|
backend_pair = (credential_backend, execution_mode)
|
||||||
try:
|
if backend_pair not in {
|
||||||
effective_user = pwd.getpwuid(os.geteuid()).pw_name
|
(HOST_CREDENTIAL_BACKEND, HOST_EXECUTION_MODE),
|
||||||
except KeyError:
|
(CONTAINER_CREDENTIAL_BACKEND, CONTAINER_EXECUTION_MODE),
|
||||||
raise VerificationError("unix_user_unknown", "runtime_user") from None
|
}:
|
||||||
if effective_user != RUNTIME_UNIX_USER:
|
raise VerificationError("credential_execution_mode_mismatch", "arguments")
|
||||||
raise VerificationError("unexpected_unix_user", "runtime_user")
|
|
||||||
|
service_account: str | None = None
|
||||||
|
if execution_mode == HOST_EXECUTION_MODE:
|
||||||
|
if effective_user is None:
|
||||||
|
try:
|
||||||
|
effective_user = pwd.getpwuid(os.geteuid()).pw_name
|
||||||
|
except KeyError:
|
||||||
|
raise VerificationError("unix_user_unknown", "runtime_user") from None
|
||||||
|
if effective_user != RUNTIME_UNIX_USER:
|
||||||
|
raise VerificationError("unexpected_unix_user", "runtime_user")
|
||||||
|
else:
|
||||||
|
actual_uid = os.geteuid() if effective_uid is None else effective_uid
|
||||||
|
actual_gid = os.getegid() if effective_gid is None else effective_gid
|
||||||
|
if actual_uid != CONTAINER_RUNTIME_UID or actual_gid != CONTAINER_RUNTIME_GID:
|
||||||
|
raise VerificationError("unexpected_container_identity", "runtime_user")
|
||||||
|
|
||||||
|
if dependency_validator is None:
|
||||||
|
dependency_validator = (
|
||||||
|
validate_runtime_dependencies
|
||||||
|
if execution_mode == HOST_EXECUTION_MODE
|
||||||
|
else validate_container_runtime_dependencies
|
||||||
|
)
|
||||||
dependency_validator()
|
dependency_validator()
|
||||||
|
|
||||||
source = os.environ if base_env is None else base_env
|
source = os.environ if base_env is None else base_env
|
||||||
source_ref = f"leo-runtime-permission:{run_id}"
|
source_ref = f"leo-runtime-permission:{run_id}"
|
||||||
generated_at = datetime.now(timezone.utc).isoformat(timespec="seconds").replace("+00:00", "Z")
|
generated_at = datetime.now(timezone.utc).isoformat(timespec="seconds").replace("+00:00", "Z")
|
||||||
|
|
||||||
with nullcontext(selected_cloudsdk_config_path()) as config_dir:
|
config_context = (
|
||||||
gcloud_env = _gcloud_environment(source, config_dir)
|
nullcontext(selected_cloudsdk_config_path())
|
||||||
password = _read_scoped_password(env=gcloud_env, runner=runner)
|
if credential_backend == HOST_CREDENTIAL_BACKEND
|
||||||
|
else nullcontext(None)
|
||||||
|
)
|
||||||
|
with config_context as config_dir:
|
||||||
|
if credential_backend == HOST_CREDENTIAL_BACKEND:
|
||||||
|
if config_dir is None:
|
||||||
|
raise VerificationError("runtime_dependency_untrusted", "runtime_dependencies")
|
||||||
|
gcloud_env = _gcloud_environment(source, config_dir)
|
||||||
|
password = _read_scoped_password(env=gcloud_env, runner=runner)
|
||||||
|
metadata_access_token: str | None = None
|
||||||
|
else:
|
||||||
|
gcloud_env = None
|
||||||
|
service_account = _read_metadata_identity(transport=http_transport)
|
||||||
|
metadata_access_token = _read_metadata_access_token(transport=http_transport)
|
||||||
|
password = _read_scoped_password_from_metadata(metadata_access_token, transport=http_transport)
|
||||||
psql_env = _psql_environment(source, password)
|
psql_env = _psql_environment(source, password)
|
||||||
|
|
||||||
identity = _assert_identity(
|
identity = _assert_identity(
|
||||||
|
|
@ -2438,7 +2800,49 @@ def verify_runtime_permissions(
|
||||||
negative_permissions = [
|
negative_permissions = [
|
||||||
_expect_sqlstate(check, env=psql_env, runner=runner) for check in _negative_checks(run_id, source_ref)
|
_expect_sqlstate(check, env=psql_env, runner=runner) for check in _negative_checks(run_id, source_ref)
|
||||||
]
|
]
|
||||||
administrator_secret = _assert_administrator_secret_denied(env=gcloud_env, runner=runner)
|
if credential_backend == HOST_CREDENTIAL_BACKEND:
|
||||||
|
if gcloud_env is None:
|
||||||
|
raise VerificationError("runtime_dependency_untrusted", "runtime_dependencies")
|
||||||
|
administrator_secret = _assert_administrator_secret_denied(env=gcloud_env, runner=runner)
|
||||||
|
else:
|
||||||
|
if metadata_access_token is None:
|
||||||
|
raise VerificationError("metadata_access_token_invalid", "metadata_access_token")
|
||||||
|
administrator_secret = _assert_administrator_secret_denied_from_metadata(
|
||||||
|
metadata_access_token,
|
||||||
|
transport=http_transport,
|
||||||
|
)
|
||||||
|
|
||||||
|
execution_receipt: dict[str, Any] = {
|
||||||
|
"arbitrary_database_writes_denied": False,
|
||||||
|
"canonical_writes_committed": False,
|
||||||
|
"direct_relation_writes_denied": True,
|
||||||
|
"service_independent": True,
|
||||||
|
"unix_user": RUNTIME_UNIX_USER,
|
||||||
|
}
|
||||||
|
scoped_secret_access: dict[str, Any] = {
|
||||||
|
"result": "readable",
|
||||||
|
"secret": SCOPED_PASSWORD_SECRET,
|
||||||
|
"value_retained": False,
|
||||||
|
}
|
||||||
|
if execution_mode == CONTAINER_EXECUTION_MODE:
|
||||||
|
execution_receipt.update(
|
||||||
|
{
|
||||||
|
"container_bound": True,
|
||||||
|
"credential_backend": CONTAINER_CREDENTIAL_BACKEND,
|
||||||
|
"execution_mode": CONTAINER_EXECUTION_MODE,
|
||||||
|
"gid": CONTAINER_RUNTIME_GID,
|
||||||
|
"service_account": service_account,
|
||||||
|
"service_independent": False,
|
||||||
|
"unix_user": None,
|
||||||
|
"uid": CONTAINER_RUNTIME_UID,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
scoped_secret_access.update(
|
||||||
|
{
|
||||||
|
"credential_backend": CONTAINER_CREDENTIAL_BACKEND,
|
||||||
|
"identity_source": "gce_metadata",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"artifact": ARTIFACT_NAME,
|
"artifact": ARTIFACT_NAME,
|
||||||
|
|
@ -2464,13 +2868,7 @@ def verify_runtime_permissions(
|
||||||
},
|
},
|
||||||
"current_tier": REQUIRED_TIER,
|
"current_tier": REQUIRED_TIER,
|
||||||
"database_identity": identity,
|
"database_identity": identity,
|
||||||
"execution": {
|
"execution": execution_receipt,
|
||||||
"arbitrary_database_writes_denied": False,
|
|
||||||
"canonical_writes_committed": False,
|
|
||||||
"direct_relation_writes_denied": True,
|
|
||||||
"service_independent": True,
|
|
||||||
"unix_user": RUNTIME_UNIX_USER,
|
|
||||||
},
|
|
||||||
"generated_at_utc": generated_at,
|
"generated_at_utc": generated_at,
|
||||||
"mode": "live_private_gcp_staging",
|
"mode": "live_private_gcp_staging",
|
||||||
"required_tier": REQUIRED_TIER,
|
"required_tier": REQUIRED_TIER,
|
||||||
|
|
@ -2481,11 +2879,7 @@ def verify_runtime_permissions(
|
||||||
**administrator_secret,
|
**administrator_secret,
|
||||||
"secret": ADMINISTRATOR_PASSWORD_SECRET,
|
"secret": ADMINISTRATOR_PASSWORD_SECRET,
|
||||||
},
|
},
|
||||||
"scoped_runtime": {
|
"scoped_runtime": scoped_secret_access,
|
||||||
"result": "readable",
|
|
||||||
"secret": SCOPED_PASSWORD_SECRET,
|
|
||||||
"value_retained": False,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
"status": "pass",
|
"status": "pass",
|
||||||
"safety": {
|
"safety": {
|
||||||
|
|
@ -2550,13 +2944,27 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||||
parser = argparse.ArgumentParser(description=__doc__)
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
parser.add_argument("--run-id", required=True, type=_run_id_arg)
|
parser.add_argument("--run-id", required=True, type=_run_id_arg)
|
||||||
parser.add_argument("--output", type=Path, help="optional sanitized JSON receipt (written mode 0600)")
|
parser.add_argument("--output", type=Path, help="optional sanitized JSON receipt (written mode 0600)")
|
||||||
|
parser.add_argument(
|
||||||
|
"--credential-backend",
|
||||||
|
choices=(HOST_CREDENTIAL_BACKEND, CONTAINER_CREDENTIAL_BACKEND),
|
||||||
|
default=HOST_CREDENTIAL_BACKEND,
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--execution-mode",
|
||||||
|
choices=(HOST_EXECUTION_MODE, CONTAINER_EXECUTION_MODE),
|
||||||
|
default=HOST_EXECUTION_MODE,
|
||||||
|
)
|
||||||
return parser.parse_args(argv)
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
|
|
||||||
def main(argv: list[str] | None = None) -> int:
|
def main(argv: list[str] | None = None) -> int:
|
||||||
args = parse_args(argv)
|
args = parse_args(argv)
|
||||||
try:
|
try:
|
||||||
receipt = verify_runtime_permissions(args.run_id)
|
receipt = verify_runtime_permissions(
|
||||||
|
args.run_id,
|
||||||
|
credential_backend=args.credential_backend,
|
||||||
|
execution_mode=args.execution_mode,
|
||||||
|
)
|
||||||
returncode = 0
|
returncode = 0
|
||||||
except VerificationError as exc:
|
except VerificationError as exc:
|
||||||
receipt = failure_receipt(args.run_id, exc)
|
receipt = failure_receipt(args.run_id, exc)
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,8 @@ from ops import gcp_leoclean_nosend_package as package
|
||||||
SCHEMA = "livingip.leocleanNoSendOciSmoke.v1"
|
SCHEMA = "livingip.leocleanNoSendOciSmoke.v1"
|
||||||
SUCCESS_CLAIM_CEILING = (
|
SUCCESS_CLAIM_CEILING = (
|
||||||
"One disposable image built from a synthetic noncanonical identity started, reached its built-in no-send health "
|
"One disposable image built from a synthetic noncanonical identity started, reached its built-in no-send health "
|
||||||
"contract, rejected an unsupported send-message entrypoint command, and was removed. This does not prove a real "
|
"contract, rejected an unsupported send-message entrypoint command, failed closed when its container-bound "
|
||||||
|
"database verifier had no GCP authority, and was removed. This does not prove a real "
|
||||||
"Hermes tool-call denial, general network-egress denial, canonical identity, Cloud SQL permissions, Artifact "
|
"Hermes tool-call denial, general network-egress denial, canonical identity, Cloud SQL permissions, Artifact "
|
||||||
"Registry publication, GCP/systemd behavior, model parity, Telegram isolation in a live service, or production "
|
"Registry publication, GCP/systemd behavior, model parity, Telegram isolation in a live service, or production "
|
||||||
"readiness. Only the candidate container, tag, and visible image ID are required absent; digest-pinned base images "
|
"readiness. Only the candidate container, tag, and visible image ID are required absent; digest-pinned base images "
|
||||||
|
|
@ -247,6 +248,35 @@ def _verify_unsupported_send(name: str) -> dict[str, Any]:
|
||||||
return {"exit_code": result.returncode, "stdout_empty": True, "redacted_error": error}
|
return {"exit_code": result.returncode, "stdout_empty": True, "redacted_error": error}
|
||||||
|
|
||||||
|
|
||||||
|
def _verify_database_without_gcp_authority(name: str) -> dict[str, Any]:
|
||||||
|
result = _run(
|
||||||
|
[
|
||||||
|
"docker",
|
||||||
|
"exec",
|
||||||
|
"--user",
|
||||||
|
f"{package.RUNTIME_UID}:{package.RUNTIME_GID}",
|
||||||
|
name,
|
||||||
|
RUNTIME_PYTHON,
|
||||||
|
ENTRYPOINT,
|
||||||
|
"verify-database",
|
||||||
|
"offline-proof-0001",
|
||||||
|
],
|
||||||
|
check=False,
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
|
_require(result.returncode == 65, "unauthorized database verification did not fail closed")
|
||||||
|
_require(result.stdout == "", "unauthorized database verification emitted stdout")
|
||||||
|
try:
|
||||||
|
error = json.loads(result.stderr)
|
||||||
|
except json.JSONDecodeError as exc:
|
||||||
|
raise SmokeError("unauthorized database verification did not return redacted JSON") from exc
|
||||||
|
_require(
|
||||||
|
error == {"status": "fail", "error": "container_contract_failed"},
|
||||||
|
"unauthorized database verification failure was not the exact redacted contract error",
|
||||||
|
)
|
||||||
|
return {"exit_code": result.returncode, "stdout_empty": True, "redacted_error": error}
|
||||||
|
|
||||||
|
|
||||||
def _cleanup(*, name: str, tag: str, image_id: str | None, smoke_label: str) -> dict[str, Any]:
|
def _cleanup(*, name: str, tag: str, image_id: str | None, smoke_label: str) -> dict[str, Any]:
|
||||||
_run(["docker", "rm", "--force", name], check=False, timeout=45)
|
_run(["docker", "rm", "--force", name], check=False, timeout=45)
|
||||||
_run(["docker", "image", "rm", "--force", tag], check=False, timeout=90)
|
_run(["docker", "image", "rm", "--force", tag], check=False, timeout=90)
|
||||||
|
|
@ -352,6 +382,7 @@ def run_smoke(*, repo_root: Path, artifact: Path, runtime_receipt: Path, output:
|
||||||
health_wait = _wait_healthy(name, expected_image_id=image_id)
|
health_wait = _wait_healthy(name, expected_image_id=image_id)
|
||||||
health = _verify_health(name)
|
health = _verify_health(name)
|
||||||
unsupported = _verify_unsupported_send(name)
|
unsupported = _verify_unsupported_send(name)
|
||||||
|
unauthorized_database = _verify_database_without_gcp_authority(name)
|
||||||
_run(["docker", "stop", "--time=30", name], timeout=60)
|
_run(["docker", "stop", "--time=30", name], timeout=60)
|
||||||
deadline = time.monotonic() + 60
|
deadline = time.monotonic() + 60
|
||||||
while time.monotonic() < deadline:
|
while time.monotonic() < deadline:
|
||||||
|
|
@ -374,6 +405,7 @@ def run_smoke(*, repo_root: Path, artifact: Path, runtime_receipt: Path, output:
|
||||||
"health_wait": health_wait,
|
"health_wait": health_wait,
|
||||||
"health": health,
|
"health": health,
|
||||||
"unsupported_send_message": unsupported,
|
"unsupported_send_message": unsupported,
|
||||||
|
"unauthorized_database_verification": unauthorized_database,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
exit_code = 0
|
exit_code = 0
|
||||||
|
|
|
||||||
|
|
@ -137,10 +137,29 @@ def test_unsupported_send_requires_exact_redacted_fail_closed_result(monkeypatch
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_database_verifier_without_gcp_authority_is_exactly_redacted(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
result = subprocess.CompletedProcess(
|
||||||
|
args=["docker"],
|
||||||
|
returncode=65,
|
||||||
|
stdout="",
|
||||||
|
stderr=json.dumps({"status": "fail", "error": "container_contract_failed"}) + "\n",
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(smoke, "_run", lambda *_args, **_kwargs: result)
|
||||||
|
|
||||||
|
receipt = smoke._verify_database_without_gcp_authority("fixture")
|
||||||
|
|
||||||
|
assert receipt == {
|
||||||
|
"exit_code": 65,
|
||||||
|
"stdout_empty": True,
|
||||||
|
"redacted_error": {"status": "fail", "error": "container_contract_failed"},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_claim_ceiling_does_not_overstate_entrypoint_dispatch_probe() -> None:
|
def test_claim_ceiling_does_not_overstate_entrypoint_dispatch_probe() -> None:
|
||||||
assert "does not prove a real Hermes tool-call denial" in smoke.SUCCESS_CLAIM_CEILING
|
assert "does not prove a real Hermes tool-call denial" in smoke.SUCCESS_CLAIM_CEILING
|
||||||
assert "general network-egress denial" in smoke.SUCCESS_CLAIM_CEILING
|
assert "general network-egress denial" in smoke.SUCCESS_CLAIM_CEILING
|
||||||
assert "Cloud SQL permissions" in smoke.SUCCESS_CLAIM_CEILING
|
assert "Cloud SQL permissions" in smoke.SUCCESS_CLAIM_CEILING
|
||||||
|
assert "container-bound database verifier had no GCP authority" in smoke.SUCCESS_CLAIM_CEILING
|
||||||
assert "production readiness" in smoke.SUCCESS_CLAIM_CEILING
|
assert "production readiness" in smoke.SUCCESS_CLAIM_CEILING
|
||||||
assert "base images and build cache may remain" in smoke.SUCCESS_CLAIM_CEILING
|
assert "base images and build cache may remain" in smoke.SUCCESS_CLAIM_CEILING
|
||||||
assert "makes no affirmative image-build" in smoke.FAILURE_CLAIM_CEILING
|
assert "makes no affirmative image-build" in smoke.FAILURE_CLAIM_CEILING
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,10 @@ def load_entrypoint_module(tmp_path: Path):
|
||||||
entrypoint_path = image_root / "entrypoint.py"
|
entrypoint_path = image_root / "entrypoint.py"
|
||||||
shutil.copy2(ROOT / "docker" / "leoclean-nosend" / "entrypoint.py", entrypoint_path)
|
shutil.copy2(ROOT / "docker" / "leoclean-nosend" / "entrypoint.py", entrypoint_path)
|
||||||
shutil.copy2(ROOT / "ops" / "gcp_leoclean_nosend_package.py", image_root / "package_contract.py")
|
shutil.copy2(ROOT / "ops" / "gcp_leoclean_nosend_package.py", image_root / "package_contract.py")
|
||||||
|
shutil.copy2(
|
||||||
|
ROOT / "ops" / "verify_gcp_leoclean_runtime_permissions.py",
|
||||||
|
image_root / "runtime_permissions_verifier.py",
|
||||||
|
)
|
||||||
scripts = image_root / "scripts"
|
scripts = image_root / "scripts"
|
||||||
scripts.mkdir()
|
scripts.mkdir()
|
||||||
for relative in package.IDENTITY_CONTRACT_SOURCES:
|
for relative in package.IDENTITY_CONTRACT_SOURCES:
|
||||||
|
|
@ -3766,6 +3770,7 @@ def test_prepare_context_is_secret_free_normalized_and_idempotently_refuses_over
|
||||||
"identity-sources",
|
"identity-sources",
|
||||||
"image-input.json",
|
"image-input.json",
|
||||||
"package_contract.py",
|
"package_contract.py",
|
||||||
|
"runtime_permissions_verifier.py",
|
||||||
"scripts",
|
"scripts",
|
||||||
]
|
]
|
||||||
assert all((output / "identity" / name).stat().st_mode & 0o777 == 0o600 for name in package.IDENTITY_FILES)
|
assert all((output / "identity" / name).stat().st_mode & 0o777 == 0o600 for name in package.IDENTITY_FILES)
|
||||||
|
|
@ -3803,6 +3808,8 @@ def test_dockerfile_has_only_digest_pinned_bases_and_no_floating_package_install
|
||||||
assert "HEALTHCHECK" in dockerfile
|
assert "HEALTHCHECK" in dockerfile
|
||||||
assert "verify-build" in dockerfile
|
assert "verify-build" in dockerfile
|
||||||
assert "identity -type f -exec chmod 0444" in dockerfile
|
assert "identity -type f -exec chmod 0444" in dockerfile
|
||||||
|
assert "COPY runtime_permissions_verifier.py" in dockerfile
|
||||||
|
assert "chmod 0444 /opt/livingip/leoclean-nosend/runtime_permissions_verifier.py" in dockerfile
|
||||||
|
|
||||||
|
|
||||||
def test_image_restores_the_digest_pinned_default_tls_trust_contract() -> None:
|
def test_image_restores_the_digest_pinned_default_tls_trust_contract() -> None:
|
||||||
|
|
@ -3853,6 +3860,70 @@ def test_entrypoint_prepares_exact_profile_drops_privileges_and_health_checks_pi
|
||||||
assert "os.execve" in source
|
assert "os.execve" in source
|
||||||
assert "OPENROUTER_API_KEY" not in source
|
assert "OPENROUTER_API_KEY" not in source
|
||||||
assert "TELEGRAM_BOT_TOKEN" not in source
|
assert "TELEGRAM_BOT_TOKEN" not in source
|
||||||
|
assert 'command == "verify-database"' in source
|
||||||
|
assert 'credential_backend="metadata"' in source
|
||||||
|
assert 'execution_mode="container"' in source
|
||||||
|
|
||||||
|
|
||||||
|
def test_entrypoint_container_database_verification_binds_image_and_inner_receipt(
|
||||||
|
tmp_path: Path,
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
entrypoint = load_entrypoint_module(tmp_path)
|
||||||
|
calls: list[tuple[str, str, str]] = []
|
||||||
|
|
||||||
|
def fake_verify(run_id: str, *, credential_backend: str, execution_mode: str) -> dict[str, object]:
|
||||||
|
calls.append((run_id, credential_backend, execution_mode))
|
||||||
|
return {"status": "pass", "run_id": run_id, "secret_access": {"value_retained": False}}
|
||||||
|
|
||||||
|
monkeypatch.setattr(entrypoint, "_drop_privileges", lambda: None)
|
||||||
|
monkeypatch.setattr(entrypoint.runtime_permissions_verifier, "verify_runtime_permissions", fake_verify)
|
||||||
|
image_input = {
|
||||||
|
"runtime": {"teleo_git_head": "1" * 40, "artifact_sha256": "2" * 64},
|
||||||
|
"identity": {"bundle_sha256": "3" * 64},
|
||||||
|
"source_bindings": {"runtime_permissions_verifier.py": "4" * 64},
|
||||||
|
"input_sha256": "5" * 64,
|
||||||
|
}
|
||||||
|
|
||||||
|
receipt = entrypoint._database_verification(image_input, "proof-run-0001")
|
||||||
|
|
||||||
|
assert calls == [("proof-run-0001", "metadata", "container")]
|
||||||
|
assert receipt["schema"] == "livingip.leocleanNoSendContainerDatabaseVerification.v1"
|
||||||
|
assert receipt["container"] == {
|
||||||
|
"teleo_git_head": "1" * 40,
|
||||||
|
"artifact_sha256": "2" * 64,
|
||||||
|
"identity_sha256": "3" * 64,
|
||||||
|
"input_sha256": "5" * 64,
|
||||||
|
"runtime_permissions_verifier_sha256": "4" * 64,
|
||||||
|
}
|
||||||
|
stable = {key: value for key, value in receipt.items() if key != "receipt_sha256"}
|
||||||
|
assert receipt["receipt_sha256"] == entrypoint.contract.canonical_sha256(stable)
|
||||||
|
|
||||||
|
|
||||||
|
def test_entrypoint_container_database_verification_redacts_internal_failure(
|
||||||
|
tmp_path: Path,
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
entrypoint = load_entrypoint_module(tmp_path)
|
||||||
|
|
||||||
|
def fail(*_args, **_kwargs):
|
||||||
|
raise RuntimeError("password=must-not-leak")
|
||||||
|
|
||||||
|
monkeypatch.setattr(entrypoint, "_drop_privileges", lambda: None)
|
||||||
|
monkeypatch.setattr(entrypoint.runtime_permissions_verifier, "verify_runtime_permissions", fail)
|
||||||
|
|
||||||
|
with pytest.raises(entrypoint.EntrypointError, match="container database verification failed") as exc_info:
|
||||||
|
entrypoint._database_verification(
|
||||||
|
{
|
||||||
|
"runtime": {"teleo_git_head": "1" * 40, "artifact_sha256": "2" * 64},
|
||||||
|
"identity": {"bundle_sha256": "3" * 64},
|
||||||
|
"source_bindings": {"runtime_permissions_verifier.py": "4" * 64},
|
||||||
|
"input_sha256": "5" * 64,
|
||||||
|
},
|
||||||
|
"proof-run-0001",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "must-not-leak" not in str(exc_info.value)
|
||||||
|
|
||||||
|
|
||||||
def test_entrypoint_readiness_binds_packaged_and_live_config(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
def test_entrypoint_readiness_binds_packaged_and_live_config(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
|
@ -3957,6 +4028,10 @@ def test_entrypoint_loads_adjacent_contract_with_python_safe_path(tmp_path: Path
|
||||||
entrypoint = image_root / "entrypoint.py"
|
entrypoint = image_root / "entrypoint.py"
|
||||||
shutil.copy2(ROOT / "docker" / "leoclean-nosend" / "entrypoint.py", entrypoint)
|
shutil.copy2(ROOT / "docker" / "leoclean-nosend" / "entrypoint.py", entrypoint)
|
||||||
shutil.copy2(ROOT / "ops" / "gcp_leoclean_nosend_package.py", image_root / "package_contract.py")
|
shutil.copy2(ROOT / "ops" / "gcp_leoclean_nosend_package.py", image_root / "package_contract.py")
|
||||||
|
shutil.copy2(
|
||||||
|
ROOT / "ops" / "verify_gcp_leoclean_runtime_permissions.py",
|
||||||
|
image_root / "runtime_permissions_verifier.py",
|
||||||
|
)
|
||||||
scripts = image_root / "scripts"
|
scripts = image_root / "scripts"
|
||||||
scripts.mkdir()
|
scripts.mkdir()
|
||||||
for relative in package.IDENTITY_CONTRACT_SOURCES:
|
for relative in package.IDENTITY_CONTRACT_SOURCES:
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import base64
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
|
@ -15,9 +16,78 @@ from ops import verify_gcp_leoclean_runtime_permissions as verifier
|
||||||
RUN_ID = "20260715-leo-security"
|
RUN_ID = "20260715-leo-security"
|
||||||
SOURCE_REF = f"leo-runtime-permission:{RUN_ID}"
|
SOURCE_REF = f"leo-runtime-permission:{RUN_ID}"
|
||||||
SECRET_VALUE = "unit-only-secret-value%42"
|
SECRET_VALUE = "unit-only-secret-value%42"
|
||||||
|
METADATA_TOKEN = "unit-only-metadata-token%42"
|
||||||
AGENT_ID = "11111111-1111-4111-8111-111111111111"
|
AGENT_ID = "11111111-1111-4111-8111-111111111111"
|
||||||
|
|
||||||
|
|
||||||
|
class FakeHttpTransport:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
service_account: str = verifier.STAGING_RUNTIME_SERVICE_ACCOUNT,
|
||||||
|
token_body: bytes | None = None,
|
||||||
|
scoped_body: bytes | None = None,
|
||||||
|
scoped_status: int = 200,
|
||||||
|
administrator_body: bytes | None = None,
|
||||||
|
administrator_status: int = 403,
|
||||||
|
metadata_header: str | None = "Google",
|
||||||
|
raise_for: str | None = None,
|
||||||
|
) -> None:
|
||||||
|
self.service_account = service_account
|
||||||
|
self.token_body = token_body or json.dumps(
|
||||||
|
{"access_token": METADATA_TOKEN, "expires_in": 300, "token_type": "Bearer"}
|
||||||
|
).encode()
|
||||||
|
self.scoped_body = scoped_body or json.dumps(
|
||||||
|
{
|
||||||
|
"payload": {"data": base64.b64encode(SECRET_VALUE.encode()).decode()},
|
||||||
|
"provider_noise": METADATA_TOKEN,
|
||||||
|
}
|
||||||
|
).encode()
|
||||||
|
self.scoped_status = scoped_status
|
||||||
|
self.administrator_body = administrator_body or json.dumps(
|
||||||
|
{
|
||||||
|
"error": {
|
||||||
|
"code": 403,
|
||||||
|
"message": (
|
||||||
|
f"Permission '{verifier.IAM_PERMISSION}' denied for administrator secret; "
|
||||||
|
f"{SECRET_VALUE}; {METADATA_TOKEN}"
|
||||||
|
),
|
||||||
|
"status": "PERMISSION_DENIED",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
).encode()
|
||||||
|
self.administrator_status = administrator_status
|
||||||
|
self.metadata_header = metadata_header
|
||||||
|
self.raise_for = raise_for
|
||||||
|
self.calls: list[tuple[str, dict[str, str], float, int]] = []
|
||||||
|
|
||||||
|
def __call__(
|
||||||
|
self,
|
||||||
|
url: str,
|
||||||
|
headers: Mapping[str, str],
|
||||||
|
timeout: float,
|
||||||
|
max_bytes: int,
|
||||||
|
) -> verifier.HttpResponse:
|
||||||
|
self.calls.append((url, dict(headers), timeout, max_bytes))
|
||||||
|
if self.raise_for is not None and self.raise_for in url:
|
||||||
|
raise RuntimeError(f"provider failure {SECRET_VALUE} {METADATA_TOKEN}")
|
||||||
|
if url == verifier._metadata_url("instance/service-accounts/default/email"):
|
||||||
|
response_headers = {} if self.metadata_header is None else {"Metadata-Flavor": self.metadata_header}
|
||||||
|
return verifier.HttpResponse(200, response_headers, self.service_account.encode())
|
||||||
|
if url == verifier._metadata_url("instance/service-accounts/default/token"):
|
||||||
|
response_headers = {} if self.metadata_header is None else {"metadata-flavor": self.metadata_header}
|
||||||
|
return verifier.HttpResponse(200, response_headers, self.token_body)
|
||||||
|
if url == verifier._secret_access_url(verifier.SCOPED_PASSWORD_SECRET):
|
||||||
|
return verifier.HttpResponse(self.scoped_status, {"content-type": "application/json"}, self.scoped_body)
|
||||||
|
if url == verifier._secret_access_url(verifier.ADMINISTRATOR_PASSWORD_SECRET):
|
||||||
|
return verifier.HttpResponse(
|
||||||
|
self.administrator_status,
|
||||||
|
{"content-type": "application/json"},
|
||||||
|
self.administrator_body,
|
||||||
|
)
|
||||||
|
raise AssertionError(f"unexpected URL: {url}")
|
||||||
|
|
||||||
|
|
||||||
class FakeRunner:
|
class FakeRunner:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
|
@ -270,6 +340,23 @@ def run_success(runner: FakeRunner) -> dict[str, object]:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_container_success(
|
||||||
|
runner: FakeRunner,
|
||||||
|
transport: FakeHttpTransport,
|
||||||
|
) -> dict[str, object]:
|
||||||
|
return verifier.verify_runtime_permissions(
|
||||||
|
RUN_ID,
|
||||||
|
runner=runner,
|
||||||
|
base_env=poisoned_environment(),
|
||||||
|
dependency_validator=lambda: None,
|
||||||
|
credential_backend=verifier.CONTAINER_CREDENTIAL_BACKEND,
|
||||||
|
execution_mode=verifier.CONTAINER_EXECUTION_MODE,
|
||||||
|
effective_uid=verifier.CONTAINER_RUNTIME_UID,
|
||||||
|
effective_gid=verifier.CONTAINER_RUNTIME_GID,
|
||||||
|
http_transport=transport,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def assert_child_boundaries(runner: FakeRunner) -> None:
|
def assert_child_boundaries(runner: FakeRunner) -> None:
|
||||||
assert runner.calls
|
assert runner.calls
|
||||||
for command, env, timeout, discard_stdout in runner.calls:
|
for command, env, timeout, discard_stdout in runner.calls:
|
||||||
|
|
@ -426,6 +513,305 @@ def test_live_receipt_contract_is_sanitized_and_uses_exact_child_environments()
|
||||||
assert administrator_call[3] is True
|
assert administrator_call[3] is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_container_metadata_backend_binds_exact_identity_and_reuses_sql_contract() -> None:
|
||||||
|
runner = FakeRunner()
|
||||||
|
transport = FakeHttpTransport()
|
||||||
|
|
||||||
|
receipt = run_container_success(runner, transport)
|
||||||
|
serialized = verifier.canonical_json(receipt)
|
||||||
|
|
||||||
|
assert receipt["status"] == "pass"
|
||||||
|
assert receipt["execution"] == {
|
||||||
|
"arbitrary_database_writes_denied": False,
|
||||||
|
"canonical_writes_committed": False,
|
||||||
|
"container_bound": True,
|
||||||
|
"credential_backend": verifier.CONTAINER_CREDENTIAL_BACKEND,
|
||||||
|
"direct_relation_writes_denied": True,
|
||||||
|
"execution_mode": verifier.CONTAINER_EXECUTION_MODE,
|
||||||
|
"gid": verifier.CONTAINER_RUNTIME_GID,
|
||||||
|
"service_account": verifier.STAGING_RUNTIME_SERVICE_ACCOUNT,
|
||||||
|
"service_independent": False,
|
||||||
|
"uid": verifier.CONTAINER_RUNTIME_UID,
|
||||||
|
"unix_user": None,
|
||||||
|
}
|
||||||
|
assert receipt["secret_access"]["scoped_runtime"] == {
|
||||||
|
"credential_backend": verifier.CONTAINER_CREDENTIAL_BACKEND,
|
||||||
|
"identity_source": "gce_metadata",
|
||||||
|
"result": "readable",
|
||||||
|
"secret": verifier.SCOPED_PASSWORD_SECRET,
|
||||||
|
"value_retained": False,
|
||||||
|
}
|
||||||
|
assert receipt["secret_access"]["administrator"] == {
|
||||||
|
"classification": "iam_permission_denied",
|
||||||
|
"permission": verifier.IAM_PERMISSION,
|
||||||
|
"response_body_retained": False,
|
||||||
|
"result": "denied",
|
||||||
|
"secret": verifier.ADMINISTRATOR_PASSWORD_SECRET,
|
||||||
|
"stdout_discarded": True,
|
||||||
|
}
|
||||||
|
assert SECRET_VALUE not in serialized
|
||||||
|
assert METADATA_TOKEN not in serialized
|
||||||
|
assert all(command[0] == verifier.PSQL_BIN for command, _env, _timeout, _discard in runner.calls)
|
||||||
|
assert_child_boundaries(runner)
|
||||||
|
|
||||||
|
assert [call[0] for call in transport.calls] == [
|
||||||
|
verifier._metadata_url("instance/service-accounts/default/email"),
|
||||||
|
verifier._metadata_url("instance/service-accounts/default/token"),
|
||||||
|
verifier._secret_access_url(verifier.SCOPED_PASSWORD_SECRET),
|
||||||
|
verifier._secret_access_url(verifier.ADMINISTRATOR_PASSWORD_SECRET),
|
||||||
|
]
|
||||||
|
for url, headers, timeout, _max_bytes in transport.calls:
|
||||||
|
assert timeout == verifier.HTTP_TIMEOUT_SECONDS
|
||||||
|
if url.startswith(verifier.METADATA_ROOT):
|
||||||
|
assert headers == verifier.METADATA_HEADERS
|
||||||
|
else:
|
||||||
|
assert headers == {"Accept": "application/json", "Authorization": f"Bearer {METADATA_TOKEN}"}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("credential_backend", "execution_mode"),
|
||||||
|
(
|
||||||
|
(verifier.HOST_CREDENTIAL_BACKEND, verifier.CONTAINER_EXECUTION_MODE),
|
||||||
|
(verifier.CONTAINER_CREDENTIAL_BACKEND, verifier.HOST_EXECUTION_MODE),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_credential_backend_and_execution_mode_cannot_be_crossed(
|
||||||
|
credential_backend: str,
|
||||||
|
execution_mode: str,
|
||||||
|
) -> None:
|
||||||
|
with pytest.raises(verifier.VerificationError) as caught:
|
||||||
|
verifier.verify_runtime_permissions(
|
||||||
|
RUN_ID,
|
||||||
|
credential_backend=credential_backend,
|
||||||
|
execution_mode=execution_mode,
|
||||||
|
dependency_validator=lambda: None,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert caught.value.code == "credential_execution_mode_mismatch"
|
||||||
|
assert caught.value.check == "arguments"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("uid", "gid"),
|
||||||
|
(
|
||||||
|
(0, verifier.CONTAINER_RUNTIME_GID),
|
||||||
|
(verifier.CONTAINER_RUNTIME_UID, 0),
|
||||||
|
(verifier.CONTAINER_RUNTIME_UID + 1, verifier.CONTAINER_RUNTIME_GID + 1),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_container_metadata_backend_requires_exact_numeric_identity(uid: int, gid: int) -> None:
|
||||||
|
runner = FakeRunner()
|
||||||
|
transport = FakeHttpTransport()
|
||||||
|
|
||||||
|
with pytest.raises(verifier.VerificationError) as caught:
|
||||||
|
verifier.verify_runtime_permissions(
|
||||||
|
RUN_ID,
|
||||||
|
runner=runner,
|
||||||
|
dependency_validator=lambda: None,
|
||||||
|
credential_backend=verifier.CONTAINER_CREDENTIAL_BACKEND,
|
||||||
|
execution_mode=verifier.CONTAINER_EXECUTION_MODE,
|
||||||
|
effective_uid=uid,
|
||||||
|
effective_gid=gid,
|
||||||
|
http_transport=transport,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert caught.value.code == "unexpected_container_identity"
|
||||||
|
assert caught.value.check == "runtime_user"
|
||||||
|
assert runner.calls == []
|
||||||
|
assert transport.calls == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_container_metadata_backend_selects_container_dependency_validation(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
selected: list[str] = []
|
||||||
|
monkeypatch.setattr(verifier, "validate_runtime_dependencies", lambda: selected.append("host"))
|
||||||
|
monkeypatch.setattr(verifier, "validate_container_runtime_dependencies", lambda: selected.append("container"))
|
||||||
|
|
||||||
|
run_container_success_without_dependency_override = verifier.verify_runtime_permissions(
|
||||||
|
RUN_ID,
|
||||||
|
runner=FakeRunner(),
|
||||||
|
base_env=poisoned_environment(),
|
||||||
|
credential_backend=verifier.CONTAINER_CREDENTIAL_BACKEND,
|
||||||
|
execution_mode=verifier.CONTAINER_EXECUTION_MODE,
|
||||||
|
effective_uid=verifier.CONTAINER_RUNTIME_UID,
|
||||||
|
effective_gid=verifier.CONTAINER_RUNTIME_GID,
|
||||||
|
http_transport=FakeHttpTransport(),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert run_container_success_without_dependency_override["status"] == "pass"
|
||||||
|
assert selected == ["container"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_metadata_service_account_must_match_before_token_or_database_access() -> None:
|
||||||
|
runner = FakeRunner()
|
||||||
|
transport = FakeHttpTransport(service_account="sa-teleo-prod-vm@teleo-501523.iam.gserviceaccount.com")
|
||||||
|
|
||||||
|
with pytest.raises(verifier.VerificationError) as caught:
|
||||||
|
run_container_success(runner, transport)
|
||||||
|
|
||||||
|
assert caught.value.code == "metadata_service_account_mismatch"
|
||||||
|
assert caught.value.check == "metadata_service_account_identity"
|
||||||
|
assert len(transport.calls) == 1
|
||||||
|
assert runner.calls == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_metadata_response_requires_the_google_flavor_header() -> None:
|
||||||
|
runner = FakeRunner()
|
||||||
|
transport = FakeHttpTransport(metadata_header=None)
|
||||||
|
|
||||||
|
with pytest.raises(verifier.VerificationError) as caught:
|
||||||
|
run_container_success(runner, transport)
|
||||||
|
|
||||||
|
assert caught.value.code == "metadata_service_account_identity_failed"
|
||||||
|
assert SECRET_VALUE not in str(caught.value)
|
||||||
|
assert METADATA_TOKEN not in str(caught.value)
|
||||||
|
assert runner.calls == []
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("raise_for", "expected_code"),
|
||||||
|
(
|
||||||
|
("default/email", "metadata_service_account_identity_failed"),
|
||||||
|
("default/token", "metadata_access_token_failed"),
|
||||||
|
(verifier.SCOPED_PASSWORD_SECRET, "scoped_secret_access_failed"),
|
||||||
|
(verifier.ADMINISTRATOR_PASSWORD_SECRET, "administrator_secret_denial_unavailable"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_metadata_http_failures_are_redacted_and_fail_closed(raise_for: str, expected_code: str) -> None:
|
||||||
|
runner = FakeRunner()
|
||||||
|
transport = FakeHttpTransport(raise_for=raise_for)
|
||||||
|
|
||||||
|
with pytest.raises(verifier.VerificationError) as caught:
|
||||||
|
run_container_success(runner, transport)
|
||||||
|
|
||||||
|
assert caught.value.code == expected_code
|
||||||
|
failure = verifier.canonical_json(verifier.failure_receipt(RUN_ID, caught.value))
|
||||||
|
assert SECRET_VALUE not in str(caught.value)
|
||||||
|
assert METADATA_TOKEN not in str(caught.value)
|
||||||
|
assert SECRET_VALUE not in failure
|
||||||
|
assert METADATA_TOKEN not in failure
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"token_body",
|
||||||
|
(
|
||||||
|
b'{"access_token":"first","access_token":"second","expires_in":300,"token_type":"Bearer"}',
|
||||||
|
b'{"access_token":"line\\ntoken","expires_in":300,"token_type":"Bearer"}',
|
||||||
|
b'{"access_token":"token","expires_in":true,"token_type":"Bearer"}',
|
||||||
|
b'[{"access_token":"token"}]',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_metadata_token_shape_is_strict_and_never_repeated(token_body: bytes) -> None:
|
||||||
|
runner = FakeRunner()
|
||||||
|
transport = FakeHttpTransport(token_body=token_body)
|
||||||
|
|
||||||
|
with pytest.raises(verifier.VerificationError) as caught:
|
||||||
|
run_container_success(runner, transport)
|
||||||
|
|
||||||
|
assert caught.value.code == "metadata_access_token_invalid"
|
||||||
|
assert token_body.decode(errors="ignore") not in str(caught.value)
|
||||||
|
assert runner.calls == []
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("scoped_status", "scoped_body", "expected_code"),
|
||||||
|
(
|
||||||
|
(403, b'{"error":{"status":"PERMISSION_DENIED"}}', "scoped_secret_access_failed"),
|
||||||
|
(200, b'{"payload":{"data":"%%%"}}', "scoped_secret_response_invalid"),
|
||||||
|
(
|
||||||
|
200,
|
||||||
|
json.dumps({"payload": {"data": base64.b64encode(b"line\nbreak").decode()}}).encode(),
|
||||||
|
"scoped_secret_value_invalid",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
200,
|
||||||
|
json.dumps({"payload": {"data": base64.b64encode(b"\xff").decode()}}).encode(),
|
||||||
|
"scoped_secret_value_invalid",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_scoped_secret_rest_response_is_bounded_strict_and_redacted(
|
||||||
|
scoped_status: int,
|
||||||
|
scoped_body: bytes,
|
||||||
|
expected_code: str,
|
||||||
|
) -> None:
|
||||||
|
runner = FakeRunner()
|
||||||
|
transport = FakeHttpTransport(scoped_status=scoped_status, scoped_body=scoped_body)
|
||||||
|
|
||||||
|
with pytest.raises(verifier.VerificationError) as caught:
|
||||||
|
run_container_success(runner, transport)
|
||||||
|
|
||||||
|
assert caught.value.code == expected_code
|
||||||
|
assert SECRET_VALUE not in str(caught.value)
|
||||||
|
assert METADATA_TOKEN not in str(caught.value)
|
||||||
|
assert runner.calls == []
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("status", "body", "expected_code"),
|
||||||
|
(
|
||||||
|
(200, b'{"payload":{"data":"must-not-be-retained"}}', "administrator_secret_unexpectedly_readable"),
|
||||||
|
(500, b'{"error":{"status":"INTERNAL","message":"provider noise"}}', "administrator_secret_denial_not_iam_permission"),
|
||||||
|
(
|
||||||
|
403,
|
||||||
|
b'{"error":{"code":403,"status":"PERMISSION_DENIED","message":"unrelated permission"}}',
|
||||||
|
"administrator_secret_denial_not_iam_permission",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_metadata_administrator_secret_requires_exact_iam_denial(
|
||||||
|
status: int,
|
||||||
|
body: bytes,
|
||||||
|
expected_code: str,
|
||||||
|
) -> None:
|
||||||
|
runner = FakeRunner()
|
||||||
|
transport = FakeHttpTransport(administrator_status=status, administrator_body=body)
|
||||||
|
|
||||||
|
with pytest.raises(verifier.VerificationError) as caught:
|
||||||
|
run_container_success(runner, transport)
|
||||||
|
|
||||||
|
assert caught.value.code == expected_code
|
||||||
|
failure = verifier.canonical_json(verifier.failure_receipt(RUN_ID, caught.value))
|
||||||
|
assert body.decode(errors="ignore") not in failure
|
||||||
|
assert SECRET_VALUE not in failure
|
||||||
|
assert METADATA_TOKEN not in failure
|
||||||
|
|
||||||
|
|
||||||
|
def test_http_response_size_limit_is_enforced_even_for_an_injected_transport() -> None:
|
||||||
|
runner = FakeRunner()
|
||||||
|
transport = FakeHttpTransport(token_body=b"x" * (verifier.MAX_METADATA_TOKEN_BYTES + 1))
|
||||||
|
|
||||||
|
with pytest.raises(verifier.VerificationError) as caught:
|
||||||
|
run_container_success(runner, transport)
|
||||||
|
|
||||||
|
assert caught.value.code == "metadata_access_token_failed"
|
||||||
|
assert runner.calls == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_cli_defaults_preserve_host_gcloud_and_container_pair_is_explicit() -> None:
|
||||||
|
host = verifier.parse_args(["--run-id", RUN_ID])
|
||||||
|
container = verifier.parse_args(
|
||||||
|
[
|
||||||
|
"--run-id",
|
||||||
|
RUN_ID,
|
||||||
|
"--credential-backend",
|
||||||
|
verifier.CONTAINER_CREDENTIAL_BACKEND,
|
||||||
|
"--execution-mode",
|
||||||
|
verifier.CONTAINER_EXECUTION_MODE,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
assert (host.credential_backend, host.execution_mode) == (
|
||||||
|
verifier.HOST_CREDENTIAL_BACKEND,
|
||||||
|
verifier.HOST_EXECUTION_MODE,
|
||||||
|
)
|
||||||
|
assert (container.credential_backend, container.execution_mode) == (
|
||||||
|
verifier.CONTAINER_CREDENTIAL_BACKEND,
|
||||||
|
verifier.CONTAINER_EXECUTION_MODE,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("attribute", "unsafe_value"),
|
("attribute", "unsafe_value"),
|
||||||
[
|
[
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue