feat(runtime-exposure): least-privilege Tailscale HTTPS broker + shared contract (PAP-17051)

Foundation for opt-in `tailscale_https` exposure of managed branch runtimes,
implementing the PAP-17049 plan under the PAP-17050 threat-model verdict.

Broker (packages/tailscale-https-broker): root/dedicated-account host broker
exposing only idempotent list/expose/remove over a Unix socket. Implements the
six required verdict controls: SO_PEERCRED + UID/GID allowlist + unguessable
broker-issued lease handles (req #1); dedicated port allowlist + /proc listener
ownership/loopback proof before mutation (req #2); per-mutation transaction
(strict-parse serve JSON -> verify absence/exact-lease -> verify :443 -> one
fixed per-port op -> reread -> require only-intended-changed + :443 identical
-> atomic root-owned registry commit, quarantine-on-ambiguity) (req #3);
execve/shell=false argv built only from canonical integers + fixed tokens,
pinned CLI schema (req #4); versioned length-bounded strict-JSON protocol
(req #5); bounded append-only audit events (req #6). Deny-by-default for
Funnel/cert/Services/reset/set-config/path-handlers/:443/reserved ports.

Shared contract (packages/shared): single source of truth for exposure config,
lifecycle state (pending/ready/failed/cleanup_pending/removed), listener
ownership, and status, with strict zod validators and deterministic
app/HMR port-pair + URL derivation so DB/server/UI/runtime cannot drift.

Tests: broker 57/57, shared runtime-exposure 13/13, both typecheck clean.
Server/DB/UI lifecycle integration + docs land in follow-on commits.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
DottaandClaude Opus 4.8 committed 2026-08-16 19:23:17 +00:00
1 parent 6e14ed3031
commit ec7adcaf3d
36 files changed
+3293

No files matched your search

@@ -0,0 +1,168 @@
/**
* Versioned, length-bounded broker request protocol with a strict schema
* (PAP-17050 verdict requirement #5). Every request is decoded here before any
* authorization or mutation logic runs. Malformed, oversized, wrong-version,
* duplicate-key, and unknown-field requests are rejected without side effects.
*/
import { assertCanonicalPort } from "./integers.js";
import { parseJsonNoDuplicateKeys } from "./strict-json.js";
import {
BROKER_PROTOCOL_VERSION,
type BrokerRequest,
type OwnedListener,
} from "./types.js";
/** Hard cap on a single request frame (bytes). Bounds memory + slowloris. */
export const MAX_REQUEST_BYTES = 8 * 1024;
/** Max listeners in one expose request (app + HMR companion). */
export const MAX_LISTENERS_PER_REQUEST = 2;
const UUID_RE =
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
const REQUEST_ID_RE = /^[A-Za-z0-9_-]{1,64}$/;
const HANDLE_RE = /^[A-Za-z0-9_-]{16,128}$/;
const ALLOWED_PURPOSES: ReadonlySet<OwnedListener["purpose"]> = new Set([
"app",
"vite_hmr",
]);
export class ProtocolError extends Error {
constructor(
readonly code:
| "unsupported_version"
| "malformed_request"
| "unknown_operation"
| "invalid_runtime_id"
| "invalid_port",
message: string,
readonly requestId: string | null = null,
) {
super(message);
this.name = "ProtocolError";
}
}
function asObject(value: unknown): Record<string, unknown> {
if (typeof value !== "object" || value === null || Array.isArray(value)) {
throw new ProtocolError("malformed_request", "request must be a JSON object");
}
return value as Record<string, unknown>;
}
function requireStringField(
obj: Record<string, unknown>,
field: string,
re: RegExp,
code: ProtocolError["code"],
requestId: string | null,
): string {
const value = obj[field];
if (typeof value !== "string" || !re.test(value)) {
throw new ProtocolError(code, `invalid or missing field: ${field}`, requestId);
}
return value;
}
/**
* Decode a raw request frame (utf-8 JSON) into a validated BrokerRequest.
* Throws ProtocolError on any violation; never mutates state.
*/
export function decodeRequest(frame: Buffer | string): BrokerRequest {
const buf = typeof frame === "string" ? Buffer.from(frame, "utf8") : frame;
if (buf.byteLength === 0) {
throw new ProtocolError("malformed_request", "empty request frame");
}
if (buf.byteLength > MAX_REQUEST_BYTES) {
throw new ProtocolError("malformed_request", "request frame exceeds size limit");
}
let parsed: unknown;
try {
parsed = parseJsonNoDuplicateKeys(buf.toString("utf8"));
} catch (error) {
throw new ProtocolError(
"malformed_request",
`invalid JSON: ${(error as Error).message}`,
);
}
const obj = asObject(parsed);
if (obj.v !== BROKER_PROTOCOL_VERSION) {
throw new ProtocolError(
"unsupported_version",
`unsupported protocol version: ${String(obj.v)}`,
);
}
const requestId = requireStringField(obj, "requestId", REQUEST_ID_RE, "malformed_request", null);
const op = obj.op;
if (op !== "expose" && op !== "remove" && op !== "list") {
throw new ProtocolError("unknown_operation", `unknown operation: ${String(op)}`, requestId);
}
if (op === "list") {
assertExactKeys(obj, ["v", "op", "requestId"], requestId);
return { op: "list", requestId };
}
if (op === "remove") {
assertExactKeys(obj, ["v", "op", "requestId", "runtimeId", "handle"], requestId);
const runtimeId = requireStringField(obj, "runtimeId", UUID_RE, "invalid_runtime_id", requestId);
const handle = requireStringField(obj, "handle", HANDLE_RE, "malformed_request", requestId);
return { op: "remove", requestId, runtimeId, handle };
}
// expose
assertExactKeys(obj, ["v", "op", "requestId", "runtimeId", "listeners"], requestId);
const runtimeId = requireStringField(obj, "runtimeId", UUID_RE, "invalid_runtime_id", requestId);
const rawListeners = obj.listeners;
if (!Array.isArray(rawListeners) || rawListeners.length === 0) {
throw new ProtocolError("malformed_request", "listeners must be a non-empty array", requestId);
}
if (rawListeners.length > MAX_LISTENERS_PER_REQUEST) {
throw new ProtocolError("malformed_request", "too many listeners in request", requestId);
}
const listeners: OwnedListener[] = rawListeners.map((entry) => {
const listener = asObject(entry);
assertExactKeys(listener, ["purpose", "port"], requestId);
const purpose = listener.purpose;
if (typeof purpose !== "string" || !ALLOWED_PURPOSES.has(purpose as OwnedListener["purpose"])) {
throw new ProtocolError("malformed_request", "invalid listener purpose", requestId);
}
let port: number;
try {
port = assertCanonicalPort(listener.port);
} catch (error) {
throw new ProtocolError("invalid_port", (error as Error).message, requestId);
}
return { purpose: purpose as OwnedListener["purpose"], port };
});
// Reject duplicate ports / duplicate purposes within one request.
const ports = new Set(listeners.map((l) => l.port));
const purposes = new Set(listeners.map((l) => l.purpose));
if (ports.size !== listeners.length || purposes.size !== listeners.length) {
throw new ProtocolError("malformed_request", "duplicate listener port or purpose", requestId);
}
return { op: "expose", requestId, runtimeId, listeners };
}
function assertExactKeys(
obj: Record<string, unknown>,
allowed: string[],
requestId: string | null,
): void {
const allowedSet = new Set(allowed);
for (const key of Object.keys(obj)) {
if (!allowedSet.has(key)) {
throw new ProtocolError("malformed_request", `unknown field: ${key}`, requestId);
}
}
}