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-adminworkers. This document is the single source of truth for the target state of worker credential isolation.
1. Objective
Two interrelated goals:
-
Credential isolation — each Claude worker role loads ONLY the secrets it needs. A compromised
dev-coderworker cannot accesssys-admincredentials. Extends the one-secret-per-exposure-point policy fromper-service-credential-isolation-plan.mdto the worker credential domain. -
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 file | Role | Key names (names only) | Deploy to |
|---|---|---|---|
secrets/role-sys-admin.env.sops | sys-admin | ROLE_SYS_ADMIN_GITHUB_CLASSIC_PAT, ROLE_SYS_ADMIN_GIT_SIGNING_KEY | bms-4, vps-i1, vps-h1 |
secrets/role-secret-manager.env.sops | secret-manager | ROLE_SECRET_MANAGER_GITHUB_PAT, ROLE_SECRET_MANAGER_VERCEL_TOKEN, ROLE_SECRET_MANAGER_GIT_SIGNING_KEY | bms-4 only |
secrets/role-dev-coder.env.sops | dev-coder | ROLE_DEV_CODER_GITHUB_PAT, ROLE_DEV_CODER_GIT_SIGNING_KEY | bms-4, vps-i1 |
secrets/role-dev-reviewer.env.sops | dev-reviewer | ROLE_DEV_REVIEWER_GITHUB_PAT, ROLE_DEV_REVIEWER_GIT_SIGNING_KEY | bms-4 |
secrets/role-dev-tester.env.sops | dev-tester | ROLE_DEV_TESTER_GITHUB_PAT, ROLE_DEV_TESTER_GIT_SIGNING_KEY | bms-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_keyROLE_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:writeon p24-infraROLE_SECRET_MANAGER_VERCEL_TOKEN— Vercel token for env var syncROLE_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:writeROLE_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:readROLE_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 truePublic 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
| Layer | Mechanism | Visible in |
|---|---|---|
| Cryptographic | SSH signing key fingerprint → role | git 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 body | git log · GitHub commit detail |
| Badge | GitHub “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.sopscreated with realROLE_SYS_ADMIN_GITHUB_CLASSIC_PAT - Classic PAT created (scopes:
repo+workflow+project+admin:ssh_signing_key) -
secrets-sync.ymlupdated: deployrole-sys-admin.envto bms-4 + vps-i1 + vps-h1 -
spawn-worker.shupdated: source role file based onjob_type→ROLEmapping - Created remaining role SOPS files (role-secret-manager, role-dev-coder, role-dev-reviewer, role-dev-tester)
-
docs/policies/credential-isolation-policy.mdwritten -
secrets/mongodb-bms.env.sopsstandalone 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_KEYin each role SOPS file - Public keys registered on GitHub (IDs 1037694–1037698) via
admin:ssh_signing_keyscope -
spawn-worker.shupdated: decodesROLE_GIT_SIGNING_KEYat 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.ymlupdated: 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.ymltriggered to deploy files to all 3 servers - Verify on bms-4:
ls /opt/p24-infra/bms-4/role-*.envshows all 5 files - Verify on vps-i1:
ls /opt/p24-infra/vps-i1/role-*.envshows all 5 files - Verify on vps-h1:
ls /opt/p24-infra/vps-h1/role-*.envshows all 5 files - Test: spawn a
dev-issuejob, confirm[git-signing] Configured SSH signing: dev-coder@bms-4in 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_keyscope 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.env6. 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
fi7. Compliance
When each role credential is created:
- Register in Supabase
dev_r_services(element_type='credential',criticality='high',rotation_freq='90d',auto_rotate=falseuntil Phase 4) - Log initial creation in
docs/secrets-rotation-log.md - SSH signing keys: register in
dev_r_serviceswithelement_type='signing-key',rotation_freq='365d'
8. Related documents
per-service-credential-isolation-plan.md— MongoDB + shared secrets isolation (complementary)roles-and-skills.md— role definitions + skill activationworker-gh-auth-standard.md— current GitHub auth pattern for workersadding-new-worker.md— worker registration proceduredocs/roles/README.md— role matrix