Automated vault.yaml delivery for RelOps workers

Step-ca-issued client certs + mTLS at a GCP HTTPS LB replace the operator's hand-dropped vault.yaml with a brokered Secret Manager fetch. Same primitive on macOS hardware (m4/r8) and Linux hardware (talos/netperf).

Why this exists

The problem we kept hitting

Every worker needs a /var/root/vault.yaml (macOS) or /root/vault.yaml (Linux) containing the per-role taskcluster tokens, cltbld credentials, livelog secret, etc. Puppet reads it via Hiera; without it, run-puppet.sh won't even start.

Today: an operator looks the role's entry up in 1Password, saves it locally, SCPs it onto the host. Every wipe, every replacement, every new role assignment โ€” same dance.

Costs: operator must be online + have 1P + SSH + the right file. Easy to mistype a role and ship the wrong secret. Doesn't survive automation. Becomes the chokepoint when a fleet of M4s or r8s gets EACS'd in a batch.

What this unlocks

The vault.yaml fetches itself. A host that's been MDM-enrolled (macOS) or operator-attested via SCEP enrollment (Linux) holds a step-ca-issued client cert; the cert's SPIFFE URI binds it to a single puppet role; the broker hands back the matching secret on demand.

Provisioning โ€” EACS a Mac mini. SimpleMDM pushes profiles + the signed bootstrap PKG; the on-network runner auto-mints the SecureToken over an on-host ssh login (DEP skips Setup Assistant, and Apple only grants the first token interactively), then the bootstrap fetches vault.yaml, puppet provisions, and the worker registers in Taskcluster. Fully hands-off โ€” one Hangar click.

Re-provisioning โ€” Same flow. Worker dies, replace hardware, EACS the new one (or run deliver_linux.sh for Linux), it bootstraps into the same role automatically. No "find the right vault.yaml again" step.

Operationally: the bottleneck moves from "a human with the right files" to "the device has a valid cert." That's a known-quantity infrastructure check, not a tribal-knowledge handoff.

End-to-end architecture

macOS m4 / r8 hardware SimpleMDM-managed โ‘  SimpleMDM enrollment DEP enroll โ†’ SimpleMDM pushes: โ€ข Dev - SCEP profile โ€ข Bootstrap Script job โ€ข 11 other config profiles mdmclient enrolls the SCEP cert โ‘ก Cert in System keychain CN = hostname, SAN URI = spiffe://relops.mozilla/host/<CN>/role/<role> Key ACL grants signing to OS network stack only (nehelper, configd, IKEv2โ€ฆ) โ‘ข Bootstrap PKG CURL_SSL_BACKEND=securetransport \ curl --cert "<CN>" \ forge.relops.mozilla.com \ /secret/<role> TLS handshake signed inside SecureTransport (in allow-list) โ€” our shell never touches the key โ‘ฃ /var/root/vault.yaml โ†’ ronin_puppet apply (hiera reads it via data/secrets/vault.yaml) โ‡’ Worker registers in Taskcluster โœ“ GCP project: relops-bootstrap forge.relops.mozilla.com ๐Ÿ›ก๏ธ HTTPS Load Balancer Cloud Armor (MDC1 source-CIDR allowlist) Trust Config = step-ca root + intermediate Server TLS Policy: ALLOW_INVALID_OR_MISSING (so /scep/* still works without a cert) Forwards X-Client-Cert-{Present,Chain-Verified,Leaf,Serial,SPIFFE} ๐Ÿชช step-ca GCE VM, zonal NEG SCEP provisioners, one per role: scep-no-sip scep-gecko-t-linux-talos scep-gecko-t-linux-2204-talos scep-gecko-t-linux-2404-talos scep-gecko-t-linux-2404-โ€ฆ-wayland scep-gecko-t-linux-netperf scep-gecko-t-linux-2404-netperf ๐Ÿ“œ vault-broker Cloud Run, serverless NEG FastAPI service: โ€ข Parses X-Client-Cert-Leaf โ€ข URL-decodes SPIFFE URI โ€ข Checks role == URL path โ€ข Rate-limits per cert serial โ€ข Logs every read to Cloud Audit Logs reads โ†“ ๐Ÿ” Secret Manager Per-role IAM binding to broker SA vault-<role> secret per puppet role vault-gecko_t_osx_1500_m4 vault-gecko_t_osx_1500_m4_no_sip vault-gecko_t_osx_1500_m4_staging vault-gecko_t_osx_1400_r8 vault-gecko_t_osx_1015 vault-gecko_t_linux_โ€ฆ (ร—6) Content mirrors the 1Password "RelOps" vault Linux talos / netperf hardware operator-bootstrapped via SSH โ‘  Operator runs deliver_linux.sh From the operator's laptop: export RELOPS_BOOTSTRAP_REPO=โ€ฆ ./deliver_linux.sh <host> <role> Helper minted locally: mint-scep-cert.sh <host> <role> openssl req โ†’ sscep enroll (challenge from Secret Manager) โ‘ก scp cert+key to worker /etc/relops-bootstrap/cert.pem /etc/relops-bootstrap/key.pem root:root 0600, dir 0700 Operator's gcloud auth = gate โ‘ข bootstrap_linux.sh fetch_vault_yaml_via_mtls(): curl --cert cert.pem \ --key key.pem \ forge.relops.mozilla.com \ /secret/<role> No keychain dance โ€” plain PEM โ‘ฃ /root/vault.yaml โ†’ puppet apply runs as usual (hiera reads via data/secrets/vault.yaml) โ‡’ Worker registers in TC โœ“ SCEP enroll mTLS GET SCEP enroll mTLS GET /scep/* /secret/*
macOS path Linux path shared GCP infrastructure

