90 lines
3.3 KiB
Python
90 lines
3.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Fail closed unless the live router reads the operator's revocation file.
|
|
|
|
Run this on the Docker host before minting or revoking a Fleet Ops demo token.
|
|
It reads Docker mount metadata and file metadata only; it never reads a token,
|
|
signing key, or revocation-file contents.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import stat
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
ROUTER_DIRECTORY = "/etc/itworx"
|
|
ROUTER_DESTINATION = ROUTER_DIRECTORY + "/demo-token-revocations.json"
|
|
|
|
|
|
def validate_mount(mounts: list[dict[str, Any]], expected_host_file: Path) -> dict[str, object]:
|
|
if not expected_host_file.is_absolute():
|
|
raise ValueError("expected host file must be an absolute path")
|
|
matching = [mount for mount in mounts if mount.get("Destination") == ROUTER_DIRECTORY]
|
|
if len(matching) != 1:
|
|
raise ValueError("router must have exactly one demo-token revocation mount")
|
|
source = Path(str(matching[0].get("Source", "")))
|
|
if source != expected_host_file.parent:
|
|
raise ValueError("router revocation directory mount does not match the operator host file")
|
|
try:
|
|
metadata = expected_host_file.stat()
|
|
except OSError as exc:
|
|
raise ValueError("operator revocation file is missing") from exc
|
|
if not stat.S_ISREG(metadata.st_mode):
|
|
raise ValueError("operator revocation file is not a regular file")
|
|
mode = stat.S_IMODE(metadata.st_mode)
|
|
# The operator command runs on Tower (Linux). Windows does not expose
|
|
# NTFS ACLs through POSIX mode bits, so local unit tests cannot make this
|
|
# assertion meaningful there.
|
|
if os.name != "nt" and mode != 0o600:
|
|
raise ValueError("operator revocation file mode must be exactly 0600")
|
|
if matching[0].get("RW") is not False:
|
|
raise ValueError("router revocation mount must be read-only")
|
|
return {
|
|
"status": "pass",
|
|
"containerDestination": ROUTER_DESTINATION,
|
|
"hostFile": str(expected_host_file),
|
|
"mode": f"{mode:04o}",
|
|
"readOnly": True,
|
|
}
|
|
|
|
|
|
def inspect_mounts(router_container: str) -> list[dict[str, Any]]:
|
|
result = subprocess.run( # noqa: S603
|
|
["docker", "inspect", router_container, "--format", "{{json .Mounts}}"], # noqa: S607
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
if result.returncode != 0:
|
|
raise ValueError("could not inspect router container")
|
|
try:
|
|
mounts = json.loads(result.stdout)
|
|
except json.JSONDecodeError as exc:
|
|
raise ValueError("router mount metadata is invalid") from exc
|
|
if not isinstance(mounts, list) or not all(isinstance(item, dict) for item in mounts):
|
|
raise ValueError("router mount metadata is invalid")
|
|
return mounts
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--router-container", required=True)
|
|
parser.add_argument("--expected-host-file", type=Path, required=True)
|
|
args = parser.parse_args()
|
|
try:
|
|
report = validate_mount(inspect_mounts(args.router_container), args.expected_host_file)
|
|
except ValueError as exc:
|
|
print(json.dumps({"status": "fail", "reason": str(exc)}, sort_keys=True))
|
|
return 1
|
|
print(json.dumps(report, sort_keys=True))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|