When a Laravel Passport API runs on several servers behind a load balancer, where must the signing keys come from, and what breaks if each server runs passport:keys?
answer
- private key signs, public key verifies
- storage/oauth-private.key by default
- PASSPORT_PRIVATE_KEY wins over files
- Passport::loadKeysFrom()
- 0600 and 0660 permissions checked
basics
~20 sEvery server must share one key pair, injected through PASSPORT_PRIVATE_KEY and PASSPORT_PUBLIC_KEY or a shared path set with Passport::loadKeysFrom(). If each server runs passport:keys, tokens signed on one server fail verification on another, so requests randomly get 401.
solid answer
~40 s`passport:keys` writes a fresh 4096-bit RSA pair to `storage/oauth-private.key` and `storage/oauth-public.key`. The **private** key signs access tokens at `/oauth/token`; the **public** key verifies them on every API request. Behind a load balancer, a token minted by server A must verify on server B, so all servers need the **same** pair. Passport reads `passport.private_key` / `passport.public_key` (from `PASSPORT_PRIVATE_KEY` / `PASSPORT_PUBLIC_KEY` in the published config) first and falls back to the files under `Passport::keyPath()`, which `Passport::loadKeysFrom()` can move. If each server generates its own pair, verification fails whenever the verifying server differs from the signing one: intermittent 401s. Keys stay out of source control; `--force` rotates them and invalidates every outstanding access token. Passport 13 also checks key file permissions (0600/0660) unless `Passport::$validateKeyPermissions` is false.
code
ini · 6 linesPASSPORT_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----
<private key here>
-----END RSA PRIVATE KEY-----"
PASSPORT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----
<public key here>
-----END PUBLIC KEY-----"go deeper
Remember that passport:keys creates an RSA pair in storage/ and that the keys are not committed to Git.
Explain which key signs and which verifies, and the lookup order: PASSPORT_PRIVATE_KEY and PASSPORT_PUBLIC_KEY first, then files under keyPath.
Diagnose intermittent 401s behind a load balancer as mismatched keys, and plan key injection and rotation for the fleet.
Treat signing keys as platform credentials: who may mint tokens, how rotation is scheduled, and how narrowly the private key is distributed.
## What the keys do Passport's OAuth2 server signs every access token with an **RSA private key** and verifies incoming tokens with the matching **public key**: - the **authorization server** (behind `/oauth/token`) is built with the private key; - the **resource server** (behind `auth:api`, `CheckToken` and friends) is built with the public key; - refresh tokens and authorization codes are separately encrypted with a key taken from the application's encrypter, `APP_KEY`, unless `Passport::encryptTokensUsing()` supplies another. ## `passport:keys` `php artisan passport:keys` generates a new RSA pair (`--length=4096` by default) and writes `oauth-public.key` and `oauth-private.key` to `Passport::keyPath()`, which is `storage/` unless `Passport::loadKeysFrom($path)` changed it. On Unix it sets the private key to `0600` and the public key to `0660`. If either file exists it refuses unless you pass `--force`. ## Where Passport looks for keys For each key Passport checks, in order: 1. the config value `passport.private_key` / `passport.public_key`, which the published `config/passport.php` reads from `PASSPORT_PRIVATE_KEY` / `PASSPORT_PUBLIC_KEY` (literal `\n` sequences are turned into newlines); 2. otherwise the file `oauth-private.key` / `oauth-public.key` under `Passport::keyPath()`. Passport 13 validates key file permissions through the underlying crypto key class. An exception about permissions is fixed by `chmod 600` on the private key and `660` on the public key; `Passport::$validateKeyPermissions = false` disables the check, which the upgrade guide calls not recommended. ## The multi-server failure Suppose a logistics API runs on three app servers and the deploy script runs `php artisan passport:keys` on each: 1. an integrator's token request hits server A and receives a token signed with **A's** private key; 2. the next API call is routed to server B, which verifies with **B's** public key; 3. the signature does not match, the resource server rejects the token, and the client gets **401**; 4. calls that happen to land on A succeed, so the failure looks random. The fix is one pair for the whole fleet: | Approach | How | |---|---| | Environment variables | publish the config, set `PASSPORT_PRIVATE_KEY` and `PASSPORT_PUBLIC_KEY` from your secret store | | Shared path | mount the key files and call `Passport::loadKeysFrom('/run/secrets/passport')` in `boot()` | | Generate once | run `passport:keys` in a one-off step and distribute the files | The docs note the keys are not typically kept in source control. ## Rotating keys - `passport:keys --force` (or new env values) replaces the pair. Every access token signed by the old private key now fails verification — a fleet-wide logout of API clients. - Refresh tokens are encrypted with the `APP_KEY`-derived key, not signed with RSA, so clients holding one can usually obtain a new access token after an RSA rotation. - Rotating `APP_KEY` is the opposite case: outstanding refresh tokens and auth codes can no longer be decrypted. - Plan rotations like any credential change: announce, rotate in a window, watch 401 rates. ## Checklist for a Passport deployment 1. Generate the pair once, store it in the secret manager, never in Git. 2. Publish `config/passport.php` if you inject keys through the environment. 3. Make sure every web node and every queue worker that issues personal access tokens sees the same keys. 4. Remove `passport:keys` from per-release deploy scripts; keep it for a first bootstrap only. 5. Keep file permissions at `0600` / `0660` if keys are files, or Passport 13 will refuse them. 6. Document the rotation procedure, including what it does to access and refresh tokens.
- Your deploy runs passport:keys unconditionally on every release. What happens on the second deploy?The command refuses to overwrite existing keys without `--force`, prints an error and returns a failure code, which may break a strict deploy script. If someone adds `--force` to silence it, every release rotates the keys and logs out every API client. Inject the keys instead.
- What does the public key alone let a server do?It can verify access tokens but not mint them. Only code paths that issue tokens, such as `/oauth/token` and `$user->createToken()`, load the private key, which is a reason to keep its distribution as narrow as your topology allows.
saying these in an interview costs you the question
- Each server should generate its own Passport keys for isolation
- Passport keys belong in the Git repository
- Rotating the Passport keys leaves existing access tokens valid
- Passport signs access tokens with APP_KEY
- passport:keys silently overwrites existing keys