Skip to content

Sign in from the command line

Every admin caller over the network presents a JWT, and getting one is the caller’s business, pistra verifies tokens and mints none. That is the right split, and on its own it leaves an awkward gap. A person at a terminal has no obvious way to turn “I am in the directory” into a header, and that gap is where static tokens get reinvented.

pistra login closes it.

Terminal window
$ pistra login https://gw.example
Opening your browser to sign in at corp
Signed in to https://gw.example as ada@example.com (via corp).
The token expires at 2026-08-23T21:14:06+03:00.
$ pistra admin -url https://gw.example /admin/v1/config

Set PISTRA_ADMIN_URL and both commands drop the URL.

Your credentials go to your identity provider and never to pistra. The gateway’s only part in this is one unauthenticated request asking where its provider is. Nothing here weakens the rule that there is no admin secret.

A login block on the issuer, and an OAuth client registered at the provider:

issuers:
- name: corp
url: https://login.example.com
admin:
listen: 0.0.0.0:8485
policy_file: /etc/pistra/admin.cedar
issuers:
- issuer: corp
audience: pistra-cli
roles:
claim: groups
map:
platform-admins: [admin]
login:
client_id: pistra-cli
scopes: [profile, groups]

Register pistra-cli as a public client: no secret, PKCE required, and a loopback redirect. RFC 8252 says the authorization server must accept any port on 127.0.0.1, and providers that let you pin one should be left unpinned. Another program on the machine can be sitting on a fixed port first.

A Keycloak realm with the public client, the group-membership mapper that becomes the groups claim and the device grant switched on is committed as ui/dev/idp/realm.json. Copy its client and mapper rather than reconstructing them, but add the loopback redirect yourself. The committed list carries the console’s /ui/callback only, because the browser tests are the only thing that signs in at that realm.

Registering the client is one step of connecting a provider, and the rest, the issuer entry, the role mapping, and the second redirect the console needs, is Connect your identity provider.

There is no client secret anywhere in this, because a program on somebody’s laptop cannot keep one. PKCE covers that. It binds the authorization code to the process that asked for it, so a code intercepted on the way back cannot be redeemed by anybody else.

audience must be what your provider puts in the token’s aud. For an ID token, the default, that is the client id, so the two are normally the same string. Setting them differently without a reason produces a token the gateway then refuses.

The block is optional and per-issuer. An issuer without one is not advertised. A workload-identity issuer, say, is for service accounts that already hold a token and have no browser to open.

pistra login picks whichever flow suits the machine.

A browser is available, authorization code with PKCE, redirecting to a loopback listener. The browser opens, you sign in, the tab says you can close it.

There is no browser, the device grant. This is the case that matters. Over SSH, the alternative to a working sign-in is somebody pasting a long-lived token into a shell.

Terminal window
$ pistra login -device https://gw.example
To sign in at corp, visit:
https://login.example.com/activate
and enter the code:
WDJB-MJHT
Waiting...

It picks the device flow on its own over SSH or with no display. -device forces it, worth using when you would rather authorize on your phone than in the browser this shell can reach. It needs the provider to publish a device_authorization_endpoint. Not all do.

The token is cached under your config directory, 0600 in a 0700 directory, keyed by gateway. Nothing else on the machine can read it and nothing is shared between gateways.

Terminal window
$ pistra logout -list
https://gw.example ada@example.com via corp valid until 2026-08-23T21:14:06+03:00

While the provider issues refresh tokens, pistra admin renews the session as it lapses and you never notice. Where it does not, the message says so at sign-in rather than surprising you an hour later:

The token expires at 2026-08-23T21:14:06+03:00, and there is no refresh
token, so signing in again is how it renews.

pistra logout forgets this machine’s copy. It does not revoke anything. The token stays valid at the provider until it expires, and an incident that needs it actually dead needs the provider. pistra can meanwhile take the caller’s access away at its own end, see Suspend a key, provider or rule.

A gateway can trust more than one. pistra login picks the only one on offer and asks when there is a choice, because signing somebody in somewhere they did not ask to be is not a default worth having:

Terminal window
$ pistra login https://gw.example
pistra: this gateway offers corp, partners; pick one with -issuer

Run this command:

Terminal window
$ curl -s https://gw.example/admin/v1/login
{"issuers":[{"name":"corp","issuer":"https://login.example.com",
"client_id":"pistra-cli","scopes":["profile","groups"],"token":"id"}]}

That endpoint is the one thing the admin API serves without a token, and it has to be. A client that must already be signed in to find out how to sign in is no use. Nothing in it is confidential, a public client’s id is public by design, and the issuer URL is on the front of every token it mints. It exists so nobody is handed an issuer URL, a client id and a scope list out of band. That is how a shared secret gets invented by accident.

A gateway with no login block anywhere answers an empty list.

Some providers want an extra authorization parameter before they will mint a token for the right audience, Auth0 wants audience, an RFC 8707 server wants resource. Those go through verbatim:

login:
client_id: pistra-cli
extra:
audience: https://pistra.example/api
token: access

token: access presents the access token instead of the ID token, for a provider whose JWT access tokens name the API rather than the client. If neither applies to yours, leave both out.

It is not break-glass. This flow depends on the identity provider being reachable, so it stops working on the day the provider is the problem. That is the local socket, and the two are complementary. A comfortable everyday sign-in keeps the break-glass door rare.