Plan: Per-Role Worker Credentials + SSH Signing Identity

Status: IN PROGRESS — Phase 1 ✅ (PR #2729) · Phase 2 ✅ (PR #2743) · Phase 3 IN PROGRESS (PR open) Extends: #2075 — Specialized role-scoped workers Author: session 2026-07-05 Type: Design + migration plan

Secret values never appear here — key names only. Execution steps are delegated to secret-manager + sys-admin workers. This document is the single source of truth for the target state of worker credential isolation.


1. Objective

Two interrelated goals:

  1. Credential isolation — each Claude worker role loads ONLY the secrets it needs. A compromised dev-coder worker cannot access sys-admin credentials. Extends the one-secret-per-exposure-point policy from per-service-credential-isolation-plan.md to the worker credential domain.

  2. Cryptographic identity — every git commit made by an autonomous worker is signed with an SSH key that uniquely identifies the role. Combined with user.name = {role}@{host} in git config, every commit is traceable to: role + host + worker slot. No commit can be forged as coming from a worker it didn’t come from.


2. Target state

2.1 Per-role SOPS files

One SOPS file per technical worker role, deployed to the servers that can run that role. Creative roles (marketing-director, content-creator, video-producer, designer) are out of scope for the initial rollout — they don’t make git commits or access servers.

SOPS fileRoleKey names (names only)Deploy to
secrets/role-sys-admin.env.sopssys-adminROLE_SYS_ADMIN_GITHUB_CLASSIC_PAT, ROLE_SYS_ADMIN_GIT_SIGNING_KEYbms-4, vps-i1, vps-h1
secrets/role-secret-manager.env.sopssecret-managerROLE_SECRET_MANAGER_GITHUB_PAT, ROLE_SECRET_MANAGER_VERCEL_TOKEN, ROLE_SECRET_MANAGER_GIT_SIGNING_KEYbms-4 only
secrets/role-dev-coder.env.sopsdev-coderROLE_DEV_CODER_GITHUB_PAT, ROLE_DEV_CODER_GIT_SIGNING_KEYbms-4, vps-i1
secrets/role-dev-reviewer.env.sopsdev-reviewerROLE_DEV_REVIEWER_GITHUB_PAT, ROLE_DEV_REVIEWER_GIT_SIGNING_KEYbms-4
secrets/role-dev-tester.env.sopsdev-testerROLE_DEV_TESTER_GITHUB_PAT, ROLE_DEV_TESTER_GIT_SIGNING_KEYbms-4

2.2 Credential design per role

sys-admin — broadest GitHub access (repo creation, CI/CD, project management):

  • ROLE_SYS_ADMIN_GITHUB_CLASSIC_PAT — Classic PAT, scopes: repo + workflow + project + admin:ssh_signing_key
  • ROLE_SYS_ADMIN_GIT_SIGNING_KEY — Ed25519 private key (base64), used to sign commits

secret-manager — SOPS + distribution operations:

  • ROLE_SECRET_MANAGER_GITHUB_PAT — Fine-grained PAT, scopes: secrets:write on p24-infra
  • ROLE_SECRET_MANAGER_VERCEL_TOKEN — Vercel token for env var sync
  • ROLE_SECRET_MANAGER_GIT_SIGNING_KEY — Ed25519 private key (base64)

dev-coder — implementation work on all repos:

  • ROLE_DEV_CODER_GITHUB_PAT — Fine-grained PAT, scopes: repo:read+write, pull_request:write
  • ROLE_DEV_CODER_GIT_SIGNING_KEY — Ed25519 private key (base64)

dev-reviewer — PR review only:

  • ROLE_DEV_REVIEWER_GITHUB_PAT — Fine-grained PAT, scopes: pull_request:write (read + comment)
  • ROLE_DEV_REVIEWER_GIT_SIGNING_KEY — Ed25519 private key (base64)

dev-tester — test execution, no pushes:

  • ROLE_DEV_TESTER_GITHUB_PAT — Fine-grained PAT, scopes: repo:read, actions:read
  • ROLE_DEV_TESTER_GIT_SIGNING_KEY — Ed25519 private key (base64)

2.3 SSH signing key design

One key per role (not per role×host). The key fingerprint identifies the ROLE. The user.name in git config identifies the HOST. Together they give full attribution.

commit abc1234
Author: sys-admin@bms-4 <noreply+sys-admin@p24-infra.zintegrowana.online>
Date:   ...

feat(#1234): provision new worker

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Executed-By: bms4-cw-1

gpg: Signature made ... using ED25519 key ID FINGERPRINT_OF_ROLE_SYS_ADMIN_KEY
gpg: Good signature from "role-sys-admin (p24-infra worker role) <noreply+sys-admin@p24-infra.zintegrowana.online>"

Why one key per role, not per role×host:

  • Simpler SOPS management (N files × 1 key, not N files × M hosts)
  • Role is the security boundary — host is tracked via user.name + Executed-By
  • If a role key is compromised, rotating one key updates all hosts for that role
  • Private key distribution is already limited to the SOPS recipients of that role’s file

Git config applied at worker startup (in spawn-worker.sh or worker pre-flight):

# Decode signing key from env
echo "$ROLE_SYS_ADMIN_GIT_SIGNING_KEY" | base64 -d > /tmp/git-signing-key-$ROLE
chmod 600 /tmp/git-signing-key-$ROLE
trap "rm -f /tmp/git-signing-key-$ROLE" EXIT
 
# Set role+host identity
git config --global user.name "sys-admin@$(hostname -s)"
git config --global user.email "noreply+sys-admin@p24-infra.zintegrowana.online"
git config --global gpg.format ssh
git config --global user.signingKey /tmp/git-signing-key-$ROLE
git config --global commit.gpgSign true

Public key registration on GitHub (one-time per role, via API using sys-admin PAT with admin:ssh_signing_key scope):

curl -X POST https://api.github.com/user/ssh_signing_keys \
  -H "Authorization: Bearer $ROLE_SYS_ADMIN_GITHUB_CLASSIC_PAT" \
  -d '{"title":"role-sys-admin","key":"ssh-ed25519 AAAA..."}'

2.4 Identification layers

LayerMechanismVisible in
CryptographicSSH signing key fingerprint → rolegit log --show-signature · git verify-commit
Readable (git)user.name = {role}@{host}git log --format='%an' · GitHub commit page
Readable (message)Executed-By: {worker_id} in commit bodygit log · GitHub commit detail
BadgeGitHub “Verified” (green badge)GitHub UI (when pubkey registered)

3. Redundancy — bms-4 offline scenario

role-sys-admin.env.sops is deployed to 3 servers (bms-4, vps-i1, vps-h1). When bms-4 is offline, the dispatcher routes sys-admin jobs to vps-i1 or vps-h1. Both have the same role credentials and can sign commits with the same role-sys-admin key. The user.name in the commit will be sys-admin@vps-i1 or sys-admin@vps-h1, preserving host attribution.

sys-admin job → dispatcher → bms-4 (primary)
                          ↘ vps-i1 (fallback #1)
                          ↘ vps-h1 (fallback #2)

Same role-sys-admin.env.sops credentials on all three.
user.name distinguishes the actual execution host in git history.

4. Phased rollout

Phase 1 — Per-role SOPS files ✅ DONE (PR #2729, merged 2026-07-05)

  • secrets/role-sys-admin.env.sops created with real ROLE_SYS_ADMIN_GITHUB_CLASSIC_PAT
  • Classic PAT created (scopes: repo + workflow + project + admin:ssh_signing_key)
  • secrets-sync.yml updated: deploy role-sys-admin.env to bms-4 + vps-i1 + vps-h1
  • spawn-worker.sh updated: source role file based on job_typeROLE mapping
  • Created remaining role SOPS files (role-secret-manager, role-dev-coder, role-dev-reviewer, role-dev-tester)
  • docs/policies/credential-isolation-policy.md written
  • secrets/mongodb-bms.env.sops standalone MongoDB credential file

Phase 2 — SSH signing key generation ✅ DONE (PR #2743, merged 2026-07-05)

  • Ed25519 key pairs generated for all 5 roles
  • Private keys base64-encoded and stored as ROLE_GIT_SIGNING_KEY in each role SOPS file
  • Public keys registered on GitHub (IDs 1037694–1037698) via admin:ssh_signing_key scope
  • spawn-worker.sh updated: decodes ROLE_GIT_SIGNING_KEY at startup, configures git SSH signing (user.name={role}@{server}, user.email=radieu@gmail.com, commit.gpgsign=true)
  • All 5 signing keys verified: p24-worker-{role} keys visible at github.com/settings/ssh

Phase 3 — Deploy all role SOPS files to workers [IN PROGRESS]

  • secrets-sync.yml updated: all 3 server jobs (bms-4, vps-i1, vps-h1) now decrypt and deploy all 5 role credential files to /opt/p24-infra/{server}/role-{role}.env
  • PR merged and secrets-sync.yml triggered to deploy files to all 3 servers
  • Verify on bms-4: ls /opt/p24-infra/bms-4/role-*.env shows all 5 files
  • Verify on vps-i1: ls /opt/p24-infra/vps-i1/role-*.env shows all 5 files
  • Verify on vps-h1: ls /opt/p24-infra/vps-h1/role-*.env shows all 5 files
  • Test: spawn a dev-issue job, confirm [git-signing] Configured SSH signing: dev-coder@bms-4 in log
  • Test: commit from worker shows “Verified” badge on GitHub

Phase 4 — Rotation automation

  • Add role SSH signing keys to credential-rotation-policy.md (rotate annually)
  • Script: re-generate key, update SOPS, revoke old public key from GitHub, register new one
  • Classify as Tier 1 autonomous (sys-admin role has admin:ssh_signing_key scope to self-rotate)

5. secrets-sync.yml changes (Phase 1 execution)

Add to each server’s deploy block:

# bms-4
- name: Deploy role-sys-admin credentials
  run: |
    sops -d secrets/role-sys-admin.env.sops > /tmp/role-sys-admin.env
    scp /tmp/role-sys-admin.env root@54.36.123.110:/opt/p24-infra/bms-4/role-sys-admin.env
    rm /tmp/role-sys-admin.env
 
# vps-i1
- name: Deploy role-sys-admin credentials
  run: |
    sops -d secrets/role-sys-admin.env.sops > /tmp/role-sys-admin.env
    scp /tmp/role-sys-admin.env root@217.154.82.162:/opt/p24-infra/vps-i1/role-sys-admin.env
    rm /tmp/role-sys-admin.env
 
# vps-h1
- name: Deploy role-sys-admin credentials
  run: |
    sops -d secrets/role-sys-admin.env.sops > /tmp/role-sys-admin.env
    scp /tmp/role-sys-admin.env root@72.60.32.61:/opt/p24-infra/vps-h1/role-sys-admin.env
    rm /tmp/role-sys-admin.env

6. spawn-worker.sh changes (Phase 1 execution)

Add role-file sourcing based on job_type → role mapping:

# job_type → role SOPS file mapping
case "$JOB_TYPE" in
  sys-admin|infra-task)    ROLE_FILE="role-sys-admin.env" ;;
  secret-manager)          ROLE_FILE="role-secret-manager.env" ;;
  dev-issue|dev-coder)     ROLE_FILE="role-dev-coder.env" ;;
  dev-review|pr-review)    ROLE_FILE="role-dev-reviewer.env" ;;
  dev-test)                ROLE_FILE="role-dev-tester.env" ;;
  *)                       ROLE_FILE="" ;;
esac
 
if [ -n "$ROLE_FILE" ] && [ -f "/opt/p24-infra/$SERVER_LABEL/$ROLE_FILE" ]; then
  set -a
  # shellcheck source=/dev/null
  source "/opt/p24-infra/$SERVER_LABEL/$ROLE_FILE"
  set +a
fi

7. Compliance

When each role credential is created:

  • Register in Supabase dev_r_services (element_type='credential', criticality='high', rotation_freq='90d', auto_rotate=false until Phase 4)
  • Log initial creation in docs/secrets-rotation-log.md
  • SSH signing keys: register in dev_r_services with element_type='signing-key', rotation_freq='365d'