Skip to content

Access control

An API key works only with keys in its own container. Inside that container, three settings narrow it further: a key list (which keys it may name), actions (what it may do) and a firewall (which networks it may be used from). Actions do not cover every route: read what actions do not restrict before you rely on them. A dashboard session is held to none of this.

  • Tenant: your account. Your first sign-in to the dashboard creates it. Every key, API key, container and firewall belongs to one tenant, and no credential reaches another tenant's.
  • Container: a group of keys, API keys and firewalls inside a tenant. Every tenant starts with a default container, named Default, which can be renamed but not deleted. Each key, API key and firewall is created in one container and stays there; nothing moves between containers. To an API key, a key in another container does not exist: it gets the same answer as for an id that was never issued.
  • API key: the bearer credential that runs the cryptographic operations (see authentication). It lives in the container it was issued in. Only a dashboard session can issue, restrict or revoke an API key, so an API key cannot change its own settings or issue another.
  • Key list (key_ids): either every key in the API key's container, including keys created later, or a list of 1 to 100 of them. Every key is the default (null in the API). An API key with a list gets 403 key_not_allowed for any key not on it, cannot create keys, and sees only its listed keys in GET /v1/keys and GET /v1/usage. A listed key that is later destroyed stays on the list and allows nothing; the list never widens to the whole container.
  • Actions (allowed_actions): what the API key may do. There are ten: encrypt, decrypt, wrap, unwrap, rewrap and rotate use or rotate a key, and create, rename, delete and restore manage keys. A new API key has none unless you choose some. An action it was not given is refused with 403 action_not_allowed. create works only for an API key with no key list; the other nine apply only to the keys its key list allows. The route each action covers, what actions leave open, and which actions older API keys lack are under what actions do not restrict.
  • Firewall: a named list of 1 to 64 IPv4 or IPv6 addresses and networks, in one container. An API key is held to at most one firewall, from its own container, and one firewall can hold many API keys. An API key held to a firewall gets 403 ip_not_allowed from any other address; one with no firewall can be used from anywhere. See firewalls.
  • Dashboard session: your sign-in to the dashboard. It sees every container and is held to no key list, actions or firewall. It cannot run encrypt, decrypt, wrap, unwrap or rewrap. See dashboard sessions.

For an API key calling POST /v1/keys/{id}/encrypt, decrypt, wrap, unwrap or rewrap, rypt runs these checks in this order and answers with the first that fails. "Recorded" means a row in the audit log of the key named in the path.

Step Check Refused with Recorded
1 The Authorization header holds a well-formed API key 401 unauthorized No
2 Its prefix names an API key that has not been revoked 401 unauthorized No
3 The API key is within its rate limit 429 rate_limited No
4 The secret matches 401 unauthorized, or 503 unavailable when rypt cannot start checking it within 5 seconds No
5 If the API key is held to a firewall, the request comes from one of its networks 403 ip_not_allowed No
6 The {id} in the path is a UUID 404 not_found No
7 If the API key has a key list, {id} is on it 403 key_not_allowed Yes, see below
8 The API key was given this action for this key 403 action_not_allowed, or 503 unavailable when rypt cannot evaluate its permissions within 2 seconds The 403, see below; not the 503
9 The body is JSON, sent as application/json, at most 128 KiB 400 invalid_request, 415 unsupported_media_type, 413 payload_too_large No
10 The key exists in the API key's container and is not destroyed 404 not_found No
11 The key is not pending deletion 409 key_pending_deletion Yes
12 The fields decode and fit the limits 400 invalid_base64, 400 invalid_request, 400 invalid_dek_length, 413 payload_too_large Yes
13 For encrypt, wrap and rewrap on a key of a paid tier, the tenant's billing is in good standing 402 billing_required Yes
14 On a Free (free) key, this UTC month's 10,000 operations are not used up 429 ops_cap_reached Yes
15 The encryption or decryption itself 400 invalid_ciphertext, 500 internal_error Yes

A request that passes every step answers 200, is recorded, and counts as one operation. A path that is not a route answers 404 not_found, and a wrong method 405 method_not_allowed; neither is recorded.

Whether a refusal at step 7 or 8 leaves a row depends on the id in the path; which refusals are recorded gives the rule, and covers refusals on other routes.

