# Dbrij Secrets Documentation

Everything you need to use Dbrij Secrets from code: the CLI, the Node, Python and Go SDKs, machine tokens, logging in from a pipeline, the machine API, AI assistants, plans and errors.

Canonical page: https://dbrij.com/products/secrets/docs

## Dbrij Secrets

Dbrij Secrets keeps your app keys: database addresses, payment keys, tokens. Each project holds environments (development, staging, production, or your own), and each environment holds named values in folders.

**Values are encrypted on your device**, in your browser or by the CLI and SDKs, before they reach Dbrij. An environment is **sealed** unless an admin chooses otherwise: Dbrij stores only encrypted values and cannot open them. An admin may make an environment **readable** so Dbrij can deploy it to Hosting, rotate a key, or sync it to an outside service; each time Dbrij opens it, the reason is written to the project’s activity.

Key names, folders, tags and the history of changes are stored as plain text, which is what makes comparing, searching and the activity trail work. Never put a secret in a key name. The [security page](https://dbrij.com/security) says exactly what Dbrij can and cannot see.

### Two ways in

- **People** sign in with `dbrij login` and unlock their vault key with its passphrase.
- **Machines** (servers, pipelines, containers) read with a machine token. Machines are free on every plan.

## Quickstart

Install the CLI, sign in, and start your app with its secrets. Values reach the disk in the clear only when you ask (`pull`, `export --out`). Otherwise the CLI keeps sealed copies only: the last values read, for when Dbrij cannot be reached, and your unlocked vault key for 12 hours. `dbrij secrets lock` removes them, and `DBRIJ_SECRETS_CACHE=off` stops the copy of the values.

Install the CLI

```
npm install -g dbrij
```

Sign in, then remember the project for this folder

```
dbrij login
dbrij secrets link --project my-app --env development
```

Start your app with its secrets in its environment

```
dbrij secrets run -- npm start
```

`link` writes `.dbrij/secrets.toml` in the folder, so later commands know the project and environment. You can also pass `--project` and `--env` every time, or set `DBRIJ_SECRETS_PROJECT` and `DBRIJ_SECRETS_ENV`.

### On a server or in a pipeline

Make a machine in the project, give it the environments it may read, and copy its token once. Then:

A server

```
DBRIJ_SECRETS_TOKEN=dbs_… npx dbrij secrets run --env production -- node server.js
```

## Machine tokens

A machine is something that reads secrets without a person: a server, a pipeline job, a container. Make one in a project’s Machines tab, choose the environments it may read or write, and optionally the addresses it may come from. Its token is shown once.

The shape of a token

```
dbs_<id>_<auth>_<key>_<check>
```

- Only `dbs_<id>_<auth>` is ever sent to Dbrij, to log in.
- The `key` part opens the machine’s private key on the machine itself, and never reaches Dbrij.
- The `check` part catches a token that was cut short when it was copied.

A login gives an access token that ends on its own (after 60 minutes unless the machine is set otherwise, and never after the token’s own end date). The CLI and SDKs log in again for you.

### Logging in from GitHub Actions

A pipeline that issues identity tokens can log in without a stored login secret. Give the machine a GitHub login in Dbrij Secrets, save the key it shows (`dbsk_…`) as `DBRIJ_SECRETS_KEY`, and let the job ask for an identity token. The token is asked for with the audience `https://secrets.dbrij.com`.

A workflow job

```
permissions:
  id-token: write
  contents: read
steps:
  - uses: actions/checkout@v4
  - run: npx dbrij secrets run --env production -- npm run deploy
    env:
      DBRIJ_SECRETS_KEY: ${{ secrets.DBRIJ_SECRETS_KEY }}
```

### Logging in from Dbrij Source CI

A job of a Source repository can log in the same way. Give the machine a Source CI login naming the repository (and the branch, which a protected environment needs), then save the key it shows in the repository’s CI secrets as `DBRIJ_SECRETS_KEY`. Each job is given an address and a request token for its own identity token, signed by Source CI and good for ten minutes; the CLI and the SDKs ask for it and log in. The login is bound to the repository itself, so a repository made later under the same name is not trusted.

**Worth knowing:** CI secrets are kept sealed by Dbrij, so with the key half there Dbrij holds both halves for this machine. For values Dbrij must never be able to open, run the job somewhere that keeps the key itself.

.dbrij/ci.yml

```
jobs:
  deploy:
    steps:
      - npm ci
      - npx dbrij secrets run --env production -- npm run deploy
```

### Logging in from AWS, Google Cloud, Azure or Kubernetes

A machine running on a cloud can log in as the identity the cloud already gave it, so no login secret is stored anywhere. Give the machine a login of that kind in Dbrij Secrets, naming the one identity it trusts, and give the machine the key it shows as `DBRIJ_SECRETS_KEY`. The key still opens the values on the machine itself; the cloud identity only proves who is asking.

WHAT EACH LOGIN TRUSTS, AND WHERE THE PROOF COMES FROM

|  |  |  |
| --- | --- | --- |
| AWS | IAM role or user ARN | A GetCallerIdentity request signed with the machine’s AWS credentials (environment, ECS, EKS web identity, or EC2 instance metadata). Dbrij sends it to AWS STS, and AWS answers who signed it. The header x-dbrij-secrets-server is signed into it, so it cannot be used anywhere else. Any session of the role is accepted. |
| Google Cloud | service account email | An identity token from the metadata server, asked for with the audience https://secrets.dbrij.com. |
| Azure | tenant, managed identity object ID, app ID URI | A managed identity token, asked for an app you register in your own tenant for Dbrij Secrets, so the token opens nothing else of yours. On App Service and Functions it comes from their own token service (IDENTITY_ENDPOINT); on virtual machines, scale sets and AKS from the instance metadata service. With more than one identity, set AZURE_CLIENT_ID. |
| Kubernetes | cluster issuer, namespace, service account | A projected service account token with the audience https://secrets.dbrij.com, read from /var/run/secrets/dbrij/token (or DBRIJ_SECRETS_KUBERNETES_TOKEN_FILE). The cluster’s issuer must publish its keys at an https address, as managed clusters do. |

A Kubernetes pod

```
containers:
  - name: app
    env:
      - name: DBRIJ_SECRETS_KEY
        valueFrom: { secretKeyRef: { name: dbrij-secrets, key: key } }
    volumeMounts:
      - { name: dbrij-token, mountPath: /var/run/secrets/dbrij }
volumes:
  - name: dbrij-token
    projected:
      sources:
        - serviceAccountToken: { audience: https://secrets.dbrij.com, expirationSeconds: 3600, path: token }
```

The CLI and the SDKs read the kind of login from Dbrij and fetch the proof themselves; there is nothing else to set.

### Environment variables

|  |  |  |
| --- | --- | --- |
| DBRIJ_SECRETS_TOKEN | string | A machine token. Read by the CLI and every SDK. |
| DBRIJ_SECRETS_KEY | string | A login key (dbsk_…) for GitHub Actions, Source CI, AWS, Google Cloud, Azure or Kubernetes, used when there is no token. |
| DBRIJ_SECRETS_ENV | string | The environment to read. Optional when the machine reads only one. |
| DBRIJ_SECRETS_PROJECT | string | The project, for people using the CLI. A machine already belongs to one project. |
| DBRIJ_API_URL | string | The API address. Defaults to https://api.dbrij.com/api/v1. |

## The CLI

Every command reads the project and environment from flags, from `.dbrij/secrets.toml`, or from the environment variables above.

### Use them

Run, export, get

```
dbrij secrets run -- npm start          # start a command with the secrets in its environment
dbrij secrets run --watch -- npm start  # restart it when the secrets change
dbrij secrets export --format env       # or json, yaml, docker, shell; --out FILE
dbrij secrets get DATABASE_URL          # print one value
```

### Change them

Set, delete, compare, pull, push

```
dbrij secrets set STRIPE_KEY=sk_live_…  # one or more; set NAME alone reads the value from stdin
dbrij secrets set --template 'DATABASE_URL=postgres://${DB_USER}@${/shared.DB_HOST}/app'
dbrij secrets delete OLD_KEY
dbrij secrets diff                      # compare with a local .env (--file, --show-values)
dbrij secrets pull                      # write them to a local .env (--file, --force)
dbrij secrets push                      # save a local file into the environment (--file, --prune)
```

`push` also takes a file dotenvx encrypted: it is opened on your machine with your dotenvx private key (`DOTENV_PRIVATE_KEY`, or `DOTENV_PRIVATE_KEY_PRODUCTION` for `.env.production`, from the environment or `.env.keys` beside the file), and every value is sealed again before it leaves. The key never does.

`run`, `export`, `get` and `pull` stop when a template cannot be filled in, naming the reference; `--allow-missing` goes on without that secret. It is never handed out empty.

### Set up

Link, status, lock

```
dbrij secrets link    # remember the project and environment for this folder
dbrij secrets status  # who is reading, and from where (with --project and --env: your role there)
dbrij secrets lock    # forget the unlocked vault key and local copies on this device
```

### Lease a database user

Lease, revoke

```
dbrij secrets lease postgres-readonly -- psql   # a short lived user in DATABASE_URL, revoked when psql ends
dbrij secrets lease postgres-readonly --ttl 1h  # print one (--var NAME, --format json)
dbrij secrets lease revoke ID                   # end one early
```

### Find leaks

Scan

```
dbrij secrets scan                  # secrets written into this folder’s files
dbrij secrets scan --staged         # only what is staged for the next commit
dbrij secrets scan --history        # every commit (--range main..HEAD for some)
dbrij secrets scan --install-hook   # run the staged scan before every commit (--force replaces a hook)
```

### Folders

`--path /api` reads one folder; repeat it to merge several in order, a later folder winning. `--recursive` includes the folders inside.

`set`, `delete` and `push` change the folder you read: `--path`, or the one folder `.dbrij/secrets.toml` names. A change to a protected environment needs `--yes`; in a company project it then becomes a change request for the approvers instead of applying at once. A machine never changes a protected environment.

## Node SDK

`@dbrij/secrets` loads an environment with a machine token and opens it on your own server. Node 20 or newer, no dependencies.

Install

```
npm install @dbrij/secrets
```

Load and use

```
import { DbrijSecrets } from '@dbrij/secrets';

// Reads DBRIJ_SECRETS_TOKEN and DBRIJ_SECRETS_ENV from the environment.
const secrets = new DbrijSecrets({ environment: 'production' });
await secrets.load();

const databaseUrl = secrets.get('DATABASE_URL');
secrets.injectEnv(); // or put every value into process.env
```

- `refresh()` asks with the last ETag, so an environment that has not changed costs a small 304.
- If Dbrij cannot be reached, the last good copy keeps being served for `failSoftMs` (one hour; `0` turns it off). A refusal, such as a revoked token, is never hidden.
- `watch(onChange)` follows the environment’s version stream and calls you when values change. It returns a function that stops watching.
- A machine with write access can `set(values)` and `delete(names)`. Values are sealed before they leave, and a save is refused if the environment changed since the copy it started from.

OPTIONS

|  |  |  |
| --- | --- | --- |
| token | string | Defaults to DBRIJ_SECRETS_TOKEN, then DBRIJ_SECRETS_KEY. |
| environment | string | Defaults to DBRIJ_SECRETS_ENV, or the only environment the machine reads. |
| paths | string[] | Folders to merge in order, a later one winning. Defaults to ['/']. |
| recursive | boolean | Include the folders inside. Defaults to false. |
| baseUrl | string | Defaults to DBRIJ_API_URL, then https://api.dbrij.com/api/v1. |
| failSoftMs | number | How long the last good copy is served while Dbrij cannot be reached. One hour; 0 turns it off. |
| timeoutMs | number | Per request. Ten seconds. |

## Python SDK

`dbrij-secrets` does the same in Python 3.9 or newer, with the `cryptography` package.

Install

```
pip install dbrij-secrets
```

Load and use

```
from dbrij_secrets import DbrijSecrets

# Reads DBRIJ_SECRETS_TOKEN and DBRIJ_SECRETS_ENV from the environment.
secrets = DbrijSecrets(environment="production")
secrets.load()

database_url = secrets.get("DATABASE_URL")
secrets.inject_env()  # or put every value into os.environ
```

- `refresh()` asks with the last ETag, so an unchanged environment costs one small request.
- If Dbrij cannot be reached, the last good copy is served for `fail_soft_seconds` (one hour). A refusal is never hidden.
- `watch(on_change, every_seconds=30)` asks again every 30 seconds in a background thread (it polls, where Node and Go follow a stream) and calls you when values change. It returns an event; set it to stop.
- A machine with write access can `set(values)` and `delete(names)`; values are sealed first.

The options match the Node SDK, in Python’s spelling: `token`, `environment`, `paths`, `recursive`, `base_url`, `fail_soft_seconds` (`0` turns failing soft off, as in Node), `timeout`, `github_audience`.

A login key in `DBRIJ_SECRETS_KEY` logs in from GitHub Actions, Dbrij Source CI, AWS, Google Cloud, Azure or Kubernetes, as in the other SDKs.

## Go SDK

The Go SDK does the same in Go 1.24 or newer, with the standard library only. A client is safe for use by several goroutines.

Install

```
go get github.com/DbrijHQ/secrets-go
```

Load and use

```
import dbrijsecrets "github.com/DbrijHQ/secrets-go"

// Reads DBRIJ_SECRETS_TOKEN and DBRIJ_SECRETS_ENV from the environment.
secrets, err := dbrijsecrets.New(dbrijsecrets.Options{Environment: "production"})
if err != nil {
	log.Fatal(err)
}
if _, err := secrets.Load(ctx); err != nil {
	log.Fatal(err)
}
databaseURL, _ := secrets.Get("DATABASE_URL")
secrets.InjectEnv(true) // or put every value into the process environment
```

- `Refresh` asks with the last ETag, so an unchanged environment costs one small request.
- If Dbrij cannot be reached, the last good copy is served for `FailSoft` (one hour; `NoFailSoft` turns it off). A refusal is never hidden.
- `Watch(ctx, onChange, onError)` follows the environment’s version stream and calls you when values change, until `ctx` is done.
- A machine with write access can `Set` and `Delete`; values are sealed first.
- Errors are `*dbrijsecrets.Error`, with the HTTP `Status` (0 when Dbrij could not be reached) and Dbrij’s `Code`.

The options match the Node SDK, in Go’s spelling: `Token`, `Environment`, `Paths`, `Recursive`, `BaseURL`, `FailSoft`, `Timeout`, `GitHubAudience`, `HTTPClient`.

One difference: in Go a zero `FailSoft` means the default (one hour), because an empty `Options` has to. Turn failing soft off with `dbrijsecrets.NoFailSoft` (any negative duration). In Node, `failSoftMs: 0` turns it off.

## Kubernetes operator

The Dbrij Secrets operator keeps a Kubernetes Secret in step with an environment. You describe what to read in a `DbrijSecret`; the operator opens the values inside your cluster with the machine’s key half, which never reaches Dbrij, writes them to the Secret you name, and can restart the workloads that use it when they change. It uses the Go SDK and nothing else.

Install (the CRD, its service account and roles, and the operator in dbrij-system)

```
kubectl apply -f https://dbrij.com/secrets-operator/0.1.0/install.yaml
```

It runs the image `ghcr.io/dbrijhq/dbrij-secrets-operator:0.1.0`, for linux/amd64 and linux/arm64, as a non root user. Set `WATCH_NAMESPACE` on its Deployment to follow one namespace instead of all of them.

### A DbrijSecret

Give it a machine token, then describe the Secret

```
# read -rs DBRIJ_SECRETS_TOKEN   (paste the token; it stays out of your shell history)
# kubectl -n shop create secret generic dbrij-token --from-literal=token="$DBRIJ_SECRETS_TOKEN"
apiVersion: secrets.dbrij.com/v1alpha1
kind: DbrijSecret
metadata: { name: api, namespace: shop }
spec:
  project: shop
  environment: production
  paths: ["/", "/api"]          # merged in order, a later folder winning
  target: { name: api-secrets, labels: { app: api } }
  refreshInterval: 1m           # at least 30s
  auth:
    tokenSecretRef: { name: dbrij-token, key: token }
  rolloutRestart:
    - { kind: Deployment, name: api }
  rolloutCooldown: 2m
```

Instead of a token, `auth.kubernetes.keySecretRef` logs in as the operator’s own service account (`dbrij-system/dbrij-secrets-operator`): give the machine a Kubernetes login in Dbrij Secrets and keep its login key (`dbsk_…`) in that Secret. The cluster’s issuer must publish its keys at an https address, as managed clusters do; a local kind or minikube cluster cannot, so use a token there.

### What it does

- Asks Dbrij every `refreshInterval` with the last version it has, so an unchanged environment costs one small request and no write. It does not follow the change stream.
- `kubectl get dbrijsecrets` shows Ready, the reason, the target Secret and the environment version in it.
- Never takes over a Secret it did not make, and writes nothing when Dbrij cannot be reached or refuses, a value cannot be opened, a template cannot be filled in, a key is not valid for Kubernetes, or the Secret would pass 1 MiB. The Secret keeps its last good values.
- When values change it restarts the Deployments, StatefulSets and DaemonSets you list, not more often than `rolloutCooldown`.

**Worth knowing:** it writes plain Kubernetes Secrets. Anyone who can read Secrets in that namespace can read the values, so Dbrij’s end to end encryption ends at the operator. Give read access to those Secrets as carefully as you would give the values themselves.

## AI assistants

`dbrij mcp` lets an assistant in your editor work with Secrets without ever seeing a value. Names come through the API; values are only handled on your machine.

TOOLS

|  |  |  |
| --- | --- | --- |
| list_secret_projects | read | The projects in a space, with their environments and how many secrets each holds. |
| list_secret_names | read | The names in one environment. Never values. |
| compare_secret_names | read | Which environments have each name and which lack it, such as a key set in staging but forgotten in production. |
| list_keys | read | The Key management keys you can see, and which operations you may use. Never key material. |
| encrypt_with_key | use a key | Encrypts a short text with a key and hands back the ciphertext. There is no decrypt tool. |
| sign_with_key | use a key | Signs a text with a signing key, or checks a signature. The key never leaves Dbrij. |
| set_secret | write | Saves a value, sealed on your machine before it leaves. |
| run_with_secrets | run | Runs a command with the secrets in its environment and hands back the output with every value hidden. |
| send_request_with_secrets | run | Sends an https request with placeholders like {{secret:STRIPE_KEY}} filled in on your machine, and hands back the answer with every value hidden. A secret goes only to the hosts a person listed on it. |
| run_with_database_lease | run | Leases a short lived database user, runs a command with its address, then revokes it. The assistant never sees the password or the address. |

### Letting an assistant call an API with a key it never sees

On a secret in Dbrij Secrets, list the hosts an assistant may send it to (its `send-to` list, such as `api.stripe.com`). The assistant writes the request with `{{secret:STRIPE_KEY}}` where the key goes (or `{{secret:NAME|base64}}`, `{{secret:NAME|url}}`), and it is filled in on your machine and sent. A request to any other host, over plain http, or with a placeholder where the host belongs is refused before anything is sent. Redirects are not followed. An assistant cannot change the list: its writes never carry it.

Opening a project needs a key: a machine token in `DBRIJ_SECRETS_TOKEN`, or your vault key unlocked in a terminal by a command that opens the environment, such as `dbrij secrets status --project my-app --env development` (it stays unlocked on that device for 12 hours by default). An assistant can never be asked for your passphrase. Encrypting and signing with a key are marked as actions, not reads, so your assistant asks before each one. On the hosted server, `https://api.dbrij.com/mcp`, an assistant sees names only, and encrypting and signing need the write grant.

## References and imports

A **template** is a value built from other secrets, filled in where it is read. Values are encrypted on your device, so filling in happens there too: in the CLI, the SDKs, and in the dashboard when you choose Show filled in. Only a value saved as a template is filled in; one that merely holds `${...}`, like a shell snippet, stays as written.

WRITING A REFERENCE

|  |  |  |
| --- | --- | --- |
| ${KEY} | same folder | KEY in the same folder. |
| ${/api.KEY} | another folder | KEY in the folder /api of the same environment. ${/.KEY} is the top level. |
| ${production.KEY} | another environment | KEY at the top of the environment called production. |
| ${production./api.KEY} | both | KEY in /api of production. |

A template

```
dbrij secrets set --template 'DATABASE_URL=postgres://${DB_USER}:${/shared.DB_PASS}@${production.DB_HOST}/app'
```

- In the dashboard, typing `${` offers the names you can use, and a value with references can be saved as a template.
- A reference may lead through others, up to five steps. A circle is refused when it is saved.
- A reference to a secret that is not there, or to an environment the reader may not read, is an error naming it. The secret is left out; it is never filled in empty.
- Deleting a secret a template uses warns first, naming the template. A folder a template uses cannot be renamed under it.
- Reading a value of another environment for a template is a read there, in that environment’s activity.
- For Hosting deploys, syncs and Forge, Dbrij fills templates of a readable environment itself, and only from environments readable for the same reason; otherwise the deploy stops with the reason. A pinned deploy fills from this environment as it was; other environments give their current values.
- A watching reader hears about changes in its own environment at once; a change in another environment a template uses reaches it on its next refresh.

### Imports

A folder can import other folders of the same environment, so `/api` gets everything in `/shared` without copying it. The folder’s own values win, then a later import over an earlier one, then what the imported folders import. A folder that another imports cannot be deleted until the import is removed. Change imports above the list of secrets in a folder.

## Database leases

A lease is a database user made for one person or machine, for a short time, and removed when it ends. Nobody keeps a standing password for a quick look at production, and every lease is in the project’s activity.

An admin adds a lease source in a project’s Connections: a Dbrij Hosting Postgres or MySQL database, whether its users may read or also write, and how long a lease may last (up to 24 hours, renewable up to 7 days in all). Whoever may read the source’s environment may take a read lease; a write lease needs a role that may change it. A machine leases with its own token.

A session that cleans up after itself

```
dbrij secrets lease postgres-readonly -- psql
```

- On Postgres, the user’s password carries an end time the database itself enforces, so no new session can start after it, even if Dbrij could not be reached. On MySQL, which has no such end time, the database holds an event that drops the user when the lease ends, beside Dbrij’s own revoke.
- On revoke, its sessions are closed and the user is dropped. A lease is shown as revoked only once the database confirms the user is gone; until then Dbrij keeps trying and tells the project’s admins.
- A read lease can read every table and change nothing. A write lease can also change rows, never the schema.

A database outside Dbrij can still have its password rotated: in Connections, a rotation that holds two Postgres or MySQL users switches between them, so the app always has one that works. On MySQL each of the two users must exist for one host only, and the admin user needs the CREATE USER privilege and to read mysql.user.

## Key management

Key management keeps keys your apps use to encrypt, decrypt, sign and verify, without the app ever holding the key. Your app sends the data and gets it back encrypted (or decrypted, or signed); the key stays in Dbrij, and every use is counted.

### Make a key

An admin of a project makes a key in Secrets, Key management, and chooses what it is for: encrypt and decrypt (AES 256 GCM, the usual choice), encrypt with a public key (RSA OAEP), sign and verify (ECDSA, Ed25519 or RSA PSS), or prove a message was not changed (HMAC). Whoever makes a key may use it at once.

### Give access

On the key’s page, give each person or machine of the project only the operations it needs: an app that stores data needs encrypt, not decrypt. Access can end on a date. Anyone else is refused, and every refusal is counted in the key’s use. The first refusal each hour for a caller and key is also written to the project’s activity.

### Use it

Node, with the app’s machine token@dbrij/secrets

```
import { DbrijSecrets } from '@dbrij/secrets';const secrets = new DbrijSecrets(); // DBRIJ_SECRETS_TOKENconst { ciphertext } = await secrets.keys.encrypt(KEY_ID, customer.bvn, { context: { table: 'customers' } });const bvn = await secrets.keys.decryptText(KEY_ID, ciphertext, { context: { table: 'customers' } });
```

A terminal or CI jobshell

```
dbrij secrets keys encrypt customer-data "a value" --context table=customersdbrij secrets keys sign release-signing < manifest.json
```

Any languagecurl

```
curl -X POST https://api.dbrij.com/api/v1/secrets/machine/kms/keys/KEY_ID/encrypt \  -H "Authorization: Bearer $DBRIJ_SECRETS_TOKEN" -H "Content-Type: application/json" \  -d '{"plaintext":"aGVsbG8=","context":{"table":"customers"}}'
```

- Data travels as base64. A ciphertext looks like `dkms1.<key>.<version>.<data>`: it names its key and version, and cannot be moved to another key, version or context.
- The context is optional name and value pairs the ciphertext is bound to. Decrypting needs the same context, so a value copied to another table does not decrypt there.
- Encrypt up to 4 KB directly. For anything larger, `encryptLarge` asks for a data key, encrypts on your server with it, and keeps only the data key’s encrypted copy beside the data.
- Signatures (base64; ECDSA in DER) verify with the public key alone, so others can check them without Dbrij.

### Rotate, disable, destroy

- Rotating adds a version: new work uses it, older ciphertexts still decrypt and older signatures still verify. Re-encrypt moves a ciphertext to the newest version without the plaintext coming back. A key can rotate on a schedule.
- A disabled key refuses every use until it is enabled again.
- Destroying is never at once: you choose 7 to 30 days. Meanwhile every use is refused and counted, so you see what still depends on it, and you can cancel. After that, nothing it encrypted can be decrypted, by anyone.
- In a protected project (a company project with a protected environment), destroying starts as a request. Another admin approves it within 7 days, and the countdown starts only then; until then the key works as usual.

### Bring your own key material

For keys you must make yourself, or are moving from elsewhere, make the key with its material imported. Its page gives a one time wrapping key (RSA 3072, good for 24 hours) and the OpenSSL commands to wrap your material with it on your own machine, so it never travels or rests in the clear. AES and HMAC keys are raw bytes; private keys are PKCS #8. The CLI does the wrapping for you:

Import materialshell

```
dbrij secrets keys import KEY_ID --file key.bin
```

Nothing can use the key until its material arrives. It rotates when you import new material, which becomes the next version; older versions still decrypt and verify.

### Keys kept for Dbrij products

Some Dbrij products keep their own keys here. Each Dbrij Ship app’s release signing key is one: it signs every release inside Key management and never leaves it. An app made before Key management kept its key moves here at its next release, unchanged, so apps already in people’s hands keep verifying updates with the key they have built in. Each mail domain’s DKIM key is another: Dbrij’s mail server asks for a signature for every message it sends, and the key moves here at the first one, unchanged, so the DKIM record you published keeps verifying (and you can rotate it from the domain’s settings: a new key under a new selector, published beside the old one before the switch). These keys belong to the company (or your personal space), need no Secrets project, and do not count toward your keys. The company’s owners and admins see them under Kept for Dbrij products, with how often each signs; nobody can read, export or remove them there.

Dbrij keeps two keys of its own here as well, in no company’s space: the key that signs “Sign in with Dbrij” ID tokens (changed every 90 days, its public keys published at /.well-known/jwks.json), and the Dbrij Docs seal, which gives every completed signature a real PDF digital signature (its certificate is at /api/v1/public/docs/seal-certificate).

### What we can say about the keys

Unlike sealed environments, Key management works on Dbrij’s servers by its nature: Dbrij performs the operations, so it must be able to open the key for them. Keys are kept encrypted under a master key Dbrij holds, and opened in memory in Dbrij’s API servers only for the operations your apps and people ask for; they are never logged. This is software, not a hardware security module: someone holding both Dbrij’s master key and its database could open them. Every use is counted per day and caller, changes are in the project’s activity, and so is the first refusal each hour for a caller and key. An imported key’s material was made outside Dbrij, so for that key Dbrij cannot say it never existed anywhere else.

## Certificates

Certificates gives your team its own private certificate authority: certificates for services that talk TLS or mTLS, for devices and apps proving who they are, and for signing code and documents. Private means just that: browsers and phones trust these certificates only on the machines where you install your root. Public websites keep their certificates from Hosting.

### Authorities and policies

- The company owner makes a **root** (valid 10 years). It signs project authorities and nothing else.
- A project admin makes the project’s own **authority** beneath it (up to 5 years). It can be limited to names under your domains, and clients themselves refuse anything outside them.
- A **policy** says what an authority may issue: names (exact, every name below a domain, IPs or ranges), what for (server, client, code or document signing, email), key types, the longest lifetime (up to 398 days), and who may ask: admins always, members if allowed, and the machines it names.
- Authority keys live in Key management, are never exported, and are used by nothing but Certificates.

### Issue

In the browser, Issue a certificate makes the key pair on your device and sends Dbrij only a request; the private key is shown to you once, to download or seal straight into Secrets beside the certificate. Servers and CI jobs make their own key and ask with their machine token:

A machine asks for its own certificateshell

```
openssl req -new -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes \  -keyout tls.key -subj "/CN=api.internal.acme.com" -addext "subjectAltName=DNS:api.internal.acme.com" -out tls.csrcurl -X POST https://api.dbrij.com/api/v1/secrets/machine/pki/authorities/AUTHORITY_ID/issue \  -H "Authorization: Bearer $DBRIJ_SECRETS_TOKEN" -H "Content-Type: application/json" \  -d "$(jq -n --rawfile csr tls.csr '{policyId:"POLICY_ID", csr:$csr}')"
```

Anything outside the policy is refused with the exact rule it breaks. The answer carries the certificate and its chain (the authority, then the root).

### Renew, revoke, alerts

- Renewing issues a fresh certificate for the same key and names, checked against the policy as it is now: nothing to send but the certificate’s id (`/secrets/machine/pki/certificates/ID/renew` for machines).
- Project admins are told 30, 7, 1 days before a certificate ends.
- Revoking puts a certificate on its authority’s revocation list at once. Every certificate names that list’s public address, `/public/secrets/pki/crl/AUTHORITY_ID.crl`, refreshed daily and on every revocation; revoking an authority puts it on its root’s list.

### ACME

Turn on ACME for a project authority and cert-manager, Caddy, Traefik, certbot or any ACME client issues and renews from it on its own, with nothing from Dbrij installed. Each client signs up with a key ID and HMAC key made on the authority’s page for one of the project’s machines (ACME’s external account binding), and gets only what the policies naming that machine allow, for TLS servers. A policy says how names are proved: with an HTTP or DNS challenge checked from Dbrij over the internet, or, for private names Dbrij cannot reach, by the machine’s place in the policy.

cert-managerClusterIssuer

```
spec:  acme:    server: https://api.dbrij.com/api/v1/public/secrets/pki/acme/AUTHORITY_ID/directory    privateKeySecretRef: { name: dbrij-acme-account }    externalAccountBinding:      keyID: KEY_ID      keySecretRef: { name: dbrij-acme-eab, key: secret }
```

Revoking a binding stops its client at once. Certificates a client got are the machine’s in the activity, and renewing names it already holds always works, whatever the plan.

### SSH certificates

An SSH authority signs OpenSSH certificates. A **login** authority signs people’s and machines’ keys for hours (8 by default, at most 7 days), as the user names its policy lists; servers trust the authority once, and nobody’s key is copied onto them again. A **host** authority signs servers’ own host keys, so clients trust the servers by their certificates instead of asking about each new host key. Policies can also limit what a login may do, force one command, or allow only some addresses. Only the public key is sent; the private key stays on its machine.

Sign your keyshell

```
dbrij secrets ssh sign AUTHORITY_ID ~/.ssh/id_ed25519.pub --principal deploy
```

The certificate is written beside the key as `id_ed25519-cert.pub`, and ssh uses it on its own. A server trusts the authority with `TrustedUserCAKeys` and reads its revocation list with `RevokedKeys`; the authority’s page has the setup to copy, and machines sign with their token at `/secrets/machine/pki/ssh/authorities/ID/sign`.

### Signers

A signer is a Key management signing key with a certificate from your authority, for code, releases or documents. Give your build machine Sign on its key: it signs with `secrets.keys.sign` or `dbrij secrets keys sign` and never holds the key, and whoever checks the signature trusts the certificate.

## Server access

Reach servers and databases through Dbrij instead of sharing their passwords. A project admin adds a server (SSH), a Postgres or MySQL database, and the accounts Dbrij signs in as; people ask for a window on an account, with a reason, and approvers decide (nobody approves their own request). Sessions go through Dbrij’s access gateway, which signs in for them: with a certificate made for that one session (from one of the project’s SSH login authorities), or with a password or key kept sealed, which nobody sees again.

### Connect

- From the web: a terminal in the browser, from the request or the server’s page.
- From a terminal, with ssh, psql or mysql as usual:

ssh, psql or mysql through the gatewayshell

```
dbrij secrets access ssh REQUEST_IDdbrij secrets access psql REQUEST_IDdbrij secrets access mysql REQUEST_IDdbrij secrets access shell REQUEST_ID   # a Dbrij Hosting container# Any other client: a local port, each connection its own sessiondbrij secrets access connect REQUEST_ID --port 6543
```

### Dbrij Hosting databases and services

When adding a server, choose In Dbrij Hosting to pick a database or service from the same space that you may operate. There is no address or password to type, and nothing is made public: the gateway reaches it inside the cluster.

- A Postgres or MySQL database: each session gets a database user of its own, made when the session starts, able to sign in only until the window closes, and dropped when the session ends. The account chooses whether it reads, or reads and writes. The database’s own password never leaves Hosting.
- A service: a terminal in its running container, from the browser or with dbrij secrets access shell. It is recorded like any terminal.
- MySQL: the gateway signs in with the account (caching_sha2_password or mysql_native_password, over TLS by default) and records each query and prepared statement. Your client needs no password and no TLS of its own on the local port; changing user and replication commands are refused, and reading files from your machine (LOAD DATA LOCAL) is never offered.

### Servers in a private network

A server with no public address is reached through a relay: a small program that runs inside its network, asks Dbrij for work over HTTPS, and connects out to the gateway for each session. It never opens a port, never holds a password, and dials only what its own allow list names.

- Add a relay under Relays. Its token is shown once, with the `docker run` command that runs it (the image `ghcr.io/dbrijhq/dbrij-secrets-relay:0.1.0`, for linux/amd64 and linux/arm64), or build the one static binary yourself.
- Set `DBRIJ_RELAY_ALLOW` to what it may reach, as `HOST:PORT` entries such as `10.0.0.0/8:22` or `db.internal:5432`.
- Add the server and choose the relay under Reached through. For SSH, paste its host key from `ssh-keyscan` run inside the network.
- A relay not heard from for 90 seconds is offline: its servers cannot be connected to, and the project’s admins are told after five minutes.

### Break glass

An account that needs approval can name people who may break glass. In an emergency, one of them opens their own request at once, without waiting: they say why, confirm who they are in the browser, and the window lasts up to an hour. Every approver and admin is told at once with the reason, the company’s chat gets a card when it follows the project, and the request and its sessions are marked Without approval. Sessions are recorded as always.

### For a task

In a company project, a request can name one of your open tasks (or, from a terminal, add `--task TASK_ID` to `dbrij secrets access request`). The task’s activity then shows the window being asked for or opened, and each session starting and ending, so whoever reads the task sees the work that was done on the server.

### Recorded, and ended on time

- Every session is recorded: what the server shows (keystrokes are not kept), and for a database each statement sent, with the values in it. A notice says so before each session.
- The project’s admins play recordings back, with seeking; each playback is in the activity. Recordings are kept sealed for the plan’s days, then deleted.
- The gateway checks every session with Dbrij each minute: when the window closes, an admin ends it, the account or server is disabled, or the person leaves the company, it ends. A database finishes the statement in flight first.
- File transfer and port forwarding do not go through the gateway, so nothing leaves without a record.

Only what goes through the gateway is recorded. If a server can still be reached directly with a known password, close that path (rotate the password away, keep the database private) for the record to be complete.

## Folder access

A person or a machine can be given some folders of an environment instead of all of it: a contractor gets `/frontend` in Development, a billing worker gets `/billing` in Production. Choose the folders when you give access, in People or in Machines.

- A folder includes the folders inside it. `/` on its own is the top level only.
- They are shown, sent and allowed to change only what is in those folders: the editor, names, history, bundles, the CLI and the SDKs all follow it. A save that touches anything outside is refused whole.
- Shaping the environment needs all of it: adding, renaming or deleting folders, imports, rotation, syncs, leases and Hosting links.
- Clear the folders to give the whole environment again. An approved access request gives the whole environment.

**What it is not:** whoever reads an environment holds its key, as the encryption needs. Folder access limits what Dbrij sends and accepts, not what that key could open if somebody already had the encrypted values. For a hard wall, put the values in an environment of their own and give access to that.

## Finding leaks

Secrets end up in code by accident. Dbrij looks for them in two places: on your own machine before they are committed, and in Dbrij Source when they are pushed.

### On your machine

Check the folder, then guard every commit

```
dbrij secrets scan
dbrij secrets scan --install-hook
```

The scan knows the shapes of common keys (cloud, payment, chat and source hosting keys, private keys, database addresses with a password, and every Dbrij key). Values that read like examples are skipped. Nothing leaves your machine. It exits with 1 when it finds something, so it can stop a pipeline too, and `--json` prints the findings for a tool to read. A line meant to be there, such as a test fixture, can carry the comment `dbrij-scan: ignore`.

### In Dbrij Source

Every push is scanned. A repository’s settings choose what happens when a secret is found: **warn** (the default: the push goes through, and the pusher and the repository’s owner are told), **block** (the push is refused, naming the file and line), or **off**. Findings are listed under Secrets found, with the value never shown, and are settled as replaced or as not a secret.

When a found key is a live Dbrij key, the finding says so, so it can be revoked first.

## Activity export and stream

Every project keeps an activity trail: who read, changed, approved or opened what, and from where. Values are never in it. Admins can take it out of Dbrij in two ways.

### Export

From the project’s Activity, download a date range as CSV or JSON, up to 50,000 events at a time. On every plan, within the history your plan keeps.

### Stream

On Business, an admin can send the trail to an https address of your own, such as a log collector or your security tools. New events are delivered in order, in batches, within about a minute.

What a delivery holds

```
{ "id": "evt_…", "type": "secrets.activity", "createdAt": "…", "data": { "project": { "id": "…", "slug": "my-app", "name": "My app" }, "events": [ { "id": "…", "action": "secret_updated", "detail": "…", "environment": { "id": "…", "name": "Production" }, "keys": ["DATABASE_URL"], "actor": { "kind": "person", "id": "…", "name": "Ada" }, "ip": "203.0.113.5", "userAgent": "…", "createdAt": "…" } ] } }
```

Each delivery is signed with the stream’s secret, shown once when you save it. Check `X-Dbrij-Signature-V2` (a time and a signature of the time and the body) before trusting a delivery.

Checking a delivery

```
import { createHmac, timingSafeEqual } from 'node:crypto';

// header: the X-Dbrij-Signature-V2 value, body: the raw request body
function verified(header, body, secret) {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;
  const want = createHmac('sha256', secret).update(`${parts.t}.${body}`).digest('hex');
  return timingSafeEqual(Buffer.from(want), Buffer.from(parts.sha256));
}
```

- Answer with any 2xx. Anything else, or no answer in ten seconds, is tried again after 1, 5 and 30 minutes, then every two hours. Nothing is dropped while it fails.
- After about half an hour of failing, the project’s admins are told.
- Redirects are not followed, and addresses inside private networks are refused.

## Decoy keys

A decoy is a key that looks real and opens nothing. Plant one in an environment, under a name an intruder would reach for. Nobody with a reason uses it, so the moment somebody does, the project’s admins are told, with the address it came from.

Decoys are real Dbrij key shapes (an API key or a Storage key), so they are recognised wherever they are tried against Dbrij. Every use is refused, exactly like a wrong key. Add them in a project’s Decoys, up to 20.

A decoy tells you a key was **used**, not that it was read. Reading an environment is in the activity trail already.

## Machine API

The SDKs and the CLI are the easy way in. This is what they call, for a language without an SDK. Every answer carries encrypted values and wrapped keys; the machine opens them with the key part of its token, which never reaches Dbrij. The opening is the same as the Node SDK’s, which is open source and has no dependencies to read through.

Answers come inside the usual envelope: `{ "success": true, "data": … }`.

### POST /secrets/machine/login

Exchange the first two parts of a machine token for an access token that ends on its own.

Request

```
curl -X POST https://api.dbrij.com/api/v1/secrets/machine/login \
  -H "Content-Type: application/json" \
  -d '{ "token": "dbs_<id>_<auth>" }'
```

Response

```
{ "accessToken": "…", "expiresAt": "2026-10-30T13:00:00.000Z", "machine": { "id": "…", "name": "api server", "projectId": "…" }, "credentialId": "…" }
```

### POST /secrets/machine/login/github

Log in from GitHub Actions with its identity token: `{ "credential": "<id from the dbsk_ key>", "token": "<identity token>" }`. The answer is the same as above.

### POST /secrets/machine/login/federated

Log in from AWS, Google Cloud, Azure or Kubernetes: `{ "credential": "<id>", "token": "<platform token>" }`, or for AWS `{ "credential": "<id>", "aws": { "url", "method": "POST", "headers", "body" } }`, a signed GetCallerIdentity request. `GET /secrets/machine/login/hint?credential=<id>` says which kind a credential is and which audience to ask for.

### POST /secrets/machine/leases

A database lease for the machine: `{ "source": "postgres-readonly", "ttlMinutes": 60 }`. The answer holds the user, its password and a ready address. `POST /secrets/machine/leases/:id/renew` and `/revoke` extend or end it.

### GET /secrets/machine/self

The machine, its project, and the environments it may read, with its role in each.

### GET /secrets/machine/bundle

Everything one read needs, in one round trip. Send the last ETag back as If-None-Match for a 304 when nothing moved.

QUERY PARAMETERS

|  |  |  |
| --- | --- | --- |
| env | string | The environment short name. Optional when the machine reads only one. |
| path | string | A folder like /api. Repeat it to read several. |
| recursive | "true" | Include the folders inside. |

Request

```
curl "https://api.dbrij.com/api/v1/secrets/machine/bundle?env=production&path=/" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H 'If-None-Match: "…"'
```

Response

```
{ "project": { "slug": "my-app", … }, "environment": { "slug": "production", "version": 42, "keyVersion": 3, "protected": true }, "access": "reader", "holder": { "kind": "machine", "encryptedPrivateKey": "…", "keySalt": "…", "algo": "…" }, "wraps": [ … ], "folders": [ … ], "items": [ { "key": "DATABASE_URL", "folderPath": "/", "version": 7, "keyVersion": 3, "ciphertext": "…", "iv": "…" } ], "etag": "…" }
```

### POST /secrets/machine/changes

Save sealed values, for a machine with write access. Send `If-Match: "v<version>"` with the environment version the change started from; the save is refused if anything moved since. Each change carries an `idempotencyKey`, so a retry never applies twice.

### GET /secrets/machine/watch

A stream (server sent events) of the environment version. Fetch the bundle when it moves. A machine revoked while it listens is cut off within a heartbeat.

## Plans, errors and limits

People are billed; machines, pipelines and servers are free on every plan. A year is billed as ten months. Every safety feature (roles, temporary access, approvals, history, the activity trail and its export, rotation, leases, decoys, share links) is on every plan. Prices are before VAT. On Team and Business, key operations and recorded server hours grow with each person billed. Past the key operations you are told and nothing is refused; past the recorded hours new sessions wait, and breaking glass always works. The company owner is not billed for a project they joined only to look after it.

| Free | Team | Business |
| --- | --- | --- |
| Free | ₦3,000 a person a month | ₦6,000 a person a month |
| Up to 3 people | Any number of people | Any number of people |
| 3 projects, 3 environments in each | 25 projects, 10 environments in each | 500 projects, 20 environments in each |
| 2 syncs | 20 syncs | 200 syncs |
| 30 days of history | 1 year of history | 3 years of history |
| Activity kept 90 days | Activity kept 1 year | Activity kept 3 years |
| Up to 1 approval | Up to 3 approvals | Up to 5 approvals |
| Built in roles | Built in roles | Roles of your own |
| Activity export | Activity export | Activity stream |
| 20,000 key operations a month | 500,000 key operations a person a month | 2,000,000 key operations a person a month |
| 2 servers and databases, recordings kept 7 days | 50 servers and databases, recordings kept 90 days | 1,000 servers and databases, recordings kept 1 year |
| 10 recorded server hours a month | 20 recorded server hours a person a month | 60 recorded server hours a person a month |

A limit only refuses something new: a project, a person, an environment, a sync. Reading values, deploys and syncs already running never stop. After moving to a smaller plan, older history is hidden and the newest syncs past the limit pause; nothing is deleted, and they come back on moving up. The activity trail is the one thing with an end: an entry past the plan’s window is hidden at once and deleted 90 days later, so IP addresses are not kept for ever and an upgrade soon after a downgrade still brings it back.

### Errors

|  |  |  |
| --- | --- | --- |
| 401 | login | The token or access token is wrong, revoked or past its end date. Log in again, or make a new token. |
| 402 | plan limit | Code plan_limit_exceeded. The limit field names which one (projects, environments, people, syncs, approvals, custom_roles, history_days), how much is used, and where to move up. |
| 403 | access | The machine may not read or write that environment, or is calling from an address it is not allowed. |
| 409 | conflict | environment_changed or secret_changed: something moved since your copy; reload and save again. change_request_required: the environment is protected and changes need approval. |
| 429 | rate | Too many requests. Wait for Retry-After. |
