> ## Documentation Index
> Fetch the complete documentation index at: https://docs.basaltic.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Authentication

> How a request proves who it is: signing with an access key.

Every request to the Basaltic API is signed with `BASALTIC-HMAC-SHA256`. There
is one credential type — an access key belonging to a service account — and one
way to present it.

## Getting credentials

Access keys belong to a **service account**, never to a person. Create one, give
it a policy, then create a credential on it — see the
[quickstart](/quickstart#issue-api-credentials). The secret is shown once, at
creation, and never again.

For short-lived credentials, `POST /v1/assume-role` on `iam.basaltic.sh` vends
temporary ones against a role your account owns. They come with a session token
that must be sent as `X-Amz-Security-Token` **and** included in `SignedHeaders`.

## The signing scheme

`BASALTIC-HMAC-SHA256` is an HMAC-SHA256 over a canonical form of your request,
keyed by a value derived from your secret. It is close to AWS SigV4, with the
differences called out below.

### Headers you send

| Header                 | Required     | What it is                                                      |
| ---------------------- | ------------ | --------------------------------------------------------------- |
| `Authorization`        | Yes          | The scheme, credential scope, signed-header list, and signature |
| `X-Date`               | Yes          | Request time, UTC, `YYYYMMDDTHHMMSSZ`                           |
| `X-Nonce`              | Yes          | Random per request — 16 bytes of hex is fine                    |
| `X-Content-Sha256`     | No           | Lowercase-hex SHA-256 of the body                               |
| `X-Amz-Security-Token` | Only for STS | The session token from temporary credentials                    |

### What actually gets signed

`SignedHeaders` is exactly `host;x-date;x-nonce` — plus
`x-amz-security-token` when you are using temporary credentials.

<Warning>
  `X-Content-Sha256` is **not** in `SignedHeaders`. The body is bound to the
  signature through the last line of the canonical request instead. Adding
  `x-content-sha256` to the signed-header list produces a signature the server
  will reject.
</Warning>

### Authorization header

```
Authorization: BASALTIC-HMAC-SHA256 Credential=<access_key_id>/<date>/<region>/basaltic/basaltic_request, SignedHeaders=host;x-date;x-nonce, Signature=<hex>
```

`<date>` is the `YYYYMMDD` day part of `X-Date`. `<region>` is the region code
you are calling, or `global` for the global services. Note the comma **and
space** between the three parameters.

### Canonical request

Six lines, joined with `\n`:

```
<HTTP-METHOD>
<canonical-path>
<canonical-query>
<canonical-headers>
<signed-headers>
<body-hash>
```

* **canonical-path** — the URL-escaped path, e.g. `/v1/instances`.
* **canonical-query** — each key and value URL-encoded, sorted by key then
  value, joined with `&`. Empty string when there is no query.
* **canonical-headers** — `name:value` per signed header, name lowercased, value
  trimmed, sorted by name, each line ending in `\n`.
* **signed-headers** — the same names, lowercased, sorted, joined with `;`.
* **body-hash** — lowercase-hex SHA-256 of the body, or the literal
  `UNSIGNED-PAYLOAD` for an empty body or a streaming upload you do not want to
  buffer just to hash.

### String to sign

```
BASALTIC-HMAC-SHA256
<date>T000000Z
<date>/<region>/basaltic/basaltic_request
<hex SHA-256 of the canonical request>
```

<Warning>
  The second line is the credential date at **midnight** — `20260901T000000Z` —
  not your `X-Date` value. This is the single most common reason a
  hand-rolled signer produces a rejected signature.
</Warning>

### Signing key

Four chained HMACs, starting from your secret with a literal `BASALTIC` prefix:

```
kDate    = HMAC-SHA256("BASALTIC" + <secret_access_key>, <date>)
kRegion  = HMAC-SHA256(kDate, <region>)
kService = HMAC-SHA256(kRegion, "basaltic")
kSigning = HMAC-SHA256(kService, "basaltic_request")

Signature = lowercase_hex(HMAC-SHA256(kSigning, <string-to-sign>))
```

A complete working implementation is in the
[quickstart](/quickstart#make-a-signed-request).

## Validity and replay

* A signature is good for **5 minutes** from its `X-Date`. If you get `401`s on
  otherwise correct requests, check the clock on the calling machine.
* `X-Nonce` is what makes each signature unique. `X-Date` has one-second
  resolution and a streaming body signs as `UNSIGNED-PAYLOAD`, so without a
  nonce two different requests in the same second can sign identically. Mutating
  requests are replay-guarded on it, and omitting it is rejected.
* The nonce is signed, so a replayer cannot strip it.

## Rate limits

There is no global request budget. A limit applies only where an operation
documents a `429`, and each such endpoint counts its own fixed window.
Everything else is bounded by quota rather than by request rate.

The limited endpoints here are the two public, unauthenticated ones —
`GET /v1/regions` and the price catalogue `GET /v1/prices` — which are counted
per client IP because there is no principal to count. Responses — `429` and `2xx` alike — carry
`X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset` so you can
pace yourself instead of discovering the ceiling by hitting it. Read
`X-RateLimit-Limit` rather than hard-coding a number.

On a `429`, honor `Retry-After`. Retrying sooner is refused and extends the
window, because the refused attempt is itself counted.

Some resources meter the work rather than the request — sending a message is
capped per identity per second — and document their own `429` on the
operation.
