Skip to content
🛡️ adminadmin@example.com · Domain admin in Acme Corp

Chapter 7 — Machine identity

No browser this time

Every chapter so far leaned on a device-code sign-in — a human, a browser, an approval click.

A CI pipeline has none of those. This chapter is the non-interactive counterpart, and it runs in the svc-demo Domain, which seeds no human users at all.

bash
CLIENT_ID=svc-deployer
CLIENT_SECRET=svc-deployer-demo-secret
🤖 machinesvc-deployer · a service identity in svc-demo

Authenticate the machine

bash
TOKEN=$(plexctl identity-tokens issue-service \
  --client-id "$CLIENT_ID" \
  --grant-type client_credentials \
  --client-secret "$CLIENT_SECRET" \
  --output json | jq -r '.access_token')

printf '%s' "$TOKEN" > /tmp/service.token
test -s /tmp/service.token && echo "machine token captured"

The client_credentials grant. One request, no redirect, no browser.

The platform knows it is a machine

bash
plexctl whoami --token-file /tmp/service.token
text
PRINCIPAL  SCOPE   SUBJECT                               DOMAIN                                ACR  AMR
service    domain  019ecc69-1a2b-7c3d-8e4f-0a1b2c3d4e5f  019ecc60-2b3c-7d4e-8f50-1a2b3c4d5e6f  -    -

PRINCIPAL = service, not user.

ACR and AMR are empty — those describe how a human authenticated, and this one did not. It presented a secret.

bash
SERVICE_ID=$(plexctl whoami --token-file /tmp/service.token --output json \
  | jq -r '.subject')

What the machine holds

bash
plexctl identity-tokens list --token-file /tmp/service.token
text
ID                                    ENV   CREATED_AT            EXPIRES_AT            LAST_USED_AT  ROTATED_AT
019ecc69-3c4d-7e5f-9061-2b3c4d5e6f70  live  2026-06-15T18:40:11Z  2026-09-13T18:40:11Z  -             -
bash
TOKEN_ID=$(plexctl identity-tokens list --token-file /tmp/service.token \
  --output json | jq -r '.items[0].id')

The wall

bash
plexctl identity-tokens issue \
  --identity-ref "service:$SERVICE_ID" \
  --env-prefix dev \
  --token-file /tmp/service.token
text
plexctl: Forbidden: a caller authenticated via an API token cannot mint another API token; sign in interactively to issue tokens

A token cannot mint another token.

Otherwise one leaked credential spawns an unbounded family of credentials, each of which spawns more, and revocation becomes a guessing game.

Minting is interactive and human-only. A machine's credential lifecycle is rotation.

Rotation has an overlap window

bash
plexctl identity-tokens rotate \
  --id "$TOKEN_ID" \
  --token-file /tmp/service.token
text
# WARNING: this is the only time this plaintext will be displayed
psk_live_4mN8pQ2rX7vK9wL3tY6bC1dF5gH0jS2aZ8eW

id:                 019ecc69-4d5e-7f60-8172-3c4d5e6f7081
env_prefix:         live
expires_at:         2026-09-13T18:42:33Z
rotation_deadline:  2026-06-17T18:42:33Z

Look at rotation_deadline: 48 hours in which both plaintexts are honoured.

A fleet picks up the new secret on its own schedule. No coordinated restart, no outage window. That overlap is what makes unattended rotation safe.

Decommission

bash
plexctl identity-tokens delete \
  --id "$TOKEN_ID" \
  --token-file /tmp/service.token

rm -f /tmp/service.token

The next request presenting that token is rejected.

The service identity itself is untouched — it can authenticate again with issue-service whenever it needs a fresh token, exactly as a person can log in again after logging out.