Credential Management¶
VERGIL agents authenticate as GitHub Apps. There are no stored
tokens, no classic PATs, and no gh auth account juggling in the
agent path. The tooling holds an App's private key, mints a
short-lived installation token on demand, and discards it. Each
operation runs under the App identity whose permission shape bounds
what it can do.
For App registration and initial setup, see Account Setup. For the identity model that credentials serve, see Identity Architecture.
The credential is the App¶
An agent's only credential is its GitHub App private key, recorded
in identities.toml as private_key_path. From that key the
tooling derives everything else at runtime:
- No static tokens. Installation tokens live ~1 hour and are minted per operation. There is nothing long-lived to leak or rotate beyond the private key itself.
- No
gh authdependency in the agent path. The agent does not rely on a logged-in account or a-vergilsuffix. The App ID and private key are injected into the agent VM by provisioning. - Permission shape is the boundary. The token a given App mints can only do what that App's declared permissions allow. The user App's token cannot write PRs; the audit App's token cannot write code. See Identity Architecture for the inverted shapes.
How a token is minted¶
The implementation lives in src/vergil_tooling/lib/github.py. The
flow is:
- Read the App config.
VRG_APP_IDandVRG_PRIVATE_KEY_PATHidentify the App and locate its private key. In an agent VM these come from the identity'sapp_idandprivate_key_pathinidentities.toml. - Generate a JWT. A short-lived JWT is signed with the private
key, using the App ID as the
issclaim (iatbackdated 60s,exp10 minutes out). - Resolve the installation. The JWT authenticates a call to
GET /app/installations; the tooling matches the installation for the target org. - Mint an installation token. The matched installation yields a
~1-hour token, which is injected as
GH_TOKENinto theghsubprocess environment for that operation.
The token is never written to disk and never persisted across operations. Parallel sessions each mint their own token independently — no shared state, no lock contention.
App ID, not Client ID — for now
The tooling authenticates using the numeric App ID today (it
becomes the JWT iss claim via VRG_APP_ID). GitHub is
gradually migrating GitHub App authentication toward the Client
ID, but that migration is only partially rolled out. Record the
Client ID during setup so the eventual switch needs no return
trip to the GitHub UI, but the App ID is what the tooling uses
until the migration lands.
Credential selection¶
The tool selects the credential, not the caller¶
There is no flag, no env var override, no way for the caller to
choose a different identity. Which App a VM authenticates as is
fixed by provisioning (VRG_APP_ID / VRG_PRIVATE_KEY_PATH for that
VM). The user VM is the user App; the audit VM is the audit App.
The permission shape decides what succeeds¶
Because each App's token is bounded by its declared permissions, the identity boundary is enforced server-side, not by the wrapper:
- The user App can push code and read PRs, but cannot open,
comment on, approve, or merge PRs — its token holds
pull_requests: read. - The audit App can write PR reviews, but cannot write code or
merge — its token holds
contents: read, and merging through the API requirescontents: write.
The vrg-gh/vrg-git wrappers add a soft ergonomic layer (clear
errors, allowlists) on top of this hard gate, but the App permission
shape is the real boundary.
Graceful degradation¶
If App config is absent (no VRG_APP_ID / VRG_PRIVATE_KEY_PATH),
the token-minting path returns nothing and the gh subprocess
inherits the parent environment. This lets the tooling run in CI
environments where credentials are provided through other mechanisms
(for example, an Actions-provided token).
Degradation applies only when App config is absent. When App config
is present, these two cases stop the command rather than fall back
to ambient gh/git auth:
- No target org. The target org comes from an explicit owner
(
-R owner/repoforvrg-gh, a command's own target argument such as--repo owner/repo, or avrg-git cloneURL) or else from the current repo's GitHuboriginremote. If neither yields an org (for example,vrg-ghwithout-Rrun outside a repo), the command stops withGitHub App mode is configured ... but no target org could be determinedand exits non-zero. Run it inside a repo whoseoriginis on GitHub, or name the target explicitly. - Minting fails. A broken or rotated key stops the command with
GitHub App authentication failed for owner '<owner>'(see Rotating an App private key).
Ambient auth may be a human's gh login. Falling back to it would
let an agent act under a different identity without any sign.
One case still falls back: the org is known, but the App has no
installation on it, and the command did not name that owner
explicitly. The tooling logs a warning naming the owner, then uses
ambient auth for it. Some human-run commands and anonymous fetches
of public repos rely on this. A command that names the owner
explicitly fails with the GitHub App has no installation for owner
instead.
Security model¶
Credential security does not rest on token scope. Defense in depth, in priority order:
- App permission shape — the installation token can only perform what the App declares. This is the primary, server-side boundary. A user-App token physically cannot merge a PR.
- Branch protection — rulesets prevent merging without review, prevent direct pushes to protected branches, and require CI to pass.
vrg-gh/vrg-gitwrappers — gate which operations an agent may attempt and surface clear errors. A soft ergonomic layer, bypassable by root, not the security boundary.- Short-lived tokens — ~1-hour installation tokens minted on demand, with no long-lived secret beyond the private key.
Multi-org support¶
A single App installed on multiple accounts works across every org the contributor operates in. Installation tokens are minted per-org from the same private key, so adding a new org requires only installing the existing App on it — no new keys, no credential reconfiguration.
Rotating an App private key¶
Routine rotation replaces an agent App's private key while the old
key is still trusted. The user App (<username>-vergil-user) and the
audit App (<username>-vergil-audit) each have their own key, so
rotate them one at a time with the steps below. If a key is known or
suspected to be exposed, do not use this procedure. Follow
Responding to a compromised credential
instead, which deletes the old key first.
A GitHub App can hold more than one active private key at a time. Rotation relies on that: you add the new key, switch the agent VM to it, prove it works, and only then delete the old one. The agent is never left without a working credential.
No rotation cadence is set for these keys. The date in each key's filename (see Account Setup, Step 1.3) records when it was generated.
Steps¶
-
Generate a new key. On the App's settings page, go to Private keys and click Generate a private key, as in Account Setup, Step 1.3 (user App) or Step 2.3 (audit App). Leave the existing key in place for now.
-
Store it next to the old key. Move the downloaded file into
~/.config/vergil/keys/and keep GitHub's filename, which carries the generation date:
-
Point
identities.tomlat the new key. In~/.config/vergil/identities.toml, set the identity'sprivate_key_pathto the new file. Theapp_id, the App's permissions, and its installations do not change. -
Re-inject the key into the agent VM. Provisioning copies the key into the VM as
~/.config/vergil/app.pem, and it does so only when the VM is created, started, restarted, or rebuilt. A running VM keeps using the old key until one of those happens: -
Lima:
vrg-vm restart --identity <identity>stops and starts the VM, then re-injects credentials. This ends any session running in the VM, so do it between sessions. - Off-platform:
vrg-vm rebuild --identity <identity>. Off-platform VMs do not supportrestart, so the rebuild destroys the VM and creates it again. Its persistent volume is reattached.
Do this for every VM provisioned from the identity, including any
dedicated workspace VM (pass its <org>/<repo> after the command,
for example vrg-vm restart --identity <identity> <org>/<repo>).
- Verify the new key. Inside the VM, run a read that has to mint an installation token, for example:
pr list is allowed for both the user and the audit identity. Use
a repository on an account the App is installed on. The check
passes when the command lists PRs. If the key cannot mint a token,
vrg-gh stops before running gh, prints
GitHub App authentication failed for owner '<owner>' with the App
ID, the key path, and GitHub's error, and exits non-zero. It never
falls back to another login, so a broken key cannot pass the check
by acting as someone else. Every other vrg-* command fails the same
way while the key is broken.
vrg-whoami --explain is not a key check. It reports which
identity the VM resolves to, from the injected files, but it does
not use the key to authenticate.
- Delete the old key on GitHub. Back on the App's Private keys list, delete the old key. Each key is listed with its creation date and a SHA-256 fingerprint. To match a local file to a fingerprint, run:
Do not delete the old key before step 5 passes. Until it is deleted, a copy of it can still mint tokens.
- Delete the old key file. Remove the old
.pemfrom~/.config/vergil/keys/.
Repeat the steps for the other App.
The org automation Apps (<org>-automation) are not covered by these
steps. Their keys are consumed by the APP_PRIVATE_KEY Actions secret,
not by an agent VM. The CI publishing secrets are not covered either.
For both, see
Org automation Apps and
CI publishing secrets under the compromise
procedure below.
Responding to a compromised credential¶
Use this procedure when a credential is known or suspected to be exposed: a private key copied off its host, a secret printed in a log, a lost or untrusted machine. Act on suspicion. Revoking a credential that turns out to be safe costs a few minutes of re-provisioning. Leaving a leaked one live can cost far more.
The response, in order¶
- Contain. Revoke or suspend the credential so it cannot be used again. The per-credential steps are below.
- Halt dependent work. Stop the operations that use the
credential, such as agent sessions, releases, and publishing
workflows, until a replacement is in place. If the agent VM is the
suspected source of the leak, stop it (
vrg-vm stopon Lima) or destroy it (vrg-vm destroyoff-platform, wherestopis not supported). An off-platform VM's injected credentials live on its ephemeral boot disk and are deleted with it. - Review activity. Check what the identity did during the window of suspected exposure (see Reviewing activity).
- Restore. Issue a replacement credential and re-provision whatever consumed the old one.
- Retrospect. Find out how the credential was exposed, and decide what process or tooling change stops it from happening again.
Agent App private key (user or audit App)¶
The private key in private_key_path is the agent's only long-lived
credential. Installation tokens are derived from it and expire after
about an hour.
Blast radius. The holder of the key can mint tokens for every account the App is installed on. Each token can reach the repositories that installation covers. The token is limited to the App's declared permissions, as listed in Account Setup:
<username>-vergil-user key |
<username>-vergil-audit key |
|
|---|---|---|
| Contents | Read and write. Can push branches and read all code in covered repos, private repos included. | Read-only. Can read all code in covered repos, private repos included. |
| Issues | Read and write. Can create, edit, and comment on issues. | Read-only |
| Pull requests | Read-only | Read and write. Can write PR reviews and comments. |
| Repository security advisories | Read and write. Can read, create and edit the draft advisories in each covered org's .github, which hold security findings. |
No access |
| Workflows | No access. Cannot push under .github/workflows/. |
No access. Cannot push under .github/workflows/. |
| Merge | Cannot open, approve, or merge PRs. | Cannot merge, because merging needs contents: write. |
Branch protection still limits a leaked user-App key. It cannot push directly to a protected branch or merge without review. It can still push malicious content to feature branches, and it can read every private repo its installations cover. Because both Apps can read code, treat a leak of either key as exposure of that code.
Contain.
- Delete the compromised key. Open the App's settings page and go to Private keys. Delete the compromised key so that no new tokens can be minted from it. A token minted before the deletion stays valid until it expires (about an hour).
- Suspend the installations to cut off access immediately, before the outstanding tokens expire. In the settings of each account the App is installed on, open the App's installation and suspend it. Unsuspend it after the replacement key is in place.
Restore. Generate a new private key for the same App as in
Account Setup, Step 1.3 or 2.3. Point
private_key_path in identities.toml at the new file and delete the
old .pem from ~/.config/vergil/keys/. Then re-inject the key into
the agent VM. Credentials are injected when a VM is created, started,
restarted, or rebuilt, so run vrg-vm restart on Lima or
vrg-vm rebuild off-platform. The App ID, permissions, and
installations do not change. Verify the new key as described in
Rotating an App private key, step 5.
For routine, non-emergency key rotation, use that procedure instead.
The human account¶
Blast radius. This is the most severe case. The human account holds the authority that the agent Apps lack: review, approval, merge, and administration (see Identity Architecture). It is also an owner of the orgs the agents work in, and it owns both agent Apps, which are registered under its Developer settings. Anyone in control of the human account can therefore generate fresh keys for both agent Apps.
Contain and restore. Secure the GitHub account itself, then treat both agent App keys as compromised and work through the App-key procedure above for each one. Review the account's recent merge and administrative activity in addition to the agent activity.
Org automation Apps¶
Each org has its own automation App, owned by the org:
vergil-project-automation, logical-minds-foundry-automation and
mnemosys-project-automation (see
Identity Architecture).
CI reaches an org's App through the APP_CLIENT_ID and
APP_PRIVATE_KEY Actions secrets. The nightly ops workflow uses them
for the config audit, the branch sweep and the dependabot-audit job.
The epic-rollup and epic-sweep workflows use them to close epics and
tasks. The cd-release workflow uses them to dispatch to the org's
packages repository (see
Package config).
Blast radius. Each App is installed on its org with access to all
repositories, not per contributor, so a leaked key reaches every repo
in that org. It does not reach the other orgs, which have their own
Apps and keys. Its permission set is not listed in the Account Setup
tables. Read the exact permissions from the App's settings page before
you assess the impact. Its Administration permission is read-only, so
the key cannot change repo settings or rulesets. With Dependabot
alerts: Read, the key can read a repo's open alerts, including on a
private repo. With Repository security advisories: Read and write, it
can read and change the draft advisories in the org's .github, which
hold the details of unfixed security findings (see
Security Records).
Contain and restore. Follow the same steps as for an agent App key:
delete the key, suspend the installation if access must stop at once,
and generate a new key. Then update the APP_PRIVATE_KEY secret
wherever it is stored. Until you do, the workflows that depend on it
(ops, epic roll-up and sweep, package-index dispatch) will fail.
CI publishing secrets¶
The release workflow forwards per-ecosystem secrets to cd-release
(see CI Architecture):
| Secret | Blast radius |
|---|---|
CARGO_REGISTRY_TOKEN, CENTRAL_USERNAME/CENTRAL_TOKEN |
Can publish to that registry under the project's account |
GPG_PRIVATE_KEY/GPG_PASSPHRASE (java) |
Can sign artifacts as the project |
PACKAGE_SIGNING_KEY/PACKAGE_SIGNING_PASSPHRASE |
Can sign .deb/.rpm packages that apt/dnf clients will trust, because clients trust the org signing key |
Python publishing uses PyPI OIDC trusted publishing, so there is no stored PyPI token to leak.
Contain. Revoke the token or key at its issuer, such as the registry account or the GPG key, so that a leaked copy stops working. Changing only the GitHub secret leaves the leaked copy valid. Halt releases until a replacement is in place.
Restore. Issue a replacement at the issuer. Then update the
repository, organization, or environment secret that held it.
PACKAGE_SIGNING_* live in the package-signing environment. The
package signing key is more involved to replace than the others,
because clients pin it. Its primary fingerprint is pinned in
vergil-tooling, checked by vrg-vm, published in
Getting Started, and shipped in the
vergil-archive-keyring package (see
VM spec). Replacing it means changing all
of those together.
Reviewing activity¶
- Org audit log. In the organization's settings, open Audit
log and filter by the compromised identity across the window of
suspected exposure. For an agent App, that identity is the
<username>-vergil-<role>[bot]actor. Repeat for every org the App is installed on. - Repository history. Every commit, PR, review, and comment made through an App carries its bot identity, so the App's work stands out in branch, PR, and issue history. Look for unexpected branches, pushes, PRs, reviews, and issue edits.
- Release artifacts. For a publishing secret, check the registry or package index for versions you did not release.
Related¶
- Account Setup — registering the Apps,
generating private keys, and configuring
identities.toml - Identity Architecture — the agent-App model and naming conventions
- Permission Model — enforcement layers beyond credential selection
- Credential management design spec — full decision rationale and alternatives considered