How it works
The path a credential takes from the encrypted store to a process on your laptop, and everything that is recorded along the way.
The pieces
Three things, and one of them is optional.
- The CLI
A single Go binary on your machine. It holds an API token in your OS keyring and nothing else - no cached secrets, no key material, no credential files.
- The API
A Laravel application holding the encrypted store, the permission model, and the audit log. Every credential request goes through it.
- The console
A React interface on the same application, used to register resources, approve access, and read the audit log. You can run a whole team without opening it after setup.
your laptop the platform
─────────── ────────────
shortspan ssh prod-web-1
|
| POST /api/v1/ssh-targets/{id}/credential
| Authorization: Bearer <token>
|-----------------------------> auth is the token valid?
| active is the account suspended?
| role may this user reach this project?
| plan is this feature in their tier?
| grant is a temporary grant required?
| |
| decrypt unwrap MEK, unwrap DEK,
| | decrypt the credential
| audit write the access record
| <---------------------------------+
| proxy token (single use, short lived)
|
| POST /api/v1/ssh/exchange-credential
|-----------------------------> redeem the proxy token
| <----------------------------- the credential itself
|
held in memory, zeroed after the
SSH handshake. never written to disk.What every request passes through
A credential request is checked several times before anything is decrypted. Each check is a separate middleware, so none of them can be skipped by a controller that forgets to call it.
| Gate | Question | Failure |
|---|---|---|
| Did this arrive over TLS? |
|
| Is this CLI version withdrawn? |
|
| Is the bearer token valid, unexpired, and scoped for this? |
|
| Is the account suspended or under review? |
|
| Does this account have a workspace yet? | Redirect or |
| Does this user hold the required role on this project? |
|
| Does the workspace's tier include this? |
|
| Has this token asked for too many secrets too fast? |
|
How the store is encrypted
Envelope encryption, three layers deep. Each layer exists so that compromising one thing does not compromise everything.
- 1
Every secret has its own data key
A random 32-byte DEK encrypts the value with libsodium's secretbox (XSalsa20-Poly1305). The DEK is stored alongside the ciphertext, itself encrypted.
- 2
Every workspace has its own master key
The MEK wraps that workspace's data keys. Two workspaces on the same deployment share no key material, so a flaw that exposes one workspace's MEK exposes nothing about any other.
- 3
One root key wraps every master key
The root key lives in
SHORTSPAN_MASTER_KEY, deliberately separate fromAPP_KEY. Rotating the root key rewrites one row per workspace, not one row per secret.
What is recorded
Every action that touches a credential writes an audit entry before the response is returned. The entry names the actor, the resource, the action, the source address, and the CLI version, and it is written whether the action succeeded or was refused - a denied request is often the more interesting record.
What is deliberately absent: the secret value, the SSH key, the database password, the proxy token, and the contents of anything injected into a process. The audit log records that a credential was released, never which credential it was.
CLI commands additionally report their own start and completion, so a session that was opened and a session that actually connected are distinguishable. See Audit log.
Why nothing lands on disk
The CLI writes exactly two things: a YAML config file recording which organization and project you are working in, and - through your operating system keyring, not a file - your API token.
Credentials are held in process memory and zeroed after use. shortspan aws spawns a subshell with credentials in its environment rather than writing ~/.aws/credentials; when the subshell exits they are gone. shortspan run passes a secret group into a child process's environment and never renders it to the terminal. shortspan ssh builds the SSH auth method from an in-memory string.
Something here wrong or missing? Tell us.