Same on both platforms

Shared infrastructure

  • One LB at forge.relops.mozilla.com validates every client cert chain via the same Trust Config
  • One broker (Cloud Run) only sees X-Client-Cert-* headers โ€” platform-agnostic
  • One step-ca with per-role SCEP provisioners that stamp the same SPIFFE URI convention into every cert SAN
  • One Secret Manager holds vault-<role> secrets; broker reads via per-secret IAM binding

SPIFFE URI convention

Same SAN URI shape across all certs:

spiffe://relops.mozilla/host/<CN>/role/<puppet-role>

Broker URL-decodes (handles Mac%20mini), parses host + role, enforces role-match against URL path. Cert chain validation already happened at the LB.

Where the platforms diverge

macOSLinux
Cert delivery SimpleMDM pushes SCEP profile โ†’ mdmclient enrolls into System keychain Operator's mint-scep-cert.sh โ†’ sscep โ†’ scp to worker
Where the key lives macOS System keychain โ€” ACL-restricted to OS network stack (configd, nehelper, NEIKEv2Provider) /etc/relops-bootstrap/key.pem on disk โ€” root:root 0600
TLS client invocation CURL_SSL_BACKEND=securetransport curl --cert "<CN>"
(SecureTransport looks up identity by Subject CN)
curl --cert cert.pem --key key.pem
(plain PEM, LibreSSL backend)
vault.yaml destination /var/root/vault.yaml /root/vault.yaml
Run-puppet trigger signed bootstrap PKG installs com.mozilla.m4-bootstrap LaunchDaemon โ†’ loops run-puppet via ssh-to-localhost until safari semaphores fire bootstrap_linux.sh runs puppet apply directly in line
Operator gate SimpleMDM ops + EACS click; one VNC/SSH login to seed BST if needed Operator's gcloud auth (reads SCEP challenge from Secret Manager) + SSH to worker

Request lifecycle โ€” one vault.yaml fetch

Time flows top to bottom. Same flow for macOS and Linux โ€” the only difference is whether the cert is presented from the keychain (macOS via SecureTransport curl) or from a PEM file on disk (Linux via plain curl).