What the order means in practice:

  • The firewall is checked only after the secret matches. A caller who has only a prefix gets 401 unauthorized, never 403 ip_not_allowed, so the answer does not reveal where an API key works.
  • The rate limit is charged before the secret is checked. Requests with a real prefix and a wrong secret spend that API key's limit, and once it is spent they get 429 rate_limited instead of 401 unauthorized. Requests the firewall refuses spend it too. rypt does not return an API key's prefix after it is issued.
  • Steps 7 and 8 depend only on the API key and the id, not on whether the key exists. For an API key with no key list, an id that names no key gets 403 action_not_allowed when the API key lacks the action, and 404 not_found only when it has it.
  • Steps 7 and 8 run before the body is read, so a refused request gets 403 even with a malformed body. For an allowed request the body is read before the key is looked up, so an unknown id with a malformed body gets 400, not 404.
  • Step 8 fails closed. If rypt cannot evaluate the API key's permissions, it refuses with 503 unavailable and the message could not check what this api key may do; retry shortly; it never allows the request instead.
  • POST /v1/keys/{id}/rotate runs steps 1 to 8 with the rotate action, waits for a slot among the key changes in flight, then runs steps 10 and 11 and the rotation; rotate a key lists its answers. It takes no body, has no billing or cap check, and does not count as an operation.
  • PATCH /v1/keys/{id} (rename), DELETE /v1/keys/{id} (delete) and POST /v1/keys/{id}/restore (restore) run steps 1 to 8 with the action of the same name. Rename then reads its body (step 9). Each then waits for a slot among the key changes in flight and runs step 10; only rename refuses a key pending deletion (step 11). None has a billing or cap check, and none counts as an operation.
  • POST /v1/keys (create) names no key. It runs steps 1 to 5, reads its body (step 9), then refuses an API key that has a key list (403 key_not_allowed), and then one that lacks the create action (403 action_not_allowed, or 503 unavailable as at step 8). So on this route, unlike the routes above, a malformed body gets 400 even from an API key that may not create keys. After that it checks the name and tier, waits for a slot among the key changes in flight, and checks the container, billing, and that the name is free. Create a key shows both refusals.

A bearer token that does not start with ry_ is treated as a dashboard session. rypt verifies it (401 unauthorized if that fails), refuses a sign-in that belongs to no tenant with 403 no_tenant, and applies a rate limit per user (429 rate_limited). After that, nothing on this page applies to it: no firewall, no key list, no actions, and every container is in reach.

  • encrypt, decrypt, wrap, unwrap and rewrap refuse a dashboard session with 403 api_key_required, before the path is looked at.
  • It can create, list, read, rename, rotate, delete and restore keys, and read audit logs and usage, in every container, with no action check.
  • Only a dashboard session can issue, restrict and revoke API keys, manage containers and firewalls, read the permission record, destroy a key at once with Delete now, and use billing. An API key on those routes gets 403 session_required.
{
  "error": "api_key_required",
  "message": "this endpoint requires an api key, not a console session",
  "request_id": "f8612160-4ea6-4cf8-9a4e-b89ac8407cf4"
}
{
  "error": "session_required",
  "message": "this endpoint requires a console session, not an api key",
  "request_id": "741d9c2f-f8d8-4cec-8db2-138afe0fe1e4"
}

Whoever can sign in to your dashboard can lift every restriction on this page. rypt has no per-user permissions: the role GET /v1/me reports (owner, admin or member) limits nothing.

These use one API key. Its key list is one key, 0f1e2d3c-4b5a-6978-8796-a5b4c3d2e1f0 ($KEY_ID), its only action is decrypt, and it is held to a firewall that lists 203.0.113.0/24. $OTHER_KEY_ID is another key in the same container. Unless an example says otherwise, the requests come from 203.0.113.7, inside the firewall.

curl -sS -X POST "https://api.rypt.dev/v1/keys/$KEY_ID/encrypt" \
  -H "Authorization: Bearer $RYPT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"plaintext": "aGVsbG8gd29ybGQ="}'

Refused at step 8 with 403:

{
  "error": "action_not_allowed",
  "message": "this api key may not encrypt with this key",
  "request_id": "488d51b0-a3f9-4e81-a24c-f23394c6d663"
}

The log of $KEY_ID gets an encrypt row with result error and error code action_not_allowed.

