This repository provisions human identities. A deployment binds an identity to one Unix user on one host. Credentials grant specific capabilities to that deployment; a profile only selects software and defaults.
Identity: example-developer
└── Deployment: example-developer@example-host
├── Unix user
├── age identity
├── optional SSH identity
├── Git authorship
└── scoped provider credentials
Do not create one master credential for an identity. Age, SSH, GitHub, and provider credentials must remain independently replaceable.
| Capability | Workstation | Devbox |
|---|---|---|
| Age identity | optional until secrets are consumed | required |
| SSH private identity | required | required |
| Human GitHub login | expected | expected |
| Git signing identity | required | required |
| Git authorship metadata | required | required |
| Provider credentials | identity-scoped | identity-scoped |
- Personal uses the workstation identity policy; personal-devbox uses the devbox identity policy.
- Inbound SSH does not require the user to own a private SSH key. Put an
administrator's public key in the target user's
authorized_keys.
workstation, personal-workstation, personal-devbox, and devbox users
require explicit Git authorship and an owner-only, unencrypted local SSH private
key for unattended commit signing. Agent-backed, encrypted, and public-key-only
signing paths are unsupported.
If the key comes from a human recovery system, export it in OpenSSH format without a passphrase, save it outside this repository, derive its public key, and lock down both files:
chmod 0600 ~/.ssh/developer_ed25519
ssh-keygen -y -f ~/.ssh/developer_ed25519 > ~/.ssh/developer_ed25519.pub
chmod 0644 ~/.ssh/developer_ed25519.pubConfigure authorship and signing from explicit operator values:
profile=workstation
GIT_USER_NAME='Developer Name' \
GIT_USER_EMAIL='developer@example.com' \
GIT_SIGNING_KEY="$HOME/.ssh/developer_ed25519" \
GIT_SSH_IDENTITY_FILE="$HOME/.ssh/developer_ed25519" \
./scripts/bootstrap/configure-git.ts --profile "$profile" --non-interactiveGIT_SSH_IDENTITY_FILEmay point to a different local key.- GitHub registers authentication and signing keys separately; add the public key for each role the deployment uses.
- The configurator writes authorship and signing state to
~/.gitconfig.local. - When SSH authentication is configured, it also writes a managed
~/.ssh/github.configblock that selects the local key and disables ambient agent identities and key additions forgithub.com. - Keep unrelated directives in
~/.ssh/config.local. - Move aside an unmanaged
~/.ssh/github.configor any otherHost github.comblock before running the configurator.
Age calls the private decryption key an identity and its derived public encryption address a recipient.
- Secret-consuming deployments (
personal-devbox,devbox, and vault or sudo consumers) require one general SOPS age identity per managed Unix user. - Portable
workstationandpersonal-workstationprofiles keep the SOPS CLI without an identity until they decrypt encrypted material. - Keep sudo-specific age identities separate: they protect a different capability and have a different rotation lifecycle.
Install the selected profile's Homebrew layers, then provision the identity when the deployment will decrypt secrets:
./scripts/secrets/configure-sops-age-identity.tsThe command is idempotent:
- Creates an identity only when one does not exist.
- Sets owner-only permissions and derives the public recipient.
- Proves a real SOPS encrypt/decrypt round trip.
- Never prints the private identity.
SOPS' native default paths are used:
| Platform | Private identity path |
|---|---|
| macOS | ~/Library/Application Support/sops/age/keys.txt |
| Linux | ~/.config/sops/age/keys.txt |
XDG_CONFIG_HOME changes the config root. Set SOPS_AGE_KEY_FILE only when an
explicit owner-only path is required. Check an existing identity without
creating or repairing it:
./scripts/secrets/configure-sops-age-identity.ts --checkPrint only the safe public recipient for a registry or SOPS policy:
./scripts/secrets/configure-sops-age-identity.ts --print-recipientProvisioning is complete only after the private identity has a verified human recovery copy and the owning encrypted repository has authorized its public recipient:
- Generate or check the local identity with the commands above.
- Back up and verify the private identity using the recovery procedure below.
- Give only the public
age1...recipient to the encrypted repository owner. - Add that recipient to the repository's
.sops.yamland update the affected encrypted files withsops updatekeys. - Prove the deployment can decrypt only the payloads it should consume.
Repository membership alone never grants decryption. Git access controls who can fetch ciphertext; the recipient policy controls which age identities can decrypt it.
Generation and recovery registration are one provisioning operation. Keep one human-controlled recovery item per deployment, not one password-manager item per private file. Attach independently replaceable credentials as separately labeled files in that item:
- general age identity
- sudo-specific age identity when the deployment uses unattended sudo
- SSH private key only when the deployment initiates outbound SSH
- account recovery material when the operator's policy permits it
Do not merge the credentials into one private key or paste their values into a note. The item is an inventory and recovery boundary; each attached credential keeps its own scope and rotation lifecycle.
Before using a new general SOPS age identity for live ciphertext:
- Create or select the deployment's recovery item.
- Attach the general SOPS age identity file and record the deployment name, public recipient, creation date, and local path.
- Restore that general SOPS age identity attachment to an owner-only temporary
path and run
age-keygen -y /path/to/restored-keys.txt. - Confirm the restored general age recipient exactly matches
configure-sops-age-identity.ts --print-recipient. - Validate each other applicable attachment against its own live source or derive and compare its public identity without exposing the private value.
- Remove the temporary restored copies.
Do not print or paste the private identity into shell history, logs, issues, pull requests, chat, or repository files. Routine unattended workloads must not have access to the human recovery system.
Consumer repositories own their .sops.yaml, encrypted payloads, runtime
wrapper, and recipient policy. This dotfiles repo owns only the portable tools,
identity provisioning, and local verification.
For a new vault, the optional SOPS vault template provides a small standalone starting point with recipient policy, safe create/edit commands, and verification:
- Create a private repository from the template.
- Replace its example recipients with the public recovery and deployment recipients.
- Run
mise run verify.
The generated repository owns its copied scripts and policy; it does not depend on this dotfiles repository or the template after creation.
Safe repository state may include:
- public age recipients and SSH fingerprints
- GitHub App slug, App ID, and installation ID
- Git author name and email
- SOPS-encrypted files and
.sops.yaml
Never commit:
- age private identities
- SSH private keys
- GitHub App private keys
- provider tokens or decrypted dotenv files
- password-manager item references tied to a private environment
Prefer one provider credential per identity. If a credential must be shared, encrypt the shared file only to the explicitly approved identity recipients and the human recovery recipient. Repository read access controls availability and integrity; the SOPS recipient set controls who can decrypt.
Create a new age identity when an identity moves to another host. Do not copy the old deployment's private identity.
- Generate and back up the new deployment identity.
- Add its public recipient to the owning repository's
.sops.yaml. - Run
sops updatekeys --yes path/to/secrets.sops.yamlfor each affected file. - Prove the new deployment can decrypt its files and cannot decrypt sibling identity files.
- Remove the old recipient and update the encrypted files again.
- Rotate the underlying secrets before retiring the old deployment because old Git revisions remain decryptable by the old identity.
- Remove the old local identity and archive its recovery item.