Authentication (/auth/)¶
Create an API access token¶
Access to the REST API is granted via an access token. These tokens expire, so you may also have to request new tokens for long lived applications from time to time. You will receive a HTTP 401 status code if an access token has expired.
Note
For further details of the authentication scheme, see the developer guide.
REST API calls
- POST /auth: Create an access token.
Python API client: creating an access token
The Python API client handles creating access tokens and refreshing them for you, so not specific action is required for this API call. The following code implies creation of an access token:
curl: creating an access token
$ curl -X POST https://shakenfist/api/auth -d '{"namespace": "system", "key": "oisoSe7T"}'
{
"access_token": "eyJhbG...IkpXVCJ9.eyJmc...wwQ",
"token_type": "Bearer",
"expires_in": 900
}
This token is then used by passing it as a HTTP Authorization header with "Bearer " prepended:
$ curl -X GET https://shakenfist/api/auth/namespaces \
-H 'Authorization: Bearer eyJhbG...IkpXVCJ9.eyJmc...wwQ' \
-H 'Content-Type: application/json'
[
{
"name": "adhoc",
"state": "created",
"trust": {"full": ["system"]}
}, {
"name": "ci",
"state": "created",
"trust": {"full": ["system"]}
}, {
"name": "system",
"state": "created",
"trust": {"full": ["system"]}
}
]
Namespaces¶
Resources in a Shaken Fist cluster are divided up into logical groupings called
namespaces. All namespaces have equal permissions, except for the system
namespace, which is used for administrative tasks.
Note
For a detailed reference on the state machine for namespaces, see the developer documentation on object states.
REST API calls
- GET /auth/namespaces: List all namespaces visible to your currently authenticated namespace.
- POST /auth/namespaces: Create a namespace, if you have permissions to do so.
- DELETE /auth/namespaces/{namespace}: Delete a namespace.
- GET /auth/namespaces/{namespace}: Get details of a single namespace.
Python API client: list namespaces
This example lists all namespaces visible to the caller:
import json
from shakenfist_client import apiclient
sf_client = apiclient.Client()
ns = sf_client.get_namespaces()
print(json.dumps(ns, indent=4, sort_keys=True))
Which returns something like:
Python API client: create a namespace
This example creates a new namespace, which is only possible if you are
currently authenticated as the system namespace:
import json
from shakenfist_client import apiclient
sf_client = apiclient.Client()
ns = sf_client.create_namespace('demo')
print(json.dumps(ns, indent=4, sort_keys=True))
Which returns something like:
Python API client: delete a namespace
This example deletes a namespace, which is only possible if you are
currently authenticated as the system namespace:
import json
from shakenfist_client import apiclient
sf_client = apiclient.Client()
ns = sf_client.delete_namespace('demo')
print(json.dumps(ns, indent=4, sort_keys=True))
The call does not return anything.
Python API client: get details of a single namespace
import json
from shakenfist_client import apiclient
sf_client = apiclient.Client()
ns = sf_client.get_namespace('demo')
print(json.dumps(ns, indent=4, sort_keys=True))
Which returns something like:
Namespace keys¶
Callers authenticate to a namespace by providing a key to a call to /auth/ as
discussed above. The calls discussed in this section relate to the management of
the keys used to authenticate to a namespace.
REST API calls
- GET /auth/namespaces/{namespace}/keys: List all authentication keys for a given namespace.
- POST /auth/namespaces/{namespace}/keys: Create a new key for a namespace.
- DELETE /auth/namespaces/{namespace}/keys/{key_name}: Delete a specific key for a namespace.
- PUT /auth/namespaces/{namespace}/keys/{key_name}: Update a key for a namespace.
Python API client: list all keys for a namespace
This example lists all the keys in a namespace:
from shakenfist_client import apiclient
sf_client = apiclient.Client()
keys = sf_client.get_namespace_keynames('ci')
print(keys)
Which returns something like:
Python API client: create a new key for a namespace
This example adds a key to a namespace and then lists all keys:
from shakenfist_client import apiclient
sf_client = apiclient.Client()
sf_client.add_namespace_key('ci', 'newkey', 'thesecretvalue')
# Fetch the list of keys to make sure the new one exists
keys = sf_client.get_namespace_keynames('ci')
print(keys)
Which returns something like:
Key expiry
The create (POST) and update (PUT) calls accept an optional expiry
body parameter, as epoch seconds. A key with no expiry never expires,
which is the default and the behaviour of every key created before this
parameter existed.
The value must be a number in the future. An expiry in the past is rejected with a 400 rather than creating a key which is unusable the instant it exists, since that is far more likely to be a units mistake -- milliseconds instead of seconds, say -- than an intent.
An expired key can neither mint new tokens nor validate a request, from the moment it lapses. Tokens already minted from it remain valid until their own expiry; delete the key if you need those invalidated immediately.
Note that updating a key rotates it, and rotation replaces the whole
mutable attribute set. Updating a key without passing expiry clears
any expiry it previously carried.
The sf-client command line does not expose a flag for this yet, so use
the REST API or the Python client directly for now.
Python API client: remove a specific key from a namespace
This example deletes a key from the namespace and then lists all keys:
from shakenfist_client import apiclient
sf_client = apiclient.Client()
sf_client.delete_namespace_key('ci', 'newkey')
# Fetch the list of keys to make sure the new one exists
keys = sf_client.get_namespace_keynames('ci')
print(keys)
Which returns something like:
Python API client: update the secret portion of an existing namespace key
This example updates the secret portion of an existing namespace key to a new value:
Trusted issuers¶
A trusted issuer is an external identity provider this cluster will
believe: GitHub Actions, an Authentik instance, or anything else that
signs OIDC-style JWTs. Issuers are cluster-wide and administrative --
deciding who may vouch for identities here is not a per-namespace
decision -- so every call below requires the system namespace.
An issuer records four things: a name used to refer to it, the
issuer_url that must match a token's iss claim exactly, the
jwks_uri its signing keys are published at, and the audience its
tokens must be minted for. The jwks_uri always comes from this
record and never from the token, because a token naming its own key
source is a token vouching for itself.
Both the name and the issuer_url must be unique among live issuers,
and a second issuer claiming either is refused with a 409. Uniqueness
on the URL matters because token validation resolves an issuer by it:
two records claiming one URL would make which provider's keys are
trusted depend on listing order, so an administrator repointing an
issuer would believe they had while some requests kept verifying
against the old JWKS. Deleting an issuer frees both its name and its
URL for reuse.
The two are enforced by different mechanisms. name has a unique
index behind it, so the database is the arbiter. issuer_url cannot
easily have one -- a soft-deleted issuer keeps its row precisely so
that its URL stays reusable -- so the create and update endpoints
instead hold a cluster-wide lock across the duplicate check and the
write it guards. The invariant holds either way, including when two
administrators configure the same provider at the same moment.
REST API calls
- GET /auth/issuers: List all trusted issuers.
- POST /auth/issuers: Configure a new trusted issuer.
- GET /auth/issuers/{issuer_name}: Fetch one trusted issuer.
- PUT /auth/issuers/{issuer_name}: Update a trusted issuer.
- DELETE /auth/issuers/{issuer_name}: Remove a trusted issuer.
Configuring GitHub Actions as a trusted issuer
curl -X POST https://sf.example.com/auth/issuers \
-H "Authorization: Bearer ${SF_ADMIN_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"name": "github",
"issuer_url": "https://token.actions.githubusercontent.com",
"jwks_uri": "https://token.actions.githubusercontent.com/.well-known/jwks",
"audience": "https://sf.example.com"
}'
Deleting an issuer
Mapping rules reference their issuer by name. Deleting an issuer does not delete the rules that name it -- those rules simply stop working, because the exchange can no longer resolve the issuer. Recreating an issuer under the same name rebinds every rule that named it, so treat the name as the durable identifier it is.
Mapping rules¶
A mapping rule is a namespace's standing offer: "an identity from
this issuer, carrying these claims, may mint a key here with these
scopes". Rules are owned by the namespace they mint into and are gated
by namespace ownership, exactly as key creation is -- a rule is the
same class of privilege as add-key, granted in advance and gated on
claims.
Rules are deleted with their namespace, so "who can get into this namespace" is answered by listing its rules. That listing is the inbound sibling of the trust list.
A rule carries:
| Field | Meaning |
|---|---|
name |
Unique within the namespace, and named by the exchange request |
issuer |
The trusted issuer whose tokens this rule accepts |
bound_claims |
Claims a token must carry, and the values they must have |
scopes |
The scopes minted keys receive |
key_ttl |
How long a minted key lives, in seconds. At most 86400 |
key_name_prefix |
Prefix for minted key names; the cluster appends a random discriminator |
bound_claims values are exact strings, or lists of exact strings
meaning "any of these". There is no globbing and no pattern matching:
shakenfist/* looks reasonable until somebody registers
shakenfist-evil, and the anchored patterns needed to make that safe
are exactly what reviewers get wrong. A rule must bind at least one
claim and grant at least one scope, both enforced at creation, because
a rule that binds nothing matches every identity the issuer will ever
sign.
Three further limits are enforced at creation and on update, so that a rule which exists is one that was safe to write:
- A rule may not grant a scope its author does not itself hold.
Otherwise a token scoped
rule.writecould write a rule granting*, satisfy that rule's own claims, and exchange it for a wildcard key. Holders of legacy unscoped keys are unrestricted, as they are everywhere else. key_ttlis capped at one day. A federated key stands in for an identity token that is typically valid for minutes, so a key outliving its own justification by more than a working day is not a policy anyone chose on purpose. Create a namespace key directly if a long lived credential is what you want.key_name_prefixmay not be a name Shaken Fist reserves for the credentials it mints for itself (service_key, or anything starting_service_key), the same reservation the key endpoints enforce.
Every field is length bounded. Exceeding a bound is a 400 naming the field, rather than a 500 from the database.
REST API calls
- GET /auth/namespaces/{namespace}/rules: List the mapping rules for a namespace.
- POST /auth/namespaces/{namespace}/rules: Create a mapping rule.
- GET /auth/namespaces/{namespace}/rules/{rule_name}: Fetch one mapping rule.
- PUT /auth/namespaces/{namespace}/rules/{rule_name}: Replace a mapping rule's policy.
- DELETE /auth/namespaces/{namespace}/rules/{rule_name}: Delete a mapping rule.
A rule for one repository and two branches
curl -X POST https://sf.example.com/auth/namespaces/ci/rules \
-H "Authorization: Bearer ${SF_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"name": "ryll",
"issuer": "github",
"bound_claims": {
"repository": "shakenfist/ryll",
"ref": ["refs/heads/develop", "refs/heads/main"]
},
"scopes": ["blob.read", "artifact.*"],
"key_ttl": 3600,
"key_name_prefix": "ryll-ci"
}'
Updating a rule does not touch keys already minted
A minted key stands alone. Its provenance records the claims that were actually satisfied, so the audit trail describes the grant as it was made rather than as the rule reads today. Narrowing a rule's scopes therefore does not retroactively narrow keys it minted earlier -- delete those keys if that is what you need.
Federated exchange¶
POST /auth/federated trades an identity token
from a trusted issuer for a namespace key. It is unauthenticated by
nature: the caller has no Shaken Fist credential yet, which is the
entire point. What stands in place of authentication is the token's
signature, checked against the issuer's published keys, plus a
mapping rule the namespace owner wrote in advance.
The request names three things, and the response returns the minted secret exactly once:
The secret is never returned again and is never written to an event or
a log -- only its bcrypt hash is stored. Use it immediately to call
POST /auth for an access token, exactly as you would any other
namespace key.
REST API calls
- POST /auth/federated: Exchange an identity token for a namespace key.
What a refusal tells you
| Status | Meaning |
|---|---|
| 400 | A required field is missing |
| 401 | The exchange was refused, with a category but no detail |
| 413 | The request body exceeds FEDERATION_MAX_TOKEN_BYTES |
| 429 | Too many attempts from this source address |
| 503 | The database was unavailable, so the exchange could not be checked |
A 401 deliberately says less than the audit log records. Telling an anonymous caller which claim missed would turn the endpoint into an oracle for guessing a rule's contents, one request at a time. The namespace that owns the rule sees the detail in its events, which is where a stream of near-miss claim failures -- what probing looks like -- belongs.
An identity token is single-use per rule
Once a token has been exchanged through a given rule it cannot be exchanged through that rule again. The same token can still be exchanged through a different rule to reach a second namespace, which is a legitimate pattern: a workflow needing two namespaces exchanges its token twice against two rules.
A refusal for any other reason does not consume the token, so fixing a rule and retrying with a still-valid token works.
Metadata¶
All objects exposed by the REST API may have metadata associated with them. This metadata is for storing values that are of interest to the owner of the resources, not Shaken Fist. Shaken Fist does not attempt to interpret these values at all, with the exception of the instance affinity metadata values. The metadata store is in the form of a key value store, and a general introduction is available in the user guide.
REST API calls
- GET /namespaces/{namespace}/metadata: Get metadata for a namespace.
- POST /namespaces/{namespace}/metadata: Create a new metadata key for a namespace.
- DELETE /namespaces/{namespace}/metadata/{key}: Delete a specific metadata key for a namespace.
- PUT /namespaces/{namespace}/metadata/{key}: Update an existing metadata key for a namespace.