curl -sS -X POST "https://api.rypt.dev/v1/keys/$OTHER_KEY_ID/decrypt" \
  -H "Authorization: Bearer $RYPT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"ciphertext": "...base64..."}'

Refused at step 7 with 403, before the action is checked and before the body is read:

{
  "error": "key_not_allowed",
  "message": "this api key is restricted to other keys",
  "request_id": "bd63e6e9-78ce-4043-84c3-fc1ab9597dd3"
}

The log of $OTHER_KEY_ID gets a decrypt row with error code key_not_allowed.

With a ciphertext that $KEY_ID made from hello world:

curl -sS -X POST "https://api.rypt.dev/v1/keys/$KEY_ID/decrypt" \
  -H "Authorization: Bearer $RYPT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"ciphertext": "...base64..."}'
{"plaintext":"aGVsbG8gd29ybGQ="}

It passes every step: the log of $KEY_ID gets a decrypt row with result ok, and the call counts as one operation.

curl -sS -X POST "https://api.rypt.dev/v1/keys" \
  -H "Authorization: Bearer $RYPT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "orders", "tier": "free"}'

Refused with 403, because the API key has a key list. Giving it the create action would not change that:

{
  "error": "key_not_allowed",
  "message": "this api key is restricted to particular keys and cannot create one",
  "request_id": "bb96c667-98ec-4869-ac01-8327912fd994"
}

Another API key, one with no key list and without the create action, is refused with 403 too:

{
  "error": "action_not_allowed",
  "message": "this api key may not create keys",
  "request_id": "0a7802b9-78c7-4aca-ae42-d8349906e250"
}

Either refusal is recorded as a failed create_key with no key id, so it is in no key's log.

The same decrypt sent from 198.51.100.20 is refused at step 5 with 403, on this route and every other:

{
  "error": "ip_not_allowed",
  "message": "this api key may not be used from 198.51.100.20",
  "request_id": "20f303dd-791b-4cc5-86a2-a54d9ff1c1cb"
}

Nothing is recorded in the audit log.

Actions do not restrict reading. An API key given no actions at all can still, in its own container:

  • list keys and read any key's details (GET /v1/keys, GET /v1/keys/{id});
  • read any key's audit log (GET /v1/keys/{id}/audit), and the usage report (GET /v1/usage).

Only the key list narrows this: an API key with a key list reads only the keys on its list. Its firewall, if it has one, still applies. Treat every live API key as able to read all of this about every key it can reach.

Every other route that accepts an API key checks one of the ten actions:

Action Route
encrypt POST /v1/keys/{id}/encrypt
decrypt POST /v1/keys/{id}/decrypt
wrap POST /v1/keys/{id}/wrap
unwrap POST /v1/keys/{id}/unwrap
rewrap POST /v1/keys/{id}/rewrap
rotate POST /v1/keys/{id}/rotate
create POST /v1/keys
rename PATCH /v1/keys/{id}
delete DELETE /v1/keys/{id}
restore POST /v1/keys/{id}/restore

create names no key, so a key list cannot allow it: an API key with a key list gets 403 key_not_allowed on POST /v1/keys whether or not it has create. The dashboard and the API let you give create to such an API key anyway; it has no effect until the key list is lifted.

Every request reads the API key's revocation, firewall (with its current networks), key list and actions in the same lookup that finds the API key. A change you save applies from that API key's next request; there is no delay to wait out. rypt remembers for 60 seconds that a secret verified, so that it need not hash it again, but it consults that memory only after the lookup, so it never keeps a revoked or newly restricted API key working. A request that is already past authentication when you save finishes under the settings it started with.

In the dashboard, open API keys, press Revoke on the API key's row, and confirm with Revoke API key. From its next request the API key gets 401 unauthorized, the same answer as a token that never existed. Revoking cannot be undone: issue a new API key instead. A revoked API key stays in the list, marked Revoked, so the rows it caused in the audit log still name it. A revocation is not an entry in the permission record.

A firewall holds 1 to 64 entries, counted after duplicates are removed. Each entry is an IPv4 or IPv6 address, which allows that address alone, or a network in CIDR notation. 0.0.0.0/0 allows all of IPv4 and ::/0 all of IPv6. There are no deny entries and no host names. These are refused with 400 invalid_allowed_ips:

  • an empty list, or more than 64 networks;
  • text that is not an address or a CIDR network;
  • a network with bits set past its prefix, such as 203.0.113.5/24: write 203.0.113.0/24 for the network or 203.0.113.5 for the address;
  • an IPv4 address written as IPv6, such as ::ffff:203.0.113.5: write it as IPv4;
  • an entry with spaces around it.

