Skip to main content
Relic is built on a zero-knowledge architecture. All encryption and decryption happens on your device. The server only stores encrypted data and can never read your secrets.

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.
Your master password is never sent to the server. If you lose it, there is no way to recover your secrets. There is no reset, no recovery email, no backdoor. Store it somewhere safe.

What it does

When you set your master password for the first time:
  1. An RSA-2048 key pair is generated on your device
  2. A key is derived from your password using Argon2id (64 MB memory, 3 iterations)
  3. Your private key is encrypted with that derived key using AES-256-GCM
  4. The encrypted private key, public key, and salt are uploaded to the server
  5. The password itself is stored only on your device
The server never sees your password or your unencrypted private key.

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.
On macOS, Relic uses the system keychain via bun:secrets, which means your master password is protected by your login password and biometrics (Touch ID).

Future integrations

We plan to support platform-native credential stores for the master password:
  • Apple Passwords (macOS/iOS)
  • GNOME Keyring (Linux)
  • KWallet (Linux/KDE)
Until then, back up your master password in a password manager you trust.

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.
The collaborator never sees the owner’s private key. The owner never sees the collaborator’s private key. The server never sees the plaintext project 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.
Key rotation is optional when revoking. If you trust that the revoked collaborator did not extract secrets, you can revoke without rotation. If you want to ensure they cannot decrypt anything going forward, use revoke with rotation.

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:
HKDF is used instead of Argon2id because the service token is already high-entropy (256-bit random), so the expensive memory-hard KDF is unnecessary.

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:
  1. Service token — for authentication (hash check) and decryption (key derivation)
  2. OIDC token — for CI platform identity verification (JWT signature + claims validation)
Neither credential is sufficient alone. The service token provides E2E decryption capability, while the OIDC token proves the request originates from a trusted CI environment (e.g., GitHub Actions for a specific repository and branch). OIDC tokens are validated by:
  • 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
See the OIDC Trust Policies guide for setup instructions.

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

When relic 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_CORE set 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
Secrets are never written to disk during injection.
Last modified on May 2, 2026