Master Password
The master password is the most critical credential in Relic. It protects your private key, which in turn protects every secret you store.What it does
When you set your master password for the first time:- An RSA-2048 key pair is generated on your device
- A key is derived from your password using Argon2id (64 MB memory, 3 iterations)
- Your private key is encrypted with that derived key using AES-256-GCM
- The encrypted private key, public key, and salt are uploaded to the server
- The password itself is stored only on your device
Password requirements
- At least 8 characters
- One uppercase letter
- One lowercase letter
- One number
- One special character
Where it is stored
The password is read in this order:
When saving, Relic tries the system keychain first. If that succeeds, the file fallback is removed. If the keychain is unavailable, the password is written to the config file.
Future integrations
We plan to support platform-native credential stores for the master password:- Apple Passwords (macOS/iOS)
- GNOME Keyring (Linux)
- KWallet (Linux/KDE)
User Keys
Every Relic user has an RSA-2048 key pair. This key pair is the foundation of the entire encryption model.
The private key is encrypted on your device before upload. The server stores only the encrypted form.
Key derivation
Local key cache
Your encrypted private key and salt are cached locally at~/.config/relic/relic.db (SQLite, or ~/.config/relic-dev/relic.db in dev mode). This avoids fetching keys from the server on every operation.
The cache:
- Persists across session expiry (no need to re-enter your password after re-authenticating)
- Is cleared on explicit
relic logout - Contains only the encrypted private key and salt, not the password
Password change
When you change your master password:1
Decrypt
Your private key is decrypted using the old password.
2
Re-encrypt
A new salt is generated. The private key is re-encrypted with the new password using a fresh
Argon2id derivation.
3
Upload
The new encrypted private key and salt are uploaded to the server.
4
Update local cache
The local key cache is updated with the new encrypted values.
Changing your password does not affect project keys or secrets. They are encrypted with the
project key, not directly with your password.
How Secrets Are Encrypted
Every project has its own AES-256 key (the “project key”). This key encrypts and decrypts all secrets within that project.Creating a project
1
Generate project key
A random AES-256 key is generated on your device.
2
Wrap with your public key
The project key is encrypted (wrapped) using your RSA public key.
3
Store on server
Only the wrapped (encrypted) project key is sent to the server.
Writing a secret
1
Unwrap project key
Your RSA private key (decrypted with your master password) unwraps the project’s AES key.
2
Encrypt the value
The secret value is encrypted with AES-256-GCM using the project key.
3
Store on server
The encrypted value is sent to the server. The plaintext never leaves your device.
Reading a secret
1
Unwrap project key
Same as above: your private key unwraps the project key.
2
Decrypt the value
The encrypted value is decrypted locally with AES-256-GCM.
3
Inject into process
The plaintext value is passed as an environment variable to your process via the Rust runner.
What the server sees
Secret names (keys like
DATABASE_URL) are stored in plaintext so you can search and organize
them. Only the values are encrypted.Collaboration
When you share a project with a teammate, the project key is re-encrypted for their public key. Each collaborator gets their own copy of the project key, encrypted with their own RSA public key.Sharing flow
1
Owner decrypts project key
The project owner unwraps the project’s AES key using their own private key.
2
Re-encrypt for collaborator
The project key is wrapped (encrypted) with the collaborator’s RSA public key.
3
Store on server
The re-encrypted project key is stored in the
projectShare record. The collaborator can now
decrypt it with their own private key.Revoking access
When a collaborator is revoked, their share record is marked as revoked. However, they previously had access to the project key, so Relic supports key rotation with revocation:1
Revoke the share
The collaborator’s share is marked as revoked. They can no longer fetch secrets.
2
Generate new project key
A new AES-256 project key is generated on the owner’s device.
3
Re-encrypt all secrets
Every secret in the project is decrypted with the old key and re-encrypted with the new key.
This happens entirely on the owner’s device.
4
Re-wrap for remaining collaborators
The new project key is wrapped with each remaining collaborator’s public key and updated on the
server.
5
Bump key version
The project’s key version is incremented. All caches are invalidated so the CLI fetches fresh
data on the next run.
Service Accounts (Machine Identity)
Service accounts provide machine identity for CI/CD pipelines. Each service account has its own RSA-2048 key pair, independent from any human user.Key management
Unlike human users (whose private key is protected by Argon2id + master password), service account private keys are protected by HKDF-SHA256 + service token:What the server stores
The raw service token never reaches the server. Only a SHA-256 hash is stored for authentication.
OIDC trust policies
Service accounts can optionally require OIDC identity verification. When configured, requests must include both:- Service token — for authentication (hash check) and decryption (key derivation)
- OIDC token — for CI platform identity verification (JWT signature + claims validation)
- Fetching the issuer’s JWKS (JSON Web Key Set) via the OpenID Connect discovery document
- Verifying the JWT signature (RS256)
- Checking issuer, subject (with pattern matching), audience, and expiration claims
Key rotation with service accounts
When a project key is rotated (e.g., after revoking a collaborator with rotation), the new project key is automatically re-wrapped with each active service account’s public key. Existing service tokens continue to work without any changes.Algorithms
Runtime Security
Whenrelic run injects secrets into a process, the Rust runner provides additional protections:
- Memory zeroing: Secret values use
Zeroizing<String>and are wiped on drop - Core dump prevention:
RLIMIT_COREset to zero on Unix - Environment isolation: Child process starts with a cleared environment, only system variables are inherited
- Signal forwarding: SIGTERM/SIGINT are forwarded to the child for graceful shutdown
- Buffer clearing: TypeScript-side buffers are zeroed after the FFI call