Worker (macOS or Linux) HTTPS Load Balancer forge.relops.mozilla.com vault-broker Cloud Run Secret Manager vault-<role> TLS handshake + client cert curl --cert ... (PEM file) or CURL_SSL_BACKEND=securetransport --cert "<CN>" validates client cert chain against Trust Config (step-ca root + intermediate) GET /secret/<role> over the now-mTLS-authenticated TLS session forwards + injects X-Client-Cert-* headers Present, Chain-Verified, Leaf (base64 PEM), Serial, SPIFFE parses leaf cert URL-decodes SPIFFE URI from SAN checks role == URL path read vault-<role> via per-secret IAM-bound broker SA vault.yaml content 200 OK + body + Cloud Audit log entry (cert serial, role, byte count) 200 OK + body atomic write /var/root/ or /root/vault.yaml

Auth happens in two stages: the LB does the cryptographic check (cert chain validation) and the broker does the authorization check (role-bound to SPIFFE URI). Splitting these lets the broker stay a thin Python service โ€” it doesn't touch a cryptography library, parses headers + reads Secret Manager. Failure modes: missing/bad cert โ†’ 401 at the broker; cert valid but wrong role โ†’ 403 with the role mismatch in the body. Every fetch (success or denial) is written to Cloud Audit Logs with the cert serial.

Trust chain

step-ca root CA self-signed, 10-year step-ca intermediate signs every worker leaf macOS worker leaf issued via SimpleMDM โ†’ mdmclient โ†’ SCEP CN = "Mac mini" (from %ComputerName%) SAN: spiffe://โ€ฆ/host/<CN>/role/<role> Linux worker leaf issued via mint-scep-cert.sh โ†’ sscep โ†’ SCEP CN = host's FQDN SAN: spiffe://โ€ฆ/host/<CN>/role/<role>

The LB's Trust Config contains the step-ca root + intermediate PEMs. Any leaf step-ca issues โ€” to any platform โ€” chains correctly. The LB validates the client cert chain on every mTLS request to forge.relops.mozilla.com. Single trust anchor for the whole fleet.

Current status

๐ŸŽ
macOS m4 โ€” zero-touch, live across the m4 pools
One click in Hangar (individual host or whole pool): quarantine โ†’ drain โ†’ EACS โ†’ DEP enroll โ†’ SimpleMDM profiles + signed bootstrap PKG โ†’ SCEP cert in keychain โ†’ runner auto-mints the SecureToken over an on-host ssh login (the reason the runner is on-network) โ†’ bootstrap fetches vault.yaml โ†’ puppet โ†’ worker re-registered โ†’ auto-unquarantine. Proven across the m4 staging pool (m4-111โ€ฆ115); m4 production verified ready.
Live
๐Ÿง
Linux talos โ€” scaffolded, not yet functional
The six per-role step-ca SCEP provisioners exist, but their vault-* Secret Manager containers are empty (0 versions) โ€” an enrollment would mint a cert but the fetch has nothing to return. Not usable until the secrets are populated. bootstrap_linux.sh + deliver_linux.sh in ronin_puppet PR #1253; pending a real-host test before merge. (macOS is the proven, live path โ€” see the reprovision flow diagram.)
Not live
๐ŸŽ
macOS r8 / staging / catalina โ€” per-role expansion
Mechanical follow-up: one new step-ca SCEP provisioner + matching SimpleMDM SCEP profile per role + populate the corresponding Secret Manager secret. Architecture is set.
Next
๐ŸชŸ
Windows hardware โ€” pending discovery
Same architecture would work. Native Windows cert store + Invoke-RestMethod -Certificate or curl.exe for the mTLS step. Need to find where the existing Windows bootstrap.ps1 lives (not in ronin_puppet).
Later

What changed conceptually

The vault.yaml itself didn't get any more or less sensitive. What changed is who's allowed to put one on a worker. Before: anyone with the 1Password vault entry and SSH access. After: only an authenticated MDM-enrolled (or operator-attested) host with a step-ca-issued, per-role-bound client cert. Every fetch is logged in Cloud Audit with the cert's serial number. The operator's job goes from "SCP this file" to "click EACS in SimpleMDM" (macOS) or "run one command" (Linux).

Want to dig in, contribute, or roll a role onto this?

Everything is open source inside Mozilla โ€” the broker code, the terraform, the operator-side helpers, the signed bootstrap PKG. Per-role rollout (r8, staging, catalina, the rest of the Linux roles) is mechanical follow-up work; happy to pair on it if you've got a role you want to migrate.