Playbook: lap1 (dev-laptop) role credential access

Background — GAP-7

The 2026-07-06 secrets-sync audit found that role credential files (role-sys-admin, role-secret-manager, role-dev-coder, role-dev-reviewer, role-dev-tester) sync to vps-i1, vps-h1, and bms-4 via secrets-sync.yml but not to lap1 (Acer laptop, also referred to as dev-laptop in older docs — see docs/environments/lap1.md). Tracked as issue #2945.

The issue’s design comment (2026-07-26) evaluated three options and recommended Option 3: lap1 decrypts role-*.env.sops files itself, locally, using its own age key — the same way a developer workstation already works. No CI push needed.

⚠️ Option 3 is now architecturally invalid — read this before doing anything else

Three days after that design was posted, plan #4556 Phase 3 (merged 2026-07-29) narrowed the .sops.yaml recipients for all five secrets/role-*.env.sops files down to developer + CI (AGE_KEY_GHA) only, deliberately removing the four universal per-host keys (bms-4, vps-i1, dev-laptop, vps-h1) that could previously decrypt them directly. See .sops.yaml lines ~71-114 and docs/plans/plan-4556-sops-role-repo-scoped-access.md §4.3/§6.1:

“Role credentials are delivered exclusively via the CI-mediated pre-decrypt-and-deploy path. No live sops -d of a role-*.env.sops file ever happens on a host.”

This was an intentional, independently-shipped security hardening step — closing exactly the kind of host-side bypass that GAP-7’s “Option 3” would have relied on. Do not re-add lap1’s age key as a recipient of any role-*.env.sops file. That would reopen the hole Phase 3 was built to close, for the benefit of the least-controlled, least-monitored host in the fleet — the opposite of what Phase 3 intended.

Net effect: lap1 currently has no way to receive role credentials, by neither the push model (no CI job targets it) nor the pull model (its age key is no longer a valid recipient). GAP-7 is still open; the fix this playbook was meant to document turned out to be blocked by a later, independent decision. This is not a regression introduced by this playbook — it is the current, correct state of .sops.yaml, documented here so the next audit pass doesn’t re-recommend Option 3.

Current state (verify without printing any secret value)

# Confirm role-*.env.sops recipients are developer + CI only (no per-host keys)
Select-String -Path .sops.yaml -Pattern "role-(sys-admin|secret-manager|dev-coder|dev-reviewer|dev-tester)" -Context 0,4

Expected: each of the five blocks lists exactly two age1... recipients (the developer key and the CI key), not four.

# A canary decrypt attempt from lap1's own key must FAIL for role-* files (this is correct,
# not a bug) — run this ON lap1, never on the CI runner:
sops --decrypt --input-type dotenv --output-type dotenv secrets/role-dev-coder.env.sops
# Expected: non-zero exit, "no matching recipient found" (or similar) — do not treat as an
# incident; it is the intended post-#4556-Phase-3 behavior.

Why this hasn’t been prioritized as a live blocker

  • lap1-que-w1, lap1-que-w2, and lap1-gh-w1 are all still pending activation / installation per docs/infrastructure-overview.md §lap1 — no live worker on lap1 is currently claiming jobs that need role credentials.
  • lap1’s reachability itself is unverified: the CF Tunnel path (lap1-ssh.tunnel.zintegrowana.online, docs/environments/lap1.md §Remote Access) is the currently-documented access method, but the machine has been offline / unverified since 2026-07-08 per prior session notes on the older autossh-based path. A CI job that pushes to an unreachable host on every secrets/*.env.sops change would either fail noisily or need a skip-on-unreachable guard from day one.

The only remaining viable path — CI-mediated push over the CF Tunnel (future work)

Once lap1 is confirmed reachable and at least one of its workers is live, the correct fix is the same CI-mediated push pattern already used for vps-i1/vps-h1/bms-4 (decrypt server-side with AGE_KEY_GHA, scp the plaintext to the host, install with restrictive permissions) — not a pull/local-decrypt model. Concretely, a future sync-lap1 job in .github/workflows/secrets-sync.yml would need to:

  1. Decrypt the five role-*.env.sops files server-side using secrets.AGE_KEY_GHA (already a valid recipient — no .sops.yaml change required for this part).
  2. Reach lap1 via cloudflared access ssh --hostname lap1-ssh.tunnel.zintegrowana.online (see docs/environments/lap1.md §Remote Access for the exact ProxyCommand invocation).
  3. Install each decrypted file to a lap1-local path (e.g. C:\p24-infra\lap1\role-<name>.env) with the tightest permission model Windows supports for a single-user workstation.
  4. Skip with a warning, never fail the job, if the tunnel is unreachable — mirroring the existing self-targeting guard already in sync-vps-i1 (.github/workflows/secrets-sync.yml, “Guard — refuse self-targeting deploy on ionos-2” step) as the precedent for a non-fatal skip branch in this workflow.
  5. Scrub the plaintext files from the CI runner’s /tmp in an if: always() step, same as every other sync-* job.

This is deliberately not implemented yet — it should be scoped as its own follow-up issue once lap1’s reachability and at least one active worker are confirmed, rather than building and maintaining a CI target that is unreachable by default.

Interim guidance for any bootstrap / job-claim logic on lap1

Until the above ships, any worker bootstrap running on lap1 must not assume a role-*.env file is present. A worker that needs role credentials and can’t find the file should refuse to claim the job rather than fail mid-task. This is the same failure mode GAP-7 originally described (“if workers there need role credentials, they currently do not receive them via CI”) — it remains true today, just for a different underlying reason than the issue first assumed.

Escalation

  • Need a role credential on lap1 right now, before the CI job above exists? Do not manually decrypt a role-*.env.sops file with the developer age key and copy it to lap1 by hand — that recreates the exact bypass Phase 3 removed, just done by a human instead of automation. Use the secret-manager-request.md queue flow instead and treat it as a one-off, logged operation, not a standing credential.
  • lap1 reachability status unclear? Check docs/environments/lap1.md §Remote Access and the worker capacity docs before assuming the tunnel is up.
  • Issue #2945 — GAP-7, this playbook’s origin
  • docs/plans/plan-4556-sops-role-repo-scoped-access.md — the Phase 3 narrowing that invalidated the issue’s original recommended resolution
  • .sops.yaml — current recipient list for secrets/role-*.env.sops
  • docs/environments/lap1.md — lap1 hardware, connectivity, and remote-access details
  • docs/infrastructure-overview.md §lap1 — inventory entry, updated alongside this playbook