title: Console commands description: What the commands of the bundle need explained beyond their help: the rotation of a key, the data keys, and the doctor.
Console commands¶
The bundle adds the commands for the tenant, the people and their journal, groups, clients, keys, data keys,
the metadata of SAML peers, the databases of addresses and the metadata of FIDO. The
reference lists every one of them; bin/console <command> --help gives its
arguments and options. This page explains the ones a help screen does not: the rotation of a key, the data
keys, and the doctor.
What every command shares¶
A command works on the tenant of the configuration. One that needs its row stops when the row is missing or no longer says what the configuration says, and names the field and the command that fixes it:
The tenant "default" of the configuration has no row yet: synchronise it before serving requests.
Run "beffroi:tenant:synchronize" first.
A command that reads is never the command that writes. beffroi:user:show reads and
beffroi:user:profile writes; beffroi:data-keys:list reads and beffroi:data-keys:rewrap writes.
What a command does is an act like any other, written to the journal with the console as where it came
from: beffroi-console/<command> (<user>@<host>), the user being the one behind sudo before the one of the
shell. A step of the schedule is written as beffroi-scheduler/beffroi (<host>).
Rotating a key¶
A key of the tenant is staged, then active, then retiring, then retired. Of the keys it publishes, the JWK Set carries the first three, so a relying party sees a key before anything is signed with it and keeps verifying it once another has taken over. Keys says what each key of a tenant is for.
| Command | What it does |
|---|---|
beffroi:keys:generate |
Draws a key, seals it and makes it the active one of its algorithm at once. The right command for a tenant that has no key of that algorithm, and a rotation with no staging delay on one that has |
beffroi:keys:rotate |
Stages a key: published at once, active once the staging delay has elapsed. On a tenant that has no active key of the algorithm, the key is active at once |
beffroi:keys:activate <kid> |
Makes a staged key active before its delay has elapsed |
beffroi:keys:retire <kid> |
Takes a retiring key out of the JWK Set once its retirement date has passed, or before it with --force; --due retires every key past its date |
beffroi:keys:list |
The keys in service with their use, status and dates, or every key with --all; --check is described below |
--algorithm names the key: a JWS algorithm for a key that signs, a JWE key management algorithm for a key
clients encrypt to, or the name of a key the tenant keeps, MESSAGE-SEAL, VAPID, SAML-SIGNING, or
SAML-ENCRYPTION: followed by the entity ID of an upstream identity provider. Left out, it is the default
signature algorithm, RS256. One run reaches one algorithm, and a tenant holds one active key per algorithm.
Time does the rest¶
The step AdvanceKeyLifecycle of the schedule runs every minute and
does, in this order, what the commands do by hand: it stages a successor for every active key that has worked
for its age, activates every staged key whose delay has elapsed, and retires every retiring key past its date.
A tenant nobody watches does not sign with the same key for ever, and the tick that stages a successor is never
the one that activates it.
| Figure | Default | Set by |
|---|---|---|
| The staging delay, how long a new key is published before it signs | jwks_max_age plus sixty seconds: 360 seconds with the default jwks_max_age of 300 |
beffroi.keys.staging_delay |
| The age, how long a key works before a successor is staged, counted from its activation | 90 days | beffroi.keys.age, in seconds |
| The retirement window, how long a replaced key stays published | The longest lifetime of what is bound to a key of that use: the ID tokens, access tokens, signed authorization responses and logout tokens for a key that signs, the request objects and pushed requests a client may still be encrypting for a key it encrypts to | Read from those lifetimes, never set on its own |
The sixty seconds are the cooldown of Symfony's oidc_login, which fetches the JWK Set again on a kid it does
not know only once its copy is that old: they cover a relying party whose copy came late through a shared
cache. An age that is not
longer than the retirement window of its use is refused at the first tick that reaches it, since every rotation
would otherwise leave one more key in the JWK Set. The SAML signing key has figures of its own, seven days of
staging and a year of age by default, under beffroi.saml.identity_provider.
Rotating by hand¶
--now skips the staging delay. The new key signs at once and the previous one starts retiring: a relying
party whose copy of the JWK Set is younger than sixty seconds refuses the new tokens, or encrypts to the old
key, until its copy is that old. It is for an operator who knows every relying party is cold.
--compromised leaves no window. The new key takes over at once and the compromised one leaves the JWK
Set at the same moment: nothing bound to it is honoured from then on.
beffroi:keys:retire --force takes a retiring key out before its date, after a compromise or when
everything bound to it is known to have expired; the journal says it was retired before its date.
Checking the keys¶
--check opens the active and the staged keys with the key management service, and says of each active key
whether it has worked past its age and whether a successor is staged behind it. A retiring key is not opened
by the check: in service, it is opened only to read what was encrypted to it. It fails in two cases, and says
what to do in both:
- A key the service cannot open. If the service is down, the same run passes once it is back. If it lost
the master key, the envelopes sealed under it are dead: the command prints one
beffroi:keys:rotate --now --algorithm=<name>per algorithm, each rotation sealing a new key under the master key the service holds now, the dead key retiring with its window, its public half still verifying what it signed. Nothing is deleted. - An active key past its age with nothing staged behind it. The step of the schedule stages one every
minute, so this is a worker that does not consume
scheduler_beffroi: the command names the transport, and the rotation that does the same thing by hand.
Data keys¶
The columns that carry a personal datum are sealed under data keys of the tenant, one per family of columns, wrapped by its master key. Persistence says which columns; these commands are how an operator holds the keys.
beffroi:data-keys:list shows the keys by scope, <slug>.<family>, with when each was minted, the master
key that wraps it and the client that opens it. The last key of a scope is the one a write uses; the ones before
it still open what they sealed, and a rotation never erases them. A family that has sealed nothing yet has no
key, a data key being minted at the first write of its family. The command never opens a key.
beffroi:data-keys:rewrap wraps the data keys of the tenant with another client from now on, or with
another master key of the same one. No sealed value is read or rewritten: what changes is how a data key is
protected, and every ciphertext goes on naming the same key. Each key it moves is bound again under the first
secret of beffroi.keys.data_key_secrets, so the command run towards the client that already wraps the keys
is how that secret is replaced (personal data).
bin/console beffroi:data-keys:rewrap --to=second --dry-run
bin/console beffroi:data-keys:rewrap --to=second --from=op
--tonames the client ofkey_management.clientsthat wraps the keys from now on, and the command lists the clients of the deployment when it is missing.--frommoves only the keys that client holds; all of them without it.--dry-runlists what would move and writes nothing.
The master key is the one beffroi.keys.key_id gives the tenant, resolved from the configuration and never
asked for: an operator who had to type it could type the one of another tenant. It moves one tenant, the one
of the configuration, and nothing schedules it.
beffroi:data-keys:destroy erases every data key of the tenant, the key of its blind indexes with them.
Every column they sealed keeps its ciphertext, and nothing in this database opens it again: that is the
erasure of a tenant, and it cannot be undone, no more than beffroi:tenant:login-floor, which forgets the
credentials the floor forbids, secret and row, or beffroi:client:delete, which deletes a client that
registered itself. It asks for the slug of the tenant typed back, and has no --force:
The rows stay; what is gone is the means of reading them. The keys of the tenant, its pairwise salt and the envelopes sealed under its master key are not touched.
The doctor¶
The doctor reads and writes nothing, so it is safe on a provider serving traffic. It looks at every point
before it answers, prints one line per point, explains the ones it does not answer for, and fails when one is
red, so a deployment pipeline can ask the question. A point is a class tagged beffroi.doctor_check, so a
deployment adds one by adding a class; a run with nothing tagged fails rather than answering for nothing.
The bundle tags one point: whether the lane a logout token is sent on is retried on the schedule the token is
written for, the one beffroi.back_channel_logout sets. On a deployment that keeps the lane of notifications
the bundle declares, with the defaults of that section, the run reads:
[ok] Back-channel logout: how long a logout token is retried
A logout token is retried on "async_notifications" for 3 minutes 20 seconds and stops being verifiable after 3 minutes 55 seconds, both read from the schedule this provider publishes (Beffroi\Bundle\Messenger\NotificationRetryPolicy): a client that is restarting is reached, and nothing arrives after the "exp" of the token has passed or after the key that signed it has left the JWK Set.
The two figures are the budget a logout token is written for; Messenger says what that budget bounds. The point is red when the notifications are routed to a lane retried on another schedule, or to no lane at all.