Files
pulse/scripts/release_browser_tailnet.py
T
pulse-triage[bot] 2fb4bf9eee Bind release browser qualification to the narrow CI identity
Use a dedicated Auth Keys write client restricted to tag:ci-deploy, never the broader demo registration. Reject missing configuration and validate the actual running Self tag and DNS before Serve. Keep exact-source, denial-stop, no-replay and cleanup contracts, with connected offline registration and Serve controls.

Change-source: pulse-maintainer
2026-10-04 04:57:17 +01:00

48 lines
1.5 KiB
Python

#!/usr/bin/env python3
"""Validate only the joined CI node's identity before exposing its loopback UI.
Reads local `tailscale status --json`; makes no network request and never emits
peer inventory, raw status or credentials. OAuth/ACL scope is configured by the
Tailnet owner, not inferred from a DNS name or a tag observation.
"""
import json
import re
import sys
MAX_STATUS_BYTES = 1024 * 1024
ERROR = "Release browser requires a running Self node with only tag:ci-deploy and a business-tailnet DNS name"
def self_origin(status):
if not isinstance(status, dict) or status.get("BackendState") != "Running":
raise ValueError(ERROR)
node = status.get("Self")
if (not isinstance(node, dict) or node.get("Online") is not True
or node.get("Tags") != ["tag:ci-deploy"]):
raise ValueError(ERROR)
name = node.get("DNSName")
if not isinstance(name, str):
raise ValueError(ERROR)
name = name.removesuffix(".")
if not re.fullmatch(r"[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.tawny-powan\.ts\.net", name):
raise ValueError(ERROR)
return "https://" + name
def main():
try:
raw = sys.stdin.buffer.read(MAX_STATUS_BYTES + 1)
if len(raw) > MAX_STATUS_BYTES:
raise ValueError(ERROR)
origin = self_origin(json.loads(raw))
except (ValueError, UnicodeError):
print(ERROR, file=sys.stderr)
return 2
print(origin)
return 0
if __name__ == "__main__":
sys.exit(main())