The address checked is the one your request reached rypt's load balancer from. Behind NAT or a proxy, that is the public address the request leaves from, not your host's private address. Headers you send, X-Forwarded-For included, do not change it. The audit log records the same address as src_ip, so the row of a request from a host shows the address to allow for it. api.rypt.dev answers on both IPv4 and IPv6, and a client that connects over IPv6 is checked by its IPv6 address: if your hosts may use either, list networks from both families.

The check runs on every request the API key makes, on every route, once the secret has matched. A refusal is 403 ip_not_allowed with the address in the message, and it is not recorded in the audit log. Firewalls apply to API keys only; nothing restricts the address a dashboard session comes from.

A firewall's container is fixed, and only that container's API keys can be held to it (400 invalid_firewall otherwise). A change to its networks applies to every API key held to it from that key's next request. A firewall that still holds a live API key cannot be deleted (409 firewall_in_use): set those API keys to No firewall with Restrict, or revoke them, first.

Each container keeps a permission record: a hash chain with an entry for every API key issued in it and for every change to an API key's actions or key list. Each entry's SHA-256 hash covers the entry and the hash of the one before it, so rewriting any entry changes every hash after it, and the latest entry, the head, stands for the whole record. Firewall changes and revocations are not entries.

The head is an entry number and a 64-character hexadecimal hash. The dashboard shows it under Permission record on the API keys page, as Latest entry and Head hash, with Copy head hash. The API returns it as permission_chain_head when an API key is issued or restricted, and from GET /v1/containers/{id}/permission-chain, which needs a dashboard session.

rypt serves the head only, never the entries, so you cannot recompute the chain yourself. Keep a copy of the head somewhere rypt cannot reach after each change you make. If the head later shows an entry number you saved with a different hash, or a higher entry number when no one has issued an API key in that container or changed one's actions or key list since, the record has changed. Checking the entries against a head needs the operator.

Every page under the Container menu acts on the container selected there; a new tenant has only Default. To make another, open Containers and press Create container.

  1. To hold API keys to a firewall, open Firewalls, press Create firewall, enter a Name and the Networks, one per line, and press Create firewall.
  2. Open API keys and press Issue API key. Enter a Name. Under What it may do, tick what it needs. The boxes are in two groups, Use keys (encrypt, decrypt, wrap, unwrap, rewrap, rotate) and Manage keys (create, rename, delete, restore); under the second is the note "create works only for an API key allowed every encryption key." None is ticked when the dialog opens. Press Issue API key. Copy the token: it is shown once.
  3. On the new API key's row, press Restrict. Set What it may do, in the same two groups. Under Encryption keys it may use, choose Every key, or Only these keys and tick them; an API key that is to create keys needs Every key. Under Firewall, choose a firewall or No firewall. Press Save.
  4. Under Permission record, press Copy head hash and keep the hash.

The Restrictions column sums up each API key's actions, keys and firewall. The issue dialog sets only the name and the actions; the key list and the firewall are set with Restrict afterwards. The quickstart on the dashboard's Overview page issues an API key with the actions encrypt and decrypt and no others.

These settings change only through routes that need a dashboard session, so they cannot be scripted with $RYPT_TOKEN. The dashboard uses POST /v1/api-keys (name, container_id, allowed_actions), PATCH /v1/api-keys/{id} (firewall_id, key_ids, allowed_actions) and DELETE /v1/api-keys/{id}. In a PATCH, a field left out keeps its value, null lifts the firewall or the key list, and an unknown field is 400 invalid_request. An empty key_ids is 400 invalid_key_ids, and a null allowed_actions is 400 invalid_allowed_actions: send [] for no actions. POST /v1/api-keys ignores fields it does not know, so a firewall_id or key_ids sent there is dropped without an error. Firewalls use POST /v1/firewalls (container_id, name, allowed_ips), PATCH /v1/firewalls/{id} (name, allowed_ips) and DELETE /v1/firewalls/{id}. The API reference has the